From 51823d568840840684c6f7c88d64d598c7d22297 Mon Sep 17 00:00:00 2001 From: Emery Hemingway Date: Mon, 4 Apr 2022 22:13:01 -0500 Subject: [PATCH] Initial commit --- .gitignore | 1 + protocol.prs | 32 +++++ src/Tupfile | 2 + src/protocol.nim | 95 ++++++++++++ src/syndicate_actor_tox.nim | 279 ++++++++++++++++++++++++++++++++++++ syndicate_actor_tox.nimble | 13 ++ syndicate_server_config.pr | 22 +++ 7 files changed, 444 insertions(+) create mode 100644 .gitignore create mode 100644 protocol.prs create mode 100644 src/Tupfile create mode 100644 src/protocol.nim create mode 100644 src/syndicate_actor_tox.nim create mode 100644 syndicate_actor_tox.nimble create mode 100644 syndicate_server_config.pr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..566bb3a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +syndicate_actor_tox diff --git a/protocol.prs b/protocol.prs new file mode 100644 index 0000000..692838f --- /dev/null +++ b/protocol.prs @@ -0,0 +1,32 @@ +version 1 . + +ListenOn = . + +BootstrapNode = . + +ToxCoreVersion = . + +ToxSelfAddress = . + +ToxSelfConnectionStatus = . + +ToxSelfName = . + +Status = =online / =away / =busy . +ToxSelfStatus = . + +ToxSelfStatusMessage = . + +ToxFriendKey = . + +ToxFriendLastOnline = . + +ToxFriendName = . + +ToxFriendStatusMessage = . + +ToxFriendTyping = . + +ToxFriendAdd = . + +ToxFriendSend = . diff --git a/src/Tupfile b/src/Tupfile new file mode 100644 index 0000000..e5ee95f --- /dev/null +++ b/src/Tupfile @@ -0,0 +1,2 @@ +include_rules +: foreach ../*.prs |> !preserves_schema_nim |> %B.nim diff --git a/src/protocol.nim b/src/protocol.nim new file mode 100644 index 0000000..fbae8f0 --- /dev/null +++ b/src/protocol.nim @@ -0,0 +1,95 @@ + +import + std/typetraits, preserves + +type + ToxFriendSend* {.preservesRecord: "ToxFriendSend".} = object + `num`*: int + `message`*: string + + ToxCoreVersion* {.preservesRecord: "ToxCoreVersion".} = object + `major`*: int + `minor`*: int + `patch`*: int + + ToxFriendLastOnline* {.preservesRecord: "ToxFriendLastNnline".} = object + `num`*: int + `unixEpoch`*: float64 + + ToxSelfConnectionStatus* {.preservesRecord: "ToxSelfConnectionStatus".} = object + `status`*: Symbol + + ToxSelfStatus* {.preservesRecord: "ToxSelfStatus".} = object + `status`*: Status + + BootstrapNode* {.preservesRecord: "bootstrap".} = object + `publicKey`*: string + `host`*: string + `port`*: int + + ToxSelfStatusMessage* {.preservesRecord: "ToxSelfStatusMessage".} = object + `message`*: string + + `Status`* {.preservesOr, pure.} = enum + `online`, `away`, `busy` + ToxSelfName* {.preservesRecord: "ToxSelfName".} = object + `name`*: string + + ToxFriendKey* {.preservesRecord: "ToxFriendkey".} = object + `num`*: int + `key`*: seq[byte] + + ListenOn*[E] {.preservesRecord: "listen-on".} = ref object + `dataspace`*: Preserve[E] + + ToxSelfAddress* {.preservesRecord: "ToxSelfAddress".} = object + `address`*: seq[byte] + + ToxFriendAdd* {.preservesRecord: "ToxFriendAdd".} = object + `address`*: seq[byte] + `message`*: string + + ToxFriendStatusMessage* {.preservesRecord: "ToxFriendStatusMessage".} = object + `num`*: int + `message`*: string + + ToxFriendName* {.preservesRecord: "ToxFriendName".} = object + `num`*: int + `name`*: string + + ToxFriendTyping* {.preservesRecord: "ToxFriendTyping".} = object + `num`*: int + +proc `$`*[E](x: ListenOn[E]): string = + `$`(toPreserve(x, E)) + +proc encode*[E](x: ListenOn[E]): seq[byte] = + encode(toPreserve(x, E)) + +proc `$`*(x: ToxFriendSend | ToxCoreVersion | ToxFriendLastOnline | + ToxSelfConnectionStatus | + ToxSelfStatus | + BootstrapNode | + ToxSelfStatusMessage | + ToxSelfName | + ToxFriendKey | + ToxSelfAddress | + ToxFriendAdd | + ToxFriendStatusMessage | + ToxFriendName | + ToxFriendTyping): string = + `$`(toPreserve(x)) + +proc encode*(x: ToxFriendSend | ToxCoreVersion | ToxFriendLastOnline | + ToxSelfConnectionStatus | + ToxSelfStatus | + BootstrapNode | + ToxSelfStatusMessage | + ToxSelfName | + ToxFriendKey | + ToxSelfAddress | + ToxFriendAdd | + ToxFriendStatusMessage | + ToxFriendName | + ToxFriendTyping): seq[byte] = + encode(toPreserve(x)) diff --git a/src/syndicate_actor_tox.nim b/src/syndicate_actor_tox.nim new file mode 100644 index 0000000..3476dbb --- /dev/null +++ b/src/syndicate_actor_tox.nim @@ -0,0 +1,279 @@ +# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[asyncdispatch, logging, parseopt, strutils, tables] +from std/os import fileExists, moveFile +from std/sequtils import toSeq +from std/times import inMilliseconds + +import syndicate +import toxcore +import ./protocol + +var alive: bool +setControlCHook do: + if not alive: + # hard stop + quit "exited without saving Tox data" + alive = false + +proc console_log_callback( + core: Core; level: Log_Level; + file: cstring; line: uint32; + `func`: cstring; message: cstring; + user_data: pointer) {.exportc, cdecl.} = + stderr.writeLine "console_log_callback: ", `func` + #[ + let console = cast[ConsoleLogger](user_data) + let lvl = case level + of TOX_LOG_LEVEL_TRACE: lvlDebug + of TOX_LOG_LEVEL_DEBUG: lvlDebug + of TOX_LOG_LEVEL_INFO: lvlInfo + of TOX_LOG_LEVEL_WARNING: lvlWarn + of TOX_LOG_LEVEL_ERROR: lvlError + if lvl != lvlNone: + log(console, lvl, `func`, ": ", message) + ]# + +type + SelfHandles = object + connectionStatus, name, status, statusMessage: Handle + FriendHandles = object + key, lastOnline, name, statusMessage, typing: Handle + + ToxActor = ref object + console: ConsoleLogger + facet: Facet + `ref`: Ref + saveFilePath: string + core: Tox + self: SelfHandles + friends: Table[Friend, FriendHandles] + +template debug(actor: ToxActor; args: varargs[string, `$`]) = + actor.console.log(lvlDebug, args) + +template info(actor: ToxActor; args: varargs[string, `$`]) = + actor.console.log(lvlInfo, args) + +template warn(actor: ToxActor; args: varargs[string, `$`]) = + actor.console.log(lvlWarn, args) + +template error(actor: ToxActor; args: varargs[string, `$`]) = + actor.console.log(lvlError, args) + +proc writeSaveData(actor: ToxActor) = + if actor.saveFilePath != "": + let + path = actor.saveFilePath + tmpPath = path & ".tmp" + actor.debug("Duming data to ", tmpPath) + writeFile(tmpPath, actor.core.saveData) + moveFile(tmpPath, path) + actor.debug("Data saved to ", path) + +proc publish[T](actor: ToxActor; assertion: T): Handle = + actor.info("publish ", assertion) + var h: Handle + actor.facet.run do (turn: var Turn): + h = publish(turn, actor.`ref`, assertion) + h + +proc retract(actor: ToxActor; h: Handle) = + actor.facet.run do (turn: var Turn): retract(turn, h) + +template replace[T](actor: ToxActor; h: var Handle; assertion: T) = + actor.facet.run do (turn: var Turn): replace(turn, actor.`ref`, h, assertion) + +proc installCallbacks(actor: ToxActor; turn: var Turn) = + actor.core.status = TOX_USER_STATUS_AWAY + + discard publish(turn, actor.`ref`, + ToxCoreVersion( + major: int version_major(), + minor: int version_minor(), + patch: int version_patch())) + discard publish(turn, actor.`ref`, + ToxSelfAddress(address: actor.core.address.bytes.toSeq)) + + actor.self.name = + publish(turn, actor.`ref`, + ToxSelfName(name: actor.core.name)) + + actor.self.statusMessage = + publish(turn, actor.`ref`, + ToxSelfStatusMessage(message: actor.core.statusMessage)) + + actor.core.onSelfConnectionStatus do (status: Connection): + let sym = + case status + of TOX_CONNECTION_NONE: Symbol"none" + of TOX_CONNECTION_TCP: Symbol"tcp" + of TOX_CONNECTION_UDP: Symbol"udp" + actor.replace(actor.self.connectionStatus, + ToxSelfConnectionStatus(status: sym)) + + for num in actor.core.friends: + var handles: FriendHandles + handles.key = publish(turn, actor.`ref`, + ToxFriendKey(num: int num, key: actor.core.publicKey(num).bytes.toSeq)) + + let lastOnline = actor.core.lastOnline(num) + if lastOnline > 0: + handles.lastOnline = publish(turn, actor.`ref`, + ToxFriendLastOnline(num: int num, unixEpoch: float64 lastOnline)) + + handles.name = publish(turn, actor.`ref`, + ToxFriendName(num: int num, name: actor.core.name(num))) + + handles.statusMessage = publish(turn, actor.`ref`, + ToxFriendStatusMessage(num: int num, message: actor.core.statusMessage(num))) + actor.friends[num] = handles + + actor.core.onFriendName do (num: Friend; name: string): + actor.replace(actor.friends[num].name, ToxFriendName(num: int num, name: name)) + + actor.core.onFriendStatusMessage do (num: Friend; msg: string): + actor.replace( + actor.friends[num].statusMessage, + ToxFriendStatusMessage(num: int num, message: msg)) + + actor.core.onFriendTyping do (num: Friend; typing: bool): + if typing: + actor.friends[num].typing = + actor.publish( + ToxFriendTyping(num: int num)) + else: + actor.retract(actor.friends[num].typing) + actor.friends[num].typing = 0 + +proc newToxActor(`ref`: Ref; turn: var Turn): ToxActor = + result = ToxActor( + console: newConsoleLogger(useStderr=true), + facet: turn.facet, + `ref`: `ref`) + let actor = result + result.core = initTox do (opts: toxcore.Options): + opts.log_callback = console_log_callback + # opts.log_user_data = addr actor.console[] + actor.debug "parsing command-line options…" + proc parseBoolParam(key, val: string): bool = + if val == "": result = true + else: + try: result = parsebool(val) + except: + quit("failed to parse " & key & " as boolean: " & val) + proc parsePortParam(key, val: string): uint16 = + try: result = uint16 parsebool(val) + except: + quit("failed to parse " & key & " as port: " & val) + var saveDataLoaded = false + for kind, key, val in getopt(): + case kind + of cmdLongOption: + case key + of "ipv6": + opts.ipv6_enabled = parseBoolParam(key, val) + of "udp": + opts.udp_enabled = parseBoolParam(key, val) + of "local-discovery": + opts.local_discovery_enabled = parseBoolParam(key, val) + of "proxy": + case val + of "none": opts.proxy_type = TOX_PROXY_TYPE_NONE + of "http": opts.proxy_type = TOX_PROXY_TYPE_HTTP + of "socks5": opts.proxy_type = TOX_PROXY_TYPE_SOCKS5 + else: + quit("unhandled proxy type: " & val) + of "proxy-host": + opts.proxy_host = val + of "proxy-port": + opts.proxy_port = parsePortParam(key, val) + of "start-port": + opts.start_port = parsePortParam(key, val) + of "end-port": + opts.end_port = parsePortParam(key, val) + of "tcp-port": + opts.tcp_port = parsePortParam(key, val) + of "hole-punching": + opts.hole_punching_enabled = parseBoolParam(key, val) + of "save-file": + actor.saveFilePath = val + else: + quit("unhandled command-line parameter: " & key) + of cmdShortOption, cmdArgument: + quit("unhandled command-line parameter: " & key) + of cmdEnd: discard + if actor.saveFilePath != "" and fileExists actor.saveFilePath: + actor.info "Loading saved data from ", actor.saveFilePath + opts.savedata_type = TOX_SAVEDATA_TYPE_TOX_SAVE + opts.savedata = readFile(actor.saveFilePath) + installCallbacks(result, turn) + let + ms = result.core.iterationInterval.inMilliseconds.int + oneshot = false + addTimer(ms, oneshot) do (fd: AsyncFD) -> bool: + if not alive: + actor.writeSaveData() + quit() + iterate actor.core + result.info "Tox actor ready" + +bootDataspace("main") do (root: Ref; turn: var Turn): + connectStdio(root, turn) + alive = true + + onPublish(turn, root, ?ListenOn[Ref]) do (a: Assertion): + let + ds = unembed a + actor = newToxActor(ds, turn) + + onPublish(turn, root, ?BootstrapNode) do (key: string; host: string; port: int): + actor.info("Bootstrapping from ", key, "@", host, ":", port) + try: actor.core.bootstrap(host, key.toPublicKey, uint16 port) + except ToxError as e: + actor.error "failed to bootstrap: ", e.msg + + onMessage(turn, ds, ?ToxSelfStatus) do (status: Status): + ## TODO: Take the status of the core from the frontend. + ## Set to away when the frontend retracts its status. + actor.debug "ToxSelfStatus ", rawValues + actor.core.status = case status + of Status.online: TOX_USER_STATUS_NONE + of Status.away: TOX_USER_STATUS_AWAY + of Status.busy: TOX_USER_STATUS_BUSY + #turn.facet.onStop do (turn: var Turn): + # actor.core.status = TOX_USER_STATUS_AWAY + + onMessage(turn, ds, ?ToxSelfName) do (name: string): + actor.debug "ToxSelfName ", rawValues + actor.core.name = name + replace(turn, ds, actor.self.name, ToxSelfName(name: actor.core.name)) + actor.writeSaveData() + + onMessage(turn, ds, ?ToxSelfStatusMessage) do (msg: string): + actor.debug "ToxSelfStatusMessage ", rawValues + actor.core.statusMessage = msg + replace(turn, ds, actor.self.statusMessage, + ToxSelfStatusMessage(message: actor.core.statusMessage)) + actor.writeSaveData() + + onMessage(turn, ds, ?ToxFriendAdd) do (toxid: seq[byte]; msg: string): + actor.debug "ToxFriendAdd ", rawValues + var address: Address + if toxid.len != address.bytes.len: + actor.error "invalid Tox address: ", toxid + else: + copyMem(addr address.bytes[0], addr toxid[0], address.bytes.len) + try: + discard actor.core.addFriend(address, msg) + # TODO: assert a pending friend? + except ToxError as e: + actor.error "failed to add friend: ", e.msg + + onMessage(turn, ds, ?ToxFriendSend) do (friend: Friend; msg: string): + actor.debug "ToxFriendSend ", rawValues + discard actor.core.send(friend, msg) + # TODO: assert pending messages? + +runForever() diff --git a/syndicate_actor_tox.nimble b/syndicate_actor_tox.nimble new file mode 100644 index 0000000..e25b78f --- /dev/null +++ b/syndicate_actor_tox.nimble @@ -0,0 +1,13 @@ +# Package + +version = "0.1.0" +author = "Emery Hemingway" +description = "Tox chat actor for Syndicate" +license = "Unlicense" +srcDir = "src" +bin = @["syndicate_actor_tox"] + + +# Dependencies + +requires "nim >= 1.6.2", "syndicate >= 1.2.1" diff --git a/syndicate_server_config.pr b/syndicate_server_config.pr new file mode 100644 index 0000000..abfba24 --- /dev/null +++ b/syndicate_server_config.pr @@ -0,0 +1,22 @@ +let ?root_ds = dataspace + + +> + + + +? ?cap> [ + $cap [ + + + + + ] +]