show popups for various events
[bioacid.git] / accobj.d
blobca4bab9484a237d0a58d09c4b03248c52d80423b
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.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 (char ch; str) {
56 if (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<<16); 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 public:
98 void markDirty () pure nothrow @safe @nogc => mDirty = true;
100 void save () {
101 import std.path : dirName;
102 // save this contact
103 assert(acc !is null);
104 // create disk name
105 if (diskFileName.length == 0) {
106 diskFileName = acc.basePath~"/contacts/groups.rc";
108 mkdirRec(diskFileName.dirName);
109 if (serialize(info, diskFileName)) mDirty = false;
112 @property bool visible () const nothrow @trusted @nogc {
113 if (!hideIfNoVisibleMembers) return true; // always visible
114 // check if we have any visible members
115 foreach (const(Contact) c; acc.contacts.byValue) {
116 if (c.gid != info.gid) continue;
117 if (c.visibleNoGroupCheck) return true;
119 return false; // nobody's here
122 private bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
123 enum lo = "info."~fld;
124 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
125 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
128 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
129 enum lo = "info."~fld;
130 if (mixin(lo) >= 0) return mixin(lo);
131 return mixin("acc.info."~fld);
134 @property nothrow @safe {
135 uint gid () const pure @nogc => info.gid;
137 bool opened () const @nogc => info.opened;
138 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
140 string name () const @nogc => info.name;
141 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
143 string note () const @nogc => info.note;
144 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
146 @nogc {
147 bool showOffline () const => getTriOpt!"showOffline";
148 bool showPopup () const => getTriOpt!"showPopup";
149 bool blinkActivity () const => getTriOpt!"blinkActivity";
150 bool skipUnread () const => getTriOpt!"skipUnread";
151 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
152 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
153 int resendRotDays () const => getIntOpt!"resendRotDays";
154 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
160 // ////////////////////////////////////////////////////////////////////////// //
161 final class Contact {
162 public:
163 // `Connecting` for non-account means "awaiting authorization"
165 static struct XMsg {
166 bool isMe; // "/me" message?
167 SysTime time;
168 string text;
169 long msgid; // ==0: unknown yet
170 int resendCount = 0;
172 string textOrig () const pure nothrow @trusted @nogc {
173 int pos = 0;
174 while (pos < text.length && text.ptr[pos] != ']') ++pos;
175 pos += 2;
176 return (pos < text.length ? text[pos..$] : null);
179 bool isOurMark (const(void)[] atext, SysTime atime) nothrow @trusted {
180 pragma(inline, true);
181 return (atime == time && textDigest(textOrig) == textDigest(atext));
184 bool isOurMark (long aid, const(void)[] atext, SysTime atime) nothrow @trusted {
185 pragma(inline, true);
186 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == textDigest(atext)));
189 bool isOurMark (long aid, in ref TextDigest adigest, SysTime atime) nothrow @trusted {
190 pragma(inline, true);
191 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == adigest));
195 private:
196 MonoTime nextSendTime; // for resend queue
198 private:
199 this (Account aOwner) nothrow { acc = aOwner; edit = new MiniEdit(); }
201 void removeData () nothrow {
202 if (diskFileName.length == 0) return;
203 try {
204 import std.file : rmdirRecurse, rename;
205 import std.path : dirName;
206 auto dn = diskFileName.dirName;
207 if (dn.length == 0 || dn == "/") return; // just in case
208 conwriteln("removing dir <", dn, ">");
209 //rmdirRecurse(dn);
210 rename(dn, "_"~dn);
211 diskFileName = null;
212 mDirty = false;
213 } catch (Exception e) {}
216 private:
217 bool mDirty; // true: write contact's config
219 public:
220 Account acc;
221 string diskFileName;
222 ContactInfo info;
223 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
224 MiniEdit edit;
225 XMsg[] resendQueue;
226 int unreadCount;
228 public:
229 void markDirty () pure nothrow @safe @nogc => mDirty = true;
231 void loadUnreadCount () nothrow {
232 assert(diskFileName.length);
233 assert(acc !is null);
234 try {
235 import std.path : dirName;
236 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
237 unreadCount = fi.readNum!int;
238 } catch (Exception e) {
239 unreadCount = 0;
243 void saveUnreadCount () nothrow {
244 assert(diskFileName.length);
245 assert(acc !is null);
246 try {
247 import std.path : dirName;
248 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
249 fo.writeNum(unreadCount);
250 } catch (Exception e) {
254 void loadResendQueue () {
255 import std.path : dirName;
256 string fname = diskFileName.dirName~"/logs/resend.log";
257 LogFile lf;
258 lf.load(fname);
259 foreach (const ref lmsg; lf.messages) {
260 XMsg xmsg;
261 xmsg.isMe = lmsg.isMe;
262 xmsg.time = lmsg.time;
263 xmsg.text = lmsg.text;
264 xmsg.msgid = 0;
265 resendQueue ~= xmsg;
267 nextSendTime = MonoTime.currTime;
270 void saveResendQueue () {
271 import std.file : remove;
272 import std.path : dirName;
273 assert(diskFileName.length);
274 assert(acc !is null);
275 mkdirRec(diskFileName.dirName~"/logs");
276 string fname = diskFileName.dirName~"/logs/resend.log";
277 try { remove(fname); } catch (Exception e) {}
278 if (resendQueue.length) {
279 foreach (const ref msg; resendQueue) {
280 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
285 void save () {
286 import std.path : dirName;
287 // save this contact
288 assert(acc !is null);
289 // create disk name
290 if (diskFileName.length == 0) {
291 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
292 acc.contacts[info.pubkey] = this;
294 mkdirRec(diskFileName.dirName);
295 mkdirRec(diskFileName.dirName~"/avatars");
296 mkdirRec(diskFileName.dirName~"/files");
297 mkdirRec(diskFileName.dirName~"/fileparts");
298 mkdirRec(diskFileName.dirName~"/logs");
299 saveUnreadCount();
300 if (serialize(info, diskFileName)) mDirty = false;
303 // save if dirty
304 void update () {
305 if (mDirty) save();
308 public:
309 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (!kfd && (optShowOffline || showOffline || acceptPending || requestPending || status != ContactStatus.Offline || unreadCount > 0));
311 @property bool visible () const nothrow @trusted @nogc {
312 if (kfd) return false;
313 if (acceptPending || requestPending) return true;
314 if (unreadCount > 0) return true;
315 if (!showOffline && !optShowOffline && status == ContactStatus.Offline) return false;
316 auto grp = acc.groupById(gid);
317 return (grp.visible && grp.opened);
320 @property nothrow @safe {
321 bool online () const pure @nogc => (status != ContactStatus.Offline && status != ContactStatus.Connecting);
323 ContactInfo.Kind kind () const @nogc => info.kind;
324 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
326 uint gid () const @nogc => info.gid;
327 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
329 string nick () const @nogc => info.nick;
330 void nick (string v) @nogc { pragma(inline, true); if (info.nick != v) { info.nick = v; markDirty(); } }
332 string visnick () const @nogc => info.visnick;
333 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
335 string displayNick () const @nogc => (info.visnick.length ? info.visnick : (info.nick.length ? info.nick : "<unknown>"));
337 string statusmsg () const @nogc => info.statusmsg;
338 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
340 bool kfd () const @nogc => (info.kind == ContactInfo.Kind.KillFuckDie);
341 bool friend () const @nogc => (info.kind == ContactInfo.Kind.Friend);
342 bool acceptPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthAccept);
343 bool requestPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthRequest);
345 void setLastOnlineNow () {
346 try {
347 auto ut = systimeNow.toUnixTime();
348 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
349 } catch (Exception e) {}
352 string note () const @nogc => info.note;
353 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
356 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
357 enum lo = "info.opts."~fld;
358 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
359 auto grp = acc.groupById(info.gid);
360 enum go = "grp.info."~fld;
361 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
362 return mixin("acc.info."~fld);
365 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
366 enum lo = "info.opts."~fld;
367 if (mixin(lo) >= 0) return mixin(lo);
368 auto grp = acc.groupById(info.gid);
369 enum go = "grp.info."~fld;
370 if (mixin(go) >= 0) return mixin(go);
371 return mixin("acc.info."~fld);
374 @property nothrow @safe @nogc {
375 bool showOffline () const => !kfd && getTriOpt!"showOffline";
376 bool showPopup () const => !kfd && getTriOpt!"showPopup";
377 bool blinkActivity () const => !kfd && getTriOpt!"blinkActivity";
378 bool skipUnread () const => kfd || getTriOpt!"skipUnread";
379 bool ftranAllowed () const => !kfd && getTriOpt!"ftranAllowed";
380 int resendRotDays () const => getIntOpt!"resendRotDays";
381 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
384 void loadLogInto (ref LogFile lf) {
385 import std.file : exists;
386 import std.path : dirName;
387 string lname = diskFileName.dirName~"/logs/hugelog.log";
388 if (lname.exists) lf.load(lname); else lf.clear();
391 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
392 import std.path : dirName;
393 string lname = diskFileName.dirName~"/logs/hugelog.log";
394 LogFile.appendLine(lname, kind, text, isMe, time);
397 void ackReceived (long msgid) {
398 if (msgid <= 0) return; // wtf?!
399 bool changed = false;
400 usize idx = 0;
401 while (idx < resendQueue.length) {
402 if (resendQueue[idx].msgid == msgid) {
403 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
404 resendQueue[$-1] = XMsg.init;
405 resendQueue.length -= 1;
406 resendQueue.assumeSafeAppend;
407 changed = true;
408 } else {
409 ++idx;
412 if (changed) saveResendQueue();
415 // <0: not found
416 long findInResendQueue (const(char)[] text, SysTime time) {
417 foreach (ref XMsg msg; resendQueue) {
418 if (msg.time == time && msg.textOrig == text) {
419 //conwriteln("<", text, "> found in resend queue; id=", msg.msgid, "; rt=<", msg.text, ">");
420 return msg.msgid;
421 } /*else if (msg.textOrig == text) {
422 conwriteln("<", text, "> (", time, ":", msg.time, ") IS NOT: id=", msg.msgid, "; rt=<", msg.text, ">");
425 return -1;
428 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
429 bool doSave = false;
430 usize pos = 0;
431 while (pos < resendQueue.length) {
432 if (resendQueue[pos].isOurMark(msgid, digest, time)) {
433 foreach (immutable c; pos+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
434 resendQueue[$-1] = XMsg.init;
435 resendQueue.length -= 1;
436 resendQueue.assumeSafeAppend;
437 doSave = true;
438 } else {
439 ++pos;
442 if (doSave) saveResendQueue();
443 return doSave;
446 // called when toxcore goes offline
447 void resetQueueIds () {
448 foreach (ref XMsg msg; resendQueue) msg.msgid = 0;
449 if (activeContact is this) logResetAckMessageIds();
450 nextSendTime = MonoTime.currTime+30.seconds;
453 void processResendQueue () {
454 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
455 bool doSave = false;
456 foreach (ref XMsg msg; resendQueue) {
457 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
458 if (msgid < 0) break;
459 if (activeContact is this) logFixAckMessageId(msg.msgid, msgid, msg.textOrig, msg.time);
460 msg.msgid = msgid;
461 if (msg.resendCount++ != 0) doSave = true;
463 if (doSave) saveResendQueue();
464 nextSendTime = MonoTime.currTime+30.seconds;
467 void send (const(char)[] text) {
468 void sendOne (const(char)[] text, bool action) {
469 if (text.length == 0) return; // just in case
471 SysTime now = systimeNow;
472 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
473 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
475 // add this to resend queue
476 XMsg xmsg;
477 xmsg.isMe = action;
478 xmsg.time = now;
479 xmsg.msgid = msgid; // 0: we are offline
480 xmsg.resendCount = 0;
482 auto ctt = MonoTime.currTime;
483 if (nextSendTime <= ctt) nextSendTime = ctt+30.seconds;
486 import std.datetime;
487 import std.format : format;
488 auto dt = cast(DateTime)now;
489 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
492 resendQueue ~= xmsg;
494 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
495 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
498 bool action = text.startsWith("/me ");
499 if (action) text = text[4..$].xstripleft;
501 while (text.length) {
502 auto ep = text.indexOf('\n');
503 if (ep < 0) ep = text.length; else ++ep; // include '\n'
504 // remove line if it contains only spaces
505 bool hasNonSpace = false;
506 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
507 if (hasNonSpace) break;
508 text = text[ep..$];
510 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
511 if (text.length == 0) return; // nothing to do
513 // now split it
514 //TODO: split at word boundaries
515 enum ReservedSpace = 23+3+3;
517 // k8: toxcore developers are idiots, so we have to do dynalloc here
518 auto tmpbuf = new char[](tox_max_message_length()+64);
519 scope(exit) delete tmpbuf;
521 bool first = true;
522 while (text.length) {
523 int epos = tox_max_message_length()-ReservedSpace;
524 if (epos < text.length) {
525 // find utf start
526 while (epos > 0) {
527 if (text[epos-1] < 128) break;
528 if ((text[epos-1]&0xc0) == 0xc0) break;
529 --epos;
531 } else {
532 epos = cast(int)text.length;
534 assert(epos > 0);
535 if (first && epos >= text.length) {
536 sendOne(text[0..epos], action);
537 } else {
538 int ofs = 0;
539 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
540 tmpbuf[ofs..ofs+epos] = text[0..epos];
541 tmpbuf[ofs+epos..ofs+epos+3] = "...";
542 sendOne(tmpbuf[0..ofs+epos+3], action);
544 first = false;
545 text = text[epos..$];
548 //saveResendQueue();
553 // ////////////////////////////////////////////////////////////////////////// //
554 final class Account {
555 public:
556 void saveGroups () nothrow {
557 import std.algorithm : sort;
558 GroupOptions[] glist;
559 scope(exit) delete glist;
560 foreach (Group g; groups) glist ~= g.info;
561 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
562 glist.serialize(basePath~"contacts/groups.rc");
565 void saveUpdatedGroups () {
566 bool needToUpdate = false;
567 foreach (Group g; groups) if (g.mDirty) { needToUpdate = true; g.mDirty = false; }
568 if (needToUpdate) saveGroups();
571 private:
572 ContactStatus mStatus = ContactStatus.Offline;
574 public:
575 PubKey toxpk = toxCoreEmptyKey;
576 string toxDataDiskName;
577 string basePath; // with trailing "/"
578 ProtoOptions protoOpts;
579 AccountConfig info;
580 Group[] groups;
581 Contact[PubKey] contacts;
583 public:
584 bool mIAmConnecting = false;
585 bool mIAmOnline = false;
586 bool forceOnline = true; // set to `false` to stop autoreconnecting
588 public:
589 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
591 @property bool isOnline () const nothrow @safe @nogc {
592 if (!toxpk.isValidKey) return false;
593 if (mIAmConnecting) return false;
594 if (!mIAmOnline) return false;
595 return true;
598 @property ContactStatus status () const nothrow @safe @nogc {
599 if (!toxpk.isValidKey) return ContactStatus.Offline;
600 if (mIAmConnecting) return ContactStatus.Connecting;
601 if (!mIAmOnline) return ContactStatus.Offline;
602 return mStatus;
605 @property void status (ContactStatus v) {
606 if (!toxpk.isValidKey) return;
607 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
608 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
609 forceOnline = (v != ContactStatus.Offline);
610 if (mStatus == ContactStatus.Offline) {
611 if (v != ContactStatus.Offline) mIAmConnecting = true;
613 toxCoreSetStatus(toxpk, v);
614 mStatus = v;
615 glconPostScreenRepaint();
616 fixTrayIcon();
619 void processResendQueue () {
620 if (!isOnline) return;
621 foreach (Contact ct; contacts.byValue) { ct.processResendQueue(); ct.update(); }
622 saveUpdatedGroups();
625 void saveResendQueue () {
626 foreach (Contact ct; contacts.byValue) { ct.saveResendQueue(); ct.update(); }
627 saveUpdatedGroups();
630 private:
631 void toxCreate () {
632 toxpk = toxCoreOpenAccount(toxDataDiskName);
633 if (!toxpk.isValidKey) {
634 conwriteln("creating new Tox account...");
635 string nick = info.nick;
636 if (nick.length > 0) {
637 //FIXME: utf
638 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
639 } else {
640 nick = "anonymous";
642 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
643 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
645 conwriteln("my address: [", tox_hex(toxCoreGetSelfAddress(toxpk)), "]");
646 toxLoadKnownContacts();
649 // load contacts from ToxCore data and add 'em to contact database
650 void toxLoadKnownContacts () {
651 if (!toxpk.isValidKey) return;
652 version(none) {
653 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] anick) {
654 string nick = anick.buildNormalizedString!true;
655 auto c = (frpub in contacts ? contacts[frpub] : null);
656 if (c is null) {
657 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
658 c = new Contact(this);
659 c.info.gid = 0;
660 c.info.nick = nick;
661 c.info.pubkey[] = frpub[];
662 c.info.opts.showOffline = TriOption.Default;
663 auto ls = toxCoreLastSeen(self, frpub);
664 if (ls != SysTime.min) {
665 c.info.lastonlinetime = cast(uint)ls.toUnixTime();
667 if (c.info.lastonlinetime == 0 && nick.length == 0) {
668 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
670 contacts[c.info.pubkey] = c;
671 c.save();
672 //HACK!
673 if (clist !is null) clist.buildAccount(this);
674 } else {
675 bool needSave = false;
676 auto ls = toxCoreLastSeen(self, frpub);
677 if (ls != SysTime.min) {
678 auto lsu = cast(uint)ls.toUnixTime();
679 if (c.info.lastonlinetime != lsu) { c.info.lastonlinetime = lsu; needSave = true; }
681 if (c.info.nick != nick) {
682 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
683 if (c.info.nick.length == 0 && nick.length != 0) {
684 needSave = true;
685 c.info.nick = nick;
687 } else {
688 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
690 if (needSave) c.save();
692 return false; // don't stop
694 } else {
695 try {
696 auto data = toxCoreLoadDataFile(VFile(toxDataDiskName));
697 foreach (const ref ci; data.friends) {
698 auto c = (ci.pubkey in contacts ? contacts[ci.pubkey] : null);
699 if (c is null) {
700 conwriteln("NEW friend with pk [", tox_hex(ci.pubkey), "]; name is: ", ci.nick);
701 c = new Contact(this);
702 c.info.gid = 0;
703 c.info.nick = ci.nick;
704 c.info.statusmsg = ci.statusmsg;
705 c.info.lastonlinetime = ci.lastonlinetime;
706 c.info.kind = ci.kind;
707 c.info.pubkey[] = ci.pubkey[];
708 c.info.opts.showOffline = TriOption.Default;
709 contacts[c.info.pubkey] = c;
710 c.save();
711 //HACK!
712 if (clist !is null) clist.buildAccount(this);
713 } else {
714 bool needSave = false;
715 version(none) {
716 if (c.info.nick != ci.nick) {
717 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; new name is: ", ci.nick);
718 } else {
719 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; old name is: ", ci.nick);
722 if (c.info.kind != ci.kind) { c.info.kind = ci.kind; needSave = true; }
723 if (c.info.lastonlinetime != ci.lastonlinetime) { c.info.lastonlinetime = ci.lastonlinetime; needSave = true; }
724 if (c.info.nick.length == 0 && ci.nick.length != 0) { c.info.nick = ci.nick; needSave = true; }
725 if (needSave) c.save();
728 } catch (Exception e) {}
732 public:
733 bool sendFriendRequest (in ref ToxAddr fraddr, const(char)[] msg) {
734 if (!toxpk.isValidKey) return false;
735 if (!isValidAddr(fraddr)) return false;
736 if (msg.length == 0) return false;
737 if (msg.length > tox_max_friend_request_length()) return false;
738 if (!toxCoreSendFriendRequest(toxpk, fraddr, msg)) return false;
739 PubKey frpub = fraddr[0..PubKey.length];
740 auto c = (frpub in contacts ? contacts[frpub] : null);
741 if (c is null) {
742 c = new Contact(this);
743 c.info.gid = 0;
744 c.info.nick = null; // unknown yet
745 c.info.pubkey[] = frpub[];
746 c.info.opts.showOffline = TriOption.Default;
747 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
748 c.info.fraddr[] = fraddr[]; // save address, just in case
749 contacts[c.info.pubkey] = c;
750 c.save();
751 //HACK!
752 if (clist !is null) clist.buildAccount(this);
754 return true;
757 private:
758 // connection established
759 void toxConnectionDropped () {
760 alias timp = this;
761 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
762 mIAmConnecting = false;
763 mIAmOnline = false;
764 auto oldst = mStatus;
765 toxCoreSetStatus(toxpk, ContactStatus.Offline); // this kills toxcore instance
766 foreach (Contact ct; contacts.byValue) {
767 ct.status = ContactStatus.Offline;
768 if (ct.kfd) toxCoreRemoveFriend(toxpk, ct.info.pubkey);
769 // this is not quite correct: we need to distinguish when user goes offline, and when toxcore temporarily goes offline
770 // but we're killing toxcore instance on disconnect, so it works
771 ct.resetQueueIds();
773 if (forceOnline) {
774 mStatus = ContactStatus.Offline;
775 status = oldst;
777 glconPostScreenRepaint();
780 // connection established
781 void toxConnectionEstablished () {
782 alias timp = this;
783 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
784 mIAmConnecting = false;
785 mIAmOnline = true;
786 if (info.statusmsg.length == 0) info.statusmsg = "Come taste the gasoline! [BioAcid]";
787 toxCoreSetStatusMessage(toxpk, info.statusmsg);
788 //toxLoadKnownContacts();
789 glconPostScreenRepaint();
792 void toxFriendOffline (in ref PubKey fpk) {
793 if (auto ct = fpk in contacts) {
794 auto ls = toxCoreLastSeen(toxpk, fpk);
795 if (ls != SysTime.min) {
796 auto lsu = cast(uint)ls.toUnixTime();
797 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); glconPostScreenRepaint(); }
799 if (ct.status != ContactStatus.Offline) {
800 conwriteln("friend <", ct.info.nick, "> gone offline");
801 ct.status = ContactStatus.Offline;
802 glconPostScreenRepaint();
807 void toxSelfStatus (ContactStatus cst) {
808 if (mStatus != cst) {
809 mStatus = cst;
810 glconPostScreenRepaint();
814 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
815 if (auto ct = fpk in contacts) {
816 if (ct.kfd) return;
817 if (ct.status != cst) {
818 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
819 auto ls = toxCoreLastSeen(toxpk, fpk);
820 if (ls != SysTime.min) {
821 auto lsu = cast(uint)ls.toUnixTime();
822 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); }
824 ct.status = cst;
825 // if it is online, and it is not a friend, turn it into a friend
826 if (ct.online && !ct.friend) ct.kind = ContactInfo.Kind.Friend;
827 if (ct.online) ct.processResendQueue(); // why not?
828 glconPostScreenRepaint();
833 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
834 if (auto ct = fpk in contacts) {
835 msg = msg.xstrip();
836 if (ct.info.statusmsg != msg) {
837 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
838 ct.info.statusmsg = msg;
839 ct.save();
840 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, (msg.length ? msg : "<nothing>"));
841 glconPostScreenRepaint();
846 void toxFriendNickChanged (in ref PubKey fpk, string nick) {
847 if (auto ct = fpk in contacts) {
848 nick = nick.xstrip();
849 if (nick.length && ct.info.nick != nick) {
850 auto onick = ct.info.nick;
851 ct.info.nick = nick;
852 ct.save();
853 if (ct.showPopup) showPopup(PopupWindow.Kind.Status, ct.nick, "changed nick to '"~nick~"' from '"~onick~"'");
854 glconPostScreenRepaint();
859 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
860 alias timp = this;
861 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
862 if (auto ct = fpk in contacts) {
863 if (ct.kfd) return;
864 // if not waiting for acceptance, force-friend it
865 if (!ct.acceptPending) {
866 conwriteln("force-friend <", ct.info.nick, ">");
867 toxCoreAddFriend(toxpk, fpk);
868 } else {
869 ct.kind = ContactInfo.Kind.PengingAuthAccept;
870 ct.info.statusmsg = msg.idup;
872 ct.setLastOnlineNow();
873 ct.save();
874 showPopup(PopupWindow.Kind.Info, "Friend Accepted", (msg.length ? msg : "<nothing>"));
875 } else {
876 // new friend request
877 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk), "]");
878 auto c = new Contact(this);
879 c.info.gid = 0;
880 c.info.nick = null;
881 c.info.pubkey[] = fpk[];
882 c.info.opts.showOffline = TriOption.Default;
883 c.info.statusmsg = msg.idup;
884 c.kind = ContactInfo.Kind.PengingAuthAccept;
885 contacts[c.info.pubkey] = c;
886 c.setLastOnlineNow();
887 c.save();
888 showPopup(PopupWindow.Kind.Info, "Friend Request", (msg.length ? msg : "<nothing>"));
889 //HACK!
890 if (clist !is null) clist.buildAccount(this);
892 glconPostScreenRepaint();
895 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
896 if (auto ct = fpk in contacts) {
897 bool doPopup = ct.showPopup;
898 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
899 ct.appendToLog(kind, msg, action, time);
900 if (*ct is activeContact) {
901 // if inactive or invisible, add divider line and increase unread count
902 if (!mainWindowVisible || !mainWindowActive) {
903 if (ct.unreadCount == 0) addDividerLine();
904 ct.unreadCount += 1;
905 ct.saveUnreadCount();
906 } else {
907 doPopup = false;
909 addTextToLog(this, *ct, kind, action, msg, time);
910 } else {
911 ct.unreadCount += 1;
912 ct.saveUnreadCount();
914 if (doPopup) showPopup(PopupWindow.Kind.Incoming, ct.nick, msg);
918 // ack for sent message
919 void toxMessageAck (in ref PubKey fpk, long msgid) {
920 alias timp = this;
921 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
922 if (auto ct = fpk in contacts) {
923 if (*ct is activeContact) ackLogMessage(msgid);
924 ct.ackReceived(msgid);
928 private:
929 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
931 public:
932 void save () {
933 serialize(info, basePath~"config.rc");
936 public:
937 this (string aBaseDir) {
938 import std.algorithm : sort;
939 import std.file : DirEntry, SpanMode, dirEntries;
940 import std.path : baseName;
942 basePath = normalizeBaseDir(aBaseDir);
943 toxDataDiskName = basePath~"toxdata.tox";
944 protoOpts.txtunser(VFile(basePath~"proto.rc"));
945 info.txtunser(VFile(basePath~"config.rc"));
947 // load groups
948 GroupOptions[] glist;
949 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
950 bool hasDefaultGroup = false;
951 bool hasMoronsGroup = false;
952 foreach (ref GroupOptions gi; glist[]) {
953 auto g = new Group(this);
954 g.info = gi;
955 bool found = false;
956 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
957 if (!found) groups ~= g;
958 if (g.gid == 0) hasDefaultGroup = true;
959 if (g.gid == g.gid.max-2) hasMoronsGroup = true;
962 // create default group if necessary
963 if (!hasDefaultGroup) {
964 GroupOptions gi;
965 gi.gid = 0;
966 gi.name = "default";
967 gi.note = "default group for new contacts";
968 gi.opened = true;
969 auto g = new Group(this);
970 g.info = gi;
971 groups ~= g;
974 // create morons group if necessary
975 if (!hasMoronsGroup) {
976 GroupOptions gi;
977 gi.gid = gi.gid.max-2;
978 gi.name = "<morons>";
979 gi.note = "group for completely ignored dumbfucks";
980 gi.opened = false;
981 gi.showOffline = TriOption.No;
982 gi.showPopup = TriOption.No;
983 gi.blinkActivity = TriOption.No;
984 gi.skipUnread = TriOption.Yes;
985 gi.hideIfNoVisibleMembers = TriOption.Yes;
986 gi.ftranAllowed = TriOption.No;
987 gi.resendRotDays = 0;
988 gi.hmcOnOpen = 0;
989 auto g = new Group(this);
990 g.info = gi;
991 groups ~= g;
992 //saveGroups();
995 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
997 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
999 static bool isValidCDName (const(char)[] str) {
1000 if (str.length != 64) return false;
1001 foreach (immutable char ch; str) {
1002 if (ch >= '0' && ch <= '9') continue;
1003 if (ch >= 'A' && ch <= 'F') continue;
1004 if (ch >= 'a' && ch <= 'f') continue;
1005 return false;
1007 return true;
1010 // load contacts
1011 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
1012 if (de.name.baseName == "." || de.name.baseName == ".." || !isValidCDName(de.name.baseName)) continue;
1013 try {
1014 import std.file : exists;
1015 if (!de.isDir) continue;
1016 string cfgfn = de.name~"/config.rc";
1017 if (!cfgfn.exists) continue;
1018 ContactInfo ci;
1019 ci.txtunser(VFile(cfgfn));
1020 auto c = new Contact(this);
1021 c.diskFileName = cfgfn;
1022 c.info = ci;
1023 contacts[c.info.pubkey] = c;
1024 c.loadResendQueue();
1025 c.loadUnreadCount();
1026 // fix contact group
1027 if (groupById!false(c.gid) is null) {
1028 c.info.gid = 0; // move to default group
1029 c.save();
1031 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1032 } catch (Exception e) {
1033 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1037 toxCreate();
1038 assert(toxpk.isValidKey, "something is VERY wrong here");
1039 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
1042 ~this () {
1043 if (toxpk.isValidKey) {
1044 toxCoreCloseAccount(toxpk);
1045 toxpk[] = toxCoreEmptyKey[];
1049 // will not write contact to disk
1050 Contact createEmptyContact () {
1051 auto c = new Contact(this);
1052 c.info.gid = 0;
1053 c.info.nick = "test contact";
1054 c.info.pubkey[] = 0;
1055 return c;
1058 // returns `null` if there is no such group, and `dofail` is `true`
1059 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
1060 foreach (const Group g; groups) if (g.gid == agid) return cast(inout)g;
1061 static if (dofail) assert(0, "group not found"); else return null;
1064 int opApply () (scope int delegate (ref Contact ct) dg) {
1065 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
1066 return 0;
1069 // returns gid, or uint.max on error
1070 uint findGroupByName (const(char)[] name) nothrow {
1071 if (name.length == 0) return uint.max;
1072 foreach (const Group g; groups) if (g.name == name) return g.gid;
1073 return uint.max;
1076 // returns gid, or uint.max on error
1077 uint createGroup(T:const(char)[]) (T name) nothrow {
1078 if (name.length == 0) return uint.max;
1079 // groups are sorted by gid, yeah
1080 uint newgid = 0;
1081 foreach (const Group g; groups) {
1082 if (g.name == name) return g.gid;
1083 if (newgid == g.gid) newgid = g.gid+1;
1085 if (newgid == uint.max) return uint.max;
1086 // create new group
1087 GroupOptions gi;
1088 gi.gid = newgid;
1089 static if (is(T == string)) gi.name = name; else gi.name = name.idup;
1090 gi.opened = true;
1091 auto g = new Group(this);
1092 g.info = gi;
1093 groups ~= g;
1094 import std.algorithm : sort;
1095 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1096 saveGroups();
1097 return gi.gid;
1100 // returns `false` on any error
1101 bool moveContactToGroup (Contact ct, uint gid) nothrow {
1102 if (ct is null || gid == uint.max) return false;
1103 // check if this is our contact
1104 bool found = false;
1105 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1106 if (!found) return false;
1107 // find group
1108 Group grp = null;
1109 foreach (Group g; groups) if (g.gid == gid) { grp = g; break; }
1110 if (grp is null) return false;
1111 // move it
1112 if (ct.info.gid != gid) {
1113 ct.info.gid = gid;
1114 ct.markDirty();
1116 return true;
1119 // returns `false` on any error
1120 bool removeContact (Contact ct) nothrow {
1121 if (ct is null || !isOnline) return false;
1122 // check if this is our contact
1123 bool found = false;
1124 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1125 if (!found) return false;
1126 if (!toxCoreRemoveFriend(toxpk, ct.info.pubkey)) return false;
1127 ct.removeData();
1128 contacts.remove(ct.info.pubkey);
1130 ct.kind = ContactInfo.Kind.KillFuckDie;
1131 ct.markDirty();
1133 return true;
1136 private:
1137 static string normalizeBaseDir (string aBaseDir) {
1138 import std.path : absolutePath, expandTilde;
1139 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir[$-1] == '/') throw new Exception("invalid base dir");
1140 if (aBaseDir.indexOf('/') < 0) aBaseDir = "~/.bioacid/"~aBaseDir;
1141 aBaseDir = aBaseDir.expandTilde.absolutePath;
1142 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1143 return aBaseDir;
1146 public:
1147 static Account CreateNew (string aBaseDir, string aAccName) {
1148 aBaseDir = normalizeBaseDir(aBaseDir);
1149 mkdirRec(aBaseDir~"contacts");
1150 // write protocol options
1152 ProtoOptions popt;
1153 serialize(popt, aBaseDir~"proto.rc");
1155 // account options
1157 AccountConfig acc;
1158 acc.nick = aAccName;
1159 acc.showPopup = true;
1160 acc.blinkActivity = true;
1161 acc.hideEmptyGroups = false;
1162 acc.ftranAllowed = true;
1163 acc.resendRotDays = 4;
1164 acc.hmcOnOpen = 10;
1165 serialize(acc, aBaseDir~"config.rc");
1167 // create default group
1169 GroupOptions[1] grp;
1170 grp[0].gid = 0;
1171 grp[0].name = "default";
1172 grp[0].opened = true;
1173 //grp[0].hideIfNoVisible = TriOption.Yes;
1174 serialize(grp[], aBaseDir~"contacts/groups.rc");
1176 // now load it
1177 return new Account(aBaseDir);
1182 // ////////////////////////////////////////////////////////////////////////// //
1183 void setupToxEventListener (SimpleWindow sdmain) {
1184 assert(sdmain !is null);
1186 sdmain.addEventListener((ToxEventBase evt) {
1187 auto acc = clist.accountByPK(evt.self);
1189 if (acc is null) return;
1191 bool fixTray = false;
1192 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1194 // connection?
1195 if (auto e = cast(ToxEventConnection)evt) {
1196 if (e.who[] == acc.toxpk[]) {
1197 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1198 fixTray = true;
1199 } else {
1200 if (!e.connected) acc.toxFriendOffline(e.who);
1202 return;
1204 // status?
1205 if (auto e = cast(ToxEventStatus)evt) {
1206 if (e.who[] == acc.toxpk[]) {
1207 acc.toxSelfStatus(e.status);
1208 fixTray = true;
1209 } else {
1210 acc.toxFriendStatus(e.who, e.status);
1212 return;
1214 // status message?
1215 if (auto e = cast(ToxEventStatusMsg)evt) {
1216 if (e.who[] != acc.toxpk[]) acc.toxFriendStatusMessage(e.who, e.message);
1217 return;
1219 // new nick?
1220 if (auto e = cast(ToxEventNick)evt) {
1221 if (e.who[] != acc.toxpk[]) acc.toxFriendNickChanged(e.who, e.nick);
1222 return;
1224 // incoming text message?
1225 if (auto e = cast(ToxEventMessage)evt) {
1226 if (e.who[] != acc.toxpk[]) {
1227 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1228 fixTray = true;
1230 return;
1232 // ack outgoing text message?
1233 if (auto e = cast(ToxEventMessageAck)evt) {
1234 if (e.who[] != acc.toxpk[]) acc.toxMessageAck(e.who, e.msgid);
1235 return;
1238 //glconProcessEventMessage();