# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[asyncdispatch, asyncfile, logging, parseopt, strutils, tables] from std/os import fileExists, copyFile, moveFile 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) proc saveFilePath(): string = for kind, key, val in getopt(): if kind == cmdLongOption and key == "save-file" and val != "": result = val proc writeSaveData(core: Tox) = let path = saveFilePath() if path != "": let tmpPath = path & ".tmp" writeFile(tmpPath, core.saveData) moveFile(tmpPath, path) debug("Data saved to ", path) type Entity = ref object of RootObj facet: Facet ds: Ref TransferEntity {.final.} = ref object of Entity dsHandle: Handle sinks: OrderedTable[string, AsyncFile] size: uint64 FriendHandles = object name, statusMessage, lastOnline, typing: Handle FriendEntity {.final.} = ref object of Entity handles: FriendHandles transfers: Table[FileTransfer, TransferEntity] 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 copyExisting(te: TransferEntity; path: string) = if te.sinks.len > 0: var err: ref Exception for existingPath in te.sinks.keys: try: copyFile(existingPath, path) # TODO: async copy, don't block tox iterate return except Exception as e: err = e raise err proc openSink(te: TransferEntity; path: string; size: int64) = copyExisting(te, path) var file: AsyncFile try: file = openAsync(path, fmReadWriteExisting) except: file = openAsync(path, fmReadWrite) # Stupid file modes setFileSize(file, size) te.sinks[path] = file proc closeSink(te: TransferEntity; path: string) = var file: AsyncFile if te.sinks.pop(path, file): close file proc closeSinks(te: TransferEntity) = for file in te.sinks.values: close(file) proc write(te: TransferEntity; pos: uint64; data: pointer; size: int): Future[void] = var futs: seq[Future[void]] for file in te.sinks.values: file.setFilePos(int64 pos) futs.add file.writeBuffer(data, size) result = all futs proc initCore(entity: CoreEntity; turn: var Turn; parentRef: Ref) = assert entity.core.isNil block: # Tox initialization var proxy_host: cstring saveIsFresh = false 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": let saveFilePath = val if fileExists saveFilePath: opts.savedata = cast[seq[byte]](readFile saveFilePath) opts.savedata_type = TOX_SAVEDATA_TYPE_TOX_SAVE else: saveIsFresh = true else: quit("unhandled command-line parameter: " & key) of cmdShortOption, cmdArgument: quit("unhandled command-line parameter: " & key) of cmdEnd: discard if saveIsFresh: writeSaveData(entity.core) 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)) let name = entity.core.name(fn) if name != "": fe.handles.name = publish(turn, fe.ds, Name(name: name)) 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) writeSaveData(entity.core) # TODO: stop watching for the accept assertion entity.core.onFriendMessage do (num: Friend; msg: string; kind: MessageType): let fe = entity.friends[num] run(fe.facet) do (turn: var Turn): message(turn, fe.ds, FriendMessage(body: msg, kind: int kind)) entity.core.onFriendLosslessPacket do ( num: Friend; data: pointer; len: int): info "friend sent a lossy packet of ", len, " bytes" entity.core.onFileRecv do (fn: Friend; tn: FileTransfer; kind: uint32; size: uint64; filename: string): let fe = entity.friends[fn] run(fe.facet) do (turn: var Turn): let te = TransferEntity(size: size) discard init(te, turn, fe.ds) te.dsHandle = publish(turn, fe.ds, TransferDataspace[Ref]( kind: int kind, size: BiggestInt size, filename: filename, entity: te.ds.embed)) during(turn, te.ds, ?TransferSink) do (path: string): te.openSink(path, int64 size) if te.sinks.len == 1: entity.core.control(fn, tn, TOX_FILE_CONTROL_RESUME) do: te.closeSink(path) if te.sinks.len == 0: entity.core.control(fn, tn, TOX_FILE_CONTROL_CANCEL) fe.transfers[tn] = te entity.core.onFileRecvChunk do (fn: Friend; tn: FileTransfer; pos: uint64; data: pointer; size: int): let fe = entity.friends[fn] te = fe.transfers[tn] if size != 0: waitFor te.write(pos, data, size) # wait for all the writes to complete within the lifetime of the callback else: te.closeSinks() run(fe.facet) do (turn: var Turn): retract(turn, te.dsHandle) fe.transfers.del(tn) var alive: bool setControlCHook do: info "quiting" 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() writeSaveData(entity.core) while alive: iterate entity.core poll(entity.core.iterationInterval.inMilliseconds.int) writeSaveData(entity.core) run(new CoreEntity)