some changes to log view colors
[bioacid.git] / accobj.d
blob10924fd1043328f6f96650f3dab3274ae1447a53
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.file : mkdirRecurse;
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 mkdirRecurse(diskFileName.dirName);
109 if (serialize(info, diskFileName)) mDirty = false;
112 // save if dirty
113 void update () {
114 if (mDirty) save();
117 @property bool visible () const nothrow @trusted @nogc {
118 if (!hideIfNoVisibleMembers) return true; // always visible
119 // check if we have any visible members
120 foreach (const(Contact) c; acc.contacts.byValue) {
121 if (c.gid != info.gid) continue;
122 if (c.visibleNoGroupCheck) return true;
124 return false; // nobody's here
127 private bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
128 enum lo = "info."~fld;
129 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
130 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
133 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
134 enum lo = "info."~fld;
135 if (mixin(lo) >= 0) return mixin(lo);
136 return mixin("acc.info."~fld);
139 @property nothrow @safe {
140 uint gid () const pure @nogc => info.gid;
142 bool opened () const @nogc => info.opened;
143 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
145 string name () const @nogc => info.name;
146 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
148 string note () const @nogc => info.note;
149 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
151 @nogc {
152 bool showOffline () const => getTriOpt!"showOffline";
153 bool showPopup () const => getTriOpt!"showPopup";
154 bool blinkActivity () const => getTriOpt!"blinkActivity";
155 bool skipUnread () const => getTriOpt!"skipUnread";
156 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
157 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
158 int resendRotDays () const => getIntOpt!"resendRotDays";
159 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
165 // ////////////////////////////////////////////////////////////////////////// //
166 final class Contact {
167 public:
168 // `Connecting` for non-account means "awaiting authorization"
170 static struct XMsg {
171 bool isMe; // "/me" message?
172 SysTime time;
173 string text;
174 long msgid; // ==0: unknown yet
175 int resendCount = 0;
177 MonoTime nextSendTime;
180 private:
181 this (Account aOwner) nothrow { acc = aOwner; edit = new MiniEdit(); }
183 void removeData () nothrow {
184 if (diskFileName.length == 0) return;
185 try {
186 import std.file : rmdirRecurse, rename;
187 import std.path : dirName;
188 auto dn = diskFileName.dirName;
189 if (dn.length == 0 || dn == "/") return; // just in case
190 conwriteln("removing dir <", dn, ">");
191 //rmdirRecurse(dn);
192 rename(dn, "_"~dn);
193 diskFileName = null;
194 mDirty = false;
195 } catch (Exception e) {}
198 private:
199 bool mDirty; // true: write contact's config
201 public:
202 Account acc;
203 string diskFileName;
204 ContactInfo info;
205 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
206 MiniEdit edit;
207 XMsg[] resendQueue;
208 int unreadCount;
210 public:
211 void markDirty () pure nothrow @safe @nogc => mDirty = true;
213 void loadUnreadCount () nothrow {
214 assert(diskFileName.length);
215 assert(acc !is null);
216 try {
217 import std.path : dirName;
218 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
219 unreadCount = fi.readNum!int;
220 } catch (Exception e) {
221 unreadCount = 0;
225 void saveUnreadCount () nothrow {
226 assert(diskFileName.length);
227 assert(acc !is null);
228 try {
229 import std.path : dirName;
230 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
231 fo.writeNum(unreadCount);
232 } catch (Exception e) {
236 void loadResendQueue () {
237 import std.path : dirName;
238 string fname = diskFileName.dirName~"/logs/resend.log";
239 LogFile lf;
240 lf.load(fname);
241 auto ctt = MonoTime.currTime;
242 foreach (const ref lmsg; lf.messages) {
243 XMsg xmsg;
244 xmsg.isMe = lmsg.isMe;
245 xmsg.time = lmsg.time;
246 xmsg.text = lmsg.text;
247 xmsg.msgid = 0;
248 xmsg.nextSendTime = ctt;
249 resendQueue ~= xmsg;
253 void saveResendQueue () {
254 import std.file : mkdirRecurse, remove;
255 import std.path : dirName;
256 assert(diskFileName.length);
257 assert(acc !is null);
258 mkdirRecurse(diskFileName.dirName~"/logs");
259 string fname = diskFileName.dirName~"/logs/resend.log";
260 try { remove(fname); } catch (Exception e) {}
261 if (resendQueue.length) {
262 foreach (const ref msg; resendQueue) {
263 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
268 void save () {
269 import std.file : mkdirRecurse;
270 import std.path : dirName;
271 // save this contact
272 assert(acc !is null);
273 // create disk name
274 if (diskFileName.length == 0) {
275 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
276 acc.contacts[info.pubkey] = this;
278 mkdirRecurse(diskFileName.dirName);
279 mkdirRecurse(diskFileName.dirName~"/avatars");
280 mkdirRecurse(diskFileName.dirName~"/files");
281 mkdirRecurse(diskFileName.dirName~"/fileparts");
282 mkdirRecurse(diskFileName.dirName~"/logs");
283 saveUnreadCount();
284 if (serialize(info, diskFileName)) mDirty = false;
287 // save if dirty
288 void update () {
289 if (mDirty) save();
292 public:
293 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (!kfd && (optShowOffline || showOffline || acceptPending || requestPending || status != ContactStatus.Offline || unreadCount > 0));
295 @property bool visible () const nothrow @trusted @nogc {
296 if (kfd) return false;
297 if (acceptPending || requestPending || optShowOffline) return true;
298 if (unreadCount > 0) return true;
299 if (!showOffline && status == ContactStatus.Offline) return false;
300 auto grp = acc.groupById(gid);
301 return grp.visible;
304 @property nothrow @safe {
305 bool online () const pure @nogc => (status != ContactStatus.Offline && status != ContactStatus.Connecting);
307 ContactInfo.Kind kind () const @nogc => info.kind;
308 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
310 uint gid () const @nogc => info.gid;
311 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
313 string nick () const @nogc => info.nick;
314 void nick (string v) @nogc { pragma(inline, true); if (info.nick != v) { info.nick = v; markDirty(); } }
316 string visnick () const @nogc => info.visnick;
317 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
319 string displayNick () const @nogc => (info.visnick.length ? info.visnick : (info.nick.length ? info.nick : "<unknown>"));
321 string statusmsg () const @nogc => info.statusmsg;
322 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
324 bool kfd () const @nogc => (info.kind == ContactInfo.Kind.KillFuckDie);
325 bool friend () const @nogc => (info.kind == ContactInfo.Kind.Friend);
326 bool acceptPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthAccept);
327 bool requestPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthRequest);
329 void setLastOnlineNow () {
330 try {
331 auto ut = Clock.currTime.toUTC().toUnixTime();
332 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
333 } catch (Exception e) {}
336 string note () const @nogc => info.note;
337 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
340 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
341 enum lo = "info.opts."~fld;
342 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
343 auto grp = acc.groupById(info.gid);
344 enum go = "grp.info."~fld;
345 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
346 return mixin("acc.info."~fld);
349 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
350 enum lo = "info.opts."~fld;
351 if (mixin(lo) >= 0) return mixin(lo);
352 auto grp = acc.groupById(info.gid);
353 enum go = "grp.info."~fld;
354 if (mixin(go) >= 0) return mixin(go);
355 return mixin("acc.info."~fld);
358 @property nothrow @safe @nogc {
359 bool showOffline () const => getTriOpt!"showOffline";
360 bool showPopup () const => getTriOpt!"showPopup";
361 bool blinkActivity () const => getTriOpt!"blinkActivity";
362 bool skipUnread () const => getTriOpt!"skipUnread";
363 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
364 int resendRotDays () const => getIntOpt!"resendRotDays";
365 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
368 void loadLogInto (ref LogFile lf) {
369 import std.file : exists;
370 import std.path : dirName;
371 string lname = diskFileName.dirName~"/logs/hugelog.log";
372 if (lname.exists) lf.load(lname); else lf.clear();
375 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
376 import std.path : dirName;
377 string lname = diskFileName.dirName~"/logs/hugelog.log";
378 LogFile.appendLine(lname, kind, text, isMe, time);
381 void ackReceived (long msgid) {
382 if (msgid <= 0) return; // wtf?!
383 bool changed = false;
384 usize idx = 0;
385 while (idx < resendQueue.length) {
386 if (resendQueue[idx].msgid == msgid) {
387 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
388 resendQueue[$-1] = XMsg.init;
389 resendQueue.length -= 1;
390 resendQueue.assumeSafeAppend;
391 changed = true;
392 } else {
393 ++idx;
396 if (changed) saveResendQueue();
399 void processResendQueue () {
400 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
401 bool doSave = false;
402 auto ctt = MonoTime.currTime+30.seconds;
403 foreach (ref XMsg msg; resendQueue) {
404 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
405 if (msgid < 0) break;
406 msg.msgid = msgid;
407 msg.nextSendTime = ctt;
408 if (msg.resendCount++ != 0) doSave = true;
410 if (doSave) saveResendQueue();
413 void send (const(char)[] text) {
414 void sendOne (const(char)[] text, bool action) {
415 if (text.length == 0) return; // just in case
417 SysTime now = Clock.currTime;
418 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
419 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
421 // add this to resend queue
422 XMsg xmsg;
423 xmsg.isMe = action;
424 xmsg.time = now;
425 xmsg.msgid = msgid;
426 xmsg.nextSendTime = MonoTime.currTime+30.seconds;
427 xmsg.resendCount = 0;
430 import std.datetime;
431 import std.format : format;
432 auto dt = cast(DateTime)now;
433 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
436 resendQueue ~= xmsg;
438 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
439 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
442 bool action = text.startsWith("/me ");
443 if (action) text = text[4..$].xstripleft;
445 while (text.length) {
446 auto ep = text.indexOf('\n');
447 if (ep < 0) ep = text.length; else ++ep; // include '\n'
448 // remove line if it contains only spaces
449 bool hasNonSpace = false;
450 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
451 if (hasNonSpace) break;
452 text = text[ep..$];
454 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
455 if (text.length == 0) return; // nothing to do
457 // now split it
458 //TODO: split at word boundaries
459 enum ReservedSpace = 23+3+3;
461 // k8: toxcore developers are idiots, so we have to do dynalloc here
462 auto tmpbuf = new char[](tox_max_message_length()+64);
463 scope(exit) delete tmpbuf;
465 bool first = true;
466 while (text.length) {
467 int epos = tox_max_message_length()-ReservedSpace;
468 if (epos < text.length) {
469 // find utf start
470 while (epos > 0) {
471 if (text[epos-1] < 128) break;
472 if ((text[epos-1]&0xc0) == 0xc0) break;
473 --epos;
475 } else {
476 epos = cast(int)text.length;
478 assert(epos > 0);
479 if (first && epos >= text.length) {
480 sendOne(text[0..epos], action);
481 } else {
482 int ofs = 0;
483 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
484 tmpbuf[ofs..ofs+epos] = text[0..epos];
485 tmpbuf[ofs+epos..ofs+epos+3] = "...";
486 sendOne(tmpbuf[0..ofs+epos+3], action);
488 first = false;
489 text = text[epos..$];
492 //saveResendQueue();
497 // ////////////////////////////////////////////////////////////////////////// //
498 final class Account {
499 public:
500 void saveGroups () nothrow {
501 import std.algorithm : sort;
502 GroupOptions[] glist;
503 scope(exit) delete glist;
504 foreach (Group g; groups) glist ~= g.info;
505 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
506 glist.serialize(basePath~"contacts/groups.rc");
509 private:
510 ContactStatus mStatus = ContactStatus.Offline;
512 public:
513 PubKey toxpk = toxCoreEmptyKey;
514 string toxDataDiskName;
515 string basePath; // with trailing "/"
516 ProtoOptions protoOpts;
517 AccountConfig info;
518 Group[] groups;
519 Contact[PubKey] contacts;
521 public:
522 bool mIAmConnecting = false;
523 bool mIAmOnline = false;
524 bool forceOnline = true; // set to `false` to stop autoreconnecting
526 public:
527 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
529 @property bool isOnline () const nothrow @safe @nogc {
530 if (!toxpk.isValidKey) return false;
531 if (mIAmConnecting) return false;
532 if (!mIAmOnline) return false;
533 return true;
536 @property ContactStatus status () const nothrow @safe @nogc {
537 if (!toxpk.isValidKey) return ContactStatus.Offline;
538 if (mIAmConnecting) return ContactStatus.Connecting;
539 if (!mIAmOnline) return ContactStatus.Offline;
540 return mStatus;
543 @property void status (ContactStatus v) {
544 if (!toxpk.isValidKey) return;
545 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
546 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
547 forceOnline = (v != ContactStatus.Offline);
548 if (mStatus == ContactStatus.Offline) {
549 if (v != ContactStatus.Offline) mIAmConnecting = true;
551 toxCoreSetStatus(toxpk, v);
552 mStatus = v;
553 glconPostScreenRepaint();
554 fixTrayIcon();
557 void processResendQueue () {
558 if (!isOnline) return;
559 foreach (Contact ct; contacts.byValue) { ct.processResendQueue(); ct.update(); }
562 void saveResendQueue () {
563 foreach (Contact ct; contacts.byValue) { ct.saveResendQueue(); ct.update(); }
566 private:
567 void toxCreate () {
568 toxpk = toxCoreOpenAccount(toxDataDiskName);
569 if (!toxpk.isValidKey) {
570 conwriteln("creating new Tox account...");
571 string nick = info.nick;
572 if (nick.length > 0) {
573 //FIXME: utf
574 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
575 } else {
576 nick = "anonymous";
578 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
579 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
581 conwriteln("my address: [", tox_hex(toxCoreGetSelfAddress(toxpk)), "]");
582 toxLoadKnownContacts();
585 // load contacts from ToxCore data and add 'em to contact database
586 void toxLoadKnownContacts () {
587 if (!toxpk.isValidKey) return;
588 version(none) {
589 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] nick) {
590 auto c = (frpub in contacts ? contacts[frpub] : null);
591 if (c is null) {
592 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
593 c = new Contact(this);
594 c.info.gid = 0;
595 c.info.nick = nick.idup;
596 c.info.pubkey[] = frpub[];
597 c.info.opts.showOffline = TriOption.Default;
598 auto ls = toxCoreLastSeen(self, frpub);
599 if (ls != SysTime.min) {
600 c.info.lastonlinetime = cast(uint)ls.toUnixTime();
602 if (c.info.lastonlinetime == 0 && nick.length == 0) {
603 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
605 contacts[c.info.pubkey] = c;
606 c.save();
607 //HACK!
608 if (clist !is null) clist.buildAccount(this);
609 } else {
610 bool needSave = false;
611 auto ls = toxCoreLastSeen(self, frpub);
612 if (ls != SysTime.min) {
613 auto lsu = cast(uint)ls.toUnixTime();
614 if (c.info.lastonlinetime != lsu) { c.info.lastonlinetime = lsu; needSave = true; }
616 if (c.info.nick != nick) {
617 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
618 if (c.info.nick.length == 0 && nick.length != 0) {
619 needSave = true;
620 c.info.nick = nick.idup;
622 } else {
623 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
625 if (needSave) c.save();
627 return false; // don't stop
629 } else {
630 try {
631 auto data = toxCoreLoadDataFile(VFile(toxDataDiskName));
632 foreach (const ref ci; data.friends) {
633 auto c = (ci.pubkey in contacts ? contacts[ci.pubkey] : null);
634 if (c is null) {
635 conwriteln("NEW friend with pk [", tox_hex(ci.pubkey), "]; name is: ", ci.nick);
636 c = new Contact(this);
637 c.info.gid = 0;
638 c.info.nick = ci.nick;
639 c.info.statusmsg = ci.statusmsg;
640 c.info.lastonlinetime = ci.lastonlinetime;
641 c.info.kind = ci.kind;
642 c.info.pubkey[] = ci.pubkey[];
643 c.info.opts.showOffline = TriOption.Default;
644 contacts[c.info.pubkey] = c;
645 c.save();
646 //HACK!
647 if (clist !is null) clist.buildAccount(this);
648 } else {
649 bool needSave = false;
650 version(none) {
651 if (c.info.nick != ci.nick) {
652 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; new name is: ", ci.nick);
653 } else {
654 conwriteln("OLD friend with pk [", tox_hex(ci.pubkey), "]; old name is: ", ci.nick);
657 if (c.info.kind != ci.kind) { c.info.kind = ci.kind; needSave = true; }
658 if (c.info.lastonlinetime != ci.lastonlinetime) { c.info.lastonlinetime = ci.lastonlinetime; needSave = true; }
659 if (c.info.nick.length == 0 && ci.nick.length != 0) { c.info.nick = ci.nick; needSave = true; }
660 if (needSave) c.save();
663 } catch (Exception e) {}
667 public:
668 bool sendFriendRequest (in ref ToxAddr fraddr, const(char)[] msg) {
669 if (!toxpk.isValidKey) return false;
670 if (!isValidAddr(fraddr)) return false;
671 if (msg.length == 0) return false;
672 if (msg.length > tox_max_friend_request_length()) return false;
673 if (!toxCoreSendFriendRequest(toxpk, fraddr, msg)) return false;
674 PubKey frpub = fraddr[0..PubKey.length];
675 auto c = (frpub in contacts ? contacts[frpub] : null);
676 if (c is null) {
677 c = new Contact(this);
678 c.info.gid = 0;
679 c.info.nick = null; // unknown yet
680 c.info.pubkey[] = frpub[];
681 c.info.opts.showOffline = TriOption.Default;
682 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
683 c.info.fraddr[] = fraddr[]; // save address, just in case
684 contacts[c.info.pubkey] = c;
685 c.save();
686 //HACK!
687 if (clist !is null) clist.buildAccount(this);
689 return true;
692 private:
693 // connection established
694 void toxConnectionDropped () {
695 alias timp = this;
696 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
697 mIAmConnecting = false;
698 mIAmOnline = false;
699 if (forceOnline) {
700 auto oldst = mStatus;
701 mStatus = ContactStatus.Offline;
702 status = oldst;
704 foreach (Contact ct; contacts.byValue) {
705 ct.status = ContactStatus.Offline;
706 if (ct.kfd) toxCoreRemoveFriend(toxpk, ct.info.pubkey);
708 glconPostScreenRepaint();
711 // connection established
712 void toxConnectionEstablished () {
713 alias timp = this;
714 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
715 mIAmConnecting = false;
716 mIAmOnline = true;
717 if (info.statusmsg.length == 0) info.statusmsg = "Come taste the gasoline! [BioAcid]";
718 toxCoreSetStatusMessage(toxpk, info.statusmsg);
719 //toxLoadKnownContacts();
720 glconPostScreenRepaint();
723 void toxFriendOffline (in ref PubKey fpk) {
724 if (auto ct = fpk in contacts) {
725 auto ls = toxCoreLastSeen(toxpk, fpk);
726 if (ls != SysTime.min) {
727 auto lsu = cast(uint)ls.toUnixTime();
728 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); glconPostScreenRepaint(); }
730 if (ct.status != ContactStatus.Offline) {
731 conwriteln("friend <", ct.info.nick, "> gone offline");
732 ct.status = ContactStatus.Offline;
733 glconPostScreenRepaint();
738 void toxSelfStatus (ContactStatus cst) {
739 if (mStatus != cst) {
740 mStatus = cst;
741 glconPostScreenRepaint();
745 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
746 if (auto ct = fpk in contacts) {
747 if (ct.kfd) return;
748 if (ct.status != cst) {
749 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
750 auto ls = toxCoreLastSeen(toxpk, fpk);
751 if (ls != SysTime.min) {
752 auto lsu = cast(uint)ls.toUnixTime();
753 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); }
755 ct.status = cst;
756 // if it is online, and it is not a friend, turn it into a friend
757 if (ct.online && !ct.friend) ct.kind = ContactInfo.Kind.Friend;
758 if (ct.online) ct.processResendQueue(); // why not?
759 glconPostScreenRepaint();
764 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
765 if (auto ct = fpk in contacts) {
766 if (ct.info.statusmsg != msg) {
767 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
768 ct.info.statusmsg = msg;
769 ct.save();
770 glconPostScreenRepaint();
775 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
776 alias timp = this;
777 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
778 if (auto ct = fpk in contacts) {
779 if (ct.kfd) return;
780 // if not waiting for acceptance, force-friend it
781 if (!ct.acceptPending) {
782 conwriteln("force-friend <", ct.info.nick, ">");
783 toxCoreAddFriend(toxpk, fpk);
784 } else {
785 ct.kind = ContactInfo.Kind.PengingAuthAccept;
786 ct.info.statusmsg = msg.idup;
788 ct.setLastOnlineNow();
789 ct.save();
790 } else {
791 // new friend request
792 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk), "]");
793 auto c = new Contact(this);
794 c.info.gid = 0;
795 c.info.nick = null;
796 c.info.pubkey[] = fpk[];
797 c.info.opts.showOffline = TriOption.Default;
798 c.info.statusmsg = msg.idup;
799 c.kind = ContactInfo.Kind.PengingAuthAccept;
800 contacts[c.info.pubkey] = c;
801 c.setLastOnlineNow();
802 c.save();
803 //HACK!
804 if (clist !is null) clist.buildAccount(this);
806 glconPostScreenRepaint();
809 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
810 if (auto ct = fpk in contacts) {
811 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
812 ct.appendToLog(kind, msg, action, time);
813 if (*ct is activeContact) {
814 // if inactive or invisible, add divider line and increase unread count
815 if (!mainWindowVisible || !mainWindowActive) {
816 if (ct.unreadCount == 0) addDividerLine();
817 ct.unreadCount += 1;
818 ct.saveUnreadCount();
820 addTextToLog(this, *ct, kind, action, msg, time);
821 } else {
822 ct.unreadCount += 1;
823 ct.saveUnreadCount();
828 // ack for sent message
829 void toxMessageAck (in ref PubKey fpk, long msgid) {
830 alias timp = this;
831 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
832 if (auto ct = fpk in contacts) {
833 if (*ct is activeContact) ackLogMessage(msgid);
834 ct.ackReceived(msgid);
838 private:
839 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
841 public:
842 void save () {
843 info.txtser(VFile(basePath~"config.rc", "w"), skipstname:true);
846 public:
847 this (string aBaseDir) {
848 import std.algorithm : sort;
849 import std.file : DirEntry, SpanMode, dirEntries;
850 import std.path : absolutePath, baseName;
852 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
853 aBaseDir = "./";
854 } else if (aBaseDir == "/") {
855 assert(0, "wtf?!");
856 } else {
857 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
860 basePath = aBaseDir.absolutePath;
861 toxDataDiskName = basePath~"toxdata.tox";
862 protoOpts.txtunser(VFile(basePath~"proto.rc"));
863 info.txtunser(VFile(basePath~"config.rc"));
865 // load groups
866 GroupOptions[] glist;
867 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
868 bool hasDefaultGroup = false;
869 bool hasMoronsGroup = false;
870 foreach (ref GroupOptions gi; glist[]) {
871 auto g = new Group(this);
872 g.info = gi;
873 bool found = false;
874 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
875 if (!found) groups ~= g;
876 if (g.gid == 0) hasDefaultGroup = true;
877 if (g.gid == g.gid.max-2) hasMoronsGroup = true;
880 // create default group if necessary
881 if (!hasDefaultGroup) {
882 GroupOptions gi;
883 gi.gid = 0;
884 gi.name = "default";
885 gi.note = "default group for new contacts";
886 gi.opened = true;
887 auto g = new Group(this);
888 g.info = gi;
889 groups ~= g;
892 // create morons group if necessary
893 if (!hasMoronsGroup) {
894 GroupOptions gi;
895 gi.gid = gi.gid.max-2;
896 gi.name = "<morons>";
897 gi.note = "group for completely ignored dumbfucks";
898 gi.opened = false;
899 gi.showOffline = TriOption.No;
900 gi.showPopup = TriOption.No;
901 gi.blinkActivity = TriOption.No;
902 gi.skipUnread = TriOption.Yes;
903 gi.hideIfNoVisibleMembers = TriOption.Yes;
904 gi.ftranAllowed = TriOption.No;
905 gi.resendRotDays = 0;
906 gi.hmcOnOpen = 0;
907 auto g = new Group(this);
908 g.info = gi;
909 groups ~= g;
910 //saveGroups();
913 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
915 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
917 static bool isValidCDName (const(char)[] str) {
918 if (str.length != 64) return false;
919 foreach (immutable char ch; str) {
920 if (ch >= '0' && ch <= '9') continue;
921 if (ch >= 'A' && ch <= 'F') continue;
922 if (ch >= 'a' && ch <= 'f') continue;
923 return false;
925 return true;
928 // load contacts
929 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
930 if (de.name.baseName == "." || de.name.baseName == ".." || !isValidCDName(de.name.baseName)) continue;
931 try {
932 import std.file : exists;
933 if (!de.isDir) continue;
934 string cfgfn = de.name~"/config.rc";
935 if (!cfgfn.exists) continue;
936 ContactInfo ci;
937 ci.txtunser(VFile(cfgfn));
938 auto c = new Contact(this);
939 c.diskFileName = cfgfn;
940 c.info = ci;
941 contacts[c.info.pubkey] = c;
942 c.loadResendQueue();
943 c.loadUnreadCount();
944 // fix contact group
945 if (groupById!false(c.gid) is null) {
946 c.info.gid = 0; // move to default group
947 c.save();
949 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
950 } catch (Exception e) {
951 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
955 toxCreate();
956 assert(toxpk.isValidKey, "something is VERY wrong here");
957 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
960 ~this () {
961 if (toxpk.isValidKey) {
962 toxCoreCloseAccount(toxpk);
963 toxpk[] = toxCoreEmptyKey[];
967 // will not write contact to disk
968 Contact createEmptyContact () {
969 auto c = new Contact(this);
970 c.info.gid = 0;
971 c.info.nick = "test contact";
972 c.info.pubkey[] = 0;
973 return c;
976 // returns `null` if there is no such group, and `dofail` is `true`
977 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
978 foreach (const Group g; groups) if (g.gid == agid) return cast(inout)g;
979 static if (dofail) assert(0, "group not found"); else return null;
982 int opApply () (scope int delegate (ref Contact ct) dg) {
983 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
984 return 0;
987 // returns gid, or uint.max on error
988 uint findGroupByName (const(char)[] name) nothrow {
989 if (name.length == 0) return uint.max;
990 foreach (const Group g; groups) if (g.name == name) return g.gid;
991 return uint.max;
994 // returns gid, or uint.max on error
995 uint createGroup(T:const(char)[]) (T name) nothrow {
996 if (name.length == 0) return uint.max;
997 // groups are sorted by gid, yeah
998 uint newgid = 0;
999 foreach (const Group g; groups) {
1000 if (g.name == name) return g.gid;
1001 if (newgid == g.gid) newgid = g.gid+1;
1003 if (newgid == uint.max) return uint.max;
1004 // create new group
1005 GroupOptions gi;
1006 gi.gid = newgid;
1007 static if (is(T == string)) gi.name = name; else gi.name = name.idup;
1008 gi.opened = true;
1009 auto g = new Group(this);
1010 g.info = gi;
1011 groups ~= g;
1012 import std.algorithm : sort;
1013 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1014 saveGroups();
1015 return gi.gid;
1018 // returns `false` on any error
1019 bool moveContactToGroup (Contact ct, uint gid) nothrow {
1020 if (ct is null || gid == uint.max) return false;
1021 // check if this is our contact
1022 bool found = false;
1023 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1024 if (!found) return false;
1025 // find group
1026 Group grp = null;
1027 foreach (Group g; groups) if (g.gid == gid) { grp = g; break; }
1028 if (grp is null) return false;
1029 // move it
1030 if (ct.info.gid != gid) {
1031 ct.info.gid = gid;
1032 ct.markDirty();
1034 return true;
1037 // returns `false` on any error
1038 bool removeContact (Contact ct) nothrow {
1039 if (ct is null || !isOnline) return false;
1040 // check if this is our contact
1041 bool found = false;
1042 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
1043 if (!found) return false;
1044 if (!toxCoreRemoveFriend(toxpk, ct.info.pubkey)) return false;
1045 ct.removeData();
1046 contacts.remove(ct.info.pubkey);
1048 ct.kind = ContactInfo.Kind.KillFuckDie;
1049 ct.markDirty();
1051 return true;
1054 public:
1055 static Account CreateNew (string aBaseDir, string aAccName) {
1056 import std.file : mkdirRecurse;
1057 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
1058 aBaseDir = "./";
1059 } else if (aBaseDir == "/") {
1060 assert(0, "wtf?!");
1061 } else {
1062 mkdirRecurse(aBaseDir);
1063 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1065 mkdirRecurse(aBaseDir~"/contacts");
1066 // write protocol options
1068 ProtoOptions popt;
1069 popt.txtser(VFile(aBaseDir~"proto.rc", "w"), skipstname:true);
1071 // account options
1073 AccountConfig acc;
1074 acc.nick = aAccName;
1075 acc.showPopup = true;
1076 acc.blinkActivity = true;
1077 acc.hideEmptyGroups = false;
1078 acc.ftranAllowed = true;
1079 acc.resendRotDays = 4;
1080 acc.hmcOnOpen = 10;
1081 acc.txtser(VFile(aBaseDir~"config.rc", "w"), skipstname:true);
1083 // create default group
1085 GroupOptions[1] grp;
1086 grp[0].gid = 0;
1087 grp[0].name = "default";
1088 grp[0].opened = true;
1089 //grp[0].hideIfNoVisible = TriOption.Yes;
1090 grp[].txtser(VFile(aBaseDir~"contacts/groups.rc", "w"), skipstname:true);
1092 // now load it
1093 return new Account(aBaseDir);
1098 // ////////////////////////////////////////////////////////////////////////// //
1099 void setupToxEventListener (SimpleWindow sdmain) {
1100 assert(sdmain !is null);
1102 sdmain.addEventListener((ToxEventBase evt) {
1103 auto acc = clist.accountByPK(evt.self);
1105 if (acc is null) return;
1107 bool fixTray = false;
1108 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1110 // connection?
1111 if (auto e = cast(ToxEventConnection)evt) {
1112 if (e.who[] == acc.toxpk[]) {
1113 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1114 fixTray = true;
1115 } else {
1116 if (!e.connected) acc.toxFriendOffline(e.who);
1118 return;
1120 // status?
1121 if (auto e = cast(ToxEventStatus)evt) {
1122 if (e.who[] == acc.toxpk[]) {
1123 acc.toxSelfStatus(e.status);
1124 fixTray = true;
1125 } else {
1126 acc.toxFriendStatus(e.who, e.status);
1128 return;
1130 // status message?
1131 if (auto e = cast(ToxEventStatusMsg)evt) {
1132 if (e.who[] != acc.toxpk[]) {
1133 acc.toxFriendStatusMessage(e.who, e.message);
1135 return;
1137 // incoming text message?
1138 if (auto e = cast(ToxEventMessage)evt) {
1139 if (e.who[] != acc.toxpk[]) {
1140 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1141 fixTray = true;
1143 return;
1145 // ack outgoing text message?
1146 if (auto e = cast(ToxEventMessageAck)evt) {
1147 if (e.who[] != acc.toxpk[]) {
1148 acc.toxMessageAck(e.who, e.msgid);
1150 return;
1153 //glconProcessEventMessage();