fixed lastseen text (cosmetix)
[bioacid.git] / accobj.d
blob9d71e6b44ec0ab478f952fdc55d145fcb0e500d0
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 int getIntOpt(string fld) () const nothrow @trusted @nogc {
369 enum lo = "info.opts."~fld;
370 if (mixin(lo) >= 0) return mixin(lo);
371 auto grp = acc.groupById(info.gid);
372 enum go = "grp.info."~fld;
373 if (mixin(go) >= 0) return mixin(go);
374 return mixin("acc.info."~fld);
377 @property nothrow @safe @nogc {
378 bool showOffline () const => !kfd && getTriOpt!"showOffline";
379 bool showPopup () const => !kfd && getTriOpt!"showPopup";
380 bool blinkActivity () const => !kfd && getTriOpt!"blinkActivity";
381 bool skipUnread () const => kfd || getTriOpt!"skipUnread";
382 bool ftranAllowed () const => !kfd && getTriOpt!"ftranAllowed";
383 int resendRotDays () const => getIntOpt!"resendRotDays";
384 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
387 void loadLogInto (ref LogFile lf) {
388 import std.file : exists;
389 import std.path : dirName;
390 string lname = diskFileName.dirName~"/logs/hugelog.log";
391 if (lname.exists) lf.load(lname); else lf.clear();
394 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
395 import std.path : dirName;
396 string lname = diskFileName.dirName~"/logs/hugelog.log";
397 LogFile.appendLine(lname, kind, text, isMe, time);
400 void ackReceived (long msgid) {
401 if (msgid <= 0) return; // wtf?!
402 bool changed = false;
403 usize idx = 0;
404 while (idx < resendQueue.length) {
405 if (resendQueue[idx].msgid == msgid) {
406 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
407 resendQueue[$-1] = XMsg.init;
408 resendQueue.length -= 1;
409 resendQueue.assumeSafeAppend;
410 changed = true;
411 } else {
412 ++idx;
415 if (changed) saveResendQueue();
418 // <0: not found
419 long findInResendQueue (const(char)[] text, SysTime time) {
420 foreach (ref XMsg msg; resendQueue) {
421 if (msg.time == time && msg.textOrig == text) {
422 //conwriteln("<", text, "> found in resend queue; id=", msg.msgid, "; rt=<", msg.text, ">");
423 return msg.msgid;
424 } /*else if (msg.textOrig == text) {
425 conwriteln("<", text, "> (", time, ":", msg.time, ") IS NOT: id=", msg.msgid, "; rt=<", msg.text, ">");
428 return -1;
431 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
432 bool doSave = false;
433 usize pos = 0;
434 while (pos < resendQueue.length) {
435 if (resendQueue[pos].isOurMark(msgid, digest, time)) {
436 foreach (immutable c; pos+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
437 resendQueue[$-1] = XMsg.init;
438 resendQueue.length -= 1;
439 resendQueue.assumeSafeAppend;
440 doSave = true;
441 } else {
442 ++pos;
445 if (doSave) saveResendQueue();
446 return doSave;
449 // called when toxcore goes offline
450 void resetQueueIds () {
451 foreach (ref XMsg msg; resendQueue) msg.msgid = 0;
452 if (activeContact is this) logResetAckMessageIds();
453 nextSendTime = MonoTime.currTime+10.seconds;
456 void processResendQueue () {
457 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
458 auto ctt = MonoTime.currTime;
459 if (nextSendTime > ctt) return; // oops
460 // resend auth request?
461 if (acceptPending) {
462 if (isValidAddr(info.fraddr)) {
463 string msg = info.statusmsg;
464 if (msg.length == 0) msg = "I brought you a tasty fish!";
465 if (!toxCoreSendFriendRequest(acc.toxpk, info.fraddr, msg)) { conwriteln("address: '", tox_hex(info.fraddr), "'; error sending friend request"); }
467 } else {
468 bool doSave = false;
469 foreach (ref XMsg msg; resendQueue) {
470 if (msg.resendAllowTime <= ctt) {
471 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
472 if (msgid < 0) break;
473 if (activeContact is this) logFixAckMessageId(msg.msgid, msgid, msg.textOrig, msg.time);
474 msg.msgid = msgid;
475 if (msg.resendCount++ != 0) doSave = true;
478 if (doSave) saveResendQueue();
480 nextSendTime = MonoTime.currTime+30.seconds;
483 void send (const(char)[] text) {
484 void sendOne (const(char)[] text, bool action) {
485 if (text.length == 0) return; // just in case
487 SysTime now = systimeNow;
488 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
489 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
491 // add this to resend queue
492 XMsg xmsg;
493 xmsg.isMe = action;
494 xmsg.time = now;
495 xmsg.msgid = msgid; // 0: we are offline
496 xmsg.resendCount = 0;
498 auto ctt = MonoTime.currTime;
499 if (nextSendTime <= ctt) nextSendTime = ctt+30.seconds;
500 xmsg.resendAllowTime = ctt+60.seconds;
503 import std.datetime;
504 import std.format : format;
505 auto dt = cast(DateTime)now;
506 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
509 resendQueue ~= xmsg;
511 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
512 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
515 bool action = text.startsWith("/me ");
516 if (action) text = text[4..$].xstripleft;
518 while (text.length) {
519 auto ep = text.indexOf('\n');
520 if (ep < 0) ep = text.length; else ++ep; // include '\n'
521 // remove line if it contains only spaces
522 bool hasNonSpace = false;
523 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
524 if (hasNonSpace) break;
525 text = text[ep..$];
527 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
528 if (text.length == 0) return; // nothing to do
530 // now split it
531 //TODO: split at word boundaries
532 enum ReservedSpace = 23+3+3;
534 // k8: toxcore developers are idiots, so we have to do dynalloc here
535 auto tmpbuf = new char[](tox_max_message_length()+64);
536 scope(exit) delete tmpbuf;
538 bool first = true;
539 while (text.length) {
540 int epos = tox_max_message_length()-ReservedSpace;
541 if (epos < text.length) {
542 // find utf start
543 while (epos > 0) {
544 if (text[epos-1] < 128) break;
545 if ((text[epos-1]&0xc0) == 0xc0) break;
546 --epos;
548 } else {
549 epos = cast(int)text.length;
551 assert(epos > 0);
552 if (first && epos >= text.length) {
553 sendOne(text[0..epos], action);
554 } else {
555 int ofs = 0;
556 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
557 tmpbuf[ofs..ofs+epos] = text[0..epos];
558 tmpbuf[ofs+epos..ofs+epos+3] = "...";
559 sendOne(tmpbuf[0..ofs+epos+3], action);
561 first = false;
562 text = text[epos..$];
565 //saveResendQueue();
570 // ////////////////////////////////////////////////////////////////////////// //
571 final class Account {
572 public:
573 void saveGroups () nothrow {
574 import std.algorithm : sort;
575 GroupOptions[] glist;
576 scope(exit) delete glist;
577 foreach (Group g; groups) glist ~= g.info;
578 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
579 glist.serialize(basePath~"contacts/groups.rc");
582 void saveUpdatedGroups () {
583 bool needToUpdate = false;
584 foreach (Group g; groups) if (g.mDirty) { needToUpdate = true; g.mDirty = false; }
585 if (needToUpdate) saveGroups();
588 private:
589 ContactStatus mStatus = ContactStatus.Offline;
591 public:
592 PubKey toxpk = toxCoreEmptyKey;
593 string toxDataDiskName;
594 string basePath; // with trailing "/"
595 ProtoOptions protoOpts;
596 AccountConfig info;
597 Group[] groups;
598 Contact[PubKey] contacts;
600 public:
601 bool mIAmConnecting = false;
602 bool mIAmOnline = false;
603 bool forceOnline = true; // set to `false` to stop autoreconnecting
605 public:
606 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
608 @property bool isOnline () const nothrow @safe @nogc {
609 if (!toxpk.isValidKey) return false;
610 if (mIAmConnecting) return false;
611 if (!mIAmOnline) return false;
612 return true;
615 @property ContactStatus status () const nothrow @safe @nogc {
616 if (!toxpk.isValidKey) return ContactStatus.Offline;
617 if (mIAmConnecting) return ContactStatus.Connecting;
618 if (!mIAmOnline) return ContactStatus.Offline;
619 return mStatus;
622 @property void status (ContactStatus v) {
623 if (!toxpk.isValidKey) return;
624 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
625 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
626 forceOnline = (v != ContactStatus.Offline);
627 if (mStatus == ContactStatus.Offline) {
628 mIAmConnecting = (v != ContactStatus.Offline);
629 mIAmOnline = false; // just in case
631 toxCoreSetStatus(toxpk, v);
632 mStatus = v;
633 // if not reconnecting, make all contacts offline
634 if (v == ContactStatus.Offline && !mIAmConnecting) {
635 foreach (Contact ct; this) ct.status = ContactStatus.Offline;
637 glconPostScreenRepaint();
638 fixTrayIcon();
641 void processResendQueue () {
642 if (!isOnline) return;
643 foreach (Contact ct; contacts.byValue) { ct.processResendQueue(); ct.update(); }
644 saveUpdatedGroups();
647 void saveResendQueue () {
648 foreach (Contact ct; contacts.byValue) { ct.saveResendQueue(); ct.update(); }
649 saveUpdatedGroups();
652 string getAddress () {
653 if (!toxpk.isValidKey) return null;
654 return tox_hex(toxCoreGetSelfAddress(toxpk));
657 private:
658 void toxCreate () {
659 toxpk = toxCoreOpenAccount(toxDataDiskName);
660 if (!toxpk.isValidKey) {
661 conwriteln("creating new Tox account...");
662 string nick = info.nick;
663 if (nick.length > 0) {
664 //FIXME: utf
665 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
666 } else {
667 nick = "anonymous";
669 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
670 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
672 conwriteln("my address: [", tox_hex(toxCoreGetSelfAddress(toxpk)), "]");
673 toxLoadKnownContacts();
676 // load contacts from ToxCore data and add 'em to contact database
677 void toxLoadKnownContacts () {
678 if (!toxpk.isValidKey) return;
679 version(none) {
680 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] anick) {
681 string nick = anick.buildNormalizedString!true;
682 auto c = (frpub in contacts ? contacts[frpub] : null);
683 if (c is null) {
684 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
685 c = new Contact(this);
686 c.info.gid = 0;
687 c.info.nick = nick;
688 c.info.pubkey[] = frpub[];
689 c.info.opts.showOffline = TriOption.Default;
690 auto ls = toxCoreLastSeen(self, frpub);
691 if (ls != SysTime.min) {
692 c.info.lastonlinetime = cast(uint)ls.toUnixTime();
694 if (c.info.lastonlinetime == 0 && nick.length == 0) {
695 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
697 contacts[c.info.pubkey] = c;
698 c.save();
699 //HACK!
700 if (clist !is null) clist.buildAccount(this);
701 } else {
702 bool needSave = false;
703 auto ls = toxCoreLastSeen(self, frpub);
704 if (ls != SysTime.min) {
705 auto lsu = cast(uint)ls.toUnixTime();
706 if (c.info.lastonlinetime != lsu) { c.info.lastonlinetime = lsu; needSave = true; }
708 if (c.info.nick != nick) {
709 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
710 if (c.info.nick.length == 0 && nick.length != 0) {
711 needSave = true;
712 c.info.nick = nick;
714 } else {
715 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
717 if (needSave) c.save();
719 return false; // don't stop
721 } else {
722 try {
723 auto data = toxCoreLoadDataFile(VFile(toxDataDiskName));
724 foreach (const ref ci; data.friends) {
725 auto c = (ci.pubkey in contacts ? contacts[ci.pubkey] : null);
726 if (c is null) {
727 conwriteln("NEW friend with pk [", tox_hex(ci.pubkey), "]; name is: ", ci.nick);
728 c = new Contact(this);
729 c.info.gid = 0;
730 c.info.nick = ci.nick;
731 c.info.statusmsg = ci.statusmsg;
732 c.info.lastonlinetime = ci.lastonlinetime;
733 c.info.kind = ci.kind;
734 c.info.pubkey[] = ci.pubkey[];
735 c.info.opts.showOffline = TriOption.Default;
736 contacts[c.info.pubkey] = c;
737 c.save();
738 //HACK!
739 if (clist !is null) clist.buildAccount(this);
740 } else {
741 bool needSave = false;
742 version(none) {
743 if (c.info.nick != ci.nick) {
744 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; new name is: ", ci.nick);
745 } else {
746 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; old name is: ", ci.nick);
749 if (c.info.kind != ci.kind) { c.info.kind = ci.kind; needSave = true; }
750 if (c.info.lastonlinetime != ci.lastonlinetime) { c.info.lastonlinetime = ci.lastonlinetime; needSave = true; }
751 if (c.info.nick.length == 0 && ci.nick.length != 0) { c.info.nick = ci.nick; needSave = true; }
752 if (needSave) c.save();
755 } catch (Exception e) {}
759 public:
760 bool sendFriendRequest (in ref ToxAddr fraddr, const(char)[] msg) {
761 if (!toxpk.isValidKey) return false;
762 if (!isValidAddr(fraddr)) return false;
763 if (msg.length == 0) return false;
764 if (msg.length > tox_max_friend_request_length()) return false;
765 if (fraddr[0..PubKey.length] == toxpk[]) return false; // there is no reason to friend myself
766 PubKey frpub = fraddr[0..PubKey.length];
767 auto c = (frpub in contacts ? contacts[frpub] : null);
768 if (c !is null) { c.statusmsg = msg.idup; c.save(); }
769 if (!toxCoreSendFriendRequest(toxpk, fraddr, msg)) return false;
770 if (c is null) {
771 c = new Contact(this);
772 c.info.gid = 0;
773 c.info.nick = null; // unknown yet
774 c.info.pubkey[] = frpub[];
775 c.info.opts.showOffline = TriOption.Default;
776 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
777 c.info.fraddr[] = fraddr[]; // save address for resending (and just in case)
778 contacts[c.info.pubkey] = c;
779 c.save();
780 //HACK!
781 if (clist !is null) clist.buildAccount(this);
783 return true;
786 private:
787 // connection established
788 void toxConnectionDropped () {
789 alias timp = this;
790 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
791 mIAmConnecting = false;
792 mIAmOnline = false;
793 auto oldst = mStatus;
794 toxCoreSetStatus(toxpk, ContactStatus.Offline); // this kills toxcore instance
795 foreach (Contact ct; contacts.byValue) {
796 ct.status = ContactStatus.Offline;
797 if (ct.kfd) toxCoreRemoveFriend(toxpk, ct.info.pubkey);
798 // this is not quite correct: we need to distinguish when user goes offline, and when toxcore temporarily goes offline
799 // but we're killing toxcore instance on disconnect, so it works
800 ct.resetQueueIds();
802 if (forceOnline) {
803 mStatus = ContactStatus.Offline;
804 status = oldst;
806 glconPostScreenRepaint();
809 // connection established
810 void toxConnectionEstablished () {
811 alias timp = this;
812 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
813 mIAmConnecting = false;
814 mIAmOnline = true;
815 if (info.statusmsg.length == 0) info.statusmsg = "Come taste the gasoline! [BioAcid]";
816 toxCoreSetStatusMessage(toxpk, info.statusmsg);
817 //toxLoadKnownContacts();
818 foreach (Contact ct; contacts.byValue) ct.resetQueueIds();
819 glconPostScreenRepaint();
822 void toxFriendOffline (in ref PubKey fpk) {
823 if (auto ct = fpk in contacts) {
824 auto ls = toxCoreLastSeen(toxpk, fpk);
825 if (ls != SysTime.min) {
826 auto lsu = cast(uint)ls.toUnixTime();
827 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); glconPostScreenRepaint(); }
829 if (ct.status != ContactStatus.Offline) {
830 conwriteln("friend <", ct.info.nick, "> gone offline");
831 ct.status = ContactStatus.Offline;
832 glconPostScreenRepaint();
837 void toxSelfStatus (ContactStatus cst) {
838 if (mStatus != cst) {
839 mStatus = cst;
840 glconPostScreenRepaint();
844 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
845 if (auto ct = fpk in contacts) {
846 if (ct.kfd) return;
847 if (ct.status != cst) {
848 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
849 auto ls = toxCoreLastSeen(toxpk, fpk);
850 if (ls != SysTime.min) {
851 auto lsu = cast(uint)ls.toUnixTime();
852 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); }
854 ct.status = cst;
855 // if it is online, and it is not a friend, turn it into a friend
856 if (ct.online && !ct.friend) ct.kind = ContactInfo.Kind.Friend;
857 if (ct.online) ct.processResendQueue(); // why not?
858 glconPostScreenRepaint();
863 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
864 if (auto ct = fpk in contacts) {
865 msg = msg.xstrip();
866 if (ct.info.statusmsg != msg) {
867 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
868 ct.info.statusmsg = msg;
869 ct.save();
870 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, (msg.length ? msg : "<nothing>"));
871 glconPostScreenRepaint();
876 void toxFriendNickChanged (in ref PubKey fpk, string nick) {
877 if (auto ct = fpk in contacts) {
878 nick = nick.xstrip();
879 if (nick.length && ct.info.nick != nick) {
880 auto onick = ct.info.nick;
881 ct.info.nick = nick;
882 ct.save();
883 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, "changed nick to '"~nick~"' from '"~onick~"'");
884 glconPostScreenRepaint();
889 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
890 alias timp = this;
891 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
892 if (auto ct = fpk in contacts) {
893 if (ct.kfd) return;
894 // if not waiting for acceptance, force-friend it
895 if (!ct.acceptPending) {
896 conwriteln("force-friend <", ct.info.nick, ">");
897 toxCoreAddFriend(toxpk, fpk);
898 } else {
899 ct.kind = ContactInfo.Kind.PengingAuthAccept;
900 ct.info.statusmsg = msg.idup;
902 ct.setLastOnlineNow();
903 ct.save();
904 showPopup(PopupWindow.Kind.Info, "Friend Accepted", (msg.length ? msg : "<nothing>"));
905 } else {
906 // new friend request
907 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk), "]");
908 auto c = new Contact(this);
909 c.info.gid = 0;
910 c.info.nick = null;
911 c.info.pubkey[] = fpk[];
912 c.info.opts.showOffline = TriOption.Default;
913 c.info.statusmsg = msg.idup;
914 c.kind = ContactInfo.Kind.PengingAuthAccept;
915 contacts[c.info.pubkey] = c;
916 c.setLastOnlineNow();
917 c.save();
918 showPopup(PopupWindow.Kind.Info, "Friend Request", (msg.length ? msg : "<nothing>"));
919 //HACK!
920 if (clist !is null) clist.buildAccount(this);
922 glconPostScreenRepaint();
925 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
926 if (auto ct = fpk in contacts) {
927 bool doPopup = ct.showPopup;
928 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
929 ct.appendToLog(kind, msg, action, time);
930 if (*ct is activeContact) {
931 // if inactive or invisible, add divider line and increase unread count
932 if (!mainWindowVisible || !mainWindowActive) {
933 if (ct.unreadCount == 0) addDividerLine();
934 ct.unreadCount += 1;
935 ct.saveUnreadCount();
936 } else {
937 doPopup = false;
939 addTextToLog(this, *ct, kind, action, msg, time);
940 } else {
941 ct.unreadCount += 1;
942 ct.saveUnreadCount();
944 if (doPopup) showPopup(PopupWindow.Kind.Incoming, ct.nick, msg);
948 // ack for sent message
949 void toxMessageAck (in ref PubKey fpk, long msgid) {
950 alias timp = this;
951 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
952 if (auto ct = fpk in contacts) {
953 if (*ct is activeContact) ackLogMessage(msgid);
954 ct.ackReceived(msgid);
958 private:
959 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
961 LockFile lockf;
963 // false: get lost
964 bool tryLockIt () {
965 if (toxDataDiskName.length == 0) return false;
966 lockf = LockFile(toxDataDiskName~".lock");
967 if (!lockf.tryLock) {
968 lockf.close();
969 return false;
971 return true;
974 void createWithBaseDir (string aBaseDir) {
975 import std.algorithm : sort;
976 import std.file : DirEntry, SpanMode, dirEntries;
977 import std.path : baseName;
979 basePath = normalizeBaseDir(aBaseDir);
980 toxDataDiskName = basePath~"toxdata.tox";
982 if (!tryLockIt) throw new Exception("cannot activate already active account");
984 protoOpts.txtunser(VFile(basePath~"proto.rc"));
985 info.txtunser(VFile(basePath~"config.rc"));
987 // load groups
988 GroupOptions[] glist;
989 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
990 bool hasDefaultGroup = false;
991 bool hasMoronsGroup = false;
992 foreach (ref GroupOptions gi; glist[]) {
993 auto g = new Group(this);
994 g.info = gi;
995 bool found = false;
996 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
997 if (!found) groups ~= g;
998 if (g.gid == 0) hasDefaultGroup = true;
999 if (g.gid == g.gid.max-2) hasMoronsGroup = true;
1002 // create default group if necessary
1003 if (!hasDefaultGroup) {
1004 GroupOptions gi;
1005 gi.gid = 0;
1006 gi.name = "default";
1007 gi.note = "default group for new contacts";
1008 gi.opened = true;
1009 auto g = new Group(this);
1010 g.info = gi;
1011 groups ~= g;
1014 // create morons group if necessary
1015 if (!hasMoronsGroup) {
1016 GroupOptions gi;
1017 gi.gid = gi.gid.max-2;
1018 gi.name = "<morons>";
1019 gi.note = "group for completely ignored dumbfucks";
1020 gi.opened = false;
1021 gi.showOffline = TriOption.No;
1022 gi.showPopup = TriOption.No;
1023 gi.blinkActivity = TriOption.No;
1024 gi.skipUnread = TriOption.Yes;
1025 gi.hideIfNoVisibleMembers = TriOption.Yes;
1026 gi.ftranAllowed = TriOption.No;
1027 gi.resendRotDays = 0;
1028 gi.hmcOnOpen = 0;
1029 auto g = new Group(this);
1030 g.info = gi;
1031 groups ~= g;
1032 //saveGroups();
1035 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1037 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
1039 static bool isValidCDName (const(char)[] str) {
1040 if (str.length != 64) return false;
1041 foreach (immutable char ch; str) {
1042 if (ch >= '0' && ch <= '9') continue;
1043 if (ch >= 'A' && ch <= 'F') continue;
1044 if (ch >= 'a' && ch <= 'f') continue;
1045 return false;
1047 return true;
1050 // load contacts
1051 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
1052 if (de.name.baseName == "." || de.name.baseName == ".." || !isValidCDName(de.name.baseName)) continue;
1053 try {
1054 import std.file : exists;
1055 if (!de.isDir) continue;
1056 string cfgfn = de.name~"/config.rc";
1057 if (!cfgfn.exists) continue;
1058 ContactInfo ci;
1059 ci.txtunser(VFile(cfgfn));
1060 auto c = new Contact(this);
1061 c.diskFileName = cfgfn;
1062 c.info = ci;
1063 contacts[c.info.pubkey] = c;
1064 c.loadResendQueue();
1065 c.loadUnreadCount();
1066 // fix contact group
1067 if (groupById!false(c.gid) is null) {
1068 c.info.gid = 0; // move to default group
1069 c.save();
1071 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1072 } catch (Exception e) {
1073 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1077 toxCreate();
1078 assert(toxpk.isValidKey, "something is VERY wrong here");
1079 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
1082 public:
1083 this (string aBaseDir) { createWithBaseDir(aBaseDir); }
1085 ~this () {
1086 if (toxpk.isValidKey) {
1087 toxCoreCloseAccount(toxpk);
1088 toxpk[] = toxCoreEmptyKey[];
1092 // save account info (but not contacts)
1093 void save () {
1094 serialize(info, basePath~"config.rc");
1097 // will not write contact to disk
1098 Contact createEmptyContact () {
1099 auto c = new Contact(this);
1100 c.info.gid = 0;
1101 c.info.nick = "test contact";
1102 c.info.pubkey[] = 0;
1103 return c;
1106 // returns `null` if there is no such group, and `dofail` is `true`
1107 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
1108 foreach (const Group g; groups) if (g.gid == agid) return cast(inout)g;
1109 static if (dofail) assert(0, "group not found"); else return null;
1112 int opApply () (scope int delegate (ref Contact ct) dg) {
1113 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
1114 return 0;
1117 // returns gid, or uint.max on error
1118 uint findGroupByName (const(char)[] name) nothrow {
1119 if (name.length == 0) return uint.max;
1120 foreach (const Group g; groups) if (g.name == name) return g.gid;
1121 return uint.max;
1124 // returns gid, or uint.max on error
1125 uint createGroup(T:const(char)[]) (T name) nothrow {
1126 if (name.length == 0) return uint.max;
1127 // groups are sorted by gid, yeah
1128 uint newgid = 0;
1129 foreach (const Group g; groups) {
1130 if (g.name == name) return g.gid;
1131 if (newgid == g.gid) newgid = g.gid+1;
1133 if (newgid == uint.max) return uint.max;
1134 // create new group
1135 GroupOptions gi;
1136 gi.gid = newgid;
1137 static if (is(T == string)) gi.name = name; else gi.name = name.idup;
1138 gi.opened = true;
1139 auto g = new Group(this);
1140 g.info = gi;
1141 groups ~= g;
1142 import std.algorithm : sort;
1143 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1144 saveGroups();
1145 return gi.gid;
1148 // returns `false` on any error
1149 bool moveContactToGroup (Contact ct, uint gid) nothrow {
1150 if (ct is null || gid == uint.max) return false;
1151 // check if this is our contact
1152 bool found = false;
1153 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1154 if (!found) return false;
1155 // find group
1156 Group grp = null;
1157 foreach (Group g; groups) if (g.gid == gid) { grp = g; break; }
1158 if (grp is null) return false;
1159 // move it
1160 if (ct.info.gid != gid) {
1161 ct.info.gid = gid;
1162 ct.markDirty();
1164 return true;
1167 // returns `false` on any error
1168 bool removeContact (Contact ct) {
1169 if (ct is null || !isOnline) return false;
1170 // check if this is our contact
1171 bool found = false;
1172 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1173 if (!found) return false;
1174 toxCoreRemoveFriend(toxpk, ct.info.pubkey);
1175 ct.removeData();
1176 contacts.remove(ct.info.pubkey);
1177 //HACK!
1178 if (clist !is null) {
1179 if (clist.isActiveContact(ct.info.pubkey)) clist.resetActiveItem();
1180 clist.buildAccount(this);
1183 ct.kind = ContactInfo.Kind.KillFuckDie;
1184 ct.markDirty();
1186 return true;
1189 private:
1190 static string normalizeBaseDir (string aBaseDir) {
1191 import std.path : absolutePath, expandTilde;
1192 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir[$-1] == '/') throw new Exception("invalid base dir");
1193 if (aBaseDir.indexOf('/') < 0) aBaseDir = "~/.bioacid/"~aBaseDir;
1194 aBaseDir = aBaseDir.expandTilde.absolutePath;
1195 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1196 return aBaseDir;
1199 public:
1200 static Account CreateNew (string aBaseDir, string aAccName) {
1201 aBaseDir = normalizeBaseDir(aBaseDir);
1202 auto lockf = LockFile(aBaseDir~"toxdata.tox.lock");
1203 if (!lockf.tryLock) {
1204 lockf.close();
1205 throw new Exception("cannot create locked account");
1207 mkdirRec(aBaseDir~"contacts");
1208 // write protocol options
1210 ProtoOptions popt;
1211 serialize(popt, aBaseDir~"proto.rc");
1213 // account options
1215 AccountConfig acc;
1216 acc.nick = aAccName;
1217 acc.showPopup = true;
1218 acc.blinkActivity = true;
1219 acc.hideEmptyGroups = false;
1220 acc.ftranAllowed = true;
1221 acc.resendRotDays = 4;
1222 acc.hmcOnOpen = 10;
1223 serialize(acc, aBaseDir~"config.rc");
1225 // create default group
1227 GroupOptions[1] grp;
1228 grp[0].gid = 0;
1229 grp[0].name = "default";
1230 grp[0].opened = true;
1231 //grp[0].hideIfNoVisible = TriOption.Yes;
1232 serialize(grp[], aBaseDir~"contacts/groups.rc");
1234 // now load it
1235 return new Account(aBaseDir);
1240 // ////////////////////////////////////////////////////////////////////////// //
1241 void setupToxEventListener (SimpleWindow sdmain) {
1242 assert(sdmain !is null);
1244 sdmain.addEventListener((ToxEventBase evt) {
1245 auto acc = clist.accountByPK(evt.self);
1247 if (acc is null) return;
1249 bool fixTray = false;
1250 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1252 // connection?
1253 if (auto e = cast(ToxEventConnection)evt) {
1254 if (e.who[] == acc.toxpk[]) {
1255 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1256 fixTray = true;
1257 } else {
1258 if (!e.connected) acc.toxFriendOffline(e.who);
1260 return;
1262 // status?
1263 if (auto e = cast(ToxEventStatus)evt) {
1264 if (e.who[] == acc.toxpk[]) {
1265 acc.toxSelfStatus(e.status);
1266 fixTray = true;
1267 } else {
1268 acc.toxFriendStatus(e.who, e.status);
1270 return;
1272 // status message?
1273 if (auto e = cast(ToxEventStatusMsg)evt) {
1274 if (e.who[] != acc.toxpk[]) acc.toxFriendStatusMessage(e.who, e.message);
1275 return;
1277 // new nick?
1278 if (auto e = cast(ToxEventNick)evt) {
1279 if (e.who[] != acc.toxpk[]) acc.toxFriendNickChanged(e.who, e.nick);
1280 return;
1282 // incoming text message?
1283 if (auto e = cast(ToxEventMessage)evt) {
1284 if (e.who[] != acc.toxpk[]) {
1285 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1286 fixTray = true;
1288 return;
1290 // ack outgoing text message?
1291 if (auto e = cast(ToxEventMessageAck)evt) {
1292 if (e.who[] != acc.toxpk[]) acc.toxMessageAck(e.who, e.msgid);
1293 return;
1295 // friend request?
1296 if (auto e = cast(ToxEventFriendReq)evt) {
1297 if (e.who[] != acc.toxpk[]) acc.toxFriendReqest(e.who, e.message);
1298 return;
1301 //glconProcessEventMessage();