This commit is contained in:
Emery Hemingway 2023-10-28 00:23:32 +01:00
parent c71ad44e3d
commit 19ce50b98b
15 changed files with 120 additions and 476 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/nim.cfg

2
Tupfile Normal file
View File

@ -0,0 +1,2 @@
include_rules
: lock.json |> !nim_cfg |> | ./<lock>

View File

@ -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)/<lock>

1
lock.json Normal file
View File

@ -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"}]}

View File

@ -1,21 +0,0 @@
version 1 .
Attributes = {symbol: any ...:...} .
MIMEData = <mime @type symbol @data bytes> .
ContactSubscription = { contact: Attributes } .
ContactSubscriptions2 = [ContactSubscription ...] .
ContactSubscriptions1 = {
contactSubscriptions: ContactSubscriptions2
type: "contactSubSummary"
} .
ContactSubscriptions = { resp: ContactSubscriptions1 } .
NewChatItem1 = {
chatInfo: Attributes
chatItem: Attributes
type: "newChatItem"
} .
NewChatItem = { resp: NewChatItem1 } .

View File

@ -1,5 +1,2 @@
let
syndicate = builtins.getFlake "syndicate";
pkgs =
import <nixpkgs> { overlays = builtins.attrValues syndicate.overlays; };
in pkgs.nimPackages.syndicate_utils
{ pkgs ? import <nixpkgs> { } }:
pkgs.nim2Packages.buildNimPackage { name = "dummy"; }

View File

@ -1,9 +1,8 @@
version 1 .
embeddedType EntityRef.Cap .
ContactAssertion = <contact @id int @cap #!any> .
GroupAssertion = <group @id int @cap #!any> .
Command = { cmd: string corrId: string }.
ReceivedMessage = <message @prevId any @msgId any @content Content> .
Content = <text @text string> .
Chat = <chat {symbol: any ...:...}> .
Contact = <contact {symbol: any ...:...}> .
Group = <group {symbol: any ...:...}> .

View File

@ -1,6 +1,6 @@
bin = @["simplex_bot_actor"]
license = "Unlicense"
srcDir = "src"
version = "20230726"
version = "20231028"
requires: "nim", "syndicate", ws
requires "nim", "syndicate"

View File

@ -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))

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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
## `<json-websocket @url string @dataspace #!Ref>`
## 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)

File diff suppressed because one or more lines are too long