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 }