diff --git a/nim_status.nimble b/nim_status.nimble index deb6402..7894655 100644 --- a/nim_status.nimble +++ b/nim_status.nimble @@ -80,3 +80,5 @@ task tests, "Run all tests": buildAndRunTest "contacts" buildAndRunTest "account" buildAndRunTest "tokens" + buildAndRunTest "messages" + buildAndRunTest "chats" diff --git a/nim_status/lib.nim b/nim_status/lib.nim index bdb4db0..c2fd08a 100644 --- a/nim_status/lib.nim +++ b/nim_status/lib.nim @@ -3,6 +3,8 @@ import lib/alias import lib/alias/data import lib/account import lib/identicon +import lib/messages +import lib/chats import lib/util import nimcrypto import strformat diff --git a/nim_status/lib/chats.nim b/nim_status/lib/chats.nim new file mode 100644 index 0000000..59f457f --- /dev/null +++ b/nim_status/lib/chats.nim @@ -0,0 +1,117 @@ +import # nim libs + json, options, strutils, strformat +import # vendor libs + web3/conversions as web3_conversions, web3/ethtypes, + sqlcipher, json_serialization, json_serialization/[reader, writer, lexer], + stew/byteutils + +type + ChatType* {.pure.} = enum + Id = "id", + Name = "name", + Color = "color", + ChatType = "chatType", + Active = "active", + Timestamp = "timestamp", + DeletedAtClockValue = "deletedAtClockValue", + PublicKey = "publicKey", + UnviewedMessageCount = "unviewedMessageCount", + LastClockValue = "lastClockValue", + LastMessage = "lastMessage", + Members = "members", + MembershipUpdates = "membershipUpdates" + Profile = "profile", + InvitationAdmin = "invitationAdmin", + Muted = "muted" + + ChatCol* {.pure.} = enum + Id = "id", + Name = "name", + Color = "color", + ChatType = "type", + Active = "active", + Timestamp = "timestamp", + DeletedAtClockValue = "deleted_at_clock_value", + PublicKey = "public_key", + UnviewedMessageCount = "unviewed_message_count", + LastClockValue = "last_clock_value", + LastMessage = "last_message", + Members = "members", + MembershipUpdates = "membership_updates" + Profile = "profile", + InvitationAdmin = "invitation_admin", + Muted = "muted" + + + Chat* = object + id* {.serializedFieldName($ChatType.Id), dbColumnName($ChatCol.Id).}: string + name* {.serializedFieldName($ChatType.Name), dbColumnName($ChatCol.Name).}: string + color* {.serializedFieldName($ChatType.Color), dbColumnName($ChatCol.Color).}: string + chatType* {.serializedFieldName($ChatType.ChatType), dbColumnName($ChatCol.ChatType).}: int + active* {.serializedFieldName($ChatType.Active), dbColumnName($ChatCol.Active).}: bool + timestamp* {.serializedFieldName($ChatType.Timestamp), dbColumnName($ChatCol.Timestamp).}: int + deletedAtClockValue* {.serializedFieldName($ChatType.DeletedAtClockValue), dbColumnName($ChatCol.DeletedAtClockValue).}: int + publicKey* {.serializedFieldName($ChatType.PublicKey), dbColumnName($ChatCol.PublicKey).}: seq[byte] + unviewedMessageCount* {.serializedFieldName($ChatType.UnviewedMessageCount), dbColumnName($ChatCol.UnviewedMessageCount).}: int + lastClockValue* {.serializedFieldName($ChatType.LastClockValue), dbColumnName($ChatCol.LastClockValue).}: int + lastMessage* {.serializedFieldName($ChatType.LastMessage), dbColumnName($ChatCol.LastMessage).}: seq[byte] + members* {.serializedFieldName($ChatType.Members), dbColumnName($ChatCol.Members).}: seq[byte] + membershipUpdates* {.serializedFieldName($ChatType.MembershipUpdates), dbColumnName($ChatCol.MembershipUpdates).}: seq[byte] + profile* {.serializedFieldName($ChatType.Profile), dbColumnName($ChatCol.Profile).}: string + invitationAdmin* {.serializedFieldName($ChatType.InvitationAdmin), dbColumnName($ChatCol.InvitationAdmin).}: string + muted* {.serializedFieldName($ChatType.Muted), dbColumnName($ChatCol.Muted).}: bool + + +proc getChats*(db: DbConn): seq[Chat] = + let query = """SELECT * from chats""" + + result = db.all(Chat, query) + +proc getChatById*(db: DbConn, id: string): Option[Chat] = + let query = """SELECT * from chats where id = ?""" + + result = db.one(Chat, query, id) + +proc saveChat*(db: DbConn, chat: Chat) = + let query = fmt"""INSERT INTO chats( + {$ChatCol.Id}, + {$ChatCol.Name}, + {$ChatCol.Color}, + {$ChatCol.ChatType}, + {$ChatCol.Active}, + {$ChatCol.Timestamp}, + {$ChatCol.DeletedAtClockValue}, + {$ChatCol.PublicKey}, + {$ChatCol.UnviewedMessageCount}, + {$ChatCol.LastClockValue}, + {$ChatCol.LastMessage}, + {$ChatCol.Members}, + {$ChatCol.MembershipUpdates}, + {$ChatCol.Profile}, + {$ChatCol.InvitationAdmin}, + {$ChatCol.Muted}) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + """ + + db.exec(query, + chat.id, + chat.name, + chat.color, + chat.chatType, + chat.active, + chat.timestamp, + chat.deletedAtClockValue, + chat.publicKey, + chat.unviewedMessageCount, + chat.lastClockValue, + chat.lastMessage, + chat.members, + chat.membershipUpdates, + chat.profile, + chat.invitationAdmin, + chat.muted) + +proc deleteChat*(db: DbConn, chat: Chat) = + let query = fmt"""DELETE FROM chats where id = ?""" + + db.exec(query, chat.id) diff --git a/nim_status/lib/messages.nim b/nim_status/lib/messages.nim new file mode 100644 index 0000000..6861acc --- /dev/null +++ b/nim_status/lib/messages.nim @@ -0,0 +1,259 @@ +import # nim libs + json, options, sequtils, strutils, strformat +import # vendor libs + web3/conversions as web3_conversions, web3/ethtypes, + sqlcipher, json_serialization, json_serialization/[reader, writer, lexer], + stew/byteutils + +import conversions, settings/types + +type + MessageType* {.pure.} = enum + Id = "id", + WhisperTimestamp = "whisperTimestamp ", + Source = "source", + Destination = "destination", + Text = "text", + ContentType = "contentType", + Username = "username", + Timestamp = "timestamp", + ChatId = "chatId", + LocalChatId = "localChatId", + Hide = "hide", + ResponseTo = "responseTo", + MessageType = "messageType", + ClockValue = "clockValue", + Seen = "seen", + OutgoingStatus = "outgoingStatus", + ParsedText = "parsedText", + RawPayload = "rawPayload", + StickerPack = "stickerPack", + StickerHash = "stickerHash", + CommandId = "commandId", + CommandValue = "commandValue", + CommandAddress = "commandAddress", + CommandFrom = "commandFrom", + CommandContract = "commandContract", + CommandTransactionHash = "commandTransactionHash", + CommandSignature = "commandSignature", + CommandState = "commandState", + AudioPayload = "audioPayload", + AudioType = "audioType", + AudioDurationMs = "audioDurationMs", + AudioBase64 = "audioBase64", + ReplaceMessage = "replaceMessage", + Rtl = "rtl", + LineCount = "lineCount", + Links = "links", + Mentions = "mentions", + ImagePayload = "imagePayload", + ImageType = "imageType", + ImageBase64 = "imageBase64" + + MessageCol* {.pure.} = enum + Id = "id", + WhisperTimestamp = "whisper_timestamp", + Source = "source", + Destination = "destination", + Text = "text", + ContentType = "content_type", + Username = "username", + Timestamp = "timestamp", + ChatId = "chat_id", + LocalChatId = "local_chat_id", + Hide = "hide", + ResponseTo = "response_to", + MessageType = "message_type", + ClockValue = "clock_value", + Seen = "seen", + OutgoingStatus = "outgoing_status", + ParsedText = "parsed_text", + RawPayload = "raw_payload", + StickerPack = "sticker_pack", + StickerHash = "sticker_hash", + CommandId = "command_id", + CommandValue = "command_value", + CommandAddress = "command_address", + CommandFrom = "command_from", + CommandContract = "command_contract", + CommandTransactionHash = "command_transaction_hash", + CommandSignature = "command_signature", + CommandState = "command_state", + AudioPayload = "audio_payload", + AudioType = "audio_type", + AudioDurationMs = "audio_duration_ms", + AudioBase64 = "audio_base64", + ReplaceMessage = "replace_message", + Rtl = "rtl", + LineCount = "line_count", + Links = "links", + Mentions = "mentions", + ImagePayload = "image_payload", + ImageType = "image_type", + ImageBase64 = "image_base64" + + Message* = object + id* {.serializedFieldName($MessageType.Id), dbColumnName($MessageCol.Id).}: string + whisperTimestamp* {.serializedFieldName($MessageType.WhisperTimestamp), dbColumnName($MessageCol.WhisperTimestamp).}: int + source* {.serializedFieldName($MessageType.Source), dbColumnName($MessageCol.Source).}: string + destination* {.serializedFieldName($MessageType.Destination), dbColumnName($MessageCol.Destination).}: seq[byte] + text* {.serializedFieldName($MessageType.Text), dbColumnName($MessageCol.Text).}: string + contentType* {.serializedFieldName($MessageType.ContentType), dbColumnName($MessageCol.ContentType).}: int + username* {.serializedFieldName($MessageType.Username), dbColumnName($MessageCol.Username).}: string + timestamp* {.serializedFieldName($MessageType.Timestamp), dbColumnName($MessageCol.Timestamp).}: int + chatId* {.serializedFieldName($MessageType.ChatId), dbColumnName($MessageCol.ChatId).}: string + localChatId* {.serializedFieldName($MessageType.LocalChatId), dbColumnName($MessageCol.LocalChatId).}: string + hide* {.serializedFieldName($MessageType.Hide), dbColumnName($MessageCol.Hide).}: bool + responseTo* {.serializedFieldName($MessageType.ResponseTo), dbColumnName($MessageCol.ResponseTo).}: string + messageType* {.serializedFieldName($MessageType.MessageType), dbColumnName($MessageCol.MessageType).}: int + clockValue* {.serializedFieldName($MessageType.ClockValue), dbColumnName($MessageCol.ClockValue).}: int + seen* {.serializedFieldName($MessageType.Seen), dbColumnName($MessageCol.Seen).}: bool + outgoingStatus* {.serializedFieldName($MessageType.OutgoingStatus), dbColumnName($MessageCol.OutgoingStatus).}: string + parsedText* {.serializedFieldName($MessageType.ParsedText), dbColumnName($MessageCol.ParsedText).}: seq[byte] + rawPayload* {.serializedFieldName($MessageType.RawPayload), dbColumnName($MessageCol.RawPayload).}: seq[byte] + stickerPack* {.serializedFieldName($MessageType.StickerPack), dbColumnName($MessageCol.StickerPack).}: int + stickerHash* {.serializedFieldName($MessageType.StickerHash), dbColumnName($MessageCol.StickerHash).}: string + commandId* {.serializedFieldName($MessageType.CommandId), dbColumnName($MessageCol.CommandId).}: string + commandValue* {.serializedFieldName($MessageType.CommandValue), dbColumnName($MessageCol.CommandValue).}: string + commandAddress* {.serializedFieldName($MessageType.CommandAddress), dbColumnName($MessageCol.CommandAddress).}: string + commandFrom* {.serializedFieldName($MessageType.CommandFrom), dbColumnName($MessageCol.CommandFrom).}: string + commandContract* {.serializedFieldName($MessageType.CommandContract), dbColumnName($MessageCol.CommandContract).}: string + commandTransactionHash* {.serializedFieldName($MessageType.CommandTransactionHash), dbColumnName($MessageCol.CommandTransactionHash).}: string + commandSignature* {.serializedFieldName($MessageType.CommandSignature), dbColumnName($MessageCol.CommandSignature).}: seq[byte] + commandState* {.serializedFieldName($MessageType.CommandState), dbColumnName($MessageCol.CommandState).}: int + audioPayload* {.serializedFieldName($MessageType.AudioPayload), dbColumnName($MessageCol.AudioPayload).}: seq[byte] + audioType* {.serializedFieldName($MessageType.AudioType), dbColumnName($MessageCol.AudioType).}: int + audioDurationMs* {.serializedFieldName($MessageType.AudioDurationMs), dbColumnName($MessageCol.AudioDurationMs).}: int + audioBase64* {.serializedFieldName($MessageType.AudioBase64), dbColumnName($MessageCol.AudioBase64).}: string + replaceMessage* {.serializedFieldName($MessageType.ReplaceMessage), dbColumnName($MessageCol.ReplaceMessage).}: string + rtl* {.serializedFieldName($MessageType.Rtl), dbColumnName($MessageCol.Rtl).}: bool + lineCount* {.serializedFieldName($MessageType.LineCount), dbColumnName($MessageCol.LineCount).}: int + links* {.serializedFieldName($MessageType.Links), dbColumnName($MessageCol.Links).}: string + mentions* {.serializedFieldName($MessageType.Mentions), dbColumnName($MessageCol.Mentions).}: string + imagePayload* {.serializedFieldName($MessageType.ImagePayload), dbColumnName($MessageCol.ImagePayload).}: seq[byte] + imageType* {.serializedFieldName($MessageType.ImageType), dbColumnName($MessageCol.ImageType).}: string + imageBase64* {.serializedFieldName($MessageType.ImageBase64), dbColumnName($MessageCol.ImageBase64).}: string + + +proc getMessageById*(db: DbConn, id: string): Option[Message] = + let query = """SELECT * from user_messages where id = ?""" + + result = db.one(Message, query, id) + +proc saveMessage*(db: DbConn, message: Message) = + let query = fmt"""INSERT INTO user_messages( + {$MessageCol.Id}, + {$MessageCol.WhisperTimestamp}, + {$MessageCol.Source}, + {$MessageCol.Destination}, + {$MessageCol.Text}, + {$MessageCol.ContentType}, + {$MessageCol.Username}, + {$MessageCol.Timestamp}, + {$MessageCol.ChatId}, + {$MessageCol.LocalChatId}, + {$MessageCol.Hide}, + {$MessageCol.ResponseTo}, + {$MessageCol.MessageType}, + {$MessageCol.ClockValue}, + {$MessageCol.Seen}, + {$MessageCol.OutgoingStatus}, + {$MessageCol.ParsedText}, + {$MessageCol.RawPayload}, + {$MessageCol.StickerPack}, + {$MessageCol.StickerHash}, + {$MessageCol.CommandId}, + {$MessageCol.CommandValue}, + {$MessageCol.CommandAddress}, + {$MessageCol.CommandFrom}, + {$MessageCol.CommandContract}, + {$MessageCol.CommandTransactionHash}, + {$MessageCol.CommandSignature}, + {$MessageCol.CommandState}, + {$MessageCol.AudioPayload}, + {$MessageCol.AudioType}, + {$MessageCol.AudioDurationMs}, + {$MessageCol.AudioBase64}, + {$MessageCol.ReplaceMessage}, + {$MessageCol.Rtl}, + {$MessageCol.LineCount}, + {$MessageCol.Links}, + {$MessageCol.Mentions}, + {$MessageCol.ImagePayload}, + {$MessageCol.ImageType}, + {$MessageCol.ImageBase64}) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """ + db.exec(query, + message.id, + message.whisperTimestamp, + message.source, + message.destination, + message.text, + message.contentType, + message.username, + message.timestamp, + message.chatId, + message.localChatId, + message.hide, + message.responseTo, + message.messageType, + message.clockValue, + message.seen, + message.outgoingStatus, + message.parsedText, + message.rawPayload, + message.stickerPack, + message.stickerHash, + message.commandId, + message.commandValue, + message.commandAddress, + message.commandFrom, + message.commandContract, + message.commandTransactionHash, + message.commandSignature, + message.commandState, + message.audioPayload, + message.audioType, + message.audioDurationMs, + message.audioBase64, + message.replaceMessage, + message.rtl, + message.lineCount, + message.links, + message.mentions, + message.imagePayload, + message.imageType, + message.imageBase64) + +proc deleteMessage*(db: DbConn, message: Message) = + let query = fmt""" + DELETE FROM user_messages where id = ?""" + + db.exec(query, message.id) + +proc markAllRead*(db: DbConn, chatId: string) = + let query = fmt""" + UPDATE user_messages SET seen = 1 WHERE local_chat_id = ? AND seen != 1""" + db.exec(query, chatId) + + let chatQuery = fmt""" + UPDATE chats SET unviewed_message_count = 0 WHERE id = ?""" + + db.exec(chatQuery, chatId) + +proc markMessagesSeen*(db: DbConn, chatId: string, messageIds: seq[string]) = + let quotedIds = sequtils.map(messageIds, proc(s:string):string = "'" & s & "'") + let inVector = strutils.join(quotedIds, ",") + let query = fmt"UPDATE user_messages SET seen = 1 WHERE id IN (" & inVector & ")" + + db.exec(query) + + let chatQuery = fmt""" + UPDATE chats SET unviewed_message_count = + (SELECT COUNT(1) + FROM user_messages + WHERE local_chat_id = ? AND seen = 0) + WHERE id = ?""" + + db.exec(chatQuery, chatId, chatId) diff --git a/test/nim/chats.nim b/test/nim/chats.nim new file mode 100644 index 0000000..e4ed753 --- /dev/null +++ b/test/nim/chats.nim @@ -0,0 +1,56 @@ +import # nim libs + os, json, options + +import # vendor libs + sqlcipher, json_serialization, web3/conversions as web3_conversions + +import # nim-status libs + ../../nim_status/lib/[chats, database, conversions] + +let passwd = "qwerty" +let path = currentSourcePath.parentDir() & "/build/myDatabase" +let db = initializeDB(path, passwd) + +var chat = Chat( + id: "local-chat-id", + name: "chat-name", + color: "blue", + chatType: 1, + active: true, + timestamp: 25, + deletedAtClockValue: 15, + publicKey: cast[seq[byte]]("public-key"), + unviewedMessageCount: 3, + lastClockValue: 18, + lastMessage: cast[seq[byte]]("lastMessage"), + members: cast[seq[byte]]("members"), + membershipUpdates: cast[seq[byte]]("membershipUpdates"), + profile: "profile", + invitationAdmin: "invitationAdmin", + muted: false, +) + +# saveChat +db.saveChat(chat) + +# getChats +chat.id = "local-chat-id-1" +db.saveChat(chat) +var dbChats = db.getChats() +assert len(dbChats) == 2 + +# getChatById +var dbChat = db.getChatById("local-chat-id").get() +assert dbChat.active == true and + dbChat.publicKey == cast[seq[byte]]("public-key") and + dbChat.unviewedMessageCount == 3 + +# deleteChat +db.deleteChat(chat) +dbChats = db.getChats() +assert len(dbChats) == 1 + + +db.close() +removeFile(path) + diff --git a/test/nim/messages.nim b/test/nim/messages.nim new file mode 100644 index 0000000..1d62730 --- /dev/null +++ b/test/nim/messages.nim @@ -0,0 +1,110 @@ +import # nim libs + os, json, options + +import # vendor libs + sqlcipher, json_serialization, web3/conversions as web3_conversions + +import # nim-status libs + ../../nim_status/lib/[messages, database, conversions, chats] + +let passwd = "qwerty" +let path = currentSourcePath.parentDir() & "/build/myDatabase" +let db = initializeDB(path, passwd) + +var msg = Message( + id: "msg1", + whisperTimestamp: 0, + source: "default_source", + destination: cast[seq[byte]]("default_destination"), + text: "text", + contentType: 0, + username: "user1", + timestamp: 25, + chatId: "chat-id", + localChatId: "local-chat-id", + hide: false, + responseTo: "user1", + messageType: 0, + clockValue: 0, + seen: false, + outgoingStatus: "Delivered", + parsedText: cast[seq[byte]]("parsed"), + rawPayload: cast[seq[byte]]("raw"), + stickerPack: 0, + stickerHash: "hash", + commandId: "command1", + commandValue: "commandValue", + commandAddress: "commandAddress", + commandFrom: "commandFrom", + commandContract: "commandContract", + commandTransactionHash: "commandTransactionHash", + commandSignature: cast[seq[byte]]("commandSignature"), + commandState: 3, + audioPayload: cast[seq[byte]]("audioPayload"), + audioType: 0, + audioDurationMs: 10, + audioBase64: "sdf", + replaceMessage: "message_replacement", + rtl: false, + lineCount: 5, + links: "links", + mentions: "mentions", + imagePayload: cast[seq[byte]]("blob"), + imageType: "type", + imageBase64: "sdfsdfsdf" +) + +# saveMessage +db.saveMessage(msg) + +# getMessageById +var dbMsg = db.getMessageById("msg1").get() +assert dbMsg.links == "links" and + dbMsg.timestamp == 25 and + dbMsg.username == "user1" + +# markAllRead +db.markAllRead("local-chat-id") +dbMsg = db.getMessageById("msg1").get() +assert dbMsg.seen == true + +# markMessagesSeen +msg.id = "msg2" +db.saveMessage(msg) + +var chat = Chat( + id: "local-chat-id", + name: "chat-name", + color: "blue", + chatType: 1, + active: true, + timestamp: 25, + deletedAtClockValue: 15, + publicKey: cast[seq[byte]]("public-key"), + unviewedMessageCount: 3, + lastClockValue: 18, + lastMessage: cast[seq[byte]]("lastMessage"), + members: cast[seq[byte]]("members"), + membershipUpdates: cast[seq[byte]]("membershipUpdates"), + profile: "profile", + invitationAdmin: "invitationAdmin", + muted: false, +) +db.saveChat(chat) +db.markMessagesSeen("local-chat-id", @["msg1", "msg2"]) +dbMsg = db.getMessageById("msg2").get() +var dbChat = db.getChatById("local-chat-id").get() +assert dbMsg.seen == true and dbChat.unviewedMessageCount == 0 + +# deleteMessage +db.deleteMessage(msg) +var found: bool +try: + discard db.getMessageById("msg2").get() + found = true +except: + found = false +assert found == false + +db.close() +removeFile(path)