Files
tavern/core/tavern.go
2023-07-17 17:47:07 +09:00

290 lines
6.3 KiB
Go

package core
import (
"context"
"encoding/json"
"errors"
"io"
"net"
"net/http"
"reflect"
"strings"
"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/bsonrw"
"go.mongodb.org/mongo-driver/bson/primitive"
)
const (
defaultMaxMemory = 32 << 10 // 32 KB
)
func writeBsonArr(w io.Writer, src []bson.M) error {
return writeBsonDoc(w, bson.M{
"r": src,
})
}
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"`
GroupTypes map[string]*groupConfig `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
wsh *wshandler.WebsocketHandler
region string
groups map[string]group
methods map[string]reflect.Method
}
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 := range config.RegionStorage {
var dbconn gocommon.MongoClient
var err error
var groupinstance group
var tmp *subTavern
methods := make(map[string]reflect.Method)
tp := reflect.TypeOf(tmp)
for i := 0; i < tp.NumMethod(); i++ {
method := tp.Method(i)
methods[method.Name] = method
}
sub := &subTavern{
wsh: tv.wsh,
mongoClient: dbconn,
region: region,
methods: methods,
}
groups := make(map[string]group)
for typename, cfg := range config.GroupTypes {
cfg.Name = typename
if cfg.Transient {
groupinstance, err = cfg.prepareInMemory(ctx, typename, sub)
//} else {
// TODO : db
// if !dbconn.Connected() {
// dbconn, err = gocommon.NewMongoClient(ctx, url.Mongo, region)
// if err != nil {
// return err
// }
// }
// groupinstance, err = cfg.preparePersistent(ctx, region, dbconn, tv.wsh)
}
if err != nil {
return err
}
groups[typename] = groupinstance
}
sub.groups = groups
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.clientMessageReceived)
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) clientMessageReceived(sender *wshandler.Sender, messageType wshandler.WebSocketMessageType, body io.Reader) {
if messageType == wshandler.Connected {
} else if messageType == wshandler.Disconnected {
} else if messageType == wshandler.BinaryMessage {
var msg map[string][]any
dec := json.NewDecoder(body)
if err := dec.Decode(&msg); err == nil {
for cmd, args := range msg {
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)
case "UpdateGroupMemberDocument":
typename := args[0].(string)
gidobj, _ := primitive.ObjectIDFromHex(args[1].(string))
doc := args[2].(map[string]any)
if group := sub.groups[typename]; group != nil {
group.UpdateMemberDocument(gidobj, sender.Accid, doc)
}
case "UpdateGroupDocument":
typename := args[0].(string)
gidobj, _ := primitive.ObjectIDFromHex(args[1].(string))
doc := args[2].(map[string]any)
if group := sub.groups[typename]; group != nil {
group.UpdateGroupDocument(gidobj, doc)
}
}
}
}
}
}
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
}
operation := r.URL.Query().Get("operation")
if len(operation) == 0 {
w.WriteHeader(http.StatusBadRequest)
return
}
method, ok := sub.methods[operation]
if !ok {
// 없는 operation
logger.Println("fail to call api. operation is not valid :", operation)
w.WriteHeader(http.StatusBadRequest)
return
}
if r.PostForm == nil {
r.ParseMultipartForm(defaultMaxMemory)
}
args := []reflect.Value{
reflect.ValueOf(sub),
reflect.ValueOf(w),
reflect.ValueOf(r),
}
method.Func.Call(args)
}