aggresive resending when contact comes online
[bioacid.git] / accobj.d
blob14ca1ec6223601066f90cc2a0f7be0c2c57c01f6
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 module accobj is aliced;
19 import std.datetime;
21 import arsd.color;
22 import arsd.image;
23 import arsd.simpledisplay;
25 import iv.cmdcon;
26 import iv.cmdcon.gl;
27 import iv.strex;
28 import iv.lockfile;
29 import iv.tox;
30 import iv.txtser;
31 import iv.unarray;
32 import iv.utfutil;
33 import iv.vfs.io;
35 import accdb;
36 import toxproto;
37 import popups;
39 import tkmain;
40 import tklog;
41 import tkminiedit;
44 // ////////////////////////////////////////////////////////////////////////// //
45 __gshared bool optShowOffline = false;
48 // ////////////////////////////////////////////////////////////////////////// //
49 __gshared string accBaseDir = ".";
50 __gshared Contact activeContact;
53 private bool decodeHexStringInto (ubyte[] dest, const(char)[] str) {
54 dest[] = 0;
55 int dpos = 0;
56 foreach (immutable char ch; str) {
57 if (ch <= ' ' || ch == '_' || ch == '.' || ch == ':') continue;
58 int dig = -1;
59 if (ch >= '0' && ch <= '9') dig = ch-'0';
60 else if (ch >= 'A' && ch <= 'F') dig = ch-'A'+10;
61 else if (ch >= 'a' && ch <= 'f') dig = ch-'a'+10;
62 else return false;
63 if (dpos >= dest.length*2) return false;
64 if (dpos%2 == 0) dest[dpos/2] = cast(ubyte)(dig<<4); else dest[dpos/2] |= cast(ubyte)dig;
65 ++dpos;
67 return (dpos == dest.length*2);
71 public PubKey decodePubKeyStr (const(char)[] str) {
72 PubKey res;
73 if (!decodeHexStringInto(res[], str)) res[] = toxCoreEmptyKey[];
74 return res;
78 public ToxAddr decodeAddrStr (const(char)[] str) {
79 ToxAddr res = 0;
80 if (!decodeHexStringInto(res[], str)) res[] = toxCoreEmptyAddr[];
81 return res;
85 // ////////////////////////////////////////////////////////////////////////// //
86 final class Group {
87 private:
88 this (Account aOwner) nothrow { acc = aOwner; }
90 private:
91 bool mDirty; // true: write contact's config
92 string diskFileName;
94 public:
95 Account acc;
96 GroupOptions info;
98 private:
99 bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
100 enum lo = "info."~fld;
101 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
102 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
105 int getIntOpt(string fld) () const nothrow @trusted @nogc {
106 enum lo = "info."~fld;
107 if (mixin(lo) >= 0) return mixin(lo);
108 return mixin("acc.info."~fld);
111 public:
112 void markDirty () pure nothrow @safe @nogc => mDirty = true;
114 void save () {
115 import std.path : dirName;
116 // save this contact
117 assert(acc !is null);
118 // create disk name
119 if (diskFileName.length == 0) {
120 diskFileName = acc.basePath~"/contacts/groups.rc";
122 mkdirRec(diskFileName.dirName);
123 if (serialize(info, diskFileName)) mDirty = false;
126 @property bool visible () const nothrow @trusted @nogc {
127 if (!hideIfNoVisibleMembers) return true; // always visible
128 // check if we have any visible members
129 foreach (const(Contact) c; acc.contacts.byValue) {
130 if (c.gid != info.gid) continue;
131 if (c.visibleNoGroupCheck) return true;
133 return false; // nobody's here
136 @property nothrow @safe {
137 uint gid () const pure @nogc => info.gid;
139 bool opened () const @nogc => info.opened;
140 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
142 string name () const @nogc => info.name;
143 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
145 string note () const @nogc => info.note;
146 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
148 @nogc {
149 bool showOffline () const => getTriOpt!"showOffline";
150 bool showPopup () const => getTriOpt!"showPopup";
151 bool blinkActivity () const => getTriOpt!"blinkActivity";
152 bool skipUnread () const => getTriOpt!"skipUnread";
153 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
154 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
155 int resendRotDays () const => getIntOpt!"resendRotDays";
156 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
162 // ////////////////////////////////////////////////////////////////////////// //
163 final class Contact {
164 public:
165 // `Connecting` for non-account means "awaiting authorization"
167 static struct XMsg {
168 bool isMe; // "/me" message?
169 SysTime time;
170 string text;
171 long msgid; // ==0: unknown yet
172 int resendCount = 0;
173 MonoTime resendAllowTime = MonoTime.zero;
175 string textOrig () const pure nothrow @trusted @nogc {
176 int pos = 0;
177 while (pos < text.length && text.ptr[pos] != ']') ++pos;
178 pos += 2;
179 return (pos < text.length ? text[pos..$] : null);
182 bool isOurMark (const(void)[] atext, SysTime atime) nothrow @trusted {
183 pragma(inline, true);
184 return (atime == time && textDigest(textOrig) == textDigest(atext));
187 bool isOurMark (long aid, const(void)[] atext, SysTime atime) nothrow @trusted {
188 pragma(inline, true);
189 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == textDigest(atext)));
192 bool isOurMark (long aid, in ref TextDigest adigest, SysTime atime) nothrow @trusted {
193 pragma(inline, true);
194 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == adigest));
198 private:
199 MonoTime nextSendTime; // for resend queue
201 private:
202 this (Account aOwner) nothrow { acc = aOwner; edit = new MiniEdit(); }
204 void removeData () nothrow {
205 if (diskFileName.length == 0) return;
206 try {
207 import std.file : rmdirRecurse, rename;
208 import std.path : dirName;
209 auto dn = diskFileName.dirName;
210 if (dn.length == 0 || dn == "/") return; // just in case
211 conwriteln("removing dir <", dn, ">");
212 rmdirRecurse(dn);
213 //rename(dn, "_"~dn);
214 diskFileName = null;
215 mDirty = false;
216 } catch (Exception e) {}
219 private:
220 bool mDirty; // true: write contact's config
222 public:
223 Account acc;
224 string diskFileName;
225 ContactInfo info;
226 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
227 MiniEdit edit;
228 XMsg[] resendQueue;
229 int unreadCount;
231 public:
232 void markDirty () pure nothrow @safe @nogc => mDirty = true;
234 void loadUnreadCount () nothrow {
235 assert(diskFileName.length);
236 assert(acc !is null);
237 try {
238 import std.path : dirName;
239 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
240 unreadCount = fi.readNum!int;
241 } catch (Exception e) {
242 unreadCount = 0;
246 void saveUnreadCount () nothrow {
247 assert(diskFileName.length);
248 assert(acc !is null);
249 try {
250 import std.path : dirName;
251 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
252 fo.writeNum(unreadCount);
253 } catch (Exception e) {
257 void loadResendQueue () {
258 import std.path : dirName;
259 string fname = diskFileName.dirName~"/logs/resend.log";
260 LogFile lf;
261 lf.load(fname);
262 foreach (const ref lmsg; lf.messages) {
263 XMsg xmsg;
264 xmsg.isMe = lmsg.isMe;
265 xmsg.time = lmsg.time;
266 xmsg.text = lmsg.text;
267 xmsg.msgid = 0;
268 resendQueue ~= xmsg;
270 nextSendTime = MonoTime.currTime;
273 void saveResendQueue () {
274 import std.file : remove;
275 import std.path : dirName;
276 assert(diskFileName.length);
277 assert(acc !is null);
278 mkdirRec(diskFileName.dirName~"/logs");
279 string fname = diskFileName.dirName~"/logs/resend.log";
280 try { remove(fname); } catch (Exception e) {}
281 if (resendQueue.length) {
282 foreach (const ref msg; resendQueue) {
283 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
288 void save () {
289 import std.path : dirName;
290 // save this contact
291 assert(acc !is null);
292 // create disk name
293 if (diskFileName.length == 0) {
294 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
295 acc.contacts[info.pubkey] = this;
297 mkdirRec(diskFileName.dirName);
298 mkdirRec(diskFileName.dirName~"/avatars");
299 mkdirRec(diskFileName.dirName~"/files");
300 mkdirRec(diskFileName.dirName~"/fileparts");
301 mkdirRec(diskFileName.dirName~"/logs");
302 saveUnreadCount();
303 if (serialize(info, diskFileName)) mDirty = false;
306 // save if dirty
307 void update () {
308 if (mDirty) save();
311 public:
312 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (!kfd && (optShowOffline || showOffline || acceptPending || requestPending || status != ContactStatus.Offline || unreadCount > 0));
314 @property bool visible () const nothrow @trusted @nogc {
315 if (kfd) return false;
316 if (acceptPending || requestPending) return true;
317 if (unreadCount > 0) return true;
318 if (!showOffline && !optShowOffline && status == ContactStatus.Offline) return false;
319 auto grp = acc.groupById(gid);
320 return (grp.visible && grp.opened);
323 @property nothrow @safe {
324 bool online () const pure @nogc => (status != ContactStatus.Offline && status != ContactStatus.Connecting);
326 ContactInfo.Kind kind () const @nogc => info.kind;
327 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
329 uint gid () const @nogc => info.gid;
330 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
332 string nick () const @nogc => info.nick;
333 void nick (string v) @nogc { pragma(inline, true); if (info.nick != v) { info.nick = v; markDirty(); } }
335 string visnick () const @nogc => info.visnick;
336 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
338 string displayNick () const @nogc => (info.visnick.length ? info.visnick : (info.nick.length ? info.nick : "<unknown>"));
340 string statusmsg () const @nogc => info.statusmsg;
341 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
343 bool kfd () const @nogc => (info.kind == ContactInfo.Kind.KillFuckDie);
344 bool friend () const @nogc => (info.kind == ContactInfo.Kind.Friend);
345 bool acceptPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthAccept);
346 bool requestPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthRequest);
348 void setLastOnlineNow () {
349 try {
350 auto ut = systimeNow.toUnixTime();
351 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
352 } catch (Exception e) {}
355 string note () const @nogc => info.note;
356 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
359 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
360 enum lo = "info.opts."~fld;
361 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
362 auto grp = acc.groupById(info.gid);
363 enum go = "grp.info."~fld;
364 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
365 return mixin("acc.info."~fld);
368 private void setTriOpt(string fld) (TriOption val) {
369 enum lo = "info.opts."~fld;
370 if (mixin(lo) != val) {
371 mixin("info.opts."~fld~" = val;");
372 markDirty();
373 update();
377 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
378 enum lo = "info.opts."~fld;
379 if (mixin(lo) >= 0) return mixin(lo);
380 auto grp = acc.groupById(info.gid);
381 enum go = "grp.info."~fld;
382 if (mixin(go) >= 0) return mixin(go);
383 return mixin("acc.info."~fld);
386 @property nothrow @safe @nogc {
387 bool showOffline () const => !kfd && getTriOpt!"showOffline";
388 bool showPopup () const => !kfd && getTriOpt!"showPopup";
389 bool blinkActivity () const => !kfd && getTriOpt!"blinkActivity";
390 bool skipUnread () const => kfd || getTriOpt!"skipUnread";
391 bool ftranAllowed () const => !kfd && getTriOpt!"ftranAllowed";
392 int resendRotDays () const => getIntOpt!"resendRotDays";
393 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
396 @property void showOffline (TriOption v) {
397 setTriOpt!"showOffline"(v);
400 void loadLogInto (ref LogFile lf) {
401 import std.file : exists;
402 import std.path : dirName;
403 string lname = diskFileName.dirName~"/logs/hugelog.log";
404 if (lname.exists) lf.load(lname); else lf.clear();
407 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
408 import std.path : dirName;
409 string lname = diskFileName.dirName~"/logs/hugelog.log";
410 LogFile.appendLine(lname, kind, text, isMe, time);
413 void ackReceived (long msgid) {
414 if (msgid <= 0) return; // wtf?!
415 bool changed = false;
416 usize idx = 0;
417 while (idx < resendQueue.length) {
418 if (resendQueue[idx].msgid == msgid) {
419 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
420 resendQueue[$-1] = XMsg.init;
421 resendQueue.length -= 1;
422 resendQueue.assumeSafeAppend;
423 changed = true;
424 } else {
425 ++idx;
428 if (changed) saveResendQueue();
431 // <0: not found
432 long findInResendQueue (const(char)[] text, SysTime time) {
433 foreach (ref XMsg msg; resendQueue) {
434 if (msg.time == time && msg.textOrig == text) {
435 //conwriteln("<", text, "> found in resend queue; id=", msg.msgid, "; rt=<", msg.text, ">");
436 return msg.msgid;
437 } /*else if (msg.textOrig == text) {
438 conwriteln("<", text, "> (", time, ":", msg.time, ") IS NOT: id=", msg.msgid, "; rt=<", msg.text, ">");
441 return -1;
444 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
445 bool doSave = false;
446 usize pos = 0;
447 while (pos < resendQueue.length) {
448 if (resendQueue[pos].isOurMark(msgid, digest, time)) {
449 foreach (immutable c; pos+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
450 resendQueue[$-1] = XMsg.init;
451 resendQueue.length -= 1;
452 resendQueue.assumeSafeAppend;
453 doSave = true;
454 } else {
455 ++pos;
458 if (doSave) saveResendQueue();
459 return doSave;
462 // called when toxcore goes offline
463 void resetQueueIds () {
464 foreach (ref XMsg msg; resendQueue) msg.msgid = 0;
465 if (activeContact is this) logResetAckMessageIds();
466 nextSendTime = MonoTime.currTime+10.seconds;
469 void processResendQueue (bool forced=false) {
470 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
471 if (forced) conwriteln("force resending for ", info.nick);
472 auto ctt = MonoTime.currTime;
473 if (!forced) {
474 if (nextSendTime > ctt) return; // oops
476 // resend auth request?
477 if (acceptPending) {
478 if (isValidAddr(info.fraddr)) {
479 string msg = info.statusmsg;
480 if (msg.length == 0) msg = "I brought you a tasty fish!";
481 if (!toxCoreSendFriendRequest(acc.toxpk, info.fraddr, msg)) { conwriteln("address: '", tox_hex(info.fraddr), "'; error sending friend request"); }
483 } else {
484 bool doSave = false;
485 if (forced) {
486 foreach (ref XMsg msg; resendQueue) msg.resendAllowTime = ctt;
488 foreach (ref XMsg msg; resendQueue) {
489 if (forced || msg.resendAllowTime <= ctt) {
490 if (forced) conwriteln(" ...resending for ", info.nick);
491 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
492 if (msgid < 0) break;
493 if (activeContact is this) logFixAckMessageId(msg.msgid, msgid, msg.textOrig, msg.time);
494 msg.msgid = msgid;
495 msg.resendAllowTime = ctt+(forced ? 2.seconds : 10.seconds);
496 if (msg.resendCount++ != 0) doSave = true;
499 if (doSave) saveResendQueue();
501 nextSendTime = MonoTime.currTime+(forced ? 5.seconds : 30.seconds);
504 void send (const(char)[] text) {
506 static bool isWordBoundary (char ch) {
507 if (ch <= ' ') return true;
508 if (ch >= 127) return false;
509 if (ch >= 'A' && ch <= 'Z') return false;
510 if (ch >= 'a' && ch <= 'z') return false;
511 if (ch >= '0' && ch <= '9') return false;
512 return true;
516 void sendOne (const(char)[] text, bool action) {
517 if (text.length == 0) return; // just in case
519 SysTime now = systimeNow;
520 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
521 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
523 // add this to resend queue
524 XMsg xmsg;
525 xmsg.isMe = action;
526 xmsg.time = now;
527 xmsg.msgid = msgid; // 0: we are offline
528 xmsg.resendCount = 0;
530 auto ctt = MonoTime.currTime;
531 if (nextSendTime <= ctt) nextSendTime = ctt+30.seconds;
532 xmsg.resendAllowTime = ctt+60.seconds;
535 import std.datetime;
536 import std.format : format;
537 auto dt = cast(DateTime)now;
538 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
541 resendQueue ~= xmsg;
543 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
544 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
547 bool action = text.startsWith("/me ");
548 if (action) text = text[4..$].xstripleft;
550 while (text.length) {
551 auto ep = text.indexOf('\n');
552 if (ep < 0) ep = text.length; else ++ep; // include '\n'
553 // remove line if it contains only spaces
554 bool hasNonSpace = false;
555 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
556 if (hasNonSpace) break;
557 text = text[ep..$];
559 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
560 if (text.length == 0) return; // nothing to do
562 // now split it
563 //TODO: split at word boundaries
564 enum ReservedSpace = 32+3+3; // 32 is for date/time stamp
565 const int maxmlen = tox_max_message_length()-ReservedSpace;
566 assert(maxmlen > 0);
568 // remove leading spaces if we have to split the text
569 while (text.length > maxmlen && text[0] <= ' ') text = text[1..$];
571 if (text.length <= maxmlen) {
572 sendOne(text, action);
573 } else {
574 // k8: toxcore developers are idiots, so we have to do dynalloc here
575 auto tmpbuf = new char[](maxmlen+ReservedSpace+64);
576 scope(exit) delete tmpbuf;
578 bool first = true;
579 assert(text[0] > ' ');
580 while (text.length) {
581 // skip trailing spaces
582 if (!first && text[0] <= ' ') { text = text[1..$]; continue; }
583 // find word boundary
584 int epos = maxmlen;
585 if (epos < text.length) {
586 // go back, remember last word start
587 while (epos > 0 && text[epos-1] > ' ') --epos;
588 if (epos == 0) {
589 // find utf start
590 epos = maxmlen;
591 while (epos > 0) {
592 --epos;
593 if (text[epos] < 128 || (text[epos]&0xc0) == 0xc0) break;
595 if (epos == 0) epos = maxmlen; // meh
596 } else {
597 // remove trailing spaces
598 while (epos > 0 && text[epos-1] <= ' ') --epos;
599 // we removed trailing spaces, so there should be at least one non-space char
600 assert(epos > 0);
602 } else {
603 epos = cast(int)text.length;
605 assert(epos > 0);
606 if (first && epos >= text.length) {
607 sendOne(text[0..epos], action);
608 } else {
609 int ofs = 0;
610 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
611 tmpbuf[ofs..ofs+epos] = text[0..epos];
612 if (epos < text.length) {
613 tmpbuf[ofs+epos..ofs+epos+3] = "...";
614 sendOne(tmpbuf[0..ofs+epos+3], action);
615 } else {
616 sendOne(tmpbuf[0..ofs+epos], action);
619 first = false;
620 text = text[epos..$];
624 //saveResendQueue();
629 // ////////////////////////////////////////////////////////////////////////// //
630 final class Account {
631 public:
632 void saveGroups () nothrow {
633 import std.algorithm : sort;
634 GroupOptions[] glist;
635 scope(exit) delete glist;
636 foreach (Group g; groups) glist ~= g.info;
637 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
638 glist.serialize(basePath~"contacts/groups.rc");
641 void saveUpdatedGroups () {
642 bool needToUpdate = false;
643 foreach (Group g; groups) if (g.mDirty) { needToUpdate = true; g.mDirty = false; }
644 if (needToUpdate) saveGroups();
647 private:
648 ContactStatus mStatus = ContactStatus.Offline;
650 public:
651 PubKey toxpk = toxCoreEmptyKey;
652 string toxDataDiskName;
653 string basePath; // with trailing "/"
654 ProtoOptions protoOpts;
655 AccountConfig info;
656 Group[] groups;
657 Contact[PubKey] contacts;
659 public:
660 bool mIAmConnecting = false;
661 bool mIAmOnline = false;
662 bool forceOnline = true; // set to `false` to stop autoreconnecting
664 public:
665 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
667 @property bool isOnline () const nothrow @safe @nogc {
668 if (!toxpk.isValidKey) return false;
669 if (mIAmConnecting) return false;
670 if (!mIAmOnline) return false;
671 return true;
674 @property ContactStatus status () const nothrow @safe @nogc {
675 if (!toxpk.isValidKey) return ContactStatus.Offline;
676 if (mIAmConnecting) return ContactStatus.Connecting;
677 if (!mIAmOnline) return ContactStatus.Offline;
678 return mStatus;
681 @property void status (ContactStatus v) {
682 if (!toxpk.isValidKey) return;
683 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
684 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
685 forceOnline = (v != ContactStatus.Offline);
686 if (mStatus == ContactStatus.Offline) {
687 mIAmConnecting = (v != ContactStatus.Offline);
688 mIAmOnline = false; // just in case
690 toxCoreSetStatus(toxpk, v);
691 mStatus = v;
692 // if not reconnecting, make all contacts offline
693 if (v == ContactStatus.Offline && !mIAmConnecting) {
694 foreach (Contact ct; this) ct.status = ContactStatus.Offline;
696 glconPostScreenRepaint();
697 fixTrayIcon();
700 void processResendQueue () {
701 if (!isOnline) return;
702 foreach (Contact ct; contacts.byValue) { ct.processResendQueue(); ct.update(); }
703 saveUpdatedGroups();
706 void saveResendQueue () {
707 foreach (Contact ct; contacts.byValue) { ct.saveResendQueue(); ct.update(); }
708 saveUpdatedGroups();
711 string getAddress () {
712 if (!toxpk.isValidKey) return null;
713 return tox_hex(toxCoreGetSelfAddress(toxpk));
716 private:
717 void toxCreate () {
718 toxpk = toxCoreOpenAccount(toxDataDiskName);
719 if (!toxpk.isValidKey) {
720 conwriteln("creating new Tox account...");
721 string nick = info.nick;
722 if (nick.length > 0) {
723 //FIXME: utf
724 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
725 } else {
726 nick = "anonymous";
728 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
729 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
731 conwriteln("my address: [", tox_hex(toxCoreGetSelfAddress(toxpk)), "]");
732 toxLoadKnownContacts();
735 // load contacts from ToxCore data and add 'em to contact database
736 void toxLoadKnownContacts () {
737 if (!toxpk.isValidKey) return;
738 version(none) {
739 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] anick) {
740 string nick = anick.buildNormalizedString!true;
741 auto c = (frpub in contacts ? contacts[frpub] : null);
742 if (c is null) {
743 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
744 c = new Contact(this);
745 c.info.gid = 0;
746 c.info.nick = nick;
747 c.info.pubkey[] = frpub[];
748 c.info.opts.showOffline = TriOption.Default;
749 auto ls = toxCoreLastSeen(self, frpub);
750 if (ls != SysTime.min) {
751 c.info.lastonlinetime = cast(uint)ls.toUnixTime();
753 if (c.info.lastonlinetime == 0 && nick.length == 0) {
754 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
756 contacts[c.info.pubkey] = c;
757 c.save();
758 //HACK!
759 if (clist !is null) clist.buildAccount(this);
760 } else {
761 bool needSave = false;
762 auto ls = toxCoreLastSeen(self, frpub);
763 if (ls != SysTime.min) {
764 auto lsu = cast(uint)ls.toUnixTime();
765 if (c.info.lastonlinetime != lsu) { c.info.lastonlinetime = lsu; needSave = true; }
767 if (c.info.nick != nick) {
768 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
769 if (c.info.nick.length == 0 && nick.length != 0) {
770 needSave = true;
771 c.info.nick = nick;
773 } else {
774 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
776 if (needSave) c.save();
778 return false; // don't stop
780 } else {
781 try {
782 auto data = toxCoreLoadDataFile(VFile(toxDataDiskName));
783 foreach (const ref ci; data.friends) {
784 auto c = (ci.pubkey in contacts ? contacts[ci.pubkey] : null);
785 if (c is null) {
786 conwriteln("NEW friend with pk [", tox_hex(ci.pubkey), "]; name is: ", ci.nick);
787 c = new Contact(this);
788 c.info.gid = 0;
789 c.info.nick = ci.nick;
790 c.info.statusmsg = ci.statusmsg;
791 c.info.lastonlinetime = ci.lastonlinetime;
792 c.info.kind = ci.kind;
793 c.info.pubkey[] = ci.pubkey[];
794 c.info.opts.showOffline = TriOption.Default;
795 contacts[c.info.pubkey] = c;
796 c.save();
797 //HACK!
798 if (clist !is null) clist.buildAccount(this);
799 } else {
800 bool needSave = false;
801 version(none) {
802 if (c.info.nick != ci.nick) {
803 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; new name is: ", ci.nick);
804 } else {
805 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; old name is: ", ci.nick);
808 if (c.info.kind != ci.kind) { c.info.kind = ci.kind; needSave = true; }
809 if (c.info.lastonlinetime != ci.lastonlinetime) { c.info.lastonlinetime = ci.lastonlinetime; needSave = true; }
810 if (c.info.nick.length == 0 && ci.nick.length != 0) { c.info.nick = ci.nick; needSave = true; }
811 if (needSave) c.save();
814 } catch (Exception e) {}
818 public:
819 bool sendFriendRequest (in ref ToxAddr fraddr, const(char)[] msg) {
820 if (!toxpk.isValidKey) return false;
821 if (!isValidAddr(fraddr)) return false;
822 if (msg.length == 0) return false;
823 if (msg.length > tox_max_friend_request_length()) return false;
824 if (fraddr[0..PubKey.length] == toxpk[]) return false; // there is no reason to friend myself
825 PubKey frpub = fraddr[0..PubKey.length];
826 auto c = (frpub in contacts ? contacts[frpub] : null);
827 if (c !is null) { c.statusmsg = msg.idup; c.save(); }
828 if (!toxCoreSendFriendRequest(toxpk, fraddr, msg)) return false;
829 if (c is null) {
830 c = new Contact(this);
831 c.info.gid = 0;
832 c.info.nick = null; // unknown yet
833 c.info.pubkey[] = frpub[];
834 c.info.opts.showOffline = TriOption.Default;
835 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
836 c.info.fraddr[] = fraddr[]; // save address for resending (and just in case)
837 contacts[c.info.pubkey] = c;
838 c.save();
839 //HACK!
840 if (clist !is null) clist.buildAccount(this);
842 return true;
845 private:
846 // connection established
847 void toxConnectionDropped () {
848 alias timp = this;
849 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
850 mIAmConnecting = false;
851 mIAmOnline = false;
852 auto oldst = mStatus;
853 toxCoreSetStatus(toxpk, ContactStatus.Offline); // this kills toxcore instance
854 foreach (Contact ct; contacts.byValue) {
855 ct.status = ContactStatus.Offline;
856 if (ct.kfd) toxCoreRemoveFriend(toxpk, ct.info.pubkey);
857 // this is not quite correct: we need to distinguish when user goes offline, and when toxcore temporarily goes offline
858 // but we're killing toxcore instance on disconnect, so it works
859 ct.resetQueueIds();
861 if (forceOnline) {
862 mStatus = ContactStatus.Offline;
863 status = oldst;
865 glconPostScreenRepaint();
868 // connection established
869 void toxConnectionEstablished () {
870 alias timp = this;
871 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
872 mIAmConnecting = false;
873 mIAmOnline = true;
874 if (info.statusmsg.length == 0) info.statusmsg = "Come taste the gasoline! [BioAcid]";
875 toxCoreSetStatusMessage(toxpk, info.statusmsg);
876 //toxLoadKnownContacts();
877 foreach (Contact ct; contacts.byValue) ct.resetQueueIds();
878 glconPostScreenRepaint();
881 void toxFriendOffline (in ref PubKey fpk) {
882 if (auto ct = fpk in contacts) {
883 auto ls = toxCoreLastSeen(toxpk, fpk);
884 if (ls != SysTime.min) {
885 auto lsu = cast(uint)ls.toUnixTime();
886 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); glconPostScreenRepaint(); }
888 if (ct.status != ContactStatus.Offline) {
889 conwriteln("friend <", ct.info.nick, "> gone offline");
890 ct.status = ContactStatus.Offline;
891 glconPostScreenRepaint();
896 void toxSelfStatus (ContactStatus cst) {
897 if (mStatus != cst) {
898 mStatus = cst;
899 glconPostScreenRepaint();
903 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
904 if (auto ct = fpk in contacts) {
905 if (ct.kfd) return;
906 if (ct.status != cst) {
907 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
908 auto ls = toxCoreLastSeen(toxpk, fpk);
909 if (ls != SysTime.min) {
910 auto lsu = cast(uint)ls.toUnixTime();
911 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); }
913 ct.status = cst;
914 // if it is online, and it is not a friend, turn it into a friend
915 if (ct.online && !ct.friend) ct.kind = ContactInfo.Kind.Friend;
916 if (ct.online) ct.processResendQueue(true); // why not?
917 glconPostScreenRepaint();
922 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
923 if (auto ct = fpk in contacts) {
924 msg = msg.xstrip();
925 if (ct.info.statusmsg != msg) {
926 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
927 ct.info.statusmsg = msg;
928 ct.save();
929 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, (msg.length ? msg : "<nothing>"));
930 glconPostScreenRepaint();
935 void toxFriendNickChanged (in ref PubKey fpk, string nick) {
936 if (auto ct = fpk in contacts) {
937 nick = nick.xstrip();
938 if (nick.length && ct.info.nick != nick) {
939 auto onick = ct.info.nick;
940 ct.info.nick = nick;
941 ct.save();
942 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, "changed nick to '"~nick~"' from '"~onick~"'");
943 glconPostScreenRepaint();
948 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
949 alias timp = this;
950 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
951 if (auto ct = fpk in contacts) {
952 if (ct.kfd) return;
953 // if not waiting for acceptance, force-friend it
954 if (!ct.acceptPending) {
955 conwriteln("force-friend <", ct.info.nick, ">");
956 toxCoreAddFriend(toxpk, fpk);
957 } else {
958 ct.kind = ContactInfo.Kind.PengingAuthAccept;
959 ct.info.statusmsg = msg.idup;
961 ct.setLastOnlineNow();
962 ct.save();
963 showPopup(PopupWindow.Kind.Info, "Friend Accepted", (msg.length ? msg : "<nothing>"));
964 } else {
965 // new friend request
966 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk), "]");
967 auto c = new Contact(this);
968 c.info.gid = 0;
969 c.info.nick = null;
970 c.info.pubkey[] = fpk[];
971 c.info.opts.showOffline = TriOption.Default;
972 c.info.statusmsg = msg.idup;
973 c.kind = ContactInfo.Kind.PengingAuthAccept;
974 contacts[c.info.pubkey] = c;
975 c.setLastOnlineNow();
976 c.save();
977 showPopup(PopupWindow.Kind.Info, "Friend Request", (msg.length ? msg : "<nothing>"));
978 //HACK!
979 if (clist !is null) clist.buildAccount(this);
981 glconPostScreenRepaint();
984 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
985 if (auto ct = fpk in contacts) {
986 bool doPopup = ct.showPopup;
987 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
988 ct.appendToLog(kind, msg, action, time);
989 if (*ct is activeContact) {
990 // if inactive or invisible, add divider line and increase unread count
991 if (!mainWindowVisible || !mainWindowActive) {
992 if (ct.unreadCount == 0) addDividerLine();
993 ct.unreadCount += 1;
994 ct.saveUnreadCount();
995 } else {
996 doPopup = false;
998 addTextToLog(this, *ct, kind, action, msg, time);
999 } else {
1000 ct.unreadCount += 1;
1001 ct.saveUnreadCount();
1003 if (doPopup) showPopup(PopupWindow.Kind.Incoming, ct.nick, msg);
1007 // ack for sent message
1008 void toxMessageAck (in ref PubKey fpk, long msgid) {
1009 alias timp = this;
1010 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
1011 if (auto ct = fpk in contacts) {
1012 if (*ct is activeContact) ackLogMessage(msgid);
1013 ct.ackReceived(msgid);
1017 private:
1018 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
1020 LockFile lockf;
1022 // false: get lost
1023 bool tryLockIt () {
1024 if (toxDataDiskName.length == 0) return false;
1025 lockf = LockFile(toxDataDiskName~".lock");
1026 if (!lockf.tryLock) {
1027 lockf.close();
1028 return false;
1030 return true;
1033 void createWithBaseDir (string aBaseDir) {
1034 import std.algorithm : sort;
1035 import std.file : DirEntry, SpanMode, dirEntries;
1036 import std.path : baseName;
1038 basePath = normalizeBaseDir(aBaseDir);
1039 toxDataDiskName = basePath~"toxdata.tox";
1041 if (!tryLockIt) throw new Exception("cannot activate already active account");
1043 protoOpts.txtunser(VFile(basePath~"proto.rc"));
1044 info.txtunser(VFile(basePath~"config.rc"));
1046 // load groups
1047 GroupOptions[] glist;
1048 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
1049 bool hasDefaultGroup = false;
1050 bool hasMoronsGroup = false;
1051 foreach (ref GroupOptions gi; glist[]) {
1052 auto g = new Group(this);
1053 g.info = gi;
1054 bool found = false;
1055 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
1056 if (!found) groups ~= g;
1057 if (g.gid == 0) hasDefaultGroup = true;
1058 if (g.gid == g.gid.max-2) hasMoronsGroup = true;
1061 // create default group if necessary
1062 if (!hasDefaultGroup) {
1063 GroupOptions gi;
1064 gi.gid = 0;
1065 gi.name = "default";
1066 gi.note = "default group for new contacts";
1067 gi.opened = true;
1068 auto g = new Group(this);
1069 g.info = gi;
1070 groups ~= g;
1073 // create morons group if necessary
1074 if (!hasMoronsGroup) {
1075 GroupOptions gi;
1076 gi.gid = gi.gid.max-2;
1077 gi.name = "<morons>";
1078 gi.note = "group for completely ignored dumbfucks";
1079 gi.opened = false;
1080 gi.showOffline = TriOption.No;
1081 gi.showPopup = TriOption.No;
1082 gi.blinkActivity = TriOption.No;
1083 gi.skipUnread = TriOption.Yes;
1084 gi.hideIfNoVisibleMembers = TriOption.Yes;
1085 gi.ftranAllowed = TriOption.No;
1086 gi.resendRotDays = 0;
1087 gi.hmcOnOpen = 0;
1088 auto g = new Group(this);
1089 g.info = gi;
1090 groups ~= g;
1091 //saveGroups();
1094 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1096 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
1098 static bool isValidCDName (const(char)[] str) {
1099 if (str.length != 64) return false;
1100 foreach (immutable char ch; str) {
1101 if (ch >= '0' && ch <= '9') continue;
1102 if (ch >= 'A' && ch <= 'F') continue;
1103 if (ch >= 'a' && ch <= 'f') continue;
1104 return false;
1106 return true;
1109 // load contacts
1110 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
1111 if (de.name.baseName == "." || de.name.baseName == ".." || !isValidCDName(de.name.baseName)) continue;
1112 try {
1113 import std.file : exists;
1114 if (!de.isDir) continue;
1115 string cfgfn = de.name~"/config.rc";
1116 if (!cfgfn.exists) continue;
1117 ContactInfo ci;
1118 ci.txtunser(VFile(cfgfn));
1119 auto c = new Contact(this);
1120 c.diskFileName = cfgfn;
1121 c.info = ci;
1122 contacts[c.info.pubkey] = c;
1123 c.loadResendQueue();
1124 c.loadUnreadCount();
1125 // fix contact group
1126 if (groupById!false(c.gid) is null) {
1127 c.info.gid = 0; // move to default group
1128 c.save();
1130 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1131 } catch (Exception e) {
1132 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1136 toxCreate();
1137 assert(toxpk.isValidKey, "something is VERY wrong here");
1138 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
1141 public:
1142 this (string aBaseDir) { createWithBaseDir(aBaseDir); }
1144 ~this () {
1145 if (toxpk.isValidKey) {
1146 toxCoreCloseAccount(toxpk);
1147 toxpk[] = toxCoreEmptyKey[];
1151 // save account info (but not contacts)
1152 void save () {
1153 serialize(info, basePath~"config.rc");
1156 // will not write contact to disk
1157 Contact createEmptyContact () {
1158 auto c = new Contact(this);
1159 c.info.gid = 0;
1160 c.info.nick = "test contact";
1161 c.info.pubkey[] = 0;
1162 return c;
1165 // returns `null` if there is no such group, and `dofail` is `true`
1166 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
1167 foreach (const Group g; groups) if (g.gid == agid) return cast(inout)g;
1168 static if (dofail) assert(0, "group not found"); else return null;
1171 int opApply () (scope int delegate (ref Contact ct) dg) {
1172 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
1173 return 0;
1176 // returns gid, or uint.max on error
1177 uint findGroupByName (const(char)[] name) nothrow {
1178 if (name.length == 0) return uint.max;
1179 foreach (const Group g; groups) if (g.name == name) return g.gid;
1180 return uint.max;
1183 // returns gid, or uint.max on error
1184 uint createGroup(T:const(char)[]) (T name) nothrow {
1185 if (name.length == 0) return uint.max;
1186 // groups are sorted by gid, yeah
1187 uint newgid = 0;
1188 foreach (const Group g; groups) {
1189 if (g.name == name) return g.gid;
1190 if (newgid == g.gid) newgid = g.gid+1;
1192 if (newgid == uint.max) return uint.max;
1193 // create new group
1194 GroupOptions gi;
1195 gi.gid = newgid;
1196 static if (is(T == string)) gi.name = name; else gi.name = name.idup;
1197 gi.opened = true;
1198 auto g = new Group(this);
1199 g.info = gi;
1200 groups ~= g;
1201 import std.algorithm : sort;
1202 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1203 saveGroups();
1204 return gi.gid;
1207 // returns `false` on any error
1208 bool moveContactToGroup (Contact ct, uint gid) nothrow {
1209 if (ct is null || gid == uint.max) return false;
1210 // check if this is our contact
1211 bool found = false;
1212 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1213 if (!found) return false;
1214 // find group
1215 Group grp = null;
1216 foreach (Group g; groups) if (g.gid == gid) { grp = g; break; }
1217 if (grp is null) return false;
1218 // move it
1219 if (ct.info.gid != gid) {
1220 ct.info.gid = gid;
1221 ct.markDirty();
1223 return true;
1226 // returns `false` on any error
1227 bool removeContact (Contact ct) {
1228 if (ct is null || !isOnline) return false;
1229 // check if this is our contact
1230 bool found = false;
1231 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1232 if (!found) return false;
1233 toxCoreRemoveFriend(toxpk, ct.info.pubkey);
1234 ct.removeData();
1235 contacts.remove(ct.info.pubkey);
1236 //HACK!
1237 if (clist !is null) {
1238 if (clist.isActiveContact(ct.info.pubkey)) clist.resetActiveItem();
1239 clist.buildAccount(this);
1242 ct.kind = ContactInfo.Kind.KillFuckDie;
1243 ct.markDirty();
1245 return true;
1248 private:
1249 static string normalizeBaseDir (string aBaseDir) {
1250 import std.path : absolutePath, expandTilde;
1251 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir[$-1] == '/') throw new Exception("invalid base dir");
1252 if (aBaseDir.indexOf('/') < 0) aBaseDir = "~/.bioacid/"~aBaseDir;
1253 aBaseDir = aBaseDir.expandTilde.absolutePath;
1254 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1255 return aBaseDir;
1258 public:
1259 static Account CreateNew (string aBaseDir, string aAccName) {
1260 aBaseDir = normalizeBaseDir(aBaseDir);
1261 auto lockf = LockFile(aBaseDir~"toxdata.tox.lock");
1262 if (!lockf.tryLock) {
1263 lockf.close();
1264 throw new Exception("cannot create locked account");
1266 mkdirRec(aBaseDir~"contacts");
1267 // write protocol options
1269 ProtoOptions popt;
1270 serialize(popt, aBaseDir~"proto.rc");
1272 // account options
1274 AccountConfig acc;
1275 acc.nick = aAccName;
1276 acc.showPopup = true;
1277 acc.blinkActivity = true;
1278 acc.hideEmptyGroups = false;
1279 acc.ftranAllowed = true;
1280 acc.resendRotDays = 4;
1281 acc.hmcOnOpen = 10;
1282 serialize(acc, aBaseDir~"config.rc");
1284 // create default group
1286 GroupOptions[1] grp;
1287 grp[0].gid = 0;
1288 grp[0].name = "default";
1289 grp[0].opened = true;
1290 //grp[0].hideIfNoVisible = TriOption.Yes;
1291 serialize(grp[], aBaseDir~"contacts/groups.rc");
1293 // now load it
1294 return new Account(aBaseDir);
1299 // ////////////////////////////////////////////////////////////////////////// //
1300 void setupToxEventListener (SimpleWindow sdmain) {
1301 assert(sdmain !is null);
1303 sdmain.addEventListener((ToxEventBase evt) {
1304 auto acc = clist.accountByPK(evt.self);
1306 if (acc is null) return;
1308 bool fixTray = false;
1309 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1311 // connection?
1312 if (auto e = cast(ToxEventConnection)evt) {
1313 if (e.who[] == acc.toxpk[]) {
1314 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1315 fixTray = true;
1316 } else {
1317 if (!e.connected) acc.toxFriendOffline(e.who);
1319 return;
1321 // status?
1322 if (auto e = cast(ToxEventStatus)evt) {
1323 if (e.who[] == acc.toxpk[]) {
1324 acc.toxSelfStatus(e.status);
1325 fixTray = true;
1326 } else {
1327 acc.toxFriendStatus(e.who, e.status);
1329 return;
1331 // status message?
1332 if (auto e = cast(ToxEventStatusMsg)evt) {
1333 if (e.who[] != acc.toxpk[]) acc.toxFriendStatusMessage(e.who, e.message);
1334 return;
1336 // new nick?
1337 if (auto e = cast(ToxEventNick)evt) {
1338 if (e.who[] != acc.toxpk[]) acc.toxFriendNickChanged(e.who, e.nick);
1339 return;
1341 // incoming text message?
1342 if (auto e = cast(ToxEventMessage)evt) {
1343 if (e.who[] != acc.toxpk[]) {
1344 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1345 fixTray = true;
1347 return;
1349 // ack outgoing text message?
1350 if (auto e = cast(ToxEventMessageAck)evt) {
1351 if (e.who[] != acc.toxpk[]) acc.toxMessageAck(e.who, e.msgid);
1352 return;
1354 // friend request?
1355 if (auto e = cast(ToxEventFriendReq)evt) {
1356 if (e.who[] != acc.toxpk[]) acc.toxFriendReqest(e.who, e.message);
1357 return;
1360 //glconProcessEventMessage();