1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 // Tox protocol thread
17 // Each Tox connection spawns a thread that does all the communication.
18 // Incoming messages are posted to [glconCtlWindow].
19 // Asking for outgoing actions are done with public interface.
20 // No tox internals are exposed to the outer world.
21 module toxproto
is aliced
;
26 import std
.concurrency
;
41 //version = ToxCoreDebug;
42 version = ToxCoreUseBuiltInDataFileParser
;
44 static assert(TOX_PUBLIC_KEY_SIZE
== ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
);
47 import accdb
: ContactStatus
, ContactInfo
;
50 // ////////////////////////////////////////////////////////////////////////// //
54 // ////////////////////////////////////////////////////////////////////////// //
55 string
buildNormalizedString(bool noteSpacesOnly
=false) (const(char)[] s
) nothrow {
57 if (s
.length
== 0) return null;
59 if (s
.length
== 0) { static if (noteSpacesOnly
) return "<spaces>"; else return null; }
61 res
.reserve(s
.length
);
62 foreach (char ch
; s
) {
64 if (res
.length
== 0 || res
[$-1] > ' ') res
~= ' ';
69 static if (noteSpacesOnly
) { if (res
.length
== 0) return "<spaces>"; }
70 return cast(string
)res
; // it is safe to cast here
74 // ////////////////////////////////////////////////////////////////////////// //
76 struct ToxCoreDataFile
{
77 string nick
; // user nick
79 ubyte[TOX_PUBLIC_KEY_SIZE
] pubkey
= toxCoreEmptyKey
;
80 ToxAddr addr
= toxCoreEmptyAddr
;
82 ContactStatus status
= ContactStatus
.Offline
;
84 ContactInfo
[] friends
;
86 @property bool valid () const pure nothrow @safe @nogc => isValidKey(pubkey
);
88 static ToxAddr
buildAddress (in ref ubyte[TOX_PUBLIC_KEY_SIZE
] pubkey
, uint nospam
) nothrow @trusted @nogc {
89 static ushort calcChecksum (const(ubyte)[] data
) nothrow @trusted @nogc {
90 ubyte[2] checksum
= 0;
91 foreach (immutable idx
, ubyte b
; data
) checksum
[idx
%2] ^
= b
;
92 return *cast(ushort*)checksum
.ptr
;
96 res
[0..TOX_PUBLIC_KEY_SIZE
] = pubkey
[];
97 *cast(uint*)(res
.ptr
+TOX_PUBLIC_KEY_SIZE
) = nospam
;
98 *cast(ushort*)(res
.ptr
+TOX_PUBLIC_KEY_SIZE
+uint.sizeof
) = calcChecksum(res
[0..$-2]);
104 // ////////////////////////////////////////////////////////////////////////// //
106 class ProtocolException
: Exception
{
107 this (string msg
, string file
=__FILE__
, usize line
=__LINE__
, Throwable next
=null) pure nothrow @safe @nogc {
108 super(msg
, file
, line
, next
);
113 // ////////////////////////////////////////////////////////////////////////// //
114 // messages thread sends to [glconCtlWindow].
115 // [who] can be the same as [self] to indicate account state changes
118 PubKey self
; // account
119 PubKey who
; // can be same as [self]
120 nothrow @trusted @nogc:
121 this (in ref PubKey aself
, in ref PubKey awho
) { self
[] = aself
[]; who
= awho
[]; }
124 // connection state changed
125 class ToxEventConnection
: ToxEventBase
{
127 nothrow @trusted @nogc:
128 this (in ref PubKey aself
, in ref PubKey awho
, bool aconnected
) { super(aself
, awho
); connected
= aconnected
; }
131 // online status changed
132 class ToxEventStatus
: ToxEventBase
{
133 ContactStatus status
;
134 nothrow @trusted @nogc:
135 this (in ref PubKey aself
, in ref PubKey awho
, ContactStatus astatus
) { super(aself
, awho
); status
= astatus
; }
139 class ToxEventNick
: ToxEventBase
{
140 string nick
; // new nick
141 nothrow @trusted @nogc:
142 this (in ref PubKey aself
, in ref PubKey awho
, string anick
) { super(aself
, awho
); nick
= anick
; }
145 // status message changed
146 class ToxEventStatusMsg
: ToxEventBase
{
147 string message
; // new message
148 nothrow @trusted @nogc:
149 this (in ref PubKey aself
, in ref PubKey awho
, string amessage
) { super(aself
, awho
); message
= amessage
; }
152 // typing status changed
153 class ToxEventTyping
: ToxEventBase
{
155 nothrow @trusted @nogc:
156 this (in ref PubKey aself
, in ref PubKey awho
, bool atyping
) { super(aself
, awho
); typing
= atyping
; }
160 class ToxEventMessage
: ToxEventBase
{
161 bool action
; // is this an "action" message? (/me)
165 this (in ref PubKey aself
, in ref PubKey awho
, bool aaction
, string amessage
) {
172 this (in ref PubKey aself
, in ref PubKey awho
, bool aaction
, string amessage
, SysTime atime
) {
180 // send message ack comes
181 class ToxEventMessageAck
: ToxEventBase
{
183 nothrow @trusted @nogc:
184 this (in ref PubKey aself
, in ref PubKey awho
, long amsgid
) { super(aself
, awho
); msgid
= amsgid
; }
187 // friend request comes
188 class ToxEventFriendReq
: ToxEventBase
{
189 string message
; // request message
190 nothrow @trusted @nogc:
191 this (in ref PubKey aself
, in ref PubKey awho
, string amessage
) { super(aself
, awho
); message
= amessage
; }
195 // ////////////////////////////////////////////////////////////////////////// //
197 /// shutdown protocol module
198 void toxCoreShutdownAll () {
200 synchronized(TPInfo
.classinfo
) {
201 foreach (TPInfo ti
; allProtos
) {
202 ti
.tid
.send(TrdCmdQuit
.init
);
206 while (acksLeft
> 0) {
208 (TrdCmdQuitAck ack
) { --acksLeft
; },
213 foreach (TPInfo tpi
; allProtos
) {
214 // save toxcode data, if there is any
215 if (tpi
.tox
!is null && tpi
.toxDataDiskName
.length
) saveToxCoreData(tpi
.tox
, tpi
.toxDataDiskName
);
216 if (tpi
.tox
!is null) { tox_kill(tpi
.tox
); tpi
.tox
= null; }
221 // ////////////////////////////////////////////////////////////////////////// //
224 static immutable PubKey toxCoreEmptyKey
= 0;
225 static immutable ToxAddr toxCoreEmptyAddr
= 0;
227 /// delegate that will be used to send messages.
228 /// can be called from any thread, so it should be thread-safe, and should avoid deadlocks.
229 /// the delegate should be set on program startup, and should not be changed anymore.
230 /// FIXME: add API to change this!
231 __gshared
void delegate (Object msg
) nothrow toxCoreSendEvent
;
234 bool isValidKey (in ref PubKey key
) pure nothrow @safe @nogc => (key
[] != toxCoreEmptyKey
[]);
237 bool isValidAddr (in ref ToxAddr key
) pure nothrow @safe @nogc => (key
[] != toxCoreEmptyAddr
[]);
240 /// parse toxcore data file.
241 /// sorry, i had to do it manually, 'cause there is no way to open toxcore data without going online.
242 public ToxCoreDataFile
toxCoreLoadDataFile (VFile fl
) nothrow {
245 version(ToxCoreUseBuiltInDataFileParser
) {
246 auto sz
= fl
.size
-fl
.tell
;
247 if (sz
< 8) throw new ProtocolException("data file too small");
248 if (fl
.readNum
!uint != 0) throw new ProtocolException("invalid something");
249 if (fl
.readNum
!uint != ToxCoreDataId
) throw new ProtocolException("not a ToxCore data file");
251 auto len
= fl
.readNum
!uint;
252 auto id
= fl
.readNum
!uint;
253 if (id
>>16 != ToxCoreChunkTypeHi
) throw new ProtocolException("invalid chunk hitype");
256 case ToxCoreChunkNoSpamKeys
:
257 if (len
== ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
+ToxSavedFriend
.CRYPTO_SECRET_KEY_SIZE
+uint.sizeof
) {
258 res
.nospam
= fl
.readNum
!uint;
259 fl
.rawReadExact(res
.pubkey
[]);
260 ubyte[ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
] privkey
;
261 fl
.rawReadExact(privkey
[]);
264 res
.addr
[] = res
.buildAddress(res
.pubkey
, res
.nospam
);
267 case ToxCoreChunkDHT
: break;
268 case ToxCoreChunkFriends
:
269 if (len
%ToxSavedFriend
.TOTAL_DATA_SIZE
!= 0) throw new ProtocolException("invalid contact list");
275 if (st
> len || st
!= ToxSavedFriend
.TOTAL_DATA_SIZE
) throw new ProtocolException("invalid contact list");
277 if (fr
.status
== ToxSavedFriend
.Status
.NoFriend
) continue;
279 ci
.nick
= fr
.name
[0..fr
.name_length
].buildNormalizedString
!true;
280 ci
.lastonlinetime
= cast(uint)fr
.last_seen_time
;
281 final switch (fr
.status
) {
282 case ToxSavedFriend
.Status
.NoFriend
: assert(0, "wtf?!");
284 case ToxSavedFriend
.Status
.Added
: ci
.kind
= ContactInfo
.Kind
.PengingAuthAccept
; break;
285 case ToxSavedFriend
.Status
.Requested
: ci
.kind
= ContactInfo
.Kind
.PengingAuthRequest
; break;
286 case ToxSavedFriend
.Status
.Confirmed
:
287 case ToxSavedFriend
.Status
.Online
:
288 ci
.kind
= ContactInfo
.Kind
.Friend
;
291 if (ci
.kind
!= ContactInfo
.Kind
.Friend
) {
292 ci
.statusmsg
= fr
.info
[0..fr
.info_size
].buildNormalizedString
!true;
294 ci
.statusmsg
= fr
.statusmessage
[0..fr
.statusmessage_length
].buildNormalizedString
;
296 ci
.nospam
= fr
.friendrequest_nospam
;
297 ci
.pubkey
[] = fr
.real_pk
[];
301 case ToxCoreChunkName
:
303 auto name
= new char[](len
);
304 fl
.rawReadExact(name
);
306 res
.nick
= cast(string
)name
; // it is safe to cast here
309 case ToxCoreChunkStatusMsg
:
311 auto msg
= new char[](len
);
312 fl
.rawReadExact(msg
);
314 res
.statusmsg
= cast(string
)msg
; // it is safe to cast here
317 case ToxCoreChunkStatus
:
319 auto st
= fl
.readNum
!ubyte;
320 if (st
< ToxSavedFriend
.UserStatus
.Invalid
) {
322 case ToxSavedFriend
.UserStatus
.None
: res
.status
= ContactStatus
.Online
; break;
323 case ToxSavedFriend
.UserStatus
.Away
: res
.status
= ContactStatus
.Away
; break;
324 case ToxSavedFriend
.UserStatus
.Busy
: res
.status
= ContactStatus
.Busy
; break;
325 default: res
.status
= ContactStatus
.Offline
; break;
331 case ToxCoreChunkTcpRelay
: break;
332 case ToxCoreChunkPathNode
: break;
335 if (id
== ToxCoreChunkEnd
) break;
336 fl
.seek(len
, Seek
.Cur
);
339 // nope, not found, try to open account
340 ProtoOptions protoOpts
;
341 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
342 if (tox
is null) throw new ProtocolException("cannot load toxcore data file");
343 scope(exit
) tox_kill(tox
); // and immediately kill it, or it will go online. fuck.
345 tox_self_get_public_key(tox
, res
.pubkey
.ptr
);
346 tox_self_get_address(tox
, res
.addr
.ptr
);
347 res
.nospam
= tox_self_get_nospam(tox
);
350 auto nsz
= tox_self_get_name_size(tox
);
352 if (nsz
> tox_max_name_length()) nsz
= tox_max_name_length(); // just in case
353 auto xbuf
= new char[](tox_max_name_length());
355 tox_self_get_name(tox
, xbuf
.ptr
);
356 res
.nick
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
359 auto msz
= tox_self_get_status_message_size(tox
);
361 if (msz
> tox_max_status_message_length()) msz
= tox_max_status_message_length(); // just in case
362 auto xbuf
= new char[](tox_max_status_message_length());
364 tox_self_get_status_message(tox
, xbuf
.ptr
);
365 res
.statusmsg
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
368 // TODO: online status, friend list
369 assert(0, "not finished");
371 } catch (Exception e
) {
378 /// returns public key for account with the given data file, or [toxCoreEmptyKey].
379 PubKey
toxCoreGetAccountPubKey (const(char)[] toxdatafname
) nothrow {
380 version(ToxCoreUseBuiltInDataFileParser
) {
382 auto data
= toxCoreLoadDataFile(VFile(toxdatafname
));
383 if (isValidKey(data
.pubkey
)) return data
.pubkey
[];
384 } catch (Exception e
) {}
387 synchronized(TPInfo
.classinfo
) {
388 foreach (TPInfo ti
; allProtos
) if (ti
.toxDataDiskName
== toxdatafname
) { tacc
= ti
; break; }
391 // nope, not found, try to open account
392 ProtoOptions protoOpts
;
393 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
394 if (tox
is null) return toxCoreEmptyKey
;
395 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
396 // and immediately kill it, or it will go online. fuck.
403 PubKey self
; tox_self_get_public_key(tacc
.tox
, self
.ptr
);
408 return toxCoreEmptyKey
;
412 /// returns address public key for account with the given data file, or [toxCoreEmptyKey].
413 ToxAddr
toxCoreGetAccountAddress (const(char)[] toxdatafname
) nothrow {
414 version(ToxCoreUseBuiltInDataFileParser
) {
416 auto data
= toxCoreLoadDataFile(VFile(toxdatafname
));
417 if (isValidKey(data
.pubkey
)) return data
.addr
[];
418 } catch (Exception e
) {}
421 synchronized(TPInfo
.classinfo
) {
422 foreach (TPInfo ti
; allProtos
) if (ti
.toxDataDiskName
== toxdatafname
) { tacc
= ti
; break; }
425 // nope, not found, try to open account
426 ProtoOptions protoOpts
;
427 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
428 if (tox
is null) return toxCoreEmptyAddr
;
429 ToxAddr addr
; tox_self_get_address(tox
, addr
.ptr
);
430 // and immediately kill it, or it will go online. fuck.
438 if (tacc
.tox
is null) {
441 tox_self_get_address(ti
.tox
, res
.ptr
);
447 return toxCoreEmptyAddr
;
451 /// returns address that can be given to other people, so they can friend you.
452 /// returns zero-filled array on invalid request.
453 ToxAddr
toxCoreGetSelfAddress (in ref PubKey self
) nothrow {
455 doWithLockedTPByKey(self
, delegate (ti
) {
456 if (ti
.tox
is null) {
459 tox_self_get_address(ti
.tox
, res
.ptr
);
466 /// checks if we have a working thread for `self`.
467 bool toxCoreIsAccountOpen (in ref PubKey self
) nothrow {
468 synchronized(TPInfo
.classinfo
) {
469 foreach (TPInfo ti
; allProtos
) {
470 if (ti
.self
[] == self
[]) return true;
477 /// returns nick for the given account, or `null`.
478 string
toxCoreGetNick (in ref PubKey self
) nothrow {
479 synchronized(TPInfo
.classinfo
) {
480 foreach (TPInfo ti
; allProtos
) {
481 if (ti
.self
[] == self
[]) return ti
.nick
;
488 /// creates new Tox account (or opens old), stores it in data file, returns public key for new account.
489 /// returns [toxCoreEmptyKey] on error.
490 /// TODO: pass protocol options here
491 /// WARNING! don't call this from more than one thread with the same `toxdatafname`
492 PubKey
toxCoreCreateAccount (const(char)[] toxdatafname
, const(char)[] nick
) nothrow {
493 if (toxdatafname
.length
== 0) return toxCoreEmptyKey
;
495 if (nick
.length
== 0 || nick
.length
> tox_max_name_length()) return toxCoreEmptyKey
;
498 import std
.path
: absolutePath
;
500 string toxDataDiskName
= toxdatafname
.idup
.absolutePath
;
502 synchronized(TPInfo
.classinfo
) {
503 foreach (TPInfo ti
; allProtos
) {
504 if (ti
.toxDataDiskName
== toxDataDiskName
) return ti
.self
[];
508 ProtoOptions protoOpts
;
509 protoOpts
.udp
= true;
511 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:true);
512 if (tox
is null) return toxCoreEmptyKey
;
514 auto ti
= new TPInfo();
515 ti
.toxDataDiskName
= toxDataDiskName
;
516 tox_self_get_public_key(tox
, ti
.self
.ptr
);
517 tox_self_get_address(tox
, ti
.addr
.ptr
);
522 tox_self_set_name(tox
, nick
.ptr
, nick
.length
, &error
);
523 saveToxCoreData(tox
, toxDataDiskName
);
525 // and immediately kill it, or it will go online. fuck.
528 synchronized(TPInfo
.classinfo
) {
533 } catch (Exception e
) {
536 return toxCoreEmptyKey
;
540 /// starts working thread for account with the given data file.
541 /// returns account public key, or [toxCoreEmptyKey] on error.
542 /// TODO: pass protocol options here
543 /// WARNING! don't call this from more than one thread with the same `toxdatafname`
544 PubKey
toxCoreOpenAccount (const(char)[] toxdatafname
) nothrow {
545 if (toxdatafname
.length
== 0) return toxCoreEmptyKey
;
548 import std
.path
: absolutePath
, dirName
;
550 string toxDataDiskName
= toxdatafname
.idup
.absolutePath
;
552 synchronized(TPInfo
.classinfo
) {
553 foreach (TPInfo ti
; allProtos
) {
554 if (ti
.toxDataDiskName
== toxDataDiskName
) return ti
.self
[];
558 version(ToxCoreUseBuiltInDataFileParser
) {
560 auto data
= toxCoreLoadDataFile(VFile(toxDataDiskName
));
561 if (!isValidKey(data
.pubkey
)) return toxCoreEmptyKey
;
563 auto ti
= new TPInfo();
564 ti
.toxDataDiskName
= toxDataDiskName
;
565 ti
.self
[] = data
.pubkey
[];
566 ti
.addr
[] = data
.addr
[];
569 // use toxcore to parse data file
570 ProtoOptions protoOpts
;
572 protoOpts
.txtunser(VFile(toxDataDiskName
.dirName
~"/proto.rc"));
573 } catch (Exception e
) {
574 protoOpts
= protoOpts
.default;
575 protoOpts
.udp
= true;
578 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
579 if (tox
is null) return toxCoreEmptyKey
;
580 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
582 auto ti
= new TPInfo();
583 ti
.toxDataDiskName
= toxDataDiskName
;
584 tox_self_get_public_key(tox
, ti
.self
.ptr
);
585 tox_self_get_address(tox
, ti
.addr
.ptr
);
588 auto nsz
= tox_self_get_name_size(tox
);
590 if (nsz
> tox_max_name_length()) nsz
= tox_max_name_length(); // just in case
591 // k8: toxcore developers are idiots, so we have to do dynalloc here
592 auto xbuf
= new char[](tox_max_name_length());
594 tox_self_get_name(tox
, xbuf
.ptr
);
595 ti
.nick
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
598 // and immediately kill it, or it will go online. fuck.
602 synchronized(TPInfo
.classinfo
) {
608 } catch (Exception e
) {
611 return toxCoreEmptyKey
;
615 /// stops working thread for account with the given data file.
616 /// returns success flag.
617 bool toxCoreCloseAccount (in ref PubKey self
) nothrow {
620 synchronized(TPInfo
.classinfo
) {
621 foreach (TPInfo ti
; allProtos
) if (ti
.self
[] == self
[]) { tpi
= ti
; break; }
623 if (tpi
is null) return false;
625 // send "quit" signal
626 tpi
.tid
.send(TrdCmdQuit
.init
);
630 (TrdCmdQuitAck ack
) { done
= true; },
636 // save toxcode data, if there is any
637 clearToxCallbacks(tpi
.tox
);
638 if (tpi
.tox
!is null && tpi
.toxDataDiskName
.length
) saveToxCoreData(tpi
.tox
, tpi
.toxDataDiskName
);
639 if (tpi
.tox
!is null) { tox_kill(tpi
.tox
); tpi
.tox
= null; }
641 // remove from the list
642 synchronized(TPInfo
.classinfo
) {
643 foreach (immutable idx
, TPInfo ti
; allProtos
) {
644 if (ti
.self
[] == self
[]) {
645 foreach (immutable c
; idx
+1..allProtos
.length
) allProtos
[c
-1] = allProtos
[c
];
646 delete allProtos
[$-1];
647 allProtos
.length
-= 1;
648 allProtos
.assumeSafeAppend
;
655 } catch (Exception e
) {
661 enum MsgIdError
= -1;
662 enum MsgIdOffline
= 0;
664 /// sends message. returns message id that can be used to track acks.
665 /// returns [MsgIdError] for unknown account, or unknown `dest`.
666 /// returns [MsgIdOffline] if `self` or `dest` is offline (message is not queued to send in this case).
667 /// note that `0` is a not a valid message id.
668 /// cannot send empty messages, and messages longer than `TOX_MAX_MESSAGE_LENGTH` chars.
669 long toxCoreSendMessage (in ref PubKey self
, in ref PubKey dest
, const(char)[] msg
, bool action
=false) nothrow {
670 long res
= MsgIdError
;
671 if (msg
.length
== 0) return res
; // cannot send empty messages
672 TOX_MESSAGE_TYPE tt
= (action ? TOX_MESSAGE_TYPE_ACTION
: TOX_MESSAGE_TYPE_NORMAL
);
674 if (msg.startsWith("/me ")) {
675 msg = msg[4..$].xstripleft;
676 if (msg.length == 0) return res; // cannot send empty messages
677 tt = TOX_MESSAGE_TYPE_ACTION;
680 if (msg
.length
> tox_max_message_length()) return res
; // message too long
681 doWithLockedTPByKey(self
, delegate (ti
) {
682 if (ti
.tox
is null) { res
= MsgIdOffline
; return; }
683 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) { res = MsgIdOffline; return; }
685 TOX_ERR_FRIEND_BY_PUBLIC_KEY errfpk
= 0;
686 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
, &errfpk
);
687 if (errfpk
) return; // error
689 //if (tox_friend_get_connection_status(ti.tox, frnum) == TOX_CONNECTION_NONE) { res = MsgIdOffline; return; }
690 // nope, online; queue the message
691 TOX_ERR_FRIEND_SEND_MESSAGE err
= 0;
692 uint msgid
= tox_friend_send_message(ti
.tox
, frnum
, tt
, msg
.ptr
, msg
.length
, &err
);
694 case TOX_ERR_FRIEND_SEND_MESSAGE_OK
: res
= (cast(long)msgid
)+1; break;
695 case TOX_ERR_FRIEND_SEND_MESSAGE_NULL
: res
= MsgIdError
; break;
696 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_FOUND
: res
= MsgIdError
; break;
697 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_CONNECTED
: res
= MsgIdOffline
; break;
698 case TOX_ERR_FRIEND_SEND_MESSAGE_SENDQ
: res
= MsgIdError
; break;
699 case TOX_ERR_FRIEND_SEND_MESSAGE_TOO_LONG
: res
= MsgIdError
; break;
700 case TOX_ERR_FRIEND_SEND_MESSAGE_EMPTY
: res
= MsgIdError
; break;
701 default: res
= MsgIdError
; break;
703 if (res
> 0) ti
.ping();
710 /// returns `false` if `self` is invalid, or some error occured.
711 /// won't send "going offline" events (caller should know this already).
712 bool toxCoreSetStatus (in ref PubKey self
, ContactStatus status
) nothrow {
713 if (status
== ContactStatus
.Connecting
) return false; // oops
715 bool waitKillAck
= false;
716 bool waitCreateAck
= false;
718 doWithLockedTPByKey(self
, delegate (ti
) {
719 // if we're going offline, kill toxcore instance
720 if (status
== ContactStatus
.Offline
) {
722 ti
.tid
.send(cast(shared)TrdCmdKillToxCore(thisTid
));
725 } catch (Exception e
) {
734 if (ti
.tox
is null) {
735 // need to create toxcore object
737 import std
.path
: absolutePath
, dirName
;
739 ProtoOptions protoOpts
;
741 protoOpts
.txtunser(VFile(ti
.toxDataDiskName
.dirName
~"/proto.rc"));
742 } catch (Exception e
) {
743 protoOpts
= protoOpts
.default;
744 protoOpts
.udp
= true;
747 conwriteln("creating ToxCore...");
748 tox
= toxCreateInstance(ti
.toxDataDiskName
, protoOpts
, allowNew
:false);
750 conwriteln("can't create ToxCore...");
753 ti
.tid
.send(cast(shared)TrdCmdSetToxCore(tox
, thisTid
));
754 waitCreateAck
= true;
755 } catch (Exception e
) {
761 assert(tox
!is null);
764 final switch (status
) {
765 case ContactStatus
.Offline
: assert(0, "wtf?!");
766 case ContactStatus
.Online
: st
= TOX_USER_STATUS_NONE
; break;
767 case ContactStatus
.Away
: st
= TOX_USER_STATUS_AWAY
; break;
768 case ContactStatus
.Busy
: st
= TOX_USER_STATUS_BUSY
; break;
769 case ContactStatus
.Connecting
: assert(0, "wtf?!");
771 tox_self_set_status(tox
, st
);
779 receive((TrdCmdKillToxCoreAck cmd
) {});
780 conwriteln("got killer ack");
781 } catch (Exception e
) {
788 receive((TrdCmdSetToxCoreAck cmd
) {});
789 conwriteln("got actioneer ack");
790 } catch (Exception e
) {
799 /// sets new status message.
800 /// returns `false` if `self` is invalid, message is too long, or on any other error.
801 bool toxCoreSetStatusMessage (in ref PubKey self
, const(char)[] message
) nothrow {
802 if (message
.length
> tox_max_status_message_length()) return false;
804 doWithLockedTPByKey(self
, delegate (ti
) {
805 if (ti
.tox
is null) return;
806 TOX_ERR_SET_INFO err
= 0;
807 res
= tox_self_set_status_message(ti
.tox
, message
.ptr
, message
.length
, &err
);
808 if (res
) ti
.needSave
= true;
815 /// sends friend request.
816 /// returns `false` if `self` is invalid, message is too long, or on any other error.
817 bool toxCoreSendFriendRequest (in ref PubKey self
, in ref ToxAddr dest
, const(char)[] message
) nothrow {
818 if (message
.length
> tox_max_friend_request_length()) return false; // cannot send long requests
820 doWithLockedTPByKey(self
, delegate (ti
) {
821 if (ti
.tox
is null) return; // this account is offline
822 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
824 TOX_ERR_FRIEND_BY_PUBLIC_KEY errfpk
= 0;
825 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
, &errfpk
);
827 // we already befriend it, do nothing
831 TOX_ERR_FRIEND_ADD errfa
= 0;
832 frnum
= tox_friend_add(ti
.tox
, dest
.ptr
, message
.ptr
, message
.length
, &errfa
);
834 if (res
) ti
.needSave
= true;
841 /// unconditionally adds a friend.
842 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
843 bool toxCoreAddFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
845 doWithLockedTPByKey(self
, delegate (ti
) {
846 if (ti
.tox
is null) return; // this account is offline
847 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
849 TOX_ERR_FRIEND_BY_PUBLIC_KEY errfpk
= 0;
850 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
, &errfpk
);
852 // we already has such friend, do nothing
856 TOX_ERR_FRIEND_ADD errfa
= 0;
857 frnum
= tox_friend_add_norequest(ti
.tox
, dest
.ptr
, &errfa
);
859 if (res
) ti
.needSave
= true;
866 /// unconditionally removes a friend.
867 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
868 bool toxCoreRemoveFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
870 doWithLockedTPByKey(self
, delegate (ti
) {
871 if (ti
.tox
is null) return; // this account is offline
872 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
874 TOX_ERR_FRIEND_BY_PUBLIC_KEY errfpk
= 0;
875 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
, &errfpk
);
876 if (errfpk
) { res
= false; return; } // no such friend
877 TOX_ERR_FRIEND_DELETE errfd
= 0;
878 res
= tox_friend_delete(ti
.tox
, frnum
, &errfd
);
879 if (res
) ti
.needSave
= true;
886 /// checks if the given accound has a friend with the given pubkey.
887 /// returns `false` if `self` is invalid or offline, or on any other error, or if there is no such friend.
888 bool toxCoreHasFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
890 doWithLockedTPByKey(self
, delegate (ti
) {
891 if (ti
.tox
is null) return; // this account is offline
893 TOX_ERR_FRIEND_BY_PUBLIC_KEY errfpk
= 0;
894 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
, &errfpk
);
901 /// calls delegate for each known friend.
902 /// return `true` from delegate to stop.
903 /// WARNING! all background operations are locked, so don't spend too much time in delegate!
904 void toxCoreForEachFriend (in ref PubKey self
, scope bool delegate (in ref PubKey self
, in ref PubKey frpub
, scope const(char)[] nick
) dg
) nothrow {
905 if (dg
is null) return;
906 doWithLockedTPByKey(self
, delegate (ti
) {
907 if (ti
.tox
is null) return; // this account is offline
908 auto frcount
= tox_self_get_friend_list_size(ti
.tox
);
909 if (frcount
== 0) return;
910 auto list
= new uint[](frcount
);
911 scope(exit
) delete list
;
913 scope(exit
) delete nick
;
914 tox_self_get_friend_list(ti
.tox
, list
.ptr
);
916 foreach (immutable fidx
, immutable fid
; list
[]) {
917 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
= 0;
918 if (!tox_friend_get_public_key(ti
.tox
, fid
, fpk
.ptr
, &errgpk
)) continue;
919 TOX_ERR_FRIEND_QUERY errgns
= 0;
920 auto nsz
= tox_friend_get_name_size(ti
.tox
, fid
, &errgns
);
922 if (nsz
> nick
.length
) nick
.length
= nsz
;
924 TOX_ERR_FRIEND_QUERY errfq
= 0;
925 tox_friend_get_name(ti
.tox
, fid
, nick
.ptr
, &errfq
);
926 if (errfq
) nick
[0] = 0;
929 dg(self
, fpk
, nick
[0..nsz
]);
930 } catch (Exception e
) {
938 /// returns the time when this friend was seen online.
939 /// returns `SysTime.min` if `self` is invalid, `dest` is invalid, or on any other error.
940 SysTime
toxCoreLastSeen (in ref PubKey self
, in ref PubKey dest
) nothrow {
941 SysTime res
= SysTime
.min
;
942 doWithLockedTPByKey(self
, delegate (ti
) {
943 if (ti
.tox
is null) return; // this account is offline
945 TOX_ERR_FRIEND_BY_PUBLIC_KEY errfpk
= 0;
946 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
, &errfpk
);
947 if (errfpk
) return; // unknown friend
948 TOX_ERR_FRIEND_GET_LAST_ONLINE err
;
949 auto ut
= tox_friend_get_last_online(ti
.tox
, frnum
, &err
);
951 if (err
== 0) res
= SysTime
.fromUnixTime(ut
);
952 } catch (Exception e
) {
960 /// calls delegate with ToxP. ugly name is intentional.
961 void toxCoreCallWithToxP (in ref PubKey self
, scope void delegate (ToxP tox
) dg
) nothrow {
962 if (dg
is null) return;
963 doWithLockedTPByKey(self
, delegate (ti
) {
964 if (ti
.tox
is null) return; // this account is offline
967 } catch (Exception e
) {}
972 // ////////////////////////////////////////////////////////////////////////// //
973 // private ToxCore protocol implementation details
977 static struct FileRejectEvent
{
983 string toxDataDiskName
;
987 ToxAddr addr
; // will be used if `tox` is `null`
989 FileRejectEvent
[] rej
;
990 bool doBootstrap
; // do bootstrapping
993 void ping () nothrow {
994 if (tox
is null) return;
995 try { tid
.send(TrdCmdPing
.init
); } catch (Exception e
) {}
999 __gshared TPInfo
[] allProtos
;
1002 // ////////////////////////////////////////////////////////////////////////// //
1003 struct TrdCmdQuit
{} // quit thread loop
1004 struct TrdCmdQuitAck
{} // quit thread loop
1005 struct TrdCmdPing
{} // do something
1006 struct TrdCmdSetToxCore
{ ToxP tox
; Tid replytid
; }
1007 struct TrdCmdSetToxCoreAck
{}
1008 struct TrdCmdKillToxCore
{ Tid replytid
; }
1009 struct TrdCmdKillToxCoreAck
{}
1012 // ////////////////////////////////////////////////////////////////////////// //
1013 // this should be called with registered `ti`
1014 void startThread (TPInfo ti
) {
1015 assert(ti
!is null);
1016 ti
.tid
= spawn(&toxCoreThread
, thisTid
, *cast(immutable(void)**)&ti
);
1020 // ////////////////////////////////////////////////////////////////////////// //
1021 void clearToxCallbacks (ToxP tox
) {
1022 if (tox
is null) return;
1024 tox_callback_self_connection_status(tox
, null);
1026 tox_callback_friend_name(tox
, null);
1027 tox_callback_friend_status_message(tox
, null);
1028 tox_callback_friend_status(tox
, null);
1029 tox_callback_friend_connection_status(tox
, null);
1030 tox_callback_friend_typing(tox
, null);
1031 tox_callback_friend_read_receipt(tox
, null);
1032 tox_callback_friend_request(tox
, null);
1033 tox_callback_friend_message(tox
, null);
1035 tox_callback_file_recv_control(tox
, null);
1036 tox_callback_file_chunk_request(tox
, null);
1037 tox_callback_file_recv(tox
, null);
1041 // ////////////////////////////////////////////////////////////////////////// //
1042 static void toxCoreThread (Tid ownerTid
, immutable(void)* tiptr
) {
1044 MonoTime lastSaveTime
= MonoTime
.zero
;
1045 TPInfo ti
= *cast(TPInfo
*)&tiptr
; // HACK!
1050 bool doQuit
= false;
1051 bool doKillTox
= false;
1053 if (ti
.tox
!is null && ti
.doBootstrap
) {
1054 conwriteln("TOX(", ti
.nick
, "): bootstrapping");
1055 ti
.doBootstrap
= false;
1057 mswait
= tox_iteration_interval(ti
.tox
);
1058 if (mswait
< 1) mswait
= 1;
1059 conwriteln("TOX(", ti
.nick
, "): bootstrapping complete (mswait=", mswait
, ")");
1062 receiveTimeout((mswait ? mswait
.msecs
: 10.hours
),
1063 (TrdCmdQuit cmd
) { doQuit
= true; },
1064 (TrdCmdPing cmd
) {},
1065 (shared TrdCmdSetToxCore cmd
) { newTox
= cast(ToxP
)cmd
.tox
; ackreptid
= cast(Tid
)cmd
.replytid
; },
1066 (shared TrdCmdKillToxCore cmd
) { doKillTox
= true; ackreptid
= cast(Tid
)cmd
.replytid
; },
1067 (Variant v
) { conwriteln("WUTAFUCK?! "); },
1071 if (newTox
!is null) {
1072 conwriteln("TOX(", ti
.nick
, "): got new toxcore pointer");
1073 if (ti
.tox
!is newTox
) {
1074 clearToxCallbacks(ti
.tox
);
1075 if (ti
.tox
!is null) tox_kill(ti
.tox
);
1079 ti
.doBootstrap
= true;
1081 ackreptid
.send(TrdCmdSetToxCoreAck
.init
);
1082 ackreptid
= Tid
.init
;
1086 if (ti
.tox
is null) { mswait
= 0; continue; }
1089 conwriteln("TOX(", ti
.nick
, "): killing toxcore pointer");
1091 if (ti
.tox
!is null) {
1092 clearToxCallbacks(ti
.tox
);
1093 if (ti
.toxDataDiskName
.length
) saveToxCoreData(ti
.tox
, ti
.toxDataDiskName
);
1097 conwriteln("TOX(", ti
.nick
, "): wtf?!");
1100 ackreptid
.send(TrdCmdKillToxCoreAck
.init
);
1101 ackreptid
= Tid
.init
;
1105 tox_iterate(ti
.tox
, null);
1106 mswait
= tox_iteration_interval(ti
.tox
);
1107 //conwriteln("TOX(", ti.nick, "): interval is ", mswait);
1108 if (mswait
< 1) mswait
= 1;
1111 auto ctt
= MonoTime
.currTime
;
1112 if ((ctt
-lastSaveTime
).total
!"minutes" > 1) {
1113 ti
.needSave
= false;
1115 saveToxCoreData(ti
.tox
, ti
.toxDataDiskName
);
1120 ownerTid
.send(TrdCmdQuitAck
.init
);
1121 } catch (Throwable e
) {
1122 // here, we are dead and fucked (the exact order doesn't matter)
1123 import core
.stdc
.stdlib
: abort
;
1124 import core
.stdc
.stdio
: fprintf
, stderr
;
1125 import core
.memory
: GC
;
1126 import core
.thread
: thread_suspendAll
;
1127 GC
.disable(); // yeah
1128 thread_suspendAll(); // stop right here, you criminal scum!
1129 auto s
= e
.toString();
1130 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
1131 abort(); // die, you bitch!
1136 // ////////////////////////////////////////////////////////////////////////// //
1137 // find TPInfo object for the given tox handle; used in callbacks
1138 TPInfo
findTP (ToxP tox
) nothrow {
1139 if (tox
is null) return null;
1140 synchronized(TPInfo
.classinfo
) {
1141 foreach (TPInfo ti
; allProtos
) {
1142 if (ti
.tox
is tox
) return ti
;
1149 // returns `true` if found and executed without errors
1150 bool doWithLockedTPByKey (in ref PubKey self
, scope void delegate (TPInfo ti
) dg
) nothrow {
1152 if (dg
is null) return false;
1153 synchronized(TPInfo
.classinfo
) {
1154 foreach (TPInfo ti
; allProtos
) if (ti
.self
[] == self
[]) { tpi
= ti
; break; }
1161 } catch (Exception e
) {
1163 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e
.toString
);
1164 } catch (Exception e
) {}
1173 // returns `true` if found and executed without errors
1174 bool doWithLockedTP (ToxP tox
, scope void delegate (TPInfo ti
) dg
) nothrow {
1176 if (tox
is null || dg
is null) return false;
1177 synchronized(TPInfo
.classinfo
) {
1178 foreach (TPInfo ti
; allProtos
) if (ti
.tox
is tox
) { tpi
= ti
; break; }
1185 } catch (Exception e
) {
1187 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e
.toString
);
1188 } catch (Exception e
) {}
1197 // ////////////////////////////////////////////////////////////////////////// //
1198 // toxcore callbacks
1200 // self connection state was changed
1201 static extern(C
) void connectionCB (Tox
* tox
, TOX_CONNECTION status
, void* udata
) nothrow {
1202 if (toxCoreSendEvent
is null) return;
1204 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1205 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1206 conwriteln("TOX(", ti
.nick
, "): ", (status
!= TOX_CONNECTION_NONE ?
"" : "dis"), "connected.");
1208 tcmsg
= new ToxEventConnection(self
, self
, status
!= TOX_CONNECTION_NONE
);
1210 toxCoreSendEvent(tcmsg
);
1214 // friend connection state was changed
1215 static extern(C
) void friendConnectionCB (Tox
* tox
, uint frnum
, TOX_CONNECTION status
, void* udata
) nothrow {
1216 if (toxCoreSendEvent
is null) return;
1218 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1219 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
;
1220 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1221 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
, &errgpk
)) return; // wtf?!
1222 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " ", (status
!= TOX_CONNECTION_NONE ?
"" : "dis"), "connected.");
1224 tcmsg
= new ToxEventConnection(self
, who
, status
!= TOX_CONNECTION_NONE
);
1225 if (status
!= TOX_CONNECTION_NONE
) ti
.needSave
= true;
1227 toxCoreSendEvent(tcmsg
);
1231 static extern(C
) void friendStatusCB (Tox
* tox
, uint frnum
, TOX_USER_STATUS status
, void* udata
) nothrow {
1232 if (toxCoreSendEvent
is null) return;
1234 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1235 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
;
1236 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1237 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
, &errgpk
)) return; // wtf?!
1238 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed status to ", status
);
1240 ContactStatus cst
= ContactStatus
.Offline
;
1242 case TOX_USER_STATUS_NONE
: cst
= ContactStatus
.Online
; break;
1243 case TOX_USER_STATUS_AWAY
: cst
= ContactStatus
.Away
; break;
1244 case TOX_USER_STATUS_BUSY
: cst
= ContactStatus
.Busy
; break;
1247 tcmsg
= new ToxEventStatus(self
, who
, cst
);
1249 toxCoreSendEvent(tcmsg
);
1253 static extern(C
) void friendNameCB (Tox
* tox
, uint frnum
, const(char)* name
, usize length
, void* udata
) nothrow {
1254 if (toxCoreSendEvent
is null) return;
1256 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1257 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
;
1258 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1259 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
, &errgpk
)) return; // wtf?!
1260 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed name to <", name
[0..length
], ">");
1262 tcmsg
= new ToxEventNick(self
, who
, name
[0..length
].buildNormalizedString
!true);
1265 toxCoreSendEvent(tcmsg
);
1269 static extern(C
) void friendStatusMessageCB (Tox
* tox
, uint frnum
, const(char)* msg
, usize msglen
, void* user_data
) nothrow {
1270 if (toxCoreSendEvent
is null) return;
1272 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1273 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
;
1274 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1275 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
, &errgpk
)) return; // wtf?!
1276 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed status to <", msg
[0..msglen
], ">");
1278 tcmsg
= new ToxEventStatusMsg(self
, who
, msg
[0..msglen
].buildNormalizedString
);
1281 toxCoreSendEvent(tcmsg
);
1285 static extern(C
) void friendReqCB (Tox
* tox
, const(ubyte)* pk
, const(char)* msg
, usize msglen
, void* udata
) nothrow {
1286 if (toxCoreSendEvent
is null) return;
1288 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1289 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1290 PubKey who
; who
[] = pk
[0..PubKey
.length
];
1291 conwriteln("TOX(", ti
.nick
, "): friend request comes: <", msg
[0..msglen
], ">");
1293 tcmsg
= new ToxEventFriendReq(self
, who
, msg
[0..msglen
].idup
);
1296 toxCoreSendEvent(tcmsg
);
1300 static extern(C
) void friendMsgCB (Tox
* tox
, uint frnum
, TOX_MESSAGE_TYPE type
, const(char)* msg
, usize msglen
, void* udata
) nothrow {
1301 if (toxCoreSendEvent
is null) return;
1303 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1304 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
;
1305 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1306 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
, &errgpk
)) return; // wtf?!
1307 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " sent a message.");
1309 tcmsg
= new ToxEventMessage(self
, who
, (type
== TOX_MESSAGE_TYPE_ACTION
), msg
[0..msglen
].idup
);
1311 toxCoreSendEvent(tcmsg
);
1315 static extern(C
) void friendReceiptCB (Tox
* tox
, uint frnum
, uint msgid
, void* udata
) nothrow {
1316 if (toxCoreSendEvent
is null) return;
1318 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1319 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
;
1320 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1321 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
, &errgpk
)) return; // wtf?!
1322 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " acked message #", msgid
);
1324 tcmsg
= new ToxEventMessageAck(self
, who
, (cast(long)msgid
)+1);
1326 toxCoreSendEvent(tcmsg
);
1330 static extern(C
) void fileRecvCB (Tox
* tox
, uint frnum
, uint flnum
, TOX_FILE_KIND kind
, ulong flsize
, const(char)* name
, usize namelen
, void* udata
) nothrow {
1331 if (toxCoreSendEvent
is null) return;
1332 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1333 //TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk;
1334 //PubKey self; tox_self_get_public_key(tox, self.ptr);
1335 //PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr, &errgpk)) return false; // wtf?!
1338 foreach (ref TPInfo
.FileRejectEvent fre
; ti
.rej
) if (fre
.frnum
== frnum
&& fre
.flnum
== flnum
) { found
= true; break; }
1339 if (!found
) ti
.rej
~= TPInfo
.FileRejectEvent(frnum
, flnum
);
1343 if (kind == TOX_FILE_KIND_AVATAR) {
1344 if (flsize < 16 || flsize > 1024*1024) {
1345 timp.addRejectEvent(frnum, flnum);
1349 timp.addRejectEvent(frnum, flnum);
1351 tox_file_control(tox, rej.frnum, rej.flnum, TOX_FILE_CONTROL_CANCEL, null);
1356 static extern(C
) void fileRecvCtlCB (Tox
* tox
, uint frnum
, uint flnum
, TOX_FILE_CONTROL ctl
, void* udata
) nothrow {
1358 auto timp = findTP(tox);
1359 if (timp is null) return;
1360 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk;
1362 if (!tox_friend_get_public_key(tox, frnum, who.ptr, &errgpk)) return false; // wtf?!
1364 conprintfln("got recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1366 synchronized(timp) {
1367 timp.friendMessageAck(who, msgid);
1370 } catch (Exception e) {
1371 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1374 auto timp = *cast(Account*)&udata;
1375 if (timp is null || timp.tox is null) return;
1377 final switch (ctl) {
1378 case TOX_FILE_CONTROL_RESUME: /*timp.resumeSendByNums(frnum, flnum);*/ break;
1379 case TOX_FILE_CONTROL_PAUSE: /*timp.pauseSendByNums(frnum, flnum);*/ break;
1380 case TOX_FILE_CONTROL_CANCEL: /*timp.abortSendByNums(frnum, flnum);*/ break;
1382 } catch (Exception e) {
1384 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1386 } catch (Exception) {}
1392 static extern(C
) void fileChunkReqCB (Tox
* tox
, uint frnum
, uint flnum
, ulong pos
, usize len
, void* udata
) nothrow {
1393 if (toxCoreSendEvent
is null) return;
1394 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1395 TOX_ERR_FRIEND_GET_PUBLIC_KEY errgpk
;
1396 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1397 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
, &errgpk
)) return; // wtf?!
1400 foreach (ref TPInfo
.FileRejectEvent fre
; ti
.rej
) if (fre
.frnum
== frnum
&& fre
.flnum
== flnum
) { found
= true; break; }
1401 if (!found
) ti
.rej
~= TPInfo
.FileRejectEvent(frnum
, flnum
);
1404 //logwritefln("got chunk req recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1407 timp.fschunks ~= ChunkToSend(frnum, flnum, pos, len);
1408 } catch (Exception e) {
1410 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1411 timp.dropNetIOConnection();
1412 } catch (Exception) {}
1418 // ////////////////////////////////////////////////////////////////////////// //
1419 void bootstrap(string mode
="any") (TPInfo ti
) nothrow if (mode
== "udp" || mode
== "tcp" || mode
== "any") {
1420 if (ti
is null || ti
.tox
is null) return;
1422 ToxBootstrapServer
[] loadBootNodes () nothrow {
1424 auto bfname
= buildPath(ti
.toxDataDiskName
.dirName
, "tox_bootstrap.rc");
1425 ToxBootstrapServer
[] bootnodes
= null;
1427 bootnodes
.txtunser(VFile(bfname
));
1428 if (bootnodes
.length
> 0) return bootnodes
;
1429 } catch (Exception e
) {}
1432 bootnodes
= tox_download_bootstrap_list();
1433 if (bootnodes
.length
> 0) serialize(bootnodes
, bfname
);
1435 } catch (Exception e
) {}
1439 conprintfln("Tox: loading bootstrap nodes...");
1440 auto nodes
= loadBootNodes();
1441 conprintfln("Tox: %s nodes loaded", nodes
.length
);
1442 if (nodes
.length
== 0) return;
1443 foreach (const ref ToxBootstrapServer srv
; nodes
) {
1444 if (srv
.ipv4
.length
< 2) continue;
1445 assert(srv
.ipv4
[$-1] == 0);
1446 //conprintfln(" node ip: %s:%u (maintainer: %s)", srv.ipv4[0..$-1], srv.port, srv.maintainer);
1447 static if (mode
== "udp") {
1449 tox_bootstrap(ti
.tox
, srv
.ipv4
.ptr
, srv
.port
, srv
.pubkey
.ptr
, null);
1451 } else static if (mode
== "tcp") {
1453 foreach (immutable ushort port
; srv
.tcpports
) {
1454 tox_add_tcp_relay(ti
.tox
, srv
.ipv4
.ptr
, port
, srv
.pubkey
.ptr
, null);
1459 tox_bootstrap(ti
.tox
, srv
.ipv4
.ptr
, srv
.port
, srv
.pubkey
.ptr
, null);
1460 } else if (srv
.udp
) {
1461 foreach (immutable ushort port
; srv
.tcpports
) {
1462 tox_add_tcp_relay(ti
.tox
, srv
.ipv4
.ptr
, port
, srv
.pubkey
.ptr
, null);
1466 tox_iterate(ti
.tox
, null);
1468 //conprintfln("Tox[%s]: %s nodes added", srvalias, nodes.length);
1472 // ////////////////////////////////////////////////////////////////////////// //
1473 // returns `null` if there is no such file or file cannot be loaded
1474 ubyte[] loadToxCoreData (const(char)[] toxdatafname
) nothrow {
1475 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, rename
, fread
, ferror
, fseek
, ftell
, SEEK_SET
, SEEK_END
;
1476 import core
.stdc
.stdlib
: malloc
, free
;
1477 import core
.sys
.posix
.unistd
: unlink
;
1479 if (toxdatafname
.length
== 0) return null;
1481 static char* namebuf
= null;
1482 static uint nbsize
= 0;
1484 if (nbsize
< toxdatafname
.length
+1024) {
1485 import core
.stdc
.stdlib
: realloc
;
1486 nbsize
= cast(uint)toxdatafname
.length
+1024;
1487 namebuf
= cast(char*)realloc(namebuf
, nbsize
);
1488 if (namebuf
is null) assert(0, "out of memory");
1491 auto origName
= expandTilde(namebuf
[0..nbsize
-6], toxdatafname
);
1492 if (origName
== null) return null; // oops
1493 origName
.ptr
[origName
.length
] = 0; // zero-terminate
1495 FILE
* fi
= fopen(namebuf
, "r");
1496 if (fi
is null) return null;
1497 scope(exit
) fclose(fi
);
1500 if (fseek(fi
, 0, SEEK_END
) == -1) {
1501 import core
.stdc
.errno
;
1502 if (errno
== EINTR
) continue;
1512 import core
.stdc
.errno
;
1513 if (errno
== EINTR
) continue;
1519 if (fsize
> 1024*1024*256) { conwriteln("toxcore data file too big"); return null; }
1520 if (fsize
== 0) return null; // it cannot be zero-sized
1523 if (fseek(fi
, 0, SEEK_SET
) == -1) {
1524 import core
.stdc
.errno
;
1525 if (errno
== EINTR
) continue;
1531 auto res
= new ubyte[](cast(int)fsize
);
1532 conwriteln("loading toxcore data; size=", fsize
);
1535 while (left
.length
> 0) {
1536 auto rd
= fread(left
.ptr
, 1, left
.length
, fi
);
1537 if (rd
== 0) { delete res
; return null; } // error
1538 if (rd
< cast(int)left
.length
) {
1539 if (!ferror(fi
)) { delete res
; return null; } // error
1540 import core
.stdc
.errno
;
1541 if (errno
!= EINTR
) { delete res
; return null; } // error
1542 if (rd
> 0) left
= left
[rd
..$];
1545 if (rd
> left
.length
) { delete res
; return null; } // error
1549 if (left
.length
) { delete res
; return null; } // error
1555 bool saveToxCoreData (ToxP tox
, const(char)[] toxdatafname
) nothrow {
1556 import core
.stdc
.stdio
: rename
;
1557 import core
.stdc
.stdlib
: malloc
, free
;
1558 import core
.sys
.posix
.fcntl
: open
, O_WRONLY
, O_CREAT
, O_TRUNC
;
1559 import core
.sys
.posix
.unistd
: unlink
, close
, write
, fdatasync
;
1561 if (tox
is null || toxdatafname
.length
== 0) return false;
1563 auto size
= tox_get_savedata_size(tox
);
1564 if (size
> int.max
/8) return false; //throw new Exception("save data too big");
1566 char* savedata
= cast(char*)malloc(size
);
1567 if (savedata
is null) return false;
1568 scope(exit
) if (savedata
!is null) free(savedata
);
1570 tox_get_savedata(tox
, savedata
);
1571 conwriteln("save toxcore data; size=", size
);
1573 static char* namebuf
= null;
1574 static char* namebuf1
= null;
1575 static uint nbsize
= 0;
1577 if (nbsize
< toxdatafname
.length
+1024) {
1578 import core
.stdc
.stdlib
: realloc
;
1579 nbsize
= cast(uint)toxdatafname
.length
+1024;
1580 namebuf
= cast(char*)realloc(namebuf
, nbsize
);
1581 if (namebuf
is null) assert(0, "out of memory");
1582 namebuf1
= cast(char*)realloc(namebuf1
, nbsize
);
1583 if (namebuf1
is null) assert(0, "out of memory");
1586 auto origName
= expandTilde(namebuf
[0..nbsize
-6], toxdatafname
);
1587 if (origName
== null) return false; // oops
1588 origName
.ptr
[origName
.length
] = 0; // zero-terminate
1590 // create temporary name
1591 namebuf1
[0..origName
.length
] = origName
[];
1592 namebuf1
[origName
.length
..origName
.length
+5] = ".$$$\x00";
1594 int fo
= open(namebuf1
, O_WRONLY|O_CREAT|O_TRUNC
, 0o600
);
1596 conwriteln("failed to create file: '", origName
, ".$$$'");
1600 auto left
= savedata
[0..size
];
1601 while (left
.length
> 0) {
1602 auto wr
= write(fo
, left
.ptr
, left
.length
);
1604 // out of disk space; oops
1610 import core
.stdc
.errno
;
1611 if (errno
== EINTR
) continue;
1612 // some other error; oops
1617 if (wr
> left
.length
) {
1629 if (rename(namebuf1
, namebuf
) != 0) {
1638 version(ToxCoreDebug
) {
1639 static extern(C
) void toxCoreLogCB (Tox
* tox
, TOX_LOG_LEVEL level
, const(char)* file
, uint line
, const(char)* func
, const(char)* message
, void* user_data
) nothrow {
1640 static inout(char)[] sz (inout(char)* s
) nothrow @trusted @nogc {
1641 if (s
is null) return null;
1643 while (s
[idx
]) ++idx
;
1644 return cast(inout(char)[])(s
[0..idx
]);
1647 conwriteln("level=", level
, "; file=", sz(file
), ":", line
, "; func=", sz(func
), "; msg=", sz(message
));
1652 // ////////////////////////////////////////////////////////////////////////// //
1653 // returns `false` if can't open/create
1654 ToxP
toxCreateInstance (const(char)[] toxdatafname
, in ref ProtoOptions protoOpts
, bool allowNew
, bool* wasCreated
=null) nothrow {
1655 if (wasCreated
!is null) *wasCreated
= false;
1657 if (toxdatafname
.length
== 0) return null;
1660 char* toxProxyHost
= null;
1662 import core
.stdc
.stdlib
: free
;
1663 if (toxProxyHost
!is null) free(toxProxyHost
);
1666 scope(exit
) delete savedata
;
1668 TOX_ERR_OPTIONS_NEW errton
= 0;
1669 auto toxOpts
= tox_options_new(&errton
);
1670 if (errton
) toxOpts
= null;
1671 assert(toxOpts
!is null);
1672 scope(exit
) if (toxOpts
!is null) tox_options_free(toxOpts
);
1673 tox_options_default(toxOpts
);
1675 bool createNewToxCoreAccount
= false;
1676 toxOpts
.tox_options_set_ipv6_enabled(protoOpts
.ipv6
);
1677 toxOpts
.tox_options_set_udp_enabled(protoOpts
.udp
);
1678 toxOpts
.tox_options_set_local_discovery_enabled(protoOpts
.localDiscovery
);
1679 toxOpts
.tox_options_set_hole_punching_enabled(protoOpts
.holePunching
);
1680 toxOpts
.tox_options_set_start_port(protoOpts
.startPort
);
1681 toxOpts
.tox_options_set_end_port(protoOpts
.endPort
);
1682 toxOpts
.tox_options_set_tcp_port(protoOpts
.tcpPort
);
1684 toxOpts
.tox_options_set_proxy_type(protoOpts
.proxyType
);
1685 if (protoOpts
.proxyType
!= TOX_PROXY_TYPE_NONE
) {
1686 import core
.stdc
.stdlib
: malloc
;
1687 toxOpts
.tox_options_set_proxy_port(protoOpts
.proxyPort
);
1688 // create proxy address string
1689 toxProxyHost
= cast(char*)malloc(protoOpts
.proxyAddr
.length
+1);
1690 if (toxProxyHost
is null) assert(0, "out of memory");
1691 toxProxyHost
[0..protoOpts
.proxyAddr
.length
] = protoOpts
.proxyAddr
[];
1692 toxProxyHost
[protoOpts
.proxyAddr
.length
] = 0;
1693 toxOpts
.tox_options_set_proxy_host(toxProxyHost
);
1696 savedata
= loadToxCoreData(toxdatafname
);
1697 if (savedata
is null) {
1698 // create new tox instance
1699 if (wasCreated
!is null) *wasCreated
= true;
1700 if (!allowNew
) return null;
1701 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_NONE
);
1702 conwriteln("creating new ToxCore account...");
1705 conwriteln("setting ToxCore account data (", savedata
.length
, " bytes)");
1706 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_TOX_SAVE
);
1707 toxOpts
.tox_options_set_savedata_length(savedata
.length
);
1708 toxOpts
.tox_options_set_savedata_data(savedata
.ptr
, savedata
.length
);
1710 scope(exit
) delete savedata
;
1712 version(ToxCoreDebug
) {
1713 toxOpts
.tox_options_set_log_callback(&toxCoreLogCB
);
1716 // create tox instance
1718 ToxP tox
= tox_new(toxOpts
, &error
);
1720 conwriteln("cannot create ToxCore instance: error is ", error
);
1724 tox_callback_self_connection_status(tox
, &connectionCB
);
1726 tox_callback_friend_name(tox
, &friendNameCB
);
1727 tox_callback_friend_status_message(tox
, &friendStatusMessageCB
);
1728 tox_callback_friend_status(tox
, &friendStatusCB
);
1729 tox_callback_friend_connection_status(tox
, &friendConnectionCB
);
1730 //tox_callback_friend_typing(tox, &friendTypingCB);
1731 tox_callback_friend_read_receipt(tox
, &friendReceiptCB
);
1732 tox_callback_friend_request(tox
, &friendReqCB
);
1733 tox_callback_friend_message(tox
, &friendMsgCB
);
1735 tox_callback_file_recv_control(tox
, &fileRecvCtlCB
);
1736 tox_callback_file_chunk_request(tox
, &fileChunkReqCB
);
1737 tox_callback_file_recv(tox
, &fileRecvCB
);
1738 //tox_callback_file_recv_chunk
1740 //tox_callback_conference_invite
1741 //tox_callback_conference_message
1742 //tox_callback_conference_title
1743 //tox_callback_conference_namelist_change
1745 //tox_callback_friend_lossy_packet
1746 //tox_callback_friend_lossless_packet
1752 // ////////////////////////////////////////////////////////////////////////// //
1754 enum ToxCoreDataId
= 0x15ed1b1fU
;
1756 enum ToxCoreChunkTypeHi
= 0x01ceU
;
1757 enum ToxCoreChunkNoSpamKeys
= 1;
1758 enum ToxCoreChunkDHT
= 2;
1759 enum ToxCoreChunkFriends
= 3;
1760 enum ToxCoreChunkName
= 4;
1761 enum ToxCoreChunkStatusMsg
= 5;
1762 enum ToxCoreChunkStatus
= 6;
1763 enum ToxCoreChunkTcpRelay
= 10;
1764 enum ToxCoreChunkPathNode
= 11;
1765 enum ToxCoreChunkEnd
= 255;
1768 struct ToxSavedFriend
{
1769 enum TOTAL_DATA_SIZE
= 2216;
1770 enum CRYPTO_PUBLIC_KEY_SIZE
= 32;
1771 enum CRYPTO_SECRET_KEY_SIZE
= 32;
1772 enum SAVED_FRIEND_REQUEST_SIZE
= 1024;
1773 enum MAX_NAME_LENGTH
= 128;
1774 enum MAX_STATUSMESSAGE_LENGTH
= 1007;
1776 enum Status
: ubyte {
1784 enum UserStatus
: ubyte {
1792 ubyte[CRYPTO_PUBLIC_KEY_SIZE
] real_pk
;
1793 char[SAVED_FRIEND_REQUEST_SIZE
] info
; // the data that is sent during the friend requests we do
1795 ushort info_size
; // length of the info
1796 char[MAX_NAME_LENGTH
] name
;
1798 char[MAX_STATUSMESSAGE_LENGTH
] statusmessage
;
1800 ushort statusmessage_length
;
1801 UserStatus userstatus
;
1803 uint friendrequest_nospam
;
1804 ulong last_seen_time
;
1806 void load (VFile fl
) {
1807 // we can use CTFE here, but meh...
1808 status
= cast(Status
)fl
.readNum
!ubyte;
1809 if (status
> Status
.max
) throw new ProtocolException("invalid friend status");
1810 fl
.rawReadExact(real_pk
[]);
1811 fl
.rawReadExact(info
[]);
1812 /*pad0 =*/ fl
.readNum
!ubyte;
1813 info_size
= fl
.readNum
!(ushort, "BE");
1814 if (info_size
> info
.length
) throw new ProtocolException("invalid friend data");
1815 fl
.rawReadExact(name
[]);
1816 name_length
= fl
.readNum
!(ushort, "BE");
1817 if (name_length
> name
.length
) throw new ProtocolException("invalid friend data");
1818 fl
.rawReadExact(statusmessage
[]);
1819 /*pad1 =*/ fl
.readNum
!ubyte;
1820 statusmessage_length
= fl
.readNum
!(ushort, "BE");
1821 if (statusmessage_length
> statusmessage
.length
) throw new ProtocolException("invalid friend data");
1822 userstatus
= cast(UserStatus
)fl
.readNum
!ubyte;
1823 if (userstatus
> UserStatus
.max
) throw new ProtocolException("invalid friend userstatus");
1824 /*pad30 =*/ fl
.readNum
!ubyte;
1825 /*pad31 =*/ fl
.readNum
!ubyte;
1826 /*pad32 =*/ fl
.readNum
!ubyte;
1827 friendrequest_nospam
= fl
.readNum
!uint;
1828 last_seen_time
= fl
.readNum
!(ulong, "BE");