427 lines
9.7 KiB
Go
427 lines
9.7 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/go-redis/redis/v8"
|
|
"github.com/nitishm/go-rejson/v4/rjs"
|
|
"repositories.action2quare.com/ayo/gocommon"
|
|
"repositories.action2quare.com/ayo/gocommon/logger"
|
|
"repositories.action2quare.com/ayo/gocommon/wshandler"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
)
|
|
|
|
type accountID = primitive.ObjectID
|
|
type ticketID = primitive.ObjectID
|
|
type groupID = primitive.ObjectID
|
|
|
|
func makeTid(gid groupID, in accountID) string {
|
|
var out primitive.ObjectID
|
|
for i := range in {
|
|
out[12-i-1] = gid[i] ^ in[12-i-1]
|
|
}
|
|
return out.Hex()
|
|
}
|
|
|
|
type Invitation struct {
|
|
GroupID groupID `json:"gid"`
|
|
TicketID string `json:"tid"`
|
|
Inviter bson.M `json:"inviter"`
|
|
ExpireAtUTC int64 `json:"expire_at_utc"`
|
|
}
|
|
|
|
// 플레이어한테 공유하는 멤버 정보
|
|
type memberDoc struct {
|
|
Body bson.M `json:"body"`
|
|
Invite bool `json:"invite"`
|
|
InviteExpire int64 `json:"invite_exp"`
|
|
}
|
|
|
|
type GroupDocBody = bson.M
|
|
type InvitationFail bson.M
|
|
|
|
type groupDoc struct {
|
|
Body GroupDocBody `json:"body"`
|
|
Members map[string]*memberDoc `json:"members"`
|
|
InCharge string `json:"incharge"`
|
|
|
|
rh *RejsonHandler
|
|
id groupID
|
|
idhex string
|
|
}
|
|
|
|
func (gd *groupDoc) strid() string {
|
|
if len(gd.idhex) == 0 {
|
|
gd.idhex = gd.id.Hex()
|
|
}
|
|
return gd.idhex
|
|
}
|
|
|
|
func (gd *groupDoc) tid(in accountID) string {
|
|
return makeTid(gd.id, in)
|
|
}
|
|
|
|
func (gd *groupDoc) mid(tid string) accountID {
|
|
tidobj, _ := primitive.ObjectIDFromHex(tid)
|
|
var out primitive.ObjectID
|
|
for i := range tidobj {
|
|
out[12-i-1] = gd.id[i] ^ tidobj[12-i-1]
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (gd *groupDoc) addInvite(inviteeDoc bson.M, ttl time.Duration, max int) (*memberDoc, error) {
|
|
mid := inviteeDoc["_mid"].(accountID)
|
|
body := inviteeDoc["body"].(bson.M)
|
|
|
|
// 초대 가능한 빈 자리가 있나
|
|
tids, err := gd.rh.JSONObjKeys(gd.strid(), "$.members")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
createNewDoc := func() *memberDoc {
|
|
return &memberDoc{
|
|
Body: body,
|
|
Invite: true,
|
|
InviteExpire: now.Add(ttl).Unix(),
|
|
}
|
|
}
|
|
|
|
if len(tids) < max {
|
|
newdoc := createNewDoc()
|
|
_, err := gd.rh.JSONSet(gd.strid(), "$.members."+gd.tid(mid), newdoc)
|
|
return newdoc, err
|
|
}
|
|
|
|
expires, err := gd.rh.JSONGetInt64(gd.strid(), "$.members..invite_exp")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var delpaths []string
|
|
for i, expire := range expires {
|
|
if expire < now.Unix() {
|
|
// 만료된 초대가 있네? 지우자
|
|
delpaths = append(delpaths, "$.members."+tids[i])
|
|
}
|
|
}
|
|
|
|
if len(delpaths) == 0 {
|
|
// 빈자리가 없다
|
|
return nil, nil
|
|
}
|
|
|
|
if err := gd.rh.JSONMDel(gd.strid(), delpaths); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newdoc := createNewDoc()
|
|
_, err = gd.rh.JSONSet(gd.strid(), "$.members."+mid.Hex(), newdoc)
|
|
return newdoc, err
|
|
}
|
|
|
|
func (gd *groupDoc) addMember(mid accountID, doc bson.M) (*memberDoc, error) {
|
|
memdoc := &memberDoc{
|
|
Invite: false,
|
|
InviteExpire: 0,
|
|
Body: doc,
|
|
}
|
|
|
|
if _, err := gd.rh.JSONSet(gd.strid(), "$.members."+gd.tid(mid), memdoc, rjs.SetOptionXX); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return memdoc, nil
|
|
}
|
|
|
|
func (gd *groupDoc) removeMember(mid accountID) error {
|
|
_, err := gd.rh.JSONDel(gd.strid(), "$.members."+gd.tid(mid))
|
|
return err
|
|
}
|
|
|
|
type groupInMemory struct {
|
|
*groupConfig
|
|
sendUpstreamMessage func(*wshandler.UpstreamMessage)
|
|
sendEnterRoomMessage func(groupID, accountID)
|
|
sendLeaveRoomMessage func(groupID, accountID)
|
|
rh *RejsonHandler
|
|
}
|
|
|
|
func (gm *groupInMemory) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) {
|
|
tid := makeTid(newid, charge)
|
|
|
|
gd := &groupDoc{
|
|
Body: bson.M{},
|
|
Members: map[string]*memberDoc{
|
|
tid: {
|
|
Body: chargeDoc,
|
|
Invite: false,
|
|
InviteExpire: 0,
|
|
},
|
|
},
|
|
InCharge: tid,
|
|
|
|
rh: gm.rh,
|
|
id: newid,
|
|
}
|
|
|
|
_, err := gm.rh.JSONSet(gd.strid(), "$", gd, rjs.SetOptionNX)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gd, nil
|
|
}
|
|
|
|
func (gm *groupInMemory) find(id groupID) (*groupDoc, error) {
|
|
if id.IsZero() {
|
|
return nil, nil
|
|
}
|
|
|
|
_, err := gm.rh.JSONObjLen(id.Hex(), "$")
|
|
if err == redis.Nil {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &groupDoc{
|
|
rh: gm.rh,
|
|
id: id,
|
|
}, nil
|
|
}
|
|
|
|
func (gm *groupInMemory) Create(form url.Values, base bson.M) (groupID, error) {
|
|
return primitive.NilObjectID, nil
|
|
}
|
|
|
|
func (gm *groupInMemory) Candidate(gid groupID, mid accountID, doc bson.M) error {
|
|
logger.Error("not implemented func : Canidate")
|
|
return nil
|
|
}
|
|
|
|
var errGroupNotExist = errors.New("group does not exist")
|
|
|
|
func (gm *groupInMemory) Join(gid groupID, mid accountID, doc bson.M) error {
|
|
group, err := gm.find(gid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if group == nil {
|
|
// 그룹이 없다. 실패
|
|
return errGroupNotExist
|
|
}
|
|
|
|
// 내 정보 업데이트할 때에도 사용됨
|
|
_, err = group.addMember(mid, doc)
|
|
|
|
return err
|
|
}
|
|
|
|
var errInviteeDocMidMissing = errors.New("inviteeDoc must have '_mid' field")
|
|
var errAlreadyInvited = errors.New("this target is already invited by someone or me")
|
|
|
|
func (gm *groupInMemory) Invite(gid groupID, mid accountID, inviterDoc bson.M, inviteeDoc bson.M) (string, error) {
|
|
targetid, ok := inviteeDoc["_mid"].(accountID)
|
|
if !ok {
|
|
return "", errInviteeDocMidMissing
|
|
}
|
|
|
|
// targetid에 초대한 mid가 들어있다.
|
|
already, err := gm.rh.Get(gm.rh.ctx, targetid.Hex()).Result()
|
|
if err != nil && err != redis.Nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(already) > 0 {
|
|
if already != mid.Hex() {
|
|
// 이미 초대 중이다.
|
|
// inviter한테 알려줘야 한다.
|
|
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
|
Target: "@" + mid.Hex(),
|
|
Body: inviteeDoc,
|
|
Tag: []string{"InvitationFail"},
|
|
})
|
|
}
|
|
return "", errAlreadyInvited
|
|
}
|
|
|
|
gd, err := gm.find(gid)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if gd == nil {
|
|
gd, err = gm.createGroup(gid, mid, inviterDoc)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// 내가 wshandler room에 입장
|
|
gm.sendEnterRoomMessage(gid, mid)
|
|
}
|
|
|
|
newdoc, err := gd.addInvite(inviteeDoc, time.Duration(gm.InviteExpire+1)*time.Second, gm.MaxMember)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 초대 중 표시
|
|
gm.rh.SetNX(gm.rh.ctx, targetid.Hex(), mid.Hex(), time.Duration(gm.InviteExpire)).Result()
|
|
|
|
// invitee에게 알림
|
|
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
|
Target: "@" + targetid.Hex(),
|
|
Body: Invitation{
|
|
GroupID: gid,
|
|
TicketID: gd.tid(targetid),
|
|
Inviter: inviterDoc,
|
|
ExpireAtUTC: newdoc.InviteExpire,
|
|
},
|
|
Tag: []string{"Invitation"},
|
|
})
|
|
|
|
return gd.strid(), nil
|
|
}
|
|
|
|
func (gm *groupInMemory) CancelInvitation(gid groupID, tid ticketID) error {
|
|
return nil
|
|
}
|
|
|
|
var errInvitationExpired = errors.New("invitation is already expired")
|
|
|
|
func (gm *groupInMemory) AcceptInvitation(gid groupID, mid accountID, member bson.M) error {
|
|
cnt, err := gm.rh.Del(gm.rh.ctx, mid.Hex()).Result()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cnt == 0 {
|
|
// 만료됨
|
|
return errInvitationExpired
|
|
}
|
|
|
|
gd := groupDoc{
|
|
id: gid,
|
|
rh: gm.rh,
|
|
}
|
|
|
|
_, err = gd.addMember(mid, member)
|
|
if err == nil {
|
|
gm.sendEnterRoomMessage(gid, mid)
|
|
}
|
|
|
|
// 실패
|
|
return err
|
|
}
|
|
|
|
func (gm *groupInMemory) DenyInvitation(gid groupID, mid accountID, tid ticketID) error {
|
|
gm.rh.Del(gm.rh.ctx, mid.Hex()).Result()
|
|
gd := groupDoc{
|
|
id: gid,
|
|
rh: gm.rh,
|
|
}
|
|
return gd.removeMember(mid)
|
|
}
|
|
|
|
func (gm *groupInMemory) QueryInvitations(mid accountID, after primitive.Timestamp) ([]bson.M, error) {
|
|
return nil, nil
|
|
}
|
|
func (gm *groupInMemory) Exist(gid groupID, filter bson.M) (bool, error) {
|
|
return false, nil
|
|
}
|
|
func (gm *groupInMemory) FindAll(filter bson.M, projection string, after primitive.Timestamp) ([]bson.M, error) {
|
|
return nil, nil
|
|
}
|
|
func (gm *groupInMemory) FindOne(gid groupID, projection string) (bson.M, error) {
|
|
return nil, nil
|
|
}
|
|
func (gm *groupInMemory) Leave(gid groupID, mid accountID) error {
|
|
gd := groupDoc{
|
|
id: gid,
|
|
rh: gm.rh,
|
|
}
|
|
if err := gd.removeMember(mid); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 나한테는 빈 FullGroupDoc을 보낸다.
|
|
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
|
Target: "@" + mid.Hex(),
|
|
Body: bson.M{"gid": gid},
|
|
Tag: []string{"FullGroupDoc", gid.Hex()},
|
|
})
|
|
gm.sendLeaveRoomMessage(gid, mid)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gm *groupInMemory) UpdateMemberDocument(gid groupID, mid accountID, doc bson.M) error {
|
|
gd := groupDoc{
|
|
id: gid,
|
|
rh: gm.rh,
|
|
}
|
|
_, err := gm.rh.JSONSet(gd.strid(), "$.members."+gd.tid(mid)+".body", doc, rjs.SetOptionXX)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
gm.sendUpstreamMessage(&wshandler.UpstreamMessage{
|
|
Target: "#" + gid.Hex(),
|
|
Body: map[string]any{
|
|
gd.tid(mid): doc,
|
|
},
|
|
Tag: []string{"GroupDocBody"},
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (gm *groupInMemory) Dismiss(gid groupID) error {
|
|
return nil
|
|
}
|
|
|
|
func (gm *groupInMemory) UpdateGroupDocument(gid groupID, body []byte) error {
|
|
gd := groupDoc{
|
|
id: gid,
|
|
rh: gm.rh,
|
|
}
|
|
_, err := gm.rh.JSONSet(gd.strid(), "$.members.body", body, rjs.SetOptionXX)
|
|
return err
|
|
}
|
|
|
|
func (cfg *groupConfig) prepareInMemory(ctx context.Context, typename string, sub *subTavern) (group, error) {
|
|
// group document
|
|
// member document
|
|
region := sub.region
|
|
wsh := sub.wsh
|
|
storage := config.RegionStorage[sub.region]
|
|
redisClient, err := gocommon.NewRedisClient(storage.Redis["tavern"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// 여기서는 subscribe channel
|
|
// 각 함수에서는 publish
|
|
gm := &groupInMemory{
|
|
groupConfig: cfg,
|
|
rh: NewReJSONHandler(ctx, redisClient),
|
|
sendUpstreamMessage: func(msg *wshandler.UpstreamMessage) {
|
|
wsh.SendUpstreamMessage(region, msg)
|
|
},
|
|
sendEnterRoomMessage: func(gid groupID, accid accountID) {
|
|
wsh.EnterRoom(region, gid.Hex(), accid)
|
|
},
|
|
sendLeaveRoomMessage: func(gid groupID, accid accountID) {
|
|
wsh.LeaveRoom(region, gid.Hex(), accid)
|
|
},
|
|
}
|
|
|
|
return gm, nil
|
|
}
|