# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[asyncdispatch, logging, parseopt, streams, strutils, tables] from std/os import getEnv, fileExists, moveFile from std/sequtils import toSeq from std/times import inMilliseconds import syndicate, syndicate/actors import cbor, toxcore import ./protocol # import ./logging addHandler(newConsoleLogger(useStderr = true)) # register global logger to stderr var alive: bool setControlCHook do: if not alive: # hard stop quit "exited without saving Tox data" alive = false proc logging_callback( core: Core; level: Log_Level; file: cstring; line: uint32; `func`: cstring; message: cstring; user_data: pointer) {.exportc, cdecl.} = 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(lvl, `func`, ": ", message) proc saveFilePath(): string = for kind, key, val in getopt(): if kind == cmdLongOption and key == "save-file" and val != "": result = val type GlobalState = ref object core: Tox statusCounts: array[3, int] proc writeSaveData(state: GlobalState) = let path = saveFilePath() if path != "": let tmpPath = path & ".tmp" debug("Dumping save data to ", tmpPath) var stream = newFileStream(tmpPath, fmWrite) stream.writeCbor(state.core.saveData) close(stream) moveFile(tmpPath, path) debug("Data saved to ", path) proc newGlobalState(): GlobalState = new result let saveFile = saveFilePath() var saveData: seq[byte] if fileExists saveFile: block: var stream = newFileStream(saveFile, fmRead) parser: CborParser dbPath: string open(parser, stream) parser.next() if not saveData.fromCbor(parser.nextNode()): raise newException(ValueError, "failed to parse Tox save data") close(stream) var proxy_host: string result.core = newTox do (opts: toxcore.Options): opts.log_callback = logging_callback 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) 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": proxy_host = val opts.proxy_host = proxy_host 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": discard else: quit("unhandled command-line parameter: " & key) of cmdShortOption, cmdArgument: quit("unhandled command-line parameter: " & key) of cmdEnd: discard if saveData.len > 0: opts.savedata_type = TOX_SAVEDATA_TYPE_TOX_SAVE opts.savedata = saveData let ms = result.core.iterationInterval.inMilliseconds.int oneshot = false global = result addTimer(ms, oneshot) do (fd: AsyncFD) -> bool: if not alive: writeSaveData(global) quit() iterate global.core writeSaveData(global) info "Tox ready" info result.core.dhtId, "@:", result.core.udpPort type SelfHandles = object connectionStatus, name, status, statusMessage: Handle FriendHandles = object `ref`: Ref key, lastOnline, name, statusMessage, typing: Handle ToxRelay = ref object global: GlobalState facet: Facet `ref`: Ref self: SelfHandles friendRequests: Table[PublicKey, Handle] friends: Table[Friend, FriendHandles] proc publish[T](relay: ToxRelay; assertion: T): Handle = info "publish ", assertion var h: Handle relay.facet.run do (turn: var Turn): h = publish(turn, relay.`ref`, assertion) h proc retract(relay: ToxRelay; h: Handle) = relay.facet.run do (turn: var Turn): retract(turn, h) template replace[T](relay: ToxRelay; h: var Handle; assertion: T) = relay.facet.run do (turn: var Turn): replace(turn, relay.`ref`, h, assertion) proc installCallbacks(relay: ToxRelay; turn: var Turn) = relay.global.core.status = TOX_USER_STATUS_AWAY discard publish(turn, relay.`ref`, ToxCoreVersion( major: int version_major(), minor: int version_minor(), patch: int version_patch())) discard publish(turn, relay.`ref`, ToxSelfAddress(address: relay.global.core.address.bytes.toSeq)) if relay.global.core.name != "": relay.self.name = publish(turn, relay.`ref`, ToxSelfName(name: relay.global.core.name)) if relay.global.core.statusMessage != "": relay.self.statusMessage = publish(turn, relay.`ref`, ToxSelfStatusMessage(message: relay.global.core.statusMessage)) relay.global.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" relay.replace(relay.self.connectionStatus, ToxSelfConnectionStatus(status: sym)) for num in relay.global.core.friends: var handles = FriendHandles(`ref`: newRef(turn, relay.`ref`.target)) handles.key = publish(turn, relay.`ref`, ToxFriendKey(num: int num, key: relay.global.core.publicKey(num).bytes.toSeq)) let lastOnline = relay.global.core.lastOnline(num) if lastOnline > 0: handles.lastOnline = publish(turn, relay.`ref`, ToxFriendLastOnline(num: int num, unixEpoch: float64 lastOnline)) handles.name = publish(turn, relay.`ref`, ToxFriendName(num: int num, name: relay.global.core.name(num))) handles.statusMessage = publish(turn, relay.`ref`, ToxFriendStatusMessage(num: int num, message: relay.global.core.statusMessage(num))) relay.friends[num] = handles relay.global.core.onFriendName do (num: Friend; name: string): relay.replace(relay.friends[num].name, ToxFriendName(num: int num, name: name)) relay.global.core.onFriendStatusMessage do (num: Friend; msg: string): relay.replace( relay.friends[num].statusMessage, ToxFriendStatusMessage(num: int num, message: msg)) relay.global.core.onFriendTyping do (num: Friend; typing: bool): if typing: relay.friends[num].typing = relay.publish( ToxFriendTyping(num: int num)) else: relay.retract(relay.friends[num].typing) relay.friends[num].typing = 0 relay.global.core.onFriendRequest do (key: PublicKey; msg: string): var a = ToxFriendRequest(key: key.bytes.toSeq, message: msg) if relay.friendRequests.hasKey key: relay.replace(relay.friendRequests[key], a) else: relay.friendRequests[key] = relay.publish(a) proc newToxRelay(global: GlobalState; `ref`: Ref; turn: var Turn): ToxRelay = result = ToxRelay( global: global, facet: turn.facet, `ref`: `ref`) installCallbacks(result, turn) bootDataspace("main") do (root: Ref; turn: var Turn): alive = true var global = newGlobalState() connectStdio(root, turn) onPublish(turn, root, ?BootstrapNode) do (key: string; host: string; port: int): info "Bootstrapping from ", key, "@", host, ":", port try: global.core.bootstrap(host, key.toPublicKey, uint16 port) except ToxError as e: error "failed to bootstrap: ", e.msg during(turn, root, ?ListenOn[Ref]) do (a: Assertion): let ds = unembed a relay = newToxRelay(global, ds, turn) during(turn, ds, ?ToxSelfStatus) do (status: Status): let e = case status of Status.online: TOX_USER_STATUS_NONE of Status.away: TOX_USER_STATUS_AWAY of Status.busy: TOX_USER_STATUS_BUSY inc relay.global.statusCounts[int e] let most = max(relay.global.statusCounts) if relay.global.statusCounts[int e] == most: relay.global.core.status = e do: relay.global.statusCounts[int e] = max(0, pred relay.global.statusCounts[int e]) let most = max(relay.global.statusCounts) for e in [ TOX_USER_STATUS_BUSY, TOX_USER_STATUS_NONE, TOX_USER_STATUS_AWAY]: if relay.global.statusCounts[int e] == most: relay.global.core.status = e break onPublish(turn, ds, ?ToxSelfName) do (name: string): debug "ToxSelfName ", rawValues relay.global.core.name = name replace(turn, ds, relay.self.name, ToxSelfName(name: relay.global.core.name)) writeSaveData(relay.global) onPublish(turn, ds, ?ToxSelfStatusMessage) do (msg: string): debug "ToxSelfStatusMessage ", rawValues relay.global.core.statusMessage = msg replace(turn, ds, relay.self.statusMessage, ToxSelfStatusMessage(message: relay.global.core.statusMessage)) relay.global.writeSaveData() onMessage(turn, ds, ?ToxFriendRequest) do (bytes: seq[byte]; msg: string): info "got a ToxFriendRequest with a ", bytes.len, " byte key" if bytes.len == TOX_PUBLIC_KEY_SIZE: var key: PublicKey copyMem(addr key.bytes[0], addr bytes[0], key.bytes.len) var h: Handle if relay.friendRequests.pop(key, h): relay.retract(h) try: debug "addFriendNoRequest ", key let friend = relay.global.core.addFriendNoRequest(key) relay.friends[friend] = FriendHandles() relay.global.writeSaveData() except ToxError as e: error "failed to add friend: ", e.msg onMessage(turn, ds, ?ToxFriendAdd) do (toxid: seq[byte]; msg: string): var address: Address if toxid.len != address.bytes.len: error "invalid Tox address: ", toxid else: copyMem(addr address.bytes[0], addr toxid[0], address.bytes.len) try: let friend = relay.global.core.addFriend(address, msg) relay.friends[friend] = FriendHandles() relay.global.writeSaveData() # TODO: assert a pending friend? except ToxError as e: error "failed to add friend: ", e.msg onMessage(turn, ds, ?ToxFriendSend) do (friend: Friend; msg: string): discard relay.global.core.send(friend, msg) # TODO: assert pending messages? do: info "facet stopped within ListenOn body" runForever()