command console is visible when activated
[bioacid.git] / accdb.d
blobda24b1f6bb42c726b06665fcbda68d46b897bc75
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 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
52 struct ProtoOptions {
53 bool ipv6;
54 bool udp;
55 bool localDiscovery;
56 bool holePunching;
57 ushort startPort;
58 ushort endPort;
59 ushort tcpPort;
60 ubyte proxyType;
61 ushort proxyPort;
62 string proxyAddr;
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
80 struct GroupOptions {
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)
96 struct ContactInfo {
97 enum Kind {
98 // normal contact
99 Friend,
100 // pending authorization acceptance
101 // statusmsg: request text
102 // lastonlinetime: request time
103 PengingAuthAccept,
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)
108 PengingAuthRequest,
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
111 KillFuckDie,
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
121 CommonOptions opts;
125 // ////////////////////////////////////////////////////////////////////////// //
126 void syncFile(bool fullsync=false) (Imp!"core.stdc.stdio".FILE* fl) nothrow @nogc {
127 if (fl !is null) {
128 import core.stdc.stdio : fileno;
129 int fd = fileno(fl);
130 if (fd >= 0) {
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;
149 try {
150 v.txtser(VFile(fo, own:false), skipstname:true);
151 } catch (Exception e) {
152 fclose(fo);
153 unlink(tmpfname.tempCString);
154 return false;
156 syncFile(fo);
157 fclose(fo);
159 if (rename(tmpfname.tempCString, fname.tempCString) != 0) {
160 unlink(tmpfname.tempCString);
161 return false;
164 return true;
168 // ////////////////////////////////////////////////////////////////////////// //
170 log format (it looks like text, but it isn't text):
171 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
172 date in local time.
173 text: chars in range of [0..31, 92, 127] are \xHH-encoded
174 T is message type:
175 !: server/app notification
176 <: outgoing
177 >: incoming
178 if char before text is '!' instead of space, this is "/me" message
180 static struct LogFile {
181 public:
182 // this struct must not outlive LogFile
183 static struct Msg {
184 enum Kind { Notification, Incoming, Outgoing }
185 Kind kind;
186 bool isMe; // "/me" message?
187 SysTime time;
188 char[] rawText; // undecoded
190 // get decoded text
191 string text () const {
192 import iv.utfutil : utf8Encode;
193 char[] text;
194 text.reserve(rawText.length);
195 // decode text
196 foreach (dchar dc; byDChar) {
197 char[4] buf = void;
198 auto len = utf8Encode(buf[], dc);
199 text ~= buf[0..len];
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
209 usize pos;
210 @property bool empty () const => (pos >= raw.length);
211 @property dchar front () const {
212 if (pos < raw.length) {
213 char ch = raw.ptr[pos];
214 if (ch <= 127) {
215 if (ch != '\\') return ch;
216 if (raw.length-pos < 2) return ch;
217 ch = raw.ptr[pos+1];
218 if (ch != 'x' && ch != 'X') {
219 switch (ch) {
220 case 'e': return '\e';
221 case 'n': return '\n';
222 case 'r': return '\r';
223 case 't': return '\t';
224 default:
226 return ch;
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);
235 Utf8DecoderFast dc;
236 uint cpos = pos;
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);
241 } else {
242 return Utf8DecoderFast.replacement;
245 void popFront () {
246 if (pos < raw.length) {
247 char ch = raw.ptr[pos];
248 if (ch <= 127) {
249 ++pos;
250 if (ch != '\\') return;
251 if (raw.length-pos < 1) { pos = raw.length; return; }
252 ch = raw.ptr[pos++];
253 if (ch != 'x' && ch != 'X') return;
254 if (raw.length-pos < 2) { pos = raw.length; return; }
255 pos += 2;
256 return;
258 Utf8DecoderFast dc;
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);
270 private:
271 char[] data;
273 public:
274 Msg[] messages;
276 public:
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 {
284 delete messages;
285 delete data;
288 void load (const(char)[] fname) nothrow {
289 import std.internal.cstring;
291 if (messages.length) {
292 messages.length = 0;
293 messages.assumeSafeAppend;
295 if (data.length) {
296 data.length = 0;
297 data.assumeSafeAppend;
300 if (fname.length == 0) return;
302 try {
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) {}
309 parseData();
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;
319 // count '\n'
320 int nlcount = 0;
322 auto dp = data.ptr;
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;
326 ++nlcount;
327 dp = nx+1;
330 messages.reserve(nlcount+1);
332 static int toInt (const(char)[] s) nothrow @trusted @nogc {
333 int res = 0;
334 while (s.length && s.ptr[0].isdigit) {
335 res = res*10+s.ptr[0]-'0';
336 s = s[1..$];
338 return res;
341 // parse messages
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;
353 Msg msg;
354 // message kind
355 switch (dp[0]) {
356 case '!': msg.kind = msg.Kind.Notification; break;
357 case '<': msg.kind = msg.Kind.Outgoing; break;
358 case '>': msg.kind = msg.Kind.Incoming; break;
359 default: continue;
361 // "/me"?
362 switch (dp[23]) {
363 case ' ': msg.isMe = false; break;
364 case '!': msg.isMe = true; break;
365 default: continue;
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;
379 bool timeok = false;
380 try {
381 msg.time = SysTime(DateTime(year, month, day, hour, minute, second));
382 timeok = true;
383 } catch (Exception e) {}
384 if (!timeok) continue;
385 msg.rawText = dp[24..(nx-dp)];
386 messages ~= msg;
390 public:
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);
406 if (wr == 0) {
407 import core.stdc.errno;
408 if (errno != EINTR) return false;
409 continue;
411 s = s[wr..$];
413 return true;
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;
431 // write date
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) {
450 usize end = 0;
451 while (end < text.length) {
452 char ch = text.ptr[end];
453 if (ch < ' ' || ch == 92 || ch == 127) break;
454 ++end;
456 if (end > 0) {
457 if (!xwrite(text[0..end])) return false;
458 text = text[end..$];
459 } else {
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;
465 default:
466 import core.stdc.stdio : snprintf;
467 char[16] buf = void;
468 auto len = snprintf(buf.ptr, buf.length, "\\x%02x", cast(uint)text.ptr[0]);
469 if (!xwrite(buf[0..len])) return false;
470 break;
472 text = text[1..$];
476 return xwrite("\n");
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 {
482 try {
483 return appendLine(fname, kind, text, isMe, Clock.currTime);
484 } catch (Exception e) {}
485 return false;