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 accdb
is aliced
;
31 // ////////////////////////////////////////////////////////////////////////// //
32 enum TriOption
{ Default
= -1, No
= 0, Yes
= 1 }
34 enum ContactStatus
{ Offline
, Online
, Away
, Busy
, Connecting
}
36 alias PubKey
= ubyte[TOX_PUBLIC_KEY_SIZE
];
39 // ////////////////////////////////////////////////////////////////////////// //
40 struct CommonOptions
{
41 TriOption showOffline
= TriOption
.Default
; // show this contact even if it is offline
42 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this contact
43 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity for this contact
44 TriOption skipUnread
= TriOption
.Default
; // skip this contacts in `next_unread` command
45 TriOption ftranAllowed
= TriOption
.Default
; // file transfers allowed for this contact
46 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
47 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
48 //@SRZNonDefaultOnly TriOption confAutoJoin; // automatically join the conference when we're going online
66 struct AccountConfig
{
67 string nick
; // my nick
68 string statusmsg
; // my status message
69 bool showOffline
; // show offline persons?
70 bool showPopup
; // show popups for messages?
71 bool blinkActivity
; // blink tray icon if we have some activity (unread msg, transfer request, etc.)?
72 bool skipUnread
; // skip contacts in `next_unread` command?
73 bool hideEmptyGroups
; // hide empty groups? (can be overriden by `hideNoVisible` group option)
74 bool ftranAllowed
; // file transfers allowed for this group (-1: use default value)
75 int resendRotDays
; // how many days message should be in "resend queue" if contact is offline
76 int hmcOnOpen
; // how many history messages we should show when we opening a chat with a contact
81 uint gid
; // group id; there is always group with gid 0, it is "common" default group
82 string name
; // group name
83 string note
; // group notes/description
84 bool opened
; // is this group opened?
85 TriOption showOffline
= TriOption
.Default
; // show offline persons in this group
86 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this group
87 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity (unread msg, transfer request, etc.) for this group
88 TriOption skipUnread
= TriOption
.Default
; // skip contacts from this group in `next_unread` command
89 TriOption hideIfNoVisibleMembers
= TriOption
.Default
; // hide this group if there are no visible items in it
90 TriOption ftranAllowed
; // file transfers allowed for this group
91 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
92 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
100 // pending authorization acceptance
101 // statusmsg: request text
102 // lastonlinetime: request time
104 // auth requested, awaiting acceptance
105 // statusmsg: request text
106 // lastonlinetime: request time
107 // this will change to `Friend` when contact gets online (it means that auth is accepted)
109 // this contact was deleted and put in "total ignore" mode
110 // i.e. any activity from this contact (especially auth requests) will be silently dropped on the floor
113 uint gid
; // group id (see groups.rc)
114 string nick
; // empty: unauthorized
115 @SRZNonDefaultOnly string visnick
; // empty: use `nick`
116 @SRZNonDefaultOnly string statusmsg
;
117 @SRZNonDefaultOnly uint lastonlinetime
; // local unixtime; changed when contact status changed between offline and online (any kind of online)
118 Kind kind
= Kind
.Friend
;
119 string note
; // contact notes/description
120 PubKey pubkey
; // used as unique contact id, same as directory name
125 // ////////////////////////////////////////////////////////////////////////// //
126 void syncFile(bool fullsync
=false) (Imp
!"core.stdc.stdio".FILE
* fl
) nothrow @nogc {
128 import core
.stdc
.stdio
: fileno
;
131 import core
.sys
.posix
.unistd
: fsync
, fdatasync
;
132 static if (fullsync
) fsync(fd
); else fdatasync(fd
);
138 // ////////////////////////////////////////////////////////////////////////// //
139 // write to temporary file, then atomically replace
140 bool serialize(T
) (in auto ref T v
, string fname
) {
141 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, rename
;
142 import core
.sys
.posix
.unistd
: unlink
;
143 import std
.internal
.cstring
;
145 string tmpfname
= fname
~".$$$";
146 FILE
* fo
= fopen(tmpfname
.tempCString
, "w");
147 if (fo
is null) return false;
150 v
.txtser(VFile(fo
, own
:false), skipstname
:true);
151 } catch (Exception e
) {
153 unlink(tmpfname
.tempCString
);
159 if (rename(tmpfname
.tempCString
, fname
.tempCString
) != 0) {
160 unlink(tmpfname
.tempCString
);
168 // ////////////////////////////////////////////////////////////////////////// //
170 log format (it looks like text, but it isn't text):
171 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
173 text: chars in range of [0..31, 92, 127] are \xHH-encoded
175 !: server/app notification
178 if char before text is '!' instead of space, this is "/me" message
180 static struct LogFile
{
182 // this struct must not outlive LogFile
184 enum Kind
{ Notification
, Incoming
, Outgoing
}
186 bool isMe
; // "/me" message?
188 char[] rawText
; // undecoded
191 string
text () const {
192 import iv
.utfutil
: utf8Encode
;
194 text
.reserve(rawText
.length
);
196 foreach (dchar dc
; byDChar
) {
198 auto len
= utf8Encode(buf
[], dc
);
201 return cast(string
)text
; // it is safe to cast here
204 // decode raw text to dchars; returns forward range
205 auto byDChar () const nothrow @safe @nogc {
206 static struct Range
{
207 pure nothrow @trusted @nogc:
208 const(char)[] raw
; // anchored
210 @property bool empty () const => (pos
>= raw
.length
);
211 @property dchar front () const {
212 if (pos
< raw
.length
) {
213 char ch
= raw
.ptr
[pos
];
215 if (ch
!= '\\') return ch
;
216 if (raw
.length
-pos
< 2) return ch
;
218 if (ch
!= 'x' && ch
!= 'X') {
220 case 'e': return '\e';
221 case 'n': return '\n';
222 case 'r': return '\r';
223 case 't': return '\t';
228 if (raw
.length
-pos
< 4) return '?';
229 int n0
= raw
.ptr
[pos
+2].digitInBase(16);
230 if (n0
< 0) return '?';
231 int n1
= raw
.ptr
[pos
+3].digitInBase(16);
232 if (n1
< 0) return '?';
233 return cast(char)(n0
*16+n1
);
237 while (!dc
.decode(cast(ubyte)raw
.ptr
[cpos
++])) {
238 if (cpos
>= raw
.length
) break; // no more chars
240 return (dc
.complete ? dc
.codepoint
: dc
.replacement
);
242 return Utf8DecoderFast
.replacement
;
246 if (pos
< raw
.length
) {
247 char ch
= raw
.ptr
[pos
];
250 if (ch
!= '\\') return;
251 if (raw
.length
-pos
< 1) { pos
= raw
.length
; return; }
253 if (ch
!= 'x' && ch
!= 'X') return;
254 if (raw
.length
-pos
< 2) { pos
= raw
.length
; return; }
259 while (!dc
.decode(cast(ubyte)raw
.ptr
[pos
++])) {
260 if (pos
>= raw
.length
) break; // no more chars
264 Range
save () const => Range(raw
, pos
);
266 return Range(rawText
, 0);
277 @disable this (this); // no copies
278 void opAssign() (in auto ref LogFile lf
) { static assert(0, "no copies!"); }
280 this (const(char)[] fname
) nothrow => load(fname
);
282 // no `Msg` should outlive this call!
283 void clear () nothrow {
288 void load (const(char)[] fname
) nothrow {
289 import std
.internal
.cstring
;
291 if (messages
.length
) {
293 messages
.assumeSafeAppend
;
297 data
.assumeSafeAppend
;
300 if (fname
.length
== 0) return;
303 auto fi
= VFile(fname
);
304 if (fi
.size
> int.max
/8) assert(0, "log file too big");
305 data
.length
= cast(int)fi
.size
;
306 fi
.rawReadExact(data
);
307 } catch (Exception e
) {}
312 // `data` should be set
313 private void parseData () nothrow {
314 import core
.stdc
.string
: memchr
;
316 assert(messages
.length
== 0); // sorry
317 if (data
.length
== 0) return;
323 while (dp
< data
.ptr
+data
.length
) {
324 auto nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
325 if (nx
is null) break;
330 messages
.reserve(nlcount
+1);
332 static int toInt (const(char)[] s
) nothrow @trusted @nogc {
334 while (s
.length
&& s
.ptr
[0].isdigit
) {
335 res
= res
*10+s
.ptr
[0]-'0';
342 auto dp
= data
.ptr
, nx
= data
.ptr
+data
.length
;
343 for (; dp
< data
.ptr
+data
.length
; dp
= nx
+1) {
344 nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
345 if (nx
is null) nx
= data
.ptr
+data
.length
;
346 // 25 is the minimal message length
347 if (nx
-dp
< 25) continue;
348 if (dp
[1] != '[' || dp
[6] != '/' || dp
[9] != '/' || dp
[12] != ' ' || dp
[15] != ':' || dp
[18] != ':' || dp
[21] != ']' || dp
[22] != ':') continue;
349 foreach (immutable cidx
, immutable char ch
; dp
[0..21]) {
350 if (cidx
< 2 || cidx
== 6 || cidx
== 9 || cidx
== 12 || cidx
== 15 || cidx
== 18) continue;
351 if (!ch
.isdigit
) continue;
356 case '!': msg
.kind
= msg
.Kind
.Notification
; break;
357 case '<': msg
.kind
= msg
.Kind
.Outgoing
; break;
358 case '>': msg
.kind
= msg
.Kind
.Incoming
; break;
363 case ' ': msg
.isMe
= false; break;
364 case '!': msg
.isMe
= true; break;
367 int year
= toInt(dp
[2..6]);
368 if (year
< 2018 || year
> 2036) continue; //FIXME in 2036! ;-)
369 int month
= toInt(dp
[7..9]);
370 if (month
< 1 || month
> 12) continue;
371 int day
= toInt(dp
[10..12]);
372 if (day
< 1 || day
> 31) continue;
373 int hour
= toInt(dp
[13..15]);
374 if (hour
< 0 || hour
> 23) continue;
375 int minute
= toInt(dp
[16..18]);
376 if (minute
< 0 || minute
> 59) continue;
377 int second
= toInt(dp
[19..21]);
378 if (second
< 0 || second
> 59) continue;
381 msg
.time
= SysTime(DateTime(year
, month
, day
, hour
, minute
, second
));
383 } catch (Exception e
) {}
384 if (!timeok
) continue;
385 msg
.rawText
= dp
[24..(nx
-dp
)];
391 // append line to log file; text is in utf8
392 // WARNING! does no validity checks!
393 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
, SysTime time
) nothrow {
394 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, fwrite
;
395 import std
.internal
.cstring
;
397 if (fname
.length
== 0) return false;
399 FILE
* fo
= fopen(fname
.tempCString
, "a");
400 if (fo
is null) return false;
401 scope(exit
) { syncFile(fo
); fclose(fo
); }
403 bool xwrite (const(char)[] s
...) nothrow @nogc {
404 while (s
.length
> 0) {
405 auto wr
= fwrite(s
.ptr
, 1, s
.length
, fo
);
407 import core
.stdc
.errno
;
408 if (errno
!= EINTR
) return false;
416 bool wrnum (int n
, int wdt
) nothrow @nogc {
417 import core
.stdc
.stdio
: snprintf
;
418 if (n
< 0) return false; // alas
419 char[128] buf
= void;
420 auto len
= (wdt
> 0 ?
snprintf(buf
.ptr
, buf
.length
, "%0*d", wdt
, n
) : snprintf(buf
.ptr
, buf
.length
, "%d", n
));
421 if (len
< 1) return false;
422 return xwrite(buf
[0..len
]);
425 final switch (kind
) {
426 case LogFile
.Msg
.Kind
.Notification
: if (!xwrite("!")) return false; break;
427 case LogFile
.Msg
.Kind
.Incoming
: if (!xwrite(">")) return false; break;
428 case LogFile
.Msg
.Kind
.Outgoing
: if (!xwrite("<")) return false; break;
432 auto dt = cast(DateTime
)time
;
433 if (!xwrite("[")) return false;
434 if (!wrnum(dt.year
, 4)) return false;
435 if (!xwrite("/")) return false;
436 if (!wrnum(cast(int)dt.month
, 2)) return false;
437 if (!xwrite("/")) return false;
438 if (!wrnum(dt.day
, 2)) return false;
439 if (!xwrite(" ")) return false;
440 if (!wrnum(dt.hour
, 2)) return false;
441 if (!xwrite(":")) return false;
442 if (!wrnum(dt.minute
, 2)) return false;
443 if (!xwrite(":")) return false;
444 if (!wrnum(dt.second
, 2)) return false;
445 if (!xwrite("]:")) return false;
446 if (!xwrite(isMe ?
"!" : " ")) return false;
448 // write encoded string
449 while (text
.length
) {
451 while (end
< text
.length
) {
452 char ch
= text
.ptr
[end
];
453 if (ch
< ' ' || ch
== 92 || ch
== 127) break;
457 if (!xwrite(text
[0..end
])) return false;
460 switch (text
.ptr
[0]) {
461 case '\e': if (!xwrite(`\e`)) return false; break;
462 case '\n': if (!xwrite(`\n`)) return false; break;
463 case '\r': if (!xwrite(`\r`)) return false; break;
464 case '\t': if (!xwrite(`\t`)) return false; break;
466 import core
.stdc
.stdio
: snprintf
;
468 auto len
= snprintf(buf
.ptr
, buf
.length
, "\\x%02x", cast(uint)text
.ptr
[0]);
469 if (!xwrite(buf
[0..len
])) return false;
479 // append line to log file; text is in utf8
480 // WARNING! does no validity checks!
481 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
=false) nothrow {
483 return appendLine(fname
, kind
, text
, isMe
, Clock
.currTime
);
484 } catch (Exception e
) {}