package core import ( "context" "encoding/gob" "encoding/json" "errors" "fmt" "net/url" "time" "github.com/go-redis/redis/v8" "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 type Body = bson.M func init() { gob.Register(memberDoc{}) } 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"` // memberDoc.Body ExpireAtUTC int64 `json:"_expire_at_utc"` } // 플레이어한테 공유하는 멤버 정보 type memberDoc struct { Body `json:",inline"` Invite bool `json:"_invite"` InviteExpire int64 `json:"_invite_exp"` } type InvitationFail bson.M type groupDoc struct { Body `json:",inline"` Members map[string]*memberDoc `json:"_members"` InCharge string `json:"_incharge"` rh *RedisonHandler id groupID idhex string } type groupDocWithId struct { *groupDoc `json:",inline"` Gid string `json:"_gid"` } 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) { targetmid := inviteeDoc["_mid"].(accountID) targetbody := 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: targetbody, Invite: true, InviteExpire: now.Add(ttl).Unix(), } } newtid := gd.tid(targetmid) if len(tids) < max { // 빈자리를 찾았다. newdoc := createNewDoc() _, err := gd.rh.JSONSet(gd.strid(), "$._members."+newtid, 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."+newtid, newdoc) return newdoc, err } func (gd *groupDoc) addMember(mid accountID, doc bson.M) (*memberDoc, error) { memdoc := &memberDoc{ Body: doc, Invite: false, InviteExpire: 0, } if _, err := gd.rh.JSONSet(gd.strid(), "$._members."+gd.tid(mid), memdoc, 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 *RedisonHandler } 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, 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) gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "@" + mid.Hex(), Body: groupDocWithId{ groupDoc: gd, Gid: gd.strid(), }, Tag: []string{"GroupDocFull"}, }) } newdoc, err := gd.addInvite(inviteeDoc, time.Duration(gm.InviteExpire+1)*time.Second, gm.MaxMember) if err != nil { return "", err } // 초대 중 표시 success, err := gm.rh.SetNX(gm.rh.ctx, targetid.Hex(), mid.Hex(), time.Duration(gm.InviteExpire)*time.Second).Result() if err != nil { return "", err } logger.Println("invitation key :", targetid.Hex(), success) // 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 { logger.Println("accept invitation key :", mid.Hex()) 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, } memberDoc, err := gd.addMember(mid, member) if err == nil { // 기존 멤버에게 새 멤버를 알림 gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: map[string]any{ gd.tid(mid): memberDoc, }, Tag: []string{"MemberDocFull"}, }) gm.sendEnterRoomMessage(gid, mid) // 새 멤버에 그룹 전체를 알림 full, err := gm.rh.JSONGet(gd.strid(), "$") if err != nil { return err } logger.Println(full) var temp []groupDoc err = json.Unmarshal([]byte(full.(string)), &temp) if err != nil { return err } gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "@" + mid.Hex(), Body: groupDocWithId{ groupDoc: &temp[0], Gid: gd.strid(), }, Tag: []string{"GroupDocFull"}, }) } // 실패 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 } // 나한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "@" + mid.Hex(), Body: bson.M{"gid": gid}, Tag: []string{"GroupDocFull", 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, } prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid)) err := gm.rh.JSONMSetRel(gd.strid(), prefixPath, doc) if err != nil { return err } gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: map[string]any{ gd.tid(mid): doc, }, Tag: []string{"MemberDocFragment"}, }) return nil } func (gm *groupInMemory) Dismiss(gid groupID) error { return nil } func (gm *groupInMemory) UpdateGroupDocument(gid groupID, frag bson.M) error { gd := groupDoc{ id: gid, rh: gm.rh, } if err := gm.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil { return err } // 업데이트 알림 gm.sendUpstreamMessage(&wshandler.UpstreamMessage{ Target: "#" + gid.Hex(), Body: frag, Tag: []string{"GroupDocFragment"}, }) return nil } 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: NewRedisonHandler(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 }