# SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[base64, options, os, tables] import hashlib/misc/blake2 import preserves import syndicate, syndicate/relays import ./schema/simple_types func step(pr: Assertion; path: varargs[string]): Option[Assertion] = ## Convenience fuction for traversing levels of JSON hell. result = some(pr) var index = "".toSymbol(Cap) for s in path: if result.isSome: index.symbol = Symbol s result = step(result.get, index) proc sendCommand(turn: var Turn; ds: Cap; cmd: string) = ## Simplex websocket only accepts command ## strings in the format of the CLI client. message(turn, ds, initRecord("send", Command(cmd: cmd).toPreserve)) proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern = ## Sugar for creating dictionary patterns. patterns.grabDictionary(bindings) proc grabResp(obj: Pattern): Pattern = ## Grab the "resp" out of messages received on the websocket. grabRecord("recv", %{ "resp": obj }) type HandleTable = Table[Assertion, Handle] ## Table for mapping Simplex identifiers to Syndicate handles. State = ref object ds, websocket: Cap contacts, groups, chats: HandleTable 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) proc extractImagePath(image: Option[Assertion]): string = ## Decode an image and return a content-addressed file- ## system path. Otherwise "/dev/null". const prefix = "data:image/jpg;base64," if image.isNone: result = "/dev/null" else: var 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) proc updateContact(turn: var Turn; state: State; id, attrs: Assertion) = ## Update the contact assertion held in the dataspace. 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)) proc updateGroup(turn: var Turn; state: State; id, attrs: Assertion) = ## Update the group assertion held in the dataspace. 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)) proc updateChat(turn: var Turn; state: State; ass: Assertion) = ## Update the chat assertion held in the dataspace. 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)) proc bootChats(turn: var Turn; state: State) = ## Install observers and seed the dataspace. 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") # initial chats request to populate dataspace type BootArgs {.preservesDictionary.} = object # Arguments passed by the Syndicate server on stdin. dataspace: Cap websocket: Cap 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)