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 private bool decodeHexStringInto (ubyte[] dest
, const(char)[] str) {
54 foreach (char ch
; str) {
55 if (ch
<= ' ') continue;
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;
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
;
65 return (dpos
== dest
.length
*2);
69 public PubKey
decodePubKeyStr (const(char)[] str) {
71 if (!decodeHexStringInto(res
[], str)) res
[] = toxCoreEmptyKey
[];
76 public ToxAddr
decodeAddrStr (const(char)[] str) {
78 if (!decodeHexStringInto(res
[], str)) res
[] = toxCoreEmptyAddr
[];
83 // ////////////////////////////////////////////////////////////////////////// //
86 this (Account aOwner
) nothrow { acc
= aOwner
; }
89 bool mDirty
; // true: write contact's config
97 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
100 import std
.file
: mkdirRecurse
;
101 import std
.path
: dirName
;
103 assert(acc
!is null);
105 if (diskFileName
.length
== 0) {
106 diskFileName
= acc
.basePath
~"/contacts/groups.rc";
108 mkdirRecurse(diskFileName
.dirName
);
109 if (serialize(info
, diskFileName
)) mDirty
= false;
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(); } }
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
{
168 // `Connecting` for non-account means "awaiting authorization"
171 bool isMe
; // "/me" message?
174 long msgid
; // ==0: unknown yet
177 MonoTime nextSendTime
;
181 this (Account aOwner
) nothrow { acc
= aOwner
; edit
= new MiniEdit(); }
183 void removeData () nothrow {
184 if (diskFileName
.length
== 0) return;
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
, ">");
195 } catch (Exception e
) {}
199 bool mDirty
; // true: write contact's config
205 ContactStatus status
= ContactStatus
.Offline
; // not saved, so if it safe to change it
211 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
213 void loadUnreadCount () nothrow {
214 assert(diskFileName
.length
);
215 assert(acc
!is null);
217 import std
.path
: dirName
;
218 auto fi
= VFile(diskFileName
.dirName
~"/logs/unread.dat");
219 unreadCount
= fi
.readNum
!int;
220 } catch (Exception e
) {
225 void saveUnreadCount () nothrow {
226 assert(diskFileName
.length
);
227 assert(acc
!is null);
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";
241 auto ctt
= MonoTime
.currTime
;
242 foreach (const ref lmsg
; lf
.messages
) {
244 xmsg
.isMe
= lmsg
.isMe
;
245 xmsg
.time
= lmsg
.time
;
246 xmsg
.text
= lmsg
.text
;
248 xmsg
.nextSendTime
= ctt
;
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
);
269 import std
.file
: mkdirRecurse
;
270 import std
.path
: dirName
;
272 assert(acc
!is null);
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");
284 if (serialize(info
, diskFileName
)) mDirty
= false;
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
);
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 () {
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;
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
;
396 if (changed
) saveResendQueue();
399 void processResendQueue () {
400 if (status
== ContactStatus
.Offline || status
== ContactStatus
.Connecting
) return;
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;
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
426 xmsg
.nextSendTime
= MonoTime
.currTime
+30.seconds
;
427 xmsg
.resendCount
= 0;
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
);
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;
454 while (text
.length
&& text
[$-1] <= ' ') text
= text
[0..$-1];
455 if (text
.length
== 0) return; // nothing to do
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
;
466 while (text
.length
) {
467 int epos
= tox_max_message_length()-ReservedSpace
;
468 if (epos
< text
.length
) {
471 if (text
[epos
-1] < 128) break;
472 if ((text
[epos
-1]&0xc0) == 0xc0) break;
476 epos
= cast(int)text
.length
;
479 if (first
&& epos
>= text
.length
) {
480 sendOne(text
[0..epos
], action
);
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
);
489 text
= text
[epos
..$];
497 // ////////////////////////////////////////////////////////////////////////// //
498 final class Account
{
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");
510 ContactStatus mStatus
= ContactStatus
.Offline
;
513 PubKey toxpk
= toxCoreEmptyKey
;
514 string toxDataDiskName
;
515 string basePath
; // with trailing "/"
516 ProtoOptions protoOpts
;
519 Contact
[PubKey
] contacts
;
522 bool mIAmConnecting
= false;
523 bool mIAmOnline
= false;
524 bool forceOnline
= true; // set to `false` to stop autoreconnecting
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;
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
;
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
);
553 glconPostScreenRepaint();
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(); }
568 toxpk
= toxCoreOpenAccount(toxDataDiskName
);
569 if (!toxpk
.isValidKey
) {
570 conwriteln("creating new Tox account...");
571 string nick
= info
.nick
;
572 if (nick
.length
> 0) {
574 if (nick
.length
> tox_max_name_length()) nick
= nick
[0..tox_max_name_length()];
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;
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);
592 conwriteln("NEW friend with pk [", tox_hex(frpub
), "]; name is: ", nick
);
593 c
= new Contact(this);
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
;
608 if (clist
!is null) clist
.buildAccount(this);
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) {
620 c
.info
.nick
= nick
.idup
;
623 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; old name is: ", nick
);
625 if (needSave
) c
.save();
627 return false; // don't stop
631 auto data
= toxCoreLoadDataFile(VFile(toxDataDiskName
));
632 foreach (const ref ci
; data
.friends
) {
633 auto c
= (ci
.pubkey
in contacts ? contacts
[ci
.pubkey
] : null);
635 conwriteln("NEW friend with pk [", tox_hex(ci
.pubkey
), "]; name is: ", ci
.nick
);
636 c
= new Contact(this);
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
;
647 if (clist
!is null) clist
.buildAccount(this);
649 bool needSave
= false;
651 if (c
.info
.nick
!= ci
.nick
) {
652 conwriteln("OLD friend with pk [", tox_hex(ci
.pubkey
), "]; new name is: ", ci
.nick
);
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
) {}
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);
677 c
= new Contact(this);
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
;
687 if (clist
!is null) clist
.buildAccount(this);
693 // connection established
694 void toxConnectionDropped () {
696 conprintfln("TOX[%s] CONNECTION DROPPED", timp
.srvalias
);
697 mIAmConnecting
= false;
700 auto oldst
= mStatus
;
701 mStatus
= ContactStatus
.Offline
;
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 () {
714 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp
.srvalias
);
715 mIAmConnecting
= false;
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
) {
741 glconPostScreenRepaint();
745 void toxFriendStatus (in ref PubKey fpk
, ContactStatus cst
) {
746 if (auto ct
= fpk
in contacts
) {
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(); }
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
;
770 glconPostScreenRepaint();
775 void toxFriendReqest (in ref PubKey fpk
, const(char)[] msg
) {
777 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp
.srvalias
, tox_hex(fpk
[]), msg
);
778 if (auto ct
= fpk
in contacts
) {
780 // if not waiting for acceptance, force-friend it
781 if (!ct
.acceptPending
) {
782 conwriteln("force-friend <", ct
.info
.nick
, ">");
783 toxCoreAddFriend(toxpk
, fpk
);
785 ct
.kind
= ContactInfo
.Kind
.PengingAuthAccept
;
786 ct
.info
.statusmsg
= msg
.idup
;
788 ct
.setLastOnlineNow();
791 // new friend request
792 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk
), "]");
793 auto c
= new Contact(this);
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();
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();
818 ct
.saveUnreadCount();
820 addTextToLog(this, *ct
, kind
, action
, msg
, time
);
823 ct
.saveUnreadCount();
828 // ack for sent message
829 void toxMessageAck (in ref PubKey fpk
, long msgid
) {
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
);
839 @property string
srvalias () const pure nothrow @safe @nogc => info
.nick
;
843 info
.txtser(VFile(basePath
~"config.rc", "w"), skipstname
:true);
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
== "./") {
854 } else if (aBaseDir
== "/") {
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"));
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);
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
) {
885 gi
.note
= "default group for new contacts";
887 auto g
= new Group(this);
892 // create morons group if necessary
893 if (!hasMoronsGroup
) {
895 gi
.gid
= gi
.gid
.max
-2;
896 gi
.name
= "<morons>";
897 gi
.note
= "group for completely ignored dumbfucks";
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;
907 auto g
= new Group(this);
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;
929 foreach (DirEntry
de; dirEntries(basePath
~"contacts", SpanMode
.shallow
)) {
930 if (de.name
.baseName
== "." ||
de.name
.baseName
== ".." ||
!isValidCDName(de.name
.baseName
)) continue;
932 import std
.file
: exists
;
933 if (!de.isDir
) continue;
934 string cfgfn
= de.name
~"/config.rc";
935 if (!cfgfn
.exists
) continue;
937 ci
.txtunser(VFile(cfgfn
));
938 auto c
= new Contact(this);
939 c
.diskFileName
= cfgfn
;
941 contacts
[c
.info
.pubkey
] = c
;
945 if (groupById
!false(c
.gid
) is null) {
946 c
.info
.gid
= 0; // move to default group
949 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
950 } catch (Exception e
) {
951 conwriteln("ERROR loading contact from '", de.name
, "/config.rc'");
956 assert(toxpk
.isValidKey
, "something is VERY wrong here");
957 conwriteln("created ToxCore for [", tox_hex(toxpk
[]), "]");
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);
971 c
.info
.nick
= "test contact";
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
;
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
;
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
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
;
1007 static if (is(T
== string
)) gi
.name
= name
; else gi
.name
= name
.idup
;
1009 auto g
= new Group(this);
1012 import std
.algorithm
: sort
;
1013 groups
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.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
1023 foreach (Contact cc
; contacts
.byValue
) if (cc
is ct
) { found
= true; break; }
1024 if (!found
) return false;
1027 foreach (Group g
; groups
) if (g
.gid
== gid
) { grp
= g
; break; }
1028 if (grp
is null) return false;
1030 if (ct
.info
.gid
!= gid
) {
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
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;
1046 contacts
.remove(ct
.info
.pubkey
);
1048 ct.kind = ContactInfo.Kind.KillFuckDie;
1055 static Account
CreateNew (string aBaseDir
, string aAccName
) {
1056 import std
.file
: mkdirRecurse
;
1057 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
1059 } else if (aBaseDir
== "/") {
1062 mkdirRecurse(aBaseDir
);
1063 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
1065 mkdirRecurse(aBaseDir
~"/contacts");
1066 // write protocol options
1069 popt
.txtser(VFile(aBaseDir
~"proto.rc", "w"), skipstname
:true);
1074 acc
.nick
= aAccName
;
1075 acc
.showPopup
= true;
1076 acc
.blinkActivity
= true;
1077 acc
.hideEmptyGroups
= false;
1078 acc
.ftranAllowed
= true;
1079 acc
.resendRotDays
= 4;
1081 acc
.txtser(VFile(aBaseDir
~"config.rc", "w"), skipstname
:true);
1083 // create default group
1085 GroupOptions
[1] grp
;
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);
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(); }
1111 if (auto e
= cast(ToxEventConnection
)evt
) {
1112 if (e
.who
[] == acc
.toxpk
[]) {
1113 if (e
.connected
) acc
.toxConnectionEstablished(); else acc
.toxConnectionDropped();
1116 if (!e
.connected
) acc
.toxFriendOffline(e
.who
);
1121 if (auto e
= cast(ToxEventStatus
)evt
) {
1122 if (e
.who
[] == acc
.toxpk
[]) {
1123 acc
.toxSelfStatus(e
.status
);
1126 acc
.toxFriendStatus(e
.who
, e
.status
);
1131 if (auto e
= cast(ToxEventStatusMsg
)evt
) {
1132 if (e
.who
[] != acc
.toxpk
[]) {
1133 acc
.toxFriendStatusMessage(e
.who
, e
.message
);
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
);
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
);
1153 //glconProcessEventMessage();