use DIY tox data file reader to get initial friend list
[bioacid.git] / toxproto.d
blob7c1a1fcc147995abd58d53f055f758212ac4786d
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;
23 private:
25 import core.time;
27 import std.concurrency;
28 import std.datetime;
30 import iv.cmdcon;
31 import iv.cmdcon.gl;
32 import iv.strex;
33 import iv.tox;
34 import iv.txtser;
35 import iv.unarray;
36 import iv.utfutil;
37 import iv.vfs;
38 import iv.vfs.util;
40 import accdb;
42 //version = ToxCoreDebug;
43 version = ToxCoreUseBuiltInDataFileParser;
45 static assert(TOX_PUBLIC_KEY_SIZE == ToxSavedFriend.CRYPTO_PUBLIC_KEY_SIZE);
47 import iv.vfs;
48 import accdb : ContactStatus, ContactInfo;
51 // ////////////////////////////////////////////////////////////////////////// //
52 public:
55 ///
56 struct ToxCoreDataFile {
57 string nick; // user nick
58 string statusmsg;
59 ubyte[TOX_PUBLIC_KEY_SIZE] pubkey = toxCoreEmptyKey;
60 ToxAddr addr = toxCoreEmptyAddr;
61 uint nospam;
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;
75 ToxAddr res = void;
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]);
79 return res[];
84 // ////////////////////////////////////////////////////////////////////////// //
85 ///
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
97 class ToxEventBase {
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 {
106 bool connected;
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; }
118 // nick changed
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 {
134 bool typing;
135 nothrow @trusted @nogc:
136 this (in ref PubKey aself, in ref PubKey awho, bool atyping) { super(aself, awho); typing = atyping; }
139 // new message comes
140 class ToxEventMessage : ToxEventBase {
141 bool action; // is this an "action" message? (/me)
142 string message;
143 SysTime time;
145 this (in ref PubKey aself, in ref PubKey awho, bool aaction, string amessage) {
146 super(aself, awho);
147 action = aaction;
148 message = amessage;
149 time = Clock.currTime;
152 this (in ref PubKey aself, in ref PubKey awho, bool aaction, string amessage, SysTime atime) {
153 super(aself, awho);
154 action = aaction;
155 message = amessage;
156 time = atime;
160 // send message ack comes
161 class ToxEventMessageAck : ToxEventBase {
162 long msgid;
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 () {
179 int acksLeft = 0;
180 synchronized(TPInfo.classinfo) {
181 foreach (TPInfo ti; allProtos) {
182 ti.tid.send(TrdCmdQuit.init);
183 ++acksLeft;
186 while (acksLeft > 0) {
187 receive(
188 (TrdCmdQuitAck ack) { --acksLeft; },
189 (Variant v) {},
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 {
223 ToxCoreDataFile res;
224 try {
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");
230 while (sz >= 8) {
231 auto len = fl.readNum!uint;
232 auto id = fl.readNum!uint;
233 if (id>>16 != ToxCoreChunkTypeHi) throw new ProtocolException("invalid chunk hitype");
234 id &= 0xffffU;
235 switch (id) {
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[]);
242 privkey[] = 0;
243 len = 0;
244 res.addr[] = res.buildAddress(res.pubkey, res.nospam);
246 break;
247 case ToxCoreChunkDHT: break;
248 case ToxCoreChunkFriends:
249 if (len%ToxSavedFriend.TOTAL_DATA_SIZE != 0) throw new ProtocolException("invalid contact list");
250 while (len > 0) {
251 ToxSavedFriend fr;
252 auto st = fl.tell;
253 fr.load(fl);
254 st = fl.tell-st;
255 if (st > len || st != ToxSavedFriend.TOTAL_DATA_SIZE) throw new ProtocolException("invalid contact list");
256 len -= cast(uint)st;
257 if (fr.status == ToxSavedFriend.Status.NoFriend) continue;
258 ContactInfo ci;
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?!");
263 // or vice versa?
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;
269 break;
271 if (ci.kind != ContactInfo.Kind.Friend) {
272 ci.statusmsg = fr.info[0..fr.info_size].idup;
273 } else {
274 ci.statusmsg = fr.statusmessage[0..fr.statusmessage_length].idup;
276 ci.nospam = fr.friendrequest_nospam;
277 ci.pubkey[] = fr.real_pk[];
278 res.friends ~= ci;
280 break;
281 case ToxCoreChunkName:
282 if (len) {
283 auto name = new char[](len);
284 fl.rawReadExact(name);
285 len = 0;
286 res.nick = cast(string)name; // it is safe to cast here
288 break;
289 case ToxCoreChunkStatusMsg:
290 if (len) {
291 auto msg = new char[](len);
292 fl.rawReadExact(msg);
293 len = 0;
294 res.statusmsg = cast(string)msg; // it is safe to cast here
296 break;
297 case ToxCoreChunkStatus:
298 if (len == 1) {
299 auto st = fl.readNum!ubyte;
300 if (st < ToxSavedFriend.UserStatus.Invalid) {
301 switch (st) {
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;
308 len = 0;
310 break;
311 case ToxCoreChunkTcpRelay: break;
312 case ToxCoreChunkPathNode: break;
313 default: break;
315 if (id == ToxCoreChunkEnd) break;
316 fl.seek(len, Seek.Cur);
318 } else {
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);
329 // get user name
330 auto nsz = tox_self_get_name_size(tox);
331 if (nsz != 0) {
332 if (nsz > tox_max_name_length()) nsz = tox_max_name_length(); // just in case
333 auto xbuf = new char[](tox_max_name_length());
334 xbuf[] = 0;
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);
340 if (msz != 0) {
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());
343 xbuf[] = 0;
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) {
352 res = res.init;
354 return res;
358 /// returns public key for account with the given data file, or [toxCoreEmptyKey].
359 PubKey toxCoreGetAccountPubKey (const(char)[] toxdatafname) nothrow {
360 version(ToxCoreUseBuiltInDataFileParser) {
361 try {
362 auto data = toxCoreLoadDataFile(VFile(toxdatafname));
363 if (isValidKey(data.pubkey)) return data.pubkey[];
364 } catch (Exception e) {}
365 } else {
366 TPInfo tacc = null;
367 synchronized(TPInfo.classinfo) {
368 foreach (TPInfo ti; allProtos) if (ti.toxDataDiskName == toxdatafname) { tacc = ti; break; }
369 // not found?
370 if (tacc is null) {
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.
377 tox_kill(tox);
378 return self[];
381 if (tacc !is null) {
382 synchronized(tacc) {
383 PubKey self; tox_self_get_public_key(tacc.tox, self.ptr);
384 return self[];
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) {
395 try {
396 auto data = toxCoreLoadDataFile(VFile(toxdatafname));
397 if (isValidKey(data.pubkey)) return data.addr[];
398 } catch (Exception e) {}
399 } else {
400 TPInfo tacc = null;
401 synchronized(TPInfo.classinfo) {
402 foreach (TPInfo ti; allProtos) if (ti.toxDataDiskName == toxdatafname) { tacc = ti; break; }
403 // not found?
404 if (tacc is null) {
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.
411 tox_kill(tox);
412 return addr[];
415 if (tacc !is null) {
416 synchronized(tacc) {
417 ToxAddr res;
418 if (tacc.tox is null) {
419 res[] = tacc.addr[];
420 } else {
421 tox_self_get_address(ti.tox, res.ptr);
423 return res[];
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 {
434 ToxAddr res = 0;
435 doWithLockedTPByKey(self, delegate (ti) {
436 if (ti.tox is null) {
437 res[] = ti.addr[];
438 } else {
439 tox_self_get_address(ti.tox, res.ptr);
442 return res[];
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;
453 return false;
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;
464 return null;
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;
477 try {
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);
498 ti.nick = nick.idup;
500 // set user name
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.
505 tox_kill(tox);
507 synchronized(TPInfo.classinfo) {
508 allProtos ~= ti;
509 startThread(ti);
511 return ti.self[];
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;
526 try {
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) {
538 // manual parsing
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[];
546 ti.nick = data.nick;
547 } else {
548 // use toxcore to parse data file
549 ProtoOptions protoOpts;
550 try {
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);
566 // get user name
567 auto nsz = tox_self_get_name_size(tox);
568 if (nsz != 0) {
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());
572 xbuf[] = 0;
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.
578 tox_kill(tox);
581 synchronized(TPInfo.classinfo) {
582 allProtos ~= ti;
583 startThread(ti);
586 return ti.self[];
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 {
597 try {
598 TPInfo tpi = null;
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);
606 for (;;) {
607 bool done = false;
608 receive(
609 (TrdCmdQuitAck ack) { done = true; },
610 (Variant v) {},
612 if (done) break;
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;
627 break;
632 return true;
633 } catch (Exception e) {
635 return false;
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; }
662 // find friend
663 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
664 if (frnum == uint.max) return; // error
665 // offline?
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);
670 switch (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();
682 return res;
686 /// sets new status.
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
690 bool res = false;
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));
700 res = true;
701 return;
704 ToxP tox;
706 // want to go online
707 if (ti.tox is null) {
708 // need to create toxcore object
709 try {
710 import std.path : absolutePath, dirName;
712 ProtoOptions protoOpts;
713 try {
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);
722 if (tox is null) {
723 conwriteln("can't create ToxCore...");
724 return;
726 TrdCmdSetToxCore cmd;
727 cmd.tox = tox;
728 ti.tid.send(cast(shared)cmd);
729 } catch (Exception e) {
730 return;
732 } else {
733 tox = ti.tox;
735 assert(tox !is null);
737 TOX_USER_STATUS st;
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);
746 ti.needSave = true;
747 res = true;
748 ti.ping();
750 return res;
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;
758 bool res = 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;
763 if (res) ti.ping();
765 return res;
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
773 bool res = false;
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
777 // find friend
778 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
779 if (frnum != uint.max) {
780 // we already has such friend, do nothing
781 res = true;
782 return;
784 frnum = tox_friend_add(ti.tox, dest.ptr, message.ptr, message.length);
785 res = (frnum != uint.max);
786 if (res) ti.needSave = true;
787 if (res) ti.ping();
789 return res;
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 {
796 bool res = false;
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
800 // find friend
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
804 res = true;
805 return;
807 frnum = tox_friend_add_norequest(ti.tox, dest.ptr);
808 res = (frnum != uint.max);
809 if (res) ti.needSave = true;
810 if (res) ti.ping();
812 return res;
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 {
819 bool res = false;
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
823 // find friend
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;
828 if (res) ti.ping();
830 return res;
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 {
837 bool res = false;
838 doWithLockedTPByKey(self, delegate (ti) {
839 if (ti.tox is null) return; // this account is offline
840 // find friend
841 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
842 res = (frnum != uint.max);
844 return res;
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;
859 char[] nick;
860 scope(exit) delete nick;
861 tox_self_get_friend_list(ti.tox, list.ptr);
862 PubKey fpk;
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);
868 try {
869 dg(self, fpk, nick[0..nsz]);
870 } catch (Exception e) {
871 break;
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
884 // find friend
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);
889 try {
890 if (err == 0) res = SysTime.fromUnixTime(ut);
891 } catch (Exception e) {
892 res = SysTime.min;
895 return res;
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
904 try {
905 dg(ti.tox);
906 } catch (Exception e) {}
911 // ////////////////////////////////////////////////////////////////////////// //
912 // private ToxCore protocol implementation details
913 private:
915 class TPInfo {
916 static struct FileRejectEvent {
917 uint frnum;
918 uint flnum;
921 //ToxProtocol pr;
922 string toxDataDiskName;
923 ToxP tox;
924 Tid tid;
925 PubKey self;
926 ToxAddr addr; // will be used if `tox` is `null`
927 string nick;
928 FileRejectEvent[] rej;
929 bool doBootstrap; // do bootstrapping
930 bool needSave;
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) {
952 assert(ti !is null);
953 ti.tid = spawn(&toxCoreThread, thisTid, *cast(immutable(void)**)&ti);
957 // ////////////////////////////////////////////////////////////////////////// //
958 static void toxCoreThread (Tid ownerTid, immutable(void)* tiptr) {
959 uint mswait = 0;
960 MonoTime lastSaveTime = MonoTime.zero;
961 TPInfo ti = *cast(TPInfo*)&tiptr; // HACK!
962 ToxP newTox = null;
963 try {
964 for (;;) {
965 bool doQuit = false;
966 bool doKillTox = false;
968 if (ti.tox !is null && ti.doBootstrap) {
969 conwriteln("TOX(", ti.nick, "): bootstrapping");
970 ti.doBootstrap = false;
971 bootstrap(ti);
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; },
979 (TrdCmdPing cmd) {},
980 (TrdCmdKillToxCore cmd) { doKillTox = true; },
981 (shared TrdCmdSetToxCore cmd) { newTox = cast(ToxP)cmd.tox; },
982 (Variant v) { conwriteln("WUTAFUCK?! "); },
984 if (doQuit) break;
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);
990 ti.tox = newTox;
992 newTox = null;
993 ti.doBootstrap = true;
994 mswait = 1;
995 continue;
998 if (ti.tox is null) { mswait = 0; continue; }
1000 if (doKillTox) {
1001 conwriteln("TOX(", ti.nick, "): killing toxcore pointer");
1002 doKillTox = false;
1003 if (ti.tox !is null) {
1004 tox_kill(ti.tox);
1005 ti.tox = null;
1006 } else {
1007 conwriteln("TOX(", ti.nick, "): wtf?!");
1009 mswait = 0;
1010 continue;
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;
1017 synchronized(ti) {
1018 if (ti.needSave) {
1019 auto ctt = MonoTime.currTime;
1020 if ((ctt-lastSaveTime).total!"minutes" > 1) {
1021 ti.needSave = false;
1022 lastSaveTime = ctt;
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;
1053 return null;
1057 // returns `true` if found and executed without errors
1058 bool doWithLockedTPByKey (in ref PubKey self, scope void delegate (TPInfo ti) dg) nothrow {
1059 TPInfo tpi = null;
1060 if (dg is null) return false;
1061 synchronized(TPInfo.classinfo) {
1062 foreach (TPInfo ti; allProtos) if (ti.self[] == self[]) { tpi = ti; break; }
1064 if (tpi !is null) {
1065 synchronized(tpi) {
1066 try {
1067 dg(tpi);
1068 return true;
1069 } catch (Exception e) {
1070 try {
1071 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e.toString);
1072 } catch (Exception e) {}
1073 return false;
1077 return false;
1081 // returns `true` if found and executed without errors
1082 bool doWithLockedTP (ToxP tox, scope void delegate (TPInfo ti) dg) nothrow {
1083 TPInfo tpi = null;
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; }
1088 if (tpi !is null) {
1089 synchronized(tpi) {
1090 try {
1091 dg(tpi);
1092 return true;
1093 } catch (Exception e) {
1094 try {
1095 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e.toString);
1096 } catch (Exception e) {}
1097 return false;
1101 return false;
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;
1111 ToxEventBase tcmsg;
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);
1117 })) return;
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;
1125 ToxEventBase tcmsg;
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;
1133 })) return;
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;
1140 ToxEventBase tcmsg;
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;
1147 switch (status) {
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;
1151 default: return;
1153 tcmsg = new ToxEventStatus(self, who, cst);
1154 })) return;
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;
1161 ToxEventBase tcmsg;
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);
1168 ti.needSave = true;
1169 })) return;
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;
1176 ToxEventBase tcmsg;
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);
1183 ti.needSave = true;
1184 })) return;
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;
1191 ToxEventBase tcmsg;
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);
1198 ti.needSave = true;
1199 })) return;
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;
1206 ToxEventBase tcmsg;
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);
1213 })) return;
1214 toxCoreSendEvent(tcmsg);
1218 static extern(C) void friendReceiptCB (Tox* tox, uint frnum, uint msgid, void* udata) nothrow {
1219 if (toxCoreSendEvent is null) return;
1220 ToxEventBase tcmsg;
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);
1227 })) return;
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?!
1238 bool found = false;
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);
1241 })) return;
1244 if (kind == TOX_FILE_KIND_AVATAR) {
1245 if (flsize < 16 || flsize > 1024*1024) {
1246 timp.addRejectEvent(frnum, flnum);
1247 return;
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;
1261 PubKey who;
1262 if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return false; // wtf?!
1263 try {
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;
1276 try {
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) {
1283 try {
1284 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1285 timp.fatalError();
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?!
1298 bool found = false;
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);
1301 })) return;
1303 //logwritefln("got chunk req recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1305 try {
1306 timp.fschunks ~= ChunkToSend(frnum, flnum, pos, len);
1307 } catch (Exception e) {
1308 try {
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 {
1322 import std.path;
1323 auto bfname = buildPath(ti.toxDataDiskName.dirName, "tox_bootstrap.rc");
1324 ToxBootstrapServer[] bootnodes = null;
1325 try {
1326 bootnodes.txtunser(VFile(bfname));
1327 if (bootnodes.length > 0) return bootnodes;
1328 } catch (Exception e) {}
1329 // try to download
1330 try {
1331 bootnodes = tox_download_bootstrap_list();
1332 if (bootnodes.length > 0) {
1333 try {
1334 bootnodes.txtser(VFile(bfname, "w"));
1335 } catch (Exception e) {}
1337 return bootnodes;
1338 } catch (Exception e) {}
1339 return null;
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") {
1351 if (srv.udp) {
1352 tox_bootstrap(ti.tox, srv.ipv4.ptr, srv.port, srv.pubkey.ptr, null);
1354 } else static if (mode == "tcp") {
1355 if (srv.tcp) {
1356 foreach (immutable ushort port; srv.tcpports) {
1357 tox_add_tcp_relay(ti.tox, srv.ipv4.ptr, port, srv.pubkey.ptr, null);
1360 } else {
1361 if (srv.udp) {
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);
1402 for (;;) {
1403 if (fseek(fi, 0, SEEK_END) == -1) {
1404 import core.stdc.errno;
1405 if (errno == EINTR) continue;
1406 return null;
1408 break;
1411 long fsize;
1412 for (;;) {
1413 fsize = ftell(fi);
1414 if (fsize == -1) {
1415 import core.stdc.errno;
1416 if (errno == EINTR) continue;
1417 return null;
1419 break;
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
1425 for (;;) {
1426 if (fseek(fi, 0, SEEK_SET) == -1) {
1427 import core.stdc.errno;
1428 if (errno == EINTR) continue;
1429 return null;
1431 break;
1434 auto res = new ubyte[](cast(int)fsize);
1435 conwriteln("loading toxcore data; size=", fsize);
1437 auto left = res;
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..$];
1446 continue;
1448 if (rd > left.length) { delete res; return null; } // error
1449 left = left[rd..$];
1452 if (left.length) { delete res; return null; } // error
1454 return res;
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..$];
1507 continue;
1509 break;
1511 if (wr > left.length) break; // wtf?!
1512 left = left[wr..$];
1515 // error?
1516 if (left.length) {
1517 fclose(fo);
1518 unlink(namebuf1);
1519 return false;
1521 syncFile(fo);
1522 fclose(fo);
1524 if (rename(namebuf1, namebuf) != 0) {
1525 unlink(namebuf1);
1526 return false;
1529 return true;
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;
1537 uint idx = 0;
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;
1554 // create toxcore
1555 char* toxProxyHost = null;
1556 scope(exit) {
1557 import core.stdc.stdlib : free;
1558 if (toxProxyHost !is null) free(toxProxyHost);
1560 ubyte[] savedata;
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...");
1596 } else {
1597 // load data
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
1610 TOX_ERR_NEW error;
1611 ToxP tox = tox_new(toxOpts, &error);
1612 if (tox is null) {
1613 conwriteln("cannot create ToxCore instance: error is ", error);
1614 return null;
1618 if (createNewToxCoreAccount) {
1619 // set user name
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);
1625 saveToxCoreData();
1626 } else {
1627 // get user name
1628 auto nsz = tox_self_get_name_size(tox);
1629 if (nsz != 0) {
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
1667 return tox;
1671 // ////////////////////////////////////////////////////////////////////////// //
1672 // ////////////////////////////////////////////////////////////////////////// //
1673 private:
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 {
1697 NoFriend,
1698 Added,
1699 Requested,
1700 Confirmed,
1701 Online,
1704 enum UserStatus : ubyte {
1705 None,
1706 Away,
1707 Busy,
1708 Invalid,
1711 Status status;
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
1714 //ubyte pad0;
1715 ushort info_size; // length of the info
1716 char[MAX_NAME_LENGTH] name;
1717 ushort name_length;
1718 char[MAX_STATUSMESSAGE_LENGTH] statusmessage;
1719 //ubyte pad1;
1720 ushort statusmessage_length;
1721 UserStatus userstatus;
1722 //ubyte[3] pad2;
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");