mirror of
https://github.com/charlienet/go-mixed.git
synced 2025-07-18 00:22:41 +08:00
Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
37e9cabde8 | |||
886723997e | |||
44304f5b16 | |||
f061b2efeb | |||
dcd803b4f2 |
@ -26,7 +26,8 @@ func NewBloomFilter() *BloomFilter {
|
||||
}
|
||||
|
||||
func (bf *BloomFilter) Add(value string) {
|
||||
for _, f := range bf.funcs {
|
||||
funcs := bf.funcs[:]
|
||||
for _, f := range funcs {
|
||||
bf.set.Set(f.hash(value))
|
||||
}
|
||||
}
|
||||
@ -36,7 +37,9 @@ func (bf *BloomFilter) Contains(value string) bool {
|
||||
return false
|
||||
}
|
||||
ret := true
|
||||
for _, f := range bf.funcs {
|
||||
|
||||
funcs := bf.funcs[:]
|
||||
for _, f := range funcs {
|
||||
ret = ret && bf.set.Test(f.hash(value))
|
||||
}
|
||||
return ret
|
||||
|
@ -16,7 +16,9 @@ func (r BytesResult) Hex() string {
|
||||
func (r BytesResult) UppercaseHex() string {
|
||||
dst := make([]byte, hex.EncodedLen(len(r)))
|
||||
j := 0
|
||||
for _, v := range r {
|
||||
|
||||
re := r[:]
|
||||
for _, v := range re {
|
||||
dst[j] = hextable[v>>4]
|
||||
dst[j+1] = hextable[v&0x0f]
|
||||
j += 2
|
||||
|
@ -42,7 +42,8 @@ func (z *zipPackage) Write(out *os.File) error {
|
||||
zipWriter := zip.NewWriter(out)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, f := range z.files {
|
||||
files := z.files
|
||||
for _, f := range files {
|
||||
fileWriter, err := zipWriter.Create(f.name)
|
||||
if err != nil {
|
||||
return err
|
||||
|
16
hmac/hmac.go
16
hmac/hmac.go
@ -21,13 +21,13 @@ var _ crypto.Signer = &hashComparer{}
|
||||
type HMacFunc func(key, msg []byte) bytesconv.BytesResult
|
||||
|
||||
var hmacFuncs = map[string]HMacFunc{
|
||||
"HmacMD5": Md5,
|
||||
"HmacSHA1": Sha1,
|
||||
"HmacSHA224": Sha224,
|
||||
"HmacSHA256": Sha256,
|
||||
"HmacSHA384": Sha384,
|
||||
"HmacSHA512": Sha512,
|
||||
"HmacSM3": Sm3,
|
||||
"HMACMD5": Md5,
|
||||
"HMACSHA1": Sha1,
|
||||
"HMACSHA224": Sha224,
|
||||
"HMACSHA256": Sha256,
|
||||
"HMACSHA384": Sha384,
|
||||
"HMACSHA512": Sha512,
|
||||
"HMACSM3": Sm3,
|
||||
}
|
||||
|
||||
type hashComparer struct {
|
||||
@ -63,7 +63,7 @@ func ByName(name string) (HMacFunc, error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("Unsupported hash functions")
|
||||
return nil, errors.New("Unsupported hash function:" + name)
|
||||
}
|
||||
|
||||
func Md5(key, msg []byte) bytesconv.BytesResult { return sum(md5.New, key, msg) }
|
||||
|
125
ip_range/ip_range.go
Normal file
125
ip_range/ip_range.go
Normal file
@ -0,0 +1,125 @@
|
||||
package iprange
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/charlienet/go-mixed/bytesconv"
|
||||
)
|
||||
|
||||
type IpRange struct {
|
||||
segments []ipSegment
|
||||
}
|
||||
|
||||
type ipSegment interface {
|
||||
Contains(net.IP) bool
|
||||
}
|
||||
|
||||
type singleIp struct {
|
||||
ip net.IP
|
||||
}
|
||||
|
||||
func (i *singleIp) Contains(ip net.IP) bool {
|
||||
return i.ip.Equal(ip)
|
||||
}
|
||||
|
||||
type cidrSegments struct {
|
||||
cidr *net.IPNet
|
||||
}
|
||||
|
||||
func (i *cidrSegments) Contains(ip net.IP) bool {
|
||||
return i.cidr.Contains(ip)
|
||||
}
|
||||
|
||||
type rangeSegment struct {
|
||||
start rangeIP
|
||||
end rangeIP
|
||||
}
|
||||
|
||||
type rangeIP struct {
|
||||
Hight uint64
|
||||
Lower uint64
|
||||
}
|
||||
|
||||
func (r *rangeSegment) Contains(ip net.IP) bool {
|
||||
ih, _ := bytesconv.BigEndian.BytesToUInt64(ip[:8])
|
||||
i, _ := bytesconv.BigEndian.BytesToUInt64(ip[8:])
|
||||
|
||||
return ih >= r.start.Hight && ih <= r.end.Hight && i >= r.start.Lower && i <= r.end.Lower
|
||||
}
|
||||
|
||||
// IP范围判断,支持以下规则:
|
||||
// 单IP地址,如 192.168.100.2
|
||||
// IP范围, 如 192.168.100.120-192.168.100.150
|
||||
// 掩码模式,如 192.168.2.0/24
|
||||
func NewRange(ip ...string) (*IpRange, error) {
|
||||
seg := make([]ipSegment, 0, len(ip))
|
||||
for _, i := range ip {
|
||||
if s, err := createSegment(i); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
seg = append(seg, s)
|
||||
}
|
||||
}
|
||||
|
||||
return &IpRange{segments: seg}, nil
|
||||
}
|
||||
|
||||
func (r *IpRange) Contains(ip string) bool {
|
||||
nip := net.ParseIP(ip)
|
||||
if nip == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range r.segments {
|
||||
if v.Contains(nip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func createSegment(ip string) (ipSegment, error) {
|
||||
switch {
|
||||
case strings.Contains(ip, "-"):
|
||||
ips := strings.Split(ip, "-")
|
||||
if len(ips) != 2 {
|
||||
return nil, fmt.Errorf("IP范围定义错误:%s", ip)
|
||||
}
|
||||
|
||||
start := net.ParseIP(ips[0])
|
||||
end := net.ParseIP(ips[1])
|
||||
if start == nil {
|
||||
return nil, fmt.Errorf("IP范围起始地址格式错误:%s", ips[0])
|
||||
}
|
||||
|
||||
if end == nil {
|
||||
return nil, fmt.Errorf("IP范围结束地址格式错误:%s", ips[0])
|
||||
}
|
||||
|
||||
sh, _ := bytesconv.BigEndian.BytesToUInt64(start[:8])
|
||||
s, _ := bytesconv.BigEndian.BytesToUInt64(start[8:])
|
||||
eh, _ := bytesconv.BigEndian.BytesToUInt64(end[:8])
|
||||
e, _ := bytesconv.BigEndian.BytesToUInt64(end[8:])
|
||||
|
||||
return &rangeSegment{start: rangeIP{
|
||||
Hight: sh, Lower: s},
|
||||
end: rangeIP{Hight: eh, Lower: e}}, nil
|
||||
|
||||
case strings.Contains(ip, "/"):
|
||||
if _, cidr, err := net.ParseCIDR(ip); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &cidrSegments{cidr: cidr}, nil
|
||||
}
|
||||
default:
|
||||
i := net.ParseIP(ip)
|
||||
if i == nil {
|
||||
return nil, fmt.Errorf("格式错误, 不是有效的IP地址:%s", ip)
|
||||
}
|
||||
|
||||
return &singleIp{ip: i}, nil
|
||||
}
|
||||
}
|
56
ip_range/ip_range_test.go
Normal file
56
ip_range/ip_range_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package iprange
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSingleErrorIP(t *testing.T) {
|
||||
values := []string{
|
||||
"192.168.01",
|
||||
"::",
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
r, err := NewRange(v)
|
||||
|
||||
t.Log(r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleIp(t *testing.T) {
|
||||
r, err := NewRange("192.168.0.1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, r.Contains("192.168.0.1"))
|
||||
assert.False(t, r.Contains("192.168.0.123"))
|
||||
}
|
||||
|
||||
func TestCIDR(t *testing.T) {
|
||||
r, err := NewRange("192.168.2.0/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, r.Contains("192.168.2.12"))
|
||||
assert.True(t, r.Contains("192.168.2.162"))
|
||||
assert.False(t, r.Contains("192.168.3.162"))
|
||||
}
|
||||
|
||||
func TestRange(t *testing.T) {
|
||||
r, err := NewRange("192.168.2.20-192.168.2.30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.True(t, r.Contains("192.168.2.20"))
|
||||
assert.True(t, r.Contains("192.168.2.21"))
|
||||
assert.True(t, r.Contains("192.168.2.30"))
|
||||
|
||||
assert.False(t, r.Contains("192.168.2.10"))
|
||||
assert.False(t, r.Contains("192.168.2.31"))
|
||||
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package logx
|
||||
|
||||
import "io"
|
||||
|
||||
var std = defaultLogger()
|
||||
|
||||
func StandardLogger() Logger {
|
||||
@ -34,4 +36,5 @@ type Logger interface {
|
||||
Println(args ...any)
|
||||
Print(args ...any)
|
||||
Printf(format string, args ...any)
|
||||
Writer() io.Writer
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package logx
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@ -37,3 +39,7 @@ func (l *logrusWrpper) WithFields(fields Fields) Logger {
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *logrusWrpper) Writer() io.Writer {
|
||||
return l.Entry.Writer()
|
||||
}
|
||||
|
@ -88,7 +88,8 @@ func (m *sorted_map[K, V]) Iter() <-chan *Entry[K, V] {
|
||||
}
|
||||
|
||||
func (m *sorted_map[K, V]) ForEach(f func(K, V) bool) {
|
||||
for _, k := range m.keys {
|
||||
keys := m.keys[:]
|
||||
for _, k := range keys {
|
||||
if v, ok := m.Get(k); ok {
|
||||
if f(k, v) {
|
||||
break
|
||||
|
81
panic/panic.go
Normal file
81
panic/panic.go
Normal file
@ -0,0 +1,81 @@
|
||||
package panic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
type Panic struct {
|
||||
R any
|
||||
Stack []byte
|
||||
}
|
||||
|
||||
func (p Panic) String() string {
|
||||
return fmt.Sprintf("%v\n%s", p.R, p.Stack)
|
||||
}
|
||||
|
||||
type PanicGroup struct {
|
||||
panics chan Panic // 致命错误通知
|
||||
dones chan int // 协程完成通知
|
||||
jobs chan int // 并发数量
|
||||
jobN int32 // 工作协程数量
|
||||
}
|
||||
|
||||
func NewPanicGroup(maxConcurrent int) *PanicGroup {
|
||||
return &PanicGroup{
|
||||
panics: make(chan Panic, 8),
|
||||
dones: make(chan int, 8),
|
||||
jobs: make(chan int, maxConcurrent),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *PanicGroup) Go(f func()) *PanicGroup {
|
||||
g.jobN++
|
||||
|
||||
go func() {
|
||||
g.jobs <- 1
|
||||
defer func() {
|
||||
<-g.jobs
|
||||
// go 语言只能在自己的协程中捕获自己的 panic
|
||||
// 如果不处理,整个*进程*都会退出
|
||||
if r := recover(); r != nil {
|
||||
g.panics <- Panic{R: r, Stack: debug.Stack()}
|
||||
// 如果发生 panic 就不再通知 Wait() 已完成
|
||||
// 不然就可能出现 g.jobN 为 0 但 g.panics 非空
|
||||
// 的情况,此时 Wait() 方法需要在正常结束的分支
|
||||
// 中再额外检查是否发生了 panic,非常麻烦
|
||||
return
|
||||
}
|
||||
|
||||
g.dones <- 1
|
||||
}()
|
||||
|
||||
f()
|
||||
}()
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *PanicGroup) Wait(ctx context.Context) error {
|
||||
if g.jobN == 0 {
|
||||
panic("no job to wait")
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-g.dones: // 协程正常结束
|
||||
g.jobN--
|
||||
if g.jobN == 0 {
|
||||
return nil
|
||||
}
|
||||
case p := <-g.panics: // 协程有 panic
|
||||
panic(p)
|
||||
case <-ctx.Done():
|
||||
// 整个 ctx 结束,超时或者调用方主动取消
|
||||
// 子协程应该共用该 ctx,都会收到相同的结束信号
|
||||
// 不需要在这里再去通知各协程结束(实现起来也麻烦)
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
32
panic/panic_test.go
Normal file
32
panic/panic_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
package panic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPanic(t *testing.T) {
|
||||
defer func() {
|
||||
t.Log("捕捉异常")
|
||||
if e := recover(); e != nil {
|
||||
if err, ok := e.(error); ok {
|
||||
t.Log(err.Error())
|
||||
}
|
||||
t.Log("格式化:", e)
|
||||
}
|
||||
}()
|
||||
|
||||
g := NewPanicGroup(10)
|
||||
g.Go(func() {
|
||||
panic("1243")
|
||||
})
|
||||
|
||||
if err := g.Wait(context.Background()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
fmt.Println("这条消息可打印")
|
||||
}
|
@ -1,20 +1,22 @@
|
||||
package snowflake
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 雪花算法默认起始时间 2020-01-01
|
||||
const defaultStarTimestamp = 1579536000
|
||||
// 雪花算法默认起始时间 2022-01-01
|
||||
const defaultStarTimestamp = 1640966400000
|
||||
|
||||
const (
|
||||
MachineIdBits = uint(8) //机器id所占的位数
|
||||
SequenceBits = uint(12) //序列所占的位数
|
||||
//MachineIdMax = int64(-1 ^ (-1 << MachineIdBits)) //支持的最大机器id数量
|
||||
SequenceMask = int64(-1 ^ (-1 << SequenceBits)) //
|
||||
MachineIdShift = SequenceBits //机器id左移位数
|
||||
TimestampShift = SequenceBits + MachineIdBits //时间戳左移位数
|
||||
MachineIdBits = uint(8) //机器id所占的位数
|
||||
SequenceBits = uint(12) //序列所占的位数
|
||||
MachineIdMax = int64(-1 ^ (-1 << MachineIdBits)) //支持的最大机器id数量
|
||||
SequenceMask = int64(-1 ^ (-1 << SequenceBits)) //
|
||||
MachineIdShift = SequenceBits //机器id左移位数
|
||||
TimestampShift = SequenceBits + MachineIdBits //时间戳左移位数
|
||||
)
|
||||
|
||||
type SnowFlake interface {
|
||||
@ -30,28 +32,42 @@ type snowflake struct {
|
||||
}
|
||||
|
||||
func CreateSnowflake(machineId int64) SnowFlake {
|
||||
timeBits := 63 - MachineIdBits - SequenceBits
|
||||
maxTime := time.UnixMilli(defaultStarTimestamp + (int64(-1 ^ (-1 << timeBits))))
|
||||
log.Println("最大可用时间:", maxTime)
|
||||
|
||||
return &snowflake{
|
||||
startTimeStamp: defaultStarTimestamp,
|
||||
machineId: machineId,
|
||||
machineId: machineId & MachineIdMax,
|
||||
}
|
||||
}
|
||||
|
||||
// 组织方式 时间戳-机器码-序列号
|
||||
func (s *snowflake) GetId() int64 {
|
||||
|
||||
// 生成序列号规则
|
||||
// 检查当前生成时间与上次生成时间对比
|
||||
// 如等于上次生成时间,检查是否已经达到序列号的最大值,如已达到等待下一个时间点并且设置序列号为零。
|
||||
// 如不相等则序列号自增
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
now := time.Now().UnixNano() / 1e6
|
||||
if s.timestamp == now {
|
||||
s.sequence = (s.sequence + 1) & SequenceMask
|
||||
if s.sequence == 0 {
|
||||
for now <= s.timestamp {
|
||||
now = time.Now().UnixNano() / 1e6
|
||||
}
|
||||
now := time.Now().UnixMilli()
|
||||
if s.timestamp == now && s.sequence == 0 {
|
||||
fmt.Println(time.Now().Format("2006-01-02 15:04:05.000"), "下一个时间点")
|
||||
for now <= s.timestamp {
|
||||
now = time.Now().UnixMilli()
|
||||
}
|
||||
} else {
|
||||
s.sequence = 0
|
||||
}
|
||||
|
||||
s.timestamp = now
|
||||
s.sequence = (s.sequence + 1) & SequenceMask
|
||||
|
||||
log.Println("时间戳:", now-s.startTimeStamp)
|
||||
|
||||
log.Println("时间差:", time.Now().Sub(time.UnixMilli(defaultStarTimestamp)))
|
||||
|
||||
r := (now-s.startTimeStamp)<<TimestampShift | (s.machineId << MachineIdShift) | (s.sequence)
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -1,6 +1,15 @@
|
||||
package snowflake
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/charlienet/go-mixed/sets"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
s := CreateSnowflake(2)
|
||||
t.Log(s.GetId())
|
||||
}
|
||||
|
||||
func TestGetId(t *testing.T) {
|
||||
s := CreateSnowflake(22)
|
||||
@ -8,3 +17,46 @@ func TestGetId(t *testing.T) {
|
||||
t.Log(s.GetId())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutiGetId(t *testing.T) {
|
||||
s := CreateSnowflake(11)
|
||||
for i := 0; i < 100000; i++ {
|
||||
s.GetId()
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutiConflict(t *testing.T) {
|
||||
set := sets.NewHashSet[int64]()
|
||||
s := CreateSnowflake(11)
|
||||
for i := 0; i < 10000000; i++ {
|
||||
id := s.GetId()
|
||||
if set.Contains(id) {
|
||||
t.Fatal("失败,生成重复数据")
|
||||
}
|
||||
|
||||
set.Add(id)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetId(b *testing.B) {
|
||||
s := CreateSnowflake(11)
|
||||
for i := 0; i < b.N; i++ {
|
||||
s.GetId()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMutiGetId(b *testing.B) {
|
||||
s := CreateSnowflake(11)
|
||||
set := sets.NewHashSet[int64]().WithSync()
|
||||
b.RunParallel(func(p *testing.PB) {
|
||||
for i := 0; p.Next(); i++ {
|
||||
id := s.GetId()
|
||||
|
||||
if set.Contains(id) {
|
||||
b.Fatal("标识重复", id)
|
||||
}
|
||||
|
||||
set.Add(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -40,7 +40,9 @@ func (s *mapSorter[T]) Desc() *mapSorter[T] {
|
||||
|
||||
func (s *mapSorter[T]) Join(sep string, f func(k string, v T) string) string {
|
||||
slice := make([]string, 0, len(s.m))
|
||||
for _, k := range s.keys {
|
||||
|
||||
keys := s.keys[:]
|
||||
for _, k := range keys {
|
||||
slice = append(slice, f(k, s.m[k]))
|
||||
}
|
||||
|
||||
@ -53,7 +55,9 @@ func (s *mapSorter[T]) Keys() []string {
|
||||
|
||||
func (s *mapSorter[T]) Values() []T {
|
||||
ret := make([]T, 0, len(s.m))
|
||||
for _, k := range s.keys {
|
||||
|
||||
keys := s.keys[:]
|
||||
for _, k := range keys {
|
||||
ret = append(ret, s.m[k])
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,8 @@ func (s *Struct) Kind() reflect.Kind {
|
||||
|
||||
func (s *Struct) Names() []string {
|
||||
names := make([]string, len(s.fields))
|
||||
for i, f := range s.fields {
|
||||
fields := s.fields[:]
|
||||
for i, f := range fields {
|
||||
names[i] = f.name
|
||||
}
|
||||
|
||||
@ -45,7 +46,9 @@ func (s *Struct) Names() []string {
|
||||
|
||||
func (s *Struct) Values() []any {
|
||||
values := make([]any, 0, len(s.fields))
|
||||
for _, fi := range s.fields {
|
||||
|
||||
fields := s.fields[:]
|
||||
for _, fi := range fields {
|
||||
v := s.value.FieldByName(fi.name)
|
||||
values = append(values, v.Interface())
|
||||
}
|
||||
@ -54,7 +57,8 @@ func (s *Struct) Values() []any {
|
||||
}
|
||||
|
||||
func (s *Struct) IsZero() bool {
|
||||
for _, fi := range s.fields {
|
||||
fields := s.fields[:]
|
||||
for _, fi := range fields {
|
||||
source := s.value.FieldByName(fi.name)
|
||||
if !source.IsZero() {
|
||||
return false
|
||||
@ -66,7 +70,9 @@ func (s *Struct) IsZero() bool {
|
||||
|
||||
func (s *Struct) ToMap() map[string]any {
|
||||
m := make(map[string]any, len(s.fields))
|
||||
for _, fi := range s.fields {
|
||||
|
||||
fields := s.fields[:]
|
||||
for _, fi := range fields {
|
||||
source := s.value.FieldByName(fi.name)
|
||||
if fi.shouldIgnore(source) {
|
||||
continue
|
||||
@ -109,7 +115,8 @@ func (s *Struct) Copy(dest any) error {
|
||||
}
|
||||
|
||||
func (s *Struct) getByName(name string) (field, bool) {
|
||||
for i := range s.fields {
|
||||
fields := s.fields[:]
|
||||
for i := range fields {
|
||||
f := s.fields[i]
|
||||
if f.name == name {
|
||||
return f, true
|
||||
|
Reference in New Issue
Block a user