# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[asyncdispatch, logging, parseopt, strutils, tables] from std/sequtils import toSeq from std/times import inMilliseconds import preserves, syndicate, syndicate/actors import toxcore import ./protocol # import ./logging addHandler(newConsoleLogger(useStderr = true)) # register global logger to stderr 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) type Entity = ref object of RootObj facet: Facet ds: Ref FriendHandles = object name, statusMessage, lastOnline, typing: Handle FriendEntity {.final.} = ref object of Entity handles: FriendHandles CoreHandles = object address, name, statusMessage, connectionStatus: Handle CoreEntity {.final.} = ref object of Entity core: Tox statusCounts: array[3, int] handles: CoreHandles friends: Table[Friend, FriendEntity] proc init(e: Entity; turn: var Turn; parent: Ref): Handle = assert e.facet.isNil e.facet = turn.facet e.ds = newDataspace(turn) proc initCore(entity: CoreEntity; turn: var Turn; parentRef: Ref) = assert entity.core.isNil block: # Tox initialization var proxy_host: cstring entity.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 block: # Syndicate entity initialization discard init(entity, turn, parentRef) discard publish(turn, parentRef, ToxDataspace[Ref]( publicKey: entity.core.publicKey.bytes.toSeq, entity: entity.ds.embed)) discard publish(turn, entity.ds, CoreVersion( major: int version_major(), minor: int version_minor(), patch: int version_patch())) entity.handles.address = publish(turn, entity.ds, protocol.Address(text: $entity.core.address)) entity.handles.name = publish(turn, entity.ds, Name(name: entity.core.name)) entity.handles.statusMessage = publish(turn, entity.ds, StatusMessage(msg: entity.core.statusMessage)) block: # Friends initialization proc createFriend(turn: var Turn; fn: Friend) = var fe = new FriendEntity discard init(fe, turn, entity.ds) discard publish(turn, entity.ds, FriendDataspace[Ref]( publicKey: entity.core.publicKey(fn).bytes.toSeq, entity: fe.ds.embed)) fe.handles.name = publish(turn, fe.ds, Name(name: entity.core.name(fn))) entity.friends[fn] = fe for fn in entity.core.friends: createFriend(turn, fn) entity.core.onSelfConnectionStatus do (status: toxcore.Connection): run(entity.facet) do (turn: var Turn): let conn = case status of TOX_CONNECTION_NONE: protocol.Connection.none of TOX_CONNECTION_TCP: protocol.Connection.tcp of TOX_CONNECTION_UDP: protocol.Connection.udp replace(turn, entity.ds, entity.handles.connectionStatus, Status(status: conn)) template update[T](fe: FriendEntity; h: var Handle; a: T) = run(fe.facet) do (turn: var Turn): replace(turn, fe.ds, h, a) entity.core.onFriendName do (num: Friend; name: string): let fe = entity.friends[num] update(fe, fe.handles.name, Name(name: name)) entity.core.onFriendStatusMessage do (num: Friend; msg: string): let fe = entity.friends[num] update(fe, fe.handles.statusMessage, StatusMessage(msg: msg)) entity.core.onFriendTyping do (num: Friend; typing: bool): let fe = entity.friends[num] if typing: update(fe, fe.handles.typing, Typing()) else: run(fe.facet) do (turn: var Turn): retract(turn, fe.handles.typing) entity.core.onFriendRequest do (pk: PublicKey; msg: string): run(entity.facet) do (turn: var Turn): let reqHandle = publish(turn, entity.ds, FriendRequest(key: pk.bytes.toSeq, msg: msg)) onPublish(turn, entity.ds, ?FriendAccept(key: pk.bytes.toSeq)) do: createFriend(turn, entity.core.addFriendNoRequest(pk)) retract(turn, reqHandle) # TODO: stop watching for the accept assertion var alive: bool setControlCHook do: if not alive: quit() alive = false proc run(entity: CoreEntity) = alive = true bootDataspace("main") do (ds: Ref; turn: var Turn): connectStdio(ds, turn) initCore(entity, turn, ds) onPublish(turn, ds, ?BootstrapNode) do (key: string; host: string; port: int): info "Bootstrapping from ", key, "@", host, ":", port try: entity.core.bootstrap(host, key.toPublicKey, uint16 port) except ToxError as e: error "failed to bootstrap: ", e.msg poll() while alive: iterate entity.core poll(entity.core.iterationInterval.inMilliseconds.int) run(new CoreEntity)