better quoting
[bioacid.git] / accobj.d
blob4baa56431588122e414e0f529a12cd352093e453
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 module accobj is aliced;
18 import std.datetime;
20 import arsd.color;
21 import arsd.image;
22 import arsd.simpledisplay;
24 import iv.cmdcon;
25 import iv.cmdcon.gl;
26 import iv.strex;
27 import iv.lockfile;
28 import iv.tox;
29 import iv.txtser;
30 import iv.unarray;
31 import iv.utfutil;
32 import iv.vfs.io;
34 import accdb;
35 import toxproto;
36 import popups;
38 import tkmain;
39 import tklog;
40 import tkminiedit;
43 // ////////////////////////////////////////////////////////////////////////// //
44 __gshared bool optShowOffline = false;
47 // ////////////////////////////////////////////////////////////////////////// //
48 __gshared string accBaseDir = ".";
49 __gshared Contact activeContact;
52 private bool decodeHexStringInto (ubyte[] dest, const(char)[] str) {
53 dest[] = 0;
54 int dpos = 0;
55 foreach (immutable char ch; str) {
56 if (ch <= ' ' || ch == '_' || ch == '.' || ch == ':') continue;
57 int dig = -1;
58 if (ch >= '0' && ch <= '9') dig = ch-'0';
59 else if (ch >= 'A' && ch <= 'F') dig = ch-'A'+10;
60 else if (ch >= 'a' && ch <= 'f') dig = ch-'a'+10;
61 else return false;
62 if (dpos >= dest.length*2) return false;
63 if (dpos%2 == 0) dest[dpos/2] = cast(ubyte)(dig<<4); else dest[dpos/2] |= cast(ubyte)dig;
64 ++dpos;
66 return (dpos == dest.length*2);
70 public PubKey decodePubKeyStr (const(char)[] str) {
71 PubKey res;
72 if (!decodeHexStringInto(res[], str)) res[] = toxCoreEmptyKey[];
73 return res;
77 public ToxAddr decodeAddrStr (const(char)[] str) {
78 ToxAddr res = 0;
79 if (!decodeHexStringInto(res[], str)) res[] = toxCoreEmptyAddr[];
80 return res;
84 // ////////////////////////////////////////////////////////////////////////// //
85 final class Group {
86 private:
87 this (Account aOwner) nothrow { acc = aOwner; }
89 private:
90 bool mDirty; // true: write contact's config
91 string diskFileName;
93 public:
94 Account acc;
95 GroupOptions info;
97 private:
98 bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
99 enum lo = "info."~fld;
100 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
101 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
104 int getIntOpt(string fld) () const nothrow @trusted @nogc {
105 enum lo = "info."~fld;
106 if (mixin(lo) >= 0) return mixin(lo);
107 return mixin("acc.info."~fld);
110 public:
111 void markDirty () pure nothrow @safe @nogc => mDirty = true;
113 void save () {
114 import std.path : dirName;
115 // save this contact
116 assert(acc !is null);
117 // create disk name
118 if (diskFileName.length == 0) {
119 diskFileName = acc.basePath~"/contacts/groups.rc";
121 mkdirRec(diskFileName.dirName);
122 if (serialize(info, diskFileName)) mDirty = false;
125 @property bool visible () const nothrow @trusted @nogc {
126 if (!hideIfNoVisibleMembers) return true; // always visible
127 // check if we have any visible members
128 foreach (const(Contact) c; acc.contacts.byValue) {
129 if (c.gid != info.gid) continue;
130 if (c.visibleNoGroupCheck) return true;
132 return false; // nobody's here
135 @property nothrow @safe {
136 uint gid () const pure @nogc => info.gid;
138 bool opened () const @nogc => info.opened;
139 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
141 string name () const @nogc => info.name;
142 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
144 string note () const @nogc => info.note;
145 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
147 @nogc {
148 bool showOffline () const => getTriOpt!"showOffline";
149 bool showPopup () const => getTriOpt!"showPopup";
150 bool blinkActivity () const => getTriOpt!"blinkActivity";
151 bool skipUnread () const => getTriOpt!"skipUnread";
152 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
153 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
154 int resendRotDays () const => getIntOpt!"resendRotDays";
155 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
161 // ////////////////////////////////////////////////////////////////////////// //
162 final class Contact {
163 public:
164 // `Connecting` for non-account means "awaiting authorization"
166 static struct XMsg {
167 bool isMe; // "/me" message?
168 SysTime time;
169 string text;
170 long msgid; // ==0: unknown yet
171 int resendCount = 0;
172 MonoTime resendAllowTime = MonoTime.zero;
174 string textOrig () const pure nothrow @trusted @nogc {
175 int pos = 0;
176 while (pos < text.length && text.ptr[pos] != ']') ++pos;
177 pos += 2;
178 return (pos < text.length ? text[pos..$] : null);
181 bool isOurMark (const(void)[] atext, SysTime atime) nothrow @trusted {
182 pragma(inline, true);
183 return (atime == time && textDigest(textOrig) == textDigest(atext));
186 bool isOurMark (long aid, const(void)[] atext, SysTime atime) nothrow @trusted {
187 pragma(inline, true);
188 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == textDigest(atext)));
191 bool isOurMark (long aid, in ref TextDigest adigest, SysTime atime) nothrow @trusted {
192 pragma(inline, true);
193 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == adigest));
197 private:
198 MonoTime nextSendTime; // for resend queue
200 private:
201 this (Account aOwner) nothrow { acc = aOwner; edit = new MiniEdit(); }
203 void removeData () nothrow {
204 if (diskFileName.length == 0) return;
205 try {
206 import std.file : rmdirRecurse, rename;
207 import std.path : dirName;
208 auto dn = diskFileName.dirName;
209 if (dn.length == 0 || dn == "/") return; // just in case
210 conwriteln("removing dir <", dn, ">");
211 rmdirRecurse(dn);
212 //rename(dn, "_"~dn);
213 diskFileName = null;
214 mDirty = false;
215 } catch (Exception e) {}
218 private:
219 bool mDirty; // true: write contact's config
221 public:
222 Account acc;
223 string diskFileName;
224 ContactInfo info;
225 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
226 MiniEdit edit;
227 XMsg[] resendQueue;
228 int unreadCount;
230 public:
231 void markDirty () pure nothrow @safe @nogc => mDirty = true;
233 void loadUnreadCount () nothrow {
234 assert(diskFileName.length);
235 assert(acc !is null);
236 try {
237 import std.path : dirName;
238 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
239 unreadCount = fi.readNum!int;
240 } catch (Exception e) {
241 unreadCount = 0;
245 void saveUnreadCount () nothrow {
246 assert(diskFileName.length);
247 assert(acc !is null);
248 try {
249 import std.path : dirName;
250 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
251 fo.writeNum(unreadCount);
252 } catch (Exception e) {
256 void loadResendQueue () {
257 import std.path : dirName;
258 string fname = diskFileName.dirName~"/logs/resend.log";
259 LogFile lf;
260 lf.load(fname);
261 foreach (const ref lmsg; lf.messages) {
262 XMsg xmsg;
263 xmsg.isMe = lmsg.isMe;
264 xmsg.time = lmsg.time;
265 xmsg.text = lmsg.text;
266 xmsg.msgid = 0;
267 resendQueue ~= xmsg;
269 nextSendTime = MonoTime.currTime;
272 void saveResendQueue () {
273 import std.file : remove;
274 import std.path : dirName;
275 assert(diskFileName.length);
276 assert(acc !is null);
277 mkdirRec(diskFileName.dirName~"/logs");
278 string fname = diskFileName.dirName~"/logs/resend.log";
279 try { remove(fname); } catch (Exception e) {}
280 if (resendQueue.length) {
281 foreach (const ref msg; resendQueue) {
282 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
287 void save () {
288 import std.path : dirName;
289 // save this contact
290 assert(acc !is null);
291 // create disk name
292 if (diskFileName.length == 0) {
293 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
294 acc.contacts[info.pubkey] = this;
296 mkdirRec(diskFileName.dirName);
297 mkdirRec(diskFileName.dirName~"/avatars");
298 mkdirRec(diskFileName.dirName~"/files");
299 mkdirRec(diskFileName.dirName~"/fileparts");
300 mkdirRec(diskFileName.dirName~"/logs");
301 saveUnreadCount();
302 if (serialize(info, diskFileName)) mDirty = false;
305 // save if dirty
306 void update () {
307 if (mDirty) save();
310 public:
311 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (!kfd && (optShowOffline || showOffline || acceptPending || requestPending || status != ContactStatus.Offline || unreadCount > 0));
313 @property bool visible () const nothrow @trusted @nogc {
314 if (kfd) return false;
315 if (acceptPending || requestPending) return true;
316 if (unreadCount > 0) return true;
317 if (!showOffline && !optShowOffline && status == ContactStatus.Offline) return false;
318 auto grp = acc.groupById(gid);
319 return (grp.visible && grp.opened);
322 @property nothrow @safe {
323 bool online () const pure @nogc => (status != ContactStatus.Offline && status != ContactStatus.Connecting);
325 ContactInfo.Kind kind () const @nogc => info.kind;
326 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
328 uint gid () const @nogc => info.gid;
329 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
331 string nick () const @nogc => info.nick;
332 void nick (string v) @nogc { pragma(inline, true); if (info.nick != v) { info.nick = v; markDirty(); } }
334 string visnick () const @nogc => info.visnick;
335 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
337 string displayNick () const @nogc => (info.visnick.length ? info.visnick : (info.nick.length ? info.nick : "<unknown>"));
339 string statusmsg () const @nogc => info.statusmsg;
340 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
342 bool kfd () const @nogc => (info.kind == ContactInfo.Kind.KillFuckDie);
343 bool friend () const @nogc => (info.kind == ContactInfo.Kind.Friend);
344 bool acceptPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthAccept);
345 bool requestPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthRequest);
347 void setLastOnlineNow () {
348 try {
349 auto ut = systimeNow.toUnixTime();
350 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
351 } catch (Exception e) {}
354 string note () const @nogc => info.note;
355 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
358 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
359 enum lo = "info.opts."~fld;
360 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
361 auto grp = acc.groupById(info.gid);
362 enum go = "grp.info."~fld;
363 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
364 return mixin("acc.info."~fld);
367 private void setTriOpt(string fld) (TriOption val) {
368 enum lo = "info.opts."~fld;
369 if (mixin(lo) != val) {
370 mixin("info.opts."~fld~" = val;");
371 markDirty();
372 update();
376 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
377 enum lo = "info.opts."~fld;
378 if (mixin(lo) >= 0) return mixin(lo);
379 auto grp = acc.groupById(info.gid);
380 enum go = "grp.info."~fld;
381 if (mixin(go) >= 0) return mixin(go);
382 return mixin("acc.info."~fld);
385 @property nothrow @safe @nogc {
386 bool showOffline () const => !kfd && getTriOpt!"showOffline";
387 bool showPopup () const => !kfd && getTriOpt!"showPopup";
388 bool blinkActivity () const => !kfd && getTriOpt!"blinkActivity";
389 bool skipUnread () const => kfd || getTriOpt!"skipUnread";
390 bool ftranAllowed () const => !kfd && getTriOpt!"ftranAllowed";
391 int resendRotDays () const => getIntOpt!"resendRotDays";
392 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
395 @property void showOffline (TriOption v) {
396 setTriOpt!"showOffline"(v);
399 void loadLogInto (ref LogFile lf) {
400 import std.file : exists;
401 import std.path : dirName;
402 string lname = diskFileName.dirName~"/logs/hugelog.log";
403 if (lname.exists) lf.load(lname); else lf.clear();
406 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
407 import std.path : dirName;
408 string lname = diskFileName.dirName~"/logs/hugelog.log";
409 LogFile.appendLine(lname, kind, text, isMe, time);
412 void ackReceived (long msgid) {
413 if (msgid <= 0) return; // wtf?!
414 bool changed = false;
415 usize idx = 0;
416 while (idx < resendQueue.length) {
417 if (resendQueue[idx].msgid == msgid) {
418 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
419 resendQueue[$-1] = XMsg.init;
420 resendQueue.length -= 1;
421 resendQueue.assumeSafeAppend;
422 changed = true;
423 } else {
424 ++idx;
427 if (changed) {
428 saveResendQueue();
429 // resend next message
430 if (resendQueue.length) processResendQueue(forced:true);
434 // <0: not found
435 long findInResendQueue (const(char)[] text, SysTime time) {
436 foreach (ref XMsg msg; resendQueue) {
437 if (msg.time == time && msg.textOrig == text) {
438 //conwriteln("<", text, "> found in resend queue; id=", msg.msgid, "; rt=<", msg.text, ">");
439 return msg.msgid;
440 } /*else if (msg.textOrig == text) {
441 conwriteln("<", text, "> (", time, ":", msg.time, ") IS NOT: id=", msg.msgid, "; rt=<", msg.text, ">");
444 return -1;
447 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
448 bool doSave = false;
449 usize pos = 0;
450 while (pos < resendQueue.length) {
451 if (resendQueue[pos].isOurMark(msgid, digest, time)) {
452 foreach (immutable c; pos+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
453 resendQueue[$-1] = XMsg.init;
454 resendQueue.length -= 1;
455 resendQueue.assumeSafeAppend;
456 doSave = true;
457 } else {
458 ++pos;
461 if (doSave) saveResendQueue();
462 return doSave;
465 // called when toxcore goes offline
466 void resetQueueIds () {
467 foreach (ref XMsg msg; resendQueue) msg.msgid = 0;
468 if (activeContact is this) logResetAckMessageIds();
469 nextSendTime = MonoTime.currTime+10.seconds;
472 void processResendQueue (bool forced=false) {
473 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
474 if (forced) conwriteln("force resending for ", info.nick);
475 auto ctt = MonoTime.currTime;
476 if (!forced) {
477 if (nextSendTime > ctt) return; // oops
479 // resend auth request?
480 if (acceptPending) {
481 if (isValidAddr(info.fraddr)) {
482 string msg = info.statusmsg;
483 if (msg.length == 0) msg = "I brought you a tasty fish!";
484 if (!toxCoreSendFriendRequest(acc.toxpk, info.fraddr, msg)) { conwriteln("address: '", tox_hex(info.fraddr), "'; error sending friend request"); }
486 } else {
487 bool doSave = false;
488 if (forced) {
489 foreach (ref XMsg msg; resendQueue) msg.resendAllowTime = ctt;
491 foreach (ref XMsg msg; resendQueue) {
492 if (forced || msg.resendAllowTime <= ctt) {
493 if (forced) conwriteln(" ...resending for ", info.nick);
494 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
495 if (msgid < 0) break;
496 if (activeContact is this) logFixAckMessageId(msg.msgid, msgid, msg.textOrig, msg.time);
497 msg.msgid = msgid;
498 msg.resendAllowTime = ctt+(forced ? 2.seconds : 10.seconds);
499 if (msg.resendCount++ != 0) doSave = true;
500 // do not resend more than one message at a time, so receiver will get them in order.
501 // ack processor will call us again in forced more if there's more messages to resend.
502 // TODO: introduce a flag to avoid resending until connection lost, or got an ack.
503 break;
506 if (doSave) saveResendQueue();
508 //nextSendTime = MonoTime.currTime+(forced ? 5.seconds : 30.seconds);
509 nextSendTime = MonoTime.currTime+30.seconds; // this should be enough to get an ack (i hope)
512 void send (const(char)[] text) {
514 static bool isWordBoundary (char ch) {
515 if (ch <= ' ') return true;
516 if (ch >= 127) return false;
517 if (ch >= 'A' && ch <= 'Z') return false;
518 if (ch >= 'a' && ch <= 'z') return false;
519 if (ch >= '0' && ch <= '9') return false;
520 return true;
524 void sendOne (const(char)[] text, bool action) {
525 if (text.length == 0) return; // just in case
527 SysTime now = systimeNow;
528 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
529 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
531 // add this to resend queue
532 XMsg xmsg;
533 xmsg.isMe = action;
534 xmsg.time = now;
535 xmsg.msgid = msgid; // 0: we are offline
536 xmsg.resendCount = 0;
538 auto ctt = MonoTime.currTime;
539 if (nextSendTime <= ctt) nextSendTime = ctt+30.seconds;
540 xmsg.resendAllowTime = ctt+60.seconds;
543 import std.datetime;
544 import std.format : format;
545 auto dt = cast(DateTime)now;
546 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
549 resendQueue ~= xmsg;
551 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
552 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
555 bool action = text.startsWith("/me ");
556 if (action) text = text[4..$].xstripleft;
558 while (text.length) {
559 auto ep = text.indexOf('\n');
560 if (ep < 0) ep = text.length; else ++ep; // include '\n'
561 // remove line if it contains only spaces
562 bool hasNonSpace = false;
563 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
564 if (hasNonSpace) break;
565 text = text[ep..$];
567 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
568 if (text.length == 0) return; // nothing to do
570 // now split it
571 //TODO: split at word boundaries
572 enum ReservedSpace = 32+3+3; // 32 is for date/time stamp
573 const int maxmlen = tox_max_message_length()-ReservedSpace;
574 assert(maxmlen > 0);
576 // remove leading spaces if we have to split the text
577 while (text.length > maxmlen && text[0] <= ' ') text = text[1..$];
579 if (text.length <= maxmlen) {
580 sendOne(text, action);
581 } else {
582 // k8: toxcore developers are idiots, so we have to do dynalloc here
583 auto tmpbuf = new char[](maxmlen+ReservedSpace+64);
584 scope(exit) delete tmpbuf;
586 bool first = true;
587 assert(text[0] > ' ');
588 while (text.length) {
589 // skip trailing spaces
590 if (!first && text[0] <= ' ') { text = text[1..$]; continue; }
591 // find word boundary
592 int epos = maxmlen;
593 if (epos < text.length) {
594 // go back, remember last word start
595 while (epos > 0 && text[epos-1] > ' ') --epos;
596 if (epos == 0) {
597 // find utf start
598 epos = maxmlen;
599 while (epos > 0) {
600 --epos;
601 if (text[epos] < 128 || (text[epos]&0xc0) == 0xc0) break;
603 if (epos == 0) epos = maxmlen; // meh
604 } else {
605 // remove trailing spaces
606 while (epos > 0 && text[epos-1] <= ' ') --epos;
607 // we removed trailing spaces, so there should be at least one non-space char
608 assert(epos > 0);
610 } else {
611 epos = cast(int)text.length;
613 assert(epos > 0);
614 if (first && epos >= text.length) {
615 sendOne(text[0..epos], action);
616 } else {
617 int ofs = 0;
618 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
619 tmpbuf[ofs..ofs+epos] = text[0..epos];
620 if (epos < text.length) {
621 tmpbuf[ofs+epos..ofs+epos+3] = "...";
622 sendOne(tmpbuf[0..ofs+epos+3], action);
623 } else {
624 sendOne(tmpbuf[0..ofs+epos], action);
627 first = false;
628 text = text[epos..$];
632 //saveResendQueue();
637 // ////////////////////////////////////////////////////////////////////////// //
638 final class Account {
639 public:
640 void saveGroups () nothrow {
641 import std.algorithm : sort;
642 GroupOptions[] glist;
643 scope(exit) delete glist;
644 foreach (Group g; groups) glist ~= g.info;
645 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
646 glist.serialize(basePath~"contacts/groups.rc");
649 void saveUpdatedGroups () {
650 bool needToUpdate = false;
651 foreach (Group g; groups) if (g.mDirty) { needToUpdate = true; g.mDirty = false; }
652 if (needToUpdate) saveGroups();
655 private:
656 ContactStatus mStatus = ContactStatus.Offline;
658 public:
659 PubKey toxpk = toxCoreEmptyKey;
660 string toxDataDiskName;
661 string basePath; // with trailing "/"
662 ProtoOptions protoOpts;
663 AccountConfig info;
664 Group[] groups;
665 Contact[PubKey] contacts;
667 public:
668 bool mIAmConnecting = false;
669 bool mIAmOnline = false;
670 bool forceOnline = true; // set to `false` to stop autoreconnecting
672 public:
673 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
675 @property bool isOnline () const nothrow @safe @nogc {
676 if (!toxpk.isValidKey) return false;
677 if (mIAmConnecting) return false;
678 if (!mIAmOnline) return false;
679 return true;
682 @property ContactStatus status () const nothrow @safe @nogc {
683 if (!toxpk.isValidKey) return ContactStatus.Offline;
684 if (mIAmConnecting) return ContactStatus.Connecting;
685 if (!mIAmOnline) return ContactStatus.Offline;
686 return mStatus;
689 @property void status (ContactStatus v) {
690 if (!toxpk.isValidKey) return;
691 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
692 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
693 forceOnline = (v != ContactStatus.Offline);
694 if (mStatus == ContactStatus.Offline) {
695 mIAmConnecting = (v != ContactStatus.Offline);
696 mIAmOnline = false; // just in case
698 toxCoreSetStatus(toxpk, v);
699 mStatus = v;
700 // if not reconnecting, make all contacts offline
701 if (v == ContactStatus.Offline && !mIAmConnecting) {
702 foreach (Contact ct; this) ct.status = ContactStatus.Offline;
704 glconPostScreenRepaint();
705 fixTrayIcon();
708 void processResendQueue () {
709 if (!isOnline) return;
710 foreach (Contact ct; contacts.byValue) { ct.processResendQueue(); ct.update(); }
711 saveUpdatedGroups();
714 void saveResendQueue () {
715 foreach (Contact ct; contacts.byValue) { ct.saveResendQueue(); ct.update(); }
716 saveUpdatedGroups();
719 string getAddress () {
720 if (!toxpk.isValidKey) return null;
721 return tox_hex(toxCoreGetSelfAddress(toxpk));
724 private:
725 void toxCreate () {
726 toxpk = toxCoreOpenAccount(toxDataDiskName);
727 if (!toxpk.isValidKey) {
728 conwriteln("creating new Tox account...");
729 string nick = info.nick;
730 if (nick.length > 0) {
731 //FIXME: utf
732 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
733 } else {
734 nick = "anonymous";
736 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
737 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
739 conwriteln("my address: [", tox_hex(toxCoreGetSelfAddress(toxpk)), "]");
740 toxLoadKnownContacts();
743 // load contacts from ToxCore data and add 'em to contact database
744 void toxLoadKnownContacts () {
745 if (!toxpk.isValidKey) return;
746 version(none) {
747 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] anick) {
748 string nick = anick.buildNormalizedString!true;
749 auto c = (frpub in contacts ? contacts[frpub] : null);
750 if (c is null) {
751 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
752 c = new Contact(this);
753 c.info.gid = 0;
754 c.info.nick = nick;
755 c.info.pubkey[] = frpub[];
756 c.info.opts.showOffline = TriOption.Default;
757 auto ls = toxCoreLastSeen(self, frpub);
758 if (ls != SysTime.min) {
759 c.info.lastonlinetime = cast(uint)ls.toUnixTime();
761 if (c.info.lastonlinetime == 0 && nick.length == 0) {
762 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
764 contacts[c.info.pubkey] = c;
765 c.save();
766 //HACK!
767 if (clist !is null) clist.buildAccount(this);
768 } else {
769 bool needSave = false;
770 auto ls = toxCoreLastSeen(self, frpub);
771 if (ls != SysTime.min) {
772 auto lsu = cast(uint)ls.toUnixTime();
773 if (c.info.lastonlinetime != lsu) { c.info.lastonlinetime = lsu; needSave = true; }
775 if (c.info.nick != nick) {
776 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
777 if (c.info.nick.length == 0 && nick.length != 0) {
778 needSave = true;
779 c.info.nick = nick;
781 } else {
782 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
784 if (needSave) c.save();
786 return false; // don't stop
788 } else {
789 try {
790 auto data = toxCoreLoadDataFile(VFile(toxDataDiskName));
791 foreach (const ref ci; data.friends) {
792 auto c = (ci.pubkey in contacts ? contacts[ci.pubkey] : null);
793 if (c is null) {
794 conwriteln("NEW friend with pk [", tox_hex(ci.pubkey), "]; name is: ", ci.nick);
795 c = new Contact(this);
796 c.info.gid = 0;
797 c.info.nick = ci.nick;
798 c.info.statusmsg = ci.statusmsg;
799 c.info.lastonlinetime = ci.lastonlinetime;
800 c.info.kind = ci.kind;
801 c.info.pubkey[] = ci.pubkey[];
802 c.info.opts.showOffline = TriOption.Default;
803 contacts[c.info.pubkey] = c;
804 c.save();
805 //HACK!
806 if (clist !is null) clist.buildAccount(this);
807 } else {
808 bool needSave = false;
809 version(none) {
810 if (c.info.nick != ci.nick) {
811 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; new name is: ", ci.nick);
812 } else {
813 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; old name is: ", ci.nick);
816 if (c.info.kind != ci.kind) { c.info.kind = ci.kind; needSave = true; }
817 if (c.info.lastonlinetime != ci.lastonlinetime) { c.info.lastonlinetime = ci.lastonlinetime; needSave = true; }
818 if (c.info.nick.length == 0 && ci.nick.length != 0) { c.info.nick = ci.nick; needSave = true; }
819 if (needSave) c.save();
822 } catch (Exception e) {}
826 public:
827 bool sendFriendRequest (in ref ToxAddr fraddr, const(char)[] msg) {
828 if (!toxpk.isValidKey) return false;
829 if (!isValidAddr(fraddr)) return false;
830 if (msg.length == 0) return false;
831 if (msg.length > tox_max_friend_request_length()) return false;
832 if (fraddr[0..PubKey.length] == toxpk[]) return false; // there is no reason to friend myself
833 PubKey frpub = fraddr[0..PubKey.length];
834 auto c = (frpub in contacts ? contacts[frpub] : null);
835 if (c !is null) { c.statusmsg = msg.idup; c.save(); }
836 if (!toxCoreSendFriendRequest(toxpk, fraddr, msg)) return false;
837 if (c is null) {
838 c = new Contact(this);
839 c.info.gid = 0;
840 c.info.nick = null; // unknown yet
841 c.info.pubkey[] = frpub[];
842 c.info.opts.showOffline = TriOption.Default;
843 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
844 c.info.fraddr[] = fraddr[]; // save address for resending (and just in case)
845 contacts[c.info.pubkey] = c;
846 c.save();
847 //HACK!
848 if (clist !is null) clist.buildAccount(this);
850 return true;
853 private:
854 // connection established
855 void toxConnectionDropped () {
856 alias timp = this;
857 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
858 mIAmConnecting = false;
859 mIAmOnline = false;
860 auto oldst = mStatus;
861 toxCoreSetStatus(toxpk, ContactStatus.Offline); // this kills toxcore instance
862 foreach (Contact ct; contacts.byValue) {
863 ct.status = ContactStatus.Offline;
864 if (ct.kfd) toxCoreRemoveFriend(toxpk, ct.info.pubkey);
865 // this is not quite correct: we need to distinguish when user goes offline, and when toxcore temporarily goes offline
866 // but we're killing toxcore instance on disconnect, so it works
867 ct.resetQueueIds();
869 if (forceOnline) {
870 mStatus = ContactStatus.Offline;
871 status = oldst;
873 glconPostScreenRepaint();
876 // connection established
877 void toxConnectionEstablished () {
878 alias timp = this;
879 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
880 mIAmConnecting = false;
881 mIAmOnline = true;
882 if (info.statusmsg.length == 0) info.statusmsg = "Come taste the gasoline! [BioAcid]";
883 toxCoreSetStatusMessage(toxpk, info.statusmsg);
884 //toxLoadKnownContacts();
885 foreach (Contact ct; contacts.byValue) ct.resetQueueIds();
886 glconPostScreenRepaint();
889 void toxFriendOffline (in ref PubKey fpk) {
890 if (auto ct = fpk in contacts) {
891 auto ls = toxCoreLastSeen(toxpk, fpk);
892 if (ls != SysTime.min) {
893 auto lsu = cast(uint)ls.toUnixTime();
894 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); glconPostScreenRepaint(); }
896 if (ct.status != ContactStatus.Offline) {
897 conwriteln("friend <", ct.info.nick, "> gone offline");
898 ct.status = ContactStatus.Offline;
899 glconPostScreenRepaint();
904 void toxSelfStatus (ContactStatus cst) {
905 if (mStatus != cst) {
906 mStatus = cst;
907 glconPostScreenRepaint();
911 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
912 if (auto ct = fpk in contacts) {
913 if (ct.kfd) return;
914 if (ct.status != cst) {
915 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
916 auto ls = toxCoreLastSeen(toxpk, fpk);
917 if (ls != SysTime.min) {
918 auto lsu = cast(uint)ls.toUnixTime();
919 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); }
921 ct.status = cst;
922 // if it is online, and it is not a friend, turn it into a friend
923 if (ct.online && !ct.friend) ct.kind = ContactInfo.Kind.Friend;
924 if (ct.online) ct.processResendQueue(true); // why not?
925 glconPostScreenRepaint();
930 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
931 if (auto ct = fpk in contacts) {
932 msg = msg.xstrip();
933 if (ct.info.statusmsg != msg) {
934 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
935 ct.info.statusmsg = msg;
936 ct.save();
937 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, (msg.length ? msg : "<nothing>"));
938 glconPostScreenRepaint();
943 void toxFriendNickChanged (in ref PubKey fpk, string nick) {
944 if (auto ct = fpk in contacts) {
945 nick = nick.xstrip();
946 if (nick.length && ct.info.nick != nick) {
947 auto onick = ct.info.nick;
948 ct.info.nick = nick;
949 ct.save();
950 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, "changed nick to '"~nick~"' from '"~onick~"'");
951 glconPostScreenRepaint();
956 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
957 alias timp = this;
958 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
959 if (auto ct = fpk in contacts) {
960 if (ct.kfd) return;
961 // if not waiting for acceptance, force-friend it
962 if (!ct.acceptPending) {
963 conwriteln("force-friend <", ct.info.nick, ">");
964 toxCoreAddFriend(toxpk, fpk);
965 } else {
966 ct.kind = ContactInfo.Kind.PengingAuthAccept;
967 ct.info.statusmsg = msg.idup;
969 ct.setLastOnlineNow();
970 ct.save();
971 showPopup(PopupWindow.Kind.Info, "Friend Accepted", (msg.length ? msg : "<nothing>"));
972 } else {
973 // new friend request
974 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk), "]");
975 auto c = new Contact(this);
976 c.info.gid = 0;
977 c.info.nick = null;
978 c.info.pubkey[] = fpk[];
979 c.info.opts.showOffline = TriOption.Default;
980 c.info.statusmsg = msg.idup;
981 c.kind = ContactInfo.Kind.PengingAuthAccept;
982 contacts[c.info.pubkey] = c;
983 c.setLastOnlineNow();
984 c.save();
985 showPopup(PopupWindow.Kind.Info, "Friend Request", (msg.length ? msg : "<nothing>"));
986 //HACK!
987 if (clist !is null) clist.buildAccount(this);
989 glconPostScreenRepaint();
992 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
993 if (auto ct = fpk in contacts) {
994 bool doPopup = ct.showPopup;
995 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
996 ct.appendToLog(kind, msg, action, time);
997 if (*ct is activeContact) {
998 // if inactive or invisible, add divider line and increase unread count
999 if (!mainWindowVisible || !mainWindowActive) {
1000 if (ct.unreadCount == 0) addDividerLine();
1001 ct.unreadCount += 1;
1002 ct.saveUnreadCount();
1003 } else {
1004 doPopup = false;
1006 addTextToLog(this, *ct, kind, action, msg, time);
1007 } else {
1008 ct.unreadCount += 1;
1009 ct.saveUnreadCount();
1011 if (doPopup) showPopup(PopupWindow.Kind.Incoming, ct.nick, msg);
1015 // ack for sent message
1016 void toxMessageAck (in ref PubKey fpk, long msgid) {
1017 alias timp = this;
1018 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
1019 if (auto ct = fpk in contacts) {
1020 if (*ct is activeContact) ackLogMessage(msgid);
1021 ct.ackReceived(msgid);
1025 private:
1026 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
1028 LockFile lockf;
1030 // false: get lost
1031 bool tryLockIt () {
1032 if (toxDataDiskName.length == 0) return false;
1033 lockf = LockFile(toxDataDiskName~".lock");
1034 if (!lockf.tryLock) {
1035 lockf.close();
1036 return false;
1038 return true;
1041 void createWithBaseDir (string aBaseDir) {
1042 import std.algorithm : sort;
1043 import std.file : DirEntry, SpanMode, dirEntries;
1044 import std.path : baseName;
1046 basePath = normalizeBaseDir(aBaseDir);
1047 toxDataDiskName = basePath~"toxdata.tox";
1049 if (!tryLockIt) throw new Exception("cannot activate already active account");
1051 protoOpts.txtunser(VFile(basePath~"proto.rc"));
1052 info.txtunser(VFile(basePath~"config.rc"));
1054 // load groups
1055 GroupOptions[] glist;
1056 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
1057 bool hasDefaultGroup = false;
1058 bool hasMoronsGroup = false;
1059 foreach (ref GroupOptions gi; glist[]) {
1060 auto g = new Group(this);
1061 g.info = gi;
1062 bool found = false;
1063 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
1064 if (!found) groups ~= g;
1065 if (g.gid == 0) hasDefaultGroup = true;
1066 if (g.gid == g.gid.max-2) hasMoronsGroup = true;
1069 // create default group if necessary
1070 if (!hasDefaultGroup) {
1071 GroupOptions gi;
1072 gi.gid = 0;
1073 gi.name = "default";
1074 gi.note = "default group for new contacts";
1075 gi.opened = true;
1076 auto g = new Group(this);
1077 g.info = gi;
1078 groups ~= g;
1081 // create morons group if necessary
1082 if (!hasMoronsGroup) {
1083 GroupOptions gi;
1084 gi.gid = gi.gid.max-2;
1085 gi.name = "<morons>";
1086 gi.note = "group for completely ignored dumbfucks";
1087 gi.opened = false;
1088 gi.showOffline = TriOption.No;
1089 gi.showPopup = TriOption.No;
1090 gi.blinkActivity = TriOption.No;
1091 gi.skipUnread = TriOption.Yes;
1092 gi.hideIfNoVisibleMembers = TriOption.Yes;
1093 gi.ftranAllowed = TriOption.No;
1094 gi.resendRotDays = 0;
1095 gi.hmcOnOpen = 0;
1096 auto g = new Group(this);
1097 g.info = gi;
1098 groups ~= g;
1099 //saveGroups();
1102 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1104 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
1106 static bool isValidCDName (const(char)[] str) {
1107 if (str.length != 64) return false;
1108 foreach (immutable char ch; str) {
1109 if (ch >= '0' && ch <= '9') continue;
1110 if (ch >= 'A' && ch <= 'F') continue;
1111 if (ch >= 'a' && ch <= 'f') continue;
1112 return false;
1114 return true;
1117 // load contacts
1118 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
1119 if (de.name.baseName == "." || de.name.baseName == ".." || !isValidCDName(de.name.baseName)) continue;
1120 try {
1121 import std.file : exists;
1122 if (!de.isDir) continue;
1123 string cfgfn = de.name~"/config.rc";
1124 if (!cfgfn.exists) continue;
1125 ContactInfo ci;
1126 ci.txtunser(VFile(cfgfn));
1127 auto c = new Contact(this);
1128 c.diskFileName = cfgfn;
1129 c.info = ci;
1130 contacts[c.info.pubkey] = c;
1131 c.loadResendQueue();
1132 c.loadUnreadCount();
1133 // fix contact group
1134 if (groupById!false(c.gid) is null) {
1135 c.info.gid = 0; // move to default group
1136 c.save();
1138 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1139 } catch (Exception e) {
1140 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1144 toxCreate();
1145 assert(toxpk.isValidKey, "something is VERY wrong here");
1146 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
1149 public:
1150 this (string aBaseDir) { createWithBaseDir(aBaseDir); }
1152 ~this () {
1153 if (toxpk.isValidKey) {
1154 toxCoreCloseAccount(toxpk);
1155 toxpk[] = toxCoreEmptyKey[];
1159 // save account info (but not contacts)
1160 void save () {
1161 serialize(info, basePath~"config.rc");
1164 // will not write contact to disk
1165 Contact createEmptyContact () {
1166 auto c = new Contact(this);
1167 c.info.gid = 0;
1168 c.info.nick = "test contact";
1169 c.info.pubkey[] = 0;
1170 return c;
1173 // returns `null` if there is no such group, and `dofail` is `true`
1174 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
1175 foreach (const Group g; groups) if (g.gid == agid) return cast(inout)g;
1176 static if (dofail) assert(0, "group not found"); else return null;
1179 int opApply () (scope int delegate (ref Contact ct) dg) {
1180 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
1181 return 0;
1184 // returns gid, or uint.max on error
1185 uint findGroupByName (const(char)[] name) nothrow {
1186 if (name.length == 0) return uint.max;
1187 foreach (const Group g; groups) if (g.name == name) return g.gid;
1188 return uint.max;
1191 // returns gid, or uint.max on error
1192 uint createGroup(T:const(char)[]) (T name) nothrow {
1193 if (name.length == 0) return uint.max;
1194 // groups are sorted by gid, yeah
1195 uint newgid = 0;
1196 foreach (const Group g; groups) {
1197 if (g.name == name) return g.gid;
1198 if (newgid == g.gid) newgid = g.gid+1;
1200 if (newgid == uint.max) return uint.max;
1201 // create new group
1202 GroupOptions gi;
1203 gi.gid = newgid;
1204 static if (is(T == string)) gi.name = name; else gi.name = name.idup;
1205 gi.opened = true;
1206 auto g = new Group(this);
1207 g.info = gi;
1208 groups ~= g;
1209 import std.algorithm : sort;
1210 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1211 saveGroups();
1212 return gi.gid;
1215 // returns `false` on any error
1216 bool moveContactToGroup (Contact ct, uint gid) nothrow {
1217 if (ct is null || gid == uint.max) return false;
1218 // check if this is our contact
1219 bool found = false;
1220 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1221 if (!found) return false;
1222 // find group
1223 Group grp = null;
1224 foreach (Group g; groups) if (g.gid == gid) { grp = g; break; }
1225 if (grp is null) return false;
1226 // move it
1227 if (ct.info.gid != gid) {
1228 ct.info.gid = gid;
1229 ct.markDirty();
1231 return true;
1234 // returns `false` on any error
1235 bool removeContact (Contact ct) {
1236 if (ct is null || !isOnline) return false;
1237 // check if this is our contact
1238 bool found = false;
1239 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1240 if (!found) return false;
1241 toxCoreRemoveFriend(toxpk, ct.info.pubkey);
1242 ct.removeData();
1243 contacts.remove(ct.info.pubkey);
1244 //HACK!
1245 if (clist !is null) {
1246 if (clist.isActiveContact(ct.info.pubkey)) clist.resetActiveItem();
1247 clist.buildAccount(this);
1250 ct.kind = ContactInfo.Kind.KillFuckDie;
1251 ct.markDirty();
1253 return true;
1256 private:
1257 static string normalizeBaseDir (string aBaseDir) {
1258 import std.path : absolutePath, expandTilde;
1259 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir[$-1] == '/') throw new Exception("invalid base dir");
1260 if (aBaseDir.indexOf('/') < 0) aBaseDir = "~/.bioacid/"~aBaseDir;
1261 aBaseDir = aBaseDir.expandTilde.absolutePath;
1262 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1263 return aBaseDir;
1266 public:
1267 static Account CreateNew (string aBaseDir, string aAccName) {
1268 aBaseDir = normalizeBaseDir(aBaseDir);
1269 auto lockf = LockFile(aBaseDir~"toxdata.tox.lock");
1270 if (!lockf.tryLock) {
1271 lockf.close();
1272 throw new Exception("cannot create locked account");
1274 mkdirRec(aBaseDir~"contacts");
1275 // write protocol options
1277 ProtoOptions popt;
1278 serialize(popt, aBaseDir~"proto.rc");
1280 // account options
1282 AccountConfig acc;
1283 acc.nick = aAccName;
1284 acc.showPopup = true;
1285 acc.blinkActivity = true;
1286 acc.hideEmptyGroups = false;
1287 acc.ftranAllowed = true;
1288 acc.resendRotDays = 4;
1289 acc.hmcOnOpen = 10;
1290 serialize(acc, aBaseDir~"config.rc");
1292 // create default group
1294 GroupOptions[1] grp;
1295 grp[0].gid = 0;
1296 grp[0].name = "default";
1297 grp[0].opened = true;
1298 //grp[0].hideIfNoVisible = TriOption.Yes;
1299 serialize(grp[], aBaseDir~"contacts/groups.rc");
1301 // now load it
1302 return new Account(aBaseDir);
1307 // ////////////////////////////////////////////////////////////////////////// //
1308 void setupToxEventListener (SimpleWindow sdmain) {
1309 assert(sdmain !is null);
1311 sdmain.addEventListener((ToxEventBase evt) {
1312 auto acc = clist.accountByPK(evt.self);
1314 if (acc is null) return;
1316 bool fixTray = false;
1317 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1319 // connection?
1320 if (auto e = cast(ToxEventConnection)evt) {
1321 if (e.who[] == acc.toxpk[]) {
1322 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1323 fixTray = true;
1324 } else {
1325 if (!e.connected) acc.toxFriendOffline(e.who);
1327 return;
1329 // status?
1330 if (auto e = cast(ToxEventStatus)evt) {
1331 if (e.who[] == acc.toxpk[]) {
1332 acc.toxSelfStatus(e.status);
1333 fixTray = true;
1334 } else {
1335 acc.toxFriendStatus(e.who, e.status);
1337 return;
1339 // status message?
1340 if (auto e = cast(ToxEventStatusMsg)evt) {
1341 if (e.who[] != acc.toxpk[]) acc.toxFriendStatusMessage(e.who, e.message);
1342 return;
1344 // new nick?
1345 if (auto e = cast(ToxEventNick)evt) {
1346 if (e.who[] != acc.toxpk[]) acc.toxFriendNickChanged(e.who, e.nick);
1347 return;
1349 // incoming text message?
1350 if (auto e = cast(ToxEventMessage)evt) {
1351 if (e.who[] != acc.toxpk[]) {
1352 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1353 fixTray = true;
1355 return;
1357 // ack outgoing text message?
1358 if (auto e = cast(ToxEventMessageAck)evt) {
1359 if (e.who[] != acc.toxpk[]) acc.toxMessageAck(e.who, e.msgid);
1360 return;
1362 // friend request?
1363 if (auto e = cast(ToxEventFriendReq)evt) {
1364 if (e.who[] != acc.toxpk[]) acc.toxFriendReqest(e.who, e.message);
1365 return;
1368 //glconProcessEventMessage();