...and popup title
[bioacid.git] / accdb.d
blobb1c3624c8adf34096358e57e8cdfc268e9b1c0e7
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;
19 import std.datetime;
21 import iv.cmdcon;
22 import iv.cmdcon.gl;
23 import iv.strex;
24 import iv.tox;
25 import iv.txtser;
26 import iv.unarray;
27 import iv.utfutil;
28 import iv.vfs.io;
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 {
41 try {
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
71 struct ProtoOptions {
72 bool ipv6;
73 bool udp;
74 bool localDiscovery;
75 bool holePunching;
76 ushort startPort;
77 ushort endPort;
78 ushort tcpPort;
79 ubyte proxyType;
80 ushort proxyPort;
81 string proxyAddr;
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
99 struct GroupOptions {
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)
115 struct ContactInfo {
116 enum Kind {
117 // normal contact
118 Friend,
119 // pending authorization acceptance
120 // statusmsg: request text
121 // lastonlinetime: request time
122 PengingAuthAccept,
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)
127 PengingAuthRequest,
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
130 KillFuckDie,
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
142 CommonOptions opts;
146 // ////////////////////////////////////////////////////////////////////////// //
147 private void syncFile(bool fullsync=false) (Imp!"core.stdc.stdio".FILE* fl) nothrow @nogc {
148 if (fl !is null) {
149 import core.stdc.stdio : fileno;
150 int fd = fileno(fl);
151 if (fd >= 0) {
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);
169 uint tpos = 0;
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];
177 path = path[1..$];
179 tpath[tpos] = 0;
180 mkdir(tpath, 0o700);
181 tpath[tpos++] = '/';
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 {
196 int fd;
197 ubyte[256] buf;
198 uint bufused;
199 bool waserror;
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; }
207 uint bufofs = 0;
208 while (bufofs < bufused) {
209 auto wr = write(fd, buf.ptr+bufofs, bufused-bufofs);
210 if (wr == 0) { waserror = true; bufused = 0; return; }
211 if (wr < 0) {
212 import core.stdc.errno;
213 if (errno != EINTR) { waserror = true; bufused = 0; return; }
214 continue;
216 bufofs += cast(uint)wr;
218 bufused = 0;
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;
231 uint tmpfnsize = 0;
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);
240 if (fo == -1) {
241 conwriteln("failed to create file: '", fname, ".$$$'");
242 return false;
245 bool waserror = false;
246 try {
247 auto or = OutRng(fo);
248 v.txtser(or, skipstname:true);
249 or.flush();
250 waserror = or.waserror;
251 } catch (Exception e) {
252 waserror = true;
255 if (!waserror) fdatasync(fo);
256 close(fo);
257 if (waserror) {
258 unlink(tmpfname);
259 return false;
262 if (rename(tmpfname, fname.tempCString) != 0) {
263 unlink(tmpfname);
264 return false;
267 return true;
271 // ////////////////////////////////////////////////////////////////////////// //
273 log format (it looks like text, but it isn't text):
274 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
275 date in local time.
276 text: chars in range of [0..31, 92, 127] are \xHH-encoded
277 T is message type:
278 !: server/app notification
279 <: outgoing
280 >: incoming
281 if char before text is '!' instead of space, this is "/me" message
283 static struct LogFile {
284 public:
285 // this struct must not outlive LogFile
286 static struct Msg {
287 enum Kind { Notification, Incoming, Outgoing }
288 Kind kind;
289 bool isMe; // "/me" message?
290 SysTime time;
291 char[] rawText; // undecoded
293 // get decoded text
294 string text () const {
295 import iv.utfutil : utf8Encode;
296 char[] text;
297 text.reserve(rawText.length);
298 // decode text
299 foreach (dchar dc; byDChar) {
300 char[4] buf = void;
301 auto len = utf8Encode(buf[], dc);
302 text ~= buf[0..len];
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
312 usize pos;
313 @property bool empty () const => (pos >= raw.length);
314 @property dchar front () const {
315 if (pos < raw.length) {
316 char ch = raw.ptr[pos];
317 if (ch <= 127) {
318 if (ch != '\\') return ch;
319 if (raw.length-pos < 2) return ch;
320 ch = raw.ptr[pos+1];
321 if (ch != 'x' && ch != 'X') {
322 switch (ch) {
323 case 'e': return '\e';
324 case 'n': return '\n';
325 case 'r': return '\r';
326 case 't': return '\t';
327 default:
329 return ch;
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);
338 Utf8DecoderFast dc;
339 uint cpos = pos;
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);
344 } else {
345 return Utf8DecoderFast.replacement;
348 void popFront () {
349 if (pos < raw.length) {
350 char ch = raw.ptr[pos];
351 if (ch <= 127) {
352 ++pos;
353 if (ch != '\\') return;
354 if (raw.length-pos < 1) { pos = raw.length; return; }
355 ch = raw.ptr[pos++];
356 if (ch != 'x' && ch != 'X') return;
357 if (raw.length-pos < 2) { pos = raw.length; return; }
358 pos += 2;
359 return;
361 Utf8DecoderFast dc;
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);
373 private:
374 char[] data;
376 public:
377 Msg[] messages;
379 public:
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 {
387 delete messages;
388 delete data;
391 void load (const(char)[] fname) nothrow {
392 import std.internal.cstring;
394 if (messages.length) {
395 messages.length = 0;
396 messages.assumeSafeAppend;
398 if (data.length) {
399 data.length = 0;
400 data.assumeSafeAppend;
403 if (fname.length == 0) return;
405 try {
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) {}
412 parseData();
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;
422 // count '\n'
423 int nlcount = 0;
425 auto dp = data.ptr;
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;
429 ++nlcount;
430 dp = nx+1;
433 messages.reserve(nlcount+1);
435 static int toInt (const(char)[] s) nothrow @trusted @nogc {
436 int res = 0;
437 while (s.length && s.ptr[0].isdigit) {
438 res = res*10+s.ptr[0]-'0';
439 s = s[1..$];
441 return res;
444 // parse messages
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;
456 Msg msg;
457 // message kind
458 switch (dp[0]) {
459 case '!': msg.kind = msg.Kind.Notification; break;
460 case '<': msg.kind = msg.Kind.Outgoing; break;
461 case '>': msg.kind = msg.Kind.Incoming; break;
462 default: continue;
464 // "/me"?
465 switch (dp[23]) {
466 case ' ': msg.isMe = false; break;
467 case '!': msg.isMe = true; break;
468 default: continue;
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;
482 bool timeok = false;
483 try {
484 msg.time = SysTime(DateTime(year, month, day, hour, minute, second));
485 timeok = true;
486 } catch (Exception e) {}
487 if (!timeok) continue;
488 msg.rawText = dp[24..(nx-dp)];
489 messages ~= msg;
493 public:
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);
505 if (fo == -1) {
506 import core.stdc.errno;
507 auto err = errno;
508 conwriteln("failed to create file: '", fname, "' (", err, ")");
509 return false;
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)
517 if (wr < 0) {
518 import core.stdc.errno;
519 if (errno != EINTR) return false;
520 continue;
522 s = s[wr..$];
524 return true;
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;
542 // write date
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) {
561 usize end = 0;
562 while (end < text.length) {
563 char ch = text.ptr[end];
564 if (ch < ' ' || ch == 92 || ch == 127) break;
565 ++end;
567 if (end > 0) {
568 if (!xwrite(text[0..end])) return false;
569 text = text[end..$];
570 } else {
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;
576 default:
577 import core.stdc.stdio : snprintf;
578 char[16] buf = void;
579 auto len = snprintf(buf.ptr, buf.length, "\\x%02x", cast(uint)text.ptr[0]);
580 if (!xwrite(buf[0..len])) return false;
581 break;
583 text = text[1..$];
587 return xwrite("\n");
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 {
593 try {
594 return appendLine(fname, kind, text, isMe, systimeNow);
595 } catch (Exception e) {}
596 return false;