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 // ////////////////////////////////////////////////////////////////////////// //
56 struct ToxCoreDataFile
{
57 string nick
; // user nick
59 ubyte[TOX_PUBLIC_KEY_SIZE
] pubkey
= toxCoreEmptyKey
;
60 ToxAddr addr
= toxCoreEmptyAddr
;
62 ContactStatus status
= ContactStatus
.Offline
;
64 ContactInfo
[] friends
;
66 @property bool valid () const pure nothrow @safe @nogc => isValidKey(pubkey
);
68 static ToxAddr
buildAddress (in ref ubyte[TOX_PUBLIC_KEY_SIZE
] pubkey
, uint nospam
) nothrow @trusted @nogc {
69 static ushort calcChecksum (const(ubyte)[] data
) nothrow @trusted @nogc {
70 ubyte[2] checksum
= 0;
71 foreach (immutable idx
, ubyte b
; data
) checksum
[idx
%2] ^
= b
;
72 return *cast(ushort*)checksum
.ptr
;
76 res
[0..TOX_PUBLIC_KEY_SIZE
] = pubkey
[];
77 *cast(uint*)(res
.ptr
+TOX_PUBLIC_KEY_SIZE
) = nospam
;
78 *cast(ushort*)(res
.ptr
+TOX_PUBLIC_KEY_SIZE
+uint.sizeof
) = calcChecksum(res
[0..$-2]);
84 // ////////////////////////////////////////////////////////////////////////// //
86 class ProtocolException
: Exception
{
87 this (string msg
, string file
=__FILE__
, usize line
=__LINE__
, Throwable next
=null) pure nothrow @safe @nogc {
88 super(msg
, file
, line
, next
);
93 // ////////////////////////////////////////////////////////////////////////// //
94 // messages thread sends to [glconCtlWindow].
95 // [who] can be the same as [self] to indicate account state changes
98 PubKey self
; // account
99 PubKey who
; // can be same as [self]
100 nothrow @trusted @nogc:
101 this (in ref PubKey aself
, in ref PubKey awho
) { self
[] = aself
[]; who
= awho
[]; }
104 // connection state changed
105 class ToxEventConnection
: ToxEventBase
{
107 nothrow @trusted @nogc:
108 this (in ref PubKey aself
, in ref PubKey awho
, bool aconnected
) { super(aself
, awho
); connected
= aconnected
; }
111 // online status changed
112 class ToxEventStatus
: ToxEventBase
{
113 ContactStatus status
;
114 nothrow @trusted @nogc:
115 this (in ref PubKey aself
, in ref PubKey awho
, ContactStatus astatus
) { super(aself
, awho
); status
= astatus
; }
119 class ToxEventNick
: ToxEventBase
{
120 string nick
; // new nick
121 nothrow @trusted @nogc:
122 this (in ref PubKey aself
, in ref PubKey awho
, string anick
) { super(aself
, awho
); nick
= anick
; }
125 // status message changed
126 class ToxEventStatusMsg
: ToxEventBase
{
127 string message
; // new message
128 nothrow @trusted @nogc:
129 this (in ref PubKey aself
, in ref PubKey awho
, string amessage
) { super(aself
, awho
); message
= amessage
; }
132 // typing status changed
133 class ToxEventTyping
: ToxEventBase
{
135 nothrow @trusted @nogc:
136 this (in ref PubKey aself
, in ref PubKey awho
, bool atyping
) { super(aself
, awho
); typing
= atyping
; }
140 class ToxEventMessage
: ToxEventBase
{
141 bool action
; // is this an "action" message? (/me)
145 this (in ref PubKey aself
, in ref PubKey awho
, bool aaction
, string amessage
) {
152 this (in ref PubKey aself
, in ref PubKey awho
, bool aaction
, string amessage
, SysTime atime
) {
160 // send message ack comes
161 class ToxEventMessageAck
: ToxEventBase
{
163 nothrow @trusted @nogc:
164 this (in ref PubKey aself
, in ref PubKey awho
, long amsgid
) { super(aself
, awho
); msgid
= amsgid
; }
167 // friend request comes
168 class ToxEventFriendReq
: ToxEventBase
{
169 string message
; // request message
170 nothrow @trusted @nogc:
171 this (in ref PubKey aself
, in ref PubKey awho
, string amessage
) { super(aself
, awho
); message
= amessage
; }
175 // ////////////////////////////////////////////////////////////////////////// //
177 /// shutdown protocol module
178 void toxCoreShutdownAll () {
180 synchronized(TPInfo
.classinfo
) {
181 foreach (TPInfo ti
; allProtos
) {
182 ti
.tid
.send(TrdCmdQuit
.init
);
186 while (acksLeft
> 0) {
188 (TrdCmdQuitAck ack
) { --acksLeft
; },
193 foreach (TPInfo tpi
; allProtos
) {
194 // save toxcode data, if there is any
195 if (tpi
.tox
!is null && tpi
.toxDataDiskName
.length
) saveToxCoreData(tpi
.tox
, tpi
.toxDataDiskName
);
196 if (tpi
.tox
!is null) { tox_kill(tpi
.tox
); tpi
.tox
= null; }
201 // ////////////////////////////////////////////////////////////////////////// //
204 static immutable PubKey toxCoreEmptyKey
= 0;
205 static immutable ToxAddr toxCoreEmptyAddr
= 0;
207 /// delegate that will be used to send messages.
208 /// can be called from any thread, so it should be thread-safe, and should avoid deadlocks.
209 /// the delegate should be set on program startup, and should not be changed anymore.
210 /// FIXME: add API to change this!
211 __gshared
void delegate (Object msg
) nothrow toxCoreSendEvent
;
214 bool isValidKey (in ref PubKey key
) pure nothrow @safe @nogc => (key
[] != toxCoreEmptyKey
[]);
217 bool isValidAddr (in ref ToxAddr key
) pure nothrow @safe @nogc => (key
[] != toxCoreEmptyAddr
[]);
220 /// parse toxcore data file.
221 /// sorry, i had to do it manually, 'cause there is no way to open toxcore data without going online.
222 public ToxCoreDataFile
toxCoreLoadDataFile (VFile fl
) nothrow {
225 version(ToxCoreUseBuiltInDataFileParser
) {
226 auto sz
= fl
.size
-fl
.tell
;
227 if (sz
< 8) throw new ProtocolException("data file too small");
228 if (fl
.readNum
!uint != 0) throw new ProtocolException("invalid something");
229 if (fl
.readNum
!uint != ToxCoreDataId
) throw new ProtocolException("not a ToxCore data file");
231 auto len
= fl
.readNum
!uint;
232 auto id
= fl
.readNum
!uint;
233 if (id
>>16 != ToxCoreChunkTypeHi
) throw new ProtocolException("invalid chunk hitype");
236 case ToxCoreChunkNoSpamKeys
:
237 if (len
== ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
+ToxSavedFriend
.CRYPTO_SECRET_KEY_SIZE
+uint.sizeof
) {
238 res
.nospam
= fl
.readNum
!uint;
239 fl
.rawReadExact(res
.pubkey
[]);
240 ubyte[ToxSavedFriend
.CRYPTO_PUBLIC_KEY_SIZE
] privkey
;
241 fl
.rawReadExact(privkey
[]);
244 res
.addr
[] = res
.buildAddress(res
.pubkey
, res
.nospam
);
247 case ToxCoreChunkDHT
: break;
248 case ToxCoreChunkFriends
:
249 if (len
%ToxSavedFriend
.TOTAL_DATA_SIZE
!= 0) throw new ProtocolException("invalid contact list");
255 if (st
> len || st
!= ToxSavedFriend
.TOTAL_DATA_SIZE
) throw new ProtocolException("invalid contact list");
257 if (fr
.status
== ToxSavedFriend
.Status
.NoFriend
) continue;
259 ci
.nick
= fr
.name
[0..fr
.name_length
].idup
;
260 ci
.lastonlinetime
= cast(uint)fr
.last_seen_time
;
261 final switch (fr
.status
) {
262 case ToxSavedFriend
.Status
.NoFriend
: assert(0, "wtf?!");
264 case ToxSavedFriend
.Status
.Added
: ci
.kind
= ContactInfo
.Kind
.PengingAuthAccept
; break;
265 case ToxSavedFriend
.Status
.Requested
: ci
.kind
= ContactInfo
.Kind
.PengingAuthRequest
; break;
266 case ToxSavedFriend
.Status
.Confirmed
:
267 case ToxSavedFriend
.Status
.Online
:
268 ci
.kind
= ContactInfo
.Kind
.Friend
;
271 if (ci
.kind
!= ContactInfo
.Kind
.Friend
) {
272 ci
.statusmsg
= fr
.info
[0..fr
.info_size
].idup
;
274 ci
.statusmsg
= fr
.statusmessage
[0..fr
.statusmessage_length
].idup
;
276 ci
.nospam
= fr
.friendrequest_nospam
;
277 ci
.pubkey
[] = fr
.real_pk
[];
281 case ToxCoreChunkName
:
283 auto name
= new char[](len
);
284 fl
.rawReadExact(name
);
286 res
.nick
= cast(string
)name
; // it is safe to cast here
289 case ToxCoreChunkStatusMsg
:
291 auto msg
= new char[](len
);
292 fl
.rawReadExact(msg
);
294 res
.statusmsg
= cast(string
)msg
; // it is safe to cast here
297 case ToxCoreChunkStatus
:
299 auto st
= fl
.readNum
!ubyte;
300 if (st
< ToxSavedFriend
.UserStatus
.Invalid
) {
302 case ToxSavedFriend
.UserStatus
.None
: res
.status
= ContactStatus
.Online
; break;
303 case ToxSavedFriend
.UserStatus
.Away
: res
.status
= ContactStatus
.Away
; break;
304 case ToxSavedFriend
.UserStatus
.Busy
: res
.status
= ContactStatus
.Busy
; break;
305 default: res
.status
= ContactStatus
.Offline
; break;
311 case ToxCoreChunkTcpRelay
: break;
312 case ToxCoreChunkPathNode
: break;
315 if (id
== ToxCoreChunkEnd
) break;
316 fl
.seek(len
, Seek
.Cur
);
319 // nope, not found, try to open account
320 ProtoOptions protoOpts
;
321 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
322 if (tox
is null) throw new ProtocolException("cannot load toxcore data file");
323 scope(exit
) tox_kill(tox
); // and immediately kill it, or it will go online. fuck.
325 tox_self_get_public_key(tox
, res
.pubkey
.ptr
);
326 tox_self_get_address(tox
, res
.addr
.ptr
);
327 res
.nospam
= tox_self_get_nospam(tox
);
330 auto nsz
= tox_self_get_name_size(tox
);
332 if (nsz
> tox_max_name_length()) nsz
= tox_max_name_length(); // just in case
333 auto xbuf
= new char[](tox_max_name_length());
335 tox_self_get_name(tox
, xbuf
.ptr
);
336 res
.nick
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
339 auto msz
= tox_self_get_status_message_size(tox
);
341 if (msz
> tox_max_status_message_length()) msz
= tox_max_status_message_length(); // just in case
342 auto xbuf
= new char[](tox_max_status_message_length());
344 tox_self_get_status_message(tox
, xbuf
.ptr
);
345 res
.statusmsg
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
348 // TODO: online status, friend list
349 assert(0, "not finished");
351 } catch (Exception e
) {
358 /// returns public key for account with the given data file, or [toxCoreEmptyKey].
359 PubKey
toxCoreGetAccountPubKey (const(char)[] toxdatafname
) nothrow {
360 version(ToxCoreUseBuiltInDataFileParser
) {
362 auto data
= toxCoreLoadDataFile(VFile(toxdatafname
));
363 if (isValidKey(data
.pubkey
)) return data
.pubkey
[];
364 } catch (Exception e
) {}
367 synchronized(TPInfo
.classinfo
) {
368 foreach (TPInfo ti
; allProtos
) if (ti
.toxDataDiskName
== toxdatafname
) { tacc
= ti
; break; }
371 // nope, not found, try to open account
372 ProtoOptions protoOpts
;
373 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
374 if (tox
is null) return toxCoreEmptyKey
;
375 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
376 // and immediately kill it, or it will go online. fuck.
383 PubKey self
; tox_self_get_public_key(tacc
.tox
, self
.ptr
);
388 return toxCoreEmptyKey
;
392 /// returns address public key for account with the given data file, or [toxCoreEmptyKey].
393 ToxAddr
toxCoreGetAccountAddress (const(char)[] toxdatafname
) nothrow {
394 version(ToxCoreUseBuiltInDataFileParser
) {
396 auto data
= toxCoreLoadDataFile(VFile(toxdatafname
));
397 if (isValidKey(data
.pubkey
)) return data
.addr
[];
398 } catch (Exception e
) {}
401 synchronized(TPInfo
.classinfo
) {
402 foreach (TPInfo ti
; allProtos
) if (ti
.toxDataDiskName
== toxdatafname
) { tacc
= ti
; break; }
405 // nope, not found, try to open account
406 ProtoOptions protoOpts
;
407 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
408 if (tox
is null) return toxCoreEmptyAddr
;
409 ToxAddr addr
; tox_self_get_address(tox
, addr
.ptr
);
410 // and immediately kill it, or it will go online. fuck.
418 if (tacc
.tox
is null) {
421 tox_self_get_address(ti
.tox
, res
.ptr
);
427 return toxCoreEmptyAddr
;
431 /// returns address that can be given to other people, so they can friend you.
432 /// returns zero-filled array on invalid request.
433 ToxAddr
toxCoreGetSelfAddress (in ref PubKey self
) nothrow {
435 doWithLockedTPByKey(self
, delegate (ti
) {
436 if (ti
.tox
is null) {
439 tox_self_get_address(ti
.tox
, res
.ptr
);
446 /// checks if we have a working thread for `self`.
447 bool toxCoreIsAccountOpen (in ref PubKey self
) nothrow {
448 synchronized(TPInfo
.classinfo
) {
449 foreach (TPInfo ti
; allProtos
) {
450 if (ti
.self
[] == self
[]) return true;
457 /// returns nick for the given account, or `null`.
458 string
toxCoreGetNick (in ref PubKey self
) nothrow {
459 synchronized(TPInfo
.classinfo
) {
460 foreach (TPInfo ti
; allProtos
) {
461 if (ti
.self
[] == self
[]) return ti
.nick
;
468 /// creates new Tox account (or opens old), stores it in data file, returns public key for new account.
469 /// returns [toxCoreEmptyKey] on error.
470 /// TODO: pass protocol options here
471 /// WARNING! don't call this from more than one thread with the same `toxdatafname`
472 PubKey
toxCoreCreateAccount (const(char)[] toxdatafname
, const(char)[] nick
) nothrow {
473 if (toxdatafname
.length
== 0) return toxCoreEmptyKey
;
475 if (nick
.length
== 0 || nick
.length
> tox_max_name_length()) return toxCoreEmptyKey
;
478 import std
.path
: absolutePath
;
480 string toxDataDiskName
= toxdatafname
.idup
.absolutePath
;
482 synchronized(TPInfo
.classinfo
) {
483 foreach (TPInfo ti
; allProtos
) {
484 if (ti
.toxDataDiskName
== toxDataDiskName
) return ti
.self
[];
488 ProtoOptions protoOpts
;
489 protoOpts
.udp
= true;
491 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:true);
492 if (tox
is null) return toxCoreEmptyKey
;
494 auto ti
= new TPInfo();
495 ti
.toxDataDiskName
= toxDataDiskName
;
496 tox_self_get_public_key(tox
, ti
.self
.ptr
);
497 tox_self_get_address(tox
, ti
.addr
.ptr
);
501 tox_self_set_name(tox
, nick
.ptr
, nick
.length
);
502 saveToxCoreData(tox
, toxDataDiskName
);
504 // and immediately kill it, or it will go online. fuck.
507 synchronized(TPInfo
.classinfo
) {
512 } catch (Exception e
) {
515 return toxCoreEmptyKey
;
519 /// starts working thread for account with the given data file.
520 /// returns account public key, or [toxCoreEmptyKey] on error.
521 /// TODO: pass protocol options here
522 /// WARNING! don't call this from more than one thread with the same `toxdatafname`
523 PubKey
toxCoreOpenAccount (const(char)[] toxdatafname
) nothrow {
524 if (toxdatafname
.length
== 0) return toxCoreEmptyKey
;
527 import std
.path
: absolutePath
, dirName
;
529 string toxDataDiskName
= toxdatafname
.idup
.absolutePath
;
531 synchronized(TPInfo
.classinfo
) {
532 foreach (TPInfo ti
; allProtos
) {
533 if (ti
.toxDataDiskName
== toxDataDiskName
) return ti
.self
[];
537 version(ToxCoreUseBuiltInDataFileParser
) {
539 auto data
= toxCoreLoadDataFile(VFile(toxDataDiskName
));
540 if (!isValidKey(data
.pubkey
)) return toxCoreEmptyKey
;
542 auto ti
= new TPInfo();
543 ti
.toxDataDiskName
= toxDataDiskName
;
544 ti
.self
[] = data
.pubkey
[];
545 ti
.addr
[] = data
.addr
[];
548 // use toxcore to parse data file
549 ProtoOptions protoOpts
;
551 protoOpts
.txtunser(VFile(toxDataDiskName
.dirName
~"/proto.rc"));
552 } catch (Exception e
) {
553 protoOpts
= protoOpts
.default;
554 protoOpts
.udp
= true;
557 auto tox
= toxCreateInstance(toxdatafname
, protoOpts
, allowNew
:false);
558 if (tox
is null) return toxCoreEmptyKey
;
559 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
561 auto ti
= new TPInfo();
562 ti
.toxDataDiskName
= toxDataDiskName
;
563 tox_self_get_public_key(tox
, ti
.self
.ptr
);
564 tox_self_get_address(tox
, ti
.addr
.ptr
);
567 auto nsz
= tox_self_get_name_size(tox
);
569 if (nsz
> tox_max_name_length()) nsz
= tox_max_name_length(); // just in case
570 // k8: toxcore developers are idiots, so we have to do dynalloc here
571 auto xbuf
= new char[](tox_max_name_length());
573 tox_self_get_name(tox
, xbuf
.ptr
);
574 ti
.nick
= cast(string
)(xbuf
[0..nsz
]); // ah, cast it directly here
577 // and immediately kill it, or it will go online. fuck.
581 synchronized(TPInfo
.classinfo
) {
587 } catch (Exception e
) {
590 return toxCoreEmptyKey
;
594 /// stops working thread for account with the given data file.
595 /// returns success flag.
596 bool toxCoreCloseAccount (in ref PubKey self
) nothrow {
599 synchronized(TPInfo
.classinfo
) {
600 foreach (TPInfo ti
; allProtos
) if (ti
.self
[] == self
[]) { tpi
= ti
; break; }
602 if (tpi
is null) return false;
604 // send "quit" signal
605 tpi
.tid
.send(TrdCmdQuit
.init
);
609 (TrdCmdQuitAck ack
) { done
= true; },
615 // save toxcode data, if there is any
616 if (tpi
.tox
!is null && tpi
.toxDataDiskName
.length
) saveToxCoreData(tpi
.tox
, tpi
.toxDataDiskName
);
617 if (tpi
.tox
!is null) { tox_kill(tpi
.tox
); tpi
.tox
= null; }
619 // remove from the list
620 synchronized(TPInfo
.classinfo
) {
621 foreach (immutable idx
, TPInfo ti
; allProtos
) {
622 if (ti
.self
[] == self
[]) {
623 foreach (immutable c
; idx
+1..allProtos
.length
) allProtos
[c
-1] = allProtos
[c
];
624 delete allProtos
[$-1];
625 allProtos
.length
-= 1;
626 allProtos
.assumeSafeAppend
;
633 } catch (Exception e
) {
639 enum MsgIdError
= -1;
640 enum MsgIdOffline
= 0;
642 /// sends message. returns message id that can be used to track acks.
643 /// returns [MsgIdError] for unknown account, or unknown `dest`.
644 /// returns [MsgIdOffline] if `self` or `dest` is offline (message is not queued to send in this case).
645 /// note that `0` is a not a valid message id.
646 /// cannot send empty messages, and messages longer than `TOX_MAX_MESSAGE_LENGTH` chars.
647 long toxCoreSendMessage (in ref PubKey self
, in ref PubKey dest
, const(char)[] msg
, bool action
=false) nothrow {
648 long res
= MsgIdError
;
649 if (msg
.length
== 0) return res
; // cannot send empty messages
650 TOX_MESSAGE_TYPE tt
= (action ? TOX_MESSAGE_TYPE_ACTION
: TOX_MESSAGE_TYPE_NORMAL
);
652 if (msg.startsWith("/me ")) {
653 msg = msg[4..$].xstripleft;
654 if (msg.length == 0) return res; // cannot send empty messages
655 tt = TOX_MESSAGE_TYPE_ACTION;
658 if (msg
.length
> tox_max_message_length()) return res
; // message too long
659 doWithLockedTPByKey(self
, delegate (ti
) {
660 if (ti
.tox
is null) { res
= MsgIdOffline
; return; }
661 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) { res = MsgIdOffline; return; }
663 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
664 if (frnum
== uint.max
) return; // error
666 //if (tox_friend_get_connection_status(ti.tox, frnum) == TOX_CONNECTION_NONE) { res = MsgIdOffline; return; }
667 // nope, online; queue the message
668 TOX_ERR_FRIEND_SEND_MESSAGE err
= 0;
669 uint msgid
= tox_friend_send_message(ti
.tox
, frnum
, tt
, msg
.ptr
, msg
.length
, &err
);
671 case TOX_ERR_FRIEND_SEND_MESSAGE_OK
: res
= (cast(long)msgid
)+1; break;
672 case TOX_ERR_FRIEND_SEND_MESSAGE_NULL
: res
= MsgIdError
; break;
673 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_FOUND
: res
= MsgIdError
; break;
674 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_CONNECTED
: res
= MsgIdOffline
; break;
675 case TOX_ERR_FRIEND_SEND_MESSAGE_SENDQ
: res
= MsgIdError
; break;
676 case TOX_ERR_FRIEND_SEND_MESSAGE_TOO_LONG
: res
= MsgIdError
; break;
677 case TOX_ERR_FRIEND_SEND_MESSAGE_EMPTY
: res
= MsgIdError
; break;
678 default: res
= MsgIdError
; break;
680 if (res
> 0) ti
.ping();
687 /// returns `false` if `self` is invalid, or some error occured.
688 bool toxCoreSetStatus (in ref PubKey self
, ContactStatus status
) nothrow {
689 if (status
== ContactStatus
.Connecting
) return false; // oops
691 doWithLockedTPByKey(self
, delegate (ti
) {
692 // if we're going offline, kill toxcore instance
693 if (status
== ContactStatus
.Offline
) {
694 ti
.tid
.send(TrdCmdKillToxCore
.init
);
695 // send messages, just in case
696 if (toxCoreSendEvent
!is null) {
697 toxCoreSendEvent(new ToxEventStatus(ti
.self
, ti
.self
, ContactStatus
.Offline
));
698 toxCoreSendEvent(new ToxEventConnection(ti
.self
, ti
.self
, false));
707 if (ti
.tox
is null) {
708 // need to create toxcore object
710 import std
.path
: absolutePath
, dirName
;
712 ProtoOptions protoOpts
;
714 protoOpts
.txtunser(VFile(ti
.toxDataDiskName
.dirName
~"/proto.rc"));
715 } catch (Exception e
) {
716 protoOpts
= protoOpts
.default;
717 protoOpts
.udp
= true;
720 conwriteln("creating ToxCore...");
721 tox
= toxCreateInstance(ti
.toxDataDiskName
, protoOpts
, allowNew
:false);
723 conwriteln("can't create ToxCore...");
726 TrdCmdSetToxCore cmd
;
728 ti
.tid
.send(cast(shared)cmd
);
729 } catch (Exception e
) {
735 assert(tox
!is null);
738 final switch (status
) {
739 case ContactStatus
.Offline
: assert(0, "wtf?!");
740 case ContactStatus
.Online
: st
= TOX_USER_STATUS_NONE
; break;
741 case ContactStatus
.Away
: st
= TOX_USER_STATUS_AWAY
; break;
742 case ContactStatus
.Busy
: st
= TOX_USER_STATUS_BUSY
; break;
743 case ContactStatus
.Connecting
: assert(0, "wtf?!");
745 tox_self_set_status(tox
, st
);
754 /// sets new status message.
755 /// returns `false` if `self` is invalid, message is too long, or on any other error.
756 bool toxCoreSetStatusMessage (in ref PubKey self
, const(char)[] message
) nothrow {
757 if (message
.length
> tox_max_status_message_length()) return false;
759 doWithLockedTPByKey(self
, delegate (ti
) {
760 if (ti
.tox
is null) return;
761 res
= tox_self_set_status_message(ti
.tox
, message
.ptr
, message
.length
);
762 if (res
) ti
.needSave
= true;
769 /// sends friend request.
770 /// returns `false` if `self` is invalid, message is too long, or on any other error.
771 bool toxCoreSendFriendRequest (in ref PubKey self
, in ref ToxAddr dest
, const(char)[] message
) nothrow {
772 if (message
.length
> tox_max_friend_request_length()) return false; // cannot send long requests
774 doWithLockedTPByKey(self
, delegate (ti
) {
775 if (ti
.tox
is null) return; // this account is offline
776 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
778 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
779 if (frnum
!= uint.max
) {
780 // we already befriend it, do nothing
784 frnum
= tox_friend_add(ti
.tox
, dest
.ptr
, message
.ptr
, message
.length
);
785 res
= (frnum
!= uint.max
);
786 if (res
) ti
.needSave
= true;
793 /// unconditionally adds a friend.
794 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
795 bool toxCoreAddFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
797 doWithLockedTPByKey(self
, delegate (ti
) {
798 if (ti
.tox
is null) return; // this account is offline
799 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
801 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
802 if (frnum
!= uint.max
) {
803 // we already has such friend, do nothing
807 frnum
= tox_friend_add_norequest(ti
.tox
, dest
.ptr
);
808 res
= (frnum
!= uint.max
);
809 if (res
) ti
.needSave
= true;
816 /// unconditionally removes a friend.
817 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
818 bool toxCoreRemoveFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
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 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
825 if (frnum
== uint.max
) { res
= false; return; } // no such friend
826 res
= tox_friend_delete(ti
.tox
, frnum
);
827 if (res
) ti
.needSave
= true;
834 /// checks if the given accound has a friend with the given pubkey.
835 /// returns `false` if `self` is invalid or offline, or on any other error, or if there is no such friend.
836 bool toxCoreHasFriend (in ref PubKey self
, in ref PubKey dest
) nothrow {
838 doWithLockedTPByKey(self
, delegate (ti
) {
839 if (ti
.tox
is null) return; // this account is offline
841 uint frnum
= tox_friend_by_public_key(ti
.tox
, dest
.ptr
);
842 res
= (frnum
!= uint.max
);
848 /// calls delegate for each known friend.
849 /// return `true` from delegate to stop.
850 /// WARNING! all background operations are locked, so don't spend too much time in delegate!
851 void toxCoreForEachFriend (in ref PubKey self
, scope bool delegate (in ref PubKey self
, in ref PubKey frpub
, scope const(char)[] nick
) dg
) nothrow {
852 if (dg
is null) return;
853 doWithLockedTPByKey(self
, delegate (ti
) {
854 if (ti
.tox
is null) return; // this account is offline
855 auto frcount
= tox_self_get_friend_list_size(ti
.tox
);
856 if (frcount
== 0) return;
857 auto list
= new uint[](frcount
);
858 scope(exit
) delete list
;
860 scope(exit
) delete nick
;
861 tox_self_get_friend_list(ti
.tox
, list
.ptr
);
863 foreach (immutable fidx
, immutable fid
; list
[]) {
864 if (!tox_friend_get_public_key(ti
.tox
, fid
, fpk
.ptr
)) continue;
865 auto nsz
= tox_friend_get_name_size(ti
.tox
, fid
);
866 if (nsz
> nick
.length
) nick
.length
= nsz
;
867 if (nsz
!= 0) tox_friend_get_name(ti
.tox
, fid
, nick
.ptr
);
869 dg(self
, fpk
, nick
[0..nsz
]);
870 } catch (Exception e
) {
878 /// returns the time when this friend was seen online.
879 /// returns `SysTime.min` if `self` is invalid, `dest` is invalid, or on any other error.
880 SysTime
toxCoreLastSeen (in ref PubKey self
, in ref PubKey dest
) nothrow {
881 SysTime res
= SysTime
.min
;
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 if (frnum
== uint.max
) return; // unknown friend
887 TOX_ERR_FRIEND_GET_LAST_ONLINE err
;
888 auto ut
= tox_friend_get_last_online(ti
.tox
, frnum
, &err
);
890 if (err
== 0) res
= SysTime
.fromUnixTime(ut
);
891 } catch (Exception e
) {
899 /// calls delegate with ToxP. ugly name is intentional.
900 void toxCoreCallWithToxP (in ref PubKey self
, scope void delegate (ToxP tox
) dg
) nothrow {
901 if (dg
is null) return;
902 doWithLockedTPByKey(self
, delegate (ti
) {
903 if (ti
.tox
is null) return; // this account is offline
906 } catch (Exception e
) {}
911 // ////////////////////////////////////////////////////////////////////////// //
912 // private ToxCore protocol implementation details
916 static struct FileRejectEvent
{
922 string toxDataDiskName
;
926 ToxAddr addr
; // will be used if `tox` is `null`
928 FileRejectEvent
[] rej
;
929 bool doBootstrap
; // do bootstrapping
932 void ping () nothrow {
933 if (tox
is null) return;
934 try { tid
.send(TrdCmdPing
.init
); } catch (Exception e
) {}
938 __gshared TPInfo
[] allProtos
;
941 // ////////////////////////////////////////////////////////////////////////// //
942 struct TrdCmdQuit
{} // quit thread loop
943 struct TrdCmdQuitAck
{} // quit thread loop
944 struct TrdCmdPing
{} // do something
945 struct TrdCmdKillToxCore
{}
946 struct TrdCmdSetToxCore
{ ToxP tox
; }
949 // ////////////////////////////////////////////////////////////////////////// //
950 // this should be called with registered `ti`
951 void startThread (TPInfo ti
) {
953 ti
.tid
= spawn(&toxCoreThread
, thisTid
, *cast(immutable(void)**)&ti
);
957 // ////////////////////////////////////////////////////////////////////////// //
958 static void toxCoreThread (Tid ownerTid
, immutable(void)* tiptr
) {
960 MonoTime lastSaveTime
= MonoTime
.zero
;
961 TPInfo ti
= *cast(TPInfo
*)&tiptr
; // HACK!
966 bool doKillTox
= false;
968 if (ti
.tox
!is null && ti
.doBootstrap
) {
969 conwriteln("TOX(", ti
.nick
, "): bootstrapping");
970 ti
.doBootstrap
= false;
972 mswait
= tox_iteration_interval(ti
.tox
);
973 if (mswait
< 1) mswait
= 1;
974 conwriteln("TOX(", ti
.nick
, "): bootstrapping complete (mswait=", mswait
, ")");
977 receiveTimeout((mswait ? mswait
.msecs
: 10.hours
),
978 (TrdCmdQuit cmd
) { doQuit
= true; },
980 (TrdCmdKillToxCore cmd
) { doKillTox
= true; },
981 (shared TrdCmdSetToxCore cmd
) { newTox
= cast(ToxP
)cmd
.tox
; },
982 (Variant v
) { conwriteln("WUTAFUCK?! "); },
986 if (newTox
!is null) {
987 conwriteln("TOX(", ti
.nick
, "): got new toxcore pointer");
988 if (ti
.tox
!is newTox
) {
989 if (ti
.tox
!is null) tox_kill(ti
.tox
);
993 ti
.doBootstrap
= true;
998 if (ti
.tox
is null) { mswait
= 0; continue; }
1001 conwriteln("TOX(", ti
.nick
, "): killing toxcore pointer");
1003 if (ti
.tox
!is null) {
1007 conwriteln("TOX(", ti
.nick
, "): wtf?!");
1013 tox_iterate(ti
.tox
, null);
1014 mswait
= tox_iteration_interval(ti
.tox
);
1015 //conwriteln("TOX(", ti.nick, "): interval is ", mswait);
1016 if (mswait
< 1) mswait
= 1;
1019 auto ctt
= MonoTime
.currTime
;
1020 if ((ctt
-lastSaveTime
).total
!"minutes" > 1) {
1021 ti
.needSave
= false;
1023 saveToxCoreData(ti
.tox
, ti
.toxDataDiskName
);
1028 ownerTid
.send(TrdCmdQuitAck
.init
);
1029 } catch (Throwable e
) {
1030 // here, we are dead and fucked (the exact order doesn't matter)
1031 import core
.stdc
.stdlib
: abort
;
1032 import core
.stdc
.stdio
: fprintf
, stderr
;
1033 import core
.memory
: GC
;
1034 import core
.thread
: thread_suspendAll
;
1035 GC
.disable(); // yeah
1036 thread_suspendAll(); // stop right here, you criminal scum!
1037 auto s
= e
.toString();
1038 fprintf(stderr
, "\n=== FATAL ===\n%.*s\n", cast(uint)s
.length
, s
.ptr
);
1039 abort(); // die, you bitch!
1044 // ////////////////////////////////////////////////////////////////////////// //
1045 // find TPInfo object for the given tox handle; used in callbacks
1046 TPInfo
findTP (ToxP tox
) nothrow {
1047 if (tox
is null) return null;
1048 synchronized(TPInfo
.classinfo
) {
1049 foreach (TPInfo ti
; allProtos
) {
1050 if (ti
.tox
is tox
) return ti
;
1057 // returns `true` if found and executed without errors
1058 bool doWithLockedTPByKey (in ref PubKey self
, scope void delegate (TPInfo ti
) dg
) nothrow {
1060 if (dg
is null) return false;
1061 synchronized(TPInfo
.classinfo
) {
1062 foreach (TPInfo ti
; allProtos
) if (ti
.self
[] == self
[]) { tpi
= ti
; break; }
1069 } catch (Exception e
) {
1071 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e
.toString
);
1072 } catch (Exception e
) {}
1081 // returns `true` if found and executed without errors
1082 bool doWithLockedTP (ToxP tox
, scope void delegate (TPInfo ti
) dg
) nothrow {
1084 if (tox
is null || dg
is null) return false;
1085 synchronized(TPInfo
.classinfo
) {
1086 foreach (TPInfo ti
; allProtos
) if (ti
.tox
is tox
) { tpi
= ti
; break; }
1093 } catch (Exception e
) {
1095 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e
.toString
);
1096 } catch (Exception e
) {}
1105 // ////////////////////////////////////////////////////////////////////////// //
1106 // toxcore callbacks
1108 // self connection state was changed
1109 static extern(C
) void connectionCB (Tox
* tox
, TOX_CONNECTION status
, void* udata
) nothrow {
1110 if (toxCoreSendEvent
is null) return;
1112 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1113 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1114 conwriteln("TOX(", ti
.nick
, "): ", (status
!= TOX_CONNECTION_NONE ?
"" : "dis"), "connected.");
1116 tcmsg
= new ToxEventConnection(self
, self
, status
!= TOX_CONNECTION_NONE
);
1118 toxCoreSendEvent(tcmsg
);
1122 // friend connection state was changed
1123 static extern(C
) void friendConnectionCB (Tox
* tox
, uint frnum
, TOX_CONNECTION status
, void* udata
) nothrow {
1124 if (toxCoreSendEvent
is null) return;
1126 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1127 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1128 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1129 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " ", (status
!= TOX_CONNECTION_NONE ?
"" : "dis"), "connected.");
1131 tcmsg
= new ToxEventConnection(self
, who
, status
!= TOX_CONNECTION_NONE
);
1132 if (status
!= TOX_CONNECTION_NONE
) ti
.needSave
= true;
1134 toxCoreSendEvent(tcmsg
);
1138 static extern(C
) void friendStatusCB (Tox
* tox
, uint frnum
, TOX_USER_STATUS status
, void* udata
) nothrow {
1139 if (toxCoreSendEvent
is null) return;
1141 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1142 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1143 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1144 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed status to ", status
);
1146 ContactStatus cst
= ContactStatus
.Offline
;
1148 case TOX_USER_STATUS_NONE
: cst
= ContactStatus
.Online
; break;
1149 case TOX_USER_STATUS_AWAY
: cst
= ContactStatus
.Away
; break;
1150 case TOX_USER_STATUS_BUSY
: cst
= ContactStatus
.Busy
; break;
1153 tcmsg
= new ToxEventStatus(self
, who
, cst
);
1155 toxCoreSendEvent(tcmsg
);
1159 static extern(C
) void friendNameCB (Tox
* tox
, uint frnum
, const(char)* name
, usize length
, void* udata
) nothrow {
1160 if (toxCoreSendEvent
is null) return;
1162 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1163 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1164 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1165 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed name to <", name
[0..length
], ">");
1167 tcmsg
= new ToxEventNick(self
, who
, name
[0..length
].idup
);
1170 toxCoreSendEvent(tcmsg
);
1174 static extern(C
) void friendStatusMessageCB (Tox
* tox
, uint frnum
, const(char)* msg
, usize msglen
, void* user_data
) nothrow {
1175 if (toxCoreSendEvent
is null) return;
1177 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1178 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1179 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1180 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " changed status to <", msg
[0..msglen
], ">");
1182 tcmsg
= new ToxEventStatusMsg(self
, who
, msg
[0..msglen
].idup
);
1185 toxCoreSendEvent(tcmsg
);
1189 static extern(C
) void friendReqCB (Tox
* tox
, const(ubyte)* pk
, const(char)* msg
, usize msglen
, void* udata
) nothrow {
1190 if (toxCoreSendEvent
is null) return;
1192 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1193 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1194 PubKey who
; who
[] = pk
[0..PubKey
.length
];
1195 conwriteln("TOX(", ti
.nick
, "): friend request comes: <", msg
[0..msglen
], ">");
1197 tcmsg
= new ToxEventFriendReq(self
, who
, msg
[0..msglen
].idup
);
1200 toxCoreSendEvent(tcmsg
);
1204 static extern(C
) void friendMsgCB (Tox
* tox
, uint frnum
, TOX_MESSAGE_TYPE type
, const(char)* msg
, usize msglen
, void* udata
) nothrow {
1205 if (toxCoreSendEvent
is null) return;
1207 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1208 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1209 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1210 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " sent a message.");
1212 tcmsg
= new ToxEventMessage(self
, who
, (type
== TOX_MESSAGE_TYPE_ACTION
), msg
[0..msglen
].idup
);
1214 toxCoreSendEvent(tcmsg
);
1218 static extern(C
) void friendReceiptCB (Tox
* tox
, uint frnum
, uint msgid
, void* udata
) nothrow {
1219 if (toxCoreSendEvent
is null) return;
1221 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1222 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1223 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1224 conwriteln("TOX(", ti
.nick
, "): friend #", frnum
, " acked message #", msgid
);
1226 tcmsg
= new ToxEventMessageAck(self
, who
, (cast(long)msgid
)+1);
1228 toxCoreSendEvent(tcmsg
);
1232 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 {
1233 if (toxCoreSendEvent
is null) return;
1234 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1235 //PubKey self; tox_self_get_public_key(tox, self.ptr);
1236 //PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return false; // wtf?!
1239 foreach (ref TPInfo
.FileRejectEvent fre
; ti
.rej
) if (fre
.frnum
== frnum
&& fre
.flnum
== flnum
) { found
= true; break; }
1240 if (!found
) ti
.rej
~= TPInfo
.FileRejectEvent(frnum
, flnum
);
1244 if (kind == TOX_FILE_KIND_AVATAR) {
1245 if (flsize < 16 || flsize > 1024*1024) {
1246 timp.addRejectEvent(frnum, flnum);
1250 timp.addRejectEvent(frnum, flnum);
1252 tox_file_control(tox, rej.frnum, rej.flnum, TOX_FILE_CONTROL_CANCEL, null);
1257 static extern(C
) void fileRecvCtlCB (Tox
* tox
, uint frnum
, uint flnum
, TOX_FILE_CONTROL ctl
, void* udata
) nothrow {
1259 auto timp = findTP(tox);
1260 if (timp is null) return;
1262 if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return false; // wtf?!
1264 conprintfln("got recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1266 synchronized(timp) {
1267 timp.friendMessageAck(who, msgid);
1270 } catch (Exception e) {
1271 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1274 auto timp = *cast(Account*)&udata;
1275 if (timp is null || timp.tox is null) return;
1277 final switch (ctl) {
1278 case TOX_FILE_CONTROL_RESUME: /*timp.resumeSendByNums(frnum, flnum);*/ break;
1279 case TOX_FILE_CONTROL_PAUSE: /*timp.pauseSendByNums(frnum, flnum);*/ break;
1280 case TOX_FILE_CONTROL_CANCEL: /*timp.abortSendByNums(frnum, flnum);*/ break;
1282 } catch (Exception e) {
1284 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1286 } catch (Exception) {}
1292 static extern(C
) void fileChunkReqCB (Tox
* tox
, uint frnum
, uint flnum
, ulong pos
, usize len
, void* udata
) nothrow {
1293 if (toxCoreSendEvent
is null) return;
1294 if (!tox
.doWithLockedTP(delegate (TPInfo ti
) {
1295 PubKey self
; tox_self_get_public_key(tox
, self
.ptr
);
1296 PubKey who
; if (!tox_friend_get_public_key(tox
, frnum
, who
.ptr
)) return; // wtf?!
1299 foreach (ref TPInfo
.FileRejectEvent fre
; ti
.rej
) if (fre
.frnum
== frnum
&& fre
.flnum
== flnum
) { found
= true; break; }
1300 if (!found
) ti
.rej
~= TPInfo
.FileRejectEvent(frnum
, flnum
);
1303 //logwritefln("got chunk req recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1306 timp.fschunks ~= ChunkToSend(frnum, flnum, pos, len);
1307 } catch (Exception e) {
1309 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1310 timp.dropNetIOConnection();
1311 } catch (Exception) {}
1317 // ////////////////////////////////////////////////////////////////////////// //
1318 void bootstrap(string mode
="any") (TPInfo ti
) nothrow if (mode
== "udp" || mode
== "tcp" || mode
== "any") {
1319 if (ti
is null || ti
.tox
is null) return;
1321 ToxBootstrapServer
[] loadBootNodes () nothrow {
1323 auto bfname
= buildPath(ti
.toxDataDiskName
.dirName
, "tox_bootstrap.rc");
1324 ToxBootstrapServer
[] bootnodes
= null;
1326 bootnodes
.txtunser(VFile(bfname
));
1327 if (bootnodes
.length
> 0) return bootnodes
;
1328 } catch (Exception e
) {}
1331 bootnodes
= tox_download_bootstrap_list();
1332 if (bootnodes
.length
> 0) {
1334 bootnodes
.txtser(VFile(bfname
, "w"));
1335 } catch (Exception e
) {}
1338 } catch (Exception e
) {}
1342 conprintfln("Tox: loading bootstrap nodes...");
1343 auto nodes
= loadBootNodes();
1344 conprintfln("Tox: %s nodes loaded", nodes
.length
);
1345 if (nodes
.length
== 0) return;
1346 foreach (const ref ToxBootstrapServer srv
; nodes
) {
1347 if (srv
.ipv4
.length
< 2) continue;
1348 assert(srv
.ipv4
[$-1] == 0);
1349 //conprintfln(" node ip: %s:%u (maintainer: %s)", srv.ipv4[0..$-1], srv.port, srv.maintainer);
1350 static if (mode
== "udp") {
1352 tox_bootstrap(ti
.tox
, srv
.ipv4
.ptr
, srv
.port
, srv
.pubkey
.ptr
, null);
1354 } else static if (mode
== "tcp") {
1356 foreach (immutable ushort port
; srv
.tcpports
) {
1357 tox_add_tcp_relay(ti
.tox
, srv
.ipv4
.ptr
, port
, srv
.pubkey
.ptr
, null);
1362 tox_bootstrap(ti
.tox
, srv
.ipv4
.ptr
, srv
.port
, srv
.pubkey
.ptr
, null);
1363 } else if (srv
.udp
) {
1364 foreach (immutable ushort port
; srv
.tcpports
) {
1365 tox_add_tcp_relay(ti
.tox
, srv
.ipv4
.ptr
, port
, srv
.pubkey
.ptr
, null);
1369 tox_iterate(ti
.tox
, null);
1371 //conprintfln("Tox[%s]: %s nodes added", srvalias, nodes.length);
1375 // ////////////////////////////////////////////////////////////////////////// //
1376 // returns `null` if there is no such file or file cannot be loaded
1377 ubyte[] loadToxCoreData (const(char)[] toxdatafname
) nothrow {
1378 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, rename
, fread
, ferror
, fseek
, ftell
, SEEK_SET
, SEEK_END
;
1379 import core
.stdc
.stdlib
: malloc
, free
;
1380 import core
.sys
.posix
.unistd
: unlink
;
1382 if (toxdatafname
.length
== 0) return null;
1384 static char* namebuf
= null;
1385 static uint nbsize
= 0;
1387 if (nbsize
< toxdatafname
.length
+1024) {
1388 import core
.stdc
.stdlib
: realloc
;
1389 nbsize
= cast(uint)toxdatafname
.length
+1024;
1390 namebuf
= cast(char*)realloc(namebuf
, nbsize
);
1391 if (namebuf
is null) assert(0, "out of memory");
1394 auto origName
= expandTilde(namebuf
[0..nbsize
-6], toxdatafname
);
1395 if (origName
== null) return null; // oops
1396 origName
.ptr
[origName
.length
] = 0; // zero-terminate
1398 FILE
* fi
= fopen(namebuf
, "r");
1399 if (fi
is null) return null;
1400 scope(exit
) fclose(fi
);
1403 if (fseek(fi
, 0, SEEK_END
) == -1) {
1404 import core
.stdc
.errno
;
1405 if (errno
== EINTR
) continue;
1415 import core
.stdc
.errno
;
1416 if (errno
== EINTR
) continue;
1422 if (fsize
> 1024*1024*256) { conwriteln("toxcore data file too big"); return null; }
1423 if (fsize
== 0) return null; // it cannot be zero-sized
1426 if (fseek(fi
, 0, SEEK_SET
) == -1) {
1427 import core
.stdc
.errno
;
1428 if (errno
== EINTR
) continue;
1434 auto res
= new ubyte[](cast(int)fsize
);
1435 conwriteln("loading toxcore data; size=", fsize
);
1438 while (left
.length
> 0) {
1439 auto rd
= fread(left
.ptr
, 1, left
.length
, fi
);
1440 if (rd
== 0) { delete res
; return null; } // error
1441 if (rd
< cast(int)left
.length
) {
1442 if (!ferror(fi
)) { delete res
; return null; } // error
1443 import core
.stdc
.errno
;
1444 if (errno
!= EINTR
) { delete res
; return null; } // error
1445 if (rd
> 0) left
= left
[rd
..$];
1448 if (rd
> left
.length
) { delete res
; return null; } // error
1452 if (left
.length
) { delete res
; return null; } // error
1458 bool saveToxCoreData (ToxP tox
, const(char)[] toxdatafname
) nothrow {
1459 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, rename
, fwrite
;
1460 import core
.stdc
.stdlib
: malloc
, free
;
1461 import core
.sys
.posix
.unistd
: unlink
;
1462 //import std.internal.cstring;
1464 if (tox
is null || toxdatafname
.length
== 0) return false;
1466 auto size
= tox_get_savedata_size(tox
);
1467 if (size
> int.max
/8) return false; //throw new Exception("save data too big");
1469 char* savedata
= cast(char*)malloc(size
);
1470 if (savedata
is null) return false;
1471 scope(exit
) if (savedata
!is null) free(savedata
);
1473 tox_get_savedata(tox
, savedata
);
1474 conwriteln("save toxcore data; size=", size
);
1476 static char* namebuf
= null;
1477 static char* namebuf1
= null;
1478 static uint nbsize
= 0;
1480 if (nbsize
< toxdatafname
.length
+1024) {
1481 import core
.stdc
.stdlib
: realloc
;
1482 nbsize
= cast(uint)toxdatafname
.length
+1024;
1483 namebuf
= cast(char*)realloc(namebuf
, nbsize
);
1484 if (namebuf
is null) assert(0, "out of memory");
1485 namebuf1
= cast(char*)realloc(namebuf1
, nbsize
);
1486 if (namebuf1
is null) assert(0, "out of memory");
1489 auto origName
= expandTilde(namebuf
[0..nbsize
-6], toxdatafname
);
1490 if (origName
== null) return false; // oops
1491 origName
.ptr
[origName
.length
] = 0; // zero-terminate
1493 // create temporary name
1494 namebuf1
[0..origName
.length
] = origName
[];
1495 namebuf1
[origName
.length
..origName
.length
+5] = ".$$$\x00";
1497 FILE
* fo
= fopen(namebuf1
, "w");
1498 if (fo
is null) return false;
1500 auto left
= savedata
[0..size
];
1501 while (left
.length
> 0) {
1502 auto wr
= fwrite(left
.ptr
, 1, left
.length
, fo
);
1503 if (wr
< cast(int)left
.length
) {
1504 import core
.stdc
.errno
;
1505 if (errno
== EINTR
) {
1506 if (wr
> 0) left
= left
[wr
..$];
1511 if (wr
> left
.length
) break; // wtf?!
1524 if (rename(namebuf1
, namebuf
) != 0) {
1533 version(ToxCoreDebug
) {
1534 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 {
1535 static inout(char)[] sz (inout(char)* s
) nothrow @trusted @nogc {
1536 if (s
is null) return null;
1538 while (s
[idx
]) ++idx
;
1539 return cast(inout(char)[])(s
[0..idx
]);
1542 conwriteln("level=", level
, "; file=", sz(file
), ":", line
, "; func=", sz(func
), "; msg=", sz(message
));
1547 // ////////////////////////////////////////////////////////////////////////// //
1548 // returns `false` if can't open/create
1549 ToxP
toxCreateInstance (const(char)[] toxdatafname
, in ref ProtoOptions protoOpts
, bool allowNew
, bool* wasCreated
=null) nothrow {
1550 if (wasCreated
!is null) *wasCreated
= false;
1552 if (toxdatafname
.length
== 0) return null;
1555 char* toxProxyHost
= null;
1557 import core
.stdc
.stdlib
: free
;
1558 if (toxProxyHost
!is null) free(toxProxyHost
);
1561 scope(exit
) delete savedata
;
1563 auto toxOpts
= tox_options_new();
1564 assert(toxOpts
!is null);
1565 scope(exit
) if (toxOpts
!is null) tox_options_free(toxOpts
);
1566 tox_options_default(toxOpts
);
1568 bool createNewToxCoreAccount
= false;
1569 toxOpts
.tox_options_set_ipv6_enabled(protoOpts
.ipv6
);
1570 toxOpts
.tox_options_set_udp_enabled(protoOpts
.udp
);
1571 toxOpts
.tox_options_set_local_discovery_enabled(protoOpts
.localDiscovery
);
1572 toxOpts
.tox_options_set_hole_punching_enabled(protoOpts
.holePunching
);
1573 toxOpts
.tox_options_set_start_port(protoOpts
.startPort
);
1574 toxOpts
.tox_options_set_end_port(protoOpts
.endPort
);
1575 toxOpts
.tox_options_set_tcp_port(protoOpts
.tcpPort
);
1577 toxOpts
.tox_options_set_proxy_type(protoOpts
.proxyType
);
1578 if (protoOpts
.proxyType
!= TOX_PROXY_TYPE_NONE
) {
1579 import core
.stdc
.stdlib
: malloc
;
1580 toxOpts
.tox_options_set_proxy_port(protoOpts
.proxyPort
);
1581 // create proxy address string
1582 toxProxyHost
= cast(char*)malloc(protoOpts
.proxyAddr
.length
+1);
1583 if (toxProxyHost
is null) assert(0, "out of memory");
1584 toxProxyHost
[0..protoOpts
.proxyAddr
.length
] = protoOpts
.proxyAddr
[];
1585 toxProxyHost
[protoOpts
.proxyAddr
.length
] = 0;
1586 toxOpts
.tox_options_set_proxy_host(toxProxyHost
);
1589 savedata
= loadToxCoreData(toxdatafname
);
1590 if (savedata
is null) {
1591 // create new tox instance
1592 if (wasCreated
!is null) *wasCreated
= true;
1593 if (!allowNew
) return null;
1594 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_NONE
);
1595 conwriteln("creating new ToxCore account...");
1598 conwriteln("setting ToxCore account data (", savedata
.length
, " bytes)");
1599 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_TOX_SAVE
);
1600 toxOpts
.tox_options_set_savedata_length(savedata
.length
);
1601 toxOpts
.tox_options_set_savedata_data(savedata
.ptr
, savedata
.length
);
1603 scope(exit
) delete savedata
;
1605 version(ToxCoreDebug
) {
1606 toxOpts
.tox_options_set_log_callback(&toxCoreLogCB
);
1609 // create tox instance
1611 ToxP tox
= tox_new(toxOpts
, &error
);
1613 conwriteln("cannot create ToxCore instance: error is ", error
);
1618 if (createNewToxCoreAccount) {
1620 if (info.nick.length > 0) {
1621 auto nc = info.nick;
1622 if (nc.length > TOX_MAX_NAME_LENGTH) nc = nc[0..TOX_MAX_NAME_LENGTH];
1623 tox_self_set_name(tox, nc.ptr, nc.length);
1628 auto nsz = tox_self_get_name_size(tox);
1630 if (nsz > TOX_MAX_NAME_LENGTH) nsz = TOX_MAX_NAME_LENGTH;
1631 char[TOX_MAX_NAME_LENGTH] xbuf = 0;
1632 tox_self_get_name(tox, xbuf.ptr);
1634 foreach (immutable idx, char ch; xbuf[]) {
1635 if (ch == 0) { info.nick = xbuf[0..idx].idup; break; }
1638 info.nick = xbuf[0..nsz].idup;
1643 tox_callback_self_connection_status(tox
, &connectionCB
);
1645 tox_callback_friend_name(tox
, &friendNameCB
);
1646 tox_callback_friend_status_message(tox
, &friendStatusMessageCB
);
1647 tox_callback_friend_status(tox
, &friendStatusCB
);
1648 tox_callback_friend_connection_status(tox
, &friendConnectionCB
);
1649 //tox_callback_friend_typing(tox, &friendTypingCB);
1650 tox_callback_friend_read_receipt(tox
, &friendReceiptCB
);
1651 tox_callback_friend_request(tox
, &friendReqCB
);
1652 tox_callback_friend_message(tox
, &friendMsgCB
);
1654 tox_callback_file_recv_control(tox
, &fileRecvCtlCB
);
1655 tox_callback_file_chunk_request(tox
, &fileChunkReqCB
);
1656 tox_callback_file_recv(tox
, &fileRecvCB
);
1657 //tox_callback_file_recv_chunk
1659 //tox_callback_conference_invite
1660 //tox_callback_conference_message
1661 //tox_callback_conference_title
1662 //tox_callback_conference_namelist_change
1664 //tox_callback_friend_lossy_packet
1665 //tox_callback_friend_lossless_packet
1671 // ////////////////////////////////////////////////////////////////////////// //
1672 // ////////////////////////////////////////////////////////////////////////// //
1674 enum ToxCoreDataId
= 0x15ed1b1fU
;
1676 enum ToxCoreChunkTypeHi
= 0x01ceU
;
1677 enum ToxCoreChunkNoSpamKeys
= 1;
1678 enum ToxCoreChunkDHT
= 2;
1679 enum ToxCoreChunkFriends
= 3;
1680 enum ToxCoreChunkName
= 4;
1681 enum ToxCoreChunkStatusMsg
= 5;
1682 enum ToxCoreChunkStatus
= 6;
1683 enum ToxCoreChunkTcpRelay
= 10;
1684 enum ToxCoreChunkPathNode
= 11;
1685 enum ToxCoreChunkEnd
= 255;
1688 struct ToxSavedFriend
{
1689 enum TOTAL_DATA_SIZE
= 2216;
1690 enum CRYPTO_PUBLIC_KEY_SIZE
= 32;
1691 enum CRYPTO_SECRET_KEY_SIZE
= 32;
1692 enum SAVED_FRIEND_REQUEST_SIZE
= 1024;
1693 enum MAX_NAME_LENGTH
= 128;
1694 enum MAX_STATUSMESSAGE_LENGTH
= 1007;
1696 enum Status
: ubyte {
1704 enum UserStatus
: ubyte {
1712 ubyte[CRYPTO_PUBLIC_KEY_SIZE
] real_pk
;
1713 char[SAVED_FRIEND_REQUEST_SIZE
] info
; // the data that is sent during the friend requests we do
1715 ushort info_size
; // length of the info
1716 char[MAX_NAME_LENGTH
] name
;
1718 char[MAX_STATUSMESSAGE_LENGTH
] statusmessage
;
1720 ushort statusmessage_length
;
1721 UserStatus userstatus
;
1723 uint friendrequest_nospam
;
1724 ulong last_seen_time
;
1726 void load (VFile fl
) {
1727 // we can use CTFE here, but meh...
1728 status
= cast(Status
)fl
.readNum
!ubyte;
1729 if (status
> Status
.max
) throw new ProtocolException("invalid friend status");
1730 fl
.rawReadExact(real_pk
[]);
1731 fl
.rawReadExact(info
[]);
1732 /*pad0 =*/ fl
.readNum
!ubyte;
1733 info_size
= fl
.readNum
!(ushort, "BE");
1734 if (info_size
> info
.length
) throw new ProtocolException("invalid friend data");
1735 fl
.rawReadExact(name
[]);
1736 name_length
= fl
.readNum
!(ushort, "BE");
1737 if (name_length
> name
.length
) throw new ProtocolException("invalid friend data");
1738 fl
.rawReadExact(statusmessage
[]);
1739 /*pad1 =*/ fl
.readNum
!ubyte;
1740 statusmessage_length
= fl
.readNum
!(ushort, "BE");
1741 if (statusmessage_length
> statusmessage
.length
) throw new ProtocolException("invalid friend data");
1742 userstatus
= cast(UserStatus
)fl
.readNum
!ubyte;
1743 if (userstatus
> UserStatus
.max
) throw new ProtocolException("invalid friend userstatus");
1744 /*pad30 =*/ fl
.readNum
!ubyte;
1745 /*pad31 =*/ fl
.readNum
!ubyte;
1746 /*pad32 =*/ fl
.readNum
!ubyte;
1747 friendrequest_nospam
= fl
.readNum
!uint;
1748 last_seen_time
= fl
.readNum
!(ulong, "BE");