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 accdb
is aliced
;
30 // ////////////////////////////////////////////////////////////////////////// //
31 public alias TextDigest
= ubyte[20]; // ripemd160
33 public TextDigest
textDigest (const(void)[] buf
) nothrow @trusted @nogc {
34 import std
.digest
.ripemd
;
35 return ripemd160Of(buf
);
39 public SysTime
systimeNow () nothrow @trusted {
41 SysTime st
= Clock
.currTime
;
42 return SysTime
.fromUnixTime(st
.toUnixTime
); // trunc to second precision
43 } catch (Exception e
) {}
44 assert(0, "time error!");
48 // ////////////////////////////////////////////////////////////////////////// //
49 enum TriOption
{ Default
= -1, No
= 0, Yes
= 1 }
51 enum ContactStatus
{ Offline
, Online
, Away
, Busy
, Connecting
}
53 alias PubKey
= ubyte[TOX_PUBLIC_KEY_SIZE
];
54 alias ToxAddr
= ubyte[TOX_ADDRESS_SIZE
];
57 // ////////////////////////////////////////////////////////////////////////// //
58 struct CommonOptions
{
59 TriOption showOffline
= TriOption
.Default
; // show this contact even if it is offline
60 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this contact
61 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity for this contact
62 TriOption skipUnread
= TriOption
.Default
; // skip this contacts in `next_unread` command
63 TriOption ftranAllowed
= TriOption
.Default
; // file transfers allowed for this contact
64 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
65 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
66 //@SRZNonDefaultOnly TriOption confAutoJoin; // automatically join the conference when we're going online
84 struct AccountConfig
{
85 string nick
; // my nick
86 string statusmsg
; // my status message
87 bool showOffline
; // show offline persons?
88 bool showPopup
; // show popups for messages?
89 bool blinkActivity
; // blink tray icon if we have some activity (unread msg, transfer request, etc.)?
90 bool skipUnread
; // skip contacts in `next_unread` command?
91 bool hideEmptyGroups
; // hide empty groups? (can be overriden by `hideNoVisible` group option)
92 bool ftranAllowed
; // file transfers allowed for this group (-1: use default value)
93 int resendRotDays
; // how many days message should be in "resend queue" if contact is offline
94 int hmcOnOpen
; // how many history messages we should show when we opening a chat with a contact
99 uint gid
; // group id; there is always group with gid 0, it is "common" default group
100 string name
; // group name
101 string note
; // group notes/description
102 bool opened
; // is this group opened?
103 TriOption showOffline
= TriOption
.Default
; // show offline persons in this group
104 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this group
105 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity (unread msg, transfer request, etc.) for this group
106 TriOption skipUnread
= TriOption
.Default
; // skip contacts from this group in `next_unread` command
107 TriOption hideIfNoVisibleMembers
= TriOption
.Default
; // hide this group if there are no visible items in it
108 TriOption ftranAllowed
; // file transfers allowed for this group
109 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
110 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
118 // pending authorization acceptance
119 // statusmsg: request text
120 // lastonlinetime: request time
122 // auth requested, awaiting acceptance
123 // statusmsg: request text
124 // lastonlinetime: request time
125 // this will change to `Friend` when contact gets online (it means that auth is accepted)
127 // this contact was deleted and put in "total ignore" mode
128 // i.e. any activity from this contact (especially auth requests) will be silently dropped on the floor
131 uint gid
; // group id (see groups.rc)
132 string nick
; // empty: unauthorized
133 @SRZNonDefaultOnly string visnick
; // empty: use `nick`
134 @SRZNonDefaultOnly string statusmsg
; // request message for pending auth request
135 @SRZNonDefaultOnly uint lastonlinetime
; // local unixtime; changed when contact status changed between offline and online (any kind of online)
136 Kind kind
= Kind
.Friend
;
137 string note
; // contact notes/description
138 PubKey pubkey
; // used as unique contact id, same as directory name
139 uint nospam
; // used only in `toxCoreLoadDataFile()`
140 @SRZNonDefaultOnly ToxAddr fraddr
; // as address includes nospam, save it too
145 // ////////////////////////////////////////////////////////////////////////// //
146 private void syncFile(bool fullsync
=false) (Imp
!"core.stdc.stdio".FILE
* fl
) nothrow @nogc {
148 import core
.stdc
.stdio
: fileno
;
151 import core
.sys
.posix
.unistd
: fsync
, fdatasync
;
152 static if (fullsync
) fsync(fd
); else fdatasync(fd
);
158 // ////////////////////////////////////////////////////////////////////////// //
159 public void mkdirRec (const(char)[] path
) nothrow {
160 import core
.stdc
.stdlib
: malloc
, free
;
161 import core
.sys
.posix
.sys
.stat
: mkdir
;
163 if (path
.length
== 0) return;
165 char* tpath
= cast(char*)malloc(path
.length
+1024);
166 if (tpath
is null) assert(0, "out of memory");
167 scope(exit
) free(tpath
);
170 if (path
[0] == '/') tpath
[tpos
++] = '/';
172 while (path
.length
> 0) {
173 if (path
[0] == '/') { path
= path
[1..$]; continue; }
174 while (path
.length
&& path
[0] != '/') {
175 tpath
[tpos
++] = path
[0];
185 // ////////////////////////////////////////////////////////////////////////// //
186 // write to temporary file, then atomically replace
187 public bool serialize(T
) (in auto ref T v
, string fname
) nothrow {
188 import core
.stdc
.stdlib
: realloc
, free
;
189 import core
.stdc
.stdio
: rename
;
190 import core
.sys
.posix
.fcntl
: open
, O_WRONLY
, O_CREAT
, O_TRUNC
;
191 import core
.sys
.posix
.unistd
: close
, write
, fdatasync
, unlink
;
192 import std
.internal
.cstring
;
194 static struct OutRng
{
200 this (int afd
) nothrow @trusted @nogc { fd
= afd
; }
201 ~this () { flush(); }
202 @disable this (this);
203 void opAssign() (in auto ref OutRng a
) { static assert(0, "no assigns!"); }
204 void flush () nothrow @trusted @nogc {
205 if (waserror
) { bufused
= 0; return; }
207 while (bufofs
< bufused
) {
208 auto wr
= write(fd
, buf
.ptr
+bufofs
, bufused
-bufofs
);
209 if (wr
== 0) { waserror
= true; bufused
= 0; return; }
211 import core
.stdc
.errno
;
212 if (errno
!= EINTR
) { waserror
= true; bufused
= 0; return; }
215 bufofs
+= cast(uint)wr
;
219 void put (char ch
) nothrow @trusted @nogc {
220 if (waserror
) return;
221 assert(bufused
< buf
.sizeof
);
222 buf
.ptr
[bufused
++] = ch
;
223 if (bufused
== buf
.sizeof
) flush();
227 if (fname
.length
== 0 || fname
.length
> 65536) return false;
229 static char* tmpfname
= null;
231 if (tmpfnsize
< fname
.length
+8) {
232 tmpfnsize
= cast(uint)fname
.length
+8;
233 tmpfname
= cast(char*)realloc(tmpfname
, tmpfnsize
);
234 if (tmpfname
is null) assert(0, "out of memory");
236 tmpfname
[0..fname
.length
] = fname
[];
237 tmpfname
[fname
.length
..fname
.length
+5] = ".$$$\x00";
238 int fo
= open(tmpfname
, O_WRONLY|O_CREAT|O_TRUNC
, 0o600
);
240 conwriteln("failed to create file: '", fname
, ".$$$'");
244 bool waserror
= false;
246 auto or = OutRng(fo
);
247 v
.txtser(or, skipstname
:true);
249 waserror
= or.waserror
;
250 } catch (Exception e
) {
254 if (!waserror
) fdatasync(fo
);
261 if (rename(tmpfname
, fname
.tempCString
) != 0) {
270 // ////////////////////////////////////////////////////////////////////////// //
272 log format (it looks like text, but it isn't text):
273 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
275 text: chars in range of [0..31, 92, 127] are \xHH-encoded
277 !: server/app notification
280 if char before text is '!' instead of space, this is "/me" message
282 static struct LogFile
{
284 // this struct must not outlive LogFile
286 enum Kind
{ Notification
, Incoming
, Outgoing
}
288 bool isMe
; // "/me" message?
290 char[] rawText
; // undecoded
293 string
text () const {
294 import iv
.utfutil
: utf8Encode
;
296 text
.reserve(rawText
.length
);
298 foreach (dchar dc
; byDChar
) {
300 auto len
= utf8Encode(buf
[], dc
);
303 return cast(string
)text
; // it is safe to cast here
306 // decode raw text to dchars; returns forward range
307 auto byDChar () const nothrow @safe @nogc {
308 static struct Range
{
309 pure nothrow @trusted @nogc:
310 const(char)[] raw
; // anchored
312 @property bool empty () const => (pos
>= raw
.length
);
313 @property dchar front () const {
314 if (pos
< raw
.length
) {
315 char ch
= raw
.ptr
[pos
];
317 if (ch
!= '\\') return ch
;
318 if (raw
.length
-pos
< 2) return ch
;
320 if (ch
!= 'x' && ch
!= 'X') {
322 case 'e': return '\e';
323 case 'n': return '\n';
324 case 'r': return '\r';
325 case 't': return '\t';
330 if (raw
.length
-pos
< 4) return '?';
331 int n0
= raw
.ptr
[pos
+2].digitInBase(16);
332 if (n0
< 0) return '?';
333 int n1
= raw
.ptr
[pos
+3].digitInBase(16);
334 if (n1
< 0) return '?';
335 return cast(char)(n0
*16+n1
);
338 uint cpos
= cast(uint)pos
;
339 while (!dc
.decode(cast(ubyte)raw
.ptr
[cpos
++])) {
340 if (cpos
>= raw
.length
) break; // no more chars
342 return (dc
.complete ? dc
.codepoint
: dc
.replacement
);
344 return Utf8DecoderFast
.replacement
;
348 if (pos
< raw
.length
) {
349 char ch
= raw
.ptr
[pos
];
352 if (ch
!= '\\') return;
353 if (raw
.length
-pos
< 1) { pos
= raw
.length
; return; }
355 if (ch
!= 'x' && ch
!= 'X') return;
356 if (raw
.length
-pos
< 2) { pos
= raw
.length
; return; }
361 while (!dc
.decode(cast(ubyte)raw
.ptr
[pos
++])) {
362 if (pos
>= raw
.length
) break; // no more chars
366 Range
save () const => Range(raw
, pos
);
368 return Range(rawText
, 0);
379 @disable this (this); // no copies
380 void opAssign() (in auto ref LogFile lf
) { static assert(0, "no copies!"); }
382 this (const(char)[] fname
) nothrow => load(fname
);
384 // no `Msg` should outlive this call!
385 void clear () nothrow {
390 void load (const(char)[] fname
) nothrow {
391 import std
.internal
.cstring
;
393 if (messages
.length
) {
395 messages
.assumeSafeAppend
;
399 data
.assumeSafeAppend
;
402 if (fname
.length
== 0) return;
405 auto fi
= VFile(fname
);
406 if (fi
.size
> int.max
/8) assert(0, "log file too big");
407 data
.length
= cast(int)fi
.size
;
408 fi
.rawReadExact(data
);
409 } catch (Exception e
) {}
414 // `data` should be set
415 private void parseData () nothrow {
416 import core
.stdc
.string
: memchr
;
418 assert(messages
.length
== 0); // sorry
419 if (data
.length
== 0) return;
425 while (dp
< data
.ptr
+data
.length
) {
426 auto nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
427 if (nx
is null) break;
432 messages
.reserve(nlcount
+1);
434 static int toInt (const(char)[] s
) nothrow @trusted @nogc {
436 while (s
.length
&& s
.ptr
[0].isdigit
) {
437 res
= res
*10+s
.ptr
[0]-'0';
444 auto dp
= data
.ptr
, nx
= data
.ptr
+data
.length
;
445 for (; dp
< data
.ptr
+data
.length
; dp
= nx
+1) {
446 nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
447 if (nx
is null) nx
= data
.ptr
+data
.length
;
448 // 25 is the minimal message length
449 if (nx
-dp
< 25) continue;
450 if (dp
[1] != '[' || dp
[6] != '/' || dp
[9] != '/' || dp
[12] != ' ' || dp
[15] != ':' || dp
[18] != ':' || dp
[21] != ']' || dp
[22] != ':') continue;
451 foreach (immutable cidx
, immutable char ch
; dp
[0..21]) {
452 if (cidx
< 2 || cidx
== 6 || cidx
== 9 || cidx
== 12 || cidx
== 15 || cidx
== 18) continue;
453 if (!ch
.isdigit
) continue;
458 case '!': msg
.kind
= msg
.Kind
.Notification
; break;
459 case '<': msg
.kind
= msg
.Kind
.Outgoing
; break;
460 case '>': msg
.kind
= msg
.Kind
.Incoming
; break;
465 case ' ': msg
.isMe
= false; break;
466 case '!': msg
.isMe
= true; break;
469 int year
= toInt(dp
[2..6]);
470 if (year
< 2018 || year
> 2036) continue; //FIXME in 2036! ;-)
471 int month
= toInt(dp
[7..9]);
472 if (month
< 1 || month
> 12) continue;
473 int day
= toInt(dp
[10..12]);
474 if (day
< 1 || day
> 31) continue;
475 int hour
= toInt(dp
[13..15]);
476 if (hour
< 0 || hour
> 23) continue;
477 int minute
= toInt(dp
[16..18]);
478 if (minute
< 0 || minute
> 59) continue;
479 int second
= toInt(dp
[19..21]);
480 if (second
< 0 || second
> 59) continue;
483 msg
.time
= SysTime(DateTime(year
, month
, day
, hour
, minute
, second
));
485 } catch (Exception e
) {}
486 if (!timeok
) continue;
487 msg
.rawText
= dp
[24..(nx
-dp
)];
493 // append line to log file; text is in utf8
494 // WARNING! does no validity checks!
495 // WARNING! not thread-safe!
496 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
, SysTime time
) nothrow {
497 import core
.sys
.posix
.fcntl
: open
, O_WRONLY
, O_CREAT
, O_APPEND
;
498 import core
.sys
.posix
.unistd
: close
, write
, fdatasync
;
499 import std
.internal
.cstring
;
501 if (fname
.length
== 0) return false;
503 int fo
= open(fname
.tempCString
, O_WRONLY|O_APPEND|O_CREAT
, 0o600
);
505 import core
.stdc
.errno
;
507 conwriteln("failed to create file: '", fname
, "' (", err
, ")");
510 scope(exit
) { fdatasync(fo
); close(fo
); }
512 bool xwrite (const(char)[] s
...) nothrow @nogc {
513 while (s
.length
> 0) {
514 auto wr
= write(fo
, s
.ptr
, s
.length
);
515 if (wr
== 0) return false; // out of disk space (and the log is probably corrupted at this point)
517 import core
.stdc
.errno
;
518 if (errno
!= EINTR
) return false;
526 bool wrnum (int n
, int wdt
) nothrow @nogc {
527 import core
.stdc
.stdio
: snprintf
;
528 if (n
< 0) return false; // alas
529 char[128] buf
= void;
530 auto len
= (wdt
> 0 ?
snprintf(buf
.ptr
, buf
.length
, "%0*d", wdt
, n
) : snprintf(buf
.ptr
, buf
.length
, "%d", n
));
531 if (len
< 1) return false;
532 return xwrite(buf
[0..len
]);
535 final switch (kind
) {
536 case LogFile
.Msg
.Kind
.Notification
: if (!xwrite("!")) return false; break;
537 case LogFile
.Msg
.Kind
.Incoming
: if (!xwrite(">")) return false; break;
538 case LogFile
.Msg
.Kind
.Outgoing
: if (!xwrite("<")) return false; break;
542 auto dt = cast(DateTime
)time
;
543 if (!xwrite("[")) return false;
544 if (!wrnum(dt.year
, 4)) return false;
545 if (!xwrite("/")) return false;
546 if (!wrnum(cast(int)dt.month
, 2)) return false;
547 if (!xwrite("/")) return false;
548 if (!wrnum(dt.day
, 2)) return false;
549 if (!xwrite(" ")) return false;
550 if (!wrnum(dt.hour
, 2)) return false;
551 if (!xwrite(":")) return false;
552 if (!wrnum(dt.minute
, 2)) return false;
553 if (!xwrite(":")) return false;
554 if (!wrnum(dt.second
, 2)) return false;
555 if (!xwrite("]:")) return false;
556 if (!xwrite(isMe ?
"!" : " ")) return false;
558 // write encoded string
559 while (text
.length
) {
561 while (end
< text
.length
) {
562 char ch
= text
.ptr
[end
];
563 if (ch
< ' ' || ch
== 92 || ch
== 127) break;
567 if (!xwrite(text
[0..end
])) return false;
570 switch (text
.ptr
[0]) {
571 case '\e': if (!xwrite(`\e`)) return false; break;
572 case '\n': if (!xwrite(`\n`)) return false; break;
573 case '\r': if (!xwrite(`\r`)) return false; break;
574 case '\t': if (!xwrite(`\t`)) return false; break;
576 import core
.stdc
.stdio
: snprintf
;
578 auto len
= snprintf(buf
.ptr
, buf
.length
, "\\x%02x", cast(uint)text
.ptr
[0]);
579 if (!xwrite(buf
[0..len
])) return false;
589 // append line to log file; text is in utf8
590 // WARNING! does no validity checks!
591 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
=false) nothrow {
593 return appendLine(fname
, kind
, text
, isMe
, systimeNow
);
594 } catch (Exception e
) {}