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 public alias TextDigest
= ubyte[20]; // ripemd160
34 public TextDigest
textDigest (const(void)[] buf
) nothrow @trusted @nogc {
35 import std
.digest
.ripemd
;
36 return ripemd160Of(buf
);
40 public SysTime
systimeNow () nothrow @trusted {
42 SysTime st
= Clock
.currTime
;
43 return SysTime
.fromUnixTime(st
.toUnixTime
); // trunc to second precision
44 } catch (Exception e
) {}
45 assert(0, "time error!");
49 // ////////////////////////////////////////////////////////////////////////// //
50 enum TriOption
{ Default
= -1, No
= 0, Yes
= 1 }
52 enum ContactStatus
{ Offline
, Online
, Away
, Busy
, Connecting
}
54 alias PubKey
= ubyte[TOX_PUBLIC_KEY_SIZE
];
55 alias ToxAddr
= ubyte[TOX_ADDRESS_SIZE
];
58 // ////////////////////////////////////////////////////////////////////////// //
59 struct CommonOptions
{
60 TriOption showOffline
= TriOption
.Default
; // show this contact even if it is offline
61 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this contact
62 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity for this contact
63 TriOption skipUnread
= TriOption
.Default
; // skip this contacts in `next_unread` command
64 TriOption ftranAllowed
= TriOption
.Default
; // file transfers allowed for this contact
65 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
66 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
67 //@SRZNonDefaultOnly TriOption confAutoJoin; // automatically join the conference when we're going online
85 struct AccountConfig
{
86 string nick
; // my nick
87 string statusmsg
; // my status message
88 bool showOffline
; // show offline persons?
89 bool showPopup
; // show popups for messages?
90 bool blinkActivity
; // blink tray icon if we have some activity (unread msg, transfer request, etc.)?
91 bool skipUnread
; // skip contacts in `next_unread` command?
92 bool hideEmptyGroups
; // hide empty groups? (can be overriden by `hideNoVisible` group option)
93 bool ftranAllowed
; // file transfers allowed for this group (-1: use default value)
94 int resendRotDays
; // how many days message should be in "resend queue" if contact is offline
95 int hmcOnOpen
; // how many history messages we should show when we opening a chat with a contact
100 uint gid
; // group id; there is always group with gid 0, it is "common" default group
101 string name
; // group name
102 string note
; // group notes/description
103 bool opened
; // is this group opened?
104 TriOption showOffline
= TriOption
.Default
; // show offline persons in this group
105 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this group
106 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity (unread msg, transfer request, etc.) for this group
107 TriOption skipUnread
= TriOption
.Default
; // skip contacts from this group in `next_unread` command
108 TriOption hideIfNoVisibleMembers
= TriOption
.Default
; // hide this group if there are no visible items in it
109 TriOption ftranAllowed
; // file transfers allowed for this group
110 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
111 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
119 // pending authorization acceptance
120 // statusmsg: request text
121 // lastonlinetime: request time
123 // auth requested, awaiting acceptance
124 // statusmsg: request text
125 // lastonlinetime: request time
126 // this will change to `Friend` when contact gets online (it means that auth is accepted)
128 // this contact was deleted and put in "total ignore" mode
129 // i.e. any activity from this contact (especially auth requests) will be silently dropped on the floor
132 uint gid
; // group id (see groups.rc)
133 string nick
; // empty: unauthorized
134 @SRZNonDefaultOnly string visnick
; // empty: use `nick`
135 @SRZNonDefaultOnly string statusmsg
; // request message for pending auth request
136 @SRZNonDefaultOnly uint lastonlinetime
; // local unixtime; changed when contact status changed between offline and online (any kind of online)
137 Kind kind
= Kind
.Friend
;
138 string note
; // contact notes/description
139 PubKey pubkey
; // used as unique contact id, same as directory name
140 uint nospam
; // used only in `toxCoreLoadDataFile()`
141 @SRZNonDefaultOnly ToxAddr fraddr
; // as address includes nospam, save it too
146 // ////////////////////////////////////////////////////////////////////////// //
147 private void syncFile(bool fullsync
=false) (Imp
!"core.stdc.stdio".FILE
* fl
) nothrow @nogc {
149 import core
.stdc
.stdio
: fileno
;
152 import core
.sys
.posix
.unistd
: fsync
, fdatasync
;
153 static if (fullsync
) fsync(fd
); else fdatasync(fd
);
159 // ////////////////////////////////////////////////////////////////////////// //
160 public void mkdirRec (const(char)[] path
) nothrow {
161 import core
.stdc
.stdlib
: malloc
, free
;
162 import core
.sys
.posix
.sys
.stat
: mkdir
;
164 if (path
.length
== 0) return;
166 char* tpath
= cast(char*)malloc(path
.length
+1024);
167 if (tpath
is null) assert(0, "out of memory");
168 scope(exit
) free(tpath
);
171 if (path
[0] == '/') tpath
[tpos
++] = '/';
173 while (path
.length
> 0) {
174 if (path
[0] == '/') { path
= path
[1..$]; continue; }
175 while (path
.length
&& path
[0] != '/') {
176 tpath
[tpos
++] = path
[0];
186 // ////////////////////////////////////////////////////////////////////////// //
187 // write to temporary file, then atomically replace
188 public bool serialize(T
) (in auto ref T v
, string fname
) nothrow {
189 import core
.stdc
.stdlib
: realloc
, free
;
190 import core
.stdc
.stdio
: rename
;
191 import core
.sys
.posix
.fcntl
: open
, O_WRONLY
, O_CREAT
, O_TRUNC
;
192 import core
.sys
.posix
.unistd
: close
, write
, fdatasync
, unlink
;
193 import std
.internal
.cstring
;
195 static struct OutRng
{
201 this (int afd
) nothrow @trusted @nogc { fd
= afd
; }
202 ~this () { flush(); }
203 @disable this (this);
204 void opAssign() (in auto ref OutRng a
) { static assert(0, "no assigns!"); }
205 void flush () nothrow @trusted @nogc {
206 if (waserror
) { bufused
= 0; return; }
208 while (bufofs
< bufused
) {
209 auto wr
= write(fd
, buf
.ptr
+bufofs
, bufused
-bufofs
);
210 if (wr
== 0) { waserror
= true; bufused
= 0; return; }
212 import core
.stdc
.errno
;
213 if (errno
!= EINTR
) { waserror
= true; bufused
= 0; return; }
216 bufofs
+= cast(uint)wr
;
220 void put (char ch
) nothrow @trusted @nogc {
221 if (waserror
) return;
222 assert(bufused
< buf
.sizeof
);
223 buf
.ptr
[bufused
++] = ch
;
224 if (bufused
== buf
.sizeof
) flush();
228 if (fname
.length
== 0 || fname
.length
> 65536) return false;
230 static char* tmpfname
= null;
232 if (tmpfnsize
< fname
.length
+8) {
233 tmpfnsize
= cast(uint)fname
.length
+8;
234 tmpfname
= cast(char*)realloc(tmpfname
, tmpfnsize
);
235 if (tmpfname
is null) assert(0, "out of memory");
237 tmpfname
[0..fname
.length
] = fname
[];
238 tmpfname
[fname
.length
..fname
.length
+5] = ".$$$\x00";
239 int fo
= open(tmpfname
, O_WRONLY|O_CREAT|O_TRUNC
, 0o600
);
241 conwriteln("failed to create file: '", fname
, ".$$$'");
245 bool waserror
= false;
247 auto or = OutRng(fo
);
248 v
.txtser(or, skipstname
:true);
250 waserror
= or.waserror
;
251 } catch (Exception e
) {
255 if (!waserror
) fdatasync(fo
);
262 if (rename(tmpfname
, fname
.tempCString
) != 0) {
271 // ////////////////////////////////////////////////////////////////////////// //
273 log format (it looks like text, but it isn't text):
274 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
276 text: chars in range of [0..31, 92, 127] are \xHH-encoded
278 !: server/app notification
281 if char before text is '!' instead of space, this is "/me" message
283 static struct LogFile
{
285 // this struct must not outlive LogFile
287 enum Kind
{ Notification
, Incoming
, Outgoing
}
289 bool isMe
; // "/me" message?
291 char[] rawText
; // undecoded
294 string
text () const {
295 import iv
.utfutil
: utf8Encode
;
297 text
.reserve(rawText
.length
);
299 foreach (dchar dc
; byDChar
) {
301 auto len
= utf8Encode(buf
[], dc
);
304 return cast(string
)text
; // it is safe to cast here
307 // decode raw text to dchars; returns forward range
308 auto byDChar () const nothrow @safe @nogc {
309 static struct Range
{
310 pure nothrow @trusted @nogc:
311 const(char)[] raw
; // anchored
313 @property bool empty () const => (pos
>= raw
.length
);
314 @property dchar front () const {
315 if (pos
< raw
.length
) {
316 char ch
= raw
.ptr
[pos
];
318 if (ch
!= '\\') return ch
;
319 if (raw
.length
-pos
< 2) return ch
;
321 if (ch
!= 'x' && ch
!= 'X') {
323 case 'e': return '\e';
324 case 'n': return '\n';
325 case 'r': return '\r';
326 case 't': return '\t';
331 if (raw
.length
-pos
< 4) return '?';
332 int n0
= raw
.ptr
[pos
+2].digitInBase(16);
333 if (n0
< 0) return '?';
334 int n1
= raw
.ptr
[pos
+3].digitInBase(16);
335 if (n1
< 0) return '?';
336 return cast(char)(n0
*16+n1
);
340 while (!dc
.decode(cast(ubyte)raw
.ptr
[cpos
++])) {
341 if (cpos
>= raw
.length
) break; // no more chars
343 return (dc
.complete ? dc
.codepoint
: dc
.replacement
);
345 return Utf8DecoderFast
.replacement
;
349 if (pos
< raw
.length
) {
350 char ch
= raw
.ptr
[pos
];
353 if (ch
!= '\\') return;
354 if (raw
.length
-pos
< 1) { pos
= raw
.length
; return; }
356 if (ch
!= 'x' && ch
!= 'X') return;
357 if (raw
.length
-pos
< 2) { pos
= raw
.length
; return; }
362 while (!dc
.decode(cast(ubyte)raw
.ptr
[pos
++])) {
363 if (pos
>= raw
.length
) break; // no more chars
367 Range
save () const => Range(raw
, pos
);
369 return Range(rawText
, 0);
380 @disable this (this); // no copies
381 void opAssign() (in auto ref LogFile lf
) { static assert(0, "no copies!"); }
383 this (const(char)[] fname
) nothrow => load(fname
);
385 // no `Msg` should outlive this call!
386 void clear () nothrow {
391 void load (const(char)[] fname
) nothrow {
392 import std
.internal
.cstring
;
394 if (messages
.length
) {
396 messages
.assumeSafeAppend
;
400 data
.assumeSafeAppend
;
403 if (fname
.length
== 0) return;
406 auto fi
= VFile(fname
);
407 if (fi
.size
> int.max
/8) assert(0, "log file too big");
408 data
.length
= cast(int)fi
.size
;
409 fi
.rawReadExact(data
);
410 } catch (Exception e
) {}
415 // `data` should be set
416 private void parseData () nothrow {
417 import core
.stdc
.string
: memchr
;
419 assert(messages
.length
== 0); // sorry
420 if (data
.length
== 0) return;
426 while (dp
< data
.ptr
+data
.length
) {
427 auto nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
428 if (nx
is null) break;
433 messages
.reserve(nlcount
+1);
435 static int toInt (const(char)[] s
) nothrow @trusted @nogc {
437 while (s
.length
&& s
.ptr
[0].isdigit
) {
438 res
= res
*10+s
.ptr
[0]-'0';
445 auto dp
= data
.ptr
, nx
= data
.ptr
+data
.length
;
446 for (; dp
< data
.ptr
+data
.length
; dp
= nx
+1) {
447 nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
448 if (nx
is null) nx
= data
.ptr
+data
.length
;
449 // 25 is the minimal message length
450 if (nx
-dp
< 25) continue;
451 if (dp
[1] != '[' || dp
[6] != '/' || dp
[9] != '/' || dp
[12] != ' ' || dp
[15] != ':' || dp
[18] != ':' || dp
[21] != ']' || dp
[22] != ':') continue;
452 foreach (immutable cidx
, immutable char ch
; dp
[0..21]) {
453 if (cidx
< 2 || cidx
== 6 || cidx
== 9 || cidx
== 12 || cidx
== 15 || cidx
== 18) continue;
454 if (!ch
.isdigit
) continue;
459 case '!': msg
.kind
= msg
.Kind
.Notification
; break;
460 case '<': msg
.kind
= msg
.Kind
.Outgoing
; break;
461 case '>': msg
.kind
= msg
.Kind
.Incoming
; break;
466 case ' ': msg
.isMe
= false; break;
467 case '!': msg
.isMe
= true; break;
470 int year
= toInt(dp
[2..6]);
471 if (year
< 2018 || year
> 2036) continue; //FIXME in 2036! ;-)
472 int month
= toInt(dp
[7..9]);
473 if (month
< 1 || month
> 12) continue;
474 int day
= toInt(dp
[10..12]);
475 if (day
< 1 || day
> 31) continue;
476 int hour
= toInt(dp
[13..15]);
477 if (hour
< 0 || hour
> 23) continue;
478 int minute
= toInt(dp
[16..18]);
479 if (minute
< 0 || minute
> 59) continue;
480 int second
= toInt(dp
[19..21]);
481 if (second
< 0 || second
> 59) continue;
484 msg
.time
= SysTime(DateTime(year
, month
, day
, hour
, minute
, second
));
486 } catch (Exception e
) {}
487 if (!timeok
) continue;
488 msg
.rawText
= dp
[24..(nx
-dp
)];
494 // append line to log file; text is in utf8
495 // WARNING! does no validity checks!
496 // WARNING! not thread-safe!
497 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
, SysTime time
) nothrow {
498 import core
.sys
.posix
.fcntl
: open
, O_WRONLY
, O_CREAT
, O_APPEND
;
499 import core
.sys
.posix
.unistd
: close
, write
, fdatasync
;
500 import std
.internal
.cstring
;
502 if (fname
.length
== 0) return false;
504 int fo
= open(fname
.tempCString
, O_WRONLY|O_APPEND|O_CREAT
, 0o600
);
506 import core
.stdc
.errno
;
508 conwriteln("failed to create file: '", fname
, "' (", err
, ")");
511 scope(exit
) { fdatasync(fo
); close(fo
); }
513 bool xwrite (const(char)[] s
...) nothrow @nogc {
514 while (s
.length
> 0) {
515 auto wr
= write(fo
, s
.ptr
, s
.length
);
516 if (wr
== 0) return false; // out of disk space (and the log is probably corrupted at this point)
518 import core
.stdc
.errno
;
519 if (errno
!= EINTR
) return false;
527 bool wrnum (int n
, int wdt
) nothrow @nogc {
528 import core
.stdc
.stdio
: snprintf
;
529 if (n
< 0) return false; // alas
530 char[128] buf
= void;
531 auto len
= (wdt
> 0 ?
snprintf(buf
.ptr
, buf
.length
, "%0*d", wdt
, n
) : snprintf(buf
.ptr
, buf
.length
, "%d", n
));
532 if (len
< 1) return false;
533 return xwrite(buf
[0..len
]);
536 final switch (kind
) {
537 case LogFile
.Msg
.Kind
.Notification
: if (!xwrite("!")) return false; break;
538 case LogFile
.Msg
.Kind
.Incoming
: if (!xwrite(">")) return false; break;
539 case LogFile
.Msg
.Kind
.Outgoing
: if (!xwrite("<")) return false; break;
543 auto dt = cast(DateTime
)time
;
544 if (!xwrite("[")) return false;
545 if (!wrnum(dt.year
, 4)) return false;
546 if (!xwrite("/")) return false;
547 if (!wrnum(cast(int)dt.month
, 2)) return false;
548 if (!xwrite("/")) return false;
549 if (!wrnum(dt.day
, 2)) return false;
550 if (!xwrite(" ")) return false;
551 if (!wrnum(dt.hour
, 2)) return false;
552 if (!xwrite(":")) return false;
553 if (!wrnum(dt.minute
, 2)) return false;
554 if (!xwrite(":")) return false;
555 if (!wrnum(dt.second
, 2)) return false;
556 if (!xwrite("]:")) return false;
557 if (!xwrite(isMe ?
"!" : " ")) return false;
559 // write encoded string
560 while (text
.length
) {
562 while (end
< text
.length
) {
563 char ch
= text
.ptr
[end
];
564 if (ch
< ' ' || ch
== 92 || ch
== 127) break;
568 if (!xwrite(text
[0..end
])) return false;
571 switch (text
.ptr
[0]) {
572 case '\e': if (!xwrite(`\e`)) return false; break;
573 case '\n': if (!xwrite(`\n`)) return false; break;
574 case '\r': if (!xwrite(`\r`)) return false; break;
575 case '\t': if (!xwrite(`\t`)) return false; break;
577 import core
.stdc
.stdio
: snprintf
;
579 auto len
= snprintf(buf
.ptr
, buf
.length
, "\\x%02x", cast(uint)text
.ptr
[0]);
580 if (!xwrite(buf
[0..len
])) return false;
590 // append line to log file; text is in utf8
591 // WARNING! does no validity checks!
592 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
=false) nothrow {
594 return appendLine(fname
, kind
, text
, isMe
, systimeNow
);
595 } catch (Exception e
) {}