mirror of
				https://github.com/simon987/imhashdb.git
				synced 2025-10-31 16:36:53 +00:00 
			
		
		
		
	Change hash types
This commit is contained in:
		
							parent
							
								
									25a19ab557
								
							
						
					
					
						commit
						91f0c9b1f9
					
				
							
								
								
									
										57
									
								
								core.go
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								core.go
									
									
									
									
									
								
							| @ -45,35 +45,62 @@ func Init() { | ||||
| 	DbInit(Pgdb) | ||||
| } | ||||
| 
 | ||||
| func ComputeHash(data []byte) (*fastimagehash.MultiHash, error) { | ||||
| 	h := &fastimagehash.MultiHash{} | ||||
| func ComputeHash(data []byte) (*Hashes, error) { | ||||
| 	h := &Hashes{} | ||||
| 	var code fastimagehash.Code | ||||
| 
 | ||||
| 	aHash, code := fastimagehash.AHashMem(data, 12) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("aHash error: %d", int(code)) | ||||
| 	} | ||||
| 	dHash, code := fastimagehash.DHashMem(data, 12) | ||||
| 	h.DHash8, code = fastimagehash.DHashMem(data, 8) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("dHash error: %d", int(code)) | ||||
| 	} | ||||
| 	mHash, code := fastimagehash.MHashMem(data, 12) | ||||
| 	h.DHash16, code = fastimagehash.DHashMem(data, 16) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("dHash error: %d", int(code)) | ||||
| 	} | ||||
| 	pHash, code := fastimagehash.PHashMem(data, 12, 4) | ||||
| 	h.DHash32, code = fastimagehash.DHashMem(data, 32) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("dHash error: %d", int(code)) | ||||
| 	} | ||||
| 
 | ||||
| 	h.MHash8, code = fastimagehash.MHashMem(data, 8) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("mHash error: %d", int(code)) | ||||
| 	} | ||||
| 	h.MHash16, code = fastimagehash.MHashMem(data, 16) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("mHash error: %d", int(code)) | ||||
| 	} | ||||
| 	h.MHash32, code = fastimagehash.MHashMem(data, 32) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("mHash error: %d", int(code)) | ||||
| 	} | ||||
| 
 | ||||
| 	h.PHash8, code = fastimagehash.PHashMem(data, 8, 4) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("pHash error: %d", int(code)) | ||||
| 	} | ||||
| 	wHash, code := fastimagehash.WHashMem(data, 8, 0, fastimagehash.Haar) | ||||
| 	h.PHash16, code = fastimagehash.PHashMem(data, 16, 4) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("pHash error: %d", int(code)) | ||||
| 	} | ||||
| 	h.PHash32, code = fastimagehash.PHashMem(data, 32, 4) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("pHash error: %d", int(code)) | ||||
| 	} | ||||
| 
 | ||||
| 	h.WHash8, code = fastimagehash.WHashMem(data, 8, 0, fastimagehash.Haar) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("wHash error: %d", int(code)) | ||||
| 	} | ||||
| 	h.WHash16, code = fastimagehash.WHashMem(data, 16, 0, fastimagehash.Haar) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("wHash error: %d", int(code)) | ||||
| 	} | ||||
| 	h.WHash32, code = fastimagehash.WHashMem(data, 32, 0, fastimagehash.Haar) | ||||
| 	if code != fastimagehash.Ok { | ||||
| 		return nil, errors.Errorf("wHash error: %d", int(code)) | ||||
| 	} | ||||
| 
 | ||||
| 	h.AHash = *aHash | ||||
| 	h.DHash = *dHash | ||||
| 	h.MHash = *mHash | ||||
| 	h.PHash = *pHash | ||||
| 	h.WHash = *wHash | ||||
| 	return h, nil | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										176
									
								
								db.go
									
									
									
									
									
								
							
							
						
						
									
										176
									
								
								db.go
									
									
									
									
									
								
							| @ -6,22 +6,18 @@ import ( | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"github.com/jackc/pgx" | ||||
| 	"github.com/jackc/pgx/pgtype" | ||||
| 	"github.com/mailru/easyjson" | ||||
| 	"github.com/simon987/fastimagehash-go" | ||||
| 	"go.uber.org/zap" | ||||
| ) | ||||
| 
 | ||||
| const MaxDistance = 30 | ||||
| const MaxDistance = 100 | ||||
| const MaxLimit = 1000 | ||||
| 
 | ||||
| type Entry struct { | ||||
| 	AHash  *fastimagehash.Hash | ||||
| 	DHash  *fastimagehash.Hash | ||||
| 	MHash  *fastimagehash.Hash | ||||
| 	PHash  *fastimagehash.Hash | ||||
| 	WHash  *fastimagehash.Hash | ||||
| 	H      *Hashes | ||||
| 	Size   int | ||||
| 	Sha1   [sha1.Size]byte | ||||
| 	Md5    [md5.Size]byte | ||||
| @ -52,26 +48,21 @@ func Store(entry *Entry) { | ||||
| 	} | ||||
| 
 | ||||
| 	if !imageExists { | ||||
| 		_, err = Pgdb.Exec("INSERT INTO hash_ahash VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.AHash.Bytes) | ||||
| 		if err != nil { | ||||
| 			Logger.Error("Could not insert ahash", zap.Error(err)) | ||||
| 		} | ||||
| 		_, err = Pgdb.Exec("INSERT INTO hash_dhash VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.DHash.Bytes) | ||||
| 		if err != nil { | ||||
| 			Logger.Error("Could not insert dhash", zap.Error(err)) | ||||
| 		} | ||||
| 		_, err = Pgdb.Exec("INSERT INTO hash_mhash VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.MHash.Bytes) | ||||
| 		if err != nil { | ||||
| 			Logger.Error("Could not insert mhash", zap.Error(err)) | ||||
| 		} | ||||
| 		_, err = Pgdb.Exec("INSERT INTO hash_phash VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.PHash.Bytes) | ||||
| 		if err != nil { | ||||
| 			Logger.Error("Could not insert phash", zap.Error(err)) | ||||
| 		} | ||||
| 		_, err = Pgdb.Exec("INSERT INTO hash_whash VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.WHash.Bytes) | ||||
| 		if err != nil { | ||||
| 			Logger.Error("Could not insert whash", zap.Error(err)) | ||||
| 		} | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_dhash8 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.DHash8.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_dhash16 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.DHash16.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_dhash32 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.DHash32.Bytes) | ||||
| 
 | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_mhash8 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.MHash8.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_mhash16 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.MHash16.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_mhash32 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.MHash32.Bytes) | ||||
| 
 | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_phash8 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.PHash8.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_phash16 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.PHash16.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_phash32 VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.PHash32.Bytes) | ||||
| 
 | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_whash8haar VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.WHash8.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_whash16haar VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.WHash16.Bytes) | ||||
| 		_, _ = Pgdb.Exec("INSERT INTO hash_whash32haar VALUES ($1, $2) ON CONFLICT DO NOTHING", id, entry.H.WHash32.Bytes) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, meta := range entry.Meta { | ||||
| @ -96,30 +87,36 @@ func Store(entry *Entry) { | ||||
| 
 | ||||
| func isHashValid(hash []byte, hashType HashType) bool { | ||||
| 	switch hashType { | ||||
| 	case AHash12: | ||||
| 		if len(hash) != 18 { | ||||
| 			return false | ||||
| 		} | ||||
| 	case DHash12: | ||||
| 		if len(hash) != 18 { | ||||
| 			return false | ||||
| 		} | ||||
| 	case MHash12: | ||||
| 		if len(hash) != 18 { | ||||
| 			return false | ||||
| 		} | ||||
| 	case PHash12: | ||||
| 		if len(hash) != 18 { | ||||
| 			return false | ||||
| 		} | ||||
| 	case DHash8: | ||||
| 		fallthrough | ||||
| 	case MHash8: | ||||
| 		fallthrough | ||||
| 	case PHash8: | ||||
| 		fallthrough | ||||
| 	case WHash8Haar: | ||||
| 		if len(hash) != 8 { | ||||
| 			return false | ||||
| 		} | ||||
| 		return len(hash) == 8 | ||||
| 
 | ||||
| 	case DHash16: | ||||
| 		fallthrough | ||||
| 	case MHash16: | ||||
| 		fallthrough | ||||
| 	case PHash16: | ||||
| 		fallthrough | ||||
| 	case WHash16Haar: | ||||
| 		return len(hash) == 32 | ||||
| 
 | ||||
| 	case DHash32: | ||||
| 		fallthrough | ||||
| 	case MHash32: | ||||
| 		fallthrough | ||||
| 	case PHash32: | ||||
| 		fallthrough | ||||
| 	case WHash32Haar: | ||||
| 		return len(hash) == 128 | ||||
| 
 | ||||
| 	default: | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
| } | ||||
| 
 | ||||
| func FindImagesByHash(ctx context.Context, hash []byte, hashType HashType, distance, limit, offset uint) ([]byte, error) { | ||||
| @ -143,23 +140,11 @@ func FindImagesByHash(ctx context.Context, hash []byte, hashType HashType, dista | ||||
| 	defer tx.Commit() | ||||
| 
 | ||||
| 	var sql string | ||||
| 	switch hashType { | ||||
| 	case AHash12: | ||||
| 		sql = `SELECT image.* FROM image INNER JOIN hash_ahash h on image.id = h.image_id  | ||||
| 				WHERE hash_is_within_distance18(h.hash, $1, $2) ORDER BY image.id LIMIT $3 OFFSET $4` | ||||
| 	case DHash12: | ||||
| 		sql = `SELECT image.* FROM image INNER JOIN hash_dhash h on image.id = h.image_id  | ||||
| 				WHERE hash_is_within_distance18(h.hash, $1, $2) ORDER BY image.id LIMIT $3 OFFSET $4` | ||||
| 	case MHash12: | ||||
| 		sql = `SELECT image.* FROM image INNER JOIN hash_mhash h on image.id = h.image_id  | ||||
| 				WHERE hash_is_within_distance18(h.hash, $1, $2) ORDER BY image.id LIMIT $3 OFFSET $4` | ||||
| 	case PHash12: | ||||
| 		sql = `SELECT image.* FROM image INNER JOIN hash_phash h on image.id = h.image_id  | ||||
| 				WHERE hash_is_within_distance18(h.hash, $1, $2) ORDER BY image.id LIMIT $3 OFFSET $4` | ||||
| 	case WHash8Haar: | ||||
| 		sql = `SELECT image.* FROM image INNER JOIN hash_whash h on image.id = h.image_id  | ||||
| 				WHERE hash_is_within_distance8(h.hash, $1, $2) ORDER BY image.id LIMIT $3 OFFSET $4` | ||||
| 	} | ||||
| 	sql = fmt.Sprintf( | ||||
| 		`SELECT image.* FROM image INNER JOIN hash_%s h on image.id = h.image_id  | ||||
| 				WHERE hash_is_within_distance%d(h.hash, $1, $2)  | ||||
| 				ORDER BY image.id LIMIT $3 OFFSET $4`, | ||||
| 		hashType, hashType.HashLength()) | ||||
| 
 | ||||
| 	rows, err := tx.Query(sql, hash, distance, limit, offset) | ||||
| 	if err != nil { | ||||
| @ -169,7 +154,7 @@ func FindImagesByHash(ctx context.Context, hash []byte, hashType HashType, dista | ||||
| 	var images []*Image | ||||
| 	for rows.Next() { | ||||
| 		var im Image | ||||
| 		err := rows.Scan(&im.id, &im.Size, &im.Sha1, &im.Md5, &im.Sha256, &im.Crc32) | ||||
| 		err := rows.Scan(&im.id, &im.Crc32, &im.Size, &im.Sha1, &im.Md5, &im.Sha256) | ||||
| 		if err != nil { | ||||
| 			Logger.Error("Error while fetching db image", zap.String("err", err.Error())) | ||||
| 			return nil, err | ||||
| @ -178,6 +163,10 @@ func FindImagesByHash(ctx context.Context, hash []byte, hashType HashType, dista | ||||
| 		images = append(images, &im) | ||||
| 	} | ||||
| 
 | ||||
| 	if images == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	batch := tx.BeginBatch() | ||||
| 	defer batch.Close() | ||||
| 	for _, im := range images { | ||||
| @ -222,56 +211,33 @@ func DbInit(pool *pgx.ConnPool) { | ||||
| 
 | ||||
| 	sql := ` | ||||
| CREATE TABLE IF NOT EXISTS image ( | ||||
| 	id BIGSERIAL PRIMARY KEY, | ||||
| 	size INT, | ||||
| 	sha1 bytea, | ||||
| 	md5 bytea, | ||||
| 	sha256 bytea, | ||||
| 	crc32 bigint | ||||
| 	id BIGSERIAL PRIMARY KEY NOT NULL, | ||||
| 	crc32 bigint NOT NULL, | ||||
| 	size INT NOT NULL, | ||||
| 	sha1 bytea NOT NULL, | ||||
| 	md5 bytea NOT NULL, | ||||
| 	sha256 bytea NOT NULL | ||||
| ); | ||||
| CREATE UNIQUE INDEX IF NOT EXISTS idx_image_sha1 ON image(sha1); | ||||
| CREATE INDEX IF NOT EXISTS idx_image_md5 ON image(md5); | ||||
| CREATE INDEX IF NOT EXISTS idx_image_sha256 ON image(sha256); | ||||
| CREATE INDEX IF NOT EXISTS idx_image_crc32 ON image(crc32); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS image_meta ( | ||||
| 	id TEXT UNIQUE, | ||||
| 	retrieved_at bigint, | ||||
| 	meta bytea | ||||
| 	id TEXT UNIQUE NOT NULL, | ||||
| 	retrieved_at bigint NOT NULL, | ||||
| 	meta bytea NOT NULL | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS image_has_meta ( | ||||
| 	image_id bigint REFERENCES image(id), | ||||
| 	url TEXT, | ||||
| 	image_meta_id text REFERENCES image_meta(id), | ||||
| 	image_id bigint REFERENCES image(id) NOT NULL, | ||||
| 	url TEXT NOT NULL, | ||||
| 	image_meta_id text REFERENCES image_meta(id) NOT NULL, | ||||
| 	UNIQUE(image_id, image_meta_id) | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS hash_phash ( | ||||
| 	image_id BIGINT REFERENCES image(id) UNIQUE, | ||||
|     hash bytea | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS hash_ahash ( | ||||
| 	image_id BIGINT REFERENCES image(id) UNIQUE, | ||||
|     hash bytea | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS hash_dhash ( | ||||
| 	image_id BIGINT REFERENCES image(id) UNIQUE, | ||||
|     hash bytea | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS hash_mhash ( | ||||
| 	image_id BIGINT REFERENCES image(id) UNIQUE, | ||||
|     hash bytea | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE IF NOT EXISTS hash_whash ( | ||||
| 	image_id BIGINT REFERENCES image(id) UNIQUE, | ||||
|     hash bytea | ||||
| ); | ||||
| ` | ||||
| 	for _, hashType := range HashTypes { | ||||
| 		sql += fmt.Sprintf(`CREATE TABLE IF NOT EXISTS hash_%s ( | ||||
| 							image_id BIGINT REFERENCES image(id) UNIQUE NOT NULL, | ||||
| 							hash bytea NOT NULL);`, hashType) | ||||
| 	} | ||||
| 
 | ||||
| 	_, err := pool.Exec(sql) | ||||
| 	if err != nil { | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -22,7 +22,7 @@ require ( | ||||
| 	github.com/onsi/gomega v1.9.0 // indirect | ||||
| 	github.com/pkg/errors v0.9.1 | ||||
| 	github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc // indirect | ||||
| 	github.com/simon987/fastimagehash-go v0.0.0-20200411154912-569fe641b1a7 | ||||
| 	github.com/simon987/fastimagehash-go v0.0.0-20200412154506-b0e9d9b3a73e | ||||
| 	github.com/stretchr/testify v1.5.1 // indirect | ||||
| 	github.com/valyala/fasthttp v1.9.0 | ||||
| 	go.uber.org/zap v1.14.1 | ||||
|  | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @ -86,6 +86,10 @@ github.com/simon987/fastimagehash-go v0.0.0-20200411005122-1886a7c50720 h1:0VrGo | ||||
| github.com/simon987/fastimagehash-go v0.0.0-20200411005122-1886a7c50720/go.mod h1:MbqNG+6OaprdElEIes1aYF7qmLlaTop4j5X6pgNiaaw= | ||||
| github.com/simon987/fastimagehash-go v0.0.0-20200411154912-569fe641b1a7 h1:4XD2rCg4hJRcCZErDLp8lfMsHw5Zinr5e2t2C18GdzU= | ||||
| github.com/simon987/fastimagehash-go v0.0.0-20200411154912-569fe641b1a7/go.mod h1:fmgaZptm6M5Kn3Ctu/R5p2fncGYPpGi/raZCZUrkRsY= | ||||
| github.com/simon987/fastimagehash-go v0.0.0-20200412153922-bcfdf954b464 h1:5p3TX77zQoyrAJqrczYLmAo4F8U+dsyHylzZ3MH4sps= | ||||
| github.com/simon987/fastimagehash-go v0.0.0-20200412153922-bcfdf954b464/go.mod h1:fmgaZptm6M5Kn3Ctu/R5p2fncGYPpGi/raZCZUrkRsY= | ||||
| github.com/simon987/fastimagehash-go v0.0.0-20200412154506-b0e9d9b3a73e h1:8+cH+kriBBb9OqtKh/wNsr+PvV8e73yNjEly5wAjFQk= | ||||
| github.com/simon987/fastimagehash-go v0.0.0-20200412154506-b0e9d9b3a73e/go.mod h1:fmgaZptm6M5Kn3Ctu/R5p2fncGYPpGi/raZCZUrkRsY= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
|  | ||||
| @ -84,11 +84,7 @@ func computeAndStore(rawTask []string) { | ||||
| 			} | ||||
| 
 | ||||
| 			Store(&Entry{ | ||||
| 				AHash:  &h.AHash, | ||||
| 				DHash:  &h.DHash, | ||||
| 				MHash:  &h.MHash, | ||||
| 				PHash:  &h.PHash, | ||||
| 				WHash:  &h.WHash, | ||||
| 				H:      h, | ||||
| 				Size:   len(data), | ||||
| 				Sha256: sha256.Sum256(data), | ||||
| 				Sha1:   sha1.Sum(data), | ||||
|  | ||||
							
								
								
									
										85
									
								
								models.go
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								models.go
									
									
									
									
									
								
							| @ -1,25 +1,86 @@ | ||||
| package imhashdb | ||||
| 
 | ||||
| import "github.com/simon987/fastimagehash-go" | ||||
| 
 | ||||
| type HashType string | ||||
| 
 | ||||
| const ( | ||||
| 	AHash12    HashType = "ahash:12" | ||||
| 	DHash12    HashType = "dhash:12" | ||||
| 	MHash12    HashType = "mhash:12" | ||||
| 	PHash12    HashType = "phaash:12:4" | ||||
| 	WHash8Haar HashType = "whash:8:haar" | ||||
| 	DHash8  HashType = "dhash8" | ||||
| 	DHash16 HashType = "dhash16" | ||||
| 	DHash32 HashType = "dhash32" | ||||
| 
 | ||||
| 	MHash8  HashType = "mhash8" | ||||
| 	MHash16 HashType = "mhash16" | ||||
| 	MHash32 HashType = "mhash32" | ||||
| 
 | ||||
| 	PHash8  HashType = "phash8" | ||||
| 	PHash16 HashType = "phash16" | ||||
| 	PHash32 HashType = "phash32" | ||||
| 
 | ||||
| 	WHash8Haar  HashType = "whash8haar" | ||||
| 	WHash16Haar HashType = "whash16haar" | ||||
| 	WHash32Haar HashType = "whash32haar" | ||||
| ) | ||||
| 
 | ||||
| var HashTypes = []HashType{ | ||||
| 	DHash8, DHash16, DHash32, | ||||
| 	MHash8, MHash16, MHash32, | ||||
| 	PHash8, PHash16, PHash32, | ||||
| 	WHash8Haar, WHash16Haar, WHash32Haar, | ||||
| } | ||||
| 
 | ||||
| func (h HashType) HashLength() int { | ||||
| 	switch h { | ||||
| 	case DHash8: | ||||
| 		fallthrough | ||||
| 	case MHash8: | ||||
| 		fallthrough | ||||
| 	case PHash8: | ||||
| 		fallthrough | ||||
| 	case WHash8Haar: | ||||
| 		return 8 | ||||
| 
 | ||||
| 	case DHash16: | ||||
| 		fallthrough | ||||
| 	case MHash16: | ||||
| 		fallthrough | ||||
| 	case PHash16: | ||||
| 		fallthrough | ||||
| 	case WHash16Haar: | ||||
| 		return 32 | ||||
| 
 | ||||
| 	case DHash32: | ||||
| 		fallthrough | ||||
| 	case MHash32: | ||||
| 		fallthrough | ||||
| 	case PHash32: | ||||
| 		fallthrough | ||||
| 	case WHash32Haar: | ||||
| 		return 128 | ||||
| 	default: | ||||
| 		panic("Invalid invalid hash") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type HashReq struct { | ||||
| 	Data []byte `json:"data"` | ||||
| } | ||||
| type Hashes struct { | ||||
| 	DHash8  *fastimagehash.Hash `json:"dhash8"` | ||||
| 	DHash16 *fastimagehash.Hash `json:"dhash16"` | ||||
| 	DHash32 *fastimagehash.Hash `json:"dhash32"` | ||||
| 
 | ||||
| type HashResp struct { | ||||
| 	AHash []byte `json:"ahash:12"` | ||||
| 	DHash []byte `json:"dhash:12"` | ||||
| 	MHash []byte `json:"mhash:12"` | ||||
| 	PHash []byte `json:"phash:12:4"` | ||||
| 	WHash []byte `json:"whash:18:haar"` | ||||
| 	MHash8  *fastimagehash.Hash `json:"mhash8"` | ||||
| 	MHash16 *fastimagehash.Hash `json:"mhash16"` | ||||
| 	MHash32 *fastimagehash.Hash `json:"mhash32"` | ||||
| 
 | ||||
| 	PHash8  *fastimagehash.Hash `json:"phash8"` | ||||
| 	PHash16 *fastimagehash.Hash `json:"phash16"` | ||||
| 	PHash32 *fastimagehash.Hash `json:"phash32"` | ||||
| 
 | ||||
| 	WHash8  *fastimagehash.Hash `json:"whash8haar"` | ||||
| 	WHash16 *fastimagehash.Hash `json:"whash16haar"` | ||||
| 	WHash32 *fastimagehash.Hash `json:"whash32haar"` | ||||
| } | ||||
| 
 | ||||
| type QueryReq struct { | ||||
| @ -35,7 +96,7 @@ type ImageList struct { | ||||
| } | ||||
| 
 | ||||
| type QueryResp struct { | ||||
| 	Err      string `json:"err,omitempty"` | ||||
| 	Err string `json:"err,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type Meta struct { | ||||
|  | ||||
| @ -7,6 +7,7 @@ import ( | ||||
| 	easyjson "github.com/mailru/easyjson" | ||||
| 	jlexer "github.com/mailru/easyjson/jlexer" | ||||
| 	jwriter "github.com/mailru/easyjson/jwriter" | ||||
| 	fastimagehash_go "github.com/simon987/fastimagehash-go" | ||||
| ) | ||||
| 
 | ||||
| // suppress unused package warning | ||||
| @ -599,7 +600,7 @@ func (v *Image) UnmarshalJSON(data []byte) error { | ||||
| func (v *Image) UnmarshalEasyJSON(l *jlexer.Lexer) { | ||||
| 	easyjsonD2b7633eDecodeGithubComSimon987Imhashdb5(l, v) | ||||
| } | ||||
| func easyjsonD2b7633eDecodeGithubComSimon987Imhashdb6(in *jlexer.Lexer, out *HashResp) { | ||||
| func easyjsonD2b7633eDecodeGithubComSimon987Imhashdb6(in *jlexer.Lexer, out *Hashes) { | ||||
| 	isTopLevel := in.IsStart() | ||||
| 	if in.IsNull() { | ||||
| 		if isTopLevel { | ||||
| @ -618,40 +619,125 @@ func easyjsonD2b7633eDecodeGithubComSimon987Imhashdb6(in *jlexer.Lexer, out *Has | ||||
| 			continue | ||||
| 		} | ||||
| 		switch key { | ||||
| 		case "ahash:12": | ||||
| 		case "dhash8": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.AHash = nil | ||||
| 				out.DHash8 = nil | ||||
| 			} else { | ||||
| 				out.AHash = in.Bytes() | ||||
| 				if out.DHash8 == nil { | ||||
| 					out.DHash8 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.DHash8) | ||||
| 			} | ||||
| 		case "dhash:12": | ||||
| 		case "dhash16": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.DHash = nil | ||||
| 				out.DHash16 = nil | ||||
| 			} else { | ||||
| 				out.DHash = in.Bytes() | ||||
| 				if out.DHash16 == nil { | ||||
| 					out.DHash16 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.DHash16) | ||||
| 			} | ||||
| 		case "mhash:12": | ||||
| 		case "dhash32": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.MHash = nil | ||||
| 				out.DHash32 = nil | ||||
| 			} else { | ||||
| 				out.MHash = in.Bytes() | ||||
| 				if out.DHash32 == nil { | ||||
| 					out.DHash32 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.DHash32) | ||||
| 			} | ||||
| 		case "phash:12:4": | ||||
| 		case "mhash8": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.PHash = nil | ||||
| 				out.MHash8 = nil | ||||
| 			} else { | ||||
| 				out.PHash = in.Bytes() | ||||
| 				if out.MHash8 == nil { | ||||
| 					out.MHash8 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.MHash8) | ||||
| 			} | ||||
| 		case "whash:18:haar": | ||||
| 		case "mhash16": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.WHash = nil | ||||
| 				out.MHash16 = nil | ||||
| 			} else { | ||||
| 				out.WHash = in.Bytes() | ||||
| 				if out.MHash16 == nil { | ||||
| 					out.MHash16 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.MHash16) | ||||
| 			} | ||||
| 		case "mhash32": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.MHash32 = nil | ||||
| 			} else { | ||||
| 				if out.MHash32 == nil { | ||||
| 					out.MHash32 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.MHash32) | ||||
| 			} | ||||
| 		case "phash8": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.PHash8 = nil | ||||
| 			} else { | ||||
| 				if out.PHash8 == nil { | ||||
| 					out.PHash8 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.PHash8) | ||||
| 			} | ||||
| 		case "phash16": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.PHash16 = nil | ||||
| 			} else { | ||||
| 				if out.PHash16 == nil { | ||||
| 					out.PHash16 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.PHash16) | ||||
| 			} | ||||
| 		case "phash32": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.PHash32 = nil | ||||
| 			} else { | ||||
| 				if out.PHash32 == nil { | ||||
| 					out.PHash32 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.PHash32) | ||||
| 			} | ||||
| 		case "whash8haar": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.WHash8 = nil | ||||
| 			} else { | ||||
| 				if out.WHash8 == nil { | ||||
| 					out.WHash8 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.WHash8) | ||||
| 			} | ||||
| 		case "whash16haar": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.WHash16 = nil | ||||
| 			} else { | ||||
| 				if out.WHash16 == nil { | ||||
| 					out.WHash16 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.WHash16) | ||||
| 			} | ||||
| 		case "whash32haar": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.WHash32 = nil | ||||
| 			} else { | ||||
| 				if out.WHash32 == nil { | ||||
| 					out.WHash32 = new(fastimagehash_go.Hash) | ||||
| 				} | ||||
| 				easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in, out.WHash32) | ||||
| 			} | ||||
| 		default: | ||||
| 			in.SkipRecursive() | ||||
| @ -663,61 +749,198 @@ func easyjsonD2b7633eDecodeGithubComSimon987Imhashdb6(in *jlexer.Lexer, out *Has | ||||
| 		in.Consumed() | ||||
| 	} | ||||
| } | ||||
| func easyjsonD2b7633eEncodeGithubComSimon987Imhashdb6(out *jwriter.Writer, in HashResp) { | ||||
| func easyjsonD2b7633eEncodeGithubComSimon987Imhashdb6(out *jwriter.Writer, in Hashes) { | ||||
| 	out.RawByte('{') | ||||
| 	first := true | ||||
| 	_ = first | ||||
| 	{ | ||||
| 		const prefix string = ",\"ahash:12\":" | ||||
| 		const prefix string = ",\"dhash8\":" | ||||
| 		out.RawString(prefix[1:]) | ||||
| 		out.Base64Bytes(in.AHash) | ||||
| 		if in.DHash8 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.DHash8) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"dhash:12\":" | ||||
| 		const prefix string = ",\"dhash16\":" | ||||
| 		out.RawString(prefix) | ||||
| 		out.Base64Bytes(in.DHash) | ||||
| 		if in.DHash16 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.DHash16) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"mhash:12\":" | ||||
| 		const prefix string = ",\"dhash32\":" | ||||
| 		out.RawString(prefix) | ||||
| 		out.Base64Bytes(in.MHash) | ||||
| 		if in.DHash32 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.DHash32) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"phash:12:4\":" | ||||
| 		const prefix string = ",\"mhash8\":" | ||||
| 		out.RawString(prefix) | ||||
| 		out.Base64Bytes(in.PHash) | ||||
| 		if in.MHash8 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.MHash8) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"whash:18:haar\":" | ||||
| 		const prefix string = ",\"mhash16\":" | ||||
| 		out.RawString(prefix) | ||||
| 		out.Base64Bytes(in.WHash) | ||||
| 		if in.MHash16 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.MHash16) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"mhash32\":" | ||||
| 		out.RawString(prefix) | ||||
| 		if in.MHash32 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.MHash32) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"phash8\":" | ||||
| 		out.RawString(prefix) | ||||
| 		if in.PHash8 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.PHash8) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"phash16\":" | ||||
| 		out.RawString(prefix) | ||||
| 		if in.PHash16 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.PHash16) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"phash32\":" | ||||
| 		out.RawString(prefix) | ||||
| 		if in.PHash32 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.PHash32) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"whash8haar\":" | ||||
| 		out.RawString(prefix) | ||||
| 		if in.WHash8 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.WHash8) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"whash16haar\":" | ||||
| 		out.RawString(prefix) | ||||
| 		if in.WHash16 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.WHash16) | ||||
| 		} | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"whash32haar\":" | ||||
| 		out.RawString(prefix) | ||||
| 		if in.WHash32 == nil { | ||||
| 			out.RawString("null") | ||||
| 		} else { | ||||
| 			easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out, *in.WHash32) | ||||
| 		} | ||||
| 	} | ||||
| 	out.RawByte('}') | ||||
| } | ||||
| 
 | ||||
| // MarshalJSON supports json.Marshaler interface | ||||
| func (v HashResp) MarshalJSON() ([]byte, error) { | ||||
| func (v Hashes) MarshalJSON() ([]byte, error) { | ||||
| 	w := jwriter.Writer{} | ||||
| 	easyjsonD2b7633eEncodeGithubComSimon987Imhashdb6(&w, v) | ||||
| 	return w.Buffer.BuildBytes(), w.Error | ||||
| } | ||||
| 
 | ||||
| // MarshalEasyJSON supports easyjson.Marshaler interface | ||||
| func (v HashResp) MarshalEasyJSON(w *jwriter.Writer) { | ||||
| func (v Hashes) MarshalEasyJSON(w *jwriter.Writer) { | ||||
| 	easyjsonD2b7633eEncodeGithubComSimon987Imhashdb6(w, v) | ||||
| } | ||||
| 
 | ||||
| // UnmarshalJSON supports json.Unmarshaler interface | ||||
| func (v *HashResp) UnmarshalJSON(data []byte) error { | ||||
| func (v *Hashes) UnmarshalJSON(data []byte) error { | ||||
| 	r := jlexer.Lexer{Data: data} | ||||
| 	easyjsonD2b7633eDecodeGithubComSimon987Imhashdb6(&r, v) | ||||
| 	return r.Error() | ||||
| } | ||||
| 
 | ||||
| // UnmarshalEasyJSON supports easyjson.Unmarshaler interface | ||||
| func (v *HashResp) UnmarshalEasyJSON(l *jlexer.Lexer) { | ||||
| func (v *Hashes) UnmarshalEasyJSON(l *jlexer.Lexer) { | ||||
| 	easyjsonD2b7633eDecodeGithubComSimon987Imhashdb6(l, v) | ||||
| } | ||||
| func easyjsonD2b7633eDecodeGithubComSimon987FastimagehashGo(in *jlexer.Lexer, out *fastimagehash_go.Hash) { | ||||
| 	isTopLevel := in.IsStart() | ||||
| 	if in.IsNull() { | ||||
| 		if isTopLevel { | ||||
| 			in.Consumed() | ||||
| 		} | ||||
| 		in.Skip() | ||||
| 		return | ||||
| 	} | ||||
| 	in.Delim('{') | ||||
| 	for !in.IsDelim('}') { | ||||
| 		key := in.UnsafeString() | ||||
| 		in.WantColon() | ||||
| 		if in.IsNull() { | ||||
| 			in.Skip() | ||||
| 			in.WantComma() | ||||
| 			continue | ||||
| 		} | ||||
| 		switch key { | ||||
| 		case "Size": | ||||
| 			out.Size = int(in.Int()) | ||||
| 		case "Bytes": | ||||
| 			if in.IsNull() { | ||||
| 				in.Skip() | ||||
| 				out.Bytes = nil | ||||
| 			} else { | ||||
| 				out.Bytes = in.Bytes() | ||||
| 			} | ||||
| 		default: | ||||
| 			in.SkipRecursive() | ||||
| 		} | ||||
| 		in.WantComma() | ||||
| 	} | ||||
| 	in.Delim('}') | ||||
| 	if isTopLevel { | ||||
| 		in.Consumed() | ||||
| 	} | ||||
| } | ||||
| func easyjsonD2b7633eEncodeGithubComSimon987FastimagehashGo(out *jwriter.Writer, in fastimagehash_go.Hash) { | ||||
| 	out.RawByte('{') | ||||
| 	first := true | ||||
| 	_ = first | ||||
| 	{ | ||||
| 		const prefix string = ",\"Size\":" | ||||
| 		out.RawString(prefix[1:]) | ||||
| 		out.Int(int(in.Size)) | ||||
| 	} | ||||
| 	{ | ||||
| 		const prefix string = ",\"Bytes\":" | ||||
| 		out.RawString(prefix) | ||||
| 		out.Base64Bytes(in.Bytes) | ||||
| 	} | ||||
| 	out.RawByte('}') | ||||
| } | ||||
| func easyjsonD2b7633eDecodeGithubComSimon987Imhashdb7(in *jlexer.Lexer, out *HashReq) { | ||||
| 	isTopLevel := in.IsStart() | ||||
| 	if in.IsNull() { | ||||
|  | ||||
							
								
								
									
										29
									
								
								web/api.go
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								web/api.go
									
									
									
									
									
								
							| @ -14,6 +14,23 @@ import ( | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| func CORSMiddleware() gin.HandlerFunc { | ||||
| 	return func(c *gin.Context) { | ||||
| 		c.Writer.Header().Set("Access-Control-Allow-Origin", "*") | ||||
| 		c.Writer.Header().Set("Access-Control-Max-Age", "86400") | ||||
| 		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") | ||||
| 		c.Writer.Header().Set("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding, x-access-token") | ||||
| 		c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") | ||||
| 		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") | ||||
| 
 | ||||
| 		if c.Request.Method == "OPTIONS" { | ||||
| 			c.AbortWithStatus(200) | ||||
| 		} else { | ||||
| 			c.Next() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func submitQuery(value string) bool { | ||||
| 	if Rdb.SIsMember(wipQueue, value).Val() { | ||||
| 		return false | ||||
| @ -80,17 +97,18 @@ func hash(c *gin.Context) { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if req.Data == nil { | ||||
| 		c.JSON(400, gin.H{"err": "Invalid request"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	h, err := ComputeHash(req.Data) | ||||
| 	if err != nil { | ||||
| 		c.JSON(500, gin.H{"err": "Couldn't compute image hash"}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	b, _ := easyjson.Marshal(HashResp{ | ||||
| 		AHash: h.AHash.Bytes, DHash: h.DHash.Bytes, | ||||
| 		MHash: h.MHash.Bytes, PHash: h.PHash.Bytes, | ||||
| 		WHash: h.WHash.Bytes, | ||||
| 	}) | ||||
| 	b, _ := easyjson.Marshal(h) | ||||
| 	c.Data(200, gin.MIMEJSON, b) | ||||
| } | ||||
| 
 | ||||
| @ -110,6 +128,7 @@ func main() { | ||||
| 	}() | ||||
| 
 | ||||
| 	r := gin.Default() | ||||
| 	r.Use(CORSMiddleware()) | ||||
| 	r.POST("/api/hash", hash) | ||||
| 	r.POST("/api/query", query) | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user