package localserver import ( "context" "encoding/json" "fmt" "io/fs" "os" "path/filepath" "strings" "time" "github.com/dgraph-io/badger/v3" "github.com/pion/webrtc/v3" ) type NodeChatConfig struct { ID string `json:"id"` Name string `json:"name"` Initiator string `json:"initiator"` Target string `json:"target"` InitiatorHost string `json:"initiatorHost"` TargetHost string `json:"targetHost"` } type NodeChatChannel struct { ID string `json:"id"` Name string `json:"name"` Initiator string `json:"initiator"` Target string `json:"target"` InitiatorHost string `json:"initiatorHost"` TargetHost string `json:"targetHost"` LastReadIndex uint `json:"lastReadIndex"` Unread uint `json:"unread"` DB *NodeChatDBHandler `json:"-"` Tracking *NodeChatTrackingDB `json:"-"` } type NodeChatChannelsHandler[T ZippytalFSInstance] struct { ChatID string ChatFSInstance T DataChannels map[string]*DataChannel ChatDataChannels map[string]*DataChannel ChatDataChannelsFlag *uint32 Flag *uint32 ChatFSInstanceFlag *uint32 ChatFlag *uint32 Chats map[string]*NodeChatChannel reqChans []chan<- *ChatRequest init bool } func NewNodeChatChannelsHandler(chatID, chatName, initiator, target, initiatorHostId, targetHostId string, dataChannels map[string]*DataChannel, flag *uint32) (nodeChatsHandler *NodeChatChannelsHandler[*NodeChatFSInstance], err error) { var dirs []fs.DirEntry dirs, err = os.ReadDir(filepath.Join(dataPath, "data", "chats", chatID)) if err != nil { if os.IsNotExist(err) { logger.Printf("creating chat directory for chat %s...\n", chatID) mkdirErr := os.MkdirAll(filepath.Join(dataPath, "data", "chats", chatID), 0700) if mkdirErr != nil { return nil, mkdirErr } file, ferr := os.Create(filepath.Join(dataPath, "data", "chats", chatID, "chatConfig.json")) if ferr != nil { return nil, ferr } baseConfig := NodeChatConfig{ ID: chatID, Name: chatName, Initiator: initiator, InitiatorHost: initiatorHostId, Target: target, TargetHost: targetHostId, } bs, jsonErr := json.Marshal(baseConfig) if jsonErr != nil { return nil, jsonErr } if _, writeErr := file.WriteString(string(bs)); writeErr != nil { return nil, writeErr } _ = file.Close() dirs, err = os.ReadDir(filepath.Join(dataPath, "data", "chats", chatID)) if err != nil { return nil, err } } else { return } } chats := make(map[string]*NodeChatChannel) for _, chat := range dirs { if strings.HasPrefix(chat.Name(), ".") { continue } nodeChatDBHandler, err := NewNodeChatDBHandler(chat.Name()) if err != nil { return nil, err } var bs []byte bs, err = os.ReadFile(filepath.Join(dataPath, "data", "chats", chat.Name(), "chatConfig.json")) if err != nil { return nil, err } logger.Println(string(bs)) var c NodeChatChannel if err = json.Unmarshal(bs, &c); err != nil { return nil, err } nodeChatTracking, err := NewNodeChatTracking(chatID, initiator, target) if err != nil { return nil, err } logger.Println("chats data :", c.ID, c.Initiator, c.InitiatorHost, c.Target) c.DB = nodeChatDBHandler c.Tracking = nodeChatTracking chats[c.ID] = &c } chatFlag := uint32(0) chatFSFlag := uint32(0) chatDCFlag := uint32(0) nodeChatsHandler = &NodeChatChannelsHandler[*NodeChatFSInstance]{ ChatID: chatID, ChatFSInstance: NewNodeChatFSInstance(chatID), ChatFSInstanceFlag: &chatFSFlag, DataChannels: dataChannels, ChatDataChannels: make(map[string]*DataChannel), ChatDataChannelsFlag: &chatDCFlag, Flag: flag, Chats: chats, ChatFlag: &chatFlag, init: false, } return } func (zch *NodeChatChannelsHandler[T]) sendZoneRequest(reqType string, from string, payload map[string]interface{}) { go func() { for _, rc := range zch.reqChans { rc <- &ChatRequest{ ReqType: reqType, From: from, Payload: payload, } } }() } func (zch *NodeChatChannelsHandler[T]) sendDataChannelMessage(reqType string, from string, to string, payload map[string]interface{}) (<-chan struct{}, <-chan error) { done, errCh := make(chan struct{}), make(chan error) go func() { if err := atomicallyExecute(zch.ChatDataChannelsFlag, func() (err error) { if _, ok := zch.ChatDataChannels[to]; ok { bs, jsonErr := json.Marshal(&ZoneResponse{ Type: reqType, From: from, To: to, Payload: payload, }) if jsonErr != nil { return jsonErr } err = zch.ChatDataChannels[to].DataChannel.SendText(string(bs)) } else { err = fmt.Errorf("no corresponding dataChannel") } return }); err != nil { errCh <- err return } done <- struct{}{} }() return done, errCh } func (zch *NodeChatChannelsHandler[T]) Init(ctx context.Context, initiator, target, initiatorNodeID, targetNodeID string) (err error) { // for _, member := range authorizedMembers { // if serr := zch.SetAllPublicChatForUser(member); serr != nil { // logger.Println(serr) // } // } zch.init = true return } func (zch *NodeChatChannelsHandler[T]) Subscribe(ctx context.Context, publisher <-chan *ChatRequest) (reqChan chan *ChatRequest, done chan struct{}, errCh chan error) { reqChan, done, errCh = make(chan *ChatRequest), make(chan struct{}), make(chan error) zch.reqChans = append(zch.reqChans, reqChan) go func() { for { select { case <-ctx.Done(): done <- struct{}{} return case req := <-publisher: if err := zch.handleChatRequest(ctx, req); err != nil { errCh <- err } } } }() return } func (zch *NodeChatChannelsHandler[T]) GetChats(userId string) (err error) { chats := make([]*NodeChatChannel, 0) for _, chat := range zch.Chats { if chat.Initiator == userId || chat.Target == userId { chats = append(chats, chat) } } for _, chat := range chats { index, err := chat.Tracking.GetUserLastIndex(userId) if err != nil { return err } unread, err := chat.DB.calculateNewChatCount(uint64(index)) if err != nil { return err } chat.LastReadIndex = index chat.Unread = unread } done, e := zch.sendDataChannelMessage(GET_CHATS_RESPONSE, "node", userId, map[string]interface{}{ "chats": chats, }) select { case <-done: case err = <-e: } return } func (zch *NodeChatChannelsHandler[T]) ListChats() (chats []*NodeChatChannel, err error) { chats = make([]*NodeChatChannel, 0) _ = atomicallyExecute(zch.ChatFlag, func() (err error) { for _, chat := range zch.Chats { chats = append(chats, chat) } return }) return } func (zch *NodeChatChannelsHandler[T]) AddNewChat(chatID, initiator, target, initiatorHostId, targetHostId string) (err error) { if chatID == "" { err = fmt.Errorf("not a valid chat name provided") return } if _, ok := zch.Chats[chatID]; ok { err = fmt.Errorf("a chat with this name already exist") return } mkdirErr := os.MkdirAll(filepath.Join(dataPath, "data", "chats", chatID), 0700) if mkdirErr != nil { return mkdirErr } mkdirErr = os.Mkdir(filepath.Join(dataPath, "data", "chats", chatID, "__tracking__"), 0700) if mkdirErr != nil { return mkdirErr } file, ferr := os.Create(filepath.Join(dataPath, "data", "chats", chatID, "chatConfig.json")) defer func() { _ = file.Close() }() if ferr != nil { return ferr } baseConfig := &NodeChatConfig{ ID: chatID, } bs, jsonErr := json.Marshal(baseConfig) if jsonErr != nil { return jsonErr } if _, writeErr := file.WriteString(string(bs)); writeErr != nil { return writeErr } err = file.Close() if err != nil { return err } nodeChatDBHandler, err := NewNodeChatDBHandler(chatID) if err != nil { return err } nodeChatTrackingDB, err := NewNodeChatTracking(chatID, initiator, target) if err != nil { return err } if err = nodeChatTrackingDB.Initialize(1, initiator, target); err != nil { return err } var c NodeChatChannel if err = json.Unmarshal(bs, &c); err != nil { return err } c.DB = nodeChatDBHandler c.Tracking = nodeChatTrackingDB _ = atomicallyExecute(zch.ChatFlag, func() (err error) { zch.Chats[c.ID] = &c return }) return } func (zch *NodeChatChannelsHandler[T]) DeleteChat(chatID string) (err error) { if err = atomicallyExecute(zch.ChatFlag, func() (err error) { defer delete(zch.Chats, chatID) if _, ok := zch.Chats[chatID]; !ok { err = fmt.Errorf("no corresponding chat") return } return }); err != nil { return } if err = os.RemoveAll(filepath.Join(dataPath, "data", "chats", chatID)); err != nil { return } return } func (zch *NodeChatChannelsHandler[T]) ReadLastMessage(userId, chatID string) (err error) { err = atomicallyExecute(zch.ChatFlag, func() (err error) { if chat, ok := zch.Chats[chatID]; ok { err = chat.Tracking.SetUserLastIndex(userId, chat.DB.PreviousId) } return }) return } func (zch *NodeChatChannelsHandler[T]) ListLatestChatMessages(userId, chatID string, lastIndex, limit float64) (err error) { var list []*ChatMessage var i int var done bool if err = atomicallyExecute(zch.ChatFlag, func() (err error) { if chat, ok := zch.Chats[chatID]; ok { list, i, done, err = zch.Chats[chatID].DB.ListChatMessages(int(lastIndex), int(limit)) if err != nil { return } err = chat.Tracking.SetUserLastIndex(userId, chat.DB.PreviousId) } return }); err != nil { return } success, e := zch.sendDataChannelMessage(CHAT_MESSAGE_LIST, "node", userId, map[string]interface{}{ "done": done || i <= 0, "lastIndex": i - 1, "chatID": chatID, "chatMessages": list, }) select { case <-success: fmt.Println("done getting latest messages") case err = <-e: } return } func (zch *NodeChatChannelsHandler[T]) ListLatestChatFiles(userId, chatID string, lastIndex, limit float64) (err error) { var list []*ChatFile var i int if err = atomicallyExecute(zch.ChatFlag, func() (err error) { if _, ok := zch.Chats[chatID]; ok { list, i, err = zch.Chats[chatID].DB.ListChatFiles(int(lastIndex), int(limit)) } return }); err != nil { return } done, e := zch.sendDataChannelMessage(CHAT_FILES_LIST, "node", userId, map[string]interface{}{ "done": i <= 0, "lastIndex": i, "chatID": chatID, "chatMessages": list, }) select { case <-done: case err = <-e: } return } func (zch *NodeChatChannelsHandler[T]) AddChatMessage(userId, chatID, content string, isResponse bool, chatResponseId uint64, file *ChatFile) (err error) { if _, ok := zch.Chats[chatID]; !ok { err = fmt.Errorf("no such chat") return } chat := zch.Chats[chatID] chatMessage := &ChatMessage{ Content: content, From: userId, IsResponse: isResponse, ResponseOf: nil, File: file, Tags: make([]string, 0), Date: time.Now().Format("Mon, 02 Jan 2006 15:04:05 MST"), } if err = atomicallyExecute(zch.ChatFlag, func() (err error) { if chat, ok := zch.Chats[chatID]; ok { if isResponse { parentMessage, getErr := chat.DB.GetChatMessage(chatResponseId) if err != nil { if getErr != badger.ErrKeyNotFound { return getErr } } chatMessage.ResponseOf = parentMessage } if err = zch.Chats[chatID].DB.AddNewChatMessage(chatMessage); err != nil { return } chatMessage.ID = zch.Chats[chatID].DB.PreviousId } else { err = fmt.Errorf("no corresponding chats") } return }); err != nil { return } notifyActivity := func(done <-chan struct{}, e <-chan error, target, initiator string) { select { case <-done: case <-e: _ = atomicallyExecute(zch.ChatFlag, func() (err error) { if chat, ok := zch.Chats[chatID]; ok { li, err := chat.Tracking.GetUserLastIndex(target) if err != nil { return err } count, err := chat.DB.calculateNewChatCount(uint64(li)) if err != nil { return err } bs, err := json.Marshal(map[string]any{ "chatID": chatID, "chatHost": NodeID, }) if err != nil { return err } zch.sendZoneRequest(CREATE_NOTIFICATION, "node", map[string]interface{}{ "type": "new_chat_message", "title": "Unread messages 👀", "body": fmt.Sprintf("%d new messages from %s", count, initiator), "isPushed": true, "payload": string(bs), "recipients": []string{target}, }) } return }) } } d1, e1 := zch.sendDataChannelMessage(NEW_CHAT_MESSAGE, "node", chat.Target, map[string]interface{}{ "chatMessage": chatMessage, "chatID": chatID, }) go notifyActivity(d1, e1, chat.Target, chat.Initiator) d2, e2 := zch.sendDataChannelMessage(NEW_CHAT_MESSAGE, "node", chat.Initiator, map[string]interface{}{ "chatMessage": chatMessage, "chatID": chatID, }) go notifyActivity(d2, e2, chat.Initiator, chat.Target) return } func (zch *NodeChatChannelsHandler[T]) RemoveAllUserChat(userId string) (err error) { chats, err := zch.ListChats() if err != nil { return } for _, chat := range chats { if chat.Initiator == userId || chat.Target == userId { if derr := zch.DeleteChat(chat.ID); derr != nil { logger.Println("**************", derr) } logger.Println("deleted chat", chat.ID) } } return } func (zch *NodeChatChannelsHandler[T]) DeleteChatMessage(key uint64, chatID string) (err error) { err = atomicallyExecute(zch.ChatFlag, func() (err error) { if _, ok := zch.Chats[chatID]; !ok { err = fmt.Errorf("no file corresponding to id %s", chatID) return } chat := zch.Chats[chatID] if err = chat.DB.DeleteChatMessage(key); err != nil { return } for _, member := range [2]string{chat.Initiator, chat.Target} { d, e := zch.sendDataChannelMessage(CHAT_MESSAGE_DELETED, "node", member, map[string]interface{}{ "chatID": chatID, "messageId": key, }) select { case <-d: case tempErr := <-e: logger.Println(tempErr) } } previousId := zch.Chats[chatID].DB.PreviousId if previousId == key { if err = zch.Chats[chatID].DB.revertPreviousId(); err != nil { return } previousId = zch.Chats[chatID].DB.PreviousId if previousId == 1 { previousId = 0 } if err = zch.Chats[chatID].Tracking.RevertTrackingLastIndex(previousId); err != nil { return } } return }) return } func (zch *NodeChatChannelsHandler[T]) UpdateChatMessage(key uint64, chatID, newContent string) (err error) { err = atomicallyExecute(zch.ChatFlag, func() (err error) { if _, ok := zch.Chats[chatID]; !ok { err = fmt.Errorf("no chat corresponding to id %s", chatID) return } chat := zch.Chats[chatID] if err = chat.DB.ModifyChatMessage(key, newContent); err != nil { return } for _, member := range [2]string{chat.Target, chat.Initiator} { d, e := zch.sendDataChannelMessage(CHAT_MESSAGE_EDITED, "node", member, map[string]interface{}{ "chatID": chatID, "messageId": key, "newContent": newContent, }) select { case <-d: case tempErr := <-e: logger.Println(tempErr) } } return }) return } func (zch *NodeChatChannelsHandler[T]) DeleteChatFile(key uint64, fileName, chatID string) (err error) { err = atomicallyExecute(zch.ChatFlag, func() (err error) { if _, ok := zch.Chats[chatID]; !ok { err = fmt.Errorf("no file corresponding to id %s", chatID) return } chat := zch.Chats[chatID] if err = chat.DB.DeleteChatFile(fileName, key); err != nil { return } for _, member := range [2]string{chat.Target, chat.Initiator} { d, e := zch.sendDataChannelMessage(CHAT_FILE_DELETED, "node", member, map[string]interface{}{ "chatID": chatID, "fileId": key, "fileName": fileName, }) select { case <-d: case tempErr := <-e: logger.Println(tempErr) } } return }) return } func (zch *NodeChatChannelsHandler[T]) chatFileUpload(chatID string, filename string, userId string, dc *DataChannel) { var writePipe chan<- []byte var done bool uploadDone := func() { if writePipe != nil { close(writePipe) writePipe = nil } done = true } dc.DataChannel.OnError(func(err error) { logger.Println(err) logger.Println("abort...") if !done { uploadDone() } if fufErr := zch.ChatFSInstance.FileUploadFailed(chatID, filename, userId); fufErr != nil { logger.Println(fufErr) } }) dc.DataChannel.OnClose(func() { if done { logger.Println("closing gracefully...") } else { logger.Println("abort...") uploadDone() if fufErr := zch.ChatFSInstance.FileUploadFailed(chatID, filename, userId); fufErr != nil { logger.Println(fufErr) } } }) dc.DataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { if msg.IsString { if string(msg.Data) == "init_upload" { logger.Println("init upload....") var initErr error if writePipe, initErr = zch.ChatFSInstance.SetupFileUpload(chatID, filename, userId, dc.DataChannel); initErr != nil { _ = dc.DataChannel.SendText("abort") _ = dc.DataChannel.Close() return } logger.Println("upload ready !") _ = dc.DataChannel.SendText("upload_ready") } else if string(msg.Data) == "upload_done" { uploadDone() } } else { writePipe <- msg.Data } }) dc.DataChannel.OnOpen(func() { _ = dc.DataChannel.SendText("channel_ready") }) } func (zch *NodeChatChannelsHandler[T]) chatFileDownload(chatID string, filename string, userId string, dc *DataChannel) { var done bool dc.DataChannel.OnError(func(err error) { if !done { logger.Println("abort...") if fdf := zch.ChatFSInstance.FileDownloadFailed(chatID, filename, userId); fdf != nil { logger.Println(fdf) } } }) dc.DataChannel.OnClose(func() { if !done { logger.Println("abort...") if fdf := zch.ChatFSInstance.FileDownloadFailed(chatID, filename, userId); fdf != nil { logger.Println(fdf) } } }) dc.DataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { if msg.IsString { if string(msg.Data) == "init_download" { logger.Println("init download....") var initErr error if initErr = zch.ChatFSInstance.SetupFileDownload(chatID, filename, userId, dc.DataChannel); initErr != nil { _ = dc.DataChannel.SendText("abort") _ = dc.DataChannel.Close() return } logger.Println("download started !") } else if string(msg.Data) == "download_done" { done = true } } }) dc.DataChannel.OnOpen(func() { _ = dc.DataChannel.SendText("channel_ready") }) } func (zch *NodeChatChannelsHandler[T]) handleDataChannel(ctx context.Context, dc *DataChannel) (catched bool) { var label string _ = atomicallyExecute(dc.l, func() (err error) { label = dc.DataChannel.Label() return }) if strings.Contains(label, "chat_upload") { command := strings.Split(label, "|") if len(command) == 4 { catched = true go zch.chatFileUpload(command[1], command[2], command[3], dc) } logger.Println(command) } else if strings.Contains(label, "chat_download") { command := strings.Split(label, "|") catched = true go zch.chatFileDownload(command[1], command[2], command[3], dc) logger.Println(command) } else if strings.Contains(label, "chat_data") { command := strings.Split(label, "|") catched = true _ = atomicallyExecute(zch.ChatDataChannelsFlag, func() (err error) { zch.ChatDataChannels[command[1]] = dc return }) dc.DataChannel.OnClose(func() { fmt.Println("closing gratefully chat dc...") _ = atomicallyExecute(zch.ChatDataChannelsFlag, func() (err error) { delete(zch.ChatDataChannels, command[1]) return }) }) dc.DataChannel.OnError(func(err error) { fmt.Println("error in chat dc...") _ = atomicallyExecute(zch.ChatDataChannelsFlag, func() (err error) { delete(zch.ChatDataChannels, command[1]) return }) }) } return } func (zch *NodeChatChannelsHandler[T]) handleChatRequest(ctx context.Context, req *ChatRequest) (err error) { logger.Println("got request in zone chat handler", req) switch req.ReqType { case LEAVE_CHAT: if err = VerifyFieldsString(req.Payload, "userId"); err != nil { return } // err = zch.LeaveChatFSInstance(req.Payload["userId"].(string)) case DELETE_CHAT: if err = VerifyFieldsString(req.Payload, "chatID"); err != nil { return } err = zch.DeleteChat(req.Payload["chatID"].(string)) case CREATE_CHAT: if err = VerifyFieldsString(req.Payload, "chatID", "owner", "chatType"); err != nil { return } if err = VerifyFieldsSliceInterface(req.Payload, "members"); err != nil { return } err = zch.AddNewChat(req.Payload["chatID"].(string), req.Payload["initiator"].(string), req.Payload["target"].(string), req.Payload["initiatorHostId"].(string), req.Payload["targetHostId"].(string)) case GET_CHATS: err = zch.GetChats(req.From) case LIST_LATEST_CHATS: if err = VerifyFieldsString(req.Payload, "chatID"); err != nil { return } if err = VerifyFieldsFloat64(req.Payload, "lastIndex", "limit"); err != nil { return } err = zch.ListLatestChatMessages(req.From, req.Payload["chatID"].(string), req.Payload["lastIndex"].(float64), req.Payload["limit"].(float64)) return case LIST_LATEST_FILES: if err = VerifyFieldsString(req.Payload, "chatID"); err != nil { return } if err = VerifyFieldsFloat64(req.Payload, "lastIndex", "limit"); err != nil { return } err = zch.ListLatestChatFiles(req.From, req.Payload["chatID"].(string), req.Payload["lastIndex"].(float64), req.Payload["limit"].(float64)) return case READ_LATEST_MESSAGE: if err = VerifyFieldsString(req.Payload, "chatID"); err != nil { return } err = zch.ReadLastMessage(req.From, req.Payload["chatID"].(string)) case ADD_CHAT_MESSAGE: logger.Println("got request in zone chat handler", req) if err = VerifyFieldsString(req.Payload, "chatID", "content"); err != nil { return } if err = VerifyFieldsBool(req.Payload, "isResponse"); err != nil { return } var parentChatId uint64 if req.Payload["isResponse"].(bool) { if err = VerifyFieldsFloat64(req.Payload, "parentChatId"); err != nil { return } parentChatId = uint64(req.Payload["parentChatId"].(float64)) } var file *ChatFile = nil if _, ok := req.Payload["file"]; ok { bs, jsonErr := json.Marshal(req.Payload["file"]) if jsonErr != nil { return jsonErr } var f ChatFile if err = json.Unmarshal(bs, &f); err != nil { err = fmt.Errorf("the file payload dont match ChatFile struct pattern : %v", err) return } file = &f } err = zch.AddChatMessage(req.From, req.Payload["chatID"].(string), req.Payload["content"].(string), req.Payload["isResponse"].(bool), parentChatId, file) case DELETE_CHAT_MESSAGE: if err = VerifyFieldsString(req.Payload, "chatID"); err != nil { return } if err = VerifyFieldsFloat64(req.Payload, "messageId"); err != nil { return } err = zch.DeleteChatMessage(uint64(req.Payload["messageId"].(float64)), req.Payload["chatID"].(string)) case EDIT_CHAT_MESSAGE: if err = VerifyFieldsString(req.Payload, "chatID", "newContent"); err != nil { return } if err = VerifyFieldsFloat64(req.Payload, "messageId"); err != nil { return } err = zch.UpdateChatMessage(uint64(req.Payload["messageId"].(float64)), req.Payload["chatID"].(string), req.Payload["newContent"].(string)) case DELETE_CHAT_FILE: if err = VerifyFieldsString(req.Payload, "chatID", "fileName"); err != nil { return } if err = VerifyFieldsFloat64(req.Payload, "fileId"); err != nil { return } err = zch.DeleteChatFile(uint64(req.Payload["fileId"].(float64)), req.Payload["fileName"].(string), req.Payload["chatID"].(string)) } return }