diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ed5b10 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# SimpleX bot actor + +A [syndicated actor](https://syndicate-lang.org/) for collecting data from the the [SimpleX](https://simplex.chat/) websocket and reformatting as Syndicate assertions. diff --git a/src/simplex_bot_actor.nim b/src/simplex_bot_actor.nim index e1a5757..402778c 100644 --- a/src/simplex_bot_actor.nim +++ b/src/simplex_bot_actor.nim @@ -9,6 +9,7 @@ import syndicate, syndicate/relays import ./schema/simple_types func step(pr: Assertion; path: varargs[string]): Option[Assertion] = + ## Convenience fuction for traversing levels of JSON hell. result = some(pr) var index = "".toSymbol(Cap) for s in path: @@ -17,6 +18,8 @@ func step(pr: Assertion; path: varargs[string]): Option[Assertion] = result = step(result.get, index) proc sendCommand(turn: var Turn; ds: Cap; cmd: string) = + ## Simplex websocket only accepts command + ## strings in the format of the CLI client. message(turn, ds, initRecord("send", Command(cmd: cmd).toPreserve)) proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern = @@ -24,10 +27,13 @@ proc `%`(bindings: sink openArray[(string, Pattern)]): Pattern = patterns.grabDictionary(bindings) proc grabResp(obj: Pattern): Pattern = + ## Grab the "resp" out of messages received on the websocket. grabRecord("recv", %{ "resp": obj }) type HandleTable = Table[Assertion, Handle] + ## Table for mapping Simplex identifiers to Syndicate handles. + State = ref object ds, websocket: Cap contacts, groups, chats: HandleTable @@ -37,6 +43,8 @@ proc updateTable(turn: var Turn; state: State; table: var HandleTable; id, ass: table[id] = replace(turn, state.ds, table.getOrDefault(id), ass) proc extractImagePath(image: Option[Assertion]): string = + ## Decode an image and return a content-addressed file- + ## system path. Otherwise "/dev/null". const prefix = "data:image/jpg;base64," if image.isNone: result = "/dev/null" @@ -52,6 +60,7 @@ proc extractImagePath(image: Option[Assertion]): string = writeFile(result, bin) proc updateContact(turn: var Turn; state: State; id, attrs: Assertion) = + ## Update the contact assertion held in the dataspace. var attrs = attrs imagePath = attrs.step("profile", "image").extractImagePath @@ -59,6 +68,7 @@ proc updateContact(turn: var Turn; state: State; id, attrs: Assertion) = updateTable(turn, state, state.contacts, id, initRecord("contact", attrs)) proc updateGroup(turn: var Turn; state: State; id, attrs: Assertion) = + ## Update the group assertion held in the dataspace. var attrs = attrs imagePath = attrs.step("groupProfile", "image").extractImagePath @@ -66,6 +76,7 @@ proc updateGroup(turn: var Turn; state: State; id, attrs: Assertion) = updateTable(turn, state, state.groups, id, initRecord("group", attrs)) proc updateChat(turn: var Turn; state: State; ass: Assertion) = + ## Update the chat assertion held in the dataspace. var id: Option[Assertion] var info = ass.step("chatInfo", "contact") if info.isSome: @@ -82,6 +93,7 @@ proc updateChat(turn: var Turn; state: State; ass: Assertion) = updateTable(turn, state, state.chats, id.get, initRecord("chat", ass)) proc bootChats(turn: var Turn; state: State) = + ## Install observers and seed the dataspace. let chatPat = grabResp(%{ "chat": grab() }) chatsPat = grabResp(%{ "chats": grab() }) @@ -94,8 +106,10 @@ proc bootChats(turn: var Turn; state: State) = for chat in chats: updateChat(turn, state, chat) sendCommand(turn, state.websocket, "/chats") + # initial chats request to populate dataspace type BootArgs {.preservesDictionary.} = object + # Arguments passed by the Syndicate server on stdin. dataspace: Cap websocket: Cap