change nick and status messages when they are changed ;-); remove extra spaces from...
[bioacid.git] / accobj.d
blob11cb77e21a9d3f04435a6e86f3ad016c69b53537
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;
37 import tkmain;
38 import tklog;
39 import tkminiedit;
42 // ////////////////////////////////////////////////////////////////////////// //
43 __gshared bool optShowOffline = false;
46 // ////////////////////////////////////////////////////////////////////////// //
47 __gshared string accBaseDir = ".";
48 __gshared Contact activeContact;
51 private bool decodeHexStringInto (ubyte[] dest, const(char)[] str) {
52 dest[] = 0;
53 int dpos = 0;
54 foreach (char ch; str) {
55 if (ch <= ' ') continue;
56 int dig = -1;
57 if (ch >= '0' && ch <= '9') dig = ch-'0';
58 else if (ch >= 'A' && ch <= 'F') dig = ch-'A'+10;
59 else if (ch >= 'a' && ch <= 'f') dig = ch-'a'+10;
60 else return false;
61 if (dpos >= dest.length*2) return false;
62 if (dpos%2 == 0) dest[dpos/2] = cast(ubyte)(dig<<16); else dest[dpos/2] |= cast(ubyte)dig;
63 ++dpos;
65 return (dpos == dest.length*2);
69 public PubKey decodePubKeyStr (const(char)[] str) {
70 PubKey res;
71 if (!decodeHexStringInto(res[], str)) res[] = toxCoreEmptyKey[];
72 return res;
76 public ToxAddr decodeAddrStr (const(char)[] str) {
77 ToxAddr res = 0;
78 if (!decodeHexStringInto(res[], str)) res[] = toxCoreEmptyAddr[];
79 return res;
83 // ////////////////////////////////////////////////////////////////////////// //
84 final class Group {
85 private:
86 this (Account aOwner) nothrow { acc = aOwner; }
88 private:
89 bool mDirty; // true: write contact's config
90 string diskFileName;
92 public:
93 Account acc;
94 GroupOptions info;
96 public:
97 void markDirty () pure nothrow @safe @nogc => mDirty = true;
99 void save () {
100 import std.path : dirName;
101 // save this contact
102 assert(acc !is null);
103 // create disk name
104 if (diskFileName.length == 0) {
105 diskFileName = acc.basePath~"/contacts/groups.rc";
107 mkdirRec(diskFileName.dirName);
108 if (serialize(info, diskFileName)) mDirty = false;
111 // save if dirty
112 void update () {
113 if (mDirty) save();
116 @property bool visible () const nothrow @trusted @nogc {
117 if (!hideIfNoVisibleMembers) return true; // always visible
118 // check if we have any visible members
119 foreach (const(Contact) c; acc.contacts.byValue) {
120 if (c.gid != info.gid) continue;
121 if (c.visibleNoGroupCheck) return true;
123 return false; // nobody's here
126 private bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
127 enum lo = "info."~fld;
128 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
129 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
132 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
133 enum lo = "info."~fld;
134 if (mixin(lo) >= 0) return mixin(lo);
135 return mixin("acc.info."~fld);
138 @property nothrow @safe {
139 uint gid () const pure @nogc => info.gid;
141 bool opened () const @nogc => info.opened;
142 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
144 string name () const @nogc => info.name;
145 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
147 string note () const @nogc => info.note;
148 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
150 @nogc {
151 bool showOffline () const => getTriOpt!"showOffline";
152 bool showPopup () const => getTriOpt!"showPopup";
153 bool blinkActivity () const => getTriOpt!"blinkActivity";
154 bool skipUnread () const => getTriOpt!"skipUnread";
155 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
156 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
157 int resendRotDays () const => getIntOpt!"resendRotDays";
158 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
164 // ////////////////////////////////////////////////////////////////////////// //
165 final class Contact {
166 public:
167 // `Connecting` for non-account means "awaiting authorization"
169 static struct XMsg {
170 bool isMe; // "/me" message?
171 SysTime time;
172 string text;
173 long msgid; // ==0: unknown yet
174 int resendCount = 0;
176 string textOrig () const pure nothrow @trusted @nogc {
177 int pos = 0;
178 while (pos < text.length && text.ptr[pos] != ']') ++pos;
179 pos += 2;
180 return (pos < text.length ? text[pos..$] : null);
183 bool isOurMark (const(void)[] atext, SysTime atime) nothrow @trusted {
184 pragma(inline, true);
185 return (atime == time && textDigest(textOrig) == textDigest(atext));
188 bool isOurMark (long aid, const(void)[] atext, SysTime atime) nothrow @trusted {
189 pragma(inline, true);
190 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == textDigest(atext)));
193 bool isOurMark (long aid, in ref TextDigest adigest, SysTime atime) nothrow @trusted {
194 pragma(inline, true);
195 return (aid > 0 ? (msgid == aid) : (atime == time && textDigest(textOrig) == adigest));
199 private:
200 MonoTime nextSendTime; // for resend queue
202 private:
203 this (Account aOwner) nothrow { acc = aOwner; edit = new MiniEdit(); }
205 void removeData () nothrow {
206 if (diskFileName.length == 0) return;
207 try {
208 import std.file : rmdirRecurse, rename;
209 import std.path : dirName;
210 auto dn = diskFileName.dirName;
211 if (dn.length == 0 || dn == "/") return; // just in case
212 conwriteln("removing dir <", dn, ">");
213 //rmdirRecurse(dn);
214 rename(dn, "_"~dn);
215 diskFileName = null;
216 mDirty = false;
217 } catch (Exception e) {}
220 private:
221 bool mDirty; // true: write contact's config
223 public:
224 Account acc;
225 string diskFileName;
226 ContactInfo info;
227 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
228 MiniEdit edit;
229 XMsg[] resendQueue;
230 int unreadCount;
232 public:
233 void markDirty () pure nothrow @safe @nogc => mDirty = true;
235 void loadUnreadCount () nothrow {
236 assert(diskFileName.length);
237 assert(acc !is null);
238 try {
239 import std.path : dirName;
240 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
241 unreadCount = fi.readNum!int;
242 } catch (Exception e) {
243 unreadCount = 0;
247 void saveUnreadCount () nothrow {
248 assert(diskFileName.length);
249 assert(acc !is null);
250 try {
251 import std.path : dirName;
252 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
253 fo.writeNum(unreadCount);
254 } catch (Exception e) {
258 void loadResendQueue () {
259 import std.path : dirName;
260 string fname = diskFileName.dirName~"/logs/resend.log";
261 LogFile lf;
262 lf.load(fname);
263 foreach (const ref lmsg; lf.messages) {
264 XMsg xmsg;
265 xmsg.isMe = lmsg.isMe;
266 xmsg.time = lmsg.time;
267 xmsg.text = lmsg.text;
268 xmsg.msgid = 0;
269 resendQueue ~= xmsg;
271 nextSendTime = MonoTime.currTime;
274 void saveResendQueue () {
275 import std.file : remove;
276 import std.path : dirName;
277 assert(diskFileName.length);
278 assert(acc !is null);
279 mkdirRec(diskFileName.dirName~"/logs");
280 string fname = diskFileName.dirName~"/logs/resend.log";
281 try { remove(fname); } catch (Exception e) {}
282 if (resendQueue.length) {
283 foreach (const ref msg; resendQueue) {
284 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
289 void save () {
290 import std.path : dirName;
291 // save this contact
292 assert(acc !is null);
293 // create disk name
294 if (diskFileName.length == 0) {
295 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
296 acc.contacts[info.pubkey] = this;
298 mkdirRec(diskFileName.dirName);
299 mkdirRec(diskFileName.dirName~"/avatars");
300 mkdirRec(diskFileName.dirName~"/files");
301 mkdirRec(diskFileName.dirName~"/fileparts");
302 mkdirRec(diskFileName.dirName~"/logs");
303 saveUnreadCount();
304 if (serialize(info, diskFileName)) mDirty = false;
307 // save if dirty
308 void update () {
309 if (mDirty) save();
312 public:
313 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (!kfd && (optShowOffline || showOffline || acceptPending || requestPending || status != ContactStatus.Offline || unreadCount > 0));
315 @property bool visible () const nothrow @trusted @nogc {
316 if (kfd) return false;
317 if (acceptPending || requestPending || optShowOffline) return true;
318 if (unreadCount > 0) return true;
319 if (!showOffline && status == ContactStatus.Offline) return false;
320 auto grp = acc.groupById(gid);
321 return grp.visible;
324 @property nothrow @safe {
325 bool online () const pure @nogc => (status != ContactStatus.Offline && status != ContactStatus.Connecting);
327 ContactInfo.Kind kind () const @nogc => info.kind;
328 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
330 uint gid () const @nogc => info.gid;
331 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
333 string nick () const @nogc => info.nick;
334 void nick (string v) @nogc { pragma(inline, true); if (info.nick != v) { info.nick = v; markDirty(); } }
336 string visnick () const @nogc => info.visnick;
337 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
339 string displayNick () const @nogc => (info.visnick.length ? info.visnick : (info.nick.length ? info.nick : "<unknown>"));
341 string statusmsg () const @nogc => info.statusmsg;
342 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
344 bool kfd () const @nogc => (info.kind == ContactInfo.Kind.KillFuckDie);
345 bool friend () const @nogc => (info.kind == ContactInfo.Kind.Friend);
346 bool acceptPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthAccept);
347 bool requestPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthRequest);
349 void setLastOnlineNow () {
350 try {
351 auto ut = systimeNow.toUnixTime();
352 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
353 } catch (Exception e) {}
356 string note () const @nogc => info.note;
357 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
360 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
361 enum lo = "info.opts."~fld;
362 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
363 auto grp = acc.groupById(info.gid);
364 enum go = "grp.info."~fld;
365 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
366 return mixin("acc.info."~fld);
369 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
370 enum lo = "info.opts."~fld;
371 if (mixin(lo) >= 0) return mixin(lo);
372 auto grp = acc.groupById(info.gid);
373 enum go = "grp.info."~fld;
374 if (mixin(go) >= 0) return mixin(go);
375 return mixin("acc.info."~fld);
378 @property nothrow @safe @nogc {
379 bool showOffline () const => getTriOpt!"showOffline";
380 bool showPopup () const => getTriOpt!"showPopup";
381 bool blinkActivity () const => getTriOpt!"blinkActivity";
382 bool skipUnread () const => getTriOpt!"skipUnread";
383 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
384 int resendRotDays () const => getIntOpt!"resendRotDays";
385 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
388 void loadLogInto (ref LogFile lf) {
389 import std.file : exists;
390 import std.path : dirName;
391 string lname = diskFileName.dirName~"/logs/hugelog.log";
392 if (lname.exists) lf.load(lname); else lf.clear();
395 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
396 import std.path : dirName;
397 string lname = diskFileName.dirName~"/logs/hugelog.log";
398 LogFile.appendLine(lname, kind, text, isMe, time);
401 void ackReceived (long msgid) {
402 if (msgid <= 0) return; // wtf?!
403 bool changed = false;
404 usize idx = 0;
405 while (idx < resendQueue.length) {
406 if (resendQueue[idx].msgid == msgid) {
407 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
408 resendQueue[$-1] = XMsg.init;
409 resendQueue.length -= 1;
410 resendQueue.assumeSafeAppend;
411 changed = true;
412 } else {
413 ++idx;
416 if (changed) saveResendQueue();
419 // <0: not found
420 long findInResendQueue (const(char)[] text, SysTime time) {
421 foreach (ref XMsg msg; resendQueue) {
422 if (msg.time == time && msg.textOrig == text) {
423 //conwriteln("<", text, "> found in resend queue; id=", msg.msgid, "; rt=<", msg.text, ">");
424 return msg.msgid;
425 } /*else if (msg.textOrig == text) {
426 conwriteln("<", text, "> (", time, ":", msg.time, ") IS NOT: id=", msg.msgid, "; rt=<", msg.text, ">");
429 return -1;
432 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
433 bool doSave = false;
434 usize pos = 0;
435 while (pos < resendQueue.length) {
436 if (resendQueue[pos].isOurMark(msgid, digest, time)) {
437 foreach (immutable c; pos+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
438 resendQueue[$-1] = XMsg.init;
439 resendQueue.length -= 1;
440 resendQueue.assumeSafeAppend;
441 doSave = true;
442 } else {
443 ++pos;
446 if (doSave) saveResendQueue();
447 return doSave;
450 // called when toxcore goes offline
451 void resetQueueIds () {
452 foreach (ref XMsg msg; resendQueue) msg.msgid = 0;
453 if (activeContact is this) logResetAckMessageIds();
454 nextSendTime = MonoTime.currTime+30.seconds;
457 void processResendQueue () {
458 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
459 bool doSave = false;
460 foreach (ref XMsg msg; resendQueue) {
461 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
462 if (msgid < 0) break;
463 if (activeContact is this) logFixAckMessageId(msg.msgid, msgid, msg.textOrig, msg.time);
464 msg.msgid = msgid;
465 if (msg.resendCount++ != 0) doSave = true;
467 if (doSave) saveResendQueue();
468 nextSendTime = MonoTime.currTime+30.seconds;
471 void send (const(char)[] text) {
472 void sendOne (const(char)[] text, bool action) {
473 if (text.length == 0) return; // just in case
475 SysTime now = systimeNow;
476 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
477 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
479 // add this to resend queue
480 XMsg xmsg;
481 xmsg.isMe = action;
482 xmsg.time = now;
483 xmsg.msgid = msgid; // 0: we are offline
484 xmsg.resendCount = 0;
486 auto ctt = MonoTime.currTime;
487 if (nextSendTime <= ctt) nextSendTime = ctt+30.seconds;
490 import std.datetime;
491 import std.format : format;
492 auto dt = cast(DateTime)now;
493 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
496 resendQueue ~= xmsg;
498 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
499 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
502 bool action = text.startsWith("/me ");
503 if (action) text = text[4..$].xstripleft;
505 while (text.length) {
506 auto ep = text.indexOf('\n');
507 if (ep < 0) ep = text.length; else ++ep; // include '\n'
508 // remove line if it contains only spaces
509 bool hasNonSpace = false;
510 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
511 if (hasNonSpace) break;
512 text = text[ep..$];
514 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
515 if (text.length == 0) return; // nothing to do
517 // now split it
518 //TODO: split at word boundaries
519 enum ReservedSpace = 23+3+3;
521 // k8: toxcore developers are idiots, so we have to do dynalloc here
522 auto tmpbuf = new char[](tox_max_message_length()+64);
523 scope(exit) delete tmpbuf;
525 bool first = true;
526 while (text.length) {
527 int epos = tox_max_message_length()-ReservedSpace;
528 if (epos < text.length) {
529 // find utf start
530 while (epos > 0) {
531 if (text[epos-1] < 128) break;
532 if ((text[epos-1]&0xc0) == 0xc0) break;
533 --epos;
535 } else {
536 epos = cast(int)text.length;
538 assert(epos > 0);
539 if (first && epos >= text.length) {
540 sendOne(text[0..epos], action);
541 } else {
542 int ofs = 0;
543 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
544 tmpbuf[ofs..ofs+epos] = text[0..epos];
545 tmpbuf[ofs+epos..ofs+epos+3] = "...";
546 sendOne(tmpbuf[0..ofs+epos+3], action);
548 first = false;
549 text = text[epos..$];
552 //saveResendQueue();
557 // ////////////////////////////////////////////////////////////////////////// //
558 final class Account {
559 public:
560 void saveGroups () nothrow {
561 import std.algorithm : sort;
562 GroupOptions[] glist;
563 scope(exit) delete glist;
564 foreach (Group g; groups) glist ~= g.info;
565 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
566 glist.serialize(basePath~"contacts/groups.rc");
569 private:
570 ContactStatus mStatus = ContactStatus.Offline;
572 public:
573 PubKey toxpk = toxCoreEmptyKey;
574 string toxDataDiskName;
575 string basePath; // with trailing "/"
576 ProtoOptions protoOpts;
577 AccountConfig info;
578 Group[] groups;
579 Contact[PubKey] contacts;
581 public:
582 bool mIAmConnecting = false;
583 bool mIAmOnline = false;
584 bool forceOnline = true; // set to `false` to stop autoreconnecting
586 public:
587 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
589 @property bool isOnline () const nothrow @safe @nogc {
590 if (!toxpk.isValidKey) return false;
591 if (mIAmConnecting) return false;
592 if (!mIAmOnline) return false;
593 return true;
596 @property ContactStatus status () const nothrow @safe @nogc {
597 if (!toxpk.isValidKey) return ContactStatus.Offline;
598 if (mIAmConnecting) return ContactStatus.Connecting;
599 if (!mIAmOnline) return ContactStatus.Offline;
600 return mStatus;
603 @property void status (ContactStatus v) {
604 if (!toxpk.isValidKey) return;
605 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
606 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
607 forceOnline = (v != ContactStatus.Offline);
608 if (mStatus == ContactStatus.Offline) {
609 if (v != ContactStatus.Offline) mIAmConnecting = true;
611 toxCoreSetStatus(toxpk, v);
612 mStatus = v;
613 glconPostScreenRepaint();
614 fixTrayIcon();
617 void processResendQueue () {
618 if (!isOnline) return;
619 foreach (Contact ct; contacts.byValue) { ct.processResendQueue(); ct.update(); }
622 void saveResendQueue () {
623 foreach (Contact ct; contacts.byValue) { ct.saveResendQueue(); ct.update(); }
626 private:
627 void toxCreate () {
628 toxpk = toxCoreOpenAccount(toxDataDiskName);
629 if (!toxpk.isValidKey) {
630 conwriteln("creating new Tox account...");
631 string nick = info.nick;
632 if (nick.length > 0) {
633 //FIXME: utf
634 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
635 } else {
636 nick = "anonymous";
638 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
639 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
641 conwriteln("my address: [", tox_hex(toxCoreGetSelfAddress(toxpk)), "]");
642 toxLoadKnownContacts();
645 // load contacts from ToxCore data and add 'em to contact database
646 void toxLoadKnownContacts () {
647 if (!toxpk.isValidKey) return;
648 version(none) {
649 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] anick) {
650 string nick = anick.buildNormalizedString!true;
651 auto c = (frpub in contacts ? contacts[frpub] : null);
652 if (c is null) {
653 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
654 c = new Contact(this);
655 c.info.gid = 0;
656 c.info.nick = nick;
657 c.info.pubkey[] = frpub[];
658 c.info.opts.showOffline = TriOption.Default;
659 auto ls = toxCoreLastSeen(self, frpub);
660 if (ls != SysTime.min) {
661 c.info.lastonlinetime = cast(uint)ls.toUnixTime();
663 if (c.info.lastonlinetime == 0 && nick.length == 0) {
664 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
666 contacts[c.info.pubkey] = c;
667 c.save();
668 //HACK!
669 if (clist !is null) clist.buildAccount(this);
670 } else {
671 bool needSave = false;
672 auto ls = toxCoreLastSeen(self, frpub);
673 if (ls != SysTime.min) {
674 auto lsu = cast(uint)ls.toUnixTime();
675 if (c.info.lastonlinetime != lsu) { c.info.lastonlinetime = lsu; needSave = true; }
677 if (c.info.nick != nick) {
678 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
679 if (c.info.nick.length == 0 && nick.length != 0) {
680 needSave = true;
681 c.info.nick = nick;
683 } else {
684 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
686 if (needSave) c.save();
688 return false; // don't stop
690 } else {
691 try {
692 auto data = toxCoreLoadDataFile(VFile(toxDataDiskName));
693 foreach (const ref ci; data.friends) {
694 auto c = (ci.pubkey in contacts ? contacts[ci.pubkey] : null);
695 if (c is null) {
696 conwriteln("NEW friend with pk [", tox_hex(ci.pubkey), "]; name is: ", ci.nick);
697 c = new Contact(this);
698 c.info.gid = 0;
699 c.info.nick = ci.nick;
700 c.info.statusmsg = ci.statusmsg;
701 c.info.lastonlinetime = ci.lastonlinetime;
702 c.info.kind = ci.kind;
703 c.info.pubkey[] = ci.pubkey[];
704 c.info.opts.showOffline = TriOption.Default;
705 contacts[c.info.pubkey] = c;
706 c.save();
707 //HACK!
708 if (clist !is null) clist.buildAccount(this);
709 } else {
710 bool needSave = false;
711 version(none) {
712 if (c.info.nick != ci.nick) {
713 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; new name is: ", ci.nick);
714 } else {
715 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; old name is: ", ci.nick);
718 if (c.info.kind != ci.kind) { c.info.kind = ci.kind; needSave = true; }
719 if (c.info.lastonlinetime != ci.lastonlinetime) { c.info.lastonlinetime = ci.lastonlinetime; needSave = true; }
720 if (c.info.nick.length == 0 && ci.nick.length != 0) { c.info.nick = ci.nick; needSave = true; }
721 if (needSave) c.save();
724 } catch (Exception e) {}
728 public:
729 bool sendFriendRequest (in ref ToxAddr fraddr, const(char)[] msg) {
730 if (!toxpk.isValidKey) return false;
731 if (!isValidAddr(fraddr)) return false;
732 if (msg.length == 0) return false;
733 if (msg.length > tox_max_friend_request_length()) return false;
734 if (!toxCoreSendFriendRequest(toxpk, fraddr, msg)) return false;
735 PubKey frpub = fraddr[0..PubKey.length];
736 auto c = (frpub in contacts ? contacts[frpub] : null);
737 if (c is null) {
738 c = new Contact(this);
739 c.info.gid = 0;
740 c.info.nick = null; // unknown yet
741 c.info.pubkey[] = frpub[];
742 c.info.opts.showOffline = TriOption.Default;
743 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
744 c.info.fraddr[] = fraddr[]; // save address, just in case
745 contacts[c.info.pubkey] = c;
746 c.save();
747 //HACK!
748 if (clist !is null) clist.buildAccount(this);
750 return true;
753 private:
754 // connection established
755 void toxConnectionDropped () {
756 alias timp = this;
757 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
758 mIAmConnecting = false;
759 mIAmOnline = false;
760 auto oldst = mStatus;
761 toxCoreSetStatus(toxpk, ContactStatus.Offline); // this kills toxcore instance
762 foreach (Contact ct; contacts.byValue) {
763 ct.status = ContactStatus.Offline;
764 if (ct.kfd) toxCoreRemoveFriend(toxpk, ct.info.pubkey);
765 // this is not quite correct: we need to distinguish when user goes offline, and when toxcore temporarily goes offline
766 // but we're killing toxcore instance on disconnect, so it works
767 ct.resetQueueIds();
769 if (forceOnline) {
770 mStatus = ContactStatus.Offline;
771 status = oldst;
773 glconPostScreenRepaint();
776 // connection established
777 void toxConnectionEstablished () {
778 alias timp = this;
779 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
780 mIAmConnecting = false;
781 mIAmOnline = true;
782 if (info.statusmsg.length == 0) info.statusmsg = "Come taste the gasoline! [BioAcid]";
783 toxCoreSetStatusMessage(toxpk, info.statusmsg);
784 //toxLoadKnownContacts();
785 glconPostScreenRepaint();
788 void toxFriendOffline (in ref PubKey fpk) {
789 if (auto ct = fpk in contacts) {
790 auto ls = toxCoreLastSeen(toxpk, fpk);
791 if (ls != SysTime.min) {
792 auto lsu = cast(uint)ls.toUnixTime();
793 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); glconPostScreenRepaint(); }
795 if (ct.status != ContactStatus.Offline) {
796 conwriteln("friend <", ct.info.nick, "> gone offline");
797 ct.status = ContactStatus.Offline;
798 glconPostScreenRepaint();
803 void toxSelfStatus (ContactStatus cst) {
804 if (mStatus != cst) {
805 mStatus = cst;
806 glconPostScreenRepaint();
810 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
811 if (auto ct = fpk in contacts) {
812 if (ct.kfd) return;
813 if (ct.status != cst) {
814 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
815 auto ls = toxCoreLastSeen(toxpk, fpk);
816 if (ls != SysTime.min) {
817 auto lsu = cast(uint)ls.toUnixTime();
818 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); }
820 ct.status = cst;
821 // if it is online, and it is not a friend, turn it into a friend
822 if (ct.online && !ct.friend) ct.kind = ContactInfo.Kind.Friend;
823 if (ct.online) ct.processResendQueue(); // why not?
824 glconPostScreenRepaint();
829 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
830 if (auto ct = fpk in contacts) {
831 msg = msg.xstrip();
832 if (ct.info.statusmsg != msg) {
833 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
834 ct.info.statusmsg = msg;
835 ct.save();
836 glconPostScreenRepaint();
841 void toxFriendNickChanged (in ref PubKey fpk, string nick) {
842 if (auto ct = fpk in contacts) {
843 nick = nick.xstrip();
844 if (nick.length && ct.info.nick != nick) {
845 ct.info.nick = nick;
846 ct.save();
847 glconPostScreenRepaint();
852 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
853 alias timp = this;
854 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
855 if (auto ct = fpk in contacts) {
856 if (ct.kfd) return;
857 // if not waiting for acceptance, force-friend it
858 if (!ct.acceptPending) {
859 conwriteln("force-friend <", ct.info.nick, ">");
860 toxCoreAddFriend(toxpk, fpk);
861 } else {
862 ct.kind = ContactInfo.Kind.PengingAuthAccept;
863 ct.info.statusmsg = msg.idup;
865 ct.setLastOnlineNow();
866 ct.save();
867 } else {
868 // new friend request
869 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk), "]");
870 auto c = new Contact(this);
871 c.info.gid = 0;
872 c.info.nick = null;
873 c.info.pubkey[] = fpk[];
874 c.info.opts.showOffline = TriOption.Default;
875 c.info.statusmsg = msg.idup;
876 c.kind = ContactInfo.Kind.PengingAuthAccept;
877 contacts[c.info.pubkey] = c;
878 c.setLastOnlineNow();
879 c.save();
880 //HACK!
881 if (clist !is null) clist.buildAccount(this);
883 glconPostScreenRepaint();
886 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
887 if (auto ct = fpk in contacts) {
888 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
889 ct.appendToLog(kind, msg, action, time);
890 if (*ct is activeContact) {
891 // if inactive or invisible, add divider line and increase unread count
892 if (!mainWindowVisible || !mainWindowActive) {
893 if (ct.unreadCount == 0) addDividerLine();
894 ct.unreadCount += 1;
895 ct.saveUnreadCount();
897 addTextToLog(this, *ct, kind, action, msg, time);
898 } else {
899 ct.unreadCount += 1;
900 ct.saveUnreadCount();
905 // ack for sent message
906 void toxMessageAck (in ref PubKey fpk, long msgid) {
907 alias timp = this;
908 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
909 if (auto ct = fpk in contacts) {
910 if (*ct is activeContact) ackLogMessage(msgid);
911 ct.ackReceived(msgid);
915 private:
916 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
918 public:
919 void save () {
920 serialize(info, basePath~"config.rc");
923 public:
924 this (string aBaseDir) {
925 import std.algorithm : sort;
926 import std.file : DirEntry, SpanMode, dirEntries;
927 import std.path : baseName;
929 basePath = normalizeBaseDir(aBaseDir);
930 toxDataDiskName = basePath~"toxdata.tox";
931 protoOpts.txtunser(VFile(basePath~"proto.rc"));
932 info.txtunser(VFile(basePath~"config.rc"));
934 // load groups
935 GroupOptions[] glist;
936 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
937 bool hasDefaultGroup = false;
938 bool hasMoronsGroup = false;
939 foreach (ref GroupOptions gi; glist[]) {
940 auto g = new Group(this);
941 g.info = gi;
942 bool found = false;
943 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
944 if (!found) groups ~= g;
945 if (g.gid == 0) hasDefaultGroup = true;
946 if (g.gid == g.gid.max-2) hasMoronsGroup = true;
949 // create default group if necessary
950 if (!hasDefaultGroup) {
951 GroupOptions gi;
952 gi.gid = 0;
953 gi.name = "default";
954 gi.note = "default group for new contacts";
955 gi.opened = true;
956 auto g = new Group(this);
957 g.info = gi;
958 groups ~= g;
961 // create morons group if necessary
962 if (!hasMoronsGroup) {
963 GroupOptions gi;
964 gi.gid = gi.gid.max-2;
965 gi.name = "<morons>";
966 gi.note = "group for completely ignored dumbfucks";
967 gi.opened = false;
968 gi.showOffline = TriOption.No;
969 gi.showPopup = TriOption.No;
970 gi.blinkActivity = TriOption.No;
971 gi.skipUnread = TriOption.Yes;
972 gi.hideIfNoVisibleMembers = TriOption.Yes;
973 gi.ftranAllowed = TriOption.No;
974 gi.resendRotDays = 0;
975 gi.hmcOnOpen = 0;
976 auto g = new Group(this);
977 g.info = gi;
978 groups ~= g;
979 //saveGroups();
982 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
984 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
986 static bool isValidCDName (const(char)[] str) {
987 if (str.length != 64) return false;
988 foreach (immutable char ch; str) {
989 if (ch >= '0' && ch <= '9') continue;
990 if (ch >= 'A' && ch <= 'F') continue;
991 if (ch >= 'a' && ch <= 'f') continue;
992 return false;
994 return true;
997 // load contacts
998 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
999 if (de.name.baseName == "." || de.name.baseName == ".." || !isValidCDName(de.name.baseName)) continue;
1000 try {
1001 import std.file : exists;
1002 if (!de.isDir) continue;
1003 string cfgfn = de.name~"/config.rc";
1004 if (!cfgfn.exists) continue;
1005 ContactInfo ci;
1006 ci.txtunser(VFile(cfgfn));
1007 auto c = new Contact(this);
1008 c.diskFileName = cfgfn;
1009 c.info = ci;
1010 contacts[c.info.pubkey] = c;
1011 c.loadResendQueue();
1012 c.loadUnreadCount();
1013 // fix contact group
1014 if (groupById!false(c.gid) is null) {
1015 c.info.gid = 0; // move to default group
1016 c.save();
1018 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1019 } catch (Exception e) {
1020 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1024 toxCreate();
1025 assert(toxpk.isValidKey, "something is VERY wrong here");
1026 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
1029 ~this () {
1030 if (toxpk.isValidKey) {
1031 toxCoreCloseAccount(toxpk);
1032 toxpk[] = toxCoreEmptyKey[];
1036 // will not write contact to disk
1037 Contact createEmptyContact () {
1038 auto c = new Contact(this);
1039 c.info.gid = 0;
1040 c.info.nick = "test contact";
1041 c.info.pubkey[] = 0;
1042 return c;
1045 // returns `null` if there is no such group, and `dofail` is `true`
1046 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
1047 foreach (const Group g; groups) if (g.gid == agid) return cast(inout)g;
1048 static if (dofail) assert(0, "group not found"); else return null;
1051 int opApply () (scope int delegate (ref Contact ct) dg) {
1052 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
1053 return 0;
1056 // returns gid, or uint.max on error
1057 uint findGroupByName (const(char)[] name) nothrow {
1058 if (name.length == 0) return uint.max;
1059 foreach (const Group g; groups) if (g.name == name) return g.gid;
1060 return uint.max;
1063 // returns gid, or uint.max on error
1064 uint createGroup(T:const(char)[]) (T name) nothrow {
1065 if (name.length == 0) return uint.max;
1066 // groups are sorted by gid, yeah
1067 uint newgid = 0;
1068 foreach (const Group g; groups) {
1069 if (g.name == name) return g.gid;
1070 if (newgid == g.gid) newgid = g.gid+1;
1072 if (newgid == uint.max) return uint.max;
1073 // create new group
1074 GroupOptions gi;
1075 gi.gid = newgid;
1076 static if (is(T == string)) gi.name = name; else gi.name = name.idup;
1077 gi.opened = true;
1078 auto g = new Group(this);
1079 g.info = gi;
1080 groups ~= g;
1081 import std.algorithm : sort;
1082 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1083 saveGroups();
1084 return gi.gid;
1087 // returns `false` on any error
1088 bool moveContactToGroup (Contact ct, uint gid) nothrow {
1089 if (ct is null || gid == uint.max) return false;
1090 // check if this is our contact
1091 bool found = false;
1092 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1093 if (!found) return false;
1094 // find group
1095 Group grp = null;
1096 foreach (Group g; groups) if (g.gid == gid) { grp = g; break; }
1097 if (grp is null) return false;
1098 // move it
1099 if (ct.info.gid != gid) {
1100 ct.info.gid = gid;
1101 ct.markDirty();
1103 return true;
1106 // returns `false` on any error
1107 bool removeContact (Contact ct) nothrow {
1108 if (ct is null || !isOnline) return false;
1109 // check if this is our contact
1110 bool found = false;
1111 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1112 if (!found) return false;
1113 if (!toxCoreRemoveFriend(toxpk, ct.info.pubkey)) return false;
1114 ct.removeData();
1115 contacts.remove(ct.info.pubkey);
1117 ct.kind = ContactInfo.Kind.KillFuckDie;
1118 ct.markDirty();
1120 return true;
1123 private:
1124 static string normalizeBaseDir (string aBaseDir) {
1125 import std.path : absolutePath, expandTilde;
1126 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir[$-1] == '/') throw new Exception("invalid base dir");
1127 if (aBaseDir.indexOf('/') < 0) aBaseDir = "~/.bioacid/"~aBaseDir;
1128 aBaseDir = aBaseDir.expandTilde.absolutePath;
1129 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1130 return aBaseDir;
1133 public:
1134 static Account CreateNew (string aBaseDir, string aAccName) {
1135 aBaseDir = normalizeBaseDir(aBaseDir);
1136 mkdirRec(aBaseDir~"contacts");
1137 // write protocol options
1139 ProtoOptions popt;
1140 serialize(popt, aBaseDir~"proto.rc");
1142 // account options
1144 AccountConfig acc;
1145 acc.nick = aAccName;
1146 acc.showPopup = true;
1147 acc.blinkActivity = true;
1148 acc.hideEmptyGroups = false;
1149 acc.ftranAllowed = true;
1150 acc.resendRotDays = 4;
1151 acc.hmcOnOpen = 10;
1152 serialize(acc, aBaseDir~"config.rc");
1154 // create default group
1156 GroupOptions[1] grp;
1157 grp[0].gid = 0;
1158 grp[0].name = "default";
1159 grp[0].opened = true;
1160 //grp[0].hideIfNoVisible = TriOption.Yes;
1161 serialize(grp[], aBaseDir~"contacts/groups.rc");
1163 // now load it
1164 return new Account(aBaseDir);
1169 // ////////////////////////////////////////////////////////////////////////// //
1170 void setupToxEventListener (SimpleWindow sdmain) {
1171 assert(sdmain !is null);
1173 sdmain.addEventListener((ToxEventBase evt) {
1174 auto acc = clist.accountByPK(evt.self);
1176 if (acc is null) return;
1178 bool fixTray = false;
1179 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1181 // connection?
1182 if (auto e = cast(ToxEventConnection)evt) {
1183 if (e.who[] == acc.toxpk[]) {
1184 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1185 fixTray = true;
1186 } else {
1187 if (!e.connected) acc.toxFriendOffline(e.who);
1189 return;
1191 // status?
1192 if (auto e = cast(ToxEventStatus)evt) {
1193 if (e.who[] == acc.toxpk[]) {
1194 acc.toxSelfStatus(e.status);
1195 fixTray = true;
1196 } else {
1197 acc.toxFriendStatus(e.who, e.status);
1199 return;
1201 // status message?
1202 if (auto e = cast(ToxEventStatusMsg)evt) {
1203 if (e.who[] != acc.toxpk[]) acc.toxFriendStatusMessage(e.who, e.message);
1204 return;
1206 // new nick?
1207 if (auto e = cast(ToxEventNick)evt) {
1208 if (e.who[] != acc.toxpk[]) acc.toxFriendNickChanged(e.who, e.nick);
1209 return;
1211 // incoming text message?
1212 if (auto e = cast(ToxEventMessage)evt) {
1213 if (e.who[] != acc.toxpk[]) {
1214 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1215 fixTray = true;
1217 return;
1219 // ack outgoing text message?
1220 if (auto e = cast(ToxEventMessageAck)evt) {
1221 if (e.who[] != acc.toxpk[]) acc.toxMessageAck(e.who, e.msgid);
1222 return;
1225 //glconProcessEventMessage();