package core import ( "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "reflect" "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/wshandler" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/bsonrw" ) var devflag = flagx.Bool("dev", false, "") const ( defaultMaxMemory = 32 << 10 // 32 KB ) func writeBsonDoc[T any](w io.Writer, src T) error { rw, err := bsonrw.NewBSONValueWriter(w) if err != nil { return err } enc, err := bson.NewEncoder(rw) if err != nil { return err } return enc.Encode(src) } func readBsonDoc(r io.Reader, src any) error { body, err := io.ReadAll(r) if err != nil { return err } if len(body) == 0 { return nil } decoder, err := bson.NewDecoder(bsonrw.NewBSONDocumentReader(body)) if err != nil { return err } err = decoder.Decode(src) if err != nil { return err } return nil } type TavernConfig struct { gocommon.RegionStorageConfig `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 { subTaverns []*subTavern wsh *wshandler.WebsocketHandler } type subTavern struct { mongoClient gocommon.MongoClient redison *gocommon.RedisonHandler wsh *wshandler.WebsocketHandler region string groups map[string]group apiFuncs *apiFuncsContainer } 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, inconfig *TavernConfig) (*Tavern, error) { if inconfig == nil { var loaded TavernConfig if err := gocommon.LoadConfig(&loaded); err != nil { return nil, err } inconfig = &loaded } config = *inconfig 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() { for _, st := range tv.subTaverns { st.mongoClient.Close() } } func (tv *Tavern) prepare(ctx context.Context) error { for region, addr := range config.RegionStorage { var dbconn gocommon.MongoClient var err error redisClient, err := gocommon.NewRedisClient(addr.Redis["tavern"]) if err != nil { return err } redison := gocommon.NewRedisonHandler(redisClient.Context(), redisClient) sub := &subTavern{ wsh: tv.wsh, mongoClient: dbconn, redison: redison, region: region, apiFuncs: &apiFuncsContainer{ normfuncs: make(map[string]apiFuncType), funcs: make(map[string][]apiFuncType), }, } groups := make(map[string]group) for typename, cfg := range config.Group { gtype, ok := groupTypeContainer()[typename] if !ok { return fmt.Errorf("%s group type is not valid", typename) } if !gtype.Implements(reflect.TypeOf((*group)(nil)).Elem()) { return fmt.Errorf("%s is not implement proper interface", typename) } ptrvalue := reflect.New(gtype.Elem()) instance := ptrvalue.Interface().(group) if err := instance.Initialize(sub, cfg); err != nil { return err } groups[typename] = instance } sub.groups = groups sub.apiFuncs.normalize() tv.subTaverns = append(tv.subTaverns, sub) } return nil } func (tv *Tavern) RegisterHandlers(ctx context.Context, serveMux *http.ServeMux, prefix string) error { for _, sub := range tv.subTaverns { tv.wsh.RegisterReceiver(sub.region, sub) var pattern string if sub.region == "default" { pattern = gocommon.MakeHttpHandlerPattern(prefix, "api") } else { pattern = gocommon.MakeHttpHandlerPattern(prefix, sub.region, "api") } serveMux.HandleFunc(pattern, sub.api) } return nil } func (sub *subTavern) OnClientMessageReceived(sender *wshandler.Sender, messageType wshandler.WebSocketMessageType, body io.Reader) { if messageType == wshandler.Connected { logger.Println("OnClientMessageReceived : connected ", sender.Accid.Hex()) sub.redison.HSet(sub.redison.Context(), sender.Accid.Hex(), "_ts", time.Now().UTC().Unix()).Result() for _, gt := range sub.groups { gt.ClientMessageReceived(sender, messageType, nil) } } else if messageType == wshandler.Disconnected { var rooms []string dec := json.NewDecoder(body) if err := dec.Decode(&rooms); err == nil { for _, gt := range sub.groups { gt.ClientMessageReceived(sender, messageType, rooms) } } sub.redison.Del(sub.redison.Context(), sender.Accid.Hex()).Result() logger.Println("OnClientMessageReceived : disconnected ", sender.Accid.Hex()) } else if messageType == wshandler.BinaryMessage { var commandline []any dec := json.NewDecoder(body) if err := dec.Decode(&commandline); err == nil { cmd := commandline[0].(string) args := commandline[1:] switch cmd { case "EnterChannel": sub.wsh.EnterRoom(sub.region, args[0].(string), sender.Accid) case "LeaveChannel": sub.wsh.LeaveRoom(sub.region, args[0].(string), sender.Accid) default: for _, gt := range sub.groups { gt.ClientMessageReceived(sender, messageType, commandline) } } } } } func (sub *subTavern) OnRoomCreated(region, name string) { cnt, err := sub.redison.IncrBy(sub.redison.Context(), "_ref_"+name, 1).Result() if err != nil && !errors.Is(err, redis.Nil) { logger.Println("OnRoomCreated JSONSet failed :", err) return } if cnt == 1 { sub.redison.JSONSet(name, "$", map[string]any{}) } } func (sub *subTavern) OnRoomDestroyed(region, name string) { cnt, err := sub.redison.IncrBy(sub.redison.Context(), "_ref_"+name, -1).Result() if err != nil { logger.Println("OnRoomDestroyed JSONNumIncrBy failed :", err) } else if cnt == 0 { sub.redison.Del(sub.redison.Context(), "_ref_"+name) sub.redison.JSONDel(name, "$") } } func (sub *subTavern) 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 } if r.PostForm == nil { r.ParseMultipartForm(defaultMaxMemory) } operation := r.URL.Query().Get("operation") if len(operation) == 0 { w.WriteHeader(http.StatusBadRequest) return } sub.apiFuncs.call(operation, w, r) }