Initial commit
This commit is contained in:
commit
51823d5688
|
@ -0,0 +1 @@
|
||||||
|
syndicate_actor_tox
|
|
@ -0,0 +1,32 @@
|
||||||
|
version 1 .
|
||||||
|
|
||||||
|
ListenOn = <listen-on @dataspace #!any> .
|
||||||
|
|
||||||
|
BootstrapNode = <bootstrap @publicKey string @host string @port int> .
|
||||||
|
|
||||||
|
ToxCoreVersion = <ToxCoreVersion @major int @minor int @patch int> .
|
||||||
|
|
||||||
|
ToxSelfAddress = <ToxSelfAddress @address bytes> .
|
||||||
|
|
||||||
|
ToxSelfConnectionStatus = <ToxSelfConnectionStatus @status symbol> .
|
||||||
|
|
||||||
|
ToxSelfName = <ToxSelfName @name string> .
|
||||||
|
|
||||||
|
Status = =online / =away / =busy .
|
||||||
|
ToxSelfStatus = <ToxSelfStatus @status Status> .
|
||||||
|
|
||||||
|
ToxSelfStatusMessage = <ToxSelfStatusMessage @message string> .
|
||||||
|
|
||||||
|
ToxFriendKey = <ToxFriendkey @num int @key bytes> .
|
||||||
|
|
||||||
|
ToxFriendLastOnline = <ToxFriendLastNnline @num int @unixEpoch double> .
|
||||||
|
|
||||||
|
ToxFriendName = <ToxFriendName @num int @name string> .
|
||||||
|
|
||||||
|
ToxFriendStatusMessage = <ToxFriendStatusMessage @num int @message string> .
|
||||||
|
|
||||||
|
ToxFriendTyping = <ToxFriendTyping @num int> .
|
||||||
|
|
||||||
|
ToxFriendAdd = <ToxFriendAdd @address bytes @message string> .
|
||||||
|
|
||||||
|
ToxFriendSend = <ToxFriendSend @num int @message string> .
|
|
@ -0,0 +1,2 @@
|
||||||
|
include_rules
|
||||||
|
: foreach ../*.prs |> !preserves_schema_nim |> %B.nim
|
|
@ -0,0 +1,95 @@
|
||||||
|
|
||||||
|
import
|
||||||
|
std/typetraits, preserves
|
||||||
|
|
||||||
|
type
|
||||||
|
ToxFriendSend* {.preservesRecord: "ToxFriendSend".} = object
|
||||||
|
`num`*: int
|
||||||
|
`message`*: string
|
||||||
|
|
||||||
|
ToxCoreVersion* {.preservesRecord: "ToxCoreVersion".} = object
|
||||||
|
`major`*: int
|
||||||
|
`minor`*: int
|
||||||
|
`patch`*: int
|
||||||
|
|
||||||
|
ToxFriendLastOnline* {.preservesRecord: "ToxFriendLastNnline".} = object
|
||||||
|
`num`*: int
|
||||||
|
`unixEpoch`*: float64
|
||||||
|
|
||||||
|
ToxSelfConnectionStatus* {.preservesRecord: "ToxSelfConnectionStatus".} = object
|
||||||
|
`status`*: Symbol
|
||||||
|
|
||||||
|
ToxSelfStatus* {.preservesRecord: "ToxSelfStatus".} = object
|
||||||
|
`status`*: Status
|
||||||
|
|
||||||
|
BootstrapNode* {.preservesRecord: "bootstrap".} = object
|
||||||
|
`publicKey`*: string
|
||||||
|
`host`*: string
|
||||||
|
`port`*: int
|
||||||
|
|
||||||
|
ToxSelfStatusMessage* {.preservesRecord: "ToxSelfStatusMessage".} = object
|
||||||
|
`message`*: string
|
||||||
|
|
||||||
|
`Status`* {.preservesOr, pure.} = enum
|
||||||
|
`online`, `away`, `busy`
|
||||||
|
ToxSelfName* {.preservesRecord: "ToxSelfName".} = object
|
||||||
|
`name`*: string
|
||||||
|
|
||||||
|
ToxFriendKey* {.preservesRecord: "ToxFriendkey".} = object
|
||||||
|
`num`*: int
|
||||||
|
`key`*: seq[byte]
|
||||||
|
|
||||||
|
ListenOn*[E] {.preservesRecord: "listen-on".} = ref object
|
||||||
|
`dataspace`*: Preserve[E]
|
||||||
|
|
||||||
|
ToxSelfAddress* {.preservesRecord: "ToxSelfAddress".} = object
|
||||||
|
`address`*: seq[byte]
|
||||||
|
|
||||||
|
ToxFriendAdd* {.preservesRecord: "ToxFriendAdd".} = object
|
||||||
|
`address`*: seq[byte]
|
||||||
|
`message`*: string
|
||||||
|
|
||||||
|
ToxFriendStatusMessage* {.preservesRecord: "ToxFriendStatusMessage".} = object
|
||||||
|
`num`*: int
|
||||||
|
`message`*: string
|
||||||
|
|
||||||
|
ToxFriendName* {.preservesRecord: "ToxFriendName".} = object
|
||||||
|
`num`*: int
|
||||||
|
`name`*: string
|
||||||
|
|
||||||
|
ToxFriendTyping* {.preservesRecord: "ToxFriendTyping".} = object
|
||||||
|
`num`*: int
|
||||||
|
|
||||||
|
proc `$`*[E](x: ListenOn[E]): string =
|
||||||
|
`$`(toPreserve(x, E))
|
||||||
|
|
||||||
|
proc encode*[E](x: ListenOn[E]): seq[byte] =
|
||||||
|
encode(toPreserve(x, E))
|
||||||
|
|
||||||
|
proc `$`*(x: ToxFriendSend | ToxCoreVersion | ToxFriendLastOnline |
|
||||||
|
ToxSelfConnectionStatus |
|
||||||
|
ToxSelfStatus |
|
||||||
|
BootstrapNode |
|
||||||
|
ToxSelfStatusMessage |
|
||||||
|
ToxSelfName |
|
||||||
|
ToxFriendKey |
|
||||||
|
ToxSelfAddress |
|
||||||
|
ToxFriendAdd |
|
||||||
|
ToxFriendStatusMessage |
|
||||||
|
ToxFriendName |
|
||||||
|
ToxFriendTyping): string =
|
||||||
|
`$`(toPreserve(x))
|
||||||
|
|
||||||
|
proc encode*(x: ToxFriendSend | ToxCoreVersion | ToxFriendLastOnline |
|
||||||
|
ToxSelfConnectionStatus |
|
||||||
|
ToxSelfStatus |
|
||||||
|
BootstrapNode |
|
||||||
|
ToxSelfStatusMessage |
|
||||||
|
ToxSelfName |
|
||||||
|
ToxFriendKey |
|
||||||
|
ToxSelfAddress |
|
||||||
|
ToxFriendAdd |
|
||||||
|
ToxFriendStatusMessage |
|
||||||
|
ToxFriendName |
|
||||||
|
ToxFriendTyping): seq[byte] =
|
||||||
|
encode(toPreserve(x))
|
|
@ -0,0 +1,279 @@
|
||||||
|
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
|
||||||
|
# SPDX-License-Identifier: Unlicense
|
||||||
|
|
||||||
|
import std/[asyncdispatch, logging, parseopt, strutils, tables]
|
||||||
|
from std/os import fileExists, moveFile
|
||||||
|
from std/sequtils import toSeq
|
||||||
|
from std/times import inMilliseconds
|
||||||
|
|
||||||
|
import syndicate
|
||||||
|
import toxcore
|
||||||
|
import ./protocol
|
||||||
|
|
||||||
|
var alive: bool
|
||||||
|
setControlCHook do:
|
||||||
|
if not alive:
|
||||||
|
# hard stop
|
||||||
|
quit "exited without saving Tox data"
|
||||||
|
alive = false
|
||||||
|
|
||||||
|
proc console_log_callback(
|
||||||
|
core: Core; level: Log_Level;
|
||||||
|
file: cstring; line: uint32;
|
||||||
|
`func`: cstring; message: cstring;
|
||||||
|
user_data: pointer) {.exportc, cdecl.} =
|
||||||
|
stderr.writeLine "console_log_callback: ", `func`
|
||||||
|
#[
|
||||||
|
let console = cast[ConsoleLogger](user_data)
|
||||||
|
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(console, lvl, `func`, ": ", message)
|
||||||
|
]#
|
||||||
|
|
||||||
|
type
|
||||||
|
SelfHandles = object
|
||||||
|
connectionStatus, name, status, statusMessage: Handle
|
||||||
|
FriendHandles = object
|
||||||
|
key, lastOnline, name, statusMessage, typing: Handle
|
||||||
|
|
||||||
|
ToxActor = ref object
|
||||||
|
console: ConsoleLogger
|
||||||
|
facet: Facet
|
||||||
|
`ref`: Ref
|
||||||
|
saveFilePath: string
|
||||||
|
core: Tox
|
||||||
|
self: SelfHandles
|
||||||
|
friends: Table[Friend, FriendHandles]
|
||||||
|
|
||||||
|
template debug(actor: ToxActor; args: varargs[string, `$`]) =
|
||||||
|
actor.console.log(lvlDebug, args)
|
||||||
|
|
||||||
|
template info(actor: ToxActor; args: varargs[string, `$`]) =
|
||||||
|
actor.console.log(lvlInfo, args)
|
||||||
|
|
||||||
|
template warn(actor: ToxActor; args: varargs[string, `$`]) =
|
||||||
|
actor.console.log(lvlWarn, args)
|
||||||
|
|
||||||
|
template error(actor: ToxActor; args: varargs[string, `$`]) =
|
||||||
|
actor.console.log(lvlError, args)
|
||||||
|
|
||||||
|
proc writeSaveData(actor: ToxActor) =
|
||||||
|
if actor.saveFilePath != "":
|
||||||
|
let
|
||||||
|
path = actor.saveFilePath
|
||||||
|
tmpPath = path & ".tmp"
|
||||||
|
actor.debug("Duming data to ", tmpPath)
|
||||||
|
writeFile(tmpPath, actor.core.saveData)
|
||||||
|
moveFile(tmpPath, path)
|
||||||
|
actor.debug("Data saved to ", path)
|
||||||
|
|
||||||
|
proc publish[T](actor: ToxActor; assertion: T): Handle =
|
||||||
|
actor.info("publish ", assertion)
|
||||||
|
var h: Handle
|
||||||
|
actor.facet.run do (turn: var Turn):
|
||||||
|
h = publish(turn, actor.`ref`, assertion)
|
||||||
|
h
|
||||||
|
|
||||||
|
proc retract(actor: ToxActor; h: Handle) =
|
||||||
|
actor.facet.run do (turn: var Turn): retract(turn, h)
|
||||||
|
|
||||||
|
template replace[T](actor: ToxActor; h: var Handle; assertion: T) =
|
||||||
|
actor.facet.run do (turn: var Turn): replace(turn, actor.`ref`, h, assertion)
|
||||||
|
|
||||||
|
proc installCallbacks(actor: ToxActor; turn: var Turn) =
|
||||||
|
actor.core.status = TOX_USER_STATUS_AWAY
|
||||||
|
|
||||||
|
discard publish(turn, actor.`ref`,
|
||||||
|
ToxCoreVersion(
|
||||||
|
major: int version_major(),
|
||||||
|
minor: int version_minor(),
|
||||||
|
patch: int version_patch()))
|
||||||
|
discard publish(turn, actor.`ref`,
|
||||||
|
ToxSelfAddress(address: actor.core.address.bytes.toSeq))
|
||||||
|
|
||||||
|
actor.self.name =
|
||||||
|
publish(turn, actor.`ref`,
|
||||||
|
ToxSelfName(name: actor.core.name))
|
||||||
|
|
||||||
|
actor.self.statusMessage =
|
||||||
|
publish(turn, actor.`ref`,
|
||||||
|
ToxSelfStatusMessage(message: actor.core.statusMessage))
|
||||||
|
|
||||||
|
actor.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"
|
||||||
|
actor.replace(actor.self.connectionStatus,
|
||||||
|
ToxSelfConnectionStatus(status: sym))
|
||||||
|
|
||||||
|
for num in actor.core.friends:
|
||||||
|
var handles: FriendHandles
|
||||||
|
handles.key = publish(turn, actor.`ref`,
|
||||||
|
ToxFriendKey(num: int num, key: actor.core.publicKey(num).bytes.toSeq))
|
||||||
|
|
||||||
|
let lastOnline = actor.core.lastOnline(num)
|
||||||
|
if lastOnline > 0:
|
||||||
|
handles.lastOnline = publish(turn, actor.`ref`,
|
||||||
|
ToxFriendLastOnline(num: int num, unixEpoch: float64 lastOnline))
|
||||||
|
|
||||||
|
handles.name = publish(turn, actor.`ref`,
|
||||||
|
ToxFriendName(num: int num, name: actor.core.name(num)))
|
||||||
|
|
||||||
|
handles.statusMessage = publish(turn, actor.`ref`,
|
||||||
|
ToxFriendStatusMessage(num: int num, message: actor.core.statusMessage(num)))
|
||||||
|
actor.friends[num] = handles
|
||||||
|
|
||||||
|
actor.core.onFriendName do (num: Friend; name: string):
|
||||||
|
actor.replace(actor.friends[num].name, ToxFriendName(num: int num, name: name))
|
||||||
|
|
||||||
|
actor.core.onFriendStatusMessage do (num: Friend; msg: string):
|
||||||
|
actor.replace(
|
||||||
|
actor.friends[num].statusMessage,
|
||||||
|
ToxFriendStatusMessage(num: int num, message: msg))
|
||||||
|
|
||||||
|
actor.core.onFriendTyping do (num: Friend; typing: bool):
|
||||||
|
if typing:
|
||||||
|
actor.friends[num].typing =
|
||||||
|
actor.publish(
|
||||||
|
ToxFriendTyping(num: int num))
|
||||||
|
else:
|
||||||
|
actor.retract(actor.friends[num].typing)
|
||||||
|
actor.friends[num].typing = 0
|
||||||
|
|
||||||
|
proc newToxActor(`ref`: Ref; turn: var Turn): ToxActor =
|
||||||
|
result = ToxActor(
|
||||||
|
console: newConsoleLogger(useStderr=true),
|
||||||
|
facet: turn.facet,
|
||||||
|
`ref`: `ref`)
|
||||||
|
let actor = result
|
||||||
|
result.core = initTox do (opts: toxcore.Options):
|
||||||
|
opts.log_callback = console_log_callback
|
||||||
|
# opts.log_user_data = addr actor.console[]
|
||||||
|
actor.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)
|
||||||
|
var saveDataLoaded = false
|
||||||
|
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":
|
||||||
|
opts.proxy_host = val
|
||||||
|
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":
|
||||||
|
actor.saveFilePath = val
|
||||||
|
else:
|
||||||
|
quit("unhandled command-line parameter: " & key)
|
||||||
|
of cmdShortOption, cmdArgument:
|
||||||
|
quit("unhandled command-line parameter: " & key)
|
||||||
|
of cmdEnd: discard
|
||||||
|
if actor.saveFilePath != "" and fileExists actor.saveFilePath:
|
||||||
|
actor.info "Loading saved data from ", actor.saveFilePath
|
||||||
|
opts.savedata_type = TOX_SAVEDATA_TYPE_TOX_SAVE
|
||||||
|
opts.savedata = readFile(actor.saveFilePath)
|
||||||
|
installCallbacks(result, turn)
|
||||||
|
let
|
||||||
|
ms = result.core.iterationInterval.inMilliseconds.int
|
||||||
|
oneshot = false
|
||||||
|
addTimer(ms, oneshot) do (fd: AsyncFD) -> bool:
|
||||||
|
if not alive:
|
||||||
|
actor.writeSaveData()
|
||||||
|
quit()
|
||||||
|
iterate actor.core
|
||||||
|
result.info "Tox actor ready"
|
||||||
|
|
||||||
|
bootDataspace("main") do (root: Ref; turn: var Turn):
|
||||||
|
connectStdio(root, turn)
|
||||||
|
alive = true
|
||||||
|
|
||||||
|
onPublish(turn, root, ?ListenOn[Ref]) do (a: Assertion):
|
||||||
|
let
|
||||||
|
ds = unembed a
|
||||||
|
actor = newToxActor(ds, turn)
|
||||||
|
|
||||||
|
onPublish(turn, root, ?BootstrapNode) do (key: string; host: string; port: int):
|
||||||
|
actor.info("Bootstrapping from ", key, "@", host, ":", port)
|
||||||
|
try: actor.core.bootstrap(host, key.toPublicKey, uint16 port)
|
||||||
|
except ToxError as e:
|
||||||
|
actor.error "failed to bootstrap: ", e.msg
|
||||||
|
|
||||||
|
onMessage(turn, ds, ?ToxSelfStatus) do (status: Status):
|
||||||
|
## TODO: Take the status of the core from the frontend.
|
||||||
|
## Set to away when the frontend retracts its status.
|
||||||
|
actor.debug "ToxSelfStatus ", rawValues
|
||||||
|
actor.core.status = case status
|
||||||
|
of Status.online: TOX_USER_STATUS_NONE
|
||||||
|
of Status.away: TOX_USER_STATUS_AWAY
|
||||||
|
of Status.busy: TOX_USER_STATUS_BUSY
|
||||||
|
#turn.facet.onStop do (turn: var Turn):
|
||||||
|
# actor.core.status = TOX_USER_STATUS_AWAY
|
||||||
|
|
||||||
|
onMessage(turn, ds, ?ToxSelfName) do (name: string):
|
||||||
|
actor.debug "ToxSelfName ", rawValues
|
||||||
|
actor.core.name = name
|
||||||
|
replace(turn, ds, actor.self.name, ToxSelfName(name: actor.core.name))
|
||||||
|
actor.writeSaveData()
|
||||||
|
|
||||||
|
onMessage(turn, ds, ?ToxSelfStatusMessage) do (msg: string):
|
||||||
|
actor.debug "ToxSelfStatusMessage ", rawValues
|
||||||
|
actor.core.statusMessage = msg
|
||||||
|
replace(turn, ds, actor.self.statusMessage,
|
||||||
|
ToxSelfStatusMessage(message: actor.core.statusMessage))
|
||||||
|
actor.writeSaveData()
|
||||||
|
|
||||||
|
onMessage(turn, ds, ?ToxFriendAdd) do (toxid: seq[byte]; msg: string):
|
||||||
|
actor.debug "ToxFriendAdd ", rawValues
|
||||||
|
var address: Address
|
||||||
|
if toxid.len != address.bytes.len:
|
||||||
|
actor.error "invalid Tox address: ", toxid
|
||||||
|
else:
|
||||||
|
copyMem(addr address.bytes[0], addr toxid[0], address.bytes.len)
|
||||||
|
try:
|
||||||
|
discard actor.core.addFriend(address, msg)
|
||||||
|
# TODO: assert a pending friend?
|
||||||
|
except ToxError as e:
|
||||||
|
actor.error "failed to add friend: ", e.msg
|
||||||
|
|
||||||
|
onMessage(turn, ds, ?ToxFriendSend) do (friend: Friend; msg: string):
|
||||||
|
actor.debug "ToxFriendSend ", rawValues
|
||||||
|
discard actor.core.send(friend, msg)
|
||||||
|
# TODO: assert pending messages?
|
||||||
|
|
||||||
|
runForever()
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Package
|
||||||
|
|
||||||
|
version = "0.1.0"
|
||||||
|
author = "Emery Hemingway"
|
||||||
|
description = "Tox chat actor for Syndicate"
|
||||||
|
license = "Unlicense"
|
||||||
|
srcDir = "src"
|
||||||
|
bin = @["syndicate_actor_tox"]
|
||||||
|
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
requires "nim >= 1.6.2", "syndicate >= 1.2.1"
|
|
@ -0,0 +1,22 @@
|
||||||
|
let ?root_ds = dataspace
|
||||||
|
<bind "syndicate" #x"" $root_ds>
|
||||||
|
|
||||||
|
<require-service <daemon tox_actor>>
|
||||||
|
|
||||||
|
<daemon tox_actor {
|
||||||
|
argv: [
|
||||||
|
"--ipv6"
|
||||||
|
"--local-discovery"
|
||||||
|
"--save-file:save.tox"
|
||||||
|
]
|
||||||
|
protocol: text/syndicate
|
||||||
|
}>
|
||||||
|
|
||||||
|
? <service-object <daemon tox_actor> ?cap> [
|
||||||
|
$cap [
|
||||||
|
<listen-on $root_ds>
|
||||||
|
<bootstrap "1D13A037DEEC07BA10A3ABA54A3D09075C51A81FA4A9939271CB11245C16A510" "201:7d01:2539:fb46:a575:bad1:98dd:d7ed" 33445>
|
||||||
|
<bootstrap "6EF679EBD205E8DF9B6975D21CD157D046287700CADDF86F94B7ED243DC26A30" "20a:c3d2:8cf8:f8e5:80fe:9194:3800:87e6" 33445>
|
||||||
|
<bootstrap "D527E5847F8330D628DAB1814F0A422F6DC9D0A300E6C357634EE2DA88C35463" "tox.novg.net" 33445>
|
||||||
|
]
|
||||||
|
]
|
Loading…
Reference in New Issue