121 lines
4.2 KiB
Nim
121 lines
4.2 KiB
Nim
# 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)
|