diff --git a/config.json b/config.json deleted file mode 100644 index 9e26dfe..0000000 --- a/config.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/config_template.json b/config_template.json index 7b32c79..871ef54 100644 --- a/config_template.json +++ b/config_template.json @@ -6,8 +6,7 @@ "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", - "wshandler": "redis://192.168.8.94:6380/4" + "tavern": "redis://192.168.8.94:6380/3" } } }, @@ -15,6 +14,9 @@ "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 new file mode 100644 index 0000000..9610363 --- /dev/null +++ b/core/friend.go @@ -0,0 +1,491 @@ +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 deleted file mode 100644 index 5ae0a82..0000000 --- a/core/group.go +++ /dev/null @@ -1,3 +0,0 @@ -package core - -type configDocument map[string]any diff --git a/core/group_chat.go b/core/group_chat.go deleted file mode 100644 index 5d84954..0000000 --- a/core/group_chat.go +++ /dev/null @@ -1,380 +0,0 @@ -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 deleted file mode 100644 index 631df34..0000000 --- a/core/group_party.go +++ /dev/null @@ -1,822 +0,0 @@ -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 new file mode 100644 index 0000000..2789b7f --- /dev/null +++ b/core/invitation.go @@ -0,0 +1,383 @@ +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 new file mode 100644 index 0000000..eaba2c7 --- /dev/null +++ b/core/social.go @@ -0,0 +1,134 @@ +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 new file mode 100644 index 0000000..adc8130 --- /dev/null +++ b/core/social_test.go @@ -0,0 +1,65 @@ +// 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 deleted file mode 100644 index 0d7ab88..0000000 --- a/core/tavern.go +++ /dev/null @@ -1,193 +0,0 @@ -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 deleted file mode 100644 index 2401990..0000000 --- a/core/tavern_test.go +++ /dev/null @@ -1,113 +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" - "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 9460476..4350eb3 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ -module repositories.action2quare.com/ayo/tavern +module repositories.action2quare.com/ayo/social 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 ) @@ -13,7 +14,6 @@ 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 2e2bc9a..80420fe 100644 --- a/go.sum +++ b/go.sum @@ -104,15 +104,5 @@ 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 61d17f5..f205b6b 100644 --- a/main.go +++ b/main.go @@ -1,4 +1,3 @@ -// warroom project main.go package main import ( @@ -7,7 +6,7 @@ import ( "repositories.action2quare.com/ayo/gocommon/flagx" "repositories.action2quare.com/ayo/gocommon/wshandler" - "repositories.action2quare.com/ayo/tavern/core" + "repositories.action2quare.com/ayo/social/core" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/logger" @@ -20,7 +19,7 @@ func main() { flagx.Parse() ctx, cancel := context.WithCancel(context.Background()) - var config core.TavernConfig + var config core.SocialConfig if err := gocommon.LoadConfig(&config); err != nil { panic(err) } @@ -35,18 +34,18 @@ func main() { panic(err) } - if tv, err := core.New(ctx, wsh); err != nil { + if so, err := core.New(ctx, wsh, &config); err != nil { panic(err) } else { serveMux := http.NewServeMux() wsh.RegisterHandlers(serveMux, *prefix) - tv.RegisterHandlers(ctx, serveMux, *prefix) + so.RegisterHandlers(ctx, serveMux, *prefix) server := gocommon.NewHTTPServer(serveMux) - logger.Println("tavern is started") + logger.Println("social is started") wsh.Start(ctx) server.Start() cancel() - tv.Cleanup() + so.Cleanup() wsh.Cleanup() } }