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