# 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()