simplex_bot_actor/src/simplex_bot_actor.nim

107 lines
3.5 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] =
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) =
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 =
grabRecord("recv", %{ "resp": obj })
type
HandleTable = Table[Assertion, Handle]
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 =
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) =
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) =
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) =
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) =
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")
type BootArgs {.preservesDictionary.} = object
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)