Simplify
This commit is contained in:
parent
c71ad44e3d
commit
19ce50b98b
|
@ -0,0 +1 @@
|
|||
/nim.cfg
|
|
@ -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>
|
||||
|
|
|
@ -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"}]}
|
|
@ -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 } .
|
|
@ -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"; }
|
||||
|
|
|
@ -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 ...:...}> .
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
bin = @["simplex_bot_actor"]
|
||||
license = "Unlicense"
|
||||
srcDir = "src"
|
||||
version = "20230726"
|
||||
version = "20231028"
|
||||
|
||||
requires: "nim", "syndicate", ws
|
||||
requires "nim", "syndicate"
|
||||
|
|
|
@ -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))
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
|
@ -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))
|
|
@ -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
Loading…
Reference in New Issue