mirror of
https://github.com/CPunch/gopenfusion.git
synced 2024-12-04 06:00:24 +00:00
Compare commits
7 Commits
899b95b4e6
...
ac62f7d64e
Author | SHA1 | Date | |
---|---|---|---|
ac62f7d64e | |||
58afc9df1f | |||
e257bf998f | |||
96eed66831 | |||
12f16645e1 | |||
76e9bdf7e7 | |||
44f3b31965 |
2
.github/workflows/tests.yaml
vendored
2
.github/workflows/tests.yaml
vendored
@ -5,9 +5,11 @@ on:
|
||||
paths:
|
||||
- cmd/**
|
||||
- config/**
|
||||
- cnet/**
|
||||
- internal/**
|
||||
- login/**
|
||||
- shard/**
|
||||
- util/**
|
||||
- go.mod
|
||||
- go.sum
|
||||
- .github/workflows/tests.yaml
|
||||
|
@ -1,6 +1,6 @@
|
||||
# gopenfusion
|
||||
|
||||
A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) and accompanying services, written in Go.
|
||||
A toy implementation of the [Fusionfall Packet Protocol](https://openpunk.com/pages/fusionfall-openfusion/) (see: `cnet/`) and accompanying services, written in Go.
|
||||
|
||||
## Landwalker demo
|
||||
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/login"
|
||||
"github.com/google/subcommands"
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"flag"
|
||||
"log"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/shard"
|
||||
"github.com/google/subcommands"
|
||||
)
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_KEY = "m@rQn~W#"
|
||||
DEFAULT_KEY = "m@rQn~W#" // if you change this, make sure to update the test data in protocol_test.go
|
||||
KEY_LENGTH = 8
|
||||
)
|
||||
|
||||
@ -50,13 +50,14 @@ func DecryptData(buff, key []byte) {
|
||||
}
|
||||
|
||||
func CreateNewKey(uTime, iv1, iv2 uint64) []byte {
|
||||
dEKey := binary.LittleEndian.Uint64([]byte(DEFAULT_KEY))
|
||||
|
||||
num := iv1 + 1
|
||||
num2 := iv2 + 1
|
||||
|
||||
dEKey := uint64(binary.LittleEndian.Uint64([]byte(DEFAULT_KEY)))
|
||||
key := dEKey * (uTime * num * num2)
|
||||
|
||||
buf := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(buf, uint64(key))
|
||||
binary.LittleEndian.PutUint64(buf, key)
|
||||
return buf
|
||||
}
|
||||
|
||||
|
@ -42,10 +42,7 @@ func (pkt Packet) encodeStructField(field reflect.StructField, value reflect.Val
|
||||
buf16 = buf16[:sz]
|
||||
} else {
|
||||
// grow
|
||||
// TODO: probably a better way to do this?
|
||||
for len(buf16) < sz {
|
||||
buf16 = append(buf16, 0)
|
||||
}
|
||||
buf16 = append(buf16, make([]uint16, sz-len(buf16))...)
|
||||
}
|
||||
|
||||
// write
|
||||
@ -125,8 +122,7 @@ func (pkt Packet) decodeStructField(field reflect.StructField, value reflect.Val
|
||||
// consume padding bytes
|
||||
pad, err := strconv.Atoi(field.Tag.Get("pad"))
|
||||
if err == nil {
|
||||
dummy := make([]byte, pad)
|
||||
if _, err := pkt.readWriter.Read(dummy); err != nil {
|
||||
if _, err := pkt.readWriter.Read(make([]byte, pad)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ func TestPacketEncode(t *testing.T) {
|
||||
err := pkt.Encode(testStruct)
|
||||
is.NoErr(err)
|
||||
|
||||
is.True(bytes.Equal(buf.Bytes(), testData[:])) // encoded data should match expected data
|
||||
is.Equal(buf.Bytes(), testData[:]) // encoded data should match expected data
|
||||
}
|
||||
|
||||
func TestPacketDecode(t *testing.T) {
|
||||
@ -73,7 +73,7 @@ func TestPacketDecode(t *testing.T) {
|
||||
var test TestPacketData
|
||||
err := pkt.Decode(&test)
|
||||
is.NoErr(err)
|
||||
is.True(test == testStruct) // decoded data should match testStruct
|
||||
is.Equal(test, testStruct) // decoded data should match testStruct
|
||||
}
|
||||
|
||||
func TestDataEncrypt(t *testing.T) {
|
||||
@ -82,8 +82,7 @@ func TestDataEncrypt(t *testing.T) {
|
||||
copy(buf, testData[:])
|
||||
|
||||
protocol.EncryptData(buf, []byte(protocol.DEFAULT_KEY))
|
||||
|
||||
is.True(bytes.Equal(buf, encTestData)) // encrypted data should match expected data
|
||||
is.Equal(buf, encTestData) // encrypted data should match expected data
|
||||
}
|
||||
|
||||
func TestDataDecrypt(t *testing.T) {
|
||||
@ -92,13 +91,12 @@ func TestDataDecrypt(t *testing.T) {
|
||||
copy(buf, encTestData)
|
||||
|
||||
protocol.DecryptData(buf, []byte(protocol.DEFAULT_KEY))
|
||||
|
||||
is.True(bytes.Equal(buf, testData[:])) // decrypted data should match expected data
|
||||
is.Equal(buf, testData[:]) // decrypted data should match expected data
|
||||
}
|
||||
|
||||
func TestCreateNewKey(t *testing.T) {
|
||||
is := is.New(t)
|
||||
key := protocol.CreateNewKey(123456789, 0x1234567890abcdef, 0x1234567890abcdef)
|
||||
|
||||
is.True(bytes.Equal(key, []byte{0x0, 0x31, 0xb8, 0xcd, 0xd, 0xc3, 0xad, 0x67})) // key should match expected data
|
||||
is.Equal(key, []byte{0x0, 0x31, 0xb8, 0xcd, 0xd, 0xc3, 0xad, 0x67}) // key should match expected data
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
)
|
||||
|
||||
type PacketHandler func(peer *Peer, pkt protocol.Packet) error
|
||||
|
3
go.mod
3
go.mod
@ -3,6 +3,7 @@ module github.com/CPunch/gopenfusion
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alicebob/miniredis/v2 v2.31.0
|
||||
github.com/bitcomplete/sqltestutil v1.0.1
|
||||
github.com/blockloop/scan v1.3.0
|
||||
github.com/georgysavva/scany/v2 v2.0.0
|
||||
@ -16,6 +17,7 @@ require (
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
@ -34,6 +36,7 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/stretchr/testify v1.8.0 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
12
go.sum
12
go.sum
@ -1,7 +1,12 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
|
||||
github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpSH4c22PU=
|
||||
github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||
github.com/bitcomplete/sqltestutil v1.0.1 h1:rj/RgrXXyuPB8KYrFmxiSjORb1hrhK6sXHpDPaSEBII=
|
||||
github.com/bitcomplete/sqltestutil v1.0.1/go.mod h1:ZgpEnW6t2RBsCo9EIEYsAvjxJeZDwOzC8aVYXK0+gdE=
|
||||
github.com/blockloop/scan v1.3.0 h1:p8xnajpGA3d/V6o23IBFdQ764+JnNJ+PQj+OwT+rkdg=
|
||||
@ -10,6 +15,9 @@ github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -33,6 +41,7 @@ github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaL
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
@ -79,6 +88,8 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
|
||||
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@ -95,6 +106,7 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/blockloop/scan"
|
||||
)
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
)
|
||||
|
||||
type LoginMetadata struct {
|
||||
|
@ -7,7 +7,7 @@ package redis
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
@ -17,10 +17,6 @@ type RedisHandler struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
const (
|
||||
SHARD_SET = "shards"
|
||||
)
|
||||
|
||||
func OpenRedis(addr string) (*RedisHandler, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: addr,
|
||||
|
77
internal/redis/redis_test.go
Normal file
77
internal/redis/redis_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package redis_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
var (
|
||||
rh *redis.RedisHandler
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
r, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
rh, err = redis.OpenRedis(r.Addr())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rh.Close()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRedisLogin(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
// test data
|
||||
serialKey := int64(1234)
|
||||
data := redis.LoginMetadata{
|
||||
FEKey: []byte("test"),
|
||||
PlayerID: 1,
|
||||
AccountID: 2,
|
||||
}
|
||||
|
||||
// queue login
|
||||
is.NoErr(rh.QueueLogin(serialKey, data))
|
||||
|
||||
// get login
|
||||
loginData, err := rh.GetLogin(serialKey)
|
||||
is.NoErr(err)
|
||||
|
||||
// compare
|
||||
is.Equal(loginData, data) // received data should be the same as sent data
|
||||
|
||||
// delete login
|
||||
is.NoErr(rh.RemoveLogin(serialKey))
|
||||
|
||||
// get login
|
||||
_, err = rh.GetLogin(serialKey)
|
||||
is.True(err != nil) // should fail to get removed login
|
||||
}
|
||||
|
||||
func TestRedisShard(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
// test data
|
||||
shard := redis.ShardMetadata{
|
||||
IP: "0.0.0.0",
|
||||
Port: 1234,
|
||||
}
|
||||
|
||||
// register shard
|
||||
is.NoErr(rh.RegisterShard(shard))
|
||||
|
||||
// get shards
|
||||
shards := rh.GetShards()
|
||||
is.True(len(shards) == 1) // should only be 1 shard
|
||||
is.Equal(shards[0], shard) // received data should be the same as sent data
|
||||
}
|
@ -7,6 +7,10 @@ type ShardMetadata struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
const (
|
||||
SHARD_SET = "shards"
|
||||
)
|
||||
|
||||
func (r *RedisHandler) RegisterShard(shard ShardMetadata) error {
|
||||
value, err := json.Marshal(shard)
|
||||
if err != nil {
|
||||
|
@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
"github.com/CPunch/gopenfusion/util"
|
||||
|
@ -4,7 +4,7 @@ import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
)
|
||||
|
||||
type ChunkPosition struct {
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/shard/entity"
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/CPunch/gopenfusion/cnet"
|
||||
"github.com/CPunch/gopenfusion/cnet/protocol"
|
||||
"github.com/CPunch/gopenfusion/config"
|
||||
"github.com/CPunch/gopenfusion/internal/config"
|
||||
"github.com/CPunch/gopenfusion/internal/db"
|
||||
"github.com/CPunch/gopenfusion/internal/redis"
|
||||
"github.com/CPunch/gopenfusion/shard/entity"
|
||||
|
Loading…
Reference in New Issue
Block a user