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
;
23 import arsd
.simpledisplay
;
42 // ////////////////////////////////////////////////////////////////////////// //
43 __gshared
bool optShowOffline
= false;
46 // ////////////////////////////////////////////////////////////////////////// //
47 __gshared string accBaseDir
= ".";
48 __gshared Contact activeContact
;
51 // ////////////////////////////////////////////////////////////////////////// //
54 this (Account aOwner
) nothrow { acc
= aOwner
; }
57 bool mDirty
; // true: write contact's config
65 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
68 import std
.file
: mkdirRecurse
;
69 import std
.path
: dirName
;
73 if (diskFileName
.length
== 0) {
74 diskFileName
= acc
.basePath
~"/contacts/groups.rc";
76 mkdirRecurse(diskFileName
.dirName
);
77 if (serialize(info
, diskFileName
)) mDirty
= false;
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(); } }
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
{
136 // `Connecting` for non-account means "awaiting authorization"
139 bool isMe
; // "/me" message?
142 long msgid
; // ==0: unknown yet
145 MonoTime nextSendTime
;
149 this (Account aOwner
) nothrow { acc
= aOwner
; edit
= new MiniEdit(); }
151 void removeData () nothrow {
152 if (diskFileName
.length
== 0) return;
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
, ">");
163 } catch (Exception e
) {}
167 bool mDirty
; // true: write contact's config
173 ContactStatus status
= ContactStatus
.Offline
; // not saved, so if it safe to change it
179 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
181 void loadUnreadCount () nothrow {
182 assert(diskFileName
.length
);
183 assert(acc
!is null);
185 import std
.path
: dirName
;
186 auto fi
= VFile(diskFileName
.dirName
~"/logs/unread.dat");
187 unreadCount
= fi
.readNum
!int;
188 } catch (Exception e
) {
193 void saveUnreadCount () nothrow {
194 assert(diskFileName
.length
);
195 assert(acc
!is null);
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";
209 auto ctt
= MonoTime
.currTime
;
210 foreach (const ref lmsg
; lf
.messages
) {
212 xmsg
.isMe
= lmsg
.isMe
;
213 xmsg
.time
= lmsg
.time
;
214 xmsg
.text
= lmsg
.text
;
216 xmsg
.nextSendTime
= ctt
;
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
);
237 import std
.file
: mkdirRecurse
;
238 import std
.path
: dirName
;
240 assert(acc
!is null);
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");
252 if (serialize(info
, diskFileName
)) mDirty
= false;
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
);
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 () {
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;
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
;
364 if (changed
) saveResendQueue();
367 void processResendQueue () {
368 if (status
== ContactStatus
.Offline || status
== ContactStatus
.Connecting
) return;
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;
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
394 xmsg
.nextSendTime
= MonoTime
.currTime
+30.seconds
;
395 xmsg
.resendCount
= 0;
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
);
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;
422 while (text
.length
&& text
[$-1] <= ' ') text
= text
[0..$-1];
423 if (text
.length
== 0) return; // nothing to do
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
;
434 while (text
.length
) {
435 int epos
= tox_max_message_length()-ReservedSpace
;
436 if (epos
< text
.length
) {
439 if (text
[epos
-1] < 128) break;
440 if ((text
[epos
-1]&0xc0) == 0xc0) break;
444 epos
= cast(int)text
.length
;
447 if (first
&& epos
>= text
.length
) {
448 sendOne(text
[0..epos
], action
);
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
);
457 text
= text
[epos
..$];
465 // ////////////////////////////////////////////////////////////////////////// //
466 final class Account
{
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");
478 ContactStatus mStatus
= ContactStatus
.Offline
;
481 PubKey toxpk
= toxCoreEmptyKey
;
482 string toxDataDiskName
;
483 string basePath
; // with trailing "/"
484 ProtoOptions protoOpts
;
487 Contact
[PubKey
] contacts
;
490 bool mIAmConnecting
= false;
491 bool mIAmOnline
= false;
492 bool forceOnline
= true; // set to `false` to stop autoreconnecting
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;
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
;
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
);
521 glconPostScreenRepaint();
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(); }
536 toxpk
= toxCoreOpenAccount(toxDataDiskName
);
537 if (!toxpk
.isValidKey
) {
538 conwriteln("creating new Tox account...");
539 string nick
= info
.nick
;
540 if (nick
.length
> 0) {
542 if (nick
.length
> tox_max_name_length()) nick
= nick
[0..tox_max_name_length()];
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);
557 conwriteln("NEW friend with pk [", tox_hex(frpub
), "]; name is: ", nick
);
558 c
= new Contact(this);
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
;
573 if (clist
!is null) clist
.buildAccount(this);
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) {
585 c
.info
.nick
= nick
.idup
;
588 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; old name is: ", nick
);
590 if (needSave
) c
.save();
592 return false; // don't stop
597 // connection established
598 void toxConnectionDropped () {
600 conprintfln("TOX[%s] CONNECTION DROPPED", timp
.srvalias
);
601 mIAmConnecting
= false;
604 auto oldst
= mStatus
;
605 mStatus
= ContactStatus
.Offline
;
608 foreach (Contact ct
; contacts
.byValue
) ct
.status
= ContactStatus
.Offline
;
609 glconPostScreenRepaint();
612 // connection established
613 void toxConnectionEstablished () {
615 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp
.srvalias
);
616 mIAmConnecting
= false;
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
) {
641 glconPostScreenRepaint();
645 void toxFriendStatus (in ref PubKey fpk
, ContactStatus cst
) {
646 if (auto ct
= fpk
in contacts
) {
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(); }
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
;
670 glconPostScreenRepaint();
675 void toxFriendReqest (in ref PubKey fpk
, const(char)[] msg
) {
677 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp
.srvalias
, tox_hex(fpk
[]), msg
);
678 if (auto ct
= fpk
in contacts
) {
680 // if not waiting for acceptance, force-friend it
681 if (!ct
.acceptPending
) {
682 conwriteln("force-friend <", ct
.info
.nick
, ">");
683 toxCoreAddFriend(toxpk
, fpk
);
685 ct
.kind
= ContactInfo
.Kind
.PengingAuthAccept
;
686 ct
.info
.statusmsg
= msg
.idup
;
688 ct
.setLastOnlineNow();
691 // new friend request
692 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk
), "]");
693 auto c
= new Contact(this);
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();
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();
718 ct
.saveUnreadCount();
720 addTextToLog(this, *ct
, kind
, action
, msg
, time
);
723 ct
.saveUnreadCount();
728 // ack for sent message
729 void toxMessageAck (in ref PubKey fpk
, long msgid
) {
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
);
739 @property string
srvalias () const pure nothrow @safe @nogc => info
.nick
;
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
== "./") {
749 } else if (aBaseDir
== "/") {
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"));
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);
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
) {
780 gi
.note
= "default group for new contacts";
782 auto g
= new Group(this);
787 // create morons group if necessary
788 if (!hasMoronsGroup
) {
790 gi
.gid
= gi
.gid
.max
-2;
791 gi
.name
= "<morons>";
792 gi
.note
= "group for completely ignored dumbfucks";
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;
802 auto g
= new Group(this);
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;
824 foreach (DirEntry
de; dirEntries(basePath
~"contacts", SpanMode
.shallow
)) {
825 if (de.name
.baseName
== "." ||
de.name
.baseName
== ".." ||
!isValidCDName(de.name
.baseName
)) continue;
827 import std
.file
: exists
;
828 if (!de.isDir
) continue;
829 string cfgfn
= de.name
~"/config.rc";
830 if (!cfgfn
.exists
) continue;
832 ci
.txtunser(VFile(cfgfn
));
833 auto c
= new Contact(this);
834 c
.diskFileName
= cfgfn
;
836 contacts
[c
.info
.pubkey
] = c
;
840 if (groupById
!false(c
.gid
) is null) {
841 c
.info
.gid
= 0; // move to default group
844 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
845 } catch (Exception e
) {
846 conwriteln("ERROR loading contact from '", de.name
, "/config.rc'");
851 assert(toxpk
.isValidKey
, "something is VERY wrong here");
852 conwriteln("created ToxCore for [", tox_hex(toxpk
[]), "]");
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);
866 c
.info
.nick
= "test contact";
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
;
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
;
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
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
;
902 static if (is(T
== string
)) gi
.name
= name
; else gi
.name
= name
.idup
;
904 auto g
= new Group(this);
907 import std
.algorithm
: sort
;
908 groups
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.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
918 foreach (Contact cc
; contacts
.byValue
) if (cc
is ct
) { found
= true; break; }
919 if (!found
) return false;
922 foreach (Group g
; groups
) if (g
.gid
== gid
) { grp
= g
; break; }
923 if (grp
is null) return false;
925 if (ct
.info
.gid
!= gid
) {
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
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;
941 contacts
.remove(ct
.info
.pubkey
);
943 ct.kind = ContactInfo.Kind.KillFuckDie;
950 static Account
CreateNew (string aBaseDir
, string aAccName
) {
951 import std
.file
: mkdirRecurse
;
952 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
954 } else if (aBaseDir
== "/") {
957 mkdirRecurse(aBaseDir
);
958 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
960 mkdirRecurse(aBaseDir
~"/contacts");
961 // write protocol options
964 popt
.txtser(VFile(aBaseDir
~"proto.rc", "w"), skipstname
:true);
970 acc
.showPopup
= true;
971 acc
.blinkActivity
= true;
972 acc
.hideEmptyGroups
= false;
973 acc
.ftranAllowed
= true;
974 acc
.resendRotDays
= 4;
976 acc
.txtser(VFile(aBaseDir
~"config.rc", "w"), skipstname
:true);
978 // create default group
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);
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(); }
1006 if (auto e
= cast(ToxEventConnection
)evt
) {
1007 if (e
.who
[] == acc
.toxpk
[]) {
1008 if (e
.connected
) acc
.toxConnectionEstablished(); else acc
.toxConnectionDropped();
1011 if (!e
.connected
) acc
.toxFriendOffline(e
.who
);
1016 if (auto e
= cast(ToxEventStatus
)evt
) {
1017 if (e
.who
[] == acc
.toxpk
[]) {
1018 acc
.toxSelfStatus(e
.status
);
1021 acc
.toxFriendStatus(e
.who
, e
.status
);
1026 if (auto e
= cast(ToxEventStatusMsg
)evt
) {
1027 if (e
.who
[] != acc
.toxpk
[]) {
1028 acc
.toxFriendStatusMessage(e
.who
, e
.message
);
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
);
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
);
1048 //glconProcessEventMessage();