change nick and status messages when they are changed ;-); remove extra spaces from...
[bioacid.git] / toxproto.d
blob25180b9c568358459a021440eafc4ec2c03ef35d
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 string buildNormalizedString(bool noteSpacesOnly=false) (const(char)[] s) nothrow {
57 auto anchor = s;
58 if (s.length == 0) return null;
59 s = s.xstrip;
60 if (s.length == 0) { static if (noteSpacesOnly) return "<spaces>"; else return null; }
61 char[] res;
62 res.reserve(s.length);
63 foreach (char ch; s) {
64 if (ch <= ' ') {
65 if (res.length == 0 || res[$-1] > ' ') res ~= ' ';
66 } else {
67 res ~= ch;
70 static if (noteSpacesOnly) { if (res.length == 0) return "<spaces>"; }
71 return cast(string)res; // it is safe to cast here
75 // ////////////////////////////////////////////////////////////////////////// //
76 ///
77 struct ToxCoreDataFile {
78 string nick; // user nick
79 string statusmsg;
80 ubyte[TOX_PUBLIC_KEY_SIZE] pubkey = toxCoreEmptyKey;
81 ToxAddr addr = toxCoreEmptyAddr;
82 uint nospam;
83 ContactStatus status = ContactStatus.Offline;
85 ContactInfo[] friends;
87 @property bool valid () const pure nothrow @safe @nogc => isValidKey(pubkey);
89 static ToxAddr buildAddress (in ref ubyte[TOX_PUBLIC_KEY_SIZE] pubkey, uint nospam) nothrow @trusted @nogc {
90 static ushort calcChecksum (const(ubyte)[] data) nothrow @trusted @nogc {
91 ubyte[2] checksum = 0;
92 foreach (immutable idx, ubyte b; data) checksum[idx%2] ^= b;
93 return *cast(ushort*)checksum.ptr;
96 ToxAddr res = void;
97 res[0..TOX_PUBLIC_KEY_SIZE] = pubkey[];
98 *cast(uint*)(res.ptr+TOX_PUBLIC_KEY_SIZE) = nospam;
99 *cast(ushort*)(res.ptr+TOX_PUBLIC_KEY_SIZE+uint.sizeof) = calcChecksum(res[0..$-2]);
100 return res[];
105 // ////////////////////////////////////////////////////////////////////////// //
107 class ProtocolException : Exception {
108 this (string msg, string file=__FILE__, usize line=__LINE__, Throwable next=null) pure nothrow @safe @nogc {
109 super(msg, file, line, next);
114 // ////////////////////////////////////////////////////////////////////////// //
115 // messages thread sends to [glconCtlWindow].
116 // [who] can be the same as [self] to indicate account state changes
118 class ToxEventBase {
119 PubKey self; // account
120 PubKey who; // can be same as [self]
121 nothrow @trusted @nogc:
122 this (in ref PubKey aself, in ref PubKey awho) { self[] = aself[]; who = awho[]; }
125 // connection state changed
126 class ToxEventConnection : ToxEventBase {
127 bool connected;
128 nothrow @trusted @nogc:
129 this (in ref PubKey aself, in ref PubKey awho, bool aconnected) { super(aself, awho); connected = aconnected; }
132 // online status changed
133 class ToxEventStatus : ToxEventBase {
134 ContactStatus status;
135 nothrow @trusted @nogc:
136 this (in ref PubKey aself, in ref PubKey awho, ContactStatus astatus) { super(aself, awho); status = astatus; }
139 // nick changed
140 class ToxEventNick : ToxEventBase {
141 string nick; // new nick
142 nothrow @trusted @nogc:
143 this (in ref PubKey aself, in ref PubKey awho, string anick) { super(aself, awho); nick = anick; }
146 // status message changed
147 class ToxEventStatusMsg : ToxEventBase {
148 string message; // new message
149 nothrow @trusted @nogc:
150 this (in ref PubKey aself, in ref PubKey awho, string amessage) { super(aself, awho); message = amessage; }
153 // typing status changed
154 class ToxEventTyping : ToxEventBase {
155 bool typing;
156 nothrow @trusted @nogc:
157 this (in ref PubKey aself, in ref PubKey awho, bool atyping) { super(aself, awho); typing = atyping; }
160 // new message comes
161 class ToxEventMessage : ToxEventBase {
162 bool action; // is this an "action" message? (/me)
163 string message;
164 SysTime time;
166 this (in ref PubKey aself, in ref PubKey awho, bool aaction, string amessage) {
167 super(aself, awho);
168 action = aaction;
169 message = amessage;
170 time = systimeNow;
173 this (in ref PubKey aself, in ref PubKey awho, bool aaction, string amessage, SysTime atime) {
174 super(aself, awho);
175 action = aaction;
176 message = amessage;
177 time = atime;
181 // send message ack comes
182 class ToxEventMessageAck : ToxEventBase {
183 long msgid;
184 nothrow @trusted @nogc:
185 this (in ref PubKey aself, in ref PubKey awho, long amsgid) { super(aself, awho); msgid = amsgid; }
188 // friend request comes
189 class ToxEventFriendReq : ToxEventBase {
190 string message; // request message
191 nothrow @trusted @nogc:
192 this (in ref PubKey aself, in ref PubKey awho, string amessage) { super(aself, awho); message = amessage; }
196 // ////////////////////////////////////////////////////////////////////////// //
198 /// shutdown protocol module
199 void toxCoreShutdownAll () {
200 int acksLeft = 0;
201 synchronized(TPInfo.classinfo) {
202 foreach (TPInfo ti; allProtos) {
203 ti.tid.send(TrdCmdQuit.init);
204 ++acksLeft;
207 while (acksLeft > 0) {
208 receive(
209 (TrdCmdQuitAck ack) { --acksLeft; },
210 (Variant v) {},
214 foreach (TPInfo tpi; allProtos) {
215 // save toxcode data, if there is any
216 if (tpi.tox !is null && tpi.toxDataDiskName.length) saveToxCoreData(tpi.tox, tpi.toxDataDiskName);
217 if (tpi.tox !is null) { tox_kill(tpi.tox); tpi.tox = null; }
222 // ////////////////////////////////////////////////////////////////////////// //
225 static immutable PubKey toxCoreEmptyKey = 0;
226 static immutable ToxAddr toxCoreEmptyAddr = 0;
228 /// delegate that will be used to send messages.
229 /// can be called from any thread, so it should be thread-safe, and should avoid deadlocks.
230 /// the delegate should be set on program startup, and should not be changed anymore.
231 /// FIXME: add API to change this!
232 __gshared void delegate (Object msg) nothrow toxCoreSendEvent;
235 bool isValidKey (in ref PubKey key) pure nothrow @safe @nogc => (key[] != toxCoreEmptyKey[]);
238 bool isValidAddr (in ref ToxAddr key) pure nothrow @safe @nogc => (key[] != toxCoreEmptyAddr[]);
241 /// parse toxcore data file.
242 /// sorry, i had to do it manually, 'cause there is no way to open toxcore data without going online.
243 public ToxCoreDataFile toxCoreLoadDataFile (VFile fl) nothrow {
244 ToxCoreDataFile res;
245 try {
246 version(ToxCoreUseBuiltInDataFileParser) {
247 auto sz = fl.size-fl.tell;
248 if (sz < 8) throw new ProtocolException("data file too small");
249 if (fl.readNum!uint != 0) throw new ProtocolException("invalid something");
250 if (fl.readNum!uint != ToxCoreDataId) throw new ProtocolException("not a ToxCore data file");
251 while (sz >= 8) {
252 auto len = fl.readNum!uint;
253 auto id = fl.readNum!uint;
254 if (id>>16 != ToxCoreChunkTypeHi) throw new ProtocolException("invalid chunk hitype");
255 id &= 0xffffU;
256 switch (id) {
257 case ToxCoreChunkNoSpamKeys:
258 if (len == ToxSavedFriend.CRYPTO_PUBLIC_KEY_SIZE+ToxSavedFriend.CRYPTO_SECRET_KEY_SIZE+uint.sizeof) {
259 res.nospam = fl.readNum!uint;
260 fl.rawReadExact(res.pubkey[]);
261 ubyte[ToxSavedFriend.CRYPTO_PUBLIC_KEY_SIZE] privkey;
262 fl.rawReadExact(privkey[]);
263 privkey[] = 0;
264 len = 0;
265 res.addr[] = res.buildAddress(res.pubkey, res.nospam);
267 break;
268 case ToxCoreChunkDHT: break;
269 case ToxCoreChunkFriends:
270 if (len%ToxSavedFriend.TOTAL_DATA_SIZE != 0) throw new ProtocolException("invalid contact list");
271 while (len > 0) {
272 ToxSavedFriend fr;
273 auto st = fl.tell;
274 fr.load(fl);
275 st = fl.tell-st;
276 if (st > len || st != ToxSavedFriend.TOTAL_DATA_SIZE) throw new ProtocolException("invalid contact list");
277 len -= cast(uint)st;
278 if (fr.status == ToxSavedFriend.Status.NoFriend) continue;
279 ContactInfo ci;
280 ci.nick = fr.name[0..fr.name_length].buildNormalizedString!true;
281 ci.lastonlinetime = cast(uint)fr.last_seen_time;
282 final switch (fr.status) {
283 case ToxSavedFriend.Status.NoFriend: assert(0, "wtf?!");
284 // or vice versa?
285 case ToxSavedFriend.Status.Added: ci.kind = ContactInfo.Kind.PengingAuthAccept; break;
286 case ToxSavedFriend.Status.Requested: ci.kind = ContactInfo.Kind.PengingAuthRequest; break;
287 case ToxSavedFriend.Status.Confirmed:
288 case ToxSavedFriend.Status.Online:
289 ci.kind = ContactInfo.Kind.Friend;
290 break;
292 if (ci.kind != ContactInfo.Kind.Friend) {
293 ci.statusmsg = fr.info[0..fr.info_size].buildNormalizedString!true;
294 } else {
295 ci.statusmsg = fr.statusmessage[0..fr.statusmessage_length].buildNormalizedString;
297 ci.nospam = fr.friendrequest_nospam;
298 ci.pubkey[] = fr.real_pk[];
299 res.friends ~= ci;
301 break;
302 case ToxCoreChunkName:
303 if (len) {
304 auto name = new char[](len);
305 fl.rawReadExact(name);
306 len = 0;
307 res.nick = cast(string)name; // it is safe to cast here
309 break;
310 case ToxCoreChunkStatusMsg:
311 if (len) {
312 auto msg = new char[](len);
313 fl.rawReadExact(msg);
314 len = 0;
315 res.statusmsg = cast(string)msg; // it is safe to cast here
317 break;
318 case ToxCoreChunkStatus:
319 if (len == 1) {
320 auto st = fl.readNum!ubyte;
321 if (st < ToxSavedFriend.UserStatus.Invalid) {
322 switch (st) {
323 case ToxSavedFriend.UserStatus.None: res.status = ContactStatus.Online; break;
324 case ToxSavedFriend.UserStatus.Away: res.status = ContactStatus.Away; break;
325 case ToxSavedFriend.UserStatus.Busy: res.status = ContactStatus.Busy; break;
326 default: res.status = ContactStatus.Offline; break;
329 len = 0;
331 break;
332 case ToxCoreChunkTcpRelay: break;
333 case ToxCoreChunkPathNode: break;
334 default: break;
336 if (id == ToxCoreChunkEnd) break;
337 fl.seek(len, Seek.Cur);
339 } else {
340 // nope, not found, try to open account
341 ProtoOptions protoOpts;
342 auto tox = toxCreateInstance(toxdatafname, protoOpts, allowNew:false);
343 if (tox is null) throw new ProtocolException("cannot load toxcore data file");
344 scope(exit) tox_kill(tox); // and immediately kill it, or it will go online. fuck.
346 tox_self_get_public_key(tox, res.pubkey.ptr);
347 tox_self_get_address(tox, res.addr.ptr);
348 res.nospam = tox_self_get_nospam(tox);
350 // get user name
351 auto nsz = tox_self_get_name_size(tox);
352 if (nsz != 0) {
353 if (nsz > tox_max_name_length()) nsz = tox_max_name_length(); // just in case
354 auto xbuf = new char[](tox_max_name_length());
355 xbuf[] = 0;
356 tox_self_get_name(tox, xbuf.ptr);
357 res.nick = cast(string)(xbuf[0..nsz]); // ah, cast it directly here
360 auto msz = tox_self_get_status_message_size(tox);
361 if (msz != 0) {
362 if (msz > tox_max_status_message_length()) msz = tox_max_status_message_length(); // just in case
363 auto xbuf = new char[](tox_max_status_message_length());
364 xbuf[] = 0;
365 tox_self_get_status_message(tox, xbuf.ptr);
366 res.statusmsg = cast(string)(xbuf[0..nsz]); // ah, cast it directly here
369 // TODO: online status, friend list
370 assert(0, "not finished");
372 } catch (Exception e) {
373 res = res.init;
375 return res;
379 /// returns public key for account with the given data file, or [toxCoreEmptyKey].
380 PubKey toxCoreGetAccountPubKey (const(char)[] toxdatafname) nothrow {
381 version(ToxCoreUseBuiltInDataFileParser) {
382 try {
383 auto data = toxCoreLoadDataFile(VFile(toxdatafname));
384 if (isValidKey(data.pubkey)) return data.pubkey[];
385 } catch (Exception e) {}
386 } else {
387 TPInfo tacc = null;
388 synchronized(TPInfo.classinfo) {
389 foreach (TPInfo ti; allProtos) if (ti.toxDataDiskName == toxdatafname) { tacc = ti; break; }
390 // not found?
391 if (tacc is null) {
392 // nope, not found, try to open account
393 ProtoOptions protoOpts;
394 auto tox = toxCreateInstance(toxdatafname, protoOpts, allowNew:false);
395 if (tox is null) return toxCoreEmptyKey;
396 PubKey self; tox_self_get_public_key(tox, self.ptr);
397 // and immediately kill it, or it will go online. fuck.
398 tox_kill(tox);
399 return self[];
402 if (tacc !is null) {
403 synchronized(tacc) {
404 PubKey self; tox_self_get_public_key(tacc.tox, self.ptr);
405 return self[];
409 return toxCoreEmptyKey;
413 /// returns address public key for account with the given data file, or [toxCoreEmptyKey].
414 ToxAddr toxCoreGetAccountAddress (const(char)[] toxdatafname) nothrow {
415 version(ToxCoreUseBuiltInDataFileParser) {
416 try {
417 auto data = toxCoreLoadDataFile(VFile(toxdatafname));
418 if (isValidKey(data.pubkey)) return data.addr[];
419 } catch (Exception e) {}
420 } else {
421 TPInfo tacc = null;
422 synchronized(TPInfo.classinfo) {
423 foreach (TPInfo ti; allProtos) if (ti.toxDataDiskName == toxdatafname) { tacc = ti; break; }
424 // not found?
425 if (tacc is null) {
426 // nope, not found, try to open account
427 ProtoOptions protoOpts;
428 auto tox = toxCreateInstance(toxdatafname, protoOpts, allowNew:false);
429 if (tox is null) return toxCoreEmptyAddr;
430 ToxAddr addr; tox_self_get_address(tox, addr.ptr);
431 // and immediately kill it, or it will go online. fuck.
432 tox_kill(tox);
433 return addr[];
436 if (tacc !is null) {
437 synchronized(tacc) {
438 ToxAddr res;
439 if (tacc.tox is null) {
440 res[] = tacc.addr[];
441 } else {
442 tox_self_get_address(ti.tox, res.ptr);
444 return res[];
448 return toxCoreEmptyAddr;
452 /// returns address that can be given to other people, so they can friend you.
453 /// returns zero-filled array on invalid request.
454 ToxAddr toxCoreGetSelfAddress (in ref PubKey self) nothrow {
455 ToxAddr res = 0;
456 doWithLockedTPByKey(self, delegate (ti) {
457 if (ti.tox is null) {
458 res[] = ti.addr[];
459 } else {
460 tox_self_get_address(ti.tox, res.ptr);
463 return res[];
467 /// checks if we have a working thread for `self`.
468 bool toxCoreIsAccountOpen (in ref PubKey self) nothrow {
469 synchronized(TPInfo.classinfo) {
470 foreach (TPInfo ti; allProtos) {
471 if (ti.self[] == self[]) return true;
474 return false;
478 /// returns nick for the given account, or `null`.
479 string toxCoreGetNick (in ref PubKey self) nothrow {
480 synchronized(TPInfo.classinfo) {
481 foreach (TPInfo ti; allProtos) {
482 if (ti.self[] == self[]) return ti.nick;
485 return null;
489 /// creates new Tox account (or opens old), stores it in data file, returns public key for new account.
490 /// returns [toxCoreEmptyKey] on error.
491 /// TODO: pass protocol options here
492 /// WARNING! don't call this from more than one thread with the same `toxdatafname`
493 PubKey toxCoreCreateAccount (const(char)[] toxdatafname, const(char)[] nick) nothrow {
494 if (toxdatafname.length == 0) return toxCoreEmptyKey;
496 if (nick.length == 0 || nick.length > tox_max_name_length()) return toxCoreEmptyKey;
498 try {
499 import std.path : absolutePath;
501 string toxDataDiskName = toxdatafname.idup.absolutePath;
503 synchronized(TPInfo.classinfo) {
504 foreach (TPInfo ti; allProtos) {
505 if (ti.toxDataDiskName == toxDataDiskName) return ti.self[];
509 ProtoOptions protoOpts;
510 protoOpts.udp = true;
512 auto tox = toxCreateInstance(toxdatafname, protoOpts, allowNew:true);
513 if (tox is null) return toxCoreEmptyKey;
515 auto ti = new TPInfo();
516 ti.toxDataDiskName = toxDataDiskName;
517 tox_self_get_public_key(tox, ti.self.ptr);
518 tox_self_get_address(tox, ti.addr.ptr);
519 ti.nick = nick.idup;
521 // set user name
522 tox_self_set_name(tox, nick.ptr, nick.length);
523 saveToxCoreData(tox, toxDataDiskName);
525 // and immediately kill it, or it will go online. fuck.
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 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
686 if (frnum == uint.max) return; // error
687 // offline?
688 //if (tox_friend_get_connection_status(ti.tox, frnum) == TOX_CONNECTION_NONE) { res = MsgIdOffline; return; }
689 // nope, online; queue the message
690 TOX_ERR_FRIEND_SEND_MESSAGE err = 0;
691 uint msgid = tox_friend_send_message(ti.tox, frnum, tt, msg.ptr, msg.length, &err);
692 switch (err) {
693 case TOX_ERR_FRIEND_SEND_MESSAGE_OK: res = (cast(long)msgid)+1; break;
694 case TOX_ERR_FRIEND_SEND_MESSAGE_NULL: res = MsgIdError; break;
695 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_FOUND: res = MsgIdError; break;
696 case TOX_ERR_FRIEND_SEND_MESSAGE_FRIEND_NOT_CONNECTED: res = MsgIdOffline; break;
697 case TOX_ERR_FRIEND_SEND_MESSAGE_SENDQ: res = MsgIdError; break;
698 case TOX_ERR_FRIEND_SEND_MESSAGE_TOO_LONG: res = MsgIdError; break;
699 case TOX_ERR_FRIEND_SEND_MESSAGE_EMPTY: res = MsgIdError; break;
700 default: res = MsgIdError; break;
702 if (res > 0) ti.ping();
704 return res;
708 /// sets new status.
709 /// returns `false` if `self` is invalid, or some error occured.
710 /// won't send "going offline" events (caller should know this already).
711 bool toxCoreSetStatus (in ref PubKey self, ContactStatus status) nothrow {
712 if (status == ContactStatus.Connecting) return false; // oops
713 bool res = false;
714 bool waitKillAck = false;
715 bool waitCreateAck = false;
717 doWithLockedTPByKey(self, delegate (ti) {
718 // if we're going offline, kill toxcore instance
719 if (status == ContactStatus.Offline) {
720 try {
721 ti.tid.send(cast(shared)TrdCmdKillToxCore(thisTid));
722 waitKillAck = true;
723 res = true;
724 } catch (Exception e) {
725 res = false;
727 return;
730 ToxP tox;
732 // want to go online
733 if (ti.tox is null) {
734 // need to create toxcore object
735 try {
736 import std.path : absolutePath, dirName;
738 ProtoOptions protoOpts;
739 try {
740 protoOpts.txtunser(VFile(ti.toxDataDiskName.dirName~"/proto.rc"));
741 } catch (Exception e) {
742 protoOpts = protoOpts.default;
743 protoOpts.udp = true;
746 conwriteln("creating ToxCore...");
747 tox = toxCreateInstance(ti.toxDataDiskName, protoOpts, allowNew:false);
748 if (tox is null) {
749 conwriteln("can't create ToxCore...");
750 return;
752 ti.tid.send(cast(shared)TrdCmdSetToxCore(tox, thisTid));
753 waitCreateAck = true;
754 } catch (Exception e) {
755 return;
757 } else {
758 tox = ti.tox;
760 assert(tox !is null);
762 TOX_USER_STATUS st;
763 final switch (status) {
764 case ContactStatus.Offline: assert(0, "wtf?!");
765 case ContactStatus.Online: st = TOX_USER_STATUS_NONE; break;
766 case ContactStatus.Away: st = TOX_USER_STATUS_AWAY; break;
767 case ContactStatus.Busy: st = TOX_USER_STATUS_BUSY; break;
768 case ContactStatus.Connecting: assert(0, "wtf?!");
770 tox_self_set_status(tox, st);
771 ti.needSave = true;
772 res = true;
773 ti.ping();
776 if (waitKillAck) {
777 try {
778 receive((TrdCmdKillToxCoreAck cmd) {});
779 conwriteln("got killer ack");
780 } catch (Exception e) {
781 res = false;
785 if (waitCreateAck) {
786 try {
787 receive((TrdCmdSetToxCoreAck cmd) {});
788 conwriteln("got actioneer ack");
789 } catch (Exception e) {
790 res = false;
794 return res;
798 /// sets new status message.
799 /// returns `false` if `self` is invalid, message is too long, or on any other error.
800 bool toxCoreSetStatusMessage (in ref PubKey self, const(char)[] message) nothrow {
801 if (message.length > tox_max_status_message_length()) return false;
802 bool res = false;
803 doWithLockedTPByKey(self, delegate (ti) {
804 if (ti.tox is null) return;
805 res = tox_self_set_status_message(ti.tox, message.ptr, message.length);
806 if (res) ti.needSave = true;
807 if (res) ti.ping();
809 return res;
813 /// sends friend request.
814 /// returns `false` if `self` is invalid, message is too long, or on any other error.
815 bool toxCoreSendFriendRequest (in ref PubKey self, in ref ToxAddr dest, const(char)[] message) nothrow {
816 if (message.length > tox_max_friend_request_length()) return false; // cannot send long requests
817 bool res = false;
818 doWithLockedTPByKey(self, delegate (ti) {
819 if (ti.tox is null) return; // this account is offline
820 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
821 // find friend
822 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
823 if (frnum != uint.max) {
824 // we already befriend it, do nothing
825 res = true;
826 return;
828 frnum = tox_friend_add(ti.tox, dest.ptr, message.ptr, message.length);
829 res = (frnum != uint.max);
830 if (res) ti.needSave = true;
831 if (res) ti.ping();
833 return res;
837 /// unconditionally adds a friend.
838 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
839 bool toxCoreAddFriend (in ref PubKey self, in ref PubKey dest) nothrow {
840 bool res = false;
841 doWithLockedTPByKey(self, delegate (ti) {
842 if (ti.tox is null) return; // this account is offline
843 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
844 // find friend
845 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
846 if (frnum != uint.max) {
847 // we already has such friend, do nothing
848 res = true;
849 return;
851 frnum = tox_friend_add_norequest(ti.tox, dest.ptr);
852 res = (frnum != uint.max);
853 if (res) ti.needSave = true;
854 if (res) ti.ping();
856 return res;
860 /// unconditionally removes a friend.
861 /// returns `false` if `self` is invalid, message is too long, friend not found, or on any other error.
862 bool toxCoreRemoveFriend (in ref PubKey self, in ref PubKey dest) nothrow {
863 bool res = false;
864 doWithLockedTPByKey(self, delegate (ti) {
865 if (ti.tox is null) return; // this account is offline
866 //if (tox_self_get_connection_status(ti.tox) == TOX_CONNECTION_NONE) return; // this account is offline
867 // find friend
868 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
869 if (frnum == uint.max) { res = false; return; } // no such friend
870 res = tox_friend_delete(ti.tox, frnum);
871 if (res) ti.needSave = true;
872 if (res) ti.ping();
874 return res;
878 /// checks if the given accound has a friend with the given pubkey.
879 /// returns `false` if `self` is invalid or offline, or on any other error, or if there is no such friend.
880 bool toxCoreHasFriend (in ref PubKey self, in ref PubKey dest) nothrow {
881 bool res = false;
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 res = (frnum != uint.max);
888 return res;
892 /// calls delegate for each known friend.
893 /// return `true` from delegate to stop.
894 /// WARNING! all background operations are locked, so don't spend too much time in delegate!
895 void toxCoreForEachFriend (in ref PubKey self, scope bool delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] nick) dg) nothrow {
896 if (dg is null) return;
897 doWithLockedTPByKey(self, delegate (ti) {
898 if (ti.tox is null) return; // this account is offline
899 auto frcount = tox_self_get_friend_list_size(ti.tox);
900 if (frcount == 0) return;
901 auto list = new uint[](frcount);
902 scope(exit) delete list;
903 char[] nick;
904 scope(exit) delete nick;
905 tox_self_get_friend_list(ti.tox, list.ptr);
906 PubKey fpk;
907 foreach (immutable fidx, immutable fid; list[]) {
908 if (!tox_friend_get_public_key(ti.tox, fid, fpk.ptr)) continue;
909 auto nsz = tox_friend_get_name_size(ti.tox, fid);
910 if (nsz > nick.length) nick.length = nsz;
911 if (nsz != 0) tox_friend_get_name(ti.tox, fid, nick.ptr);
912 try {
913 dg(self, fpk, nick[0..nsz]);
914 } catch (Exception e) {
915 break;
922 /// returns the time when this friend was seen online.
923 /// returns `SysTime.min` if `self` is invalid, `dest` is invalid, or on any other error.
924 SysTime toxCoreLastSeen (in ref PubKey self, in ref PubKey dest) nothrow {
925 SysTime res = SysTime.min;
926 doWithLockedTPByKey(self, delegate (ti) {
927 if (ti.tox is null) return; // this account is offline
928 // find friend
929 uint frnum = tox_friend_by_public_key(ti.tox, dest.ptr);
930 if (frnum == uint.max) return; // unknown friend
931 TOX_ERR_FRIEND_GET_LAST_ONLINE err;
932 auto ut = tox_friend_get_last_online(ti.tox, frnum, &err);
933 try {
934 if (err == 0) res = SysTime.fromUnixTime(ut);
935 } catch (Exception e) {
936 res = SysTime.min;
939 return res;
943 /// calls delegate with ToxP. ugly name is intentional.
944 void toxCoreCallWithToxP (in ref PubKey self, scope void delegate (ToxP tox) dg) nothrow {
945 if (dg is null) return;
946 doWithLockedTPByKey(self, delegate (ti) {
947 if (ti.tox is null) return; // this account is offline
948 try {
949 dg(ti.tox);
950 } catch (Exception e) {}
955 // ////////////////////////////////////////////////////////////////////////// //
956 // private ToxCore protocol implementation details
957 private:
959 class TPInfo {
960 static struct FileRejectEvent {
961 uint frnum;
962 uint flnum;
965 //ToxProtocol pr;
966 string toxDataDiskName;
967 ToxP tox;
968 Tid tid;
969 PubKey self;
970 ToxAddr addr; // will be used if `tox` is `null`
971 string nick;
972 FileRejectEvent[] rej;
973 bool doBootstrap; // do bootstrapping
974 bool needSave;
976 void ping () nothrow {
977 if (tox is null) return;
978 try { tid.send(TrdCmdPing.init); } catch (Exception e) {}
982 __gshared TPInfo[] allProtos;
985 // ////////////////////////////////////////////////////////////////////////// //
986 struct TrdCmdQuit {} // quit thread loop
987 struct TrdCmdQuitAck {} // quit thread loop
988 struct TrdCmdPing {} // do something
989 struct TrdCmdSetToxCore { ToxP tox; Tid replytid; }
990 struct TrdCmdSetToxCoreAck {}
991 struct TrdCmdKillToxCore { Tid replytid; }
992 struct TrdCmdKillToxCoreAck {}
995 // ////////////////////////////////////////////////////////////////////////// //
996 // this should be called with registered `ti`
997 void startThread (TPInfo ti) {
998 assert(ti !is null);
999 ti.tid = spawn(&toxCoreThread, thisTid, *cast(immutable(void)**)&ti);
1003 // ////////////////////////////////////////////////////////////////////////// //
1004 void clearToxCallbacks (ToxP tox) {
1005 if (tox is null) return;
1007 tox_callback_self_connection_status(tox, null);
1009 tox_callback_friend_name(tox, null);
1010 tox_callback_friend_status_message(tox, null);
1011 tox_callback_friend_status(tox, null);
1012 tox_callback_friend_connection_status(tox, null);
1013 tox_callback_friend_typing(tox, null);
1014 tox_callback_friend_read_receipt(tox, null);
1015 tox_callback_friend_request(tox, null);
1016 tox_callback_friend_message(tox, null);
1018 tox_callback_file_recv_control(tox, null);
1019 tox_callback_file_chunk_request(tox, null);
1020 tox_callback_file_recv(tox, null);
1024 // ////////////////////////////////////////////////////////////////////////// //
1025 static void toxCoreThread (Tid ownerTid, immutable(void)* tiptr) {
1026 uint mswait = 0;
1027 MonoTime lastSaveTime = MonoTime.zero;
1028 TPInfo ti = *cast(TPInfo*)&tiptr; // HACK!
1029 ToxP newTox = null;
1030 Tid ackreptid;
1031 try {
1032 for (;;) {
1033 bool doQuit = false;
1034 bool doKillTox = false;
1036 if (ti.tox !is null && ti.doBootstrap) {
1037 conwriteln("TOX(", ti.nick, "): bootstrapping");
1038 ti.doBootstrap = false;
1039 bootstrap(ti);
1040 mswait = tox_iteration_interval(ti.tox);
1041 if (mswait < 1) mswait = 1;
1042 conwriteln("TOX(", ti.nick, "): bootstrapping complete (mswait=", mswait, ")");
1045 receiveTimeout((mswait ? mswait.msecs : 10.hours),
1046 (TrdCmdQuit cmd) { doQuit = true; },
1047 (TrdCmdPing cmd) {},
1048 (shared TrdCmdSetToxCore cmd) { newTox = cast(ToxP)cmd.tox; ackreptid = cast(Tid)cmd.replytid; },
1049 (shared TrdCmdKillToxCore cmd) { doKillTox = true; ackreptid = cast(Tid)cmd.replytid; },
1050 (Variant v) { conwriteln("WUTAFUCK?! "); },
1052 if (doQuit) break;
1054 if (newTox !is null) {
1055 conwriteln("TOX(", ti.nick, "): got new toxcore pointer");
1056 if (ti.tox !is newTox) {
1057 clearToxCallbacks(ti.tox);
1058 if (ti.tox !is null) tox_kill(ti.tox);
1059 ti.tox = newTox;
1061 newTox = null;
1062 ti.doBootstrap = true;
1063 mswait = 1;
1064 ackreptid.send(TrdCmdSetToxCoreAck.init);
1065 ackreptid = Tid.init;
1066 continue;
1069 if (ti.tox is null) { mswait = 0; continue; }
1071 if (doKillTox) {
1072 conwriteln("TOX(", ti.nick, "): killing toxcore pointer");
1073 doKillTox = false;
1074 if (ti.tox !is null) {
1075 clearToxCallbacks(ti.tox);
1076 if (ti.toxDataDiskName.length) saveToxCoreData(ti.tox, ti.toxDataDiskName);
1077 tox_kill(ti.tox);
1078 ti.tox = null;
1079 } else {
1080 conwriteln("TOX(", ti.nick, "): wtf?!");
1082 mswait = 0;
1083 ackreptid.send(TrdCmdKillToxCoreAck.init);
1084 ackreptid = Tid.init;
1085 continue;
1088 tox_iterate(ti.tox, null);
1089 mswait = tox_iteration_interval(ti.tox);
1090 //conwriteln("TOX(", ti.nick, "): interval is ", mswait);
1091 if (mswait < 1) mswait = 1;
1092 synchronized(ti) {
1093 if (ti.needSave) {
1094 auto ctt = MonoTime.currTime;
1095 if ((ctt-lastSaveTime).total!"minutes" > 1) {
1096 ti.needSave = false;
1097 lastSaveTime = ctt;
1098 saveToxCoreData(ti.tox, ti.toxDataDiskName);
1103 ownerTid.send(TrdCmdQuitAck.init);
1104 } catch (Throwable e) {
1105 // here, we are dead and fucked (the exact order doesn't matter)
1106 import core.stdc.stdlib : abort;
1107 import core.stdc.stdio : fprintf, stderr;
1108 import core.memory : GC;
1109 import core.thread : thread_suspendAll;
1110 GC.disable(); // yeah
1111 thread_suspendAll(); // stop right here, you criminal scum!
1112 auto s = e.toString();
1113 fprintf(stderr, "\n=== FATAL ===\n%.*s\n", cast(uint)s.length, s.ptr);
1114 abort(); // die, you bitch!
1119 // ////////////////////////////////////////////////////////////////////////// //
1120 // find TPInfo object for the given tox handle; used in callbacks
1121 TPInfo findTP (ToxP tox) nothrow {
1122 if (tox is null) return null;
1123 synchronized(TPInfo.classinfo) {
1124 foreach (TPInfo ti; allProtos) {
1125 if (ti.tox is tox) return ti;
1128 return null;
1132 // returns `true` if found and executed without errors
1133 bool doWithLockedTPByKey (in ref PubKey self, scope void delegate (TPInfo ti) dg) nothrow {
1134 TPInfo tpi = null;
1135 if (dg is null) return false;
1136 synchronized(TPInfo.classinfo) {
1137 foreach (TPInfo ti; allProtos) if (ti.self[] == self[]) { tpi = ti; break; }
1139 if (tpi !is null) {
1140 synchronized(tpi) {
1141 try {
1142 dg(tpi);
1143 return true;
1144 } catch (Exception e) {
1145 try {
1146 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e.toString);
1147 } catch (Exception e) {}
1148 return false;
1152 return false;
1156 // returns `true` if found and executed without errors
1157 bool doWithLockedTP (ToxP tox, scope void delegate (TPInfo ti) dg) nothrow {
1158 TPInfo tpi = null;
1159 if (tox is null || dg is null) return false;
1160 synchronized(TPInfo.classinfo) {
1161 foreach (TPInfo ti; allProtos) if (ti.tox is tox) { tpi = ti; break; }
1163 if (tpi !is null) {
1164 synchronized(tpi) {
1165 try {
1166 dg(tpi);
1167 return true;
1168 } catch (Exception e) {
1169 try {
1170 conprintfln("\nTOX CB EXCEPTION: %s\n\n", e.toString);
1171 } catch (Exception e) {}
1172 return false;
1176 return false;
1180 // ////////////////////////////////////////////////////////////////////////// //
1181 // toxcore callbacks
1183 // self connection state was changed
1184 static extern(C) void connectionCB (Tox* tox, TOX_CONNECTION status, void* udata) nothrow {
1185 if (toxCoreSendEvent is null) return;
1186 ToxEventBase tcmsg;
1187 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1188 PubKey self; tox_self_get_public_key(tox, self.ptr);
1189 conwriteln("TOX(", ti.nick, "): ", (status != TOX_CONNECTION_NONE ? "" : "dis"), "connected.");
1191 tcmsg = new ToxEventConnection(self, self, status != TOX_CONNECTION_NONE);
1192 })) return;
1193 toxCoreSendEvent(tcmsg);
1197 // friend connection state was changed
1198 static extern(C) void friendConnectionCB (Tox* tox, uint frnum, TOX_CONNECTION status, void* udata) nothrow {
1199 if (toxCoreSendEvent is null) return;
1200 ToxEventBase tcmsg;
1201 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1202 PubKey self; tox_self_get_public_key(tox, self.ptr);
1203 PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return; // wtf?!
1204 conwriteln("TOX(", ti.nick, "): friend #", frnum, " ", (status != TOX_CONNECTION_NONE ? "" : "dis"), "connected.");
1206 tcmsg = new ToxEventConnection(self, who, status != TOX_CONNECTION_NONE);
1207 if (status != TOX_CONNECTION_NONE) ti.needSave = true;
1208 })) return;
1209 toxCoreSendEvent(tcmsg);
1213 static extern(C) void friendStatusCB (Tox* tox, uint frnum, TOX_USER_STATUS status, void* udata) nothrow {
1214 if (toxCoreSendEvent is null) return;
1215 ToxEventBase tcmsg;
1216 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1217 PubKey self; tox_self_get_public_key(tox, self.ptr);
1218 PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return; // wtf?!
1219 conwriteln("TOX(", ti.nick, "): friend #", frnum, " changed status to ", status);
1221 ContactStatus cst = ContactStatus.Offline;
1222 switch (status) {
1223 case TOX_USER_STATUS_NONE: cst = ContactStatus.Online; break;
1224 case TOX_USER_STATUS_AWAY: cst = ContactStatus.Away; break;
1225 case TOX_USER_STATUS_BUSY: cst = ContactStatus.Busy; break;
1226 default: return;
1228 tcmsg = new ToxEventStatus(self, who, cst);
1229 })) return;
1230 toxCoreSendEvent(tcmsg);
1234 static extern(C) void friendNameCB (Tox* tox, uint frnum, const(char)* name, usize length, void* udata) nothrow {
1235 if (toxCoreSendEvent is null) return;
1236 ToxEventBase tcmsg;
1237 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1238 PubKey self; tox_self_get_public_key(tox, self.ptr);
1239 PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return; // wtf?!
1240 conwriteln("TOX(", ti.nick, "): friend #", frnum, " changed name to <", name[0..length], ">");
1242 tcmsg = new ToxEventNick(self, who, name[0..length].buildNormalizedString!true);
1243 ti.needSave = true;
1244 })) return;
1245 toxCoreSendEvent(tcmsg);
1249 static extern(C) void friendStatusMessageCB (Tox* tox, uint frnum, const(char)* msg, usize msglen, void* user_data) nothrow {
1250 if (toxCoreSendEvent is null) return;
1251 ToxEventBase tcmsg;
1252 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1253 PubKey self; tox_self_get_public_key(tox, self.ptr);
1254 PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return; // wtf?!
1255 conwriteln("TOX(", ti.nick, "): friend #", frnum, " changed status to <", msg[0..msglen], ">");
1257 tcmsg = new ToxEventStatusMsg(self, who, msg[0..msglen].buildNormalizedString);
1258 ti.needSave = true;
1259 })) return;
1260 toxCoreSendEvent(tcmsg);
1264 static extern(C) void friendReqCB (Tox* tox, const(ubyte)* pk, const(char)* msg, usize msglen, void* udata) nothrow {
1265 if (toxCoreSendEvent is null) return;
1266 ToxEventBase tcmsg;
1267 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1268 PubKey self; tox_self_get_public_key(tox, self.ptr);
1269 PubKey who; who[] = pk[0..PubKey.length];
1270 conwriteln("TOX(", ti.nick, "): friend request comes: <", msg[0..msglen], ">");
1272 tcmsg = new ToxEventFriendReq(self, who, msg[0..msglen].idup);
1273 ti.needSave = true;
1274 })) return;
1275 toxCoreSendEvent(tcmsg);
1279 static extern(C) void friendMsgCB (Tox* tox, uint frnum, TOX_MESSAGE_TYPE type, const(char)* msg, usize msglen, void* udata) nothrow {
1280 if (toxCoreSendEvent is null) return;
1281 ToxEventBase tcmsg;
1282 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1283 PubKey self; tox_self_get_public_key(tox, self.ptr);
1284 PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return; // wtf?!
1285 conwriteln("TOX(", ti.nick, "): friend #", frnum, " sent a message.");
1287 tcmsg = new ToxEventMessage(self, who, (type == TOX_MESSAGE_TYPE_ACTION), msg[0..msglen].idup);
1288 })) return;
1289 toxCoreSendEvent(tcmsg);
1293 static extern(C) void friendReceiptCB (Tox* tox, uint frnum, uint msgid, void* udata) nothrow {
1294 if (toxCoreSendEvent is null) return;
1295 ToxEventBase tcmsg;
1296 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1297 PubKey self; tox_self_get_public_key(tox, self.ptr);
1298 PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return; // wtf?!
1299 conwriteln("TOX(", ti.nick, "): friend #", frnum, " acked message #", msgid);
1301 tcmsg = new ToxEventMessageAck(self, who, (cast(long)msgid)+1);
1302 })) return;
1303 toxCoreSendEvent(tcmsg);
1307 static extern(C) void fileRecvCB (Tox* tox, uint frnum, uint flnum, TOX_FILE_KIND kind, ulong flsize, const(char)* name, usize namelen, void* udata) nothrow {
1308 if (toxCoreSendEvent is null) return;
1309 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1310 //PubKey self; tox_self_get_public_key(tox, self.ptr);
1311 //PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return false; // wtf?!
1313 bool found = false;
1314 foreach (ref TPInfo.FileRejectEvent fre; ti.rej) if (fre.frnum == frnum && fre.flnum == flnum) { found = true; break; }
1315 if (!found) ti.rej ~= TPInfo.FileRejectEvent(frnum, flnum);
1316 })) return;
1319 if (kind == TOX_FILE_KIND_AVATAR) {
1320 if (flsize < 16 || flsize > 1024*1024) {
1321 timp.addRejectEvent(frnum, flnum);
1322 return;
1325 timp.addRejectEvent(frnum, flnum);
1327 tox_file_control(tox, rej.frnum, rej.flnum, TOX_FILE_CONTROL_CANCEL, null);
1332 static extern(C) void fileRecvCtlCB (Tox* tox, uint frnum, uint flnum, TOX_FILE_CONTROL ctl, void* udata) nothrow {
1334 auto timp = findTP(tox);
1335 if (timp is null) return;
1336 PubKey who;
1337 if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return false; // wtf?!
1338 try {
1339 conprintfln("got recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1341 synchronized(timp) {
1342 timp.friendMessageAck(who, msgid);
1345 } catch (Exception e) {
1346 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1349 auto timp = *cast(Account*)&udata;
1350 if (timp is null || timp.tox is null) return;
1351 try {
1352 final switch (ctl) {
1353 case TOX_FILE_CONTROL_RESUME: /*timp.resumeSendByNums(frnum, flnum);*/ break;
1354 case TOX_FILE_CONTROL_PAUSE: /*timp.pauseSendByNums(frnum, flnum);*/ break;
1355 case TOX_FILE_CONTROL_CANCEL: /*timp.abortSendByNums(frnum, flnum);*/ break;
1357 } catch (Exception e) {
1358 try {
1359 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1360 timp.fatalError();
1361 } catch (Exception) {}
1367 static extern(C) void fileChunkReqCB (Tox* tox, uint frnum, uint flnum, ulong pos, usize len, void* udata) nothrow {
1368 if (toxCoreSendEvent is null) return;
1369 if (!tox.doWithLockedTP(delegate (TPInfo ti) {
1370 PubKey self; tox_self_get_public_key(tox, self.ptr);
1371 PubKey who; if (!tox_friend_get_public_key(tox, frnum, who.ptr)) return; // wtf?!
1373 bool found = false;
1374 foreach (ref TPInfo.FileRejectEvent fre; ti.rej) if (fre.frnum == frnum && fre.flnum == flnum) { found = true; break; }
1375 if (!found) ti.rej ~= TPInfo.FileRejectEvent(frnum, flnum);
1376 })) return;
1378 //logwritefln("got chunk req recv ctl for friend #%s, file #%s, ctl:%s", frnum, flnum, ctl);
1380 try {
1381 timp.fschunks ~= ChunkToSend(frnum, flnum, pos, len);
1382 } catch (Exception e) {
1383 try {
1384 conprintfln("\nTOX[%s] CB EXCEPTION: %s\n\n", timp.srvalias, e.toString);
1385 timp.dropNetIOConnection();
1386 } catch (Exception) {}
1392 // ////////////////////////////////////////////////////////////////////////// //
1393 void bootstrap(string mode="any") (TPInfo ti) nothrow if (mode == "udp" || mode == "tcp" || mode == "any") {
1394 if (ti is null || ti.tox is null) return;
1396 ToxBootstrapServer[] loadBootNodes () nothrow {
1397 import std.path;
1398 auto bfname = buildPath(ti.toxDataDiskName.dirName, "tox_bootstrap.rc");
1399 ToxBootstrapServer[] bootnodes = null;
1400 try {
1401 bootnodes.txtunser(VFile(bfname));
1402 if (bootnodes.length > 0) return bootnodes;
1403 } catch (Exception e) {}
1404 // try to download
1405 try {
1406 bootnodes = tox_download_bootstrap_list();
1407 if (bootnodes.length > 0) serialize(bootnodes, bfname);
1408 return bootnodes;
1409 } catch (Exception e) {}
1410 return null;
1413 conprintfln("Tox: loading bootstrap nodes...");
1414 auto nodes = loadBootNodes();
1415 conprintfln("Tox: %s nodes loaded", nodes.length);
1416 if (nodes.length == 0) return;
1417 foreach (const ref ToxBootstrapServer srv; nodes) {
1418 if (srv.ipv4.length < 2) continue;
1419 assert(srv.ipv4[$-1] == 0);
1420 //conprintfln(" node ip: %s:%u (maintainer: %s)", srv.ipv4[0..$-1], srv.port, srv.maintainer);
1421 static if (mode == "udp") {
1422 if (srv.udp) {
1423 tox_bootstrap(ti.tox, srv.ipv4.ptr, srv.port, srv.pubkey.ptr, null);
1425 } else static if (mode == "tcp") {
1426 if (srv.tcp) {
1427 foreach (immutable ushort port; srv.tcpports) {
1428 tox_add_tcp_relay(ti.tox, srv.ipv4.ptr, port, srv.pubkey.ptr, null);
1431 } else {
1432 if (srv.udp) {
1433 tox_bootstrap(ti.tox, srv.ipv4.ptr, srv.port, srv.pubkey.ptr, null);
1434 } else if (srv.udp) {
1435 foreach (immutable ushort port; srv.tcpports) {
1436 tox_add_tcp_relay(ti.tox, srv.ipv4.ptr, port, srv.pubkey.ptr, null);
1440 tox_iterate(ti.tox, null);
1442 //conprintfln("Tox[%s]: %s nodes added", srvalias, nodes.length);
1446 // ////////////////////////////////////////////////////////////////////////// //
1447 // returns `null` if there is no such file or file cannot be loaded
1448 ubyte[] loadToxCoreData (const(char)[] toxdatafname) nothrow {
1449 import core.stdc.stdio : FILE, fopen, fclose, rename, fread, ferror, fseek, ftell, SEEK_SET, SEEK_END;
1450 import core.stdc.stdlib : malloc, free;
1451 import core.sys.posix.unistd : unlink;
1453 if (toxdatafname.length == 0) return null;
1455 static char* namebuf = null;
1456 static uint nbsize = 0;
1458 if (nbsize < toxdatafname.length+1024) {
1459 import core.stdc.stdlib : realloc;
1460 nbsize = cast(uint)toxdatafname.length+1024;
1461 namebuf = cast(char*)realloc(namebuf, nbsize);
1462 if (namebuf is null) assert(0, "out of memory");
1465 auto origName = expandTilde(namebuf[0..nbsize-6], toxdatafname);
1466 if (origName == null) return null; // oops
1467 origName.ptr[origName.length] = 0; // zero-terminate
1469 FILE* fi = fopen(namebuf, "r");
1470 if (fi is null) return null;
1471 scope(exit) fclose(fi);
1473 for (;;) {
1474 if (fseek(fi, 0, SEEK_END) == -1) {
1475 import core.stdc.errno;
1476 if (errno == EINTR) continue;
1477 return null;
1479 break;
1482 long fsize;
1483 for (;;) {
1484 fsize = ftell(fi);
1485 if (fsize == -1) {
1486 import core.stdc.errno;
1487 if (errno == EINTR) continue;
1488 return null;
1490 break;
1493 if (fsize > 1024*1024*256) { conwriteln("toxcore data file too big"); return null; }
1494 if (fsize == 0) return null; // it cannot be zero-sized
1496 for (;;) {
1497 if (fseek(fi, 0, SEEK_SET) == -1) {
1498 import core.stdc.errno;
1499 if (errno == EINTR) continue;
1500 return null;
1502 break;
1505 auto res = new ubyte[](cast(int)fsize);
1506 conwriteln("loading toxcore data; size=", fsize);
1508 auto left = res;
1509 while (left.length > 0) {
1510 auto rd = fread(left.ptr, 1, left.length, fi);
1511 if (rd == 0) { delete res; return null; } // error
1512 if (rd < cast(int)left.length) {
1513 if (!ferror(fi)) { delete res; return null; } // error
1514 import core.stdc.errno;
1515 if (errno != EINTR) { delete res; return null; } // error
1516 if (rd > 0) left = left[rd..$];
1517 continue;
1519 if (rd > left.length) { delete res; return null; } // error
1520 left = left[rd..$];
1523 if (left.length) { delete res; return null; } // error
1525 return res;
1529 bool saveToxCoreData (ToxP tox, const(char)[] toxdatafname) nothrow {
1530 import core.stdc.stdio : rename;
1531 import core.stdc.stdlib : malloc, free;
1532 import core.sys.posix.fcntl : open, O_WRONLY, O_CREAT, O_TRUNC;
1533 import core.sys.posix.unistd : unlink, close, write, fdatasync;
1535 if (tox is null || toxdatafname.length == 0) return false;
1537 auto size = tox_get_savedata_size(tox);
1538 if (size > int.max/8) return false; //throw new Exception("save data too big");
1540 char* savedata = cast(char*)malloc(size);
1541 if (savedata is null) return false;
1542 scope(exit) if (savedata !is null) free(savedata);
1544 tox_get_savedata(tox, savedata);
1545 conwriteln("save toxcore data; size=", size);
1547 static char* namebuf = null;
1548 static char* namebuf1 = null;
1549 static uint nbsize = 0;
1551 if (nbsize < toxdatafname.length+1024) {
1552 import core.stdc.stdlib : realloc;
1553 nbsize = cast(uint)toxdatafname.length+1024;
1554 namebuf = cast(char*)realloc(namebuf, nbsize);
1555 if (namebuf is null) assert(0, "out of memory");
1556 namebuf1 = cast(char*)realloc(namebuf1, nbsize);
1557 if (namebuf1 is null) assert(0, "out of memory");
1560 auto origName = expandTilde(namebuf[0..nbsize-6], toxdatafname);
1561 if (origName == null) return false; // oops
1562 origName.ptr[origName.length] = 0; // zero-terminate
1564 // create temporary name
1565 namebuf1[0..origName.length] = origName[];
1566 namebuf1[origName.length..origName.length+5] = ".$$$\x00";
1568 int fo = open(namebuf1, O_WRONLY|O_CREAT|O_TRUNC, 0o600);
1569 if (fo == -1) {
1570 conwriteln("failed to create file: '", origName, ".$$$'");
1571 return false;
1574 auto left = savedata[0..size];
1575 while (left.length > 0) {
1576 auto wr = write(fo, left.ptr, left.length);
1577 if (wr == 0) {
1578 // out of disk space; oops
1579 close(fo);
1580 unlink(namebuf1);
1581 return false;
1583 if (wr < 0) {
1584 import core.stdc.errno;
1585 if (errno == EINTR) continue;
1586 // some other error; oops
1587 close(fo);
1588 unlink(namebuf1);
1589 return false;
1591 if (wr > left.length) {
1592 // wtf?!
1593 close(fo);
1594 unlink(namebuf1);
1595 return false;
1597 left = left[wr..$];
1600 fdatasync(fo);
1601 close(fo);
1603 if (rename(namebuf1, namebuf) != 0) {
1604 unlink(namebuf1);
1605 return false;
1608 return true;
1612 version(ToxCoreDebug) {
1613 static extern(C) void toxCoreLogCB (Tox* tox, TOX_LOG_LEVEL level, const(char)* file, uint line, const(char)* func, const(char)* message, void* user_data) nothrow {
1614 static inout(char)[] sz (inout(char)* s) nothrow @trusted @nogc {
1615 if (s is null) return null;
1616 uint idx = 0;
1617 while (s[idx]) ++idx;
1618 return cast(inout(char)[])(s[0..idx]);
1621 conwriteln("level=", level, "; file=", sz(file), ":", line, "; func=", sz(func), "; msg=", sz(message));
1626 // ////////////////////////////////////////////////////////////////////////// //
1627 // returns `false` if can't open/create
1628 ToxP toxCreateInstance (const(char)[] toxdatafname, in ref ProtoOptions protoOpts, bool allowNew, bool* wasCreated=null) nothrow {
1629 if (wasCreated !is null) *wasCreated = false;
1631 if (toxdatafname.length == 0) return null;
1633 // create toxcore
1634 char* toxProxyHost = null;
1635 scope(exit) {
1636 import core.stdc.stdlib : free;
1637 if (toxProxyHost !is null) free(toxProxyHost);
1639 ubyte[] savedata;
1640 scope(exit) delete savedata;
1642 auto toxOpts = tox_options_new();
1643 assert(toxOpts !is null);
1644 scope(exit) if (toxOpts !is null) tox_options_free(toxOpts);
1645 tox_options_default(toxOpts);
1647 bool createNewToxCoreAccount = false;
1648 toxOpts.tox_options_set_ipv6_enabled(protoOpts.ipv6);
1649 toxOpts.tox_options_set_udp_enabled(protoOpts.udp);
1650 toxOpts.tox_options_set_local_discovery_enabled(protoOpts.localDiscovery);
1651 toxOpts.tox_options_set_hole_punching_enabled(protoOpts.holePunching);
1652 toxOpts.tox_options_set_start_port(protoOpts.startPort);
1653 toxOpts.tox_options_set_end_port(protoOpts.endPort);
1654 toxOpts.tox_options_set_tcp_port(protoOpts.tcpPort);
1656 toxOpts.tox_options_set_proxy_type(protoOpts.proxyType);
1657 if (protoOpts.proxyType != TOX_PROXY_TYPE_NONE) {
1658 import core.stdc.stdlib : malloc;
1659 toxOpts.tox_options_set_proxy_port(protoOpts.proxyPort);
1660 // create proxy address string
1661 toxProxyHost = cast(char*)malloc(protoOpts.proxyAddr.length+1);
1662 if (toxProxyHost is null) assert(0, "out of memory");
1663 toxProxyHost[0..protoOpts.proxyAddr.length] = protoOpts.proxyAddr[];
1664 toxProxyHost[protoOpts.proxyAddr.length] = 0;
1665 toxOpts.tox_options_set_proxy_host(toxProxyHost);
1668 savedata = loadToxCoreData(toxdatafname);
1669 if (savedata is null) {
1670 // create new tox instance
1671 if (wasCreated !is null) *wasCreated = true;
1672 if (!allowNew) return null;
1673 toxOpts.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_NONE);
1674 conwriteln("creating new ToxCore account...");
1675 } else {
1676 // load data
1677 conwriteln("setting ToxCore account data (", savedata.length, " bytes)");
1678 toxOpts.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_TOX_SAVE);
1679 toxOpts.tox_options_set_savedata_length(savedata.length);
1680 toxOpts.tox_options_set_savedata_data(savedata.ptr, savedata.length);
1682 scope(exit) delete savedata;
1684 version(ToxCoreDebug) {
1685 toxOpts.tox_options_set_log_callback(&toxCoreLogCB);
1688 // create tox instance
1689 TOX_ERR_NEW error;
1690 ToxP tox = tox_new(toxOpts, &error);
1691 if (tox is null) {
1692 conwriteln("cannot create ToxCore instance: error is ", error);
1693 return null;
1696 tox_callback_self_connection_status(tox, &connectionCB);
1698 tox_callback_friend_name(tox, &friendNameCB);
1699 tox_callback_friend_status_message(tox, &friendStatusMessageCB);
1700 tox_callback_friend_status(tox, &friendStatusCB);
1701 tox_callback_friend_connection_status(tox, &friendConnectionCB);
1702 //tox_callback_friend_typing(tox, &friendTypingCB);
1703 tox_callback_friend_read_receipt(tox, &friendReceiptCB);
1704 tox_callback_friend_request(tox, &friendReqCB);
1705 tox_callback_friend_message(tox, &friendMsgCB);
1707 tox_callback_file_recv_control(tox, &fileRecvCtlCB);
1708 tox_callback_file_chunk_request(tox, &fileChunkReqCB);
1709 tox_callback_file_recv(tox, &fileRecvCB);
1710 //tox_callback_file_recv_chunk
1712 //tox_callback_conference_invite
1713 //tox_callback_conference_message
1714 //tox_callback_conference_title
1715 //tox_callback_conference_namelist_change
1717 //tox_callback_friend_lossy_packet
1718 //tox_callback_friend_lossless_packet
1720 return tox;
1724 // ////////////////////////////////////////////////////////////////////////// //
1725 private:
1726 enum ToxCoreDataId = 0x15ed1b1fU;
1728 enum ToxCoreChunkTypeHi = 0x01ceU;
1729 enum ToxCoreChunkNoSpamKeys = 1;
1730 enum ToxCoreChunkDHT = 2;
1731 enum ToxCoreChunkFriends = 3;
1732 enum ToxCoreChunkName = 4;
1733 enum ToxCoreChunkStatusMsg = 5;
1734 enum ToxCoreChunkStatus = 6;
1735 enum ToxCoreChunkTcpRelay = 10;
1736 enum ToxCoreChunkPathNode = 11;
1737 enum ToxCoreChunkEnd = 255;
1740 struct ToxSavedFriend {
1741 enum TOTAL_DATA_SIZE = 2216;
1742 enum CRYPTO_PUBLIC_KEY_SIZE = 32;
1743 enum CRYPTO_SECRET_KEY_SIZE = 32;
1744 enum SAVED_FRIEND_REQUEST_SIZE = 1024;
1745 enum MAX_NAME_LENGTH = 128;
1746 enum MAX_STATUSMESSAGE_LENGTH = 1007;
1748 enum Status : ubyte {
1749 NoFriend,
1750 Added,
1751 Requested,
1752 Confirmed,
1753 Online,
1756 enum UserStatus : ubyte {
1757 None,
1758 Away,
1759 Busy,
1760 Invalid,
1763 Status status;
1764 ubyte[CRYPTO_PUBLIC_KEY_SIZE] real_pk;
1765 char[SAVED_FRIEND_REQUEST_SIZE] info; // the data that is sent during the friend requests we do
1766 //ubyte pad0;
1767 ushort info_size; // length of the info
1768 char[MAX_NAME_LENGTH] name;
1769 ushort name_length;
1770 char[MAX_STATUSMESSAGE_LENGTH] statusmessage;
1771 //ubyte pad1;
1772 ushort statusmessage_length;
1773 UserStatus userstatus;
1774 //ubyte[3] pad2;
1775 uint friendrequest_nospam;
1776 ulong last_seen_time;
1778 void load (VFile fl) {
1779 // we can use CTFE here, but meh...
1780 status = cast(Status)fl.readNum!ubyte;
1781 if (status > Status.max) throw new ProtocolException("invalid friend status");
1782 fl.rawReadExact(real_pk[]);
1783 fl.rawReadExact(info[]);
1784 /*pad0 =*/ fl.readNum!ubyte;
1785 info_size = fl.readNum!(ushort, "BE");
1786 if (info_size > info.length) throw new ProtocolException("invalid friend data");
1787 fl.rawReadExact(name[]);
1788 name_length = fl.readNum!(ushort, "BE");
1789 if (name_length > name.length) throw new ProtocolException("invalid friend data");
1790 fl.rawReadExact(statusmessage[]);
1791 /*pad1 =*/ fl.readNum!ubyte;
1792 statusmessage_length = fl.readNum!(ushort, "BE");
1793 if (statusmessage_length > statusmessage.length) throw new ProtocolException("invalid friend data");
1794 userstatus = cast(UserStatus)fl.readNum!ubyte;
1795 if (userstatus > UserStatus.max) throw new ProtocolException("invalid friend userstatus");
1796 /*pad30 =*/ fl.readNum!ubyte;
1797 /*pad31 =*/ fl.readNum!ubyte;
1798 /*pad32 =*/ fl.readNum!ubyte;
1799 friendrequest_nospam = fl.readNum!uint;
1800 last_seen_time = fl.readNum!(ulong, "BE");