C-3 to set "busy" status
[bioacid.git] / accdb.d
blob6d9e87d8cf5ebc94cc1b5fccc2d337c7f4f7ff06
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 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 // write to temporary file, then atomically replace
161 bool serialize(T) (in auto ref T v, string fname) {
162 import core.stdc.stdio : FILE, fopen, fclose, rename;
163 import core.sys.posix.unistd : unlink;
164 import std.internal.cstring;
166 string tmpfname = fname~".$$$";
167 FILE* fo = fopen(tmpfname.tempCString, "w");
168 if (fo is null) return false;
170 try {
171 v.txtser(VFile(fo, own:false), skipstname:true);
172 } catch (Exception e) {
173 fclose(fo);
174 unlink(tmpfname.tempCString);
175 return false;
177 syncFile(fo);
178 fclose(fo);
180 if (rename(tmpfname.tempCString, fname.tempCString) != 0) {
181 unlink(tmpfname.tempCString);
182 return false;
185 return true;
189 // ////////////////////////////////////////////////////////////////////////// //
191 log format (it looks like text, but it isn't text):
192 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
193 date in local time.
194 text: chars in range of [0..31, 92, 127] are \xHH-encoded
195 T is message type:
196 !: server/app notification
197 <: outgoing
198 >: incoming
199 if char before text is '!' instead of space, this is "/me" message
201 static struct LogFile {
202 public:
203 // this struct must not outlive LogFile
204 static struct Msg {
205 enum Kind { Notification, Incoming, Outgoing }
206 Kind kind;
207 bool isMe; // "/me" message?
208 SysTime time;
209 char[] rawText; // undecoded
211 // get decoded text
212 string text () const {
213 import iv.utfutil : utf8Encode;
214 char[] text;
215 text.reserve(rawText.length);
216 // decode text
217 foreach (dchar dc; byDChar) {
218 char[4] buf = void;
219 auto len = utf8Encode(buf[], dc);
220 text ~= buf[0..len];
222 return cast(string)text; // it is safe to cast here
225 // decode raw text to dchars; returns forward range
226 auto byDChar () const nothrow @safe @nogc {
227 static struct Range {
228 pure nothrow @trusted @nogc:
229 const(char)[] raw; // anchored
230 usize pos;
231 @property bool empty () const => (pos >= raw.length);
232 @property dchar front () const {
233 if (pos < raw.length) {
234 char ch = raw.ptr[pos];
235 if (ch <= 127) {
236 if (ch != '\\') return ch;
237 if (raw.length-pos < 2) return ch;
238 ch = raw.ptr[pos+1];
239 if (ch != 'x' && ch != 'X') {
240 switch (ch) {
241 case 'e': return '\e';
242 case 'n': return '\n';
243 case 'r': return '\r';
244 case 't': return '\t';
245 default:
247 return ch;
249 if (raw.length-pos < 4) return '?';
250 int n0 = raw.ptr[pos+2].digitInBase(16);
251 if (n0 < 0) return '?';
252 int n1 = raw.ptr[pos+3].digitInBase(16);
253 if (n1 < 0) return '?';
254 return cast(char)(n0*16+n1);
256 Utf8DecoderFast dc;
257 uint cpos = pos;
258 while (!dc.decode(cast(ubyte)raw.ptr[cpos++])) {
259 if (cpos >= raw.length) break; // no more chars
261 return (dc.complete ? dc.codepoint : dc.replacement);
262 } else {
263 return Utf8DecoderFast.replacement;
266 void popFront () {
267 if (pos < raw.length) {
268 char ch = raw.ptr[pos];
269 if (ch <= 127) {
270 ++pos;
271 if (ch != '\\') return;
272 if (raw.length-pos < 1) { pos = raw.length; return; }
273 ch = raw.ptr[pos++];
274 if (ch != 'x' && ch != 'X') return;
275 if (raw.length-pos < 2) { pos = raw.length; return; }
276 pos += 2;
277 return;
279 Utf8DecoderFast dc;
280 while (!dc.decode(cast(ubyte)raw.ptr[pos++])) {
281 if (pos >= raw.length) break; // no more chars
285 Range save () const => Range(raw, pos);
287 return Range(rawText, 0);
291 private:
292 char[] data;
294 public:
295 Msg[] messages;
297 public:
298 @disable this (this); // no copies
299 void opAssign() (in auto ref LogFile lf) { static assert(0, "no copies!"); }
301 this (const(char)[] fname) nothrow => load(fname);
303 // no `Msg` should outlive this call!
304 void clear () nothrow {
305 delete messages;
306 delete data;
309 void load (const(char)[] fname) nothrow {
310 import std.internal.cstring;
312 if (messages.length) {
313 messages.length = 0;
314 messages.assumeSafeAppend;
316 if (data.length) {
317 data.length = 0;
318 data.assumeSafeAppend;
321 if (fname.length == 0) return;
323 try {
324 auto fi = VFile(fname);
325 if (fi.size > int.max/8) assert(0, "log file too big");
326 data.length = cast(int)fi.size;
327 fi.rawReadExact(data);
328 } catch (Exception e) {}
330 parseData();
333 // `data` should be set
334 private void parseData () nothrow {
335 import core.stdc.string : memchr;
337 assert(messages.length == 0); // sorry
338 if (data.length == 0) return;
340 // count '\n'
341 int nlcount = 0;
343 auto dp = data.ptr;
344 while (dp < data.ptr+data.length) {
345 auto nx = cast(char*)memchr(dp, '\n', data.length-(dp-data.ptr));
346 if (nx is null) break;
347 ++nlcount;
348 dp = nx+1;
351 messages.reserve(nlcount+1);
353 static int toInt (const(char)[] s) nothrow @trusted @nogc {
354 int res = 0;
355 while (s.length && s.ptr[0].isdigit) {
356 res = res*10+s.ptr[0]-'0';
357 s = s[1..$];
359 return res;
362 // parse messages
363 auto dp = data.ptr, nx = data.ptr+data.length;
364 for (; dp < data.ptr+data.length; dp = nx+1) {
365 nx = cast(char*)memchr(dp, '\n', data.length-(dp-data.ptr));
366 if (nx is null) nx = data.ptr+data.length;
367 // 25 is the minimal message length
368 if (nx-dp < 25) continue;
369 if (dp[1] != '[' || dp[6] != '/' || dp[9] != '/' || dp[12] != ' ' || dp[15] != ':' || dp[18] != ':' || dp[21] != ']' || dp[22] != ':') continue;
370 foreach (immutable cidx, immutable char ch; dp[0..21]) {
371 if (cidx < 2 || cidx == 6 || cidx == 9 || cidx == 12 || cidx == 15 || cidx == 18) continue;
372 if (!ch.isdigit) continue;
374 Msg msg;
375 // message kind
376 switch (dp[0]) {
377 case '!': msg.kind = msg.Kind.Notification; break;
378 case '<': msg.kind = msg.Kind.Outgoing; break;
379 case '>': msg.kind = msg.Kind.Incoming; break;
380 default: continue;
382 // "/me"?
383 switch (dp[23]) {
384 case ' ': msg.isMe = false; break;
385 case '!': msg.isMe = true; break;
386 default: continue;
388 int year = toInt(dp[2..6]);
389 if (year < 2018 || year > 2036) continue; //FIXME in 2036! ;-)
390 int month = toInt(dp[7..9]);
391 if (month < 1 || month > 12) continue;
392 int day = toInt(dp[10..12]);
393 if (day < 1 || day > 31) continue;
394 int hour = toInt(dp[13..15]);
395 if (hour < 0 || hour > 23) continue;
396 int minute = toInt(dp[16..18]);
397 if (minute < 0 || minute > 59) continue;
398 int second = toInt(dp[19..21]);
399 if (second < 0 || second > 59) continue;
400 bool timeok = false;
401 try {
402 msg.time = SysTime(DateTime(year, month, day, hour, minute, second));
403 timeok = true;
404 } catch (Exception e) {}
405 if (!timeok) continue;
406 msg.rawText = dp[24..(nx-dp)];
407 messages ~= msg;
411 public:
412 // append line to log file; text is in utf8
413 // WARNING! does no validity checks!
414 static bool appendLine (const(char)[] fname, LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) nothrow {
415 import core.stdc.stdio : FILE, fopen, fclose, fwrite;
416 import std.internal.cstring;
418 if (fname.length == 0) return false;
420 FILE* fo = fopen(fname.tempCString, "a");
421 if (fo is null) return false;
422 scope(exit) { syncFile(fo); fclose(fo); }
424 bool xwrite (const(char)[] s...) nothrow @nogc {
425 while (s.length > 0) {
426 auto wr = fwrite(s.ptr, 1, s.length, fo);
427 if (wr == 0) {
428 import core.stdc.errno;
429 if (errno != EINTR) return false;
430 continue;
432 s = s[wr..$];
434 return true;
437 bool wrnum (int n, int wdt) nothrow @nogc {
438 import core.stdc.stdio : snprintf;
439 if (n < 0) return false; // alas
440 char[128] buf = void;
441 auto len = (wdt > 0 ? snprintf(buf.ptr, buf.length, "%0*d", wdt, n) : snprintf(buf.ptr, buf.length, "%d", n));
442 if (len < 1) return false;
443 return xwrite(buf[0..len]);
446 final switch (kind) {
447 case LogFile.Msg.Kind.Notification: if (!xwrite("!")) return false; break;
448 case LogFile.Msg.Kind.Incoming: if (!xwrite(">")) return false; break;
449 case LogFile.Msg.Kind.Outgoing: if (!xwrite("<")) return false; break;
452 // write date
453 auto dt = cast(DateTime)time;
454 if (!xwrite("[")) return false;
455 if (!wrnum(dt.year, 4)) return false;
456 if (!xwrite("/")) return false;
457 if (!wrnum(cast(int)dt.month, 2)) return false;
458 if (!xwrite("/")) return false;
459 if (!wrnum(dt.day, 2)) return false;
460 if (!xwrite(" ")) return false;
461 if (!wrnum(dt.hour, 2)) return false;
462 if (!xwrite(":")) return false;
463 if (!wrnum(dt.minute, 2)) return false;
464 if (!xwrite(":")) return false;
465 if (!wrnum(dt.second, 2)) return false;
466 if (!xwrite("]:")) return false;
467 if (!xwrite(isMe ? "!" : " ")) return false;
469 // write encoded string
470 while (text.length) {
471 usize end = 0;
472 while (end < text.length) {
473 char ch = text.ptr[end];
474 if (ch < ' ' || ch == 92 || ch == 127) break;
475 ++end;
477 if (end > 0) {
478 if (!xwrite(text[0..end])) return false;
479 text = text[end..$];
480 } else {
481 switch (text.ptr[0]) {
482 case '\e': if (!xwrite(`\e`)) return false; break;
483 case '\n': if (!xwrite(`\n`)) return false; break;
484 case '\r': if (!xwrite(`\r`)) return false; break;
485 case '\t': if (!xwrite(`\t`)) return false; break;
486 default:
487 import core.stdc.stdio : snprintf;
488 char[16] buf = void;
489 auto len = snprintf(buf.ptr, buf.length, "\\x%02x", cast(uint)text.ptr[0]);
490 if (!xwrite(buf[0..len])) return false;
491 break;
493 text = text[1..$];
497 return xwrite("\n");
500 // append line to log file; text is in utf8
501 // WARNING! does no validity checks!
502 static bool appendLine (const(char)[] fname, LogFile.Msg.Kind kind, const(char)[] text, bool isMe=false) nothrow {
503 try {
504 return appendLine(fname, kind, text, isMe, systimeNow);
505 } catch (Exception e) {}
506 return false;