diff --git a/config.json b/config.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/config.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/config_template.json b/config_template.json index 871ef54..7b32c79 100644 --- a/config_template.json +++ b/config_template.json @@ -6,7 +6,8 @@ "cache": "redis://192.168.8.94:6380/0", "session": "redis://192.168.8.94:6380/1", "tx": "redis://192.168.8.94:6380/2", - "tavern": "redis://192.168.8.94:6380/3" + "tavern": "redis://192.168.8.94:6380/3", + "wshandler": "redis://192.168.8.94:6380/4" } } }, @@ -14,9 +15,6 @@ "maingate_session_ttl" : 3600, "maingate_api_token": "63d08aa34f0162622c11284b", - "social_redis_url": "redis://192.168.8.94:6380/4", - "social_storage_url" : "mongodb://192.168.8.94:27017/social?replicaSet=repl01&retrywrites=false", - "tavern_service_url": "http://localhost/tavern", "tavern_group_types": { "party": { diff --git a/core/friend.go b/core/friend.go deleted file mode 100644 index 9610363..0000000 --- a/core/friend.go +++ /dev/null @@ -1,491 +0,0 @@ -package core - -import ( - "context" - "encoding/gob" - "encoding/json" - "errors" - "fmt" - "strings" - "sync" - "time" - - "github.com/gorilla/websocket" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo/options" - "repositories.action2quare.com/ayo/gocommon" - "repositories.action2quare.com/ayo/gocommon/logger" - "repositories.action2quare.com/ayo/gocommon/wshandler" -) - -const ( - friends_collection_name = gocommon.CollectionName("friends") - monitoring_center_count = 100 - state_online = "online" - state_offline = "offline" -) - -var friend_state_tag = []string{"social.FriendState"} - -type friendDoc struct { - Id primitive.ObjectID `bson:"_id,omitempty" json:"_id"` - From primitive.ObjectID `bson:"from" json:"-"` - To primitive.ObjectID `bson:"to" json:"-"` - ToAlias string `bson:"talias" json:"to"` - Timestamp int64 `bson:"ts" json:"ts"` - Deleted bool `bson:"deleted,omitempty" json:"deleted,omitempty"` -} - -type registerListener struct { - src primitive.ObjectID - alias string - l *listener -} - -type monitoringCenter struct { - regChan chan registerListener - publishState func(string, string, string) -} - -type connWithFriends struct { - c *websocket.Conn - friends []*friendDoc - initialized bool -} - -type connections struct { - connLock sync.Mutex - conns map[primitive.ObjectID]*connWithFriends -} - -func (cs *connections) new(accid primitive.ObjectID, conn *websocket.Conn) { - cs.connLock.Lock() - defer cs.connLock.Unlock() - cs.conns[accid] = &connWithFriends{ - c: conn, - initialized: false, - } -} - -func (cs *connections) delete(accid primitive.ObjectID) { - cs.connLock.Lock() - defer cs.connLock.Unlock() - - delete(cs.conns, accid) -} - -func (cs *connections) conn(accid primitive.ObjectID) *websocket.Conn { - cs.connLock.Lock() - defer cs.connLock.Unlock() - if cf, ok := cs.conns[accid]; ok { - return cf.c - } - return nil -} - -func (cs *connections) addFriend(accid primitive.ObjectID, fdoc *friendDoc) bool { - cs.connLock.Lock() - defer cs.connLock.Unlock() - if cf, ok := cs.conns[accid]; ok { - if cf.initialized { - cf.friends = append(cf.friends, fdoc) - return true - } - } - return false -} - -func (cs *connections) initFriends(accid primitive.ObjectID, fdocs []*friendDoc) { - cs.connLock.Lock() - defer cs.connLock.Unlock() - if cf, ok := cs.conns[accid]; ok { - cf.friends = fdocs - cf.initialized = true - } -} - -func (cs *connections) clearFriends(accid primitive.ObjectID) (out []*friendDoc) { - cs.connLock.Lock() - defer cs.connLock.Unlock() - if cf, ok := cs.conns[accid]; ok { - out = cf.friends - cf.friends = nil - cf.initialized = false - } - return -} - -type friends struct { - mongoClient gocommon.MongoClient - redison *gocommon.RedisonHandler - wsh *wshandler.WebsocketHandler - moncen []monitoringCenter - conns connections -} - -type listener struct { - c *websocket.Conn - me primitive.ObjectID -} - -type listenerMap struct { - listeners map[primitive.ObjectID]*listener - connected bool - online []byte - offline []byte -} - -func init() { - gob.Register([]friendDoc{}) -} - -// per channel -// src(alias) - listener(objectid) : socket -// - listener(objectid) : socket -// - listener(objectid) : socket - -func makeSrcMap(src string, connected bool) *listenerMap { - online, _ := json.Marshal(wshandler.DownstreamMessage{ - Body: bson.M{ - "from": src, - "state": state_online, - }, - Tag: friend_state_tag, - }) - - offline, _ := json.Marshal(wshandler.DownstreamMessage{ - Body: bson.M{ - "from": src, - "state": state_offline, - }, - Tag: friend_state_tag, - }) - - return &listenerMap{ - listeners: make(map[primitive.ObjectID]*listener), - connected: connected, - online: online, - offline: offline, - } -} - -func makeFriends(ctx context.Context, so *Social) (*friends, error) { - if err := so.mongoClient.MakeUniqueIndices(friends_collection_name, map[string]bson.D{ - "fromto": {{Key: "from", Value: 1}, {Key: "to", Value: 1}}, - }); err != nil { - return nil, err - } - - var moncen []monitoringCenter - for i := 0; i < monitoring_center_count; i++ { - subChannel := fmt.Sprintf("_soc_fr_monitor_ch_%d_%d", i, so.redison.Options().DB) - regChan := make(chan registerListener) - moncen = append(moncen, monitoringCenter{ - regChan: regChan, - publishState: func(src, alias, state string) { - so.redison.Publish(ctx, subChannel, src+alias+":"+state).Result() - }, - }) - - go func(subChannel string, regChan chan registerListener) { - pubsub := so.redison.Subscribe(ctx, subChannel) - listeners := make(map[primitive.ObjectID]*listenerMap) - for { - select { - case reg := <-regChan: - // 내가 관심있는 애들 등록 - srcmap, online := listeners[reg.src] - if !online { - srcmap = makeSrcMap(reg.alias, false) - listeners[reg.src] = srcmap - } - - if reg.l.c == nil { - // 등록 해제. 모니터링 종료 - // listener목록에서 나(reg.l.me)를 제거 - delete(srcmap.listeners, reg.l.me) - online = false - logger.Println("regChan unregistered :", reg.src.Hex(), reg.l.me.Hex()) - } else if oldl, ok := srcmap.listeners[reg.l.me]; ok { - // 내가 이미 리스너로 등록되어 있다. - // 상대방이 나를 차단했을 경우에는 기존 리스너가 nil임 - online = oldl != nil - logger.Println("regChan registered :", reg.src.Hex(), reg.l.me.Hex(), "old", online) - } else { - logger.Println("regChan registered :", reg.src.Hex(), reg.l.me.Hex()) - srcmap.listeners[reg.l.me] = reg.l - } - - if online && srcmap != nil { - logger.Println("regChan send online :", reg.l.me.Hex(), string(srcmap.online)) - reg.l.c.WriteMessage(websocket.TextMessage, srcmap.online) - } - - if len(srcmap.listeners) == 0 && !srcmap.connected { - delete(listeners, reg.src) - } - - case msg := <-pubsub.Channel(): - target, _ := primitive.ObjectIDFromHex(msg.Payload[:24]) - aliasstate := strings.SplitN(msg.Payload[24:], ":", 2) - var sent []byte - if srcmap, ok := listeners[target]; ok { - if aliasstate[1] == state_online { - sent = srcmap.online - srcmap.connected = true - } else if aliasstate[1] == state_offline { - sent = srcmap.offline - srcmap.connected = false - if len(srcmap.listeners) == 0 { - delete(listeners, target) - } - } - - if len(sent) > 0 { - for _, l := range srcmap.listeners { - logger.Println("state fire :", l.me, string(sent)) - l.c.WriteMessage(websocket.TextMessage, sent) - } - } - } else if aliasstate[1] == state_online { - listeners[target] = makeSrcMap(aliasstate[0], true) - } - } - } - }(subChannel, regChan) - } - - return &friends{ - mongoClient: so.mongoClient, - redison: so.redison, - wsh: so.wsh, - moncen: moncen, - conns: connections{ - conns: make(map[primitive.ObjectID]*connWithFriends), - }, - }, nil -} - -func (fs *friends) ClientConnected(conn *websocket.Conn, callby *wshandler.Sender) { - fs.conns.new(callby.Accid, conn) - - // 내 로그인 상태를 알림 - meidx := callby.Accid[11] % monitoring_center_count - fs.moncen[meidx].publishState(callby.Accid.Hex(), callby.Alias, state_online) -} - -func (fs *friends) ClientDisconnected(conn *websocket.Conn, callby *wshandler.Sender) { - // 로그 오프 상태를 알림 - meidx := callby.Accid[11] % monitoring_center_count - fs.moncen[meidx].publishState(callby.Accid.Hex(), callby.Alias, state_offline) - - fs.stopMonitoringFriends(callby.Accid) - - fs.conns.delete(callby.Accid) -} - -func (fs *friends) writeMessage(acc primitive.ObjectID, src any) { - c := fs.conns.conn(acc) - if c == nil { - return - } - - if bt, err := json.Marshal(src); err == nil { - c.WriteMessage(websocket.TextMessage, bt) - } -} - -var errAddFriendFailed = errors.New("addFriend failed") - -func (fs *friends) addFriend(f *friendDoc) error { - _, newid, err := fs.mongoClient.Update(friends_collection_name, bson.M{ - "_id": primitive.NewObjectID(), - }, bson.M{ - "$setOnInsert": f, - }, options.Update().SetUpsert(true)) - if err != nil { - return err - } - - if newid == nil { - return errAddFriendFailed - } - - f.Id = newid.(primitive.ObjectID) - if fs.conns.addFriend(f.From, f) { - // 모니터링 중 - conn := fs.conns.conn(f.From) - if conn != nil { - toidx := f.To[11] % monitoring_center_count - fs.moncen[toidx].regChan <- registerListener{ - src: f.To, - alias: f.ToAlias, - l: &listener{ - c: conn, - me: f.From, - }, - } - } - } - - return nil -} - -func (fs *friends) Block(ctx wshandler.ApiCallContext) { - // BlockByMe 에 추가하고 상대의 BlockByYou를 설정한다. - - // var bi struct { - // From primitive.ObjectID - // To primitive.ObjectID - // } - // if err := gocommon.MakeDecoder(r).Decode(&bi); err != nil { - // logger.Println("friends.Block failed :", err) - // w.WriteHeader(http.StatusBadRequest) - // return - // } - // logger.Println("friends.Block :", bi) -} - -func (fs *friends) DeleteFriend(ctx wshandler.ApiCallContext) { - fid, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) - - var fdoc friendDoc - if err := fs.mongoClient.FindOneAs(friends_collection_name, bson.M{ - "_id": fid, - }, &fdoc, options.FindOne().SetProjection(bson.M{ - "from": 1, - "to": 1, - })); err != nil { - logger.Println("DeleteFriend is failed :", err) - return - } - - if fdoc.Id.IsZero() { - return - } - - now := time.Now().UTC().Unix() - fdoc.Deleted = true - fdoc.Timestamp = now - - // 나한테 삭제 - fs.mongoClient.Update(friends_collection_name, bson.M{ - "_id": fid, - }, bson.M{ - "$set": bson.M{ - "deleted": true, - "ts": fdoc.Timestamp, - }, - }, options.Update().SetUpsert(false)) - fs.writeMessage(ctx.CallBy.Accid, &wshandler.DownstreamMessage{ - Body: []friendDoc{fdoc}, - Tag: friends_tag, - }) - - // 상대방에게 삭제 - var yourdoc friendDoc - if err := fs.mongoClient.FindOneAndUpdateAs(friends_collection_name, bson.M{ - "from": fdoc.To, - "to": fdoc.From, - }, bson.M{ - "$set": bson.M{ - "deleted": true, - "ts": now, - }, - }, &yourdoc, options.FindOneAndUpdate().SetReturnDocument(options.After).SetUpsert(false)); err == nil { - fs.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: fdoc.To.Hex(), - Body: []friendDoc{yourdoc}, - Tag: friends_tag, - }) - } -} - -func (fs *friends) StartMonitoringFriends(ctx wshandler.ApiCallContext) { - // 내 친구 목록에 나를 등록 - var friends []*friendDoc - if err := fs.mongoClient.FindAllAs(friends_collection_name, bson.M{ - "from": ctx.CallBy.Accid, - }, &friends, options.Find().SetProjection(bson.M{"to": 1, "talias": 1})); err != nil { - logger.Println("StartMonitoringFriends is failed :", err) - return - } - - me := &listener{ - c: fs.conns.conn(ctx.CallBy.Accid), - me: ctx.CallBy.Accid, - } - - for _, f := range friends { - toidx := f.To[11] % monitoring_center_count - fs.moncen[toidx].regChan <- registerListener{ - src: f.To, - alias: f.ToAlias, - l: me, - } - } - - fs.conns.initFriends(ctx.CallBy.Accid, friends) -} - -func (fs *friends) stopMonitoringFriends(accid primitive.ObjectID) { - friends := fs.conns.clearFriends(accid) - - if len(friends) > 0 { - // 나를 상대방 모니터링에서 뺀다 - nilListener := &listener{c: nil, me: accid} - for _, f := range friends { - toidx := f.To[11] % monitoring_center_count - fs.moncen[toidx].regChan <- registerListener{ - src: f.To, - alias: f.ToAlias, - l: nilListener, - } - } - } -} - -func (fs *friends) StopMonitoringFriends(ctx wshandler.ApiCallContext) { - fs.stopMonitoringFriends(ctx.CallBy.Accid) -} - -func (fs *friends) QueryFriends(ctx wshandler.ApiCallContext) { - queryfrom := int64(ctx.Arguments[0].(float64)) - - var myfriends []friendDoc - err := fs.mongoClient.FindAllAs(friends_collection_name, bson.M{ - "from": ctx.CallBy.Accid, - "ts": bson.M{"$gt": queryfrom}, - }, &myfriends) - if err != nil { - logger.Println("QueryReceivedInvitations failed. FindAllAs err :", err) - } - - if len(myfriends) > 0 { - fs.writeMessage(ctx.CallBy.Accid, &wshandler.DownstreamMessage{ - Alias: ctx.CallBy.Alias, - Body: myfriends, - Tag: friends_tag, - }) - } -} - -func (fs *friends) Trim(ctx wshandler.ApiCallContext) { - stringsTobjs := func(in []any) (out []primitive.ObjectID) { - for _, i := range in { - p, _ := primitive.ObjectIDFromHex(i.(string)) - out = append(out, p) - } - return - } - - ids := stringsTobjs(ctx.Arguments[2].([]any)) - if len(ids) > 0 { - if len(ids) == 1 { - fs.mongoClient.Delete(friends_collection_name, bson.M{"_id": ids[0]}) - } else { - fs.mongoClient.DeleteMany(friends_collection_name, bson.D{{Key: "_id", Value: bson.M{"$in": ids}}}) - } - } -} diff --git a/core/group.go b/core/group.go new file mode 100644 index 0000000..5ae0a82 --- /dev/null +++ b/core/group.go @@ -0,0 +1,3 @@ +package core + +type configDocument map[string]any diff --git a/core/group_chat.go b/core/group_chat.go new file mode 100644 index 0000000..5d84954 --- /dev/null +++ b/core/group_chat.go @@ -0,0 +1,380 @@ +package core + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/go-redis/redis/v8" + "go.mongodb.org/mongo-driver/bson/primitive" + "repositories.action2quare.com/ayo/gocommon" + "repositories.action2quare.com/ayo/gocommon/logger" + "repositories.action2quare.com/ayo/gocommon/wshandler" +) + +type channelID = string +type channelConfig struct { + Capacity int64 `json:"capacity"` + Size int64 `json:"size"` + Key string `json:"key"` + emptyJson string +} + +type chatConfig struct { + DefaultCapacity int64 `json:"default_capacity"` + Channels map[string]*channelConfig `json:"channels"` +} + +type groupChat struct { + chatConfig + rh *gocommon.RedisonHandler + enterRoom func(channelID, accountID) + leaveRoom func(channelID, accountID) + sendUpstreamMessage func(msg *wshandler.UpstreamMessage) +} + +func (gc *groupChat) Initialize(tv *Tavern, cfg configDocument) error { + rem, _ := json.Marshal(cfg) + if err := json.Unmarshal(rem, &gc.chatConfig); err != nil { + return err + } + + gc.enterRoom = func(chanid channelID, accid accountID) { + tv.wsh.EnterRoom(string(chanid), accid) + } + gc.leaveRoom = func(chanid channelID, accid accountID) { + tv.wsh.LeaveRoom(string(chanid), accid) + } + gc.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) { + tv.wsh.SendUpstreamMessage(msg) + } + + gc.rh = tv.redison + + for name, cfg := range gc.chatConfig.Channels { + if cfg.Capacity == 0 { + cfg.Capacity = gc.chatConfig.DefaultCapacity + } + cfg.Key = name + cfg.Size = 0 + + jm, _ := json.Marshal(cfg) + cfg.emptyJson = fmt.Sprintf("[%s]", string(jm)) + + _, err := gc.rh.JSONSet(name, "$", cfg) + if *devflag && err != nil { + gc.rh.JSONDel(name, "$") + _, err = gc.rh.JSONSet(name, "$", cfg) + } + + if err != nil { + return err + } + } + + return nil +} + +func (gc *groupChat) ClientConnected(ctx wshandler.ApiCallContext) { + gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel", map[string]any{}) +} + +func (gc *groupChat) ClientDisconnected(ctx wshandler.ApiCallContext) { + docs, _ := gc.rh.JSONGetDocuments(ctx.CallBy.Accid.Hex(), "$.channel") + + if len(docs) > 0 { + for k, v := range docs[0] { + typename := k + chanid := v.(string) + gc.leaveRoom(chanid, ctx.CallBy.Accid) + if k == "public" { + gc.rh.JSONNumIncrBy(chanid, "$.size", -1) + } else { + gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + chanid, + Body: map[string]any{"sender": ctx.CallBy.Alias, "typename": typename}, + Tag: []string{"LeavePrivateChannel"}, + }) + } + } + } +} + +func (gc *groupChat) EnterPublicChannel(ctx wshandler.ApiCallContext) { + chanid := ctx.Arguments[0].(string) + if cfg, ok := gc.chatConfig.Channels[chanid]; ok { + size, err := gc.rh.JSONGetInt64(chanid, "$.size") + if err != nil || len(size) == 0 { + logger.Println("JSONGetInt64 failed :", chanid, err) + } else if size[0] < cfg.Capacity { + // 입장 + newsize, err := gc.rh.JSONNumIncrBy(chanid, "$.size", 1) + if err == nil { + gc.enterRoom(chanid, ctx.CallBy.Accid) + gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel.public", chanid) + gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + chanid, + Body: map[string]any{"size": newsize[0]}, + Tag: []string{"ChattingChannelProperties"}, + }) + } + } else { + // 풀방 + logger.Println("chatting channel is full :", chanid, size, cfg.Capacity) + } + } else { + logger.Println("chatting channel not valid :", chanid) + } +} + +func (gc *groupChat) LeavePublicChannel(ctx wshandler.ApiCallContext) { + chanid := ctx.Arguments[0].(string) + cnt, _ := gc.rh.JSONDel(ctx.CallBy.Accid.Hex(), "$.channel.public") + if cnt > 0 { + gc.leaveRoom(chanid, ctx.CallBy.Accid) + gc.rh.JSONNumIncrBy(chanid, "$.size", -1) + } +} + +func (gc *groupChat) TextMessage(ctx wshandler.ApiCallContext) { + chanid := ctx.Arguments[0].(string) + msg := ctx.Arguments[1].(string) + gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + chanid, + Body: map[string]any{"sender": ctx.CallBy.Alias, "msg": msg}, + Tag: []string{"TextMessage"}, + }) +} + +func (gc *groupChat) EnterPrivateChannel(ctx wshandler.ApiCallContext) { + typename := ctx.Arguments[0].(string) + channel := ctx.Arguments[1].(string) + var reason string + if len(ctx.Arguments) > 2 { + reason = ctx.Arguments[2].(string) + } + + if len(reason) > 0 { + // 수락 + ok, err := gc.rh.JSONSet(ctx.CallBy.Accid.Hex(), "$.channel."+typename, channel, gocommon.RedisonSetOptionNX) + if err != nil || !ok { + // 이미 다른 private channel 참여 중 + logger.Println("EnterPrivateChannel failed. HSetNX return err :", err, ctx.CallBy.Accid.Hex(), typename, channel) + return + } + gc.enterRoom(channel, ctx.CallBy.Accid) + } + + gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + channel, + Body: map[string]any{ + "sender": ctx.CallBy.Alias, + "msg": reason, + "typename": typename, + }, + Tag: []string{"EnterPrivateChannel"}, + }) +} + +func (gc *groupChat) LeavePrivateChannel(ctx wshandler.ApiCallContext) { + typename := ctx.Arguments[0].(string) + chanid := ctx.Arguments[1].(string) + cnt, _ := gc.rh.JSONDel(ctx.CallBy.Accid.Hex(), "$.channel."+typename) + if cnt > 0 { + gc.leaveRoom(chanid, ctx.CallBy.Accid) + gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + chanid, + Body: map[string]any{"sender": ctx.CallBy.Alias, "typename": typename}, + Tag: []string{"LeavePrivateChannel"}, + }) + } +} + +// func (gc *groupChat) ClientMessageReceived(sender *wshandler.Sender, mt wshandler.WebSocketMessageType, message any) { +// if mt == wshandler.Disconnected { +// if _, err := gc.rh.Del(gc.rh.Context(), accidHex(sender.Accid)).Result(); err != nil { +// logger.Println(err) +// } +// } else if mt == wshandler.BinaryMessage { +// commandline := message.([]any) +// cmd := commandline[0].(string) +// args := commandline[1:] +// switch cmd { +// case "EnterPublicChannel": +// chanid := args[0].(string) +// if cfg, ok := gc.chatConfig.Channels[chanid]; ok { +// size, err := gc.rh.JSONGetInt64(chanid, "$.size") +// if err != nil || len(size) == 0 { +// logger.Println("JSONGetInt64 failed :", chanid, err) +// } else if size[0] < cfg.Capacity { +// // 입장 +// newsize, err := gc.rh.JSONNumIncrBy(chanid, "$.size", 1) +// if err == nil { +// gc.enterRoom(chanid, sender.Accid) +// sender.RegistDisconnectedCallback(chanid, func() { +// size, err := gc.rh.JSONNumIncrBy(chanid, "$.size", -1) +// if err == nil { +// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ +// Target: "#" + chanid, +// Body: map[string]any{"size": size}, +// Tag: []string{"ChattingChannelProperties"}, +// }) +// } +// }) + +// gc.rh.HSet(gc.rh.Context(), accidHex(sender.Accid), "cc_pub", chanid) +// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ +// Target: "#" + chanid, +// Body: map[string]any{"size": newsize[0]}, +// Tag: []string{"ChattingChannelProperties"}, +// }) +// } +// } else { +// // 풀방 +// logger.Println("chatting channel is full :", chanid, size, cfg.Capacity) +// } +// } else { +// logger.Println("chatting channel not valid :", chanid) +// } + +// case "LeavePublicChannel": +// chanid := args[0].(string) +// gc.rh.HDel(gc.rh.Context(), accidHex(sender.Accid), "cc_pub") +// gc.leaveRoom(chanid, sender.Accid) +// if f := sender.PopDisconnectedCallback(chanid); f != nil { +// f() +// } + +// case "TextMessage": +// chanid := args[0].(string) +// msg := args[1].(string) +// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ +// Target: "#" + chanid, +// Body: map[string]any{"sender": sender.Alias, "msg": msg}, +// Tag: []string{"TextMessage"}, +// }) + +// case "EnterPrivateChannel": +// typename := args[0].(string) +// channel := args[1].(string) +// var reason string +// if len(args) > 2 { +// reason = args[2].(string) +// } + +// if len(reason) > 0 { +// // 수락 +// // 이거 HSet 하면 안되겠는데? JSONSet해야할 듯? +// ok, err := gc.rh.HSetNX(gc.rh.Context(), accidHex(sender.Accid), "cc_"+typename, channel).Result() +// if err != nil || !ok { +// // 이미 다른 private channel 참여 중 +// logger.Println("EnterPrivateChannel failed. HSetNX return err :", err, sender.Accid.Hex(), typename, channel) +// return +// } +// gc.enterRoom(channel, sender.Accid) + +// sender.RegistDisconnectedCallback(channel, func() { +// gc.rh.JSONDel(channel, "$."+sender.Accid.Hex()) +// // 이거 HDel 하면 안되겠는데? JSONDel해야할 듯? +// cnt, _ := gc.rh.HDel(gc.rh.Context(), accidHex(sender.Accid), "cc_"+typename).Result() +// if cnt > 0 { +// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ +// Target: "#" + channel, +// Body: map[string]any{"sender": sender.Alias, "typename": typename}, +// Tag: []string{"LeavePrivateChannel"}, +// }) +// } +// }) +// } else { +// // 내가 이미 private channel에 있다는 것을 다른 사람들에게 알려주기 위함 +// } + +// gc.sendUpstreamMessage(&wshandler.UpstreamMessage{ +// Target: "#" + channel, +// Body: map[string]any{ +// "sender": sender.Alias, +// "msg": reason, +// "typename": typename, +// }, +// Tag: []string{"EnterPrivateChannel"}, +// }) + +// case "LeavePrivateChannel": +// channel := args[1].(string) +// gc.leaveRoom(channel, sender.Accid) +// if f := sender.PopDisconnectedCallback(channel); f != nil { +// f() +// } +// } +// } +// } + +func (gc *groupChat) FetchChattingChannels(w http.ResponseWriter, r *http.Request) { + var prefix string + if err := gocommon.MakeDecoder(r).Decode(&prefix); err != nil { + logger.Println("FetchChattingChannels failed. ReadJsonDocumentFromBody returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if len(prefix) == 0 { + logger.Println("FetchChattingChannel failed. prefix is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + var rows []string + for name, cfg := range gc.chatConfig.Channels { + if len(prefix) > 0 { + if !strings.HasPrefix(name, prefix) { + continue + } + } + + onechan, err := gc.rh.JSONGet(name, "$") + if err != nil && err != redis.Nil { + logger.Println("FetchChattingChannel failed. HGetAll return err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if err == redis.Nil || onechan == nil { + rows = append(rows, cfg.emptyJson) + } else { + // json array로 나온다 + rows = append(rows, strings.Trim(onechan.(string), "[]")) + } + } + + gocommon.MakeEncoder(w, r).Encode(rows) +} + +func (gc *groupChat) QueryPlayerChattingChannel(w http.ResponseWriter, r *http.Request) { + var accid primitive.ObjectID + if err := gocommon.MakeDecoder(r).Decode(&accid); err != nil { + logger.Println("QueryPlayerChattingChannel failed. ReadJsonDocumentFromBody returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + sub, err := gc.rh.JSONGetDocuments(accid.Hex(), "$.channel") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if len(sub) > 0 { + gocommon.MakeEncoder(w, r).Encode(sub[0]) + } +} + +func (gc *groupChat) SendMessageOnChannel(w http.ResponseWriter, r *http.Request) { + var msg wshandler.UpstreamMessage + if err := gocommon.MakeDecoder(r).Decode(&msg); err != nil { + logger.Println("SendMessageOnChannel failed. ReadJsonDocumentFromBody return err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + gc.sendUpstreamMessage(&msg) +} diff --git a/core/group_party.go b/core/group_party.go new file mode 100644 index 0000000..631df34 --- /dev/null +++ b/core/group_party.go @@ -0,0 +1,822 @@ +package core + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "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 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() +} + +func midFromTid(gid groupID, in string) accountID { + h, _ := primitive.ObjectIDFromHex(in) + + var out accountID + for i := range h { + out[12-i-1] = gid[i] ^ h[12-i-1] + } + return out +} + +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 bson.M `json:"_body"` + Invite bool `json:"_invite"` + InviteExpire int64 `json:"_invite_exp"` +} + +type InvitationFail bson.M + +type groupDoc struct { + Members map[string]any `json:"_members"` + InCharge string `json:"_incharge"` + Gid string `json:"_gid"` + + rh *gocommon.RedisonHandler + id groupID +} + +func (gd *groupDoc) loadMemberFull(tid string) (bson.M, error) { + full, err := gd.rh.JSONGet(gd.strid(), "$._members."+tid) + if err != nil { + return nil, err + } + + bt := []byte(full.(string)) + bt = bt[1 : len(bt)-1] + + var doc bson.M + if err = json.Unmarshal(bt, &doc); err != nil { + return nil, err + } + + return doc, nil +} + +func (gd *groupDoc) loadFull() (doc bson.M) { + // 새 멤버에 그룹 전체를 알림 + full, err := gd.rh.JSONGet(gd.strid(), "$") + if err == nil { + bt := []byte(full.(string)) + bt = bt[1 : len(bt)-1] + err = json.Unmarshal(bt, &doc) + if err != nil { + logger.Println("loadFull err :", err) + } + } else { + logger.Println("loadFull err :", err) + } + return +} + +func (gd *groupDoc) strid() string { + if len(gd.Gid) == 0 { + gd.Gid = gd.id.Hex() + } + return gd.Gid +} + +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(mid accountID, body bson.M, ttl time.Duration, max int) (*memberDoc, error) { + targetmid := mid + targetbody := body + + // 초대 가능한 빈 자리가 있나 + 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, character bson.M) (bson.M, error) { + tid := gd.tid(mid) + prefix := "$._members." + tid + + if _, err := gd.rh.JSONSet(gd.strid(), prefix+"._body", character, gocommon.RedisonSetOptionXX); err != nil { + return nil, err + } + + if err := gd.rh.JSONMDel(gd.strid(), []string{prefix + "._invite", prefix + "._invite_exp"}); err != nil { + return nil, err + } + gd.rh.Persist(gd.rh.Context(), gd.strid()).Result() + + return gd.loadMemberFull(tid) +} + +func (gd *groupDoc) removeMemberByTid(tid string) error { + _, err := gd.rh.JSONDel(gd.strid(), "$._members."+tid) + if err != nil { + return err + } + + counts, err := gd.rh.JSONObjLen(gd.strid(), "$._members") + if err != nil { + return err + } + + if len(counts) > 0 && counts[0] == 0 { + _, err = gd.rh.Del(gd.rh.Context(), gd.strid()).Result() + } + + return err +} + +func (gd *groupDoc) removeMember(mid accountID) error { + return gd.removeMemberByTid(gd.tid(mid)) +} + +func (gd *groupDoc) getMembers() (map[string]any, error) { + res, err := gd.rh.JSONGet(gd.strid(), "$._members") + if err != nil { + return nil, err + } + + var temp []map[string]any + err = json.Unmarshal([]byte(res.(string)), &temp) + if err != nil { + return nil, err + } + + out := make(map[string]any) + for k, v := range temp[0] { + body := v.(map[string]any)["_body"] + out[gd.mid(k).Hex()] = body + } + + return out, nil +} + +type partyConfig struct { + InviteExpire int32 `json:"invite_ttl"` // 그룹이 개인에게 보낸 초대장 만료 기한 + MaxMember int `json:"max_member"` + + Name string +} + +type groupParty struct { + partyConfig + sendUpstreamMessage func(*wshandler.UpstreamMessage) + enterRoom func(groupID, accountID) + leaveRoom func(groupID, accountID) + rh *gocommon.RedisonHandler +} + +func (gp *groupParty) Initialize(tv *Tavern, cfg configDocument) error { + rem, _ := json.Marshal(cfg) + err := json.Unmarshal(rem, &gp.partyConfig) + if err != nil { + return err + } + + gp.rh = tv.redison + gp.sendUpstreamMessage = func(msg *wshandler.UpstreamMessage) { + tv.wsh.SendUpstreamMessage(msg) + } + gp.enterRoom = func(gid groupID, accid accountID) { + tv.wsh.EnterRoom(gid.Hex(), accid) + } + gp.leaveRoom = func(gid groupID, accid accountID) { + tv.wsh.LeaveRoom(gid.Hex(), accid) + } + + return nil +} + +func (gp *groupParty) RegisterApiFunctions() { + +} + +// JoinParty : 그룹에 참가 +// - type : 그룹 타입 +// - 그룹 타입에 맞는 키(주로 _id) +// - member_id : 참가 멤버의 아이디 +// - body : 멤버의 속성 bson document +func (gp *groupParty) JoinParty(w http.ResponseWriter, r *http.Request) { + var data struct { + Gid primitive.ObjectID + Mid primitive.ObjectID + First bool + Character bson.M + } + if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { + logger.Println("JoinParty failed. DecodeGob returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + character := data.Character + gid := data.Gid + mid := data.Mid + + if gid.IsZero() || mid.IsZero() { + logger.Println("JoinParty failed. mid should be exist") + w.WriteHeader(http.StatusBadRequest) + return + } + + gd, err := gp.find(gid) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + if gd == nil { + // 그룹이 없다. 실패 + w.WriteHeader(http.StatusBadRequest) + return + } + + // 내 정보 업데이트할 때에도 사용됨 + if data.First { + if memdoc, err := gd.addMember(mid, character); err == nil { + // 기존 유저에게 새 유저 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gid.Hex(), + Body: map[string]any{ + gd.tid(mid): memdoc, + }, + Tag: []string{"MemberDocFull"}, + }) + + gp.enterRoom(gid, mid) + + // 최초 입장이라면 새 멤버에 그룹 전체를 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: mid.Hex(), + Body: gd.loadFull(), + Tag: []string{"GroupDocFull"}, + }) + } else if err != nil { + logger.Error("JoinParty failed :", err) + w.WriteHeader(http.StatusInternalServerError) + } + } else { + path := "$._members." + gd.tid(mid) + "._body" + if _, err := gd.rh.JSONSet(gd.strid(), path, character, gocommon.RedisonSetOptionXX); err != nil { + logger.Error("JoinParty failed :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // 기존 유저에게 캐릭터 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gid.Hex(), + Body: map[string]any{ + gd.tid(mid): bson.M{ + "_body": character, + }, + }, + Tag: []string{"MemberDocFragment"}, + }) + + } +} + +// InviteToParty : 초대 +// - type : 초대 타입 (required) +// - from : 초대하는 자 (required) +// - to : 초대받는 자 (required) +// - timeout : 초대 유지시간(optional. 없으면 config 기본 값) +// - (body) : 검색시 노출되는 document +func (gp *groupParty) InviteToParty(w http.ResponseWriter, r *http.Request) { + var doc struct { + Gid primitive.ObjectID + Mid primitive.ObjectID + Target primitive.ObjectID + Inviter bson.M + Invitee bson.M + } + + if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil { + logger.Println("InviteToParty failed. DecodeGob returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + targetid := doc.Target + gid := doc.Gid + mid := doc.Mid + + // targetid에 초대한 mid가 들어있다. + success, err := gp.rh.SetNX(context.Background(), "inv."+targetid.Hex(), mid.Hex(), time.Duration(gp.InviteExpire)*time.Second).Result() + if err != nil { + logger.Println("InviteToParty failed. gp.rh.SetNX() return err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if !success { + // 이미 초대 중이다. + // inviter한테 알려줘야 한다. + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: mid.Hex(), + Body: doc.Invitee, + Tag: []string{"InvitationFail"}, + }) + return + } + + gd, err := gp.find(gid) + if err != nil { + logger.Println("InviteToParty failed. gp.find() return err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if gd == nil { + gid = primitive.NewObjectID() + gd, err = gp.createGroup(gid, mid, doc.Inviter) + if err != nil { + logger.Println("InviteToParty failed. gp.createGroup() return err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + // 내가 wshandler room에 입장 + gp.enterRoom(gid, mid) + gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()}) + + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: mid.Hex(), + Body: gd, + Tag: []string{"GroupDocFull"}, + }) + } + + newdoc, err := gd.addInvite(targetid, doc.Invitee, time.Duration(gp.InviteExpire+1)*time.Second, gp.MaxMember) + if err != nil { + logger.Println("InviteToParty failed. gp.addInvite() return err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // invitee에게 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: targetid.Hex(), + Body: Invitation{ + GroupID: gid, + TicketID: gd.tid(targetid), + Inviter: doc.Inviter, + ExpireAtUTC: newdoc.InviteExpire, + }, + Tag: []string{"Invitation"}, + }) +} + +func (gp *groupParty) AcceptPartyInvitation(w http.ResponseWriter, r *http.Request) { + var doc struct { + Gid primitive.ObjectID + Mid primitive.ObjectID + Tid string + Character bson.M + } + if err := gocommon.MakeDecoder(r).Decode(&doc); err != nil { + logger.Println("AcceptPartyInvitation failed. DecodeGob returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + gid := doc.Gid + mid := doc.Mid + member := doc.Character + + cnt, err := gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result() + if err != nil { + logger.Error("AcceptPartyInvitation failed. gp.rh.Del returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if cnt == 0 { + // 만료됨 + w.WriteHeader(http.StatusGatewayTimeout) + return + } + + pids, err := gp.rh.JSONGetString(mid.Hex(), "$.party.id") + if err != nil { + logger.Error("AcceptPartyInvitation failed. gp.rh.JSONGetString returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if len(pids) > 0 && len(pids[0]) > 0 { + // 기존에 이미 파티에 들어가 있다. + // 기존 파티에서는 탈퇴 + oldgid, _ := primitive.ObjectIDFromHex(pids[0]) + oldgd := &groupDoc{ + id: oldgid, + rh: gp.rh, + } + + // gid에는 제거 메시지 보냄 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + oldgd.strid(), + Body: bson.M{ + oldgd.tid(mid): bson.M{}, + }, + Tag: []string{"MemberDocFull"}, + }) + + gp.leaveRoom(oldgid, mid) + } + + gd := &groupDoc{ + id: gid, + rh: gp.rh, + } + + memberDoc, err := gd.addMember(mid, member) + if err == nil { + // 기존 멤버에게 새 멤버를 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gid.Hex(), + Body: map[string]any{ + gd.tid(mid): memberDoc, + }, + Tag: []string{"MemberDocFull"}, + }) + + gp.enterRoom(gid, mid) + + // 현재 내 파티를 기록 + gp.rh.JSONSet(mid.Hex(), "$.party", bson.M{"id": gid.Hex()}) + + // 새 멤버에 그룹 전체를 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: mid.Hex(), + Body: gd.loadFull(), + Tag: []string{"GroupDocFull"}, + }) + } else { + logger.Println("AcceptPartyInvitation failed. group.AcceptPartyInvitation returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + } +} + +func (gp *groupParty) QueryPartyMemberState(w http.ResponseWriter, r *http.Request) { + var mid primitive.ObjectID + if err := gocommon.MakeDecoder(r).Decode(&mid); err != nil { + logger.Println("DenyPartyInvitation failed. DecodeGob returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if cnt, _ := gp.rh.Exists(gp.rh.Context(), mid.Hex()).Result(); cnt == 0 { + return + } + + states, _ := gp.rh.JSONGetString(mid.Hex(), "$.party.state") + if len(states) > 0 && len(states[0]) > 0 { + gocommon.MakeEncoder(w, r).Encode(states[0]) + } else { + gocommon.MakeEncoder(w, r).Encode("connected") + } +} + +func (gp *groupParty) updateMemberDocument(gid groupID, mid accountID, doc bson.M) error { + gd := &groupDoc{ + id: gid, + rh: gp.rh, + } + prefixPath := fmt.Sprintf("$._members.%s.", gd.tid(mid)) + err := gp.rh.JSONMSetRel(gd.strid(), prefixPath, doc) + if err != nil { + return err + } + + if newstate, ok := doc["_state"]; ok { + gp.rh.JSONSet(mid.Hex(), "$.party.state", newstate) + } + + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gid.Hex(), + Body: map[string]any{ + gd.tid(mid): doc, + }, + Tag: []string{"MemberDocFragment"}, + }) + return nil +} + +func (gp *groupParty) updatePartyDocument(gid groupID, frag bson.M) error { + gd := groupDoc{ + id: gid, + rh: gp.rh, + } + if err := gp.rh.JSONMSetRel(gd.strid(), "$.", frag); err != nil { + return err + } + // 업데이트 알림 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gid.Hex(), + Body: frag, + Tag: []string{"GroupDocFragment"}, + }) + return nil +} + +func (gp *groupParty) UpdatePartyDocument(w http.ResponseWriter, r *http.Request) { + var data struct { + Gid primitive.ObjectID + Doc bson.M + } + + if err := gocommon.MakeDecoder(r).Decode(&data); err != nil { + logger.Println("UpdatePartyDocument failed. DecodeGob returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + gid := data.Gid + frag := data.Doc + + if err := gp.updatePartyDocument(gid, frag); err != nil { + logger.Error("UpdatePartyDocument failed. group.UpdatePartyDocument returns err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } +} + +func (gp *groupParty) QueryPartyMembers(w http.ResponseWriter, r *http.Request) { + var gid primitive.ObjectID + if err := gocommon.MakeDecoder(r).Decode(&gid); err != nil { + logger.Println("QueryPartyMembers failed. DecodeGob returns err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + gd := groupDoc{ + id: gid, + rh: gp.rh, + } + + members, err := gd.getMembers() + if err != nil { + logger.Error("QueryPartyMembers failed. group.QueryPartyMembers returns err :", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + if err := gocommon.MakeEncoder(w, r).Encode(members); err != nil { + logger.Error("QueryPartyMembers failed. writeBsonDoc return err :", err) + w.WriteHeader(http.StatusInternalServerError) + return + } +} + +func (gp *groupParty) createGroup(newid groupID, charge accountID, chargeDoc bson.M) (*groupDoc, error) { + tid := makeTid(newid, charge) + + gd := &groupDoc{ + Members: map[string]any{ + tid: &memberDoc{ + Body: chargeDoc, + Invite: false, + InviteExpire: 0, + }, + }, + InCharge: tid, + + rh: gp.rh, + id: newid, + } + + _, err := gp.rh.JSONSet(gd.strid(), "$", gd, gocommon.RedisonSetOptionNX) + if err != nil { + return nil, err + } + return gd, nil +} + +func (gp *groupParty) find(id groupID) (*groupDoc, error) { + if id.IsZero() { + return nil, nil + } + + _, err := gp.rh.JSONObjLen(id.Hex(), "$") + if err == redis.Nil { + return nil, nil + } + if err != nil { + return nil, err + } + + return &groupDoc{ + rh: gp.rh, + id: id, + }, nil +} + +func (gp *groupParty) ClientDisconnected(ctx wshandler.ApiCallContext) { + gids, _ := gp.rh.JSONGetString(ctx.CallBy.Accid.Hex(), "$.party.id") + + if len(gids) > 0 && len(gids[0]) > 0 { + // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 + gidstr := gids[0] + gid, _ := primitive.ObjectIDFromHex(gidstr) + + // 나를 먼저 룸에서 빼야 나한테 메시지가 안감 + gp.leaveRoom(gid, ctx.CallBy.Accid) + + // gid에는 제거 메시지 보냄 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gidstr, + Body: bson.M{ + makeTid(gid, ctx.CallBy.Accid): bson.M{}, + }, + Tag: []string{"MemberDocFull"}, + }) + + } +} + +func (gp *groupParty) UpdatePartyMemberDocumentDirect(ctx wshandler.ApiCallContext) { + gidobj, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) + doc := ctx.Arguments[1].(map[string]any) + + gp.updateMemberDocument(gidobj, ctx.CallBy.Accid, doc) +} + +func (gp *groupParty) UpdatePartyDocumentDirect(ctx wshandler.ApiCallContext) { + // 파티 오너만 가능 + gidobj, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) + doc := ctx.Arguments[1].(map[string]any) + + gd := groupDoc{ + id: gidobj, + rh: gp.rh, + } + + incharge, err := gp.rh.JSONGet(gd.strid(), "$._incharge") + if err != nil { + logger.Println("UpdatePartyDocumentDirect failed. gp.rh.JSONGet returns err :", err) + return + } + + if !strings.Contains(incharge.(string), gd.tid(ctx.CallBy.Accid)) { + // incharge가 아니네? + logger.Println("UpdatePartyDocumentDirect failed. caller is not incharge") + return + } + + gp.updatePartyDocument(gidobj, doc) +} + +func (gp *groupParty) LeaveParty(ctx wshandler.ApiCallContext) { + gids, _ := gp.rh.JSONGetString(ctx.CallBy.Accid.Hex(), "$.party.id") + + if len(gids) == 0 || len(gids[0]) == 0 { + return + } + // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 + gidstr := gids[0] + gid, _ := primitive.ObjectIDFromHex(gidstr) + mid := ctx.CallBy.Accid + tid := ctx.Arguments[0].(string) + + gd := groupDoc{ + id: gid, + rh: gp.rh, + } + + var err error + if len(tid) > 0 { + if tid != gd.tid(mid) { + // mid가 incharge여야 한다. 그래야 tid를 쫓아낼 수 있음 + incharge, err := gp.rh.JSONGet(gd.strid(), "$._incharge") + if err != nil { + logger.Println("LeaveParty failed. gp.rh.JSONGet returns err :", err) + return + } + if !strings.Contains(incharge.(string), gd.tid(mid)) { + // incharge가 아니네? + logger.Println("LeaveParty failed. mid is not incharge") + return + } + mid = midFromTid(gd.id, tid) + } + err = gd.removeMemberByTid(tid) + } else { + err = gd.removeMember(mid) + // 내가 나갔다 + gp.rh.JSONDel(mid.Hex(), "$.party.id") + } + + if err != nil { + logger.Println("LeaveParty failed. gd.removeMember returns err :", err) + return + } + + // mid한테는 빈 GroupDocFull을 보낸다. 그러면 지워짐 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: mid.Hex(), + Body: bson.M{"gid": gid}, + Tag: []string{"GroupDocFull", gid.Hex()}, + }) + + // gid에는 제거 메시지 보냄 + gp.sendUpstreamMessage(&wshandler.UpstreamMessage{ + Target: "#" + gd.strid(), + Body: bson.M{ + tid: bson.M{}, + }, + Tag: []string{"MemberDocFull"}, + }) + + gp.leaveRoom(gid, mid) +} + +func (gp *groupParty) DenyPartyInvitation(ctx wshandler.ApiCallContext) { + gid, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) + mid := ctx.CallBy.Accid + + gp.rh.Del(context.Background(), "inv."+mid.Hex()).Result() + gd := groupDoc{ + id: gid, + rh: gp.rh, + } + gd.removeMember(mid) +} diff --git a/core/invitation.go b/core/invitation.go deleted file mode 100644 index 2789b7f..0000000 --- a/core/invitation.go +++ /dev/null @@ -1,383 +0,0 @@ -package core - -import ( - "context" - "encoding/gob" - "net/http" - "time" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "go.mongodb.org/mongo-driver/mongo/options" - "repositories.action2quare.com/ayo/gocommon" - "repositories.action2quare.com/ayo/gocommon/logger" - "repositories.action2quare.com/ayo/gocommon/wshandler" -) - -const ( - invitation_collection_name = gocommon.CollectionName("invitation") -) - -var invitation_sent_tag = []string{"social.InvitationsSent"} -var invitation_received_tag = []string{"social.InvitationsReceived"} -var friends_tag = []string{"social.Friends"} - -type invitation struct { - mongoClient gocommon.MongoClient - redison *gocommon.RedisonHandler - wsh *wshandler.WebsocketHandler - f *friends -} - -type invitationDoc struct { - Id primitive.ObjectID `bson:"_id,omitempty" json:"_id"` - From primitive.ObjectID `bson:"from,omitempty" json:"-"` - To primitive.ObjectID `bson:"to,omitempty" json:"-"` - FromAlias string `bson:"falias,omitempty" json:"from"` - ToAlias string `bson:"talias,omitempty" json:"to"` - Timestamp int64 `bson:"ts" json:"ts"` - Deleted bool `bson:"deleted,omitempty" json:"deleted,omitempty"` -} - -func init() { - gob.Register([]invitationDoc{}) -} - -func makeInvitation(ctx context.Context, s *Social, f *friends) (*invitation, error) { - if err := s.mongoClient.MakeUniqueIndices(invitation_collection_name, map[string]bson.D{ - "fromto": {{Key: "from", Value: 1}, {Key: "to", Value: 1}}, - }); err != nil { - return nil, err - } - - // 내가 받은거 - if err := s.mongoClient.MakeIndices(invitation_collection_name, map[string]bson.D{ - "received": {{Key: "to", Value: 1}, {Key: "ts", Value: -1}}, - }); err != nil { - return nil, err - } - - return &invitation{ - mongoClient: s.mongoClient, - redison: s.redison, - wsh: s.wsh, - f: f, - }, nil -} - -func (iv *invitation) QueryReceivedInvitations(ctx wshandler.ApiCallContext) { - // 내가 받은 초대 목록 - queryfrom := int64(ctx.Arguments[0].(float64)) - - var receives []*invitationDoc - - err := iv.mongoClient.FindAllAs(invitation_collection_name, bson.M{ - "to": ctx.CallBy.Accid, - "ts": bson.M{"$gt": queryfrom}, - }, &receives) - if err != nil { - logger.Println("QueryReceivedInvitations failed. FindAllAs err :", err) - } - - if len(receives) > 0 { - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ctx.CallBy.Accid.Hex(), - Body: receives, - Tag: invitation_received_tag, - }) - } -} - -func (iv *invitation) QuerySentInvitations(ctx wshandler.ApiCallContext) { - // 내가 보낸 초대 목록 - queryfrom := int64(ctx.Arguments[0].(float64)) - - var receives []*invitationDoc - - err := iv.mongoClient.FindAllAs(invitation_collection_name, bson.M{ - "from": ctx.CallBy.Accid, - "ts": bson.M{"$gt": queryfrom}, - "falias": bson.M{"$exists": true}, - }, &receives) - if err != nil { - logger.Println("QueryReceivedInvitations failed. FindAllAs err :", err) - } - - if len(receives) > 0 { - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ctx.CallBy.Accid.Hex(), - Body: receives, - Tag: invitation_sent_tag, - }) - } -} - -func (iv *invitation) CancelInvitation(ctx wshandler.ApiCallContext) { - // ctx.CallBy.Accid - id, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) - - var ivdoc invitationDoc - if err := iv.mongoClient.FindOneAs(invitation_collection_name, bson.M{ - "_id": id, - }, &ivdoc); err != nil { - logger.Println("CancelInvitation failed:", err) - return - } - - if ivdoc.From != ctx.CallBy.Accid { - return - } - - ivdoc.Deleted = true - if _, _, err := iv.mongoClient.Update(invitation_collection_name, bson.M{ - "_id": id, - }, bson.M{ - "$set": bson.M{ - "falias": "", - "deleted": true, - "ts": time.Now().UTC().Unix(), - }, - }); err != nil { - logger.Println("CancelInvitation failed:", err) - return - } - - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ivdoc.To.Hex(), - Body: []invitationDoc{ivdoc}, - Tag: invitation_received_tag, - }) - - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ivdoc.From.Hex(), - Body: []invitationDoc{ivdoc}, - Tag: invitation_sent_tag, - }) -} - -func (iv *invitation) AcceptInvitation(ctx wshandler.ApiCallContext) { - invId, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) - - var ivdoc invitationDoc - if err := iv.mongoClient.FindOneAs(invitation_collection_name, bson.M{ - "_id": invId, - }, &ivdoc); err != nil { - logger.Println("AcceptInvitation failed:", err) - return - } - - if ivdoc.Id != invId { - // 초대가 없다 - return - } - - if ivdoc.To != ctx.CallBy.Accid { - // 내가 받은 초대가 아니네? - return - } - - now := time.Now().UTC().Unix() - f1 := friendDoc{ - From: ivdoc.To, // 수락한 나 - To: ivdoc.From, // 상대방 - ToAlias: ivdoc.FromAlias, - Timestamp: now, - } - f2 := friendDoc{ - From: ivdoc.From, // 상대방 - To: ivdoc.To, // 나 - ToAlias: ivdoc.ToAlias, - Timestamp: now, - } - - // 나한테 상대방을 친구로 만들고 - if err := iv.f.addFriend(&f1); err == nil { - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: f1.From.Hex(), - Body: []friendDoc{f1}, - Tag: friends_tag, - }) - } else { - logger.Println("AcceptInvitation failed. addFriend(f1) err :", err) - return - } - - // 상대방한테 나를 친구로 만듬 - if err := iv.f.addFriend(&f2); err == nil { - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: f2.From.Hex(), - Body: []friendDoc{f2}, - Tag: friends_tag, - }) - } else { - logger.Println("AcceptInvitation failed. addFriend(f2) err :", err) - return - } - - iv.mongoClient.Update(invitation_collection_name, bson.M{ - "_id": invId, - }, bson.M{ - "$set": bson.M{ - "deleted": true, - "ts": now, - }, - }, options.Update().SetUpsert(false)) -} - -func (iv *invitation) DenyInvitation(ctx wshandler.ApiCallContext) { - invId, _ := primitive.ObjectIDFromHex(ctx.Arguments[0].(string)) - - var ivdoc invitationDoc - if err := iv.mongoClient.FindOneAs(invitation_collection_name, bson.M{ - "_id": invId, - }, &ivdoc); err != nil { - logger.Println("AcceptInvitation failed:", err) - return - } - - if ivdoc.Id != invId { - // 초대가 없다 - return - } - - if ivdoc.To != ctx.CallBy.Accid { - // 내가 받은 초대가 아니네? - return - } - - now := time.Now().UTC().Unix() - ivdoc.Timestamp = now - ivdoc.Deleted = true - if _, _, err := iv.mongoClient.Update(invitation_collection_name, bson.M{ - "_id": invId, - }, bson.M{ - "$set": bson.M{ - "deleted": true, - "ts": now, - }, - }, options.Update().SetUpsert(false)); err != nil { - logger.Println("DenyInvitation failed. addFriend(f2) err :", err) - return - } - - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ivdoc.To.Hex(), - Body: []invitationDoc{ivdoc}, - Tag: invitation_received_tag, - }) - - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ivdoc.From.Hex(), - Body: []invitationDoc{ivdoc}, - Tag: invitation_sent_tag, - }) -} - -func (iv *invitation) Trim(ctx wshandler.ApiCallContext) { - stringsTobjs := func(in []any) (out []primitive.ObjectID) { - for _, i := range in { - p, _ := primitive.ObjectIDFromHex(i.(string)) - out = append(out, p) - } - return - } - - ids := stringsTobjs(ctx.Arguments[0].([]any)) - ids = append(ids, stringsTobjs(ctx.Arguments[1].([]any))...) - if len(ids) > 0 { - if len(ids) == 1 { - iv.mongoClient.Delete(invitation_collection_name, bson.M{"_id": ids[0], "deleted": true}) - } else { - iv.mongoClient.DeleteMany(invitation_collection_name, bson.D{ - {Key: "_id", Value: bson.M{"$in": ids}}, - {Key: "deleted", Value: true}, - }) - } - } -} - -func (iv *invitation) InviteAsFriend(w http.ResponseWriter, r *http.Request) { - // 1. mongodb에 추가 - // 1-1. block이 되어있다면(==이미 도큐먼트가 있다면) 마치 성공인 것처럼 아무것도 안하고 끝 - // 2. mongodb에 추가가 성공하면 publish - var ivdoc invitationDoc - - if err := gocommon.MakeDecoder(r).Decode(&ivdoc); err != nil { - logger.Println("IniviteAsFriend failed:", err) - w.WriteHeader(http.StatusBadRequest) - return - } - - ivdoc.Timestamp = time.Now().UTC().Unix() - _, newid, err := iv.mongoClient.Update(invitation_collection_name, bson.M{ - "from": ivdoc.From, - "to": ivdoc.To, - }, bson.M{ - "$set": bson.M{ - "ts": ivdoc.Timestamp, - "falias": ivdoc.FromAlias, - "talias": ivdoc.ToAlias, - }, - }, options.Update().SetUpsert(true)) - if err != nil { - logger.Println("IniviteAsFriend failed:", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if newid != nil { - ivdoc.Id = newid.(primitive.ObjectID) - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ivdoc.To.Hex(), - Body: []invitationDoc{ivdoc}, - Tag: invitation_received_tag, - }) - } else { - found, _ := iv.mongoClient.FindOne(invitation_collection_name, bson.M{ - "from": ivdoc.From, - "to": ivdoc.To, - }, options.FindOne().SetProjection(bson.M{"_id": 1})) - - ivdoc.Id = found["_id"].(primitive.ObjectID) - } - - if !ivdoc.Id.IsZero() { - iv.wsh.SendUpstreamMessage(&wshandler.UpstreamMessage{ - Target: ivdoc.From.Hex(), - Body: []invitationDoc{ivdoc}, - Tag: invitation_sent_tag, - }) - } -} - -func (iv *invitation) Block(w http.ResponseWriter, r *http.Request) { - // 초대가 있으면 - // var bi struct { - // From primitive.ObjectID - // To primitive.ObjectID - // FromAlias string - // } - // if err := gocommon.MakeDecoder(r).Decode(&bi); err != nil { - // logger.Println("invitation.Block failed :", err) - // w.WriteHeader(http.StatusBadRequest) - // return - // } - - // now := time.Now().UTC().Unix() - // // From이 To를 block했으므로 To가 From을 초대하는 것을 방지하려면 둘을 뒤집어서 문서를 만들어 놔야 함 - // // 이미 존재하는 초대일 수도 있다. - // _, _, err := iv.mongoClient.Update(invitation_collection_name, bson.M{ - // "from": bi.To, - // "to": bi.From, - // }, bson.M{ - // "$set": invitationDoc{ - // ToAlias: bi.FromAlias, - // Timestamp: now, - // }, - // }, options.Update().SetUpsert(true)) - // if err != nil { - // logger.Println("Block failed:", err) - // w.WriteHeader(http.StatusInternalServerError) - // return - // } -} diff --git a/core/social.go b/core/social.go deleted file mode 100644 index eaba2c7..0000000 --- a/core/social.go +++ /dev/null @@ -1,134 +0,0 @@ -package core - -import ( - "context" - "io" - "net/http" - - "github.com/go-redis/redis/v8" - "repositories.action2quare.com/ayo/gocommon" - "repositories.action2quare.com/ayo/gocommon/flagx" - "repositories.action2quare.com/ayo/gocommon/logger" - "repositories.action2quare.com/ayo/gocommon/session" - "repositories.action2quare.com/ayo/gocommon/wshandler" -) - -var devflag = flagx.Bool("dev", false, "") - -type SocialConfig struct { - session.SessionConfig `json:",inline"` - - MaingateApiToken string `json:"maingate_api_token"` - RedisURL string `json:"social_redis_url"` - MongoURL string `json:"social_storage_url"` -} - -var config SocialConfig - -type Social struct { - wsh *wshandler.WebsocketHandler - mongoClient gocommon.MongoClient - redison *gocommon.RedisonHandler - httpApiBorker gocommon.HttpApiBroker -} - -// New : -func New(ctx context.Context, wsh *wshandler.WebsocketHandler, inconfig *SocialConfig) (*Social, error) { - if inconfig == nil { - var loaded SocialConfig - if err := gocommon.LoadConfig(&loaded); err != nil { - return nil, err - } - inconfig = &loaded - } - - config = *inconfig - opt, err := redis.ParseURL(config.RedisURL) - if err != nil { - return nil, logger.ErrorWithCallStack(err) - } - - mc, err := gocommon.NewMongoClient(ctx, config.MongoURL) - if err != nil { - return nil, logger.ErrorWithCallStack(err) - } - - so := &Social{ - wsh: wsh, - redison: gocommon.NewRedisonHandler(ctx, redis.NewClient(opt)), - mongoClient: mc, - } - - if err := so.prepare(ctx); err != nil { - logger.Println("social prepare() failed :", err) - return nil, logger.ErrorWithCallStack(err) - } - - return so, nil -} - -func (so *Social) Cleanup() { - so.mongoClient.Close() -} - -func (so *Social) prepare(ctx context.Context) error { - redisClient, err := gocommon.NewRedisClient(config.RedisURL) - if err != nil { - return logger.ErrorWithCallStack(err) - } - - so.redison = gocommon.NewRedisonHandler(redisClient.Context(), redisClient) - - friends, err := makeFriends(ctx, so) - if err != nil { - return logger.ErrorWithCallStack(err) - } - so.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(friends, "social")) - so.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(friends, "social")) - - invitation, err := makeInvitation(ctx, so, friends) - if err != nil { - return logger.ErrorWithCallStack(err) - } - so.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(invitation, "social")) - so.httpApiBorker.AddHandler(gocommon.MakeHttpApiHandler(invitation, "social")) - - return nil -} - -func (so *Social) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error { - so.wsh.AddHandler(wshandler.MakeWebsocketApiHandler(so, "social")) - pattern := gocommon.MakeHttpHandlerPattern(prefix, "api") - serveMux.HandleFunc(pattern, so.api) - - return nil -} - -func (so *Social) api(w http.ResponseWriter, r *http.Request) { - defer func() { - s := recover() - if s != nil { - logger.Error(s) - } - io.Copy(io.Discard, r.Body) - r.Body.Close() - }() - - // 서버에서 오는 요청만 처리 - apitoken := r.Header.Get("MG-X-API-TOKEN") - if apitoken != config.MaingateApiToken { - // 서버가 보내는 쿼리만 허용 - logger.Println("MG-X-API-TOKEN is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - funcname := r.URL.Query().Get("call") - if len(funcname) == 0 { - logger.Println("query param 'call' is missing") - w.WriteHeader(http.StatusBadRequest) - return - } - - so.httpApiBorker.Call(funcname, w, r) -} diff --git a/core/social_test.go b/core/social_test.go deleted file mode 100644 index adc8130..0000000 --- a/core/social_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// warroom project main.go -package core - -import ( - "context" - "encoding/binary" - "fmt" - "testing" - "time" - - "github.com/go-redis/redis/v8" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -func TestPubSub(t *testing.T) { - opt0, _ := redis.ParseURL("redis://192.168.8.94:6380/0") - opt1, _ := redis.ParseURL("redis://192.168.8.94:6380/1") - - rc0 := redis.NewClient(opt0) - rc1 := redis.NewClient(opt1) - - go func() { - time.Sleep(time.Second) - rc1.Publish(context.Background(), "__testchan", "real???") - fmt.Println("published") - }() - - pubsub := rc0.Subscribe(context.Background(), "__testchan") - msg, err := pubsub.ReceiveMessage(context.Background()) - fmt.Println(msg.Payload, err) -} -func makeHash(chanName string, index uint32) string { - for len(chanName) < 12 { - chanName += chanName - } - left := chanName[:6] - right := chanName[len(chanName)-6:] - base := []byte(left + right) - for i := 0; i < 12; i++ { - base[i] += base[12-i-1] - } - - bs := make([]byte, 4) - binary.LittleEndian.PutUint32(bs, index) - for i, c := range bs { - base[i] ^= c - } - var gid primitive.ObjectID - copy(gid[:], base) - - return gid.Hex() -} - -func TestNameHash(t *testing.T) { - for i := 0; i < 10; i++ { - makeHash("Urud", uint32(i)) - fmt.Printf("Urud:%d - %s\n", i, makeHash("Urud", uint32(i))) - makeHash("Sheldon", uint32(i)) - fmt.Printf("Sheldon:%d - %s\n", i, makeHash("Sheldon", uint32(i))) - } -} - -func TestReJSON(t *testing.T) { - -} diff --git a/core/tavern.go b/core/tavern.go new file mode 100644 index 0000000..0d7ab88 --- /dev/null +++ b/core/tavern.go @@ -0,0 +1,193 @@ +package core + +import ( + "context" + "errors" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/go-redis/redis/v8" + "repositories.action2quare.com/ayo/gocommon" + "repositories.action2quare.com/ayo/gocommon/flagx" + "repositories.action2quare.com/ayo/gocommon/logger" + "repositories.action2quare.com/ayo/gocommon/session" + "repositories.action2quare.com/ayo/gocommon/wshandler" + + "go.mongodb.org/mongo-driver/bson" +) + +var devflag = flagx.Bool("dev", false, "") + +type TavernConfig struct { + session.SessionConfig `json:",inline"` + Group map[string]configDocument `json:"tavern_group_types"` + MaingateApiToken string `json:"maingate_api_token"` + RedisURL string `json:"tavern_redis_url"` + macAddr string +} + +var config TavernConfig + +type Tavern struct { + wsh *wshandler.WebsocketHandler + mongoClient gocommon.MongoClient + redison *gocommon.RedisonHandler + httpApiBorker gocommon.HttpApiHandlerContainer +} + +func getMacAddr() (string, error) { + ifas, err := net.Interfaces() + if err != nil { + return "", err + } + + for _, ifa := range ifas { + a := ifa.HardwareAddr.String() + if a != "" { + a = strings.ReplaceAll(a, ":", "") + return a, nil + } + } + return "", errors.New("no net interface") +} + +// New : +func New(context context.Context, wsh *wshandler.WebsocketHandler) (*Tavern, error) { + if err := gocommon.LoadConfig(&config); err != nil { + return nil, err + } + + macaddr, err := getMacAddr() + if err != nil { + return nil, err + } + config.macAddr = macaddr + tv := &Tavern{ + wsh: wsh, + } + + if err = tv.prepare(context); err != nil { + logger.Println("tavern prepare() failed :", err) + return nil, err + } + + return tv, nil +} + +func (tv *Tavern) Cleanup() { + tv.mongoClient.Close() +} + +func (tv *Tavern) prepare(ctx context.Context) error { + redisClient, err := gocommon.NewRedisClient(config.RedisURL) + if err != nil { + return err + } + + tv.redison = gocommon.NewRedisonHandler(redisClient.Context(), redisClient) + tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(tv, "tv")) + + if cfg, ok := config.Group["chat"]; ok { + chat := new(groupChat) + if err := chat.Initialize(tv, cfg); err != nil { + return err + } + tv.httpApiBorker.RegisterApiHandler(gocommon.MakeHttpApiHandler(chat, "chat")) + tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(chat, "chat")) + } + + if cfg, ok := config.Group["party"]; ok { + party := new(groupParty) + if err := party.Initialize(tv, cfg); err != nil { + return err + } + tv.httpApiBorker.RegisterApiHandler(gocommon.MakeHttpApiHandler(party, "party")) + tv.wsh.RegisterApiHandler(wshandler.MakeWebsocketApiHandler(party, "party")) + } + + return nil +} + +func (tv *Tavern) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error { + // tv.wsh.RegisterReceiver(tv) + pattern := gocommon.MakeHttpHandlerPattern(prefix, "api") + serveMux.HandleFunc(pattern, tv.api) + + return nil +} + +func (tv *Tavern) EnterChannel(ctx wshandler.ApiCallContext) { + tv.wsh.EnterRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid) +} + +func (tv *Tavern) LeaveChannel(ctx wshandler.ApiCallContext) { + tv.wsh.LeaveRoom(ctx.Arguments[0].(string), ctx.CallBy.Accid) +} + +func (tv *Tavern) ClientConnected(ctx wshandler.ApiCallContext) { + logger.Println("ClientConnected :", ctx.CallBy.Alias) + tv.redison.Del(tv.redison.Context(), ctx.CallBy.Accid.Hex()) + _, err := tv.redison.JSONSet(ctx.CallBy.Accid.Hex(), "$", bson.M{"_ts": time.Now().UTC().Unix()}) + if err != nil { + logger.Println("OnClientMessageReceived HSet error :", err) + } +} + +func (tv *Tavern) ClientDisconnected(ctx wshandler.ApiCallContext) { + tv.redison.Del(tv.redison.Context(), ctx.CallBy.Accid.Hex()).Result() + logger.Println("ClientDisconnected :", ctx.CallBy.Alias) +} + +func (tv *Tavern) OnRoomCreated(name string) { + cnt, err := tv.redison.IncrBy(tv.redison.Context(), "_ref_"+name, 1).Result() + if err != nil && !errors.Is(err, redis.Nil) { + logger.Println("OnRoomCreated JSONSet failed :", err) + return + } + + if cnt == 1 { + tv.redison.JSONSet(name, "$", map[string]any{}, gocommon.RedisonSetOptionNX) + } +} + +func (tv *Tavern) OnRoomDestroyed(name string) { + cnt, err := tv.redison.IncrBy(tv.redison.Context(), "_ref_"+name, -1).Result() + if err != nil { + logger.Println("OnRoomDestroyed JSONNumIncrBy failed :", err) + } else if cnt == 0 { + tv.redison.Del(tv.redison.Context(), "_ref_"+name) + tv.redison.JSONDel(name, "$") + } +} + +func (tv *Tavern) api(w http.ResponseWriter, r *http.Request) { + defer func() { + s := recover() + if s != nil { + logger.Error(s) + } + io.Copy(io.Discard, r.Body) + r.Body.Close() + }() + + // 서버에서 오는 요청만 처리 + apitoken := r.Header.Get("MG-X-API-TOKEN") + if apitoken != config.MaingateApiToken { + // 서버가 보내는 쿼리만 허용 + logger.Println("MG-X-API-TOKEN is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + funcname := r.URL.Query().Get("call") + if len(funcname) == 0 { + logger.Println("query param 'call' is missing") + w.WriteHeader(http.StatusBadRequest) + return + } + + tv.httpApiBorker.Call(funcname, w, r) +} diff --git a/core/tavern_test.go b/core/tavern_test.go new file mode 100644 index 0000000..2401990 --- /dev/null +++ b/core/tavern_test.go @@ -0,0 +1,113 @@ +// warroom project main.go +package core + +import ( + "context" + "encoding/binary" + "fmt" + "testing" + "time" + + "github.com/go-redis/redis/v8" + "go.mongodb.org/mongo-driver/bson/primitive" + "repositories.action2quare.com/ayo/gocommon" + "repositories.action2quare.com/ayo/gocommon/logger" +) + +func TestPubSub(t *testing.T) { + opt0, _ := redis.ParseURL("redis://192.168.8.94:6380/0") + opt1, _ := redis.ParseURL("redis://192.168.8.94:6380/1") + + rc0 := redis.NewClient(opt0) + rc1 := redis.NewClient(opt1) + + go func() { + time.Sleep(time.Second) + rc1.Publish(context.Background(), "__testchan", "real???") + fmt.Println("published") + }() + + pubsub := rc0.Subscribe(context.Background(), "__testchan") + msg, err := pubsub.ReceiveMessage(context.Background()) + fmt.Println(msg.Payload, err) +} +func makeHash(chanName string, index uint32) string { + for len(chanName) < 12 { + chanName += chanName + } + left := chanName[:6] + right := chanName[len(chanName)-6:] + base := []byte(left + right) + for i := 0; i < 12; i++ { + base[i] += base[12-i-1] + } + + bs := make([]byte, 4) + binary.LittleEndian.PutUint32(bs, index) + for i, c := range bs { + base[i] ^= c + } + var gid primitive.ObjectID + copy(gid[:], base) + + return gid.Hex() +} + +func TestNameHash(t *testing.T) { + for i := 0; i < 10; i++ { + makeHash("Urud", uint32(i)) + fmt.Printf("Urud:%d - %s\n", i, makeHash("Urud", uint32(i))) + makeHash("Sheldon", uint32(i)) + fmt.Printf("Sheldon:%d - %s\n", i, makeHash("Sheldon", uint32(i))) + } +} + +func TestReJSON(t *testing.T) { + rc := redis.NewClient(&redis.Options{Addr: "192.168.8.94:6380"}) + rh := gocommon.NewRedisonHandler(context.Background(), rc) + + success, err := rc.HSetNX(context.Background(), "setnxtest", "cap", 100).Result() + fmt.Println(success, err) + success, err = rc.HSetNX(context.Background(), "setnxtest", "cap", 100).Result() + fmt.Println(success, err) + + testDoc := map[string]any{ + "members": map[string]any{ + "mid2": map[string]any{ + "key": "val", + "exp": 20202020, + }, + "mid1": map[string]any{ + "key": "val", + "exp": 10101010, + }, + }, + } + + gd := groupDoc{ + id: primitive.NewObjectID(), + } + + midin := primitive.NewObjectID() + tid := gd.tid(midin) + midout := gd.mid(tid) + logger.Println(midin, tid, midout) + + logger.Println(rh.JSONSet("jsontest", "$", testDoc)) + logger.Println(rh.JSONGet("jsontest", "$")) + logger.Println(rh.JSONResp("jsontest", "$.members")) + logger.Println(rh.JSONGetString("jsontest", "$.members..key")) + logger.Println(rh.JSONGetInt64("jsontest", "$.members..exp")) + logger.Println(rh.JSONObjKeys("jsontest", "$.members")) + + err = rh.JSONMSet("jsontest", map[string]any{ + "$.members.mid1.key": "newval", + "$.members.mid2.key": "newval", + }) + logger.Println(err) + + logger.Println(rh.JSONGet("jsontest", "$")) + logger.Println(rh.JSONMDel("jsontest", []string{"$.members.mid1", "$.members.mid2"})) + logger.Println(rh.JSONGet("jsontest", "$")) + logger.Println(rh.JSONObjLen("jsontest", "$.members")) +} diff --git a/go.mod b/go.mod index 4350eb3..9460476 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ -module repositories.action2quare.com/ayo/social +module repositories.action2quare.com/ayo/tavern go 1.20 require ( github.com/go-redis/redis/v8 v8.11.5 - github.com/gorilla/websocket v1.5.0 go.mongodb.org/mongo-driver v1.11.7 repositories.action2quare.com/ayo/gocommon v0.0.0-20230911034515-1af5d7281946 ) @@ -14,6 +13,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.4 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/klauspost/compress v1.16.6 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect diff --git a/go.sum b/go.sum index 80420fe..2e2bc9a 100644 --- a/go.sum +++ b/go.sum @@ -104,5 +104,15 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230906142024-eb54fa2e3a44 h1:90XY5WSLtxvfi6YktDY4Sv1CMPRViZvPLPunA1eIxZA= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230906142024-eb54fa2e3a44/go.mod h1:PdpZ16O1czKKxCxn+0AFNaEX/0kssYwC3G8jR0V7ybw= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908023557-6cbf32c3868b h1:Rx6tP6IhlGlVGGgMDZ7OuIDU9cHfvm2L05L2tqF7G58= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908023557-6cbf32c3868b/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908025007-3603c0386b29 h1:Ts40m9MLMMx4uaQWko5QXkg/HX4uYQB9TGGEN6twhiU= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908025007-3603c0386b29/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908062630-46ce5f09897a h1:xKUI2xlP6LcUV5fy+4QEHoaZOhkSsMYgeIp6H5ADBCM= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908062630-46ce5f09897a/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908091916-23231dc6d705 h1:sK2mbRwqTMTZFmP9F50MIFZG9hcQ+EeW7tsGTzBgAow= +repositories.action2quare.com/ayo/gocommon v0.0.0-20230908091916-23231dc6d705/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY= repositories.action2quare.com/ayo/gocommon v0.0.0-20230911034515-1af5d7281946 h1:YSvgTNuHeKis37+FfOvzVLYCaXQ0oF+CWBTy4bRqq3g= repositories.action2quare.com/ayo/gocommon v0.0.0-20230911034515-1af5d7281946/go.mod h1:XvklTTSvQX5uviivGBcZo8eIL+mV94W2e4uBBXcT5JY= diff --git a/main.go b/main.go index f205b6b..61d17f5 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ +// warroom project main.go package main import ( @@ -6,7 +7,7 @@ import ( "repositories.action2quare.com/ayo/gocommon/flagx" "repositories.action2quare.com/ayo/gocommon/wshandler" - "repositories.action2quare.com/ayo/social/core" + "repositories.action2quare.com/ayo/tavern/core" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" @@ -19,7 +20,7 @@ func main() { flagx.Parse() ctx, cancel := context.WithCancel(context.Background()) - var config core.SocialConfig + var config core.TavernConfig if err := gocommon.LoadConfig(&config); err != nil { panic(err) } @@ -34,18 +35,18 @@ func main() { panic(err) } - if so, err := core.New(ctx, wsh, &config); err != nil { + if tv, err := core.New(ctx, wsh); err != nil { panic(err) } else { serveMux := http.NewServeMux() wsh.RegisterHandlers(serveMux, *prefix) - so.RegisterHandlers(ctx, serveMux, *prefix) + tv.RegisterHandlers(ctx, serveMux, *prefix) server := gocommon.NewHTTPServer(serveMux) - logger.Println("social is started") + logger.Println("tavern is started") wsh.Start(ctx) server.Start() cancel() - so.Cleanup() + tv.Cleanup() wsh.Cleanup() } }