diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8454dc7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/nim.cfg diff --git a/Tupfile b/Tupfile new file mode 100644 index 0000000..28c450e --- /dev/null +++ b/Tupfile @@ -0,0 +1,2 @@ +include_rules +: lock.json |> !nim_cfg |> | ./ diff --git a/Tuprules.tup b/Tuprules.tup index acc5cb1..e673b0a 100644 --- a/Tuprules.tup +++ b/Tuprules.tup @@ -1,3 +1,5 @@ +include ../eris-nim/depends.tup include ../syndicate-nim/depends.tup NIM_FLAGS += --path:$(TUP_CWD)/../syndicate-nim/src -NIM_FLAGS += --path:$(TUP_CWD)/../nimble/ws/src +NIM_FLAGS += --path:$(TUP_CWD)/../eris-nim/src +NIM_GROUPS += $(TUP_CWD)/ diff --git a/lock.json b/lock.json new file mode 100644 index 0000000..25030d0 --- /dev/null +++ b/lock.json @@ -0,0 +1 @@ +{"depends":[{"method":"fetchzip","path":"/nix/store/v03nzlpdgbfxd2zhcnkfbkq01d5kqxcl-source","rev":"84e0247555e4488594975900401baaf5bbbfb53","sha256":"1pfczsv8kl36qpv543f93d2y2vgz2acckssfap7l51s2x62m6qwx","url":"https://github.com/khchen/hashlib/archive/84e0247555e4488594975900401baaf5bbbfb53.tar.gz","packages":["hashlib"],"srcDir":""},{"method":"fetchzip","path":"/nix/store/008s11kkqscfqxs6g29q77c38pnrlppi-source","rev":"552e51899c82c0c2f4f466382be7d8e22a1da689","sha256":"1j3k0zlh5z02adhfvb7rdqz8fjzc6gri4v3v1fgcv2h2b7vrf0dg","url":"https://git.syndicate-lang.org/ehmry/syndicate-nim/archive/552e51899c82c0c2f4f466382be7d8e22a1da689.tar.gz","ref":"20231005","packages":["syndicate"],"srcDir":"src"},{"method":"fetchzip","path":"/nix/store/vx6ihnickx7d5lwy69i8k7fsjicv33r3-source","rev":"c915accf7d2a36ca1f323e2f02e2df7375e815f1","sha256":"11rlcbs9mvk335ibkbj8fk9aslhmnlaiqhcsjpp5n04k447sr7nx","url":"https://git.syndicate-lang.org/ehmry/preserves-nim/archive/c915accf7d2a36ca1f323e2f02e2df7375e815f1.tar.gz","ref":"20230914","packages":["preserves"],"srcDir":"src"},{"method":"fetchzip","path":"/nix/store/zyr8zwh7vaiycn1s4r8cxwc71f2k5l0h-source","rev":"602c5d20c69c76137201b5d41f788f72afb95aa8","sha256":"1dmdmgb6b9m5f8dyxk781nnd61dsk3hdxqks7idk9ncnpj9fng65","url":"https://github.com/cheatfate/nimcrypto/archive/602c5d20c69c76137201b5d41f788f72afb95aa8.tar.gz","ref":"traditional-api","packages":["nimcrypto"],"srcDir":""},{"method":"fetchzip","path":"/nix/store/ffkxmjmigfs7zhhiiqm0iw2c34smyciy-source","rev":"26d62fdc40feb84c6533956dc11d5ee9ea9b6c09","sha256":"0xpzifjkfp49w76qmaylan8q181bs45anmp46l4bwr3lkrr7bpwh","url":"https://github.com/zevv/npeg/archive/26d62fdc40feb84c6533956dc11d5ee9ea9b6c09.tar.gz","ref":"1.2.1","packages":["npeg"],"srcDir":"src"}]} diff --git a/message_types.prs b/message_types.prs deleted file mode 100644 index 6d9c3c3..0000000 --- a/message_types.prs +++ /dev/null @@ -1,21 +0,0 @@ -version 1 . - -Attributes = {symbol: any ...:...} . - -MIMEData = . - -ContactSubscription = { contact: Attributes } . -ContactSubscriptions2 = [ContactSubscription ...] . -ContactSubscriptions1 = { - contactSubscriptions: ContactSubscriptions2 - type: "contactSubSummary" -} . -ContactSubscriptions = { resp: ContactSubscriptions1 } . - -NewChatItem1 = { - chatInfo: Attributes - chatItem: Attributes - type: "newChatItem" -} . - -NewChatItem = { resp: NewChatItem1 } . diff --git a/shell.nix b/shell.nix index 160e711..24f8937 100644 --- a/shell.nix +++ b/shell.nix @@ -1,5 +1,2 @@ -let - syndicate = builtins.getFlake "syndicate"; - pkgs = - import { overlays = builtins.attrValues syndicate.overlays; }; -in pkgs.nimPackages.syndicate_utils +{ pkgs ? import { } }: +pkgs.nim2Packages.buildNimPackage { name = "dummy"; } diff --git a/simple_types.prs b/simple_types.prs index 2a50d82..11a7302 100644 --- a/simple_types.prs +++ b/simple_types.prs @@ -1,9 +1,8 @@ version 1 . embeddedType EntityRef.Cap . -ContactAssertion = . -GroupAssertion = . +Command = { cmd: string corrId: string }. -ReceivedMessage = . - -Content = . +Chat = . +Contact = . +Group = . diff --git a/simplex_bot_actor.nimble b/simplex_bot_actor.nimble index edcf7a3..1e248aa 100644 --- a/simplex_bot_actor.nimble +++ b/simplex_bot_actor.nimble @@ -1,6 +1,6 @@ bin = @["simplex_bot_actor"] license = "Unlicense" srcDir = "src" -version = "20230726" +version = "20231028" -requires: "nim", "syndicate", ws +requires "nim", "syndicate" diff --git a/src/simplex_bot_actor/Tupfile b/src/schema/Tupfile similarity index 100% rename from src/simplex_bot_actor/Tupfile rename to src/schema/Tupfile diff --git a/src/schema/simple_types.nim b/src/schema/simple_types.nim new file mode 100644 index 0000000..f9f87b0 --- /dev/null +++ b/src/schema/simple_types.nim @@ -0,0 +1,23 @@ + +import + preserves, std/tables + +type + Contact* {.preservesRecord: "contact".} = object + `field0`*: Table[Symbol, Preserve[void]] + + Command* {.preservesDictionary.} = object + `cmd`*: string + `corrId`*: string + + Chat* {.preservesRecord: "chat".} = object + `field0`*: Table[Symbol, Preserve[void]] + + Group* {.preservesRecord: "group".} = object + `field0`*: Table[Symbol, Preserve[void]] + +proc `$`*(x: Contact | Command | Chat | Group): string = + `$`(toPreserve(x)) + +proc encode*(x: Contact | Command | Chat | Group): seq[byte] = + encode(toPreserve(x)) diff --git a/src/simplex_bot_actor.nim b/src/simplex_bot_actor.nim index dd7a41e..e1a5757 100644 --- a/src/simplex_bot_actor.nim +++ b/src/simplex_bot_actor.nim @@ -1,30 +1,23 @@ # SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense -import std/[base64, streams, tables] -import preserves, syndicate +import std/[base64, options, os, tables] +import hashlib/misc/blake2 +import preserves +import syndicate, syndicate/relays -import ./simplex_bot_actor/[message_types, simple_types, websockets] +import ./schema/simple_types -type - Value = Preserve[void] +func step(pr: Assertion; path: varargs[string]): Option[Assertion] = + result = some(pr) + var index = "".toSymbol(Cap) + for s in path: + if result.isSome: + index.symbol = Symbol s + result = step(result.get, index) - ContactAssertion {.preservesRecord: "contact".} = object - id: int - info: Attributes - - GroupAssertion {.preservesRecord: "group".} = object - id: int - info: Attributes - - ChatItemAssertion {.preservesRecord: "chat-item".} = object - id: int - info: Attributes - - ContactSubscription {.preservesDictionary.} = object - contact: Attributes - ChatItemMeta {.preservesDictionary.} = object - itemId: int +proc sendCommand(turn: var Turn; ds: Cap; cmd: string) = + message(turn, ds, initRecord("send", Command(cmd: cmd).toPreserve)) proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern = ## Sugar for creating dictionary patterns. @@ -33,78 +26,81 @@ proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern = proc grabResp(obj: Pattern): Pattern = grabRecord("recv", %{ "resp": obj }) -proc bootClient(turn: var Turn; extern, intern: Cap) = +type + HandleTable = Table[Assertion, Handle] + State = ref object + ds, websocket: Cap + contacts, groups, chats: HandleTable - var contacts = initTable[int, Handle]() - proc updateContact(turn: var Turn; attrs: Attributes) = - var ass: ContactAssertion - if ass.id.fromPreserve(attrs.getOrDefault(Symbol"contactId")): - ass.info = attrs - contacts[ass.id] = replace(turn, extern, contacts.getOrDefault(ass.id), ass) +proc updateTable(turn: var Turn; state: State; table: var HandleTable; id, ass: Assertion) = + assert ass.isRecord + table[id] = replace(turn, state.ds, table.getOrDefault(id), ass) - var groups = newTable[int, Handle]() - proc updateGroup(turn: var Turn; attrs: Attributes) = - var ass: GroupAssertion - if ass.id.fromPreserve(attrs.getOrDefault(Symbol"groupId")): - ass.info = attrs - groups[ass.id] = replace(turn, extern, groups.getOrDefault(ass.id), ass) - - var chatItems = newTable[int, Handle]() - proc updateChatItem(turn: var Turn; attrs: Attributes) = +proc extractImagePath(image: Option[Assertion]): string = + const prefix = "data:image/jpg;base64," + if image.isNone: + result = "/dev/null" + else: var - ass: ChatItemAssertion - meta: ChatItemMeta - if meta.fromPreserve(attrs.getOrDefault(Symbol"meta")): - ass.id = meta.itemId - ass.info = attrs - chatItems[ass.id] = replace(turn, extern, chatItems.getOrDefault(ass.id), ass) + ctx = init[BLAKE2B_512]() + txt = image.get.string + bin = decode(txt[prefix.len..txt.high]) + ctx.update(bin) + var digest = $ctx.final() + result = getTempDir() / digest & ".png" + if not fileExists(result): + writeFile(result, bin) - block: - let dumpStream = openFileStream("/tmp/simplex_bot_actor.log", fmWrite) - onMessage(turn, intern, grab()) do (msg: Assertion): - # Dump messages to a log stream - writeText(dumpStream, msg) - write(dumpStream, '\n') - flush(dumpStream) +proc updateContact(turn: var Turn; state: State; id, attrs: Assertion) = + var + attrs = attrs + imagePath = attrs.step("profile", "image").extractImagePath + attrs["image".toSymbol(Cap)] = imagePath.toPreserve(Cap) + updateTable(turn, state, state.contacts, id, initRecord("contact", attrs)) - block: # contacts - let pat = grabResp(%{ - "contactSubscriptions": grab(), - "type": grab"contactSubSummary", - }) - debugEcho "grab contacts with ", pat - onMessage(turn, intern, pat) do (subs: seq[ContactSubscription]): - for sub in subs: updateContact(turn, sub.contact) +proc updateGroup(turn: var Turn; state: State; id, attrs: Assertion) = + var + attrs = attrs + imagePath = attrs.step("groupProfile", "image").extractImagePath + attrs["image".toSymbol(Cap)] = imagePath.toPreserve(Cap) + updateTable(turn, state, state.groups, id, initRecord("group", attrs)) - block: # groups - let pat = grabResp(%{ "groupInfo": grab() }) - onMessage(turn, intern, pat) do (groupInfo: Attributes): - updateGroup(turn, groupInfo) - block: - let pat = grabResp(%{ "chatItem": %{ "chatInfo": - %{ "groupInfo": grab() }}}) - onMessage(turn, intern, pat) do (groupInfo: Attributes): - updateGroup(turn, groupInfo) +proc updateChat(turn: var Turn; state: State; ass: Assertion) = + var id: Option[Assertion] + var info = ass.step("chatInfo", "contact") + if info.isSome: + id = info.get.step("contactId") + if id.isSome: + updateContact(turn, state, id.get, info.get) + else: + info = ass.step("chatInfo", "groupInfo") + if info.isSome: + id = info.get.step("groupId") + if id.isSome: + updateGroup(turn, state, id.get, info.get) + if id.isSome: + updateTable(turn, state, state.chats, id.get, initRecord("chat", ass)) - block: # messages - let pat = grabResp(%{ "chatItem": %{ "chatInfo": %{ "chatItem": grab() }}}) - onMessage(turn, intern, pat) do (chatItem: Attributes): - updateChatItem(turn, chatItem) +proc bootChats(turn: var Turn; state: State) = + let + chatPat = grabResp(%{ "chat": grab() }) + chatsPat = grabResp(%{ "chats": grab() }) + chatItemPat = grabResp(%{ "chatItem": grab() }) + onMessage(turn, state.websocket, chatItemPat) do (chat: Assertion): + updateChat(turn, state, chat) + onMessage(turn, state.websocket, chatPat) do (chat: Assertion): + updateChat(turn, state, chat) + onMessage(turn, state.websocket, chatsPat) do (chats: seq[Assertion]): + for chat in chats: + updateChat(turn, state, chat) + sendCommand(turn, state.websocket, "/chats") - onPublish(turn, extern, ContactAssertion ? {0: grab()}) do (contactId: int): - onPublish(turn, extern, ContactAssertion ? { - 0: grab(contactId), 1: %{ "localDisplayName": grab() }}) do (name: string): - debugEcho "contact ", contactId, " is ", name - -type Args {.preservesDictionary.} = object +type BootArgs {.preservesDictionary.} = object dataspace: Cap - url: string + websocket: Cap -runActor("eris_actor") do (root: Cap; turn: var Turn): - # connectStdio(root, turn) - spawnWebsocketJsonActor(turn, root) - during(turn, root, ?Args) do (extern: Cap, url: string): - during(turn, root, JsonWebsocketAssertion ? { 0: ?url, 1: grab() }) do (intern: Cap): - bootClient(turn, extern, intern) - - discard publish(turn, root, Args(dataspace: root, url: "ws://127.0.0.1:5225/")) +runActor("simplex_bot_actor") do (turn: var Turn, root: Cap): + connectStdio(turn, root) + during(turn, root, ?BootArgs) do (ds: Cap, websocket: Cap): + let state = State(ds: ds, websocket: websocket) + bootChats(turn, state) diff --git a/src/simplex_bot_actor/message_types.nim b/src/simplex_bot_actor/message_types.nim deleted file mode 100644 index 88d9d15..0000000 --- a/src/simplex_bot_actor/message_types.nim +++ /dev/null @@ -1,42 +0,0 @@ - -import - preserves, std/tables - -type - Attributes* = Table[Symbol, Preserve[void]] - MIMEData* {.preservesRecord: "mime".} = object - `type`*: Symbol - `data`*: seq[byte] - - NewChatItem* {.preservesDictionary.} = object - `resp`*: NewChatItem1 - - ContactSubscriptions* {.preservesDictionary.} = object - `resp`*: ContactSubscriptions1 - - NewChatItem1* {.preservesDictionary.} = object - `chatInfo`*: Attributes - `chatItem`*: Attributes - `type`* {.preservesLiteral: "\"newChatItem\"".}: tuple[] - - ContactSubscriptions1* {.preservesDictionary.} = object - `contactSubscriptions`*: ContactSubscriptions2 - `type`* {.preservesLiteral: "\"contactSubSummary\"".}: tuple[] - - ContactSubscriptions2* = seq[ContactSubscription] - ContactSubscription* {.preservesDictionary.} = object - `contact`*: Attributes - -proc `$`*(x: Attributes | MIMEData | NewChatItem | ContactSubscriptions | - NewChatItem1 | - ContactSubscriptions1 | - ContactSubscriptions2 | - ContactSubscription): string = - `$`(toPreserve(x)) - -proc encode*(x: Attributes | MIMEData | NewChatItem | ContactSubscriptions | - NewChatItem1 | - ContactSubscriptions1 | - ContactSubscriptions2 | - ContactSubscription): seq[byte] = - encode(toPreserve(x)) diff --git a/src/simplex_bot_actor/simple_types.nim b/src/simplex_bot_actor/simple_types.nim deleted file mode 100644 index a8d2029..0000000 --- a/src/simplex_bot_actor/simple_types.nim +++ /dev/null @@ -1,27 +0,0 @@ - -import - preserves - -type - ContactAssertion* {.preservesRecord: "contact".} = object - `id`*: BiggestInt - `cap`* {.preservesEmbedded.}: Preserve[void] - - ReceivedMessage* {.preservesRecord: "message".} = object - `prevId`*: Preserve[void] - `msgId`*: Preserve[void] - `content`*: Content - - Content* {.preservesRecord: "text".} = object - `text`*: string - - GroupAssertion* {.preservesRecord: "group".} = object - `id`*: BiggestInt - `cap`* {.preservesEmbedded.}: Preserve[void] - -proc `$`*(x: ContactAssertion | ReceivedMessage | Content | GroupAssertion): string = - `$`(toPreserve(x)) - -proc encode*(x: ContactAssertion | ReceivedMessage | Content | GroupAssertion): seq[ - byte] = - encode(toPreserve(x)) diff --git a/src/simplex_bot_actor/websockets.nim b/src/simplex_bot_actor/websockets.nim deleted file mode 100644 index 355237f..0000000 --- a/src/simplex_bot_actor/websockets.nim +++ /dev/null @@ -1,73 +0,0 @@ -# SPDX-FileCopyrightText: ☭ Emery Hemingway -# SPDX-License-Identifier: Unlicense - -import std/[asyncdispatch, json] -import preserves, preserves/jsonhooks -import syndicate, syndicate/actors -import ws - -type JsonWebsocketAssertion* {.preservesRecord: "json-websocket".} = object - url: string - dataspace: Cap - -type - SendJson* {.preservesRecord: "send".} = object - data: JsonNode - RecvJson* {.preservesRecord: "recv".} = object - data: JsonNode - -proc spawnWebsocketJsonActor*(turn: var Turn; ds: Cap): Actor {.discardable.} = - ## Spawn an actor that responds to observations of - ## `` - ## by connecting to Websocket urls and publishing dataspaces - ## that carry messages to and from the Websocket endpoint. - spawn("json-websocket-actor", turn) do (turn: var Turn): - during(turn, ds, ?Observe(pattern: !JsonWebsocketAssertion) ?? {0: grabLit()}) do (url: string): - var ws: WebSocket - newWebSocket(url).addCallback(turn) do (turn: var Turn; sock: WebSocket): - ws = sock - let - facet = turn.facet - messageSpace = newDataspace(turn) - handle = publish(turn, ds, - JsonWebsocketAssertion(url: url, dataspace: messageSpace)) - onStop(facet) do (turn: var Turn): - close(ws) - var fut: Future[(Opcode, string)] - proc recvMessage() {.gcsafe.} = - fut = receivePacket ws - addCallback(fut, facet) do (turn: var Turn): - let (opcode, data) = read fut - case opcode - of Text: - message(turn, messageSpace, - RecvJson(data: data.parseJson)) - of Binary: - message(turn, messageSpace, - initRecord("recv", cast[seq[byte]](data).toPreserve)) - of Ping: - asyncCheck(turn, ws.send(data, Pong)) - of Pong, Cont: - discard - of Close: - stderr.writeLine "closed connection with ", url - retract(turn, handle) - stop(turn) - return - recvMessage() - recvMessage() - onMessage(turn, messageSpace, ?SendJson) do (data: JsonNode): - asyncCheck(turn, ws.send($data, Text)) - do: - close(ws) - -when isMainModule: - # Run as an independent component. - - type Args {.preservesDictionary.} = object - dataspace: Cap - url: string - - runActor("websocket-json-actor") do (root: Cap; turn: var Turn): - connectStdio(root, turn) - spawnWebsocketJsonActor(turn, root) diff --git a/tests/test_grab.nim b/tests/test_grab.nim deleted file mode 100644 index b399a7f..0000000 --- a/tests/test_grab.nim +++ /dev/null @@ -1,214 +0,0 @@ -# SPDX-FileCopyrightText: ☭ Emery Hemingway -# SPDX-License-Identifier: Unlicense - -import std/[unittest] -import preserves, syndicate - -proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern = - ## Sugar for creating dictionary patterns. - patterns.grabDictionary(bindings) - -suite "chatItem": - let data = parsePreserves(""" -{ - resp: { - chatItem: { - chatInfo: { - contact: { - activeConn: { - agentConnId: "TGRmT3B6UUFRalVGSHVtVg==" - authErrCounter: 0 - connId: 56 - connLevel: 1 - connStatus: "ready" - connType: "contact" - createdAt: "2023-07-24T19:57:10.282893401Z" - entityId: 19 - localAlias: "" - viaContact: 21 - viaGroupLink: false - } - chatSettings: { - enableNtfs: true - } - chatTs: "2023-07-25T12:41:47.886547076Z" - contactId: 19 - contactUsed: true - createdAt: "2023-05-26T06:46:48.95069518Z" - localDisplayName: "bidston-burner" - mergedPreferences: { - calls: { - contactPreference: { - allow: "yes" - } - enabled: { - forContact: true - forUser: true - } - userPreference: { - preference: { - allow: "yes" - } - type: "user" - } - } - fullDelete: { - contactPreference: { - allow: "no" - } - enabled: { - forContact: false - forUser: false - } - userPreference: { - preference: { - allow: "no" - } - type: "user" - } - } - reactions: { - contactPreference: { - allow: "yes" - } - enabled: { - forContact: true - forUser: true - } - userPreference: { - preference: { - allow: "yes" - } - type: "user" - } - } - timedMessages: { - contactPreference: { - allow: "yes" - } - enabled: { - forContact: true - forUser: true - } - userPreference: { - preference: { - allow: "yes" - } - type: "user" - } - } - voice: { - contactPreference: { - allow: "yes" - } - enabled: { - forContact: true - forUser: true - } - userPreference: { - preference: { - allow: "yes" - } - type: "user" - } - } - } - profile: { - displayName: "bidston-burner" - fullName: "BOARC mobile phone" - image: - localAlias: "" - preferences: { - calls: { - allow: "yes" - } - fullDelete: { - allow: "no" - } - timedMessages: { - allow: "yes" - } - voice: { - allow: "yes" - } - } - profileId: 19 - } - updatedAt: "2023-07-24T20:24:50.412385408Z" - userPreferences: {} - } - type: "direct" - } - chatItem: { - chatDir: { - type: "directRcv" - } - content: { - msgContent: { - text: "Ping" - type: "text" - } - type: "rcvMsgContent" - } - meta: { - createdAt: "2023-07-25T12:42:00.474365917Z" - editable: false - itemDeleted: null - itemEdited: false - itemId: 2748 - itemLive: null - itemSharedMsgId: "eDhCSzFMY04xODFzaklDMA==" - itemStatus: { - type: "rcvNew" - } - itemText: "Ping" - itemTimed: null - itemTs: "2023-07-25T12:42:00Z" - localItemTs: "2023-07-25T13:42:00+01:00" - updatedAt: "2023-07-25T12:42:00.474365917Z" - } - reactions: [] - } - } - type: "newChatItem" - user: { - activeUser: true - agentUserId: "1" - fullPreferences: { - calls: { - allow: "yes" - } - fullDelete: { - allow: "no" - } - reactions: { - allow: "yes" - } - timedMessages: { - allow: "yes" - } - voice: { - allow: "yes" - } - } - localDisplayName: "emery" - profile: { - displayName: "emery" - fullName: "Emery (laptop)" - localAlias: "" - profileId: 1 - } - showNtfs: true - userContactId: 1 - userId: 1 - } - } -} -""", Cap) - - let pat = %{"resp": %{"chatItem": %{ - "chatInfo": %{"contact": %{"contactId": grab(19)}}, - "chatItem": grab(), - }}} - - check matches(pat, data)