Compare commits
2 Commits
1f9f3fb03a
...
85648f4956
Author | SHA1 | Date |
---|---|---|
Emery Hemingway | 85648f4956 | |
Emery Hemingway | 3aea5c1696 |
|
@ -1,33 +1,21 @@
|
|||
version 1 .
|
||||
|
||||
Attributes = {string: any ...:...} .
|
||||
Attributes = {symbol: any ...:...} .
|
||||
|
||||
Resp = <resp @time RespItem> .
|
||||
MIMEData = <mime @type symbol @data bytes> .
|
||||
|
||||
RespItem = NewChatItem / Attributes .
|
||||
ContactSubscription = { contact: Attributes } .
|
||||
ContactSubscriptions2 = [ContactSubscription ...] .
|
||||
ContactSubscriptions1 = {
|
||||
contactSubscriptions: ContactSubscriptions2
|
||||
type: "contactSubSummary"
|
||||
} .
|
||||
ContactSubscriptions = { resp: ContactSubscriptions1 } .
|
||||
|
||||
NewChatItem = <newChatItem {"chatItem": ChatItem0 }> .
|
||||
|
||||
ChatItem0 = {
|
||||
"chatInfo": ChatInfo
|
||||
"user": User
|
||||
NewChatItem1 = {
|
||||
chatInfo: Attributes
|
||||
chatItem: Attributes
|
||||
type: "newChatItem"
|
||||
} .
|
||||
|
||||
ChatInfo = {
|
||||
"chatInfo": ChatInfo
|
||||
"chatItem": ChatItem1
|
||||
} .
|
||||
|
||||
ChatItem1 = {
|
||||
"content": Content
|
||||
} .
|
||||
|
||||
Content = Attributes .
|
||||
|
||||
User = {
|
||||
"activeUser": bool
|
||||
"agentUserId": string
|
||||
"localDisplayName": string
|
||||
"userContactId": int
|
||||
"userId": int
|
||||
} .
|
||||
NewChatItem = { resp: NewChatItem1 } .
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
version 1 .
|
||||
embeddedType EntityRef.Cap .
|
||||
|
||||
ContactAssertion = <contact @id int @cap #!any> .
|
||||
GroupAssertion = <group @id int @cap #!any> .
|
||||
|
||||
ReceivedMessage = <message @prevId any @msgId any @content Content> .
|
||||
|
||||
Content = <text @text string> .
|
|
@ -1,6 +1,6 @@
|
|||
bin = @["simplex_bot_actor"]
|
||||
license = "Unlicense"
|
||||
srcDir = "src"
|
||||
version = "20230723"
|
||||
version = "20230726"
|
||||
|
||||
requires: "nim", "syndicate", ws
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
include_rules
|
||||
: foreach ../*.prs |> !preserves_schema_nim |> {schema}
|
||||
: simplex_bot_actor.nim | $(SYNDICATE_PROTOCOL) {schema} |> !nim_bin |> {bin}
|
||||
: simplex_bot_actor.nim | $(SYNDICATE_PROTOCOL) ../<schema> |> !nim_bin |> {bin}
|
||||
: {bin} |> !assert_built |>
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
|
||||
import
|
||||
preserves, std/tables
|
||||
|
||||
type
|
||||
Resp* {.preservesRecord: "resp".} = ref object
|
||||
`time`*: RespItem
|
||||
|
||||
RespItemKind* {.pure.} = enum
|
||||
`NewChatItem`, `Attributes`
|
||||
`RespItem`* {.preservesOr.} = ref object
|
||||
case orKind*: RespItemKind
|
||||
of RespItemKind.`NewChatItem`:
|
||||
`newchatitem`*: NewChatItem
|
||||
|
||||
of RespItemKind.`Attributes`:
|
||||
`attributes`*: Attributes
|
||||
|
||||
|
||||
Attributes* = Table[string, Preserve[void]]
|
||||
NewChatItemField0* {.preservesDictionary.} = ref object
|
||||
`"chatItem"`*: ChatItem0
|
||||
|
||||
NewChatItem* {.preservesRecord: "newChatItem".} = ref object
|
||||
`field0`*: NewChatItemField0
|
||||
|
||||
ChatInfo* {.preservesDictionary.} = ref object
|
||||
`"chatInfo"`*: ChatInfo
|
||||
`"chatItem"`*: ChatItem1
|
||||
|
||||
Content* = Attributes
|
||||
ChatItem0* {.preservesDictionary.} = ref object
|
||||
`"chatInfo"`*: ChatInfo
|
||||
`"user"`*: User
|
||||
|
||||
User* {.preservesDictionary.} = object
|
||||
`"activeUser"`*: bool
|
||||
`"agentUserId"`*: string
|
||||
`"localDisplayName"`*: string
|
||||
`"userContactId"`*: BiggestInt
|
||||
`"userId"`*: BiggestInt
|
||||
|
||||
ChatItem1* {.preservesDictionary.} = object
|
||||
`"content"`*: Content
|
||||
|
||||
proc `$`*(x: Resp | RespItem | Attributes | NewChatItem | ChatInfo | Content |
|
||||
ChatItem0 |
|
||||
User |
|
||||
ChatItem1): string =
|
||||
`$`(toPreserve(x))
|
||||
|
||||
proc encode*(x: Resp | RespItem | Attributes | NewChatItem | ChatInfo | Content |
|
||||
ChatItem0 |
|
||||
User |
|
||||
ChatItem1): seq[byte] =
|
||||
encode(toPreserve(x))
|
|
@ -1,58 +0,0 @@
|
|||
# SPDX-FileCopyrightText: ☭ Emery Hemingway
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
import std/[base64, json, parseutils, tables]
|
||||
import preserves
|
||||
|
||||
proc toPreserveHook*(js: JsonNode; E: typedesc): Preserve[E] {.gcsafe.} =
|
||||
case js.kind
|
||||
of JString:
|
||||
var off = js.str.skip("data:")
|
||||
if off == 0:
|
||||
result = Preserve[E](kind: pkString, string: js.str)
|
||||
else:
|
||||
var mime: string
|
||||
off.inc js.str.parseUntil(mime, ';', off)
|
||||
var dataOff = off + js.str.skip(";base64,", off)
|
||||
if dataOff != off:
|
||||
var data = cast[seq[byte]](substr(js.str, dataOff).decode)
|
||||
result = initRecord("mime", mime.toSymbol(E), data.toPreserve(E))
|
||||
else:
|
||||
result = Preserve[E](kind: pkString, string: js.str)
|
||||
of JInt:
|
||||
result = Preserve[E](kind: pkSignedInteger, int: js.num)
|
||||
of JFloat:
|
||||
result = Preserve[E](kind: pkDouble, double: js.fnum)
|
||||
of JBool:
|
||||
result = case js.bval
|
||||
of false: toSymbol("false", E)
|
||||
of true: toSymbol("true", E)
|
||||
of JNull:
|
||||
result = toSymbol("null", E)
|
||||
of JArray:
|
||||
result = Preserve[E](kind: pkSequence,
|
||||
sequence: newSeq[Preserve[E]](js.elems.len))
|
||||
for i, e in js.elems:
|
||||
result.sequence[i] = toPreserveHook(e, E)
|
||||
of JObject:
|
||||
if js.hasKey("type"):
|
||||
var label = js.getOrDefault("type").getStr.toSymbol(E)
|
||||
if js.len == 1:
|
||||
result = initRecord(label)
|
||||
elif js.len == 2:
|
||||
for key, val in js.fields.pairs:
|
||||
if key != "type":
|
||||
result = initRecord(label, val.toPreserve(E))
|
||||
else:
|
||||
var dict = Preserve[E](kind: pkDictionary)
|
||||
for key, val in js.fields.pairs:
|
||||
if key != "type":
|
||||
dict[Preserve[E](kind: pkString, string: key)] = toPreserveHook(val, E)
|
||||
result = initRecord(label, dict)
|
||||
elif js.len == 1:
|
||||
for key, val in js.fields.pairs:
|
||||
result = initRecord(key, val.toPreserve(E))
|
||||
else:
|
||||
result = Preserve[E](kind: pkDictionary)
|
||||
for key, val in js.fields.pairs:
|
||||
result[Preserve[E](kind: pkString, string: key)] = toPreserveHook(val, E)
|
|
@ -1,52 +1,137 @@
|
|||
# SPDX-FileCopyrightText: ☭ Emery Hemingway
|
||||
# SPDX-License-Identifier: Unlicense
|
||||
|
||||
import std/[asyncdispatch, json, sequtils, streams, tables, uri]
|
||||
import std/[base64, streams, tables]
|
||||
import preserves, syndicate
|
||||
import ws
|
||||
|
||||
import private/jsonhooks
|
||||
import ./simplex_bot_actor/[message_types, simple_types, websockets]
|
||||
|
||||
type
|
||||
Value = Preserve[void]
|
||||
|
||||
Args {.preservesDictionary.} = object
|
||||
dataspace: Ref
|
||||
dataspace: Cap
|
||||
url: string
|
||||
|
||||
ContactSubscription {.preservesDictionary.} = object
|
||||
contact: Attributes
|
||||
|
||||
Internal* {.preservesRecord: "internal".} = object
|
||||
dataspace: Ref
|
||||
dataspace: Cap
|
||||
|
||||
proc recvMessages(facet: Facet; ds: Ref; ws: WebSocket) =
|
||||
var fut: Future[string]
|
||||
proc recvMessage() {.gcsafe.} =
|
||||
fut = receiveStrPacket ws
|
||||
addCallback(fut, facet) do (turn: var Turn):
|
||||
message(turn, ds, fut.read.parseJson)
|
||||
recvMessage()
|
||||
recvMessage()
|
||||
Contact = ref object
|
||||
# TODO: does a Contact get its own facet
|
||||
dataspace: Cap
|
||||
capHandle, summaryHandle, profileHandle, chatItemHandle: Handle
|
||||
|
||||
proc bootClient(turn: var Turn; ds: Ref; ws: WebSocket) =
|
||||
let dumpStream = openFileStream("/tmp/simplex_bot_actor.log", fmWrite)
|
||||
let intern = newDataspace(turn)
|
||||
discard publish(turn, ds, Internal(dataspace: intern))
|
||||
recvMessages(turn.facet, intern, ws)
|
||||
onMessage(turn, intern, grab()) do (msg: Assertion):
|
||||
writeText(dumpStream, msg)
|
||||
write(dumpStream, '\n')
|
||||
flush(dumpStream)
|
||||
Group = ref object
|
||||
dataspace: Cap
|
||||
capHandle, infoHandle: Handle
|
||||
|
||||
proc boot*(root: Ref; turn: var Turn) =
|
||||
during(turn, root, ?Args) do (dataspace: Ref, url: string):
|
||||
debugEcho "got dataspace ", dataspace, " and URL ", url
|
||||
var ws: WebSocket
|
||||
newWebSocket(url).addCallback(turn) do (turn: var Turn; s: WebSocket):
|
||||
ws = s
|
||||
debugecho "connected to ", url
|
||||
bootClient(turn, dataspace, ws)
|
||||
do:
|
||||
close ws
|
||||
# ContactAssertion = simple_types.ContactAssertion[Cap]
|
||||
ContactAssertion {.preservesRecord: "contact".} = object
|
||||
id: int
|
||||
cap: Cap
|
||||
|
||||
when isMainModule:
|
||||
runActor("eris_actor") do (root: Ref; turn: var Turn):
|
||||
# connectStdio(root, turn)
|
||||
boot(root, turn)
|
||||
discard publish(turn, root, Args(dataspace: root, url: "ws://127.0.0.1:5225/"))
|
||||
GroupAssertion {.preservesRecord: "group".} = object
|
||||
id: int
|
||||
cap: Cap
|
||||
|
||||
proc updateAttrs(contact: Contact; turn: var Turn; attrs: Attributes) =
|
||||
replace(turn, contact.dataspace, contact.summaryHandle, attrs)
|
||||
var profile = attrs.getOrDefault(Symbol"profile")
|
||||
if not profile.isFalse:
|
||||
replace(turn, contact.dataspace, contact.profileHandle, profile)
|
||||
|
||||
proc updateAttrs(group: Group; turn: var Turn; info: Attributes) =
|
||||
replace(turn, group.dataspace, group.infoHandle, info)
|
||||
|
||||
proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern =
|
||||
## Sugar for creating dictionary patterns.
|
||||
patterns.grabDictionary(bindings)
|
||||
|
||||
proc bootContact(turn: var Turn; intern: Cap; contactId: int): Contact =
|
||||
let contact = Contact(dataspace: newDataspace(turn))
|
||||
block:
|
||||
let pat = grabRecord("recv", %{"resp": %{"contactUpdated": %{
|
||||
"fromContact": %{"contactId": grab(contactId)},
|
||||
"toContact": grab(),
|
||||
}}})
|
||||
onMessage(turn, intern, pat) do (attrs: Attributes):
|
||||
updateAttrs(contact, turn, attrs)
|
||||
|
||||
block:
|
||||
let pat = grabRecord("recv", %{"resp": %{"chatItem": %{
|
||||
"chatInfo": %{"contact": %{"contactId": grab(contactId)}},
|
||||
"chatItem": grab(),
|
||||
}}}) # TODO: could update contact profiles from these messages
|
||||
onMessage(turn, intern, pat) do (attrs: Attributes):
|
||||
var
|
||||
msgId = cast[seq[byte]](base64.decode(attrs[Symbol"meta"]["itemSharedMsgId".toSymbol].string))
|
||||
msg = initRecord("message", Preserve[void](), msgId.toPreserve, attrs[Symbol"content"])
|
||||
debugEcho "publish message ", msg
|
||||
contact.chatItemHandle = publish(turn, contact.dataspace, msg)
|
||||
|
||||
contact
|
||||
|
||||
proc bootGroup(turn: var Turn; intern: Cap; groupId: int): Group =
|
||||
let group = Group(dataspace: newDataspace(turn))
|
||||
group
|
||||
|
||||
proc bootClient(turn: var Turn; ds, intern: Cap) =
|
||||
var
|
||||
contacts = initTable[int, Contact]()
|
||||
groups = initTable[int, Group]()
|
||||
# mapping of contactId to Contact data
|
||||
|
||||
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)
|
||||
|
||||
block: # concats
|
||||
let pat = grabRecord("recv", %{"resp": %{
|
||||
"contactSubscriptions": grab(),
|
||||
"type": grab"contactSubSummary",
|
||||
}})
|
||||
onMessage(turn, intern, pat) do (subs: seq[ContactSubscription]):
|
||||
for e in subs:
|
||||
var id: int
|
||||
if id.fromPreserve(e.contact[Symbol"contactId"]):
|
||||
var contact = contacts.getOrDefault(id)
|
||||
if contact.isNil:
|
||||
contact = bootContact(turn, intern, id)
|
||||
contacts[id] = contact
|
||||
contact.capHandle = publish(turn, ds,
|
||||
ContactAssertion(id: id, cap: contact.dataspace))
|
||||
updateAttrs(contact, turn, e.contact)
|
||||
|
||||
block: # groups
|
||||
let pat = grabRecord("recv", %{"resp": %{"groupInfo": grab()}})
|
||||
onMessage(turn, intern, pat) do (info: Attributes):
|
||||
var id: int
|
||||
if id.fromPreserve(info[Symbol"groupId"]):
|
||||
var group = groups.getOrDefault(id)
|
||||
if group.isNil:
|
||||
group = bootGroup(turn, intern, id)
|
||||
groups[id] = group
|
||||
group.capHandle = publish(turn, ds,
|
||||
GroupAssertion(id: id, cap: group.dataspace))
|
||||
updateAttrs(group, turn, info)
|
||||
|
||||
onPublish(turn, ds, ?ContactAssertion) do (contactId: int; cap: Cap):
|
||||
onPublish(turn, cap, %{"localDisplayName": grab()}) do (name: string):
|
||||
onPublish(turn, cap, %{"image": ?MIMEData}) do (typ: Symbol, data: seq[byte]):
|
||||
debugEcho "contact ", name, " has an image of ", data.len, " bytes"
|
||||
|
||||
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/"))
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
include_rules
|
||||
: foreach ../../*.prs |> !preserves_schema_nim |> | ../../<schema>
|
|
@ -0,0 +1,42 @@
|
|||
|
||||
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))
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
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))
|
|
@ -0,0 +1,72 @@
|
|||
# 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:
|
||||
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)
|
|
@ -0,0 +1,2 @@
|
|||
include_rules
|
||||
: foreach test*.nim | $(SYNDICATE_PROTOCOL) ../<protocol> |> !nim_run |>
|
|
@ -0,0 +1 @@
|
|||
switch("path", "$projectDir/../src")
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue