prevent popup flooding
[bioacid.git] / accdb.d
blob6caee1ca01ab370c92828c90fd80a83fc2c67bf3
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;
18 import std.datetime;
20 import iv.cmdcon;
21 import iv.cmdcon.gl;
22 import iv.strex;
23 import iv.tox;
24 import iv.txtser;
25 import iv.unarray;
26 import iv.utfutil;
27 import iv.vfs.io;
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 {
40 try {
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
70 struct ProtoOptions {
71 bool ipv6;
72 bool udp;
73 bool localDiscovery;
74 bool holePunching;
75 ushort startPort;
76 ushort endPort;
77 ushort tcpPort;
78 ubyte proxyType;
79 ushort proxyPort;
80 string proxyAddr;
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
98 struct GroupOptions {
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)
114 struct ContactInfo {
115 enum Kind {
116 // normal contact
117 Friend,
118 // pending authorization acceptance
119 // statusmsg: request text
120 // lastonlinetime: request time
121 PengingAuthAccept,
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)
126 PengingAuthRequest,
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
129 KillFuckDie,
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
141 CommonOptions opts;
145 // ////////////////////////////////////////////////////////////////////////// //
146 private void syncFile(bool fullsync=false) (Imp!"core.stdc.stdio".FILE* fl) nothrow @nogc {
147 if (fl !is null) {
148 import core.stdc.stdio : fileno;
149 int fd = fileno(fl);
150 if (fd >= 0) {
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);
168 uint tpos = 0;
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];
176 path = path[1..$];
178 tpath[tpos] = 0;
179 mkdir(tpath, 0o700);
180 tpath[tpos++] = '/';
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 {
195 int fd;
196 ubyte[256] buf;
197 uint bufused;
198 bool waserror;
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; }
206 uint bufofs = 0;
207 while (bufofs < bufused) {
208 auto wr = write(fd, buf.ptr+bufofs, bufused-bufofs);
209 if (wr == 0) { waserror = true; bufused = 0; return; }
210 if (wr < 0) {
211 import core.stdc.errno;
212 if (errno != EINTR) { waserror = true; bufused = 0; return; }
213 continue;
215 bufofs += cast(uint)wr;
217 bufused = 0;
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;
230 uint tmpfnsize = 0;
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);
239 if (fo == -1) {
240 conwriteln("failed to create file: '", fname, ".$$$'");
241 return false;
244 bool waserror = false;
245 try {
246 auto or = OutRng(fo);
247 v.txtser(or, skipstname:true);
248 or.flush();
249 waserror = or.waserror;
250 } catch (Exception e) {
251 waserror = true;
254 if (!waserror) fdatasync(fo);
255 close(fo);
256 if (waserror) {
257 unlink(tmpfname);
258 return false;
261 if (rename(tmpfname, fname.tempCString) != 0) {
262 unlink(tmpfname);
263 return false;
266 return true;
270 // ////////////////////////////////////////////////////////////////////////// //
272 log format (it looks like text, but it isn't text):
273 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
274 date in local time.
275 text: chars in range of [0..31, 92, 127] are \xHH-encoded
276 T is message type:
277 !: server/app notification
278 <: outgoing
279 >: incoming
280 if char before text is '!' instead of space, this is "/me" message
282 static struct LogFile {
283 public:
284 // this struct must not outlive LogFile
285 static struct Msg {
286 enum Kind { Notification, Incoming, Outgoing }
287 Kind kind;
288 bool isMe; // "/me" message?
289 SysTime time;
290 char[] rawText; // undecoded
292 // get decoded text
293 string text () const {
294 import iv.utfutil : utf8Encode;
295 char[] text;
296 text.reserve(rawText.length);
297 // decode text
298 foreach (dchar dc; byDChar) {
299 char[4] buf = void;
300 auto len = utf8Encode(buf[], dc);
301 text ~= buf[0..len];
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
311 usize pos;
312 @property bool empty () const => (pos >= raw.length);
313 @property dchar front () const {
314 if (pos < raw.length) {
315 char ch = raw.ptr[pos];
316 if (ch <= 127) {
317 if (ch != '\\') return ch;
318 if (raw.length-pos < 2) return ch;
319 ch = raw.ptr[pos+1];
320 if (ch != 'x' && ch != 'X') {
321 switch (ch) {
322 case 'e': return '\e';
323 case 'n': return '\n';
324 case 'r': return '\r';
325 case 't': return '\t';
326 default:
328 return ch;
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);
337 Utf8DecoderFast dc;
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);
343 } else {
344 return Utf8DecoderFast.replacement;
347 void popFront () {
348 if (pos < raw.length) {
349 char ch = raw.ptr[pos];
350 if (ch <= 127) {
351 ++pos;
352 if (ch != '\\') return;
353 if (raw.length-pos < 1) { pos = raw.length; return; }
354 ch = raw.ptr[pos++];
355 if (ch != 'x' && ch != 'X') return;
356 if (raw.length-pos < 2) { pos = raw.length; return; }
357 pos += 2;
358 return;
360 Utf8DecoderFast dc;
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);
372 private:
373 char[] data;
375 public:
376 Msg[] messages;
378 public:
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 {
386 delete messages;
387 delete data;
390 void load (const(char)[] fname) nothrow {
391 import std.internal.cstring;
393 if (messages.length) {
394 messages.length = 0;
395 messages.assumeSafeAppend;
397 if (data.length) {
398 data.length = 0;
399 data.assumeSafeAppend;
402 if (fname.length == 0) return;
404 try {
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) {}
411 parseData();
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;
421 // count '\n'
422 int nlcount = 0;
424 auto dp = data.ptr;
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;
428 ++nlcount;
429 dp = nx+1;
432 messages.reserve(nlcount+1);
434 static int toInt (const(char)[] s) nothrow @trusted @nogc {
435 int res = 0;
436 while (s.length && s.ptr[0].isdigit) {
437 res = res*10+s.ptr[0]-'0';
438 s = s[1..$];
440 return res;
443 // parse messages
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;
455 Msg msg;
456 // message kind
457 switch (dp[0]) {
458 case '!': msg.kind = msg.Kind.Notification; break;
459 case '<': msg.kind = msg.Kind.Outgoing; break;
460 case '>': msg.kind = msg.Kind.Incoming; break;
461 default: continue;
463 // "/me"?
464 switch (dp[23]) {
465 case ' ': msg.isMe = false; break;
466 case '!': msg.isMe = true; break;
467 default: continue;
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;
481 bool timeok = false;
482 try {
483 msg.time = SysTime(DateTime(year, month, day, hour, minute, second));
484 timeok = true;
485 } catch (Exception e) {}
486 if (!timeok) continue;
487 msg.rawText = dp[24..(nx-dp)];
488 messages ~= msg;
492 public:
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);
504 if (fo == -1) {
505 import core.stdc.errno;
506 auto err = errno;
507 conwriteln("failed to create file: '", fname, "' (", err, ")");
508 return false;
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)
516 if (wr < 0) {
517 import core.stdc.errno;
518 if (errno != EINTR) return false;
519 continue;
521 s = s[wr..$];
523 return true;
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;
541 // write date
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) {
560 usize end = 0;
561 while (end < text.length) {
562 char ch = text.ptr[end];
563 if (ch < ' ' || ch == 92 || ch == 127) break;
564 ++end;
566 if (end > 0) {
567 if (!xwrite(text[0..end])) return false;
568 text = text[end..$];
569 } else {
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;
575 default:
576 import core.stdc.stdio : snprintf;
577 char[16] buf = void;
578 auto len = snprintf(buf.ptr, buf.length, "\\x%02x", cast(uint)text.ptr[0]);
579 if (!xwrite(buf[0..len])) return false;
580 break;
582 text = text[1..$];
586 return xwrite("\n");
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 {
592 try {
593 return appendLine(fname, kind, text, isMe, systimeNow);
594 } catch (Exception e) {}
595 return false;