311 lines
10 KiB
Nim
311 lines
10 KiB
Nim
# 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)
|