diff --git a/message_types.prs b/message_types.prs index b6e4781..6d9c3c3 100644 --- a/message_types.prs +++ b/message_types.prs @@ -1,224 +1,21 @@ version 1 . -AllowNo = . -AllowYes = . -Allow = AllowNo / AllowYes . -UserAllow = . - -EnableOffOn = EnableOff / EnableOn . -EnableOff = . -EnableOn = . - -EnableNtfs = . - -MIMEData = . - -Contact = . - -Origin = -/ Contact -/ -/ . - Attributes = {symbol: any ...:...} . -Resp = . +MIMEData = . -RespItem = -/ TimedAction -/ ContactSubSummary -/ GroupSubscribed -/ MemberSubSummary -/ PendingSubSummary -/ NewChatItem -/ Attributes . +ContactSubscription = { contact: Attributes } . +ContactSubscriptions2 = [ContactSubscription ...] . +ContactSubscriptions1 = { + contactSubscriptions: ContactSubscriptions2 + type: "contactSubSummary" +} . +ContactSubscriptions = { resp: ContactSubscriptions1 } . -TimedAction = . - -ContactSubSummary = . - -ContactSubscription = . - -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 = . - -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 = . - -MemberSubscription = . - -PendingSubSummary = . - -Connection = . - -NewChatItem = . - -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 } . diff --git a/simple_types.prs b/simple_types.prs new file mode 100644 index 0000000..2a50d82 --- /dev/null +++ b/simple_types.prs @@ -0,0 +1,9 @@ +version 1 . +embeddedType EntityRef.Cap . + +ContactAssertion = . +GroupAssertion = . + +ReceivedMessage = . + +Content = . diff --git a/simplex_bot_actor.nimble b/simplex_bot_actor.nimble index c3dcffc..edcf7a3 100644 --- a/simplex_bot_actor.nimble +++ b/simplex_bot_actor.nimble @@ -1,6 +1,6 @@ bin = @["simplex_bot_actor"] license = "Unlicense" srcDir = "src" -version = "20230724" +version = "20230726" requires: "nim", "syndicate", ws diff --git a/src/Tupfile b/src/Tupfile index 4a51cbf..6ec6a4b 100644 --- a/src/Tupfile +++ b/src/Tupfile @@ -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) ../ |> !nim_bin |> {bin} : {bin} |> !assert_built |> diff --git a/src/message_types.nim b/src/message_types.nim deleted file mode 100644 index 60fca9f..0000000 --- a/src/message_types.nim +++ /dev/null @@ -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)) diff --git a/src/private/jsonhooks.nim b/src/private/jsonhooks.nim deleted file mode 100644 index 64575a0..0000000 --- a/src/private/jsonhooks.nim +++ /dev/null @@ -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) diff --git a/src/simplex_bot_actor.nim b/src/simplex_bot_actor.nim index ce8fbef..2f4e6ea 100644 --- a/src/simplex_bot_actor.nim +++ b/src/simplex_bot_actor.nim @@ -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/")) diff --git a/src/simplex_bot_actor/Tupfile b/src/simplex_bot_actor/Tupfile new file mode 100644 index 0000000..ff832c4 --- /dev/null +++ b/src/simplex_bot_actor/Tupfile @@ -0,0 +1,2 @@ +include_rules +: foreach ../../*.prs |> !preserves_schema_nim |> | ../../ diff --git a/src/simplex_bot_actor/message_types.nim b/src/simplex_bot_actor/message_types.nim new file mode 100644 index 0000000..88d9d15 --- /dev/null +++ b/src/simplex_bot_actor/message_types.nim @@ -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)) diff --git a/src/simplex_bot_actor/simple_types.nim b/src/simplex_bot_actor/simple_types.nim new file mode 100644 index 0000000..a8d2029 --- /dev/null +++ b/src/simplex_bot_actor/simple_types.nim @@ -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)) diff --git a/src/simplex_bot_actor/websockets.nim b/src/simplex_bot_actor/websockets.nim new file mode 100644 index 0000000..81b04e0 --- /dev/null +++ b/src/simplex_bot_actor/websockets.nim @@ -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 + ## `` + ## 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) diff --git a/tests/Tupfile b/tests/Tupfile new file mode 100644 index 0000000..32f22a3 --- /dev/null +++ b/tests/Tupfile @@ -0,0 +1,2 @@ +include_rules +: foreach test*.nim | $(SYNDICATE_PROTOCOL) ../ |> !nim_run |> diff --git a/tests/config.nims b/tests/config.nims new file mode 100644 index 0000000..3bb69f8 --- /dev/null +++ b/tests/config.nims @@ -0,0 +1 @@ +switch("path", "$projectDir/../src") \ No newline at end of file diff --git a/tests/test_grab.nim b/tests/test_grab.nim new file mode 100644 index 0000000..b399a7f --- /dev/null +++ b/tests/test_grab.nim @@ -0,0 +1,214 @@ +# SPDX-FileCopyrightText: ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[unittest] +import preserves, syndicate + +proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern = + ## Sugar for creating dictionary patterns. + patterns.grabDictionary(bindings) + +suite "chatItem": + let data = parsePreserves(""" +{ + resp: { + chatItem: { + chatInfo: { + contact: { + activeConn: { + agentConnId: "TGRmT3B6UUFRalVGSHVtVg==" + authErrCounter: 0 + connId: 56 + connLevel: 1 + connStatus: "ready" + connType: "contact" + createdAt: "2023-07-24T19:57:10.282893401Z" + entityId: 19 + localAlias: "" + viaContact: 21 + viaGroupLink: false + } + chatSettings: { + enableNtfs: true + } + chatTs: "2023-07-25T12:41:47.886547076Z" + contactId: 19 + contactUsed: true + createdAt: "2023-05-26T06:46:48.95069518Z" + localDisplayName: "bidston-burner" + mergedPreferences: { + calls: { + contactPreference: { + allow: "yes" + } + enabled: { + forContact: true + forUser: true + } + userPreference: { + preference: { + allow: "yes" + } + type: "user" + } + } + fullDelete: { + contactPreference: { + allow: "no" + } + enabled: { + forContact: false + forUser: false + } + userPreference: { + preference: { + allow: "no" + } + type: "user" + } + } + reactions: { + contactPreference: { + allow: "yes" + } + enabled: { + forContact: true + forUser: true + } + userPreference: { + preference: { + allow: "yes" + } + type: "user" + } + } + timedMessages: { + contactPreference: { + allow: "yes" + } + enabled: { + forContact: true + forUser: true + } + userPreference: { + preference: { + allow: "yes" + } + type: "user" + } + } + voice: { + contactPreference: { + allow: "yes" + } + enabled: { + forContact: true + forUser: true + } + userPreference: { + preference: { + allow: "yes" + } + type: "user" + } + } + } + profile: { + displayName: "bidston-burner" + fullName: "BOARC mobile phone" + image: + localAlias: "" + preferences: { + calls: { + allow: "yes" + } + fullDelete: { + allow: "no" + } + timedMessages: { + allow: "yes" + } + voice: { + allow: "yes" + } + } + profileId: 19 + } + updatedAt: "2023-07-24T20:24:50.412385408Z" + userPreferences: {} + } + type: "direct" + } + chatItem: { + chatDir: { + type: "directRcv" + } + content: { + msgContent: { + text: "Ping" + type: "text" + } + type: "rcvMsgContent" + } + meta: { + createdAt: "2023-07-25T12:42:00.474365917Z" + editable: false + itemDeleted: null + itemEdited: false + itemId: 2748 + itemLive: null + itemSharedMsgId: "eDhCSzFMY04xODFzaklDMA==" + itemStatus: { + type: "rcvNew" + } + itemText: "Ping" + itemTimed: null + itemTs: "2023-07-25T12:42:00Z" + localItemTs: "2023-07-25T13:42:00+01:00" + updatedAt: "2023-07-25T12:42:00.474365917Z" + } + reactions: [] + } + } + type: "newChatItem" + user: { + activeUser: true + agentUserId: "1" + fullPreferences: { + calls: { + allow: "yes" + } + fullDelete: { + allow: "no" + } + reactions: { + allow: "yes" + } + timedMessages: { + allow: "yes" + } + voice: { + allow: "yes" + } + } + localDisplayName: "emery" + profile: { + displayName: "emery" + fullName: "Emery (laptop)" + localAlias: "" + profileId: 1 + } + showNtfs: true + userContactId: 1 + userId: 1 + } + } +} +""", Cap) + + let pat = %{"resp": %{"chatItem": %{ + "chatInfo": %{"contact": %{"contactId": grab(19)}}, + "chatItem": grab(), + }}} + + check matches(pat, data)