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, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module accobj
is aliced
;
22 import arsd
.simpledisplay
;
43 // ////////////////////////////////////////////////////////////////////////// //
44 __gshared
bool optShowOffline
= false;
47 // ////////////////////////////////////////////////////////////////////////// //
48 __gshared string accBaseDir
= ".";
49 __gshared Contact activeContact
;
52 private bool decodeHexStringInto (ubyte[] dest
, const(char)[] str) {
55 foreach (immutable char ch
; str) {
56 if (ch
<= ' ' || ch
== '_' || ch
== '.' || ch
== ':') continue;
58 if (ch
>= '0' && ch
<= '9') dig
= ch
-'0';
59 else if (ch
>= 'A' && ch
<= 'F') dig
= ch
-'A'+10;
60 else if (ch
>= 'a' && ch
<= 'f') dig
= ch
-'a'+10;
62 if (dpos
>= dest
.length
*2) return false;
63 if (dpos
%2 == 0) dest
[dpos
/2] = cast(ubyte)(dig
<<4); else dest
[dpos
/2] |
= cast(ubyte)dig
;
66 return (dpos
== dest
.length
*2);
70 public PubKey
decodePubKeyStr (const(char)[] str) {
72 if (!decodeHexStringInto(res
[], str)) res
[] = toxCoreEmptyKey
[];
77 public ToxAddr
decodeAddrStr (const(char)[] str) {
79 if (!decodeHexStringInto(res
[], str)) res
[] = toxCoreEmptyAddr
[];
84 // ////////////////////////////////////////////////////////////////////////// //
87 this (Account aOwner
) nothrow { acc
= aOwner
; }
90 bool mDirty
; // true: write contact's config
98 bool getTriOpt(string
fld, string fld2
=null) () const nothrow @trusted @nogc {
99 enum lo
= "info."~fld;
100 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
101 static if (fld2
.length
) return mixin("acc.info."~fld2
); else return mixin("acc.info."~fld);
104 int getIntOpt(string
fld) () const nothrow @trusted @nogc {
105 enum lo
= "info."~fld;
106 if (mixin(lo
) >= 0) return mixin(lo
);
107 return mixin("acc.info."~fld);
111 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
114 import std
.path
: dirName
;
116 assert(acc
!is null);
118 if (diskFileName
.length
== 0) {
119 diskFileName
= acc
.basePath
~"/contacts/groups.rc";
121 mkdirRec(diskFileName
.dirName
);
122 if (serialize(info
, diskFileName
)) mDirty
= false;
125 @property bool visible () const nothrow @trusted @nogc {
126 if (!hideIfNoVisibleMembers
) return true; // always visible
127 // check if we have any visible members
128 foreach (const(Contact
) c
; acc
.contacts
.byValue
) {
129 if (c
.gid
!= info
.gid
) continue;
130 if (c
.visibleNoGroupCheck
) return true;
132 return false; // nobody's here
135 @property nothrow @safe {
136 uint gid () const pure @nogc => info
.gid
;
138 bool opened () const @nogc => info
.opened
;
139 void opened (bool v
) @nogc { pragma(inline
, true); if (info
.opened
!= v
) { info
.opened
= v
; markDirty(); } }
141 string
name () const @nogc => info
.name
;
142 void name (string v
) @nogc { pragma(inline
, true); if (v
.length
== 0) v
= "<unnamed>"; if (info
.name
!= v
) { info
.name
= v
; markDirty(); } }
144 string
note () const @nogc => info
.note
;
145 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
148 bool showOffline () const => getTriOpt
!"showOffline";
149 bool showPopup () const => getTriOpt
!"showPopup";
150 bool blinkActivity () const => getTriOpt
!"blinkActivity";
151 bool skipUnread () const => getTriOpt
!"skipUnread";
152 bool hideIfNoVisibleMembers () const => getTriOpt
!("hideIfNoVisibleMembers", "hideEmptyGroups");
153 bool ftranAllowed () const => getTriOpt
!"ftranAllowed";
154 int resendRotDays () const => getIntOpt
!"resendRotDays";
155 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
161 // ////////////////////////////////////////////////////////////////////////// //
162 final class Contact
{
164 // `Connecting` for non-account means "awaiting authorization"
167 bool isMe
; // "/me" message?
170 long msgid
; // ==0: unknown yet
172 MonoTime resendAllowTime
= MonoTime
.zero
;
174 string
textOrig () const pure nothrow @trusted @nogc {
176 while (pos
< text
.length
&& text
.ptr
[pos
] != ']') ++pos
;
178 return (pos
< text
.length ? text
[pos
..$] : null);
181 bool isOurMark (const(void)[] atext
, SysTime atime
) nothrow @trusted {
182 pragma(inline
, true);
183 return (atime
== time
&& textDigest(textOrig
) == textDigest(atext
));
186 bool isOurMark (long aid
, const(void)[] atext
, SysTime atime
) nothrow @trusted {
187 pragma(inline
, true);
188 return (aid
> 0 ?
(msgid
== aid
) : (atime
== time
&& textDigest(textOrig
) == textDigest(atext
)));
191 bool isOurMark (long aid
, in ref TextDigest adigest
, SysTime atime
) nothrow @trusted {
192 pragma(inline
, true);
193 return (aid
> 0 ?
(msgid
== aid
) : (atime
== time
&& textDigest(textOrig
) == adigest
));
198 MonoTime nextSendTime
; // for resend queue
201 this (Account aOwner
) nothrow { acc
= aOwner
; edit
= new MiniEdit(); }
203 void removeData () nothrow {
204 if (diskFileName
.length
== 0) return;
206 import std
.file
: rmdirRecurse
, rename
;
207 import std
.path
: dirName
;
208 auto dn
= diskFileName
.dirName
;
209 if (dn
.length
== 0 || dn
== "/") return; // just in case
210 conwriteln("removing dir <", dn
, ">");
212 //rename(dn, "_"~dn);
215 } catch (Exception e
) {}
219 bool mDirty
; // true: write contact's config
225 ContactStatus status
= ContactStatus
.Offline
; // not saved, so if it safe to change it
231 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
233 void loadUnreadCount () nothrow {
234 assert(diskFileName
.length
);
235 assert(acc
!is null);
237 import std
.path
: dirName
;
238 auto fi
= VFile(diskFileName
.dirName
~"/logs/unread.dat");
239 unreadCount
= fi
.readNum
!int;
240 } catch (Exception e
) {
245 void saveUnreadCount () nothrow {
246 assert(diskFileName
.length
);
247 assert(acc
!is null);
249 import std
.path
: dirName
;
250 auto fo
= VFile(diskFileName
.dirName
~"/logs/unread.dat", "w");
251 fo
.writeNum(unreadCount
);
252 } catch (Exception e
) {
256 void loadResendQueue () {
257 import std
.path
: dirName
;
258 string fname
= diskFileName
.dirName
~"/logs/resend.log";
261 foreach (const ref lmsg
; lf
.messages
) {
263 xmsg
.isMe
= lmsg
.isMe
;
264 xmsg
.time
= lmsg
.time
;
265 xmsg
.text
= lmsg
.text
;
269 nextSendTime
= MonoTime
.currTime
;
272 void saveResendQueue () {
273 import std
.file
: remove
;
274 import std
.path
: dirName
;
275 assert(diskFileName
.length
);
276 assert(acc
!is null);
277 mkdirRec(diskFileName
.dirName
~"/logs");
278 string fname
= diskFileName
.dirName
~"/logs/resend.log";
279 try { remove(fname
); } catch (Exception e
) {}
280 if (resendQueue
.length
) {
281 foreach (const ref msg
; resendQueue
) {
282 LogFile
.appendLine(fname
, LogFile
.Msg
.Kind
.Outgoing
, msg
.text
, msg
.isMe
, msg
.time
);
288 import std
.path
: dirName
;
290 assert(acc
!is null);
292 if (diskFileName
.length
== 0) {
293 diskFileName
= acc
.basePath
~"/contacts/"~tox_hex(info
.pubkey
[])~"/config.rc";
294 acc
.contacts
[info
.pubkey
] = this;
296 mkdirRec(diskFileName
.dirName
);
297 mkdirRec(diskFileName
.dirName
~"/avatars");
298 mkdirRec(diskFileName
.dirName
~"/files");
299 mkdirRec(diskFileName
.dirName
~"/fileparts");
300 mkdirRec(diskFileName
.dirName
~"/logs");
302 if (serialize(info
, diskFileName
)) mDirty
= false;
311 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (!kfd
&& (optShowOffline || showOffline || acceptPending || requestPending || status
!= ContactStatus
.Offline || unreadCount
> 0));
313 @property bool visible () const nothrow @trusted @nogc {
314 if (kfd
) return false;
315 if (acceptPending || requestPending
) return true;
316 if (unreadCount
> 0) return true;
317 if (!showOffline
&& !optShowOffline
&& status
== ContactStatus
.Offline
) return false;
318 auto grp
= acc
.groupById(gid
);
319 return (grp
.visible
&& grp
.opened
);
322 @property nothrow @safe {
323 bool online () const pure @nogc => (status
!= ContactStatus
.Offline
&& status
!= ContactStatus
.Connecting
);
325 ContactInfo
.Kind
kind () const @nogc => info
.kind
;
326 void kind (ContactInfo
.Kind v
) @nogc { pragma(inline
, true); if (info
.kind
!= v
) { info
.kind
= v
; markDirty(); } }
328 uint gid () const @nogc => info
.gid
;
329 void gid (uint v
) @nogc { pragma(inline
, true); if (info
.gid
!= v
) { info
.gid
= v
; markDirty(); } }
331 string
nick () const @nogc => info
.nick
;
332 void nick (string v
) @nogc { pragma(inline
, true); if (info
.nick
!= v
) { info
.nick
= v
; markDirty(); } }
334 string
visnick () const @nogc => info
.visnick
;
335 void visnick (string v
) @nogc { pragma(inline
, true); if (info
.visnick
!= v
) { info
.visnick
= v
; markDirty(); } }
337 string
displayNick () const @nogc => (info
.visnick
.length ? info
.visnick
: (info
.nick
.length ? info
.nick
: "<unknown>"));
339 string
statusmsg () const @nogc => info
.statusmsg
;
340 void statusmsg (string v
) @nogc { pragma(inline
, true); if (info
.statusmsg
!= v
) { info
.statusmsg
= v
; markDirty(); } }
342 bool kfd () const @nogc => (info
.kind
== ContactInfo
.Kind
.KillFuckDie
);
343 bool friend () const @nogc => (info
.kind
== ContactInfo
.Kind
.Friend
);
344 bool acceptPending () const @nogc => (info
.kind
== ContactInfo
.Kind
.PengingAuthAccept
);
345 bool requestPending () const @nogc => (info
.kind
== ContactInfo
.Kind
.PengingAuthRequest
);
347 void setLastOnlineNow () {
349 auto ut
= systimeNow
.toUnixTime();
350 if (info
.lastonlinetime
!= cast(uint)ut
) { info
.lastonlinetime
= cast(uint)ut
; markDirty(); }
351 } catch (Exception e
) {}
354 string
note () const @nogc => info
.note
;
355 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
358 private bool getTriOpt(string
fld) () const nothrow @trusted @nogc {
359 enum lo
= "info.opts."~fld;
360 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
361 auto grp
= acc
.groupById(info
.gid
);
362 enum go
= "grp.info."~fld;
363 if (mixin(go
) != TriOption
.Default
) return (mixin(go
) == TriOption
.Yes
);
364 return mixin("acc.info."~fld);
367 private void setTriOpt(string
fld) (TriOption val
) {
368 enum lo
= "info.opts."~fld;
369 if (mixin(lo
) != val
) {
370 mixin("info.opts."~fld~" = val;");
376 private int getIntOpt(string
fld) () const nothrow @trusted @nogc {
377 enum lo
= "info.opts."~fld;
378 if (mixin(lo
) >= 0) return mixin(lo
);
379 auto grp
= acc
.groupById(info
.gid
);
380 enum go
= "grp.info."~fld;
381 if (mixin(go
) >= 0) return mixin(go
);
382 return mixin("acc.info."~fld);
385 @property nothrow @safe @nogc {
386 bool showOffline () const => !kfd
&& getTriOpt
!"showOffline";
387 bool showPopup () const => !kfd
&& getTriOpt
!"showPopup";
388 bool blinkActivity () const => !kfd
&& getTriOpt
!"blinkActivity";
389 bool skipUnread () const => kfd || getTriOpt
!"skipUnread";
390 bool ftranAllowed () const => !kfd
&& getTriOpt
!"ftranAllowed";
391 int resendRotDays () const => getIntOpt
!"resendRotDays";
392 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
395 @property void showOffline (TriOption v
) {
396 setTriOpt
!"showOffline"(v
);
399 void loadLogInto (ref LogFile lf
) {
400 import std
.file
: exists
;
401 import std
.path
: dirName
;
402 string lname
= diskFileName
.dirName
~"/logs/hugelog.log";
403 if (lname
.exists
) lf
.load(lname
); else lf
.clear();
406 void appendToLog (LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
, SysTime time
) {
407 import std
.path
: dirName
;
408 string lname
= diskFileName
.dirName
~"/logs/hugelog.log";
409 LogFile
.appendLine(lname
, kind
, text
, isMe
, time
);
412 void ackReceived (long msgid
) {
413 if (msgid
<= 0) return; // wtf?!
414 bool changed
= false;
416 while (idx
< resendQueue
.length
) {
417 if (resendQueue
[idx
].msgid
== msgid
) {
418 foreach (immutable c
; idx
+1..resendQueue
.length
) resendQueue
[c
-1] = resendQueue
[c
];
419 resendQueue
[$-1] = XMsg
.init
;
420 resendQueue
.length
-= 1;
421 resendQueue
.assumeSafeAppend
;
429 // resend next message
430 if (resendQueue
.length
) processResendQueue(forced
:true);
435 long findInResendQueue (const(char)[] text
, SysTime time
) {
436 foreach (ref XMsg msg
; resendQueue
) {
437 if (msg
.time
== time
&& msg
.textOrig
== text
) {
438 //conwriteln("<", text, "> found in resend queue; id=", msg.msgid, "; rt=<", msg.text, ">");
440 } /*else if (msg.textOrig == text) {
441 conwriteln("<", text, "> (", time, ":", msg.time, ") IS NOT: id=", msg.msgid, "; rt=<", msg.text, ">");
447 bool removeFromResendQueue (long msgid
, in ref TextDigest digest
, SysTime time
) {
450 while (pos
< resendQueue
.length
) {
451 if (resendQueue
[pos
].isOurMark(msgid
, digest
, time
)) {
452 foreach (immutable c
; pos
+1..resendQueue
.length
) resendQueue
[c
-1] = resendQueue
[c
];
453 resendQueue
[$-1] = XMsg
.init
;
454 resendQueue
.length
-= 1;
455 resendQueue
.assumeSafeAppend
;
461 if (doSave
) saveResendQueue();
465 // called when toxcore goes offline
466 void resetQueueIds () {
467 foreach (ref XMsg msg
; resendQueue
) msg
.msgid
= 0;
468 if (activeContact
is this) logResetAckMessageIds();
469 nextSendTime
= MonoTime
.currTime
+10.seconds
;
472 void processResendQueue (bool forced
=false) {
473 if (status
== ContactStatus
.Offline || status
== ContactStatus
.Connecting
) return;
474 if (forced
) conwriteln("force resending for ", info
.nick
);
475 auto ctt
= MonoTime
.currTime
;
477 if (nextSendTime
> ctt
) return; // oops
479 // resend auth request?
481 if (isValidAddr(info
.fraddr
)) {
482 string msg
= info
.statusmsg
;
483 if (msg
.length
== 0) msg
= "I brought you a tasty fish!";
484 if (!toxCoreSendFriendRequest(acc
.toxpk
, info
.fraddr
, msg
)) { conwriteln("address: '", tox_hex(info
.fraddr
), "'; error sending friend request"); }
489 foreach (ref XMsg msg
; resendQueue
) msg
.resendAllowTime
= ctt
;
491 foreach (ref XMsg msg
; resendQueue
) {
492 if (forced || msg
.resendAllowTime
<= ctt
) {
493 if (forced
) conwriteln(" ...resending for ", info
.nick
);
494 long msgid
= toxCoreSendMessage(acc
.toxpk
, info
.pubkey
, msg
.text
, msg
.isMe
);
495 if (msgid
< 0) break;
496 if (activeContact
is this) logFixAckMessageId(msg
.msgid
, msgid
, msg
.textOrig
, msg
.time
);
498 msg
.resendAllowTime
= ctt
+(forced ?
2.seconds
: 10.seconds
);
499 if (msg
.resendCount
++ != 0) doSave
= true;
500 // do not resend more than one message at a time, so receiver will get them in order.
501 // ack processor will call us again in forced more if there's more messages to resend.
502 // TODO: introduce a flag to avoid resending until connection lost, or got an ack.
506 if (doSave
) saveResendQueue();
508 //nextSendTime = MonoTime.currTime+(forced ? 5.seconds : 30.seconds);
509 nextSendTime
= MonoTime
.currTime
+30.seconds
; // this should be enough to get an ack (i hope)
512 void send (const(char)[] text
) {
514 static bool isWordBoundary (char ch) {
515 if (ch <= ' ') return true;
516 if (ch >= 127) return false;
517 if (ch >= 'A' && ch <= 'Z') return false;
518 if (ch >= 'a' && ch <= 'z') return false;
519 if (ch >= '0' && ch <= '9') return false;
524 void sendOne (const(char)[] text
, bool action
) {
525 if (text
.length
== 0) return; // just in case
527 SysTime now
= systimeNow
;
528 long msgid
= toxCoreSendMessage(acc
.toxpk
, info
.pubkey
, text
, action
);
529 if (msgid
< 0) { conwriteln("ERROR sending message to '", info
.nick
, "'"); return; }
531 // add this to resend queue
535 xmsg
.msgid
= msgid
; // 0: we are offline
536 xmsg
.resendCount
= 0;
538 auto ctt
= MonoTime
.currTime
;
539 if (nextSendTime
<= ctt
) nextSendTime
= ctt
+30.seconds
;
540 xmsg
.resendAllowTime
= ctt
+60.seconds
;
544 import std
.format
: format
;
545 auto dt = cast(DateTime
)now
;
546 xmsg
.text
= "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year
, dt.month
, dt.day
, dt.hour
, dt.minute
, dt.second
, text
);
551 if (activeContact
is this) addTextToLog(acc
, this, LogFile
.Msg
.Kind
.Outgoing
, action
, text
, now
, msgid
);
552 appendToLog(LogFile
.Msg
.Kind
.Outgoing
, text
, action
, now
);
555 bool action
= text
.startsWith("/me ");
556 if (action
) text
= text
[4..$].xstripleft
;
558 while (text
.length
) {
559 auto ep
= text
.indexOf('\n');
560 if (ep
< 0) ep
= text
.length
; else ++ep
; // include '\n'
561 // remove line if it contains only spaces
562 bool hasNonSpace
= false;
563 foreach (immutable char ch
; text
[0..ep
]) if (ch
> ' ') { hasNonSpace
= true; break; }
564 if (hasNonSpace
) break;
567 while (text
.length
&& text
[$-1] <= ' ') text
= text
[0..$-1];
568 if (text
.length
== 0) return; // nothing to do
571 //TODO: split at word boundaries
572 enum ReservedSpace
= 32+3+3; // 32 is for date/time stamp
573 const int maxmlen
= tox_max_message_length()-ReservedSpace
;
576 // remove leading spaces if we have to split the text
577 while (text
.length
> maxmlen
&& text
[0] <= ' ') text
= text
[1..$];
579 if (text
.length
<= maxmlen
) {
580 sendOne(text
, action
);
582 // k8: toxcore developers are idiots, so we have to do dynalloc here
583 auto tmpbuf
= new char[](maxmlen
+ReservedSpace
+64);
584 scope(exit
) delete tmpbuf
;
587 assert(text
[0] > ' ');
588 while (text
.length
) {
589 // skip trailing spaces
590 if (!first
&& text
[0] <= ' ') { text
= text
[1..$]; continue; }
591 // find word boundary
593 if (epos
< text
.length
) {
594 // go back, remember last word start
595 while (epos
> 0 && text
[epos
-1] > ' ') --epos
;
601 if (text
[epos
] < 128 ||
(text
[epos
]&0xc0) == 0xc0) break;
603 if (epos
== 0) epos
= maxmlen
; // meh
605 // remove trailing spaces
606 while (epos
> 0 && text
[epos
-1] <= ' ') --epos
;
607 // we removed trailing spaces, so there should be at least one non-space char
611 epos
= cast(int)text
.length
;
614 if (first
&& epos
>= text
.length
) {
615 sendOne(text
[0..epos
], action
);
618 if (!first
) { tmpbuf
[0..3] = "..."; ofs
= 3; }
619 tmpbuf
[ofs
..ofs
+epos
] = text
[0..epos
];
620 if (epos
< text
.length
) {
621 tmpbuf
[ofs
+epos
..ofs
+epos
+3] = "...";
622 sendOne(tmpbuf
[0..ofs
+epos
+3], action
);
624 sendOne(tmpbuf
[0..ofs
+epos
], action
);
628 text
= text
[epos
..$];
637 // ////////////////////////////////////////////////////////////////////////// //
638 final class Account
{
640 void saveGroups () nothrow {
641 import std
.algorithm
: sort
;
642 GroupOptions
[] glist
;
643 scope(exit
) delete glist
;
644 foreach (Group g
; groups
) glist
~= g
.info
;
645 glist
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
646 glist
.serialize(basePath
~"contacts/groups.rc");
649 void saveUpdatedGroups () {
650 bool needToUpdate
= false;
651 foreach (Group g
; groups
) if (g
.mDirty
) { needToUpdate
= true; g
.mDirty
= false; }
652 if (needToUpdate
) saveGroups();
656 ContactStatus mStatus
= ContactStatus
.Offline
;
659 PubKey toxpk
= toxCoreEmptyKey
;
660 string toxDataDiskName
;
661 string basePath
; // with trailing "/"
662 ProtoOptions protoOpts
;
665 Contact
[PubKey
] contacts
;
668 bool mIAmConnecting
= false;
669 bool mIAmOnline
= false;
670 bool forceOnline
= true; // set to `false` to stop autoreconnecting
673 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting
;
675 @property bool isOnline () const nothrow @safe @nogc {
676 if (!toxpk
.isValidKey
) return false;
677 if (mIAmConnecting
) return false;
678 if (!mIAmOnline
) return false;
682 @property ContactStatus
status () const nothrow @safe @nogc {
683 if (!toxpk
.isValidKey
) return ContactStatus
.Offline
;
684 if (mIAmConnecting
) return ContactStatus
.Connecting
;
685 if (!mIAmOnline
) return ContactStatus
.Offline
;
689 @property void status (ContactStatus v
) {
690 if (!toxpk
.isValidKey
) return;
691 conwriteln("changing status to ", v
, " (old: ", mStatus
, ")");
692 if (v
== ContactStatus
.Connecting
) v
= ContactStatus
.Online
;
693 forceOnline
= (v
!= ContactStatus
.Offline
);
694 if (mStatus
== ContactStatus
.Offline
) {
695 mIAmConnecting
= (v
!= ContactStatus
.Offline
);
696 mIAmOnline
= false; // just in case
698 toxCoreSetStatus(toxpk
, v
);
700 // if not reconnecting, make all contacts offline
701 if (v
== ContactStatus
.Offline
&& !mIAmConnecting
) {
702 foreach (Contact ct
; this) ct
.status
= ContactStatus
.Offline
;
704 glconPostScreenRepaint();
708 void processResendQueue () {
709 if (!isOnline
) return;
710 foreach (Contact ct
; contacts
.byValue
) { ct
.processResendQueue(); ct
.update(); }
714 void saveResendQueue () {
715 foreach (Contact ct
; contacts
.byValue
) { ct
.saveResendQueue(); ct
.update(); }
719 string
getAddress () {
720 if (!toxpk
.isValidKey
) return null;
721 return tox_hex(toxCoreGetSelfAddress(toxpk
));
726 toxpk
= toxCoreOpenAccount(toxDataDiskName
);
727 if (!toxpk
.isValidKey
) {
728 conwriteln("creating new Tox account...");
729 string nick
= info
.nick
;
730 if (nick
.length
> 0) {
732 if (nick
.length
> tox_max_name_length()) nick
= nick
[0..tox_max_name_length()];
736 toxpk
= toxCoreCreateAccount(toxDataDiskName
, nick
);
737 if (!toxpk
.isValidKey
) throw new Exception("cannot create Tox account");
739 conwriteln("my address: [", tox_hex(toxCoreGetSelfAddress(toxpk
)), "]");
740 toxLoadKnownContacts();
743 // load contacts from ToxCore data and add 'em to contact database
744 void toxLoadKnownContacts () {
745 if (!toxpk
.isValidKey
) return;
747 toxCoreForEachFriend(toxpk
, delegate (in ref PubKey self
, in ref PubKey frpub
, scope const(char)[] anick
) {
748 string nick
= anick
.buildNormalizedString
!true;
749 auto c
= (frpub
in contacts ? contacts
[frpub
] : null);
751 conwriteln("NEW friend with pk [", tox_hex(frpub
), "]; name is: ", nick
);
752 c
= new Contact(this);
755 c
.info
.pubkey
[] = frpub
[];
756 c
.info
.opts
.showOffline
= TriOption
.Default
;
757 auto ls
= toxCoreLastSeen(self
, frpub
);
758 if (ls
!= SysTime
.min
) {
759 c
.info
.lastonlinetime
= cast(uint)ls
.toUnixTime();
761 if (c
.info
.lastonlinetime
== 0 && nick
.length
== 0) {
762 c
.info
.kind
= ContactInfo
.Kind
.PengingAuthRequest
;
764 contacts
[c
.info
.pubkey
] = c
;
767 if (clist
!is null) clist
.buildAccount(this);
769 bool needSave
= false;
770 auto ls
= toxCoreLastSeen(self
, frpub
);
771 if (ls
!= SysTime
.min
) {
772 auto lsu
= cast(uint)ls
.toUnixTime();
773 if (c
.info
.lastonlinetime
!= lsu
) { c
.info
.lastonlinetime
= lsu
; needSave
= true; }
775 if (c
.info
.nick
!= nick
) {
776 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; new name is: ", nick
);
777 if (c
.info
.nick
.length
== 0 && nick
.length
!= 0) {
782 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; old name is: ", nick
);
784 if (needSave
) c
.save();
786 return false; // don't stop
790 auto data
= toxCoreLoadDataFile(VFile(toxDataDiskName
));
791 foreach (const ref ci
; data
.friends
) {
792 auto c
= (ci
.pubkey
in contacts ? contacts
[ci
.pubkey
] : null);
794 conwriteln("NEW friend with pk [", tox_hex(ci
.pubkey
), "]; name is: ", ci
.nick
);
795 c
= new Contact(this);
797 c
.info
.nick
= ci
.nick
;
798 c
.info
.statusmsg
= ci
.statusmsg
;
799 c
.info
.lastonlinetime
= ci
.lastonlinetime
;
800 c
.info
.kind
= ci
.kind
;
801 c
.info
.pubkey
[] = ci
.pubkey
[];
802 c
.info
.opts
.showOffline
= TriOption
.Default
;
803 contacts
[c
.info
.pubkey
] = c
;
806 if (clist
!is null) clist
.buildAccount(this);
808 bool needSave
= false;
810 if (c
.info
.nick
!= ci
.nick
) {
811 conwriteln("OLD friend with pk [", tox_hex(ci
.pubkey
), "]; new name is: ", ci
.nick
);
813 conwriteln("OLD friend with pk [", tox_hex(ci
.pubkey
), "]; old name is: ", ci
.nick
);
816 if (c
.info
.kind
!= ci
.kind
) { c
.info
.kind
= ci
.kind
; needSave
= true; }
817 if (c
.info
.lastonlinetime
!= ci
.lastonlinetime
) { c
.info
.lastonlinetime
= ci
.lastonlinetime
; needSave
= true; }
818 if (c
.info
.nick
.length
== 0 && ci
.nick
.length
!= 0) { c
.info
.nick
= ci
.nick
; needSave
= true; }
819 if (needSave
) c
.save();
822 } catch (Exception e
) {}
827 bool sendFriendRequest (in ref ToxAddr fraddr
, const(char)[] msg
) {
828 if (!toxpk
.isValidKey
) return false;
829 if (!isValidAddr(fraddr
)) return false;
830 if (msg
.length
== 0) return false;
831 if (msg
.length
> tox_max_friend_request_length()) return false;
832 if (fraddr
[0..PubKey
.length
] == toxpk
[]) return false; // there is no reason to friend myself
833 PubKey frpub
= fraddr
[0..PubKey
.length
];
834 auto c
= (frpub
in contacts ? contacts
[frpub
] : null);
835 if (c
!is null) { c
.statusmsg
= msg
.idup
; c
.save(); }
836 if (!toxCoreSendFriendRequest(toxpk
, fraddr
, msg
)) return false;
838 c
= new Contact(this);
840 c
.info
.nick
= null; // unknown yet
841 c
.info
.pubkey
[] = frpub
[];
842 c
.info
.opts
.showOffline
= TriOption
.Default
;
843 c
.info
.kind
= ContactInfo
.Kind
.PengingAuthRequest
;
844 c
.info
.fraddr
[] = fraddr
[]; // save address for resending (and just in case)
845 contacts
[c
.info
.pubkey
] = c
;
848 if (clist
!is null) clist
.buildAccount(this);
854 // connection established
855 void toxConnectionDropped () {
857 conprintfln("TOX[%s] CONNECTION DROPPED", timp
.srvalias
);
858 mIAmConnecting
= false;
860 auto oldst
= mStatus
;
861 toxCoreSetStatus(toxpk
, ContactStatus
.Offline
); // this kills toxcore instance
862 foreach (Contact ct
; contacts
.byValue
) {
863 ct
.status
= ContactStatus
.Offline
;
864 if (ct
.kfd
) toxCoreRemoveFriend(toxpk
, ct
.info
.pubkey
);
865 // this is not quite correct: we need to distinguish when user goes offline, and when toxcore temporarily goes offline
866 // but we're killing toxcore instance on disconnect, so it works
870 mStatus
= ContactStatus
.Offline
;
873 glconPostScreenRepaint();
876 // connection established
877 void toxConnectionEstablished () {
879 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp
.srvalias
);
880 mIAmConnecting
= false;
882 if (info
.statusmsg
.length
== 0) info
.statusmsg
= "Come taste the gasoline! [BioAcid]";
883 toxCoreSetStatusMessage(toxpk
, info
.statusmsg
);
884 //toxLoadKnownContacts();
885 foreach (Contact ct
; contacts
.byValue
) ct
.resetQueueIds();
886 glconPostScreenRepaint();
889 void toxFriendOffline (in ref PubKey fpk
) {
890 if (auto ct
= fpk
in contacts
) {
891 auto ls
= toxCoreLastSeen(toxpk
, fpk
);
892 if (ls
!= SysTime
.min
) {
893 auto lsu
= cast(uint)ls
.toUnixTime();
894 if (ct
.info
.lastonlinetime
!= lsu
) { ct
.info
.lastonlinetime
= lsu
; ct
.markDirty(); glconPostScreenRepaint(); }
896 if (ct
.status
!= ContactStatus
.Offline
) {
897 conwriteln("friend <", ct
.info
.nick
, "> gone offline");
898 ct
.status
= ContactStatus
.Offline
;
899 glconPostScreenRepaint();
904 void toxSelfStatus (ContactStatus cst
) {
905 if (mStatus
!= cst
) {
907 glconPostScreenRepaint();
911 void toxFriendStatus (in ref PubKey fpk
, ContactStatus cst
) {
912 if (auto ct
= fpk
in contacts
) {
914 if (ct
.status
!= cst
) {
915 conwriteln("status for friend <", ct
.info
.nick
, "> changed to ", cst
);
916 auto ls
= toxCoreLastSeen(toxpk
, fpk
);
917 if (ls
!= SysTime
.min
) {
918 auto lsu
= cast(uint)ls
.toUnixTime();
919 if (ct
.info
.lastonlinetime
!= lsu
) { ct
.info
.lastonlinetime
= lsu
; ct
.markDirty(); }
922 // if it is online, and it is not a friend, turn it into a friend
923 if (ct
.online
&& !ct
.friend
) ct
.kind
= ContactInfo
.Kind
.Friend
;
924 if (ct
.online
) ct
.processResendQueue(true); // why not?
925 glconPostScreenRepaint();
930 void toxFriendStatusMessage (in ref PubKey fpk
, string msg
) {
931 if (auto ct
= fpk
in contacts
) {
933 if (ct
.info
.statusmsg
!= msg
) {
934 conwriteln("status message for friend <", ct
.info
.nick
, "> changed to <", msg
, ">");
935 ct
.info
.statusmsg
= msg
;
937 if (ct
.showPopup
) showPopup(PopupWindow
.Kind
.Status
, ct
.nick
, (msg
.length ? msg
: "<nothing>"));
938 glconPostScreenRepaint();
943 void toxFriendNickChanged (in ref PubKey fpk
, string nick
) {
944 if (auto ct
= fpk
in contacts
) {
945 nick
= nick
.xstrip();
946 if (nick
.length
&& ct
.info
.nick
!= nick
) {
947 auto onick
= ct
.info
.nick
;
950 if (ct
.showPopup
) showPopup(PopupWindow
.Kind
.Status
, ct
.nick
, "changed nick to '"~nick
~"' from '"~onick
~"'");
951 glconPostScreenRepaint();
956 void toxFriendReqest (in ref PubKey fpk
, const(char)[] msg
) {
958 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp
.srvalias
, tox_hex(fpk
[]), msg
);
959 if (auto ct
= fpk
in contacts
) {
961 // if not waiting for acceptance, force-friend it
962 if (!ct
.acceptPending
) {
963 conwriteln("force-friend <", ct
.info
.nick
, ">");
964 toxCoreAddFriend(toxpk
, fpk
);
966 ct
.kind
= ContactInfo
.Kind
.PengingAuthAccept
;
967 ct
.info
.statusmsg
= msg
.idup
;
969 ct
.setLastOnlineNow();
971 showPopup(PopupWindow
.Kind
.Info
, "Friend Accepted", (msg
.length ? msg
: "<nothing>"));
973 // new friend request
974 conwriteln("AUTH REQUEST from pk [", tox_hex(fpk
), "]");
975 auto c
= new Contact(this);
978 c
.info
.pubkey
[] = fpk
[];
979 c
.info
.opts
.showOffline
= TriOption
.Default
;
980 c
.info
.statusmsg
= msg
.idup
;
981 c
.kind
= ContactInfo
.Kind
.PengingAuthAccept
;
982 contacts
[c
.info
.pubkey
] = c
;
983 c
.setLastOnlineNow();
985 showPopup(PopupWindow
.Kind
.Info
, "Friend Request", (msg
.length ? msg
: "<nothing>"));
987 if (clist
!is null) clist
.buildAccount(this);
989 glconPostScreenRepaint();
992 void toxFriendMessage (in ref PubKey fpk
, bool action
, string msg
, SysTime time
) {
993 if (auto ct
= fpk
in contacts
) {
994 bool doPopup
= ct
.showPopup
;
995 LogFile
.Msg
.Kind kind
= LogFile
.Msg
.Kind
.Incoming
;
996 ct
.appendToLog(kind
, msg
, action
, time
);
997 if (*ct
is activeContact
) {
998 // if inactive or invisible, add divider line and increase unread count
999 if (!mainWindowVisible ||
!mainWindowActive
) {
1000 if (ct
.unreadCount
== 0) addDividerLine();
1001 ct
.unreadCount
+= 1;
1002 ct
.saveUnreadCount();
1006 addTextToLog(this, *ct
, kind
, action
, msg
, time
);
1008 ct
.unreadCount
+= 1;
1009 ct
.saveUnreadCount();
1011 if (doPopup
) showPopup(PopupWindow
.Kind
.Incoming
, ct
.nick
, msg
);
1015 // ack for sent message
1016 void toxMessageAck (in ref PubKey fpk
, long msgid
) {
1018 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp
.srvalias
, tox_hex(fpk
[]), msgid
);
1019 if (auto ct
= fpk
in contacts
) {
1020 if (*ct
is activeContact
) ackLogMessage(msgid
);
1021 ct
.ackReceived(msgid
);
1026 @property string
srvalias () const pure nothrow @safe @nogc => info
.nick
;
1032 if (toxDataDiskName
.length
== 0) return false;
1033 lockf
= LockFile(toxDataDiskName
~".lock");
1034 if (!lockf
.tryLock
) {
1041 void createWithBaseDir (string aBaseDir
) {
1042 import std
.algorithm
: sort
;
1043 import std
.file
: DirEntry
, SpanMode
, dirEntries
;
1044 import std
.path
: baseName
;
1046 basePath
= normalizeBaseDir(aBaseDir
);
1047 toxDataDiskName
= basePath
~"toxdata.tox";
1049 if (!tryLockIt
) throw new Exception("cannot activate already active account");
1051 protoOpts
.txtunser(VFile(basePath
~"proto.rc"));
1052 info
.txtunser(VFile(basePath
~"config.rc"));
1055 GroupOptions
[] glist
;
1056 glist
.txtunser(VFile(basePath
~"contacts/groups.rc"));
1057 bool hasDefaultGroup
= false;
1058 bool hasMoronsGroup
= false;
1059 foreach (ref GroupOptions gi
; glist
[]) {
1060 auto g
= new Group(this);
1063 foreach (ref gg
; groups
[]) if (gg
.gid
== g
.gid
) { delete gg
; gg
= g
; found
= true; }
1064 if (!found
) groups
~= g
;
1065 if (g
.gid
== 0) hasDefaultGroup
= true;
1066 if (g
.gid
== g
.gid
.max
-2) hasMoronsGroup
= true;
1069 // create default group if necessary
1070 if (!hasDefaultGroup
) {
1073 gi
.name
= "default";
1074 gi
.note
= "default group for new contacts";
1076 auto g
= new Group(this);
1081 // create morons group if necessary
1082 if (!hasMoronsGroup
) {
1084 gi
.gid
= gi
.gid
.max
-2;
1085 gi
.name
= "<morons>";
1086 gi
.note
= "group for completely ignored dumbfucks";
1088 gi
.showOffline
= TriOption
.No
;
1089 gi
.showPopup
= TriOption
.No
;
1090 gi
.blinkActivity
= TriOption
.No
;
1091 gi
.skipUnread
= TriOption
.Yes
;
1092 gi
.hideIfNoVisibleMembers
= TriOption
.Yes
;
1093 gi
.ftranAllowed
= TriOption
.No
;
1094 gi
.resendRotDays
= 0;
1096 auto g
= new Group(this);
1102 groups
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
1104 if (!hasDefaultGroup ||
!hasMoronsGroup
) saveGroups();
1106 static bool isValidCDName (const(char)[] str) {
1107 if (str.length
!= 64) return false;
1108 foreach (immutable char ch
; str) {
1109 if (ch
>= '0' && ch
<= '9') continue;
1110 if (ch
>= 'A' && ch
<= 'F') continue;
1111 if (ch
>= 'a' && ch
<= 'f') continue;
1118 foreach (DirEntry
de; dirEntries(basePath
~"contacts", SpanMode
.shallow
)) {
1119 if (de.name
.baseName
== "." ||
de.name
.baseName
== ".." ||
!isValidCDName(de.name
.baseName
)) continue;
1121 import std
.file
: exists
;
1122 if (!de.isDir
) continue;
1123 string cfgfn
= de.name
~"/config.rc";
1124 if (!cfgfn
.exists
) continue;
1126 ci
.txtunser(VFile(cfgfn
));
1127 auto c
= new Contact(this);
1128 c
.diskFileName
= cfgfn
;
1130 contacts
[c
.info
.pubkey
] = c
;
1131 c
.loadResendQueue();
1132 c
.loadUnreadCount();
1133 // fix contact group
1134 if (groupById
!false(c
.gid
) is null) {
1135 c
.info
.gid
= 0; // move to default group
1138 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1139 } catch (Exception e
) {
1140 conwriteln("ERROR loading contact from '", de.name
, "/config.rc'");
1145 assert(toxpk
.isValidKey
, "something is VERY wrong here");
1146 conwriteln("created ToxCore for [", tox_hex(toxpk
[]), "]");
1150 this (string aBaseDir
) { createWithBaseDir(aBaseDir
); }
1153 if (toxpk
.isValidKey
) {
1154 toxCoreCloseAccount(toxpk
);
1155 toxpk
[] = toxCoreEmptyKey
[];
1159 // save account info (but not contacts)
1161 serialize(info
, basePath
~"config.rc");
1164 // will not write contact to disk
1165 Contact
createEmptyContact () {
1166 auto c
= new Contact(this);
1168 c
.info
.nick
= "test contact";
1169 c
.info
.pubkey
[] = 0;
1173 // returns `null` if there is no such group, and `dofail` is `true`
1174 inout(Group
) groupById(bool dofail
=true) (uint agid
) inout nothrow @nogc {
1175 foreach (const Group g
; groups
) if (g
.gid
== agid
) return cast(inout)g
;
1176 static if (dofail
) assert(0, "group not found"); else return null;
1179 int opApply () (scope int delegate (ref Contact ct
) dg
) {
1180 foreach (Contact ct
; contacts
.byValue
) if (auto res
= dg(ct
)) return res
;
1184 // returns gid, or uint.max on error
1185 uint findGroupByName (const(char)[] name
) nothrow {
1186 if (name
.length
== 0) return uint.max
;
1187 foreach (const Group g
; groups
) if (g
.name
== name
) return g
.gid
;
1191 // returns gid, or uint.max on error
1192 uint createGroup(T
:const(char)[]) (T name
) nothrow {
1193 if (name
.length
== 0) return uint.max
;
1194 // groups are sorted by gid, yeah
1196 foreach (const Group g
; groups
) {
1197 if (g
.name
== name
) return g
.gid
;
1198 if (newgid
== g
.gid
) newgid
= g
.gid
+1;
1200 if (newgid
== uint.max
) return uint.max
;
1204 static if (is(T
== string
)) gi
.name
= name
; else gi
.name
= name
.idup
;
1206 auto g
= new Group(this);
1209 import std
.algorithm
: sort
;
1210 groups
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
1215 // returns `false` on any error
1216 bool moveContactToGroup (Contact ct
, uint gid
) nothrow {
1217 if (ct
is null || gid
== uint.max
) return false;
1218 // check if this is our contact
1220 foreach (Contact cc
; contacts
.byValue
) if (cc
is ct
) { found
= true; break; }
1221 if (!found
) return false;
1224 foreach (Group g
; groups
) if (g
.gid
== gid
) { grp
= g
; break; }
1225 if (grp
is null) return false;
1227 if (ct
.info
.gid
!= gid
) {
1234 // returns `false` on any error
1235 bool removeContact (Contact ct
) {
1236 if (ct
is null ||
!isOnline
) return false;
1237 // check if this is our contact
1239 foreach (Contact cc
; contacts
.byValue
) if (cc
is ct
) { found
= true; break; }
1240 if (!found
) return false;
1241 toxCoreRemoveFriend(toxpk
, ct
.info
.pubkey
);
1243 contacts
.remove(ct
.info
.pubkey
);
1245 if (clist
!is null) {
1246 if (clist
.isActiveContact(ct
.info
.pubkey
)) clist
.resetActiveItem();
1247 clist
.buildAccount(this);
1250 ct.kind = ContactInfo.Kind.KillFuckDie;
1257 static string
normalizeBaseDir (string aBaseDir
) {
1258 import std
.path
: absolutePath
, expandTilde
;
1259 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
[$-1] == '/') throw new Exception("invalid base dir");
1260 if (aBaseDir
.indexOf('/') < 0) aBaseDir
= "~/.bioacid/"~aBaseDir
;
1261 aBaseDir
= aBaseDir
.expandTilde
.absolutePath
;
1262 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
1267 static Account
CreateNew (string aBaseDir
, string aAccName
) {
1268 aBaseDir
= normalizeBaseDir(aBaseDir
);
1269 auto lockf
= LockFile(aBaseDir
~"toxdata.tox.lock");
1270 if (!lockf
.tryLock
) {
1272 throw new Exception("cannot create locked account");
1274 mkdirRec(aBaseDir
~"contacts");
1275 // write protocol options
1278 serialize(popt
, aBaseDir
~"proto.rc");
1283 acc
.nick
= aAccName
;
1284 acc
.showPopup
= true;
1285 acc
.blinkActivity
= true;
1286 acc
.hideEmptyGroups
= false;
1287 acc
.ftranAllowed
= true;
1288 acc
.resendRotDays
= 4;
1290 serialize(acc
, aBaseDir
~"config.rc");
1292 // create default group
1294 GroupOptions
[1] grp
;
1296 grp
[0].name
= "default";
1297 grp
[0].opened
= true;
1298 //grp[0].hideIfNoVisible = TriOption.Yes;
1299 serialize(grp
[], aBaseDir
~"contacts/groups.rc");
1302 return new Account(aBaseDir
);
1307 // ////////////////////////////////////////////////////////////////////////// //
1308 void setupToxEventListener (SimpleWindow sdmain
) {
1309 assert(sdmain
!is null);
1311 sdmain
.addEventListener((ToxEventBase evt
) {
1312 auto acc
= clist
.accountByPK(evt
.self
);
1314 if (acc
is null) return;
1316 bool fixTray
= false;
1317 scope(exit
) { if (fixTray
) fixTrayIcon(); glconPostScreenRepaint(); }
1320 if (auto e
= cast(ToxEventConnection
)evt
) {
1321 if (e
.who
[] == acc
.toxpk
[]) {
1322 if (e
.connected
) acc
.toxConnectionEstablished(); else acc
.toxConnectionDropped();
1325 if (!e
.connected
) acc
.toxFriendOffline(e
.who
);
1330 if (auto e
= cast(ToxEventStatus
)evt
) {
1331 if (e
.who
[] == acc
.toxpk
[]) {
1332 acc
.toxSelfStatus(e
.status
);
1335 acc
.toxFriendStatus(e
.who
, e
.status
);
1340 if (auto e
= cast(ToxEventStatusMsg
)evt
) {
1341 if (e
.who
[] != acc
.toxpk
[]) acc
.toxFriendStatusMessage(e
.who
, e
.message
);
1345 if (auto e
= cast(ToxEventNick
)evt
) {
1346 if (e
.who
[] != acc
.toxpk
[]) acc
.toxFriendNickChanged(e
.who
, e
.nick
);
1349 // incoming text message?
1350 if (auto e
= cast(ToxEventMessage
)evt
) {
1351 if (e
.who
[] != acc
.toxpk
[]) {
1352 acc
.toxFriendMessage(e
.who
, e
.action
, e
.message
, e
.time
);
1357 // ack outgoing text message?
1358 if (auto e
= cast(ToxEventMessageAck
)evt
) {
1359 if (e
.who
[] != acc
.toxpk
[]) acc
.toxMessageAck(e
.who
, e
.msgid
);
1363 if (auto e
= cast(ToxEventFriendReq
)evt
) {
1364 if (e
.who
[] != acc
.toxpk
[]) acc
.toxFriendReqest(e
.who
, e
.message
);
1368 //glconProcessEventMessage();