splitted main modile to several smaller modules (the split is not perfect yet, but...
[bioacid.git] / accobj.d
blob58a80720d626d9247929ef76312472717c843def
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 // ////////////////////////////////////////////////////////////////////////// //
52 final class Group {
53 private:
54 this (Account aOwner) nothrow { acc = aOwner; }
56 private:
57 bool mDirty; // true: write contact's config
58 string diskFileName;
60 public:
61 Account acc;
62 GroupOptions info;
64 public:
65 void markDirty () pure nothrow @safe @nogc => mDirty = true;
67 void save () {
68 import std.file : mkdirRecurse;
69 import std.path : dirName;
70 // save this contact
71 assert(acc !is null);
72 // create disk name
73 if (diskFileName.length == 0) {
74 diskFileName = acc.basePath~"/contacts/groups.rc";
76 mkdirRecurse(diskFileName.dirName);
77 if (serialize(info, diskFileName)) mDirty = false;
80 // save if dirty
81 void update () {
82 if (mDirty) save();
85 @property bool visible () const nothrow @trusted @nogc {
86 if (!hideIfNoVisibleMembers) return true; // always visible
87 // check if we have any visible members
88 foreach (const(Contact) c; acc.contacts.byValue) {
89 if (c.gid != info.gid) continue;
90 if (c.visibleNoGroupCheck) return true;
92 return false; // nobody's here
95 private bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
96 enum lo = "info."~fld;
97 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
98 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
101 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
102 enum lo = "info."~fld;
103 if (mixin(lo) >= 0) return mixin(lo);
104 return mixin("acc.info."~fld);
107 @property nothrow @safe {
108 uint gid () const pure @nogc => info.gid;
110 bool opened () const @nogc => info.opened;
111 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
113 string name () const @nogc => info.name;
114 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
116 string note () const @nogc => info.note;
117 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
119 @nogc {
120 bool showOffline () const => getTriOpt!"showOffline";
121 bool showPopup () const => getTriOpt!"showPopup";
122 bool blinkActivity () const => getTriOpt!"blinkActivity";
123 bool skipUnread () const => getTriOpt!"skipUnread";
124 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
125 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
126 int resendRotDays () const => getIntOpt!"resendRotDays";
127 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
133 // ////////////////////////////////////////////////////////////////////////// //
134 final class Contact {
135 public:
136 // `Connecting` for non-account means "awaiting authorization"
138 static struct XMsg {
139 bool isMe; // "/me" message?
140 SysTime time;
141 string text;
142 long msgid; // ==0: unknown yet
143 int resendCount = 0;
145 MonoTime nextSendTime;
148 private:
149 this (Account aOwner) nothrow { acc = aOwner; edit = new MiniEdit(); }
151 void removeData () nothrow {
152 if (diskFileName.length == 0) return;
153 try {
154 import std.file : rmdirRecurse, rename;
155 import std.path : dirName;
156 auto dn = diskFileName.dirName;
157 if (dn.length == 0 || dn == "/") return; // just in case
158 conwriteln("removing dir <", dn, ">");
159 //rmdirRecurse(dn);
160 rename(dn, "_"~dn);
161 diskFileName = null;
162 mDirty = false;
163 } catch (Exception e) {}
166 private:
167 bool mDirty; // true: write contact's config
169 public:
170 Account acc;
171 string diskFileName;
172 ContactInfo info;
173 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
174 MiniEdit edit;
175 XMsg[] resendQueue;
176 int unreadCount;
178 public:
179 void markDirty () pure nothrow @safe @nogc => mDirty = true;
181 void loadUnreadCount () nothrow {
182 assert(diskFileName.length);
183 assert(acc !is null);
184 try {
185 import std.path : dirName;
186 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
187 unreadCount = fi.readNum!int;
188 } catch (Exception e) {
189 unreadCount = 0;
193 void saveUnreadCount () nothrow {
194 assert(diskFileName.length);
195 assert(acc !is null);
196 try {
197 import std.path : dirName;
198 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
199 fo.writeNum(unreadCount);
200 } catch (Exception e) {
204 void loadResendQueue () {
205 import std.path : dirName;
206 string fname = diskFileName.dirName~"/logs/resend.log";
207 LogFile lf;
208 lf.load(fname);
209 auto ctt = MonoTime.currTime;
210 foreach (const ref lmsg; lf.messages) {
211 XMsg xmsg;
212 xmsg.isMe = lmsg.isMe;
213 xmsg.time = lmsg.time;
214 xmsg.text = lmsg.text;
215 xmsg.msgid = 0;
216 xmsg.nextSendTime = ctt;
217 resendQueue ~= xmsg;
221 void saveResendQueue () {
222 import std.file : mkdirRecurse, remove;
223 import std.path : dirName;
224 assert(diskFileName.length);
225 assert(acc !is null);
226 mkdirRecurse(diskFileName.dirName~"/logs");
227 string fname = diskFileName.dirName~"/logs/resend.log";
228 try { remove(fname); } catch (Exception e) {}
229 if (resendQueue.length) {
230 foreach (const ref msg; resendQueue) {
231 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
236 void save () {
237 import std.file : mkdirRecurse;
238 import std.path : dirName;
239 // save this contact
240 assert(acc !is null);
241 // create disk name
242 if (diskFileName.length == 0) {
243 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
244 acc.contacts[info.pubkey] = this;
246 mkdirRecurse(diskFileName.dirName);
247 mkdirRecurse(diskFileName.dirName~"/avatars");
248 mkdirRecurse(diskFileName.dirName~"/files");
249 mkdirRecurse(diskFileName.dirName~"/fileparts");
250 mkdirRecurse(diskFileName.dirName~"/logs");
251 saveUnreadCount();
252 if (serialize(info, diskFileName)) mDirty = false;
255 // save if dirty
256 void update () {
257 if (mDirty) save();
260 public:
261 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (!kfd && (optShowOffline || showOffline || acceptPending || requestPending || status != ContactStatus.Offline || unreadCount > 0));
263 @property bool visible () const nothrow @trusted @nogc {
264 if (kfd) return false;
265 if (acceptPending || requestPending || optShowOffline) return true;
266 if (unreadCount > 0) return true;
267 if (!showOffline && status == ContactStatus.Offline) return false;
268 auto grp = acc.groupById(gid);
269 return grp.visible;
272 @property nothrow @safe {
273 bool online () const pure @nogc => (status != ContactStatus.Offline && status != ContactStatus.Connecting);
275 ContactInfo.Kind kind () const @nogc => info.kind;
276 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
278 uint gid () const @nogc => info.gid;
279 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
281 string nick () const @nogc => info.nick;
282 void nick (string v) @nogc { pragma(inline, true); if (info.nick != v) { info.nick = v; markDirty(); } }
284 string visnick () const @nogc => info.visnick;
285 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
287 string displayNick () const @nogc => (info.visnick.length ? info.visnick : (info.nick.length ? info.nick : "<unknown>"));
289 string statusmsg () const @nogc => info.statusmsg;
290 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
292 bool kfd () const @nogc => (info.kind == ContactInfo.Kind.KillFuckDie);
293 bool friend () const @nogc => (info.kind == ContactInfo.Kind.Friend);
294 bool acceptPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthAccept);
295 bool requestPending () const @nogc => (info.kind == ContactInfo.Kind.PengingAuthRequest);
297 void setLastOnlineNow () {
298 try {
299 auto ut = Clock.currTime.toUTC().toUnixTime();
300 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
301 } catch (Exception e) {}
304 string note () const @nogc => info.note;
305 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
308 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
309 enum lo = "info.opts."~fld;
310 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
311 auto grp = acc.groupById(info.gid);
312 enum go = "grp.info."~fld;
313 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
314 return mixin("acc.info."~fld);
317 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
318 enum lo = "info.opts."~fld;
319 if (mixin(lo) >= 0) return mixin(lo);
320 auto grp = acc.groupById(info.gid);
321 enum go = "grp.info."~fld;
322 if (mixin(go) >= 0) return mixin(go);
323 return mixin("acc.info."~fld);
326 @property nothrow @safe @nogc {
327 bool showOffline () const => getTriOpt!"showOffline";
328 bool showPopup () const => getTriOpt!"showPopup";
329 bool blinkActivity () const => getTriOpt!"blinkActivity";
330 bool skipUnread () const => getTriOpt!"skipUnread";
331 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
332 int resendRotDays () const => getIntOpt!"resendRotDays";
333 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
336 void loadLogInto (ref LogFile lf) {
337 import std.file : exists;
338 import std.path : dirName;
339 string lname = diskFileName.dirName~"/logs/hugelog.log";
340 if (lname.exists) lf.load(lname); else lf.clear();
343 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
344 import std.path : dirName;
345 string lname = diskFileName.dirName~"/logs/hugelog.log";
346 LogFile.appendLine(lname, kind, text, isMe, time);
349 void ackReceived (long msgid) {
350 if (msgid <= 0) return; // wtf?!
351 bool changed = false;
352 usize idx = 0;
353 while (idx < resendQueue.length) {
354 if (resendQueue[idx].msgid == msgid) {
355 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
356 resendQueue[$-1] = XMsg.init;
357 resendQueue.length -= 1;
358 resendQueue.assumeSafeAppend;
359 changed = true;
360 } else {
361 ++idx;
364 if (changed) saveResendQueue();
367 void processResendQueue () {
368 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
369 bool doSave = false;
370 auto ctt = MonoTime.currTime+30.seconds;
371 foreach (ref XMsg msg; resendQueue) {
372 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
373 if (msgid < 0) break;
374 msg.msgid = msgid;
375 msg.nextSendTime = ctt;
376 if (msg.resendCount++ != 0) doSave = true;
378 if (doSave) saveResendQueue();
381 void send (const(char)[] text) {
382 void sendOne (const(char)[] text, bool action) {
383 if (text.length == 0) return; // just in case
385 SysTime now = Clock.currTime;
386 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
387 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
389 // add this to resend queue
390 XMsg xmsg;
391 xmsg.isMe = action;
392 xmsg.time = now;
393 xmsg.msgid = msgid;
394 xmsg.nextSendTime = MonoTime.currTime+30.seconds;
395 xmsg.resendCount = 0;
398 import std.datetime;
399 import std.format : format;
400 auto dt = cast(DateTime)now;
401 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
404 resendQueue ~= xmsg;
406 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
407 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
410 bool action = text.startsWith("/me ");
411 if (action) text = text[4..$].xstripleft;
413 while (text.length) {
414 auto ep = text.indexOf('\n');
415 if (ep < 0) ep = text.length; else ++ep; // include '\n'
416 // remove line if it contains only spaces
417 bool hasNonSpace = false;
418 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
419 if (hasNonSpace) break;
420 text = text[ep..$];
422 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
423 if (text.length == 0) return; // nothing to do
425 // now split it
426 //TODO: split at word boundaries
427 enum ReservedSpace = 23+3+3;
429 // k8: toxcore developers are idiots, so we have to do dynalloc here
430 auto tmpbuf = new char[](tox_max_message_length()+64);
431 scope(exit) delete tmpbuf;
433 bool first = true;
434 while (text.length) {
435 int epos = tox_max_message_length()-ReservedSpace;
436 if (epos < text.length) {
437 // find utf start
438 while (epos > 0) {
439 if (text[epos-1] < 128) break;
440 if ((text[epos-1]&0xc0) == 0xc0) break;
441 --epos;
443 } else {
444 epos = cast(int)text.length;
446 assert(epos > 0);
447 if (first && epos >= text.length) {
448 sendOne(text[0..epos], action);
449 } else {
450 int ofs = 0;
451 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
452 tmpbuf[ofs..ofs+epos] = text[0..epos];
453 tmpbuf[ofs+epos..ofs+epos+3] = "...";
454 sendOne(tmpbuf[0..ofs+epos+3], action);
456 first = false;
457 text = text[epos..$];
460 //saveResendQueue();
465 // ////////////////////////////////////////////////////////////////////////// //
466 final class Account {
467 public:
468 void saveGroups () nothrow {
469 import std.algorithm : sort;
470 GroupOptions[] glist;
471 scope(exit) delete glist;
472 foreach (Group g; groups) glist ~= g.info;
473 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
474 glist.serialize(basePath~"contacts/groups.rc");
477 private:
478 ContactStatus mStatus = ContactStatus.Offline;
480 public:
481 PubKey toxpk = toxCoreEmptyKey;
482 string toxDataDiskName;
483 string basePath; // with trailing "/"
484 ProtoOptions protoOpts;
485 AccountConfig info;
486 Group[] groups;
487 Contact[PubKey] contacts;
489 public:
490 bool mIAmConnecting = false;
491 bool mIAmOnline = false;
492 bool forceOnline = true; // set to `false` to stop autoreconnecting
494 public:
495 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
497 @property bool isOnline () const nothrow @safe @nogc {
498 if (!toxpk.isValidKey) return false;
499 if (mIAmConnecting) return false;
500 if (!mIAmOnline) return false;
501 return true;
504 @property ContactStatus status () const nothrow @safe @nogc {
505 if (!toxpk.isValidKey) return ContactStatus.Offline;
506 if (mIAmConnecting) return ContactStatus.Connecting;
507 if (!mIAmOnline) return ContactStatus.Offline;
508 return mStatus;
511 @property void status (ContactStatus v) {
512 if (!toxpk.isValidKey) return;
513 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
514 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
515 forceOnline = (v != ContactStatus.Offline);
516 if (mStatus == ContactStatus.Offline) {
517 if (v != ContactStatus.Offline) mIAmConnecting = true;
519 toxCoreSetStatus(toxpk, v);
520 mStatus = v;
521 glconPostScreenRepaint();
522 fixTrayIcon();
525 void processResendQueue () {
526 if (!isOnline) return;
527 foreach (Contact ct; contacts.byValue) { ct.processResendQueue(); ct.update(); }
530 void saveResendQueue () {
531 foreach (Contact ct; contacts.byValue) { ct.saveResendQueue(); ct.update(); }
534 private:
535 void toxCreate () {
536 toxpk = toxCoreOpenAccount(toxDataDiskName);
537 if (!toxpk.isValidKey) {
538 conwriteln("creating new Tox account...");
539 string nick = info.nick;
540 if (nick.length > 0) {
541 //FIXME: utf
542 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
543 } else {
544 nick = "anonymous";
546 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
547 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
551 // load contacts from ToxCore data and add 'em to contact database
552 void toxLoadKnownContacts () {
553 if (!toxpk.isValidKey) return;
554 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] nick) {
555 auto c = (frpub in contacts ? contacts[frpub] : null);
556 if (c is null) {
557 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
558 c = new Contact(this);
559 c.info.gid = 0;
560 c.info.nick = nick.idup;
561 c.info.pubkey[] = frpub[];
562 c.info.opts.showOffline = TriOption.Default;
563 auto ls = toxCoreLastSeen(self, frpub);
564 if (ls != SysTime.min) {
565 c.info.lastonlinetime = cast(uint)ls.toUnixTime();
567 if (c.info.lastonlinetime == 0 && nick.length == 0) {
568 c.info.kind = ContactInfo.Kind.PengingAuthRequest;
570 contacts[c.info.pubkey] = c;
571 c.save();
572 //HACK!
573 if (clist !is null) clist.buildAccount(this);
574 } else {
575 bool needSave = false;
576 auto ls = toxCoreLastSeen(self, frpub);
577 if (ls != SysTime.min) {
578 auto lsu = cast(uint)ls.toUnixTime();
579 if (c.info.lastonlinetime != lsu) { c.info.lastonlinetime = lsu; needSave = true; }
581 if (c.info.nick != nick) {
582 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
583 if (c.info.nick.length == 0 && nick.length != 0) {
584 needSave = true;
585 c.info.nick = nick.idup;
587 } else {
588 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
590 if (needSave) c.save();
592 return false; // don't stop
596 private:
597 // connection established
598 void toxConnectionDropped () {
599 alias timp = this;
600 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
601 mIAmConnecting = false;
602 mIAmOnline = false;
603 if (forceOnline) {
604 auto oldst = mStatus;
605 mStatus = ContactStatus.Offline;
606 status = oldst;
608 foreach (Contact ct; contacts.byValue) ct.status = ContactStatus.Offline;
609 glconPostScreenRepaint();
612 // connection established
613 void toxConnectionEstablished () {
614 alias timp = this;
615 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
616 mIAmConnecting = false;
617 mIAmOnline = true;
618 toxCoreSetStatusMessage(toxpk, "Come taste the gasoline! [BioAcid]");
619 toxLoadKnownContacts();
620 glconPostScreenRepaint();
623 void toxFriendOffline (in ref PubKey fpk) {
624 if (auto ct = fpk in contacts) {
625 auto ls = toxCoreLastSeen(toxpk, fpk);
626 if (ls != SysTime.min) {
627 auto lsu = cast(uint)ls.toUnixTime();
628 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); glconPostScreenRepaint(); }
630 if (ct.status != ContactStatus.Offline) {
631 conwriteln("friend <", ct.info.nick, "> gone offline");
632 ct.status = ContactStatus.Offline;
633 glconPostScreenRepaint();
638 void toxSelfStatus (ContactStatus cst) {
639 if (mStatus != cst) {
640 mStatus = cst;
641 glconPostScreenRepaint();
645 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
646 if (auto ct = fpk in contacts) {
647 if (ct.kfd) return;
648 if (ct.status != cst) {
649 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
650 auto ls = toxCoreLastSeen(toxpk, fpk);
651 if (ls != SysTime.min) {
652 auto lsu = cast(uint)ls.toUnixTime();
653 if (ct.info.lastonlinetime != lsu) { ct.info.lastonlinetime = lsu; ct.markDirty(); }
655 ct.status = cst;
656 // if it is online, and it is not a friend, turn it into a friend
657 if (ct.online && !ct.friend) ct.kind = ContactInfo.Kind.Friend;
658 if (ct.online) ct.processResendQueue(); // why not?
659 glconPostScreenRepaint();
664 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
665 if (auto ct = fpk in contacts) {
666 if (ct.info.statusmsg != msg) {
667 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
668 ct.info.statusmsg = msg;
669 ct.save();
670 glconPostScreenRepaint();
675 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
676 alias timp = this;
677 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
678 if (auto ct = fpk in contacts) {
679 if (ct.kfd) return;
680 // if not waiting for acceptance, force-friend it
681 if (!ct.acceptPending) {
682 conwriteln("force-friend <", ct.info.nick, ">");
683 toxCoreAddFriend(toxpk, fpk);
684 } else {
685 ct.kind = ContactInfo.Kind.PengingAuthAccept;
686 ct.info.statusmsg = msg.idup;
688 ct.setLastOnlineNow();
689 ct.save();
690 } else {
691 // new friend request
692 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk), "]");
693 auto c = new Contact(this);
694 c.info.gid = 0;
695 c.info.nick = null;
696 c.info.pubkey[] = fpk[];
697 c.info.opts.showOffline = TriOption.Default;
698 c.info.statusmsg = msg.idup;
699 c.kind = ContactInfo.Kind.PengingAuthAccept;
700 contacts[c.info.pubkey] = c;
701 c.setLastOnlineNow();
702 c.save();
703 //HACK!
704 if (clist !is null) clist.buildAccount(this);
706 glconPostScreenRepaint();
709 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
710 if (auto ct = fpk in contacts) {
711 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
712 ct.appendToLog(kind, msg, action, time);
713 if (*ct is activeContact) {
714 // if inactive or invisible, add divider line and increase unread count
715 if (!mainWindowVisible || !mainWindowActive) {
716 if (ct.unreadCount == 0) addDividerLine();
717 ct.unreadCount += 1;
718 ct.saveUnreadCount();
720 addTextToLog(this, *ct, kind, action, msg, time);
721 } else {
722 ct.unreadCount += 1;
723 ct.saveUnreadCount();
728 // ack for sent message
729 void toxMessageAck (in ref PubKey fpk, long msgid) {
730 alias timp = this;
731 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
732 if (auto ct = fpk in contacts) {
733 if (*ct is activeContact) ackLogMessage(msgid);
734 ct.ackReceived(msgid);
738 private:
739 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
741 public:
742 this (string aBaseDir) {
743 import std.algorithm : sort;
744 import std.file : DirEntry, SpanMode, dirEntries;
745 import std.path : absolutePath, baseName;
747 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
748 aBaseDir = "./";
749 } else if (aBaseDir == "/") {
750 assert(0, "wtf?!");
751 } else {
752 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
755 basePath = aBaseDir.absolutePath;
756 toxDataDiskName = basePath~"toxdata.tox";
757 protoOpts.txtunser(VFile(basePath~"proto.rc"));
758 info.txtunser(VFile(basePath~"config.rc"));
760 // load groups
761 GroupOptions[] glist;
762 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
763 bool hasDefaultGroup = false;
764 bool hasMoronsGroup = false;
765 foreach (ref GroupOptions gi; glist[]) {
766 auto g = new Group(this);
767 g.info = gi;
768 bool found = false;
769 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
770 if (!found) groups ~= g;
771 if (g.gid == 0) hasDefaultGroup = true;
772 if (g.gid == g.gid.max-2) hasMoronsGroup = true;
775 // create default group if necessary
776 if (!hasDefaultGroup) {
777 GroupOptions gi;
778 gi.gid = 0;
779 gi.name = "default";
780 gi.note = "default group for new contacts";
781 gi.opened = true;
782 auto g = new Group(this);
783 g.info = gi;
784 groups ~= g;
787 // create morons group if necessary
788 if (!hasMoronsGroup) {
789 GroupOptions gi;
790 gi.gid = gi.gid.max-2;
791 gi.name = "<morons>";
792 gi.note = "group for completely ignored dumbfucks";
793 gi.opened = false;
794 gi.showOffline = TriOption.No;
795 gi.showPopup = TriOption.No;
796 gi.blinkActivity = TriOption.No;
797 gi.skipUnread = TriOption.Yes;
798 gi.hideIfNoVisibleMembers = TriOption.Yes;
799 gi.ftranAllowed = TriOption.No;
800 gi.resendRotDays = 0;
801 gi.hmcOnOpen = 0;
802 auto g = new Group(this);
803 g.info = gi;
804 groups ~= g;
805 //saveGroups();
808 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
810 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
812 static bool isValidCDName (const(char)[] str) {
813 if (str.length != 64) return false;
814 foreach (immutable char ch; str) {
815 if (ch >= '0' && ch <= '9') continue;
816 if (ch >= 'A' && ch <= 'F') continue;
817 if (ch >= 'a' && ch <= 'f') continue;
818 return false;
820 return true;
823 // load contacts
824 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
825 if (de.name.baseName == "." || de.name.baseName == ".." || !isValidCDName(de.name.baseName)) continue;
826 try {
827 import std.file : exists;
828 if (!de.isDir) continue;
829 string cfgfn = de.name~"/config.rc";
830 if (!cfgfn.exists) continue;
831 ContactInfo ci;
832 ci.txtunser(VFile(cfgfn));
833 auto c = new Contact(this);
834 c.diskFileName = cfgfn;
835 c.info = ci;
836 contacts[c.info.pubkey] = c;
837 c.loadResendQueue();
838 c.loadUnreadCount();
839 // fix contact group
840 if (groupById!false(c.gid) is null) {
841 c.info.gid = 0; // move to default group
842 c.save();
844 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
845 } catch (Exception e) {
846 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
850 toxCreate();
851 assert(toxpk.isValidKey, "something is VERY wrong here");
852 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
855 ~this () {
856 if (toxpk.isValidKey) {
857 toxCoreCloseAccount(toxpk);
858 toxpk[] = toxCoreEmptyKey[];
862 // will not write contact to disk
863 Contact createEmptyContact () {
864 auto c = new Contact(this);
865 c.info.gid = 0;
866 c.info.nick = "test contact";
867 c.info.pubkey[] = 0;
868 return c;
871 // returns `null` if there is no such group, and `dofail` is `true`
872 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
873 foreach (const Group g; groups) if (g.gid == agid) return cast(inout)g;
874 static if (dofail) assert(0, "group not found"); else return null;
877 int opApply () (scope int delegate (ref Contact ct) dg) {
878 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
879 return 0;
882 // returns gid, or uint.max on error
883 uint findGroupByName (const(char)[] name) nothrow {
884 if (name.length == 0) return uint.max;
885 foreach (const Group g; groups) if (g.name == name) return g.gid;
886 return uint.max;
889 // returns gid, or uint.max on error
890 uint createGroup(T:const(char)[]) (T name) nothrow {
891 if (name.length == 0) return uint.max;
892 // groups are sorted by gid, yeah
893 uint newgid = 0;
894 foreach (const Group g; groups) {
895 if (g.name == name) return g.gid;
896 if (newgid == g.gid) newgid = g.gid+1;
898 if (newgid == uint.max) return uint.max;
899 // create new group
900 GroupOptions gi;
901 gi.gid = newgid;
902 static if (is(T == string)) gi.name = name; else gi.name = name.idup;
903 gi.opened = true;
904 auto g = new Group(this);
905 g.info = gi;
906 groups ~= g;
907 import std.algorithm : sort;
908 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
909 saveGroups();
910 return gi.gid;
913 // returns `false` on any error
914 bool moveContactToGroup (Contact ct, uint gid) nothrow {
915 if (ct is null || gid == uint.max) return false;
916 // check if this is our contact
917 bool found = false;
918 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
919 if (!found) return false;
920 // find group
921 Group grp = null;
922 foreach (Group g; groups) if (g.gid == gid) { grp = g; break; }
923 if (grp is null) return false;
924 // move it
925 if (ct.info.gid != gid) {
926 ct.info.gid = gid;
927 ct.markDirty();
929 return true;
932 // returns `false` on any error
933 bool removeContact (Contact ct) nothrow {
934 if (ct is null || !isOnline) return false;
935 // check if this is our contact
936 bool found = false;
937 foreach (Contact cc; contacts.byValue) if (cc is ct) { found = true; break; }
938 if (!found) return false;
939 if (!toxCoreRemoveFriend(toxpk, ct.info.pubkey)) return false;
940 ct.removeData();
941 contacts.remove(ct.info.pubkey);
943 ct.kind = ContactInfo.Kind.KillFuckDie;
944 ct.markDirty();
946 return true;
949 public:
950 static Account CreateNew (string aBaseDir, string aAccName) {
951 import std.file : mkdirRecurse;
952 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
953 aBaseDir = "./";
954 } else if (aBaseDir == "/") {
955 assert(0, "wtf?!");
956 } else {
957 mkdirRecurse(aBaseDir);
958 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
960 mkdirRecurse(aBaseDir~"/contacts");
961 // write protocol options
963 ProtoOptions popt;
964 popt.txtser(VFile(aBaseDir~"proto.rc", "w"), skipstname:true);
966 // account options
968 AccountConfig acc;
969 acc.nick = aAccName;
970 acc.showPopup = true;
971 acc.blinkActivity = true;
972 acc.hideEmptyGroups = false;
973 acc.ftranAllowed = true;
974 acc.resendRotDays = 4;
975 acc.hmcOnOpen = 10;
976 acc.txtser(VFile(aBaseDir~"config.rc", "w"), skipstname:true);
978 // create default group
980 GroupOptions[1] grp;
981 grp[0].gid = 0;
982 grp[0].name = "default";
983 grp[0].opened = true;
984 //grp[0].hideIfNoVisible = TriOption.Yes;
985 grp[].txtser(VFile(aBaseDir~"contacts/groups.rc", "w"), skipstname:true);
987 // now load it
988 return new Account(aBaseDir);
993 // ////////////////////////////////////////////////////////////////////////// //
994 void setupToxEventListener (SimpleWindow sdmain) {
995 assert(sdmain !is null);
997 sdmain.addEventListener((ToxEventBase evt) {
998 auto acc = clist.accountByPK(evt.self);
1000 if (acc is null) return;
1002 bool fixTray = false;
1003 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1005 // connection?
1006 if (auto e = cast(ToxEventConnection)evt) {
1007 if (e.who[] == acc.toxpk[]) {
1008 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1009 fixTray = true;
1010 } else {
1011 if (!e.connected) acc.toxFriendOffline(e.who);
1013 return;
1015 // status?
1016 if (auto e = cast(ToxEventStatus)evt) {
1017 if (e.who[] == acc.toxpk[]) {
1018 acc.toxSelfStatus(e.status);
1019 fixTray = true;
1020 } else {
1021 acc.toxFriendStatus(e.who, e.status);
1023 return;
1025 // status message?
1026 if (auto e = cast(ToxEventStatusMsg)evt) {
1027 if (e.who[] != acc.toxpk[]) {
1028 acc.toxFriendStatusMessage(e.who, e.message);
1030 return;
1032 // incoming text message?
1033 if (auto e = cast(ToxEventMessage)evt) {
1034 if (e.who[] != acc.toxpk[]) {
1035 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1036 fixTray = true;
1038 return;
1040 // ack outgoing text message?
1041 if (auto e = cast(ToxEventMessageAck)evt) {
1042 if (e.who[] != acc.toxpk[]) {
1043 acc.toxMessageAck(e.who, e.msgid);
1045 return;
1048 //glconProcessEventMessage();