switched to GPLv3 ONLY, because i don't trust FSF anymore
[knntp.git] / twitlist.d
blobd8c75a8a936ad7f74fd60005055d6bb710800f64
1 /* DigitalMars NNTP reader
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, version 3 of the License ONLY.
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 twitlist;
19 import iv.cmdcon;
20 import iv.vfs;
22 import nntp;
25 // ////////////////////////////////////////////////////////////////////////// //
26 public __gshared TwitList twits;
27 public __gshared ThreadTwitList threadtwits;
30 // ////////////////////////////////////////////////////////////////////////// //
31 public final class TwitList {
32 public:
33 static final class Twit {
34 enum Flag : ubyte {
35 NameMatch = 1U<<0, // match only name part (for names that came from web)
36 HardIgnoreThreads = 1U<<1, // hard-ignore threads by this idiot?
38 ubyte flags;
39 string name;
40 string title;
41 string url;
42 string msgid;
44 @property bool nameMatch () const pure nothrow @safe @nogc { return ((flags&Flag.NameMatch) != 0); }
45 @property bool hardIgnoreThread () const pure nothrow @safe @nogc { return ((flags&Flag.HardIgnoreThreads) != 0); }
47 override string toString () const {
48 string res;
49 if (flags&Flag.HardIgnoreThreads) res ~= "!";
50 if (flags&Flag.NameMatch) res ~= "*";
51 res ~= "<";
52 res ~= name;
53 res ~= ">";
54 return res;
58 private:
59 Twit[] nlist;
60 Twit[string] list; // keyed by name+mail
62 public:
63 this () {}
65 Twit check (in ref Article art) {
66 synchronized(this) {
67 if (!art.valid) return null;
68 char[256+256+4] nmm;
69 uint pos = cast(uint)art.fromname.length;
70 nmm[0..pos] = art.fromname[];
71 nmm[pos++] = ' ';
72 nmm[pos++] = '<';
73 nmm[pos..pos+art.frommail.length] = art.frommail[];
74 pos += cast(uint)art.frommail.length;
75 nmm[pos++] = '>';
76 const(char)[] nm = nmm[0..pos];
77 if (auto tp = nm in list) return *tp;
78 foreach (Twit t; nlist) {
79 //conwriteln("|", getName(art.from), "|");
80 if (getName(art.fromname) == t.name) return t;
83 return null;
86 void clear () {
87 synchronized(this) {
88 list.clear();
89 nlist.length = 0;
90 nlist.assumeSafeAppend;
94 // msgid can be empty
95 // title can be empty
96 void update (string name, string msgid, string title, string fullname) {
97 if (name.length > 255 || fullname.length > 255 || msgid.length > 255 || title.length > 255) throw new Exception("invalid data");
98 if (name.length == 0) return;
99 synchronized(this) {
100 bool hardHide = false;
101 if (title.length && title[0] == '!') {
102 hardHide = true;
103 title = title[1..$];
104 if (title.length == 0) return;
106 scope(exit) save();
107 string aurl, aid;
108 foreach (immutable idx, Twit t; nlist) {
109 if (t.name == name) {
110 aurl = t.url;
111 aid = t.msgid;
112 // remove
113 import std.algorithm : remove;
114 nlist = nlist.remove(idx);
117 if (msgid.length == 0 && aid.length > 0) msgid = aid;
118 // add
119 if (title.length == 0) return;
121 auto t = new Twit();
123 if (fullname.length) {
124 t.name = fullname;
125 } else {
126 t.name = name;
127 t.flags |= t.Flag.NameMatch;
129 if (hardHide) t.flags |= t.Flag.HardIgnoreThreads;
130 t.title = title;
131 t.msgid = msgid;
132 t.url = aurl;
133 if (t.flags&t.Flag.NameMatch) {
134 nlist ~= t;
135 } else {
136 list[t.name] = t;
141 void load () {
142 import std.file : exists;
143 clear();
144 if (!"./data/twit.dat".exists) return;
145 scope(failure) clear;
146 try {
147 auto fl = VFile("./data/twit.dat");
148 char[4] sign;
149 fl.rawReadExact(sign[]);
150 if (sign != "KNTL") throw new Exception("invalid signature");
151 if (fl.readNum!uint != 0) throw new Exception("invalid version");
152 auto fsize = fl.size;
153 auto cpos = fl.tell;
154 while (cpos != fsize) {
155 auto rsz = fl.readNum!ushort;
156 if (rsz < 6) throw new Exception("invalid record size (too small)");
157 auto t = new Twit();
158 t.flags = fl.readNum!ubyte;
159 t.name = readBStr(fl);
160 t.title = readBStr(fl);
161 t.url = readBStr(fl);
162 t.msgid = readBStr(fl);
163 auto epos = fl.tell;
164 //conwriteln("rsz=", rsz, " (", epos-cpos-2, ")");
165 if (epos-cpos-2 != rsz) throw new Exception("invalid record size");
166 cpos = epos;
167 if (t.flags&t.Flag.NameMatch) {
168 nlist ~= t;
169 } else {
170 list[t.name] = t;
173 } catch (Exception e) {
174 conwriteln("ERROR loading twitlist: ", e.msg);
178 void save () {
179 try {
180 auto fo = VFile("./data/twit.dat", "w");
181 fo.rawWriteExact("KNTL");
182 fo.writeNum!uint(0); // version
183 foreach (Twit t; nlist) {
184 auto cpos = fo.tell;
185 fo.writeNum!ushort(0);
186 fo.writeNum!ubyte(t.flags);
187 writeBStr(fo, t.name);
188 writeBStr(fo, t.title);
189 writeBStr(fo, t.url);
190 writeBStr(fo, t.msgid);
191 auto epos = fo.tell;
192 if (epos-cpos-2 > ushort.max) throw new Exception("invalid record size");
193 fo.seek(cpos);
194 fo.writeNum!ushort(cast(ushort)(epos-cpos-2));
195 fo.seek(epos);
197 foreach (Twit t; list.byValue) {
198 auto cpos = fo.tell;
199 fo.writeNum!ushort(0);
200 fo.writeNum!ubyte(t.flags);
201 writeBStr(fo, t.name);
202 writeBStr(fo, t.title);
203 writeBStr(fo, t.url);
204 writeBStr(fo, t.msgid);
205 auto epos = fo.tell;
206 if (epos-cpos-2 > ushort.max) throw new Exception("invalid record size");
207 fo.seek(cpos);
208 fo.writeNum!ushort(cast(ushort)(epos-cpos-2));
209 fo.seek(epos);
211 } catch (Exception e) {
212 conwriteln("ERROR saving twitlist: ", e.msg);
216 static:
217 string readBStr (VFile fl) {
218 auto len = fl.readNum!ubyte;
219 if (len == 0) return null;
220 auto s = new char[](len);
221 fl.rawReadExact(s);
222 return cast(string)s; // it is safe to cast here
225 void writeBStr (VFile fo, const(char)[] s) {
226 if (s.length > 255) throw new Exception("wtf?!");
227 fo.writeNum!ubyte(cast(ubyte)s.length);
228 fo.rawWriteExact(s);
231 T getName(T : const(char)[]) (T s) {
232 while (s.length && s[0] <= ' ') s = s[1..$];
233 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
234 if (s.length >= 2 && s[$-1] == '>') {
235 while (s.length && s[$-1] != '<') s = s[0..$-1];
236 if (s.length) {
237 assert(s[$-1] == '<');
238 s = s[0..$-1];
240 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
242 return s;
247 // ////////////////////////////////////////////////////////////////////////// //
248 public final class ThreadTwitList {
249 public:
250 enum Action : ubyte {
251 None,
252 Ignore,
253 Hide,
256 private:
257 bool[string] list; // msgids
259 public:
260 this () {}
262 Action check (in ref Article art) {
263 synchronized(this) {
264 if (!art.valid) return Action.None;
265 if (art.msgid in list) return Action.Hide;
267 return Action.None;
270 void clear () {
271 synchronized(this) {
272 list.clear();
276 void add (string s) {
277 if (s.length == 0 || s.length > 255) return;
278 synchronized(this) {
279 list[s] = true;
280 save();
284 void load () {
285 import std.file : exists;
286 clear();
287 if (!"./data/twit_threads.dat".exists) return;
288 scope(failure) clear;
289 try {
290 auto fl = VFile("./data/twit_threads.dat");
291 char[4] sign;
292 fl.rawReadExact(sign[]);
293 if (sign != "KNTT") throw new Exception("invalid signature");
294 if (fl.readNum!uint != 0) throw new Exception("invalid version");
295 auto fsize = fl.size;
296 while (fl.tell != fsize) {
297 string s = TwitList.readBStr(fl);
298 if (s.length == 0) continue;
299 if (s in list) continue;
300 list[s] = true;
302 } catch (Exception e) {
303 conwriteln("ERROR loading twitlist: ", e.msg);
307 void save () {
308 try {
309 auto fo = VFile("./data/twit_threads.dat", "w");
310 fo.rawWriteExact("KNTT");
311 fo.writeNum!uint(0); // version
312 foreach (string s; list.byKey) {
313 TwitList.writeBStr(fo, s);
315 } catch (Exception e) {
316 conwriteln("ERROR saving twitlist: ", e.msg);