Add file transfer conversations
This commit is contained in:
parent
2f83d0ef62
commit
72d959dd6e
|
@ -23,4 +23,13 @@ FriendRequest = <request @key bytes @msg string> .
|
||||||
; Asserted to the core to accept a friend request.
|
; Asserted to the core to accept a friend request.
|
||||||
FriendAccept = <accept @key bytes> .
|
FriendAccept = <accept @key bytes> .
|
||||||
|
|
||||||
|
; Messages sent by friend entities.
|
||||||
|
FriendMessage = <msg @body string @kind int> .
|
||||||
|
|
||||||
|
; Asserted by a friend while a transfer is active.
|
||||||
|
TransferDataspace = <transfer @kind int @size int @filename string @entity #!any> .
|
||||||
|
|
||||||
|
; Asserted to the transfer entity to indicate the transfer should be saved to a file
|
||||||
|
TransferSink = <sink @path string> .
|
||||||
|
|
||||||
BootstrapNode = <bootstrap @publicKey string @host string @port int> .
|
BootstrapNode = <bootstrap @publicKey string @host string @port int> .
|
||||||
|
|
|
@ -21,16 +21,26 @@ type
|
||||||
`publicKey`*: seq[byte]
|
`publicKey`*: seq[byte]
|
||||||
`entity`*: Preserve[E]
|
`entity`*: Preserve[E]
|
||||||
|
|
||||||
|
FriendMessage* {.preservesRecord: "msg".} = object
|
||||||
|
`body`*: string
|
||||||
|
`kind`*: BiggestInt
|
||||||
|
|
||||||
Typing* {.preservesRecord: "typing".} = object
|
Typing* {.preservesRecord: "typing".} = object
|
||||||
|
|
||||||
BootstrapNode* {.preservesRecord: "bootstrap".} = object
|
BootstrapNode* {.preservesRecord: "bootstrap".} = object
|
||||||
`publicKey`*: string
|
`publicKey`*: string
|
||||||
`host`*: string
|
`host`*: string
|
||||||
`port`*: int
|
`port`*: BiggestInt
|
||||||
|
|
||||||
StatusMessage* {.preservesRecord: "status-message".} = object
|
StatusMessage* {.preservesRecord: "status-message".} = object
|
||||||
`msg`*: string
|
`msg`*: string
|
||||||
|
|
||||||
|
TransferDataspace*[E] {.preservesRecord: "transfer".} = ref object
|
||||||
|
`kind`*: BiggestInt
|
||||||
|
`size`*: BiggestInt
|
||||||
|
`filename`*: string
|
||||||
|
`entity`*: Preserve[E]
|
||||||
|
|
||||||
Status* {.preservesRecord: "status".} = object
|
Status* {.preservesRecord: "status".} = object
|
||||||
`status`*: Connection
|
`status`*: Connection
|
||||||
|
|
||||||
|
@ -39,27 +49,35 @@ type
|
||||||
|
|
||||||
`Connection`* {.preservesOr, pure.} = enum
|
`Connection`* {.preservesOr, pure.} = enum
|
||||||
`none`, `tcp`, `udp`
|
`none`, `tcp`, `udp`
|
||||||
CoreVersion* {.preservesRecord: "core".} = object
|
TransferSink* {.preservesRecord: "sink".} = object
|
||||||
`major`*: int
|
`path`*: string
|
||||||
`minor`*: int
|
|
||||||
`patch`*: int
|
|
||||||
|
|
||||||
proc `$`*[E](x: FriendDataspace[E] | ToxDataspace[E]): string =
|
CoreVersion* {.preservesRecord: "core".} = object
|
||||||
|
`major`*: BiggestInt
|
||||||
|
`minor`*: BiggestInt
|
||||||
|
`patch`*: BiggestInt
|
||||||
|
|
||||||
|
proc `$`*[E](x: FriendDataspace[E] | ToxDataspace[E] | TransferDataspace[E]): string =
|
||||||
`$`(toPreserve(x, E))
|
`$`(toPreserve(x, E))
|
||||||
|
|
||||||
proc encode*[E](x: FriendDataspace[E] | ToxDataspace[E]): seq[byte] =
|
proc encode*[E](x: FriendDataspace[E] | ToxDataspace[E] | TransferDataspace[E]): seq[
|
||||||
|
byte] =
|
||||||
encode(toPreserve(x, E))
|
encode(toPreserve(x, E))
|
||||||
|
|
||||||
proc `$`*(x: Name | FriendRequest | Address | Typing | BootstrapNode |
|
proc `$`*(x: Name | FriendRequest | Address | FriendMessage | Typing |
|
||||||
|
BootstrapNode |
|
||||||
StatusMessage |
|
StatusMessage |
|
||||||
Status |
|
Status |
|
||||||
FriendAccept |
|
FriendAccept |
|
||||||
|
TransferSink |
|
||||||
CoreVersion): string =
|
CoreVersion): string =
|
||||||
`$`(toPreserve(x))
|
`$`(toPreserve(x))
|
||||||
|
|
||||||
proc encode*(x: Name | FriendRequest | Address | Typing | BootstrapNode |
|
proc encode*(x: Name | FriendRequest | Address | FriendMessage | Typing |
|
||||||
|
BootstrapNode |
|
||||||
StatusMessage |
|
StatusMessage |
|
||||||
Status |
|
Status |
|
||||||
FriendAccept |
|
FriendAccept |
|
||||||
|
TransferSink |
|
||||||
CoreVersion): seq[byte] =
|
CoreVersion): seq[byte] =
|
||||||
encode(toPreserve(x))
|
encode(toPreserve(x))
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
|
# SPDX-FileCopyrightText: ☭ 2022 Emery Hemingway
|
||||||
# SPDX-License-Identifier: Unlicense
|
# SPDX-License-Identifier: Unlicense
|
||||||
|
|
||||||
import std/[asyncdispatch, logging, parseopt, strutils, tables]
|
import std/[asyncdispatch, asyncfile, logging, parseopt, strutils, tables]
|
||||||
|
from std/os import fileExists, copyFile, moveFile
|
||||||
from std/sequtils import toSeq
|
from std/sequtils import toSeq
|
||||||
from std/times import inMilliseconds
|
from std/times import inMilliseconds
|
||||||
|
|
||||||
|
@ -28,16 +29,35 @@ proc logging_callback(
|
||||||
if lvl != lvlNone:
|
if lvl != lvlNone:
|
||||||
log(lvl, `func`, ": ", message)
|
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
|
type
|
||||||
Entity = ref object of RootObj
|
Entity = ref object of RootObj
|
||||||
facet: Facet
|
facet: Facet
|
||||||
ds: Ref
|
ds: Ref
|
||||||
|
|
||||||
|
TransferEntity {.final.} = ref object of Entity
|
||||||
|
dsHandle: Handle
|
||||||
|
sinks: OrderedTable[string, AsyncFile]
|
||||||
|
size: uint64
|
||||||
|
|
||||||
FriendHandles = object
|
FriendHandles = object
|
||||||
name, statusMessage, lastOnline, typing: Handle
|
name, statusMessage, lastOnline, typing: Handle
|
||||||
|
|
||||||
FriendEntity {.final.} = ref object of Entity
|
FriendEntity {.final.} = ref object of Entity
|
||||||
handles: FriendHandles
|
handles: FriendHandles
|
||||||
|
transfers: Table[FileTransfer, TransferEntity]
|
||||||
|
|
||||||
CoreHandles = object
|
CoreHandles = object
|
||||||
address, name, statusMessage, connectionStatus: Handle
|
address, name, statusMessage, connectionStatus: Handle
|
||||||
|
@ -52,10 +72,50 @@ proc init(e: Entity; turn: var Turn; parent: Ref): Handle =
|
||||||
e.facet = turn.facet
|
e.facet = turn.facet
|
||||||
e.ds = newDataspace(turn)
|
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) =
|
proc initCore(entity: CoreEntity; turn: var Turn; parentRef: Ref) =
|
||||||
assert entity.core.isNil
|
assert entity.core.isNil
|
||||||
block: # Tox initialization
|
block: # Tox initialization
|
||||||
var proxy_host: cstring
|
var
|
||||||
|
proxy_host: cstring
|
||||||
|
saveIsFresh = false
|
||||||
entity.core = newTox do (opts: toxcore.Options):
|
entity.core = newTox do (opts: toxcore.Options):
|
||||||
opts.log_callback = logging_callback
|
opts.log_callback = logging_callback
|
||||||
debug "parsing command-line options…"
|
debug "parsing command-line options…"
|
||||||
|
@ -99,12 +159,19 @@ proc initCore(entity: CoreEntity; turn: var Turn; parentRef: Ref) =
|
||||||
opts.tcp_port = parsePortParam(key, val)
|
opts.tcp_port = parsePortParam(key, val)
|
||||||
of "hole-punching":
|
of "hole-punching":
|
||||||
opts.hole_punching_enabled = parseBoolParam(key, val)
|
opts.hole_punching_enabled = parseBoolParam(key, val)
|
||||||
of "save-file": discard
|
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:
|
else:
|
||||||
quit("unhandled command-line parameter: " & key)
|
quit("unhandled command-line parameter: " & key)
|
||||||
of cmdShortOption, cmdArgument:
|
of cmdShortOption, cmdArgument:
|
||||||
quit("unhandled command-line parameter: " & key)
|
quit("unhandled command-line parameter: " & key)
|
||||||
of cmdEnd: discard
|
of cmdEnd: discard
|
||||||
|
if saveIsFresh:
|
||||||
|
writeSaveData(entity.core)
|
||||||
block: # Syndicate entity initialization
|
block: # Syndicate entity initialization
|
||||||
discard init(entity, turn, parentRef)
|
discard init(entity, turn, parentRef)
|
||||||
discard publish(turn, parentRef, ToxDataspace[Ref](
|
discard publish(turn, parentRef, ToxDataspace[Ref](
|
||||||
|
@ -128,7 +195,9 @@ proc initCore(entity: CoreEntity; turn: var Turn; parentRef: Ref) =
|
||||||
discard publish(turn, entity.ds, FriendDataspace[Ref](
|
discard publish(turn, entity.ds, FriendDataspace[Ref](
|
||||||
publicKey: entity.core.publicKey(fn).bytes.toSeq,
|
publicKey: entity.core.publicKey(fn).bytes.toSeq,
|
||||||
entity: fe.ds.embed))
|
entity: fe.ds.embed))
|
||||||
fe.handles.name = publish(turn, fe.ds, Name(name: entity.core.name(fn)))
|
let name = entity.core.name(fn)
|
||||||
|
if name != "":
|
||||||
|
fe.handles.name = publish(turn, fe.ds, Name(name: name))
|
||||||
entity.friends[fn] = fe
|
entity.friends[fn] = fe
|
||||||
|
|
||||||
for fn in entity.core.friends: createFriend(turn, fn)
|
for fn in entity.core.friends: createFriend(turn, fn)
|
||||||
|
@ -168,10 +237,54 @@ proc initCore(entity: CoreEntity; turn: var Turn; parentRef: Ref) =
|
||||||
onPublish(turn, entity.ds, ?FriendAccept(key: pk.bytes.toSeq)) do:
|
onPublish(turn, entity.ds, ?FriendAccept(key: pk.bytes.toSeq)) do:
|
||||||
createFriend(turn, entity.core.addFriendNoRequest(pk))
|
createFriend(turn, entity.core.addFriendNoRequest(pk))
|
||||||
retract(turn, reqHandle)
|
retract(turn, reqHandle)
|
||||||
|
writeSaveData(entity.core)
|
||||||
# TODO: stop watching for the accept assertion
|
# 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
|
var alive: bool
|
||||||
setControlCHook do:
|
setControlCHook do:
|
||||||
|
info "quiting"
|
||||||
if not alive: quit()
|
if not alive: quit()
|
||||||
alive = false
|
alive = false
|
||||||
|
|
||||||
|
@ -188,8 +301,10 @@ proc run(entity: CoreEntity) =
|
||||||
error "failed to bootstrap: ", e.msg
|
error "failed to bootstrap: ", e.msg
|
||||||
|
|
||||||
poll()
|
poll()
|
||||||
|
writeSaveData(entity.core)
|
||||||
while alive:
|
while alive:
|
||||||
iterate entity.core
|
iterate entity.core
|
||||||
poll(entity.core.iterationInterval.inMilliseconds.int)
|
poll(entity.core.iterationInterval.inMilliseconds.int)
|
||||||
|
writeSaveData(entity.core)
|
||||||
|
|
||||||
run(new CoreEntity)
|
run(new CoreEntity)
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
let ?root_ds = dataspace
|
|
||||||
<bind "syndicate" #x"" $root_ds>
|
|
||||||
<require-service <relay-listener <tcp "0.0.0.0" 9001> $gatekeeper>>
|
|
||||||
|
|
||||||
<require-service <daemon tox_actor>>
|
|
||||||
|
|
||||||
<daemon tox_actor {
|
|
||||||
argv: [
|
|
||||||
"/home/repo/syndicate/syndicate_actor_tox/src/syndicate_actor_tox"
|
|
||||||
"--ipv6"
|
|
||||||
"--local-discovery"
|
|
||||||
"--udp"
|
|
||||||
]
|
|
||||||
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>
|
|
||||||
]
|
|
||||||
]
|
|
|
@ -1,29 +1,60 @@
|
||||||
<require-service <daemon tox_bot>>
|
<require-service <daemon tox_bot>>
|
||||||
<daemon tox_bot {
|
<daemon tox_bot {
|
||||||
argv: "/home/repo/syndicate/syndicate_actor_tox/syndicate_actor_tox"
|
argv: [
|
||||||
dir: "/home/repo/syndicate/syndicate_actor_tox"
|
"/home/repo/syndicate/syndicate_actor_tox/syndicate_actor_tox"
|
||||||
|
"--save-file:/home/emery/lib/syndicate/tox.save"
|
||||||
|
"--local-discovery:true"
|
||||||
|
]
|
||||||
protocol: text/syndicate
|
protocol: text/syndicate
|
||||||
clearEnv: #t
|
clearEnv: #t
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
<require-service <daemon tox_eris_ingester>>
|
||||||
|
<daemon tox_eris_ingester {
|
||||||
|
argv: [ "/home/repo/syndicate/syndicate_actor_tox/tox_eris_ingester" ]
|
||||||
|
protocol: text/syndicate
|
||||||
|
clearEnv: #t
|
||||||
|
}>
|
||||||
|
|
||||||
|
|
||||||
|
; wait for the tox_bot to come up and announce itself
|
||||||
? <service-object <daemon tox_bot> ?tox> [
|
? <service-object <daemon tox_bot> ?tox> [
|
||||||
$config ? <service-object <daemon freedesktop_notifier> ?notifier> [
|
$tox <bootstrap "6EF679EBD205E8DF9B6975D21CD157D046287700CADDF86F94B7ED243DC26A30" "192.168.144.1" 33445>
|
||||||
$tox [
|
|
||||||
<bootstrap "6EF679EBD205E8DF9B6975D21CD157D046287700CADDF86F94B7ED243DC26A30" "20a:c3d2:8cf8:f8e5:80fe:9194:3800:87e6" 33445>
|
; wait for the core capability to be announced
|
||||||
? <tox ?pk ?core> $core [
|
$tox ? <tox ?pk ?core> $core [
|
||||||
? <address ?addr> $log ! <log "-" { line: ["tox address" $addr] }>
|
|
||||||
? <request ?pk ?msg> [
|
; log the address of the core
|
||||||
$notifier ! <notify "friend request" $msg 0 Low>
|
? <address ?addr> $log ! <log "-" { line: ["tox address" $addr] }>
|
||||||
<accept $pk>
|
|
||||||
|
; notify on friend request
|
||||||
|
? <request ?pk ?msg> [
|
||||||
|
; auto-accept
|
||||||
|
<accept $pk>
|
||||||
|
]
|
||||||
|
|
||||||
|
; wait for capability to a friend
|
||||||
|
? <friend ?pk ?friend> $friend [
|
||||||
|
|
||||||
|
; get the friend name
|
||||||
|
? <name ?name> [
|
||||||
|
; get the status message
|
||||||
|
? <status-message ?msg> [ $log ! <log "-" { line: [$name $msg]}> ]
|
||||||
|
|
||||||
|
?? <msg ?body ?kind> [
|
||||||
|
$log ! <log "-" { line: [$name $kind $body]}>
|
||||||
]
|
]
|
||||||
? <friend ?pk ?friend> $friend [
|
]
|
||||||
$notifier ! <notify "new friend" $pk 0 Low>
|
|
||||||
? <name ?name> [
|
? <transfer ?kind ?size ?filename ?transfer> [
|
||||||
$notifier ! <notify "friend name" $name 0 Low>
|
$log ! <log "-" { line: [$kind $size $filename $transfer]}>
|
||||||
? <status-message ?msg> [ $notifier ! <notify $name $msg 0 Low> ]
|
$config ? <service-object <daemon tox_eris_ingester> ?ingester> [
|
||||||
]
|
$log ! <log "-" { line: ["ingester is up at " $ingester] }>
|
||||||
|
$ingester <transfer $kind $size $filename $transfer>
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue