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, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 // Tox protocol thread
18 // Each Tox connection spawns a thread that does all the communication.
19 // Incoming messages are posted to [glconCtlWindow].
20 // Asking for outgoing actions are done with public interface.
21 // No tox internals are exposed to the outer world.
22 module toxproto
is aliced
;
27 import std
.concurrency
;
42 //version = ToxCoreDebug;
43 version = ToxCoreUseBuiltInDataFileParser
;
45 static assert(TOX_PUBLIC_KEY_SIZE
== ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
);
48 import accdb
: ContactStatus
, ContactInfo
;
51 // ////////////////////////////////////////////////////////////////////////// //
55 // ////////////////////////////////////////////////////////////////////////// //
56 string
buildNormalizedString(bool noteSpacesOnly
=false) (const(char)[] s
) nothrow {
58 if (s
.length
== 0) return null;
60 if (s
.length
== 0) { static if (noteSpacesOnly
) return "<spaces>"; else return null; }
62 res
.reserve(s
.length
);
63 foreach (char ch
; s
) {
65 if (res
.length
== 0 || res
[$-1] > ' ') res
~= ' ';
70 static if (noteSpacesOnly
) { if (res
.length
== 0) return "<spaces>"; }
71 return cast(string
)res
; // it is safe to cast here
75 // ////////////////////////////////////////////////////////////////////////// //
77 struct ToxCoreDataFile
{
78 string nick
; // user nick
80 ubyte[TOX_PUBLIC_KEY_SIZE
] pubkey
= toxCoreEmptyKey
;
81 ToxAddr addr
= toxCoreEmptyAddr
;
83 ContactStatus status
= ContactStatus
.Offline
;
85 ContactInfo
[] friends
;
87 @property bool valid () const pure nothrow @safe @nogc => isValidKey(pubkey
);
89 static ToxAddr
buildAddress (in ref ubyte[TOX_PUBLIC_KEY_SIZE
] pubkey
, uint nospam
) nothrow @trusted @nogc {
90 static ushort calcChecksum (const(ubyte)[] data
) nothrow @trusted @nogc {
91 ubyte[2] checksum
= 0;
92 foreach (immutable idx
, ubyte b
; data
) checksum
[idx
%2] ^
= b
;
93 return *cast(ushort*)checksum
.ptr
;
97 res
[0..TOX_PUBLIC_KEY_SIZE
] = pubkey
[];
98 *cast(uint*)(res
.ptr
+TOX_PUBLIC_KEY_SIZE
) = nospam
;
99 *cast(ushort*)(res
.ptr
+TOX_PUBLIC_KEY_SIZE
+uint.sizeof
) = calcChecksum(res
[0..$-2]);
105 // ////////////////////////////////////////////////////////////////////////// //
107 class ProtocolException
: Exception
{
108 this (string msg
, string file
=__FILE__
, usize line
=__LINE__
, Throwable next
=null) pure nothrow @safe @nogc {
109 super(msg
, file
, line
, next
);
114 // ////////////////////////////////////////////////////////////////////////// //
115 // messages thread sends to [glconCtlWindow].
116 // [who] can be the same as [self] to indicate account state changes
119 PubKey self
; // account
120 PubKey who
; // can be same as [self]
121 nothrow @trusted @nogc:
122 this (in ref PubKey aself
, in ref PubKey awho
) { self
[] = aself
[]; who
= awho
[]; }
125 // connection state changed
126 class ToxEventConnection
: ToxEventBase
{
128 nothrow @trusted @nogc:
129 this (in ref PubKey aself
, in ref PubKey awho
, bool aconnected
) { super(aself
, awho
); connected
= aconnected
; }
132 // online status changed
133 class ToxEventStatus
: ToxEventBase
{
134 ContactStatus status
;
135 nothrow @trusted @nogc:
136 this (in ref PubKey aself
, in ref PubKey awho
, ContactStatus astatus
) { super(aself
, awho
); status
= astatus
; }
140 class ToxEventNick
: ToxEventBase
{
141 string nick
; // new nick
142 nothrow @trusted @nogc:
143 this (in ref PubKey aself
, in ref PubKey awho
, string anick
) { super(aself
, awho
); nick
= anick
; }
146 // status message changed
147 class ToxEventStatusMsg
: ToxEventBase
{
148 string message
; // new message
149 nothrow @trusted @nogc:
150 this (in ref PubKey aself
, in ref PubKey awho
, string amessage
) { super(aself
, awho
); message
= amessage
; }
153 // typing status changed
154 class ToxEventTyping
: ToxEventBase
{
156 nothrow @trusted @nogc:
157 this (in ref PubKey aself
, in ref PubKey awho
, bool atyping
) { super(aself
, awho
); typing
= atyping
; }
161 class ToxEventMessage
: ToxEventBase
{
162 bool action
; // is this an "action" message? (/me)
166 this (in ref PubKey aself
, in ref PubKey awho
, bool aaction
, string amessage
) {
173 this (in ref PubKey aself
, in ref PubKey awho
, bool aaction
, string amessage
, SysTime atime
) {
181 // send message ack comes
182 class ToxEventMessageAck
: ToxEventBase
{
184 nothrow @trusted @nogc:
185 this (in ref PubKey aself
, in ref PubKey awho
, long amsgid
) { super(aself
, awho
); msgid
= amsgid
; }
188 // friend request comes
189 class ToxEventFriendReq
: ToxEventBase
{
190 string message
; // request message
191 nothrow @trusted @nogc:
192 this (in ref PubKey aself
, in ref PubKey awho
, string amessage
) { super(aself
, awho
); message
= amessage
; }
196 // ////////////////////////////////////////////////////////////////////////// //
198 /// shutdown protocol module
199 void toxCoreShutdownAll () {
201 synchronized(TPInfo
.classinfo
) {
202 foreach (TPInfo ti
; allProtos
) {
203 ti
.tid
.send(TrdCmdQuit
.init
);
207 while (acksLeft
> 0) {
209 (TrdCmdQuitAck ack
) { --acksLeft
; },
214 foreach (TPInfo tpi
; allProtos
) {
215 // save toxcode data, if there is any
216 if (tpi
.tox
!is null && tpi
.toxDataDiskName
.length
) saveToxCoreData(tpi
.tox
, tpi
.toxDataDiskName
);
217 if (tpi
.tox
!is null) { tox_kill(tpi
.tox
); tpi
.tox
= null; }
222 // ////////////////////////////////////////////////////////////////////////// //
225 static immutable PubKey toxCoreEmptyKey
= 0;
226 static immutable ToxAddr toxCoreEmptyAddr
= 0;
228 /// delegate that will be used to send messages.
229 /// can be called from any thread, so it should be thread-safe, and should avoid deadlocks.
230 /// the delegate should be set on program startup, and should not be changed anymore.
231 /// FIXME: add API to change this!
232 __gshared
void delegate (Object msg
) nothrow toxCoreSendEvent
;
235 bool isValidKey (in ref PubKey key
) pure nothrow @safe @nogc => (key
[] != toxCoreEmptyKey
[]);
238 bool isValidAddr (in ref ToxAddr key
) pure nothrow @safe @nogc => (key
[] != toxCoreEmptyAddr
[]);
241 /// parse toxcore data file.
242 /// sorry, i had to do it manually, 'cause there is no way to open toxcore data without going online.
243 public ToxCoreDataFile
toxCoreLoadDataFile (VFile fl
) nothrow {
246 version(ToxCoreUseBuiltInDataFileParser
) {
247 auto sz
= fl
.size
-fl
.tell
;
248 if (sz
< 8) throw new ProtocolException("data file too small");
249 if (fl
.readNum
!uint != 0) throw new ProtocolException("invalid something");
250 if (fl
.readNum
!uint != ToxCoreDataId
) throw new ProtocolException("not a ToxCore data file");
252 auto len
= fl
.readNum
!uint;
253 auto id
= fl
.readNum
!uint;
254 if (id
>>16 != ToxCoreChunkTypeHi
) throw new ProtocolException("invalid chunk hitype");
257 case ToxCoreChunkNoSpamKeys
:
258 if (len
== ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
+ToxSavedFriend
.CRYPTO_SECRET_KEY_SIZE
+uint.sizeof
) {
259 res
.nospam
= fl
.readNum
!uint;
260 fl
.rawReadExact(res
.pubkey
[]);
261 ubyte[ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
] privkey
;
262 fl
.rawReadExact(privkey
[]);
265 res
.addr
[] = res
.buildAddress(res
.pubkey
, res
.nospam
);
268 case ToxCoreChunkDHT
: break;
269 case ToxCoreChunkFriends
:
270 if (len
%ToxSavedFriend
.TOTAL_DATA_SIZE
!= 0) throw new ProtocolException("invalid contact list");
276 if (st
> len || st
!= ToxSavedFriend
.TOTAL_DATA_SIZE
) throw new ProtocolException("invalid contact list");
278 if (fr
.status
== ToxSavedFriend
.Status
.NoFriend
) continue;
280 ci
.nick
= fr
.name
[0..fr
.name_length
].buildNormalizedString
!true;
281 ci
.lastonlinetime
= cast(uint)fr
.last_seen_time
;
282 final switch (fr
.status
) {
283 case ToxSavedFriend
.Status
.NoFriend
: assert(0, "wtf?!");
285 case ToxSavedFriend
.Status
.Added
: ci
.kind
= ContactInfo
.Kind
.PengingAuthAccept
; break;
286 case ToxSavedFriend
.Status
.Requested
: ci
.kind
= ContactInfo
.Kind
.PengingAuthRequest
; break;
287 case ToxSavedFriend
.Status
.Confirmed
:
288 case ToxSavedFriend
.Status
.Online
:
289 ci
.kind
= ContactInfo
.Kind
.Friend
;
292 if (ci
.kind
!= ContactInfo
.Kind
.Friend
) {
293 ci
.statusmsg
= fr
.info
[0..fr
.info_size
].buildNormalizedString
!true;
295 ci
.statusmsg
= fr
.statusmessage
[0..fr
.statusmessage_length
].buildNormalizedString
;
297 ci
.nospam
= fr
.friendrequest_nospam
;
298 ci
.pubkey
[] = fr
.real_pk
[];
302 case ToxCoreChunkName
:
304 auto name
= new char[](len
);
305 fl
.rawReadExact(name
);
307 res
.nick
= cast(string
)name
; // it is safe to cast here
310 case ToxCoreChunkStatusMsg
:
312 auto msg
= new char[](len
);
313 fl
.rawReadExact(msg
);
315 res
.statusmsg
= cast(string
)msg
; // it is safe to cast here
318 case ToxCoreChunkStatus
:
320 auto st
= fl
.readNum
!ubyte;
321 if (st
< ToxSavedFriend
.UserStatus
.Invalid
) {
323 case ToxSavedFriend
.UserStatus
.None
: res
.status
= ContactStatus
.Online
; break;
324 case ToxSavedFriend
.UserStatus
.Away
: res
.status
= ContactStatus
.Away
; break;
325 case ToxSavedFriend
.UserStatus
.Busy
: res
.status
= ContactStatus
.Busy
; break;
326 default: res
.status
= ContactStatus
.Offline
; break;
332 case ToxCoreChunkTcpRelay
: break;
333 case ToxCoreChunkPathNode
: break;
336 if (id
== ToxCoreChunkEnd
) break;
337 fl
.seek(len
, Seek
.Cur
);
340 // nope, not found, try to open account
341 ProtoOptions protoOpts
;
342 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
343 if (tox
is null) throw new ProtocolException("cannot load toxcore data file");
344 scope(exit
) tox_kill(tox
); // and immediately kill it, or it will go online. fuck.
346 tox_self_get_public_key(tox
, res
.pubkey
.ptr
);
347 tox_self_get_address(tox
, res
.addr
.ptr
);
348 res
.nospam
= tox_self_get_nospam(tox
);
351 auto nsz
= tox_self_get_name_size(tox
);
353 if (nsz
> tox_max_name_length()) nsz
= tox_max_name_length(); // just in case
354 auto xbuf
= new char[](tox_max_name_length());
356 tox_self_get_name(tox
, xbuf
.ptr
);
357 res
.nick
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
360 auto msz
= tox_self_get_status_message_size(tox
);
362 if (msz
> tox_max_status_message_length()) msz
= tox_max_status_message_length(); // just in case
363 auto xbuf
= new char[](tox_max_status_message_length());
365 tox_self_get_status_message(tox
, xbuf
.ptr
);
366 res
.statusmsg
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
369 // TODO: online status, friend list
370 assert(0, "not finished");
372 } catch (Exception e
) {
379 /// returns public key for account with the given data file, or [toxCoreEmptyKey].
380 PubKey
toxCoreGetAccountPubKey (const(char)[] toxdatafname
) nothrow {
381 version(ToxCoreUseBuiltInDataFileParser
) {
383 auto data
= toxCoreLoadDataFile(VFile(toxdatafname
));
384 if (isValidKey(data
.pubkey
)) return data
.pubkey
[];
385 } catch (Exception e
) {}
388 synchronized(TPInfo
.classinfo
) {
389 foreach (TPInfo ti
; allProtos
) if (ti
.toxDataDiskName
== toxdatafname
) { tacc
= ti
; break; }
392 // nope, not found, try to open account
393 ProtoOptions protoOpts
;
394 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
395 if (tox
is null) return toxCoreEmptyKey
;
396 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
397 // and immediately kill it, or it will go online. fuck.
404 PubKey self
; tox_self_get_public_key(tacc
.tox
, self
.ptr
);
409 return toxCoreEmptyKey
;
413 /// returns address public key for account with the given data file, or [toxCoreEmptyKey].
414 ToxAddr
toxCoreGetAccountAddress (const(char)[] toxdatafname
) nothrow {
415 version(ToxCoreUseBuiltInDataFileParser
) {
417 auto data
= toxCoreLoadDataFile(VFile(toxdatafname
));
418 if (isValidKey(data
.pubkey
)) return data
.addr
[];
419 } catch (Exception e
) {}
422 synchronized(TPInfo
.classinfo
) {
423 foreach (TPInfo ti
; allProtos
) if (ti
.toxDataDiskName
== toxdatafname
) { tacc
= ti
; break; }
426 // nope, not found, try to open account
427 ProtoOptions protoOpts
;
428 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
429 if (tox
is null) return toxCoreEmptyAddr
;
430 ToxAddr addr
; tox_self_get_address(tox
, addr
.ptr
);
431 // and immediately kill it, or it will go online. fuck.
439 if (tacc
.tox
is null) {
442 tox_self_get_address(ti
.tox
, res
.ptr
);
448 return toxCoreEmptyAddr
;
452 /// returns address that can be given to other people, so they can friend you.
453 /// returns zero-filled array on invalid request.
454 ToxAddr
toxCoreGetSelfAddress (in ref PubKey self
) nothrow {
456 doWithLockedTPByKey(self
, delegate (ti
) {
457 if (ti
.tox
is null) {
460 tox_self_get_address(ti
.tox
, res
.ptr
);
467 /// checks if we have a working thread for `self`.
468 bool toxCoreIsAccountOpen (in ref PubKey self
) nothrow {
469 synchronized(TPInfo
.classinfo
) {
470 foreach (TPInfo ti
; allProtos
) {
471 if (ti
.self
[] == self
[]) return true;
478 /// returns nick for the given account, or `null`.
479 string
toxCoreGetNick (in ref PubKey self
) nothrow {
480 synchronized(TPInfo
.classinfo
) {
481 foreach (TPInfo ti
; allProtos
) {
482 if (ti
.self
[] == self
[]) return ti
.nick
;
489 /// creates new Tox account (or opens old), stores it in data file, returns public key for new account.
490 /// returns [toxCoreEmptyKey] on error.
491 /// TODO: pass protocol options here
492 /// WARNING! don't call this from more than one thread with the same `toxdatafname`
493 PubKey
toxCoreCreateAccount (const(char)[] toxdatafname
, const(char)[] nick
) nothrow {
494 if (toxdatafname
.length
== 0) return toxCoreEmptyKey
;
496 if (nick
.length
== 0 || nick
.length
> tox_max_name_length()) return toxCoreEmptyKey
;
499 import std
.path
: absolutePath
;
501 string toxDataDiskName
= toxdatafname
.idup
.absolutePath
;
503 synchronized(TPInfo
.classinfo
) {
504 foreach (TPInfo ti
; allProtos
) {
505 if (ti
.toxDataDiskName
== toxDataDiskName
) return ti
.self
[];
509 ProtoOptions protoOpts
;
510 protoOpts
.udp
= true;
512 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:true);
513 if (tox
is null) return toxCoreEmptyKey
;
515 auto ti
= new TPInfo();
516 ti
.toxDataDiskName
= toxDataDiskName
;
517 tox_self_get_public_key(tox
, ti
.self
.ptr
);
518 tox_self_get_address(tox
, ti
.addr
.ptr
);
522 tox_self_set_name(tox
, nick
.ptr
, nick
.length
);
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 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
686 if (frnum
== uint.max
) return; // error
688 //if (tox_friend_get_connection_status(ti.tox, frnum) == TOX_CONNECTION_NONE) { res = MsgIdOffline; return; }
689 // nope, online; queue the message
690 TOX_ERR_FRIEND_SEND_MESSAGE err
= 0;
691 uint msgid
= tox_friend_send_message(ti
.tox
, frnum
, tt
, msg
.ptr
, msg
.length
, &err
);
693 case TOX_ERR_FRIEND_SEND_MESSAGE_OK
: res
= (cast(long)msgid
)+1; break;
694 case TOX_ERR_FRIEND_SEND_MESSAGE_NULL
: res
= MsgIdError
; break;
695 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_FOUND
: res
= MsgIdError
; break;
696 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_CONNECTED
: res
= MsgIdOffline
; break;
697 case TOX_ERR_FRIEND_SEND_MESSAGE_SENDQ
: res
= MsgIdError
; break;
698 case TOX_ERR_FRIEND_SEND_MESSAGE_TOO_LONG
: res
= MsgIdError
; break;
699 case TOX_ERR_FRIEND_SEND_MESSAGE_EMPTY
: res
= MsgIdError
; break;
700 default: res
= MsgIdError
; break;
702 if (res
> 0) ti
.ping();
709 /// returns `false` if `self` is invalid, or some error occured.
710 /// won't send "going offline" events (caller should know this already).
711 bool toxCoreSetStatus (in ref PubKey self
, ContactStatus status
) nothrow {
712 if (status
== ContactStatus
.Connecting
) return false; // oops
714 bool waitKillAck
= false;
715 bool waitCreateAck
= false;
717 doWithLockedTPByKey(self
, delegate (ti
) {
718 // if we're going offline, kill toxcore instance
719 if (status
== ContactStatus
.Offline
) {
721 ti
.tid
.send(cast(shared)TrdCmdKillToxCore(thisTid
));
724 } catch (Exception e
) {
733 if (ti
.tox
is null) {
734 // need to create toxcore object
736 import std
.path
: absolutePath
, dirName
;
738 ProtoOptions protoOpts
;
740 protoOpts
.txtunser(VFile(ti
.toxDataDiskName
.dirName
~"/proto.rc"));
741 } catch (Exception e
) {
742 protoOpts
= protoOpts
.default;
743 protoOpts
.udp
= true;
746 conwriteln("creating ToxCore...");
747 tox
= toxCreateInstance(ti
.toxDataDiskName
, protoOpts
, allowNew
:false);
749 conwriteln("can't create ToxCore...");
752 ti
.tid
.send(cast(shared)TrdCmdSetToxCore(tox
, thisTid
));
753 waitCreateAck
= true;
754 } catch (Exception e
) {
760 assert(tox
!is null);
763 final switch (status
) {
764 case ContactStatus
.Offline
: assert(0, "wtf?!");
765 case ContactStatus
.Online
: st
= TOX_USER_STATUS_NONE
; break;
766 case ContactStatus
.Away
: st
= TOX_USER_STATUS_AWAY
; break;
767 case ContactStatus
.Busy
: st
= TOX_USER_STATUS_BUSY
; break;
768 case ContactStatus
.Connecting
: assert(0, "wtf?!");
770 tox_self_set_status(tox
, st
);
778 receive((TrdCmdKillToxCoreAck cmd
) {});
779 conwriteln("got killer ack");
780 } catch (Exception e
) {
787 receive((TrdCmdSetToxCoreAck cmd
) {});
788 conwriteln("got actioneer ack");
789 } catch (Exception e
) {
798 /// sets new status message.
799 /// returns `false` if `self` is invalid, message is too long, or on any other error.
800 bool toxCoreSetStatusMessage (in ref PubKey self
, const(char)[] message
) nothrow {
801 if (message
.length
> tox_max_status_message_length()) return false;
803 doWithLockedTPByKey(self
, delegate (ti
) {
804 if (ti
.tox
is null) return;
805 res
= tox_self_set_status_message(ti
.tox
, message
.ptr
, message
.length
);
806 if (res
) ti
.needSave
= true;
813 /// sends friend request.
814 /// returns `false` if `self` is invalid, message is too long, or on any other error.
815 bool toxCoreSendFriendRequest (in ref PubKey self
, in ref ToxAddr dest
, const(char)[] message
) nothrow {
816 if (message
.length
> tox_max_friend_request_length()) return false; // cannot send long requests
818 doWithLockedTPByKey(self
, delegate (ti
) {
819 if (ti
.tox
is null) return; // this account is offline
820 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
822 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
823 if (frnum
!= uint.max
) {
824 // we already befriend it, do nothing
828 frnum
= tox_friend_add(ti
.tox
, dest
.ptr
, message
.ptr
, message
.length
);
829 res
= (frnum
!= uint.max
);
830 if (res
) ti
.needSave
= true;
837 /// unconditionally adds a friend.
838 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
839 bool toxCoreAddFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
841 doWithLockedTPByKey(self
, delegate (ti
) {
842 if (ti
.tox
is null) return; // this account is offline
843 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
845 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
846 if (frnum
!= uint.max
) {
847 // we already has such friend, do nothing
851 frnum
= tox_friend_add_norequest(ti
.tox
, dest
.ptr
);
852 res
= (frnum
!= uint.max
);
853 if (res
) ti
.needSave
= true;
860 /// unconditionally removes a friend.
861 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
862 bool toxCoreRemoveFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
864 doWithLockedTPByKey(self
, delegate (ti
) {
865 if (ti
.tox
is null) return; // this account is offline
866 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
868 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
869 if (frnum
== uint.max
) { res
= false; return; } // no such friend
870 res
= tox_friend_delete(ti
.tox
, frnum
);
871 if (res
) ti
.needSave
= true;
878 /// checks if the given accound has a friend with the given pubkey.
879 /// returns `false` if `self` is invalid or offline, or on any other error, or if there is no such friend.
880 bool toxCoreHasFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
882 doWithLockedTPByKey(self
, delegate (ti
) {
883 if (ti
.tox
is null) return; // this account is offline
885 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
886 res
= (frnum
!= uint.max
);
892 /// calls delegate for each known friend.
893 /// return `true` from delegate to stop.
894 /// WARNING! all background operations are locked, so don't spend too much time in delegate!
895 void toxCoreForEachFriend (in ref PubKey self
, scope bool delegate (in ref PubKey self
, in ref PubKey frpub
, scope const(char)[] nick
) dg
) nothrow {
896 if (dg
is null) return;
897 doWithLockedTPByKey(self
, delegate (ti
) {
898 if (ti
.tox
is null) return; // this account is offline
899 auto frcount
= tox_self_get_friend_list_size(ti
.tox
);
900 if (frcount
== 0) return;
901 auto list
= new uint[](frcount
);
902 scope(exit
) delete list
;
904 scope(exit
) delete nick
;
905 tox_self_get_friend_list(ti
.tox
, list
.ptr
);
907 foreach (immutable fidx
, immutable fid
; list
[]) {
908 if (!tox_friend_get_public_key(ti
.tox
, fid
, fpk
.ptr
)) continue;
909 auto nsz
= tox_friend_get_name_size(ti
.tox
, fid
);
910 if (nsz
> nick
.length
) nick
.length
= nsz
;
911 if (nsz
!= 0) tox_friend_get_name(ti
.tox
, fid
, nick
.ptr
);
913 dg(self
, fpk
, nick
[0..nsz
]);
914 } catch (Exception e
) {
922 /// returns the time when this friend was seen online.
923 /// returns `SysTime.min` if `self` is invalid, `dest` is invalid, or on any other error.
924 SysTime
toxCoreLastSeen (in ref PubKey self
, in ref PubKey dest
) nothrow {
925 SysTime res
= SysTime
.min
;
926 doWithLockedTPByKey(self
, delegate (ti
) {
927 if (ti
.tox
is null) return; // this account is offline
929 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
930 if (frnum
== uint.max
) return; // unknown friend
931 TOX_ERR_FRIEND_GET_LAST_ONLINE err
;
932 auto ut
= tox_friend_get_last_online(ti
.tox
, frnum
, &err
);
934 if (err
== 0) res
= SysTime
.fromUnixTime(ut
);
935 } catch (Exception e
) {
943 /// calls delegate with ToxP. ugly name is intentional.
944 void toxCoreCallWithToxP (in ref PubKey self
, scope void delegate (ToxP tox
) dg
) nothrow {
945 if (dg
is null) return;
946 doWithLockedTPByKey(self
, delegate (ti
) {
947 if (ti
.tox
is null) return; // this account is offline
950 } catch (Exception e
) {}
955 // ////////////////////////////////////////////////////////////////////////// //
956 // private ToxCore protocol implementation details
960 static struct FileRejectEvent
{
966 string toxDataDiskName
;
970 ToxAddr addr
; // will be used if `tox` is `null`
972 FileRejectEvent
[] rej
;
973 bool doBootstrap
; // do bootstrapping
976 void ping () nothrow {
977 if (tox
is null) return;
978 try { tid
.send(TrdCmdPing
.init
); } catch (Exception e
) {}
982 __gshared TPInfo
[] allProtos
;
985 // ////////////////////////////////////////////////////////////////////////// //
986 struct TrdCmdQuit
{} // quit thread loop
987 struct TrdCmdQuitAck
{} // quit thread loop
988 struct TrdCmdPing
{} // do something
989 struct TrdCmdSetToxCore
{ ToxP tox
; Tid replytid
; }
990 struct TrdCmdSetToxCoreAck
{}
991 struct TrdCmdKillToxCore
{ Tid replytid
; }
992 struct TrdCmdKillToxCoreAck
{}
995 // ////////////////////////////////////////////////////////////////////////// //
996 // this should be called with registered `ti`
997 void startThread (TPInfo ti
) {
999 ti
.tid
= spawn(&toxCoreThread
, thisTid
, *cast(immutable(void)**)&ti
);
1003 // ////////////////////////////////////////////////////////////////////////// //
1004 void clearToxCallbacks (ToxP tox
) {
1005 if (tox
is null) return;
1007 tox_callback_self_connection_status(tox
, null);
1009 tox_callback_friend_name(tox
, null);
1010 tox_callback_friend_status_message(tox
, null);
1011 tox_callback_friend_status(tox
, null);
1012 tox_callback_friend_connection_status(tox
, null);
1013 tox_callback_friend_typing(tox
, null);
1014 tox_callback_friend_read_receipt(tox
, null);
1015 tox_callback_friend_request(tox
, null);
1016 tox_callback_friend_message(tox
, null);
1018 tox_callback_file_recv_control(tox
, null);
1019 tox_callback_file_chunk_request(tox
, null);
1020 tox_callback_file_recv(tox
, null);
1024 // ////////////////////////////////////////////////////////////////////////// //
1025 static void toxCoreThread (Tid ownerTid
, immutable(void)* tiptr
) {
1027 MonoTime lastSaveTime
= MonoTime
.zero
;
1028 TPInfo ti
= *cast(TPInfo
*)&tiptr
; // HACK!
1033 bool doQuit
= false;
1034 bool doKillTox
= false;
1036 if (ti
.tox
!is null && ti
.doBootstrap
) {
1037 conwriteln("TOX(", ti
.nick
, "): bootstrapping");
1038 ti
.doBootstrap
= false;
1040 mswait
= tox_iteration_interval(ti
.tox
);
1041 if (mswait
< 1) mswait
= 1;
1042 conwriteln("TOX(", ti
.nick
, "): bootstrapping complete (mswait=", mswait
, ")");
1045 receiveTimeout((mswait ? mswait
.msecs
: 10.hours
),
1046 (TrdCmdQuit cmd
) { doQuit
= true; },
1047 (TrdCmdPing cmd
) {},
1048 (shared TrdCmdSetToxCore cmd
) { newTox
= cast(ToxP
)cmd
.tox
; ackreptid
= cast(Tid
)cmd
.replytid
; },
1049 (shared TrdCmdKillToxCore cmd
) { doKillTox
= true; ackreptid
= cast(Tid
)cmd
.replytid
; },
1050 (Variant v
) { conwriteln("WUTAFUCK?! "); },
1054 if (newTox
!is null) {
1055 conwriteln("TOX(", ti
.nick
, "): got new toxcore pointer");
1056 if (ti
.tox
!is newTox
) {
1057 clearToxCallbacks(ti
.tox
);
1058 if (ti
.tox
!is null) tox_kill(ti
.tox
);
1062 ti
.doBootstrap
= true;
1064 ackreptid
.send(TrdCmdSetToxCoreAck
.init
);
1065 ackreptid
= Tid
.init
;
1069 if (ti
.tox
is null) { mswait
= 0; continue; }
1072 conwriteln("TOX(", ti
.nick
, "): killing toxcore pointer");
1074 if (ti
.tox
!is null) {
1075 clearToxCallbacks(ti
.tox
);
1076 if (ti
.toxDataDiskName
.length
) saveToxCoreData(ti
.tox
, ti
.toxDataDiskName
);
1080 conwriteln("TOX(", ti
.nick
, "): wtf?!");
1083 ackreptid
.send(TrdCmdKillToxCoreAck
.init
);
1084 ackreptid
= Tid
.init
;
1088 tox_iterate(ti
.tox
, null);
1089 mswait
= tox_iteration_interval(ti
.tox
);
1090 //conwriteln("TOX(", ti.nick, "): interval is ", mswait);
1091 if (mswait
< 1) mswait
= 1;
1094 auto ctt
= MonoTime
.currTime
;
1095 if ((ctt
-lastSaveTime
).total
!"minutes" > 1) {
1096 ti
.needSave
= false;
1098 saveToxCoreData(ti
.tox
, ti
.toxDataDiskName
);
1103 ownerTid
.send(TrdCmdQuitAck
.init
);
1104 } catch (Throwable e
) {
1105 // here, we are dead and fucked (the exact order doesn't matter)
1106 import core
.stdc
.stdlib
: abort
;
1107 import core
.stdc
.stdio
: fprintf
, stderr
;
1108 import core
.memory
: GC
;
1109 import core
.thread
: thread_suspendAll
;
1110 GC
.disable(); // yeah
1111 thread_suspendAll(); // stop right here, you criminal scum!
1112 auto s
= e
.toString();
1113 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
1114 abort(); // die, you bitch!
1119 // ////////////////////////////////////////////////////////////////////////// //
1120 // find TPInfo object for the given tox handle; used in callbacks
1121 TPInfo
findTP (ToxP tox
) nothrow {
1122 if (tox
is null) return null;
1123 synchronized(TPInfo
.classinfo
) {
1124 foreach (TPInfo ti
; allProtos
) {
1125 if (ti
.tox
is tox
) return ti
;
1132 // returns `true` if found and executed without errors
1133 bool doWithLockedTPByKey (in ref PubKey self
, scope void delegate (TPInfo ti
) dg
) nothrow {
1135 if (dg
is null) return false;
1136 synchronized(TPInfo
.classinfo
) {
1137 foreach (TPInfo ti
; allProtos
) if (ti
.self
[] == self
[]) { tpi
= ti
; break; }
1144 } catch (Exception e
) {
1146 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e
.toString
);
1147 } catch (Exception e
) {}
1156 // returns `true` if found and executed without errors
1157 bool doWithLockedTP (ToxP tox
, scope void delegate (TPInfo ti
) dg
) nothrow {
1159 if (tox
is null || dg
is null) return false;
1160 synchronized(TPInfo
.classinfo
) {
1161 foreach (TPInfo ti
; allProtos
) if (ti
.tox
is tox
) { tpi
= ti
; break; }
1168 } catch (Exception e
) {
1170 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e
.toString
);
1171 } catch (Exception e
) {}
1180 // ////////////////////////////////////////////////////////////////////////// //
1181 // toxcore callbacks
1183 // self connection state was changed
1184 static extern(C
) void connectionCB (Tox
* tox
, TOX_CONNECTION status
, void* udata
) nothrow {
1185 if (toxCoreSendEvent
is null) return;
1187 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1188 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1189 conwriteln("TOX(", ti
.nick
, "): ", (status
!= TOX_CONNECTION_NONE ?
"" : "dis"), "connected.");
1191 tcmsg
= new ToxEventConnection(self
, self
, status
!= TOX_CONNECTION_NONE
);
1193 toxCoreSendEvent(tcmsg
);
1197 // friend connection state was changed
1198 static extern(C
) void friendConnectionCB (Tox
* tox
, uint frnum
, TOX_CONNECTION status
, void* udata
) nothrow {
1199 if (toxCoreSendEvent
is null) return;
1201 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1202 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1203 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1204 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " ", (status
!= TOX_CONNECTION_NONE ?
"" : "dis"), "connected.");
1206 tcmsg
= new ToxEventConnection(self
, who
, status
!= TOX_CONNECTION_NONE
);
1207 if (status
!= TOX_CONNECTION_NONE
) ti
.needSave
= true;
1209 toxCoreSendEvent(tcmsg
);
1213 static extern(C
) void friendStatusCB (Tox
* tox
, uint frnum
, TOX_USER_STATUS status
, void* udata
) nothrow {
1214 if (toxCoreSendEvent
is null) return;
1216 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1217 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1218 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1219 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed status to ", status
);
1221 ContactStatus cst
= ContactStatus
.Offline
;
1223 case TOX_USER_STATUS_NONE
: cst
= ContactStatus
.Online
; break;
1224 case TOX_USER_STATUS_AWAY
: cst
= ContactStatus
.Away
; break;
1225 case TOX_USER_STATUS_BUSY
: cst
= ContactStatus
.Busy
; break;
1228 tcmsg
= new ToxEventStatus(self
, who
, cst
);
1230 toxCoreSendEvent(tcmsg
);
1234 static extern(C
) void friendNameCB (Tox
* tox
, uint frnum
, const(char)* name
, usize length
, void* udata
) nothrow {
1235 if (toxCoreSendEvent
is null) return;
1237 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1238 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1239 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1240 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed name to <", name
[0..length
], ">");
1242 tcmsg
= new ToxEventNick(self
, who
, name
[0..length
].buildNormalizedString
!true);
1245 toxCoreSendEvent(tcmsg
);
1249 static extern(C
) void friendStatusMessageCB (Tox
* tox
, uint frnum
, const(char)* msg
, usize msglen
, void* user_data
) nothrow {
1250 if (toxCoreSendEvent
is null) return;
1252 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1253 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1254 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1255 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed status to <", msg
[0..msglen
], ">");
1257 tcmsg
= new ToxEventStatusMsg(self
, who
, msg
[0..msglen
].buildNormalizedString
);
1260 toxCoreSendEvent(tcmsg
);
1264 static extern(C
) void friendReqCB (Tox
* tox
, const(ubyte)* pk
, const(char)* msg
, usize msglen
, void* udata
) nothrow {
1265 if (toxCoreSendEvent
is null) return;
1267 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1268 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1269 PubKey who
; who
[] = pk
[0..PubKey
.length
];
1270 conwriteln("TOX(", ti
.nick
, "): friend request comes: <", msg
[0..msglen
], ">");
1272 tcmsg
= new ToxEventFriendReq(self
, who
, msg
[0..msglen
].idup
);
1275 toxCoreSendEvent(tcmsg
);
1279 static extern(C
) void friendMsgCB (Tox
* tox
, uint frnum
, TOX_MESSAGE_TYPE type
, const(char)* msg
, usize msglen
, void* udata
) nothrow {
1280 if (toxCoreSendEvent
is null) return;
1282 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1283 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1284 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1285 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " sent a message.");
1287 tcmsg
= new ToxEventMessage(self
, who
, (type
== TOX_MESSAGE_TYPE_ACTION
), msg
[0..msglen
].idup
);
1289 toxCoreSendEvent(tcmsg
);
1293 static extern(C
) void friendReceiptCB (Tox
* tox
, uint frnum
, uint msgid
, void* udata
) nothrow {
1294 if (toxCoreSendEvent
is null) return;
1296 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1297 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1298 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1299 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " acked message #", msgid
);
1301 tcmsg
= new ToxEventMessageAck(self
, who
, (cast(long)msgid
)+1);
1303 toxCoreSendEvent(tcmsg
);
1307 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 {
1308 if (toxCoreSendEvent
is null) return;
1309 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1310 //PubKey self; tox_self_get_public_key(tox, self.ptr);
1311 //PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return false; // wtf?!
1314 foreach (ref TPInfo
.FileRejectEvent fre
; ti
.rej
) if (fre
.frnum
== frnum
&& fre
.flnum
== flnum
) { found
= true; break; }
1315 if (!found
) ti
.rej
~= TPInfo
.FileRejectEvent(frnum
, flnum
);
1319 if (kind == TOX_FILE_KIND_AVATAR) {
1320 if (flsize < 16 || flsize > 1024*1024) {
1321 timp.addRejectEvent(frnum, flnum);
1325 timp.addRejectEvent(frnum, flnum);
1327 tox_file_control(tox, rej.frnum, rej.flnum, TOX_FILE_CONTROL_CANCEL, null);
1332 static extern(C
) void fileRecvCtlCB (Tox
* tox
, uint frnum
, uint flnum
, TOX_FILE_CONTROL ctl
, void* udata
) nothrow {
1334 auto timp = findTP(tox);
1335 if (timp is null) return;
1337 if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return false; // wtf?!
1339 conprintfln("got recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1341 synchronized(timp) {
1342 timp.friendMessageAck(who, msgid);
1345 } catch (Exception e) {
1346 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1349 auto timp = *cast(Account*)&udata;
1350 if (timp is null || timp.tox is null) return;
1352 final switch (ctl) {
1353 case TOX_FILE_CONTROL_RESUME: /*timp.resumeSendByNums(frnum, flnum);*/ break;
1354 case TOX_FILE_CONTROL_PAUSE: /*timp.pauseSendByNums(frnum, flnum);*/ break;
1355 case TOX_FILE_CONTROL_CANCEL: /*timp.abortSendByNums(frnum, flnum);*/ break;
1357 } catch (Exception e) {
1359 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1361 } catch (Exception) {}
1367 static extern(C
) void fileChunkReqCB (Tox
* tox
, uint frnum
, uint flnum
, ulong pos
, usize len
, void* udata
) nothrow {
1368 if (toxCoreSendEvent
is null) return;
1369 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1370 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1371 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1374 foreach (ref TPInfo
.FileRejectEvent fre
; ti
.rej
) if (fre
.frnum
== frnum
&& fre
.flnum
== flnum
) { found
= true; break; }
1375 if (!found
) ti
.rej
~= TPInfo
.FileRejectEvent(frnum
, flnum
);
1378 //logwritefln("got chunk req recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1381 timp.fschunks ~= ChunkToSend(frnum, flnum, pos, len);
1382 } catch (Exception e) {
1384 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1385 timp.dropNetIOConnection();
1386 } catch (Exception) {}
1392 // ////////////////////////////////////////////////////////////////////////// //
1393 void bootstrap(string mode
="any") (TPInfo ti
) nothrow if (mode
== "udp" || mode
== "tcp" || mode
== "any") {
1394 if (ti
is null || ti
.tox
is null) return;
1396 ToxBootstrapServer
[] loadBootNodes () nothrow {
1398 auto bfname
= buildPath(ti
.toxDataDiskName
.dirName
, "tox_bootstrap.rc");
1399 ToxBootstrapServer
[] bootnodes
= null;
1401 bootnodes
.txtunser(VFile(bfname
));
1402 if (bootnodes
.length
> 0) return bootnodes
;
1403 } catch (Exception e
) {}
1406 bootnodes
= tox_download_bootstrap_list();
1407 if (bootnodes
.length
> 0) serialize(bootnodes
, bfname
);
1409 } catch (Exception e
) {}
1413 conprintfln("Tox: loading bootstrap nodes...");
1414 auto nodes
= loadBootNodes();
1415 conprintfln("Tox: %s nodes loaded", nodes
.length
);
1416 if (nodes
.length
== 0) return;
1417 foreach (const ref ToxBootstrapServer srv
; nodes
) {
1418 if (srv
.ipv4
.length
< 2) continue;
1419 assert(srv
.ipv4
[$-1] == 0);
1420 //conprintfln(" node ip: %s:%u (maintainer: %s)", srv.ipv4[0..$-1], srv.port, srv.maintainer);
1421 static if (mode
== "udp") {
1423 tox_bootstrap(ti
.tox
, srv
.ipv4
.ptr
, srv
.port
, srv
.pubkey
.ptr
, null);
1425 } else static if (mode
== "tcp") {
1427 foreach (immutable ushort port
; srv
.tcpports
) {
1428 tox_add_tcp_relay(ti
.tox
, srv
.ipv4
.ptr
, port
, srv
.pubkey
.ptr
, null);
1433 tox_bootstrap(ti
.tox
, srv
.ipv4
.ptr
, srv
.port
, srv
.pubkey
.ptr
, null);
1434 } else if (srv
.udp
) {
1435 foreach (immutable ushort port
; srv
.tcpports
) {
1436 tox_add_tcp_relay(ti
.tox
, srv
.ipv4
.ptr
, port
, srv
.pubkey
.ptr
, null);
1440 tox_iterate(ti
.tox
, null);
1442 //conprintfln("Tox[%s]: %s nodes added", srvalias, nodes.length);
1446 // ////////////////////////////////////////////////////////////////////////// //
1447 // returns `null` if there is no such file or file cannot be loaded
1448 ubyte[] loadToxCoreData (const(char)[] toxdatafname
) nothrow {
1449 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, rename
, fread
, ferror
, fseek
, ftell
, SEEK_SET
, SEEK_END
;
1450 import core
.stdc
.stdlib
: malloc
, free
;
1451 import core
.sys
.posix
.unistd
: unlink
;
1453 if (toxdatafname
.length
== 0) return null;
1455 static char* namebuf
= null;
1456 static uint nbsize
= 0;
1458 if (nbsize
< toxdatafname
.length
+1024) {
1459 import core
.stdc
.stdlib
: realloc
;
1460 nbsize
= cast(uint)toxdatafname
.length
+1024;
1461 namebuf
= cast(char*)realloc(namebuf
, nbsize
);
1462 if (namebuf
is null) assert(0, "out of memory");
1465 auto origName
= expandTilde(namebuf
[0..nbsize
-6], toxdatafname
);
1466 if (origName
== null) return null; // oops
1467 origName
.ptr
[origName
.length
] = 0; // zero-terminate
1469 FILE
* fi
= fopen(namebuf
, "r");
1470 if (fi
is null) return null;
1471 scope(exit
) fclose(fi
);
1474 if (fseek(fi
, 0, SEEK_END
) == -1) {
1475 import core
.stdc
.errno
;
1476 if (errno
== EINTR
) continue;
1486 import core
.stdc
.errno
;
1487 if (errno
== EINTR
) continue;
1493 if (fsize
> 1024*1024*256) { conwriteln("toxcore data file too big"); return null; }
1494 if (fsize
== 0) return null; // it cannot be zero-sized
1497 if (fseek(fi
, 0, SEEK_SET
) == -1) {
1498 import core
.stdc
.errno
;
1499 if (errno
== EINTR
) continue;
1505 auto res
= new ubyte[](cast(int)fsize
);
1506 conwriteln("loading toxcore data; size=", fsize
);
1509 while (left
.length
> 0) {
1510 auto rd
= fread(left
.ptr
, 1, left
.length
, fi
);
1511 if (rd
== 0) { delete res
; return null; } // error
1512 if (rd
< cast(int)left
.length
) {
1513 if (!ferror(fi
)) { delete res
; return null; } // error
1514 import core
.stdc
.errno
;
1515 if (errno
!= EINTR
) { delete res
; return null; } // error
1516 if (rd
> 0) left
= left
[rd
..$];
1519 if (rd
> left
.length
) { delete res
; return null; } // error
1523 if (left
.length
) { delete res
; return null; } // error
1529 bool saveToxCoreData (ToxP tox
, const(char)[] toxdatafname
) nothrow {
1530 import core
.stdc
.stdio
: rename
;
1531 import core
.stdc
.stdlib
: malloc
, free
;
1532 import core
.sys
.posix
.fcntl
: open
, O_WRONLY
, O_CREAT
, O_TRUNC
;
1533 import core
.sys
.posix
.unistd
: unlink
, close
, write
, fdatasync
;
1535 if (tox
is null || toxdatafname
.length
== 0) return false;
1537 auto size
= tox_get_savedata_size(tox
);
1538 if (size
> int.max
/8) return false; //throw new Exception("save data too big");
1540 char* savedata
= cast(char*)malloc(size
);
1541 if (savedata
is null) return false;
1542 scope(exit
) if (savedata
!is null) free(savedata
);
1544 tox_get_savedata(tox
, savedata
);
1545 conwriteln("save toxcore data; size=", size
);
1547 static char* namebuf
= null;
1548 static char* namebuf1
= null;
1549 static uint nbsize
= 0;
1551 if (nbsize
< toxdatafname
.length
+1024) {
1552 import core
.stdc
.stdlib
: realloc
;
1553 nbsize
= cast(uint)toxdatafname
.length
+1024;
1554 namebuf
= cast(char*)realloc(namebuf
, nbsize
);
1555 if (namebuf
is null) assert(0, "out of memory");
1556 namebuf1
= cast(char*)realloc(namebuf1
, nbsize
);
1557 if (namebuf1
is null) assert(0, "out of memory");
1560 auto origName
= expandTilde(namebuf
[0..nbsize
-6], toxdatafname
);
1561 if (origName
== null) return false; // oops
1562 origName
.ptr
[origName
.length
] = 0; // zero-terminate
1564 // create temporary name
1565 namebuf1
[0..origName
.length
] = origName
[];
1566 namebuf1
[origName
.length
..origName
.length
+5] = ".$$$\x00";
1568 int fo
= open(namebuf1
, O_WRONLY|O_CREAT|O_TRUNC
, 0o600
);
1570 conwriteln("failed to create file: '", origName
, ".$$$'");
1574 auto left
= savedata
[0..size
];
1575 while (left
.length
> 0) {
1576 auto wr
= write(fo
, left
.ptr
, left
.length
);
1578 // out of disk space; oops
1584 import core
.stdc
.errno
;
1585 if (errno
== EINTR
) continue;
1586 // some other error; oops
1591 if (wr
> left
.length
) {
1603 if (rename(namebuf1
, namebuf
) != 0) {
1612 version(ToxCoreDebug
) {
1613 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 {
1614 static inout(char)[] sz (inout(char)* s
) nothrow @trusted @nogc {
1615 if (s
is null) return null;
1617 while (s
[idx
]) ++idx
;
1618 return cast(inout(char)[])(s
[0..idx
]);
1621 conwriteln("level=", level
, "; file=", sz(file
), ":", line
, "; func=", sz(func
), "; msg=", sz(message
));
1626 // ////////////////////////////////////////////////////////////////////////// //
1627 // returns `false` if can't open/create
1628 ToxP
toxCreateInstance (const(char)[] toxdatafname
, in ref ProtoOptions protoOpts
, bool allowNew
, bool* wasCreated
=null) nothrow {
1629 if (wasCreated
!is null) *wasCreated
= false;
1631 if (toxdatafname
.length
== 0) return null;
1634 char* toxProxyHost
= null;
1636 import core
.stdc
.stdlib
: free
;
1637 if (toxProxyHost
!is null) free(toxProxyHost
);
1640 scope(exit
) delete savedata
;
1642 auto toxOpts
= tox_options_new();
1643 assert(toxOpts
!is null);
1644 scope(exit
) if (toxOpts
!is null) tox_options_free(toxOpts
);
1645 tox_options_default(toxOpts
);
1647 bool createNewToxCoreAccount
= false;
1648 toxOpts
.tox_options_set_ipv6_enabled(protoOpts
.ipv6
);
1649 toxOpts
.tox_options_set_udp_enabled(protoOpts
.udp
);
1650 toxOpts
.tox_options_set_local_discovery_enabled(protoOpts
.localDiscovery
);
1651 toxOpts
.tox_options_set_hole_punching_enabled(protoOpts
.holePunching
);
1652 toxOpts
.tox_options_set_start_port(protoOpts
.startPort
);
1653 toxOpts
.tox_options_set_end_port(protoOpts
.endPort
);
1654 toxOpts
.tox_options_set_tcp_port(protoOpts
.tcpPort
);
1656 toxOpts
.tox_options_set_proxy_type(protoOpts
.proxyType
);
1657 if (protoOpts
.proxyType
!= TOX_PROXY_TYPE_NONE
) {
1658 import core
.stdc
.stdlib
: malloc
;
1659 toxOpts
.tox_options_set_proxy_port(protoOpts
.proxyPort
);
1660 // create proxy address string
1661 toxProxyHost
= cast(char*)malloc(protoOpts
.proxyAddr
.length
+1);
1662 if (toxProxyHost
is null) assert(0, "out of memory");
1663 toxProxyHost
[0..protoOpts
.proxyAddr
.length
] = protoOpts
.proxyAddr
[];
1664 toxProxyHost
[protoOpts
.proxyAddr
.length
] = 0;
1665 toxOpts
.tox_options_set_proxy_host(toxProxyHost
);
1668 savedata
= loadToxCoreData(toxdatafname
);
1669 if (savedata
is null) {
1670 // create new tox instance
1671 if (wasCreated
!is null) *wasCreated
= true;
1672 if (!allowNew
) return null;
1673 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_NONE
);
1674 conwriteln("creating new ToxCore account...");
1677 conwriteln("setting ToxCore account data (", savedata
.length
, " bytes)");
1678 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_TOX_SAVE
);
1679 toxOpts
.tox_options_set_savedata_length(savedata
.length
);
1680 toxOpts
.tox_options_set_savedata_data(savedata
.ptr
, savedata
.length
);
1682 scope(exit
) delete savedata
;
1684 version(ToxCoreDebug
) {
1685 toxOpts
.tox_options_set_log_callback(&toxCoreLogCB
);
1688 // create tox instance
1690 ToxP tox
= tox_new(toxOpts
, &error
);
1692 conwriteln("cannot create ToxCore instance: error is ", error
);
1696 tox_callback_self_connection_status(tox
, &connectionCB
);
1698 tox_callback_friend_name(tox
, &friendNameCB
);
1699 tox_callback_friend_status_message(tox
, &friendStatusMessageCB
);
1700 tox_callback_friend_status(tox
, &friendStatusCB
);
1701 tox_callback_friend_connection_status(tox
, &friendConnectionCB
);
1702 //tox_callback_friend_typing(tox, &friendTypingCB);
1703 tox_callback_friend_read_receipt(tox
, &friendReceiptCB
);
1704 tox_callback_friend_request(tox
, &friendReqCB
);
1705 tox_callback_friend_message(tox
, &friendMsgCB
);
1707 tox_callback_file_recv_control(tox
, &fileRecvCtlCB
);
1708 tox_callback_file_chunk_request(tox
, &fileChunkReqCB
);
1709 tox_callback_file_recv(tox
, &fileRecvCB
);
1710 //tox_callback_file_recv_chunk
1712 //tox_callback_conference_invite
1713 //tox_callback_conference_message
1714 //tox_callback_conference_title
1715 //tox_callback_conference_namelist_change
1717 //tox_callback_friend_lossy_packet
1718 //tox_callback_friend_lossless_packet
1724 // ////////////////////////////////////////////////////////////////////////// //
1726 enum ToxCoreDataId
= 0x15ed1b1fU
;
1728 enum ToxCoreChunkTypeHi
= 0x01ceU
;
1729 enum ToxCoreChunkNoSpamKeys
= 1;
1730 enum ToxCoreChunkDHT
= 2;
1731 enum ToxCoreChunkFriends
= 3;
1732 enum ToxCoreChunkName
= 4;
1733 enum ToxCoreChunkStatusMsg
= 5;
1734 enum ToxCoreChunkStatus
= 6;
1735 enum ToxCoreChunkTcpRelay
= 10;
1736 enum ToxCoreChunkPathNode
= 11;
1737 enum ToxCoreChunkEnd
= 255;
1740 struct ToxSavedFriend
{
1741 enum TOTAL_DATA_SIZE
= 2216;
1742 enum CRYPTO_PUBLIC_KEY_SIZE
= 32;
1743 enum CRYPTO_SECRET_KEY_SIZE
= 32;
1744 enum SAVED_FRIEND_REQUEST_SIZE
= 1024;
1745 enum MAX_NAME_LENGTH
= 128;
1746 enum MAX_STATUSMESSAGE_LENGTH
= 1007;
1748 enum Status
: ubyte {
1756 enum UserStatus
: ubyte {
1764 ubyte[CRYPTO_PUBLIC_KEY_SIZE
] real_pk
;
1765 char[SAVED_FRIEND_REQUEST_SIZE
] info
; // the data that is sent during the friend requests we do
1767 ushort info_size
; // length of the info
1768 char[MAX_NAME_LENGTH
] name
;
1770 char[MAX_STATUSMESSAGE_LENGTH
] statusmessage
;
1772 ushort statusmessage_length
;
1773 UserStatus userstatus
;
1775 uint friendrequest_nospam
;
1776 ulong last_seen_time
;
1778 void load (VFile fl
) {
1779 // we can use CTFE here, but meh...
1780 status
= cast(Status
)fl
.readNum
!ubyte;
1781 if (status
> Status
.max
) throw new ProtocolException("invalid friend status");
1782 fl
.rawReadExact(real_pk
[]);
1783 fl
.rawReadExact(info
[]);
1784 /*pad0 =*/ fl
.readNum
!ubyte;
1785 info_size
= fl
.readNum
!(ushort, "BE");
1786 if (info_size
> info
.length
) throw new ProtocolException("invalid friend data");
1787 fl
.rawReadExact(name
[]);
1788 name_length
= fl
.readNum
!(ushort, "BE");
1789 if (name_length
> name
.length
) throw new ProtocolException("invalid friend data");
1790 fl
.rawReadExact(statusmessage
[]);
1791 /*pad1 =*/ fl
.readNum
!ubyte;
1792 statusmessage_length
= fl
.readNum
!(ushort, "BE");
1793 if (statusmessage_length
> statusmessage
.length
) throw new ProtocolException("invalid friend data");
1794 userstatus
= cast(UserStatus
)fl
.readNum
!ubyte;
1795 if (userstatus
> UserStatus
.max
) throw new ProtocolException("invalid friend userstatus");
1796 /*pad30 =*/ fl
.readNum
!ubyte;
1797 /*pad31 =*/ fl
.readNum
!ubyte;
1798 /*pad32 =*/ fl
.readNum
!ubyte;
1799 friendrequest_nospam
= fl
.readNum
!uint;
1800 last_seen_time
= fl
.readNum
!(ulong, "BE");