syndicate_actor_tox/src/syndicate_actor_tox.nim

335 lines
11 KiB
Nim

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