Schema rewrite

This commit is contained in:
Emery Hemingway 2023-07-26 11:35:59 +01:00
parent 3aea5c1696
commit 85648f4956
14 changed files with 477 additions and 706 deletions

View File

@ -1,224 +1,21 @@
version 1 .
AllowNo = <allow "no"> .
AllowYes = <allow "yes"> .
Allow = AllowNo / AllowYes .
UserAllow = <user Allow> .
EnableOffOn = EnableOff / EnableOn .
EnableOff = <enable "off"> .
EnableOn = <enable "on"> .
EnableNtfs = <enableNtfs bool> .
MIMEData = <mime @type symbol @data bytes> .
Contact = <contact @num int> .
Origin =
/ Contact
/ <user>
/ <unknown> .
Attributes = {symbol: any ...:...} .
Resp = <resp RespItem> .
MIMEData = <mime @type symbol @data bytes> .
RespItem =
/ TimedAction
/ ContactSubSummary
/ GroupSubscribed
/ MemberSubSummary
/ PendingSubSummary
/ NewChatItem
/ Attributes .
ContactSubscription = { contact: Attributes } .
ContactSubscriptions2 = [ContactSubscription ...] .
ContactSubscriptions1 = {
contactSubscriptions: ContactSubscriptions2
type: "contactSubSummary"
} .
ContactSubscriptions = { resp: ContactSubscriptions1 } .
TimedAction = <timedAction {
action: string
durationMilliseconds: int
}> .
ContactSubSummary = <contactSubSummary {
contactSubscriptions: [ContactSubscription ...]
user: User
}> .
ContactSubscription = <contact @attrs Attributes> .
ContactSubscriptionAttributes = {
activeConn: ActiveConn
chatSettings: EnableNtfs
chatTs: string
contactId: int
contactUsed: bool
createdAt: string
localDisplayName: string
mergedPreferences: MergedPreferences
profile: Profile
updatedAt: string
userPreferences: AllowPreferences
NewChatItem1 = {
chatInfo: Attributes
chatItem: Attributes
type: "newChatItem"
} .
ActiveConn = {
agentConnId: string
authErrCounter: int
connId: int
connLevel: int
connStatus: string
connType: string
createdAt: string
entityId: int
localAlias: string
viaGroupLink: bool
} .
MergedPreferences = {
calls: ContactPreference
fullDelete: ContactPreference
reactions: ContactPreference
timedMessages: ContactPreference
voice: ContactPreference
} .
ContactPreference = {
contactPreference: Allow
enabled: ContactEnabled
userPreference: UserAllow
} .
ContactEnabled = {
forContact: bool
forUser: bool
} .
AllowPreferences = {string: Allow ...:...} .
GroupSubscribed = <groupSubscribed {
groupInfo: GroupInfo
user: LocalUser
}> .
GroupInfo = {
chatSettings: EnableNtfs
chatTs: "2023-07-22T11:54:25.14370346Z"
createdAt: "2023-07-22T11:52:01.935384979Z"
fullGroupPreferences: FullGroupPreferences
groupId: 6
groupProfile: Profile
hostConnCustomUserProfileId: any
localDisplayName: string
membership: MemberShip
updatedAt: string
} .
FullGroupPreferences = {
directMessages: EnableOffOn
fullDelete: EnableOffOn
reactions: EnableOffOn
timedMessages: TimedMessages
voice: EnableOffOn
} .
TimedMessages = {
enable: string ; "off"/"on"
ttl: int
} .
MemberShip = {
groupId: int
groupMemberId: int
invitedBy: Origin
localDisplayName: string
memberCategory: string
memberContactId: int
memberContactProfileId: int
memberId: string
memberProfile: Profile
memberRole: string
memberStatus: string
} .
Profile = {
displayName: string
fullName: string
} .
BigProfile = {
displayName: string
fullName: string
localAlias: string
profileId: int
}.
LargeProfile = {
displayName: string
fullName: string
image: MIMEData
localAlias: string
preferences: AllowPreferences
profileId: int
} .
LocalUser = {
activeUser: bool
agentUserId: string
fullPreferences: AllowPreferences
localDisplayName: string
profile: Profile
showNtfs: bool
userContactId: int
userId: int
} .
MemberSubSummary = <memberSubSummary {
memberSubscriptions: [MemberSubscription ...]
user: LocalUser
}> .
MemberSubscription = <member {
activeConn: ActiveConn
groupId: int
groupMemberId: int
invitedBy: Origin
localDisplayName: string
memberCategory: string
memberContactId: int
memberContactProfileId: int
memberId: string
memberProfile: Profile
memberRole: string
memberStatus: string
}> .
PendingSubSummary = <pendingSubSummary {
pendingSubscriptions: [Connection ...]
user: Attributes
}> .
Connection = <connection @attrs Attributes> .
NewChatItem = <newChatItem { chatItem: ChatItem0 }> .
ChatItem0 = {
chatInfo: ChatInfo
user: User
} .
ChatInfo = {
chatInfo: ChatInfo
chatItem: ChatItem1
} .
ChatItem1 = {
content: Content
} .
Content = Attributes .
User = {
activeUser: bool
agentUserId: string
localDisplayName: string
userContactId: int
userId: int
} .
NewChatItem = { resp: NewChatItem1 } .

9
simple_types.prs Normal file
View File

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

View File

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

View File

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

View File

@ -1,369 +0,0 @@
import
preserves, std/tables
type
ChatItem1* {.preservesDictionary.} = object
`content`*: Content
Resp* {.preservesRecord: "resp".} = ref object
`field0`*: RespItem
RespItemKind* {.pure.} = enum
`TimedAction`, `ContactSubSummary`, `GroupSubscribed`, `MemberSubSummary`,
`PendingSubSummary`, `NewChatItem`, `Attributes`
`RespItem`* {.preservesOr.} = ref object
case orKind*: RespItemKind
of RespItemKind.`TimedAction`:
`timedaction`*: TimedAction
of RespItemKind.`ContactSubSummary`:
`contactsubsummary`*: ContactSubSummary
of RespItemKind.`GroupSubscribed`:
`groupsubscribed`*: GroupSubscribed
of RespItemKind.`MemberSubSummary`:
`membersubsummary`*: MemberSubSummary
of RespItemKind.`PendingSubSummary`:
`pendingsubsummary`*: PendingSubSummary
of RespItemKind.`NewChatItem`:
`newchatitem`*: NewChatItem
of RespItemKind.`Attributes`:
`attributes`*: Attributes
AllowPreferences* = Table[string, Allow]
LocalUser* {.preservesDictionary.} = object
`activeUser`*: bool
`agentUserId`*: string
`fullPreferences`*: AllowPreferences
`localDisplayName`*: string
`profile`*: Profile
`showNtfs`*: bool
`userContactId`*: BiggestInt
`userId`*: BiggestInt
Attributes* = Table[Symbol, Preserve[void]]
MIMEData* {.preservesRecord: "mime".} = object
`type`*: Symbol
`data`*: seq[byte]
PendingSubSummaryField0* {.preservesDictionary.} = object
`pendingSubscriptions`*: seq[Connection]
`user`*: Attributes
PendingSubSummary* {.preservesRecord: "pendingSubSummary".} = object
`field0`*: PendingSubSummaryField0
ActiveConn* {.preservesDictionary.} = object
`agentConnId`*: string
`authErrCounter`*: BiggestInt
`connId`*: BiggestInt
`connLevel`*: BiggestInt
`connStatus`*: string
`connType`*: string
`createdAt`*: string
`entityId`*: BiggestInt
`localAlias`*: string
`viaGroupLink`*: bool
MemberSubSummaryField0* {.preservesDictionary.} = object
`memberSubscriptions`*: seq[MemberSubscription]
`user`*: LocalUser
MemberSubSummary* {.preservesRecord: "memberSubSummary".} = object
`field0`*: MemberSubSummaryField0
FullGroupPreferences* {.preservesDictionary.} = object
`directMessages`*: EnableOffOn
`fullDelete`*: EnableOffOn
`reactions`*: EnableOffOn
`timedMessages`*: TimedMessages
`voice`*: EnableOffOn
AllowNo* {.preservesRecord: "allow".} = object
`field0`* {.preservesLiteral: "\"no\"".}: tuple[]
TimedActionField0* {.preservesDictionary.} = object
`action`*: string
`durationMilliseconds`*: BiggestInt
TimedAction* {.preservesRecord: "timedAction".} = object
`field0`*: TimedActionField0
MemberSubscriptionField0* {.preservesDictionary.} = object
`activeConn`*: ActiveConn
`groupId`*: BiggestInt
`groupMemberId`*: BiggestInt
`invitedBy`*: Origin
`localDisplayName`*: string
`memberCategory`*: string
`memberContactId`*: BiggestInt
`memberContactProfileId`*: BiggestInt
`memberId`*: string
`memberProfile`*: Profile
`memberRole`*: string
`memberStatus`*: string
MemberSubscription* {.preservesRecord: "member".} = object
`field0`*: MemberSubscriptionField0
Contact* {.preservesRecord: "contact".} = object
`num`*: BiggestInt
UserAllow* {.preservesRecord: "user".} = object
`field0`*: Allow
ContactSubSummaryField0* {.preservesDictionary.} = object
`contactSubscriptions`*: seq[ContactSubscription]
`user`*: User
ContactSubSummary* {.preservesRecord: "contactSubSummary".} = object
`field0`*: ContactSubSummaryField0
EnableNtfs* {.preservesRecord: "enableNtfs".} = object
`field0`*: bool
EnableOff* {.preservesRecord: "enable".} = object
`field0`* {.preservesLiteral: "\"off\"".}: tuple[]
OriginKind* {.pure.} = enum
`Contact`, `user`, `unknown`
OriginUser* {.preservesRecord: "user".} = object
OriginUnknown* {.preservesRecord: "unknown".} = object
`Origin`* {.preservesOr.} = object
case orKind*: OriginKind
of OriginKind.`Contact`:
`contact`*: Contact
of OriginKind.`user`:
`user`*: OriginUser
of OriginKind.`unknown`:
`unknown`*: OriginUnknown
MergedPreferences* {.preservesDictionary.} = object
`calls`*: ContactPreference
`fullDelete`*: ContactPreference
`reactions`*: ContactPreference
`timedMessages`*: ContactPreference
`voice`*: ContactPreference
AllowKind* {.pure.} = enum
`AllowNo`, `AllowYes`
`Allow`* {.preservesOr.} = object
case orKind*: AllowKind
of AllowKind.`AllowNo`:
`allowno`*: AllowNo
of AllowKind.`AllowYes`:
`allowyes`*: AllowYes
NewChatItemField0* {.preservesDictionary.} = ref object
`chatItem`*: ChatItem0
NewChatItem* {.preservesRecord: "newChatItem".} = ref object
`field0`*: NewChatItemField0
EnableOn* {.preservesRecord: "enable".} = object
`field0`* {.preservesLiteral: "\"on\"".}: tuple[]
ChatInfo* {.preservesDictionary.} = ref object
`chatInfo`*: ChatInfo
`chatItem`*: ChatItem1
MemberShip* {.preservesDictionary.} = object
`groupId`*: BiggestInt
`groupMemberId`*: BiggestInt
`invitedBy`*: Origin
`localDisplayName`*: string
`memberCategory`*: string
`memberContactId`*: BiggestInt
`memberContactProfileId`*: BiggestInt
`memberId`*: string
`memberProfile`*: Profile
`memberRole`*: string
`memberStatus`*: string
TimedMessages* {.preservesDictionary.} = object
`enable`*: string
`ttl`*: BiggestInt
Profile* {.preservesDictionary.} = object
`displayName`*: string
`fullName`*: string
LargeProfile* {.preservesDictionary.} = object
`displayName`*: string
`fullName`*: string
`image`*: MIMEData
`localAlias`*: string
`preferences`*: AllowPreferences
`profileId`*: BiggestInt
ContactPreference* {.preservesDictionary.} = object
`contactPreference`*: Allow
`enabled`*: ContactEnabled
`userPreference`*: UserAllow
Content* = Attributes
ChatItem0* {.preservesDictionary.} = ref object
`chatInfo`*: ChatInfo
`user`*: User
EnableOffOnKind* {.pure.} = enum
`EnableOff`, `EnableOn`
`EnableOffOn`* {.preservesOr.} = object
case orKind*: EnableOffOnKind
of EnableOffOnKind.`EnableOff`:
`enableoff`*: EnableOff
of EnableOffOnKind.`EnableOn`:
`enableon`*: EnableOn
AllowYes* {.preservesRecord: "allow".} = object
`field0`* {.preservesLiteral: "\"yes\"".}: tuple[]
ContactEnabled* {.preservesDictionary.} = object
`forContact`*: bool
`forUser`*: bool
BigProfile* {.preservesDictionary.} = object
`displayName`*: string
`fullName`*: string
`localAlias`*: string
`profileId`*: BiggestInt
User* {.preservesDictionary.} = object
`activeUser`*: bool
`agentUserId`*: string
`localDisplayName`*: string
`userContactId`*: BiggestInt
`userId`*: BiggestInt
ContactSubscription* {.preservesRecord: "contact".} = object
`attrs`*: Attributes
ContactSubscriptionAttributes* {.preservesDictionary.} = object
`activeConn`*: ActiveConn
`chatSettings`*: EnableNtfs
`chatTs`*: string
`contactId`*: BiggestInt
`contactUsed`*: bool
`createdAt`*: string
`localDisplayName`*: string
`mergedPreferences`*: MergedPreferences
`profile`*: Profile
`updatedAt`*: string
`userPreferences`*: AllowPreferences
Connection* {.preservesRecord: "connection".} = object
`attrs`*: Attributes
GroupInfo* {.preservesDictionary.} = object
`chatSettings`*: EnableNtfs
`chatTs`* {.preservesLiteral: "\"2023-07-22T11:54:25.14370346Z\"".}: tuple[]
`createdAt`* {.preservesLiteral: "\"2023-07-22T11:52:01.935384979Z\"".}: tuple[]
`fullGroupPreferences`*: FullGroupPreferences
`groupId`* {.preservesLiteral: "6".}: tuple[]
`groupProfile`*: Profile
`hostConnCustomUserProfileId`*: Preserve[void]
`localDisplayName`*: string
`membership`*: MemberShip
`updatedAt`*: string
GroupSubscribedField0* {.preservesDictionary.} = object
`groupInfo`*: GroupInfo
`user`*: LocalUser
GroupSubscribed* {.preservesRecord: "groupSubscribed".} = object
`field0`*: GroupSubscribedField0
proc `$`*(x: ChatItem1 | Resp | RespItem | AllowPreferences | LocalUser |
Attributes |
MIMEData |
PendingSubSummary |
ActiveConn |
MemberSubSummary |
FullGroupPreferences |
AllowNo |
TimedAction |
MemberSubscription |
Contact |
UserAllow |
ContactSubSummary |
EnableNtfs |
EnableOff |
Origin |
MergedPreferences |
Allow |
NewChatItem |
EnableOn |
ChatInfo |
MemberShip |
TimedMessages |
Profile |
LargeProfile |
ContactPreference |
Content |
ChatItem0 |
EnableOffOn |
AllowYes |
ContactEnabled |
BigProfile |
User |
ContactSubscription |
ContactSubscriptionAttributes |
Connection |
GroupInfo |
GroupSubscribed): string =
`$`(toPreserve(x))
proc encode*(x: ChatItem1 | Resp | RespItem | AllowPreferences | LocalUser |
Attributes |
MIMEData |
PendingSubSummary |
ActiveConn |
MemberSubSummary |
FullGroupPreferences |
AllowNo |
TimedAction |
MemberSubscription |
Contact |
UserAllow |
ContactSubSummary |
EnableNtfs |
EnableOff |
Origin |
MergedPreferences |
Allow |
NewChatItem |
EnableOn |
ChatInfo |
MemberShip |
TimedMessages |
Profile |
LargeProfile |
ContactPreference |
Content |
ChatItem0 |
EnableOffOn |
AllowYes |
ContactEnabled |
BigProfile |
User |
ContactSubscription |
ContactSubscriptionAttributes |
Connection |
GroupInfo |
GroupSubscribed): seq[byte] =
encode(toPreserve(x))

View File

@ -1,60 +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.} =
## Convert JSON to Preserves with some special rules to make
## traversing messages easier.
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[key.toSymbol(E)] = 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[key.toSymbol(E)] = toPreserveHook(val, E)

View File

@ -1,12 +1,10 @@
# 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 ./message_types
import ./private/jsonhooks
import ./simplex_bot_actor/[message_types, simple_types, websockets]
type
Value = Preserve[void]
@ -15,42 +13,77 @@ type
dataspace: Cap
url: string
ContactSubscription {.preservesDictionary.} = object
contact: Attributes
Internal* {.preservesRecord: "internal".} = object
dataspace: Cap
Contact = ref object
# TODO: does a Contact get its own facet
dataspace: Cap
capHandle, subSummaryHandle, profileHandle: Handle
capHandle, summaryHandle, profileHandle, chatItemHandle: Handle
Group = ref object
dataspace: Cap
capHandle, infoHandle: Handle
# ContactAssertion = simple_types.ContactAssertion[Cap]
ContactAssertion {.preservesRecord: "contact".} = object
id: int
cap: Cap
proc recvMessages(facet: Facet; ds: Cap; 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()
GroupAssertion {.preservesRecord: "group".} = object
id: int
cap: Cap
proc bootContact(turn: var Turn; ds: Cap; id: int): Contact =
result = Contact(dataspace: newDataspace(turn))
result.capHandle = publish(turn, ds, ContactAssertion(id: id, cap: result.dataspace))
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 bootClient(turn: var Turn; ds: Cap; ws: WebSocket) =
var contacts = initTable[int, Contact]()
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
let intern = newDataspace(turn)
discard publish(turn, ds, Internal(dataspace: intern))
# allocate a dataspace for internal messages and publish
recvMessages(turn.facet, intern, ws)
# start receiving loop
block:
let dumpStream = openFileStream("/tmp/simplex_bot_actor.log", fmWrite)
onMessage(turn, intern, grab()) do (msg: Assertion):
@ -59,44 +92,46 @@ proc bootClient(turn: var Turn; ds: Cap; ws: WebSocket) =
write(dumpStream, '\n')
flush(dumpStream)
block:
let pat = Resp ? { 0: (ContactSubSummary ? {
0: { "contactSubscriptions": grab() }.grabDictionary
})
}
onMessage(turn, intern, pat) do (conSubs: seq[ContactSubscription]):
for conSub in conSubs:
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(conSub.attrs[Symbol"contactId"]):
if id.fromPreserve(e.contact[Symbol"contactId"]):
var contact = contacts.getOrDefault(id)
if contact.isNil:
contact = bootContact(turn, ds, id)
contact = bootContact(turn, intern, id)
contacts[id] = contact
replace(turn, contact.dataspace, contact.subSummaryHandle, conSub.attrs)
var profile = conSub.attrs.getOrDefault(Symbol"profile")
if not profile.isFalse:
replace(turn, contact.dataspace, contact.profileHandle, profile)
contact.capHandle = publish(turn, ds,
ContactAssertion(id: id, cap: contact.dataspace))
updateAttrs(contact, turn, e.contact)
proc boot*(root: Cap; turn: var Turn) =
during(turn, root, ?Args) do (ds: Cap, url: string):
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 (id: int; cap: Cap):
onPublish(turn, cap, { "localDisplayName": grab() }.grabDictionary) do (name: string):
debugEcho "got contact ", name, " cap ", cap
onPublish(turn, cap, { "image": ?MIMEData }.grabDictionary) do (typ: Symbol, data: seq[byte]):
debugEcho "contact ", name, " has an image of ", data.len, " bytes"
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"
debugEcho "got dataspace ", ds, " and URL ", url
var ws: WebSocket
newWebSocket(url).addCallback(turn) do (turn: var Turn; s: WebSocket):
ws = s
debugecho "connected to ", url
bootClient(turn, ds, ws)
do:
close ws
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)
when isMainModule:
runActor("eris_actor") do (root: Cap; turn: var Turn):
# connectStdio(root, turn)
boot(root, turn)
discard publish(turn, root, Args(dataspace: root, url: "ws://127.0.0.1:5225/"))
discard publish(turn, root, Args(dataspace: root, url: "ws://127.0.0.1:5225/"))

View File

@ -0,0 +1,2 @@
include_rules
: foreach ../../*.prs |> !preserves_schema_nim |> | ../../<schema>

View File

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

View File

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

View File

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

2
tests/Tupfile Normal file
View File

@ -0,0 +1,2 @@
include_rules
: foreach test*.nim | $(SYNDICATE_PROTOCOL) ../<protocol> |> !nim_run |>

1
tests/config.nims Normal file
View File

@ -0,0 +1 @@
switch("path", "$projectDir/../src")

214
tests/test_grab.nim Normal file

File diff suppressed because one or more lines are too long