fixed bug in article insertion
[knntp.git] / twitlist.d
blobb0cbe00d16f8f2bb83b4ff0d84268e59717da604
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, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module twitlist;
20 import iv.cmdcon;
21 import iv.vfs;
23 import nntp;
26 // ////////////////////////////////////////////////////////////////////////// //
27 public __gshared TwitList twits;
28 public __gshared ThreadTwitList threadtwits;
31 // ////////////////////////////////////////////////////////////////////////// //
32 public final class TwitList {
33 public:
34 static final class Twit {
35 enum Flag : ubyte {
36 NameMatch = 1U<<0, // match only name part (for names that came from web)
37 HardIgnoreThreads = 1U<<1, // hard-ignore threads by this idiot?
39 ubyte flags;
40 string name;
41 string title;
42 string url;
43 string msgid;
45 @property bool nameMatch () const pure nothrow @safe @nogc { return ((flags&Flag.NameMatch) != 0); }
46 @property bool hardIgnoreThread () const pure nothrow @safe @nogc { return ((flags&Flag.HardIgnoreThreads) != 0); }
48 override string toString () const {
49 string res;
50 if (flags&Flag.HardIgnoreThreads) res ~= "!";
51 if (flags&Flag.NameMatch) res ~= "*";
52 res ~= "<";
53 res ~= name;
54 res ~= ">";
55 return res;
59 private:
60 Twit[] nlist;
61 Twit[string] list; // keyed by name+mail
63 public:
64 this () {}
66 Twit check (in ref Article art) {
67 synchronized(this) {
68 if (!art.valid) return null;
69 char[256+256+4] nmm;
70 uint pos = cast(uint)art.fromname.length;
71 nmm[0..pos] = art.fromname[];
72 nmm[pos++] = ' ';
73 nmm[pos++] = '<';
74 nmm[pos..pos+art.frommail.length] = art.frommail[];
75 pos += cast(uint)art.frommail.length;
76 nmm[pos++] = '>';
77 const(char)[] nm = nmm[0..pos];
78 if (auto tp = nm in list) return *tp;
79 foreach (Twit t; nlist) {
80 //conwriteln("|", getName(art.from), "|");
81 if (getName(art.fromname) == t.name) return t;
84 return null;
87 void clear () {
88 synchronized(this) {
89 list.clear();
90 nlist.length = 0;
91 nlist.assumeSafeAppend;
95 // msgid can be empty
96 // title can be empty
97 void update (string name, string msgid, string title, string fullname) {
98 if (name.length > 255 || fullname.length > 255 || msgid.length > 255 || title.length > 255) throw new Exception("invalid data");
99 if (name.length == 0) return;
100 synchronized(this) {
101 bool hardHide = false;
102 if (title.length && title[0] == '!') {
103 hardHide = true;
104 title = title[1..$];
105 if (title.length == 0) return;
107 scope(exit) save();
108 string aurl, aid;
109 foreach (immutable idx, Twit t; nlist) {
110 if (t.name == name) {
111 aurl = t.url;
112 aid = t.msgid;
113 // remove
114 import std.algorithm : remove;
115 nlist = nlist.remove(idx);
118 if (msgid.length == 0 && aid.length > 0) msgid = aid;
119 // add
120 if (title.length == 0) return;
122 auto t = new Twit();
124 if (fullname.length) {
125 t.name = fullname;
126 } else {
127 t.name = name;
128 t.flags |= t.Flag.NameMatch;
130 if (hardHide) t.flags |= t.Flag.HardIgnoreThreads;
131 t.title = title;
132 t.msgid = msgid;
133 t.url = aurl;
134 if (t.flags&t.Flag.NameMatch) {
135 nlist ~= t;
136 } else {
137 list[t.name] = t;
142 void load () {
143 import std.file : exists;
144 clear();
145 if (!"./data/twit.dat".exists) return;
146 scope(failure) clear;
147 try {
148 auto fl = VFile("./data/twit.dat");
149 char[4] sign;
150 fl.rawReadExact(sign[]);
151 if (sign != "KNTL") throw new Exception("invalid signature");
152 if (fl.readNum!uint != 0) throw new Exception("invalid version");
153 auto fsize = fl.size;
154 auto cpos = fl.tell;
155 while (cpos != fsize) {
156 auto rsz = fl.readNum!ushort;
157 if (rsz < 6) throw new Exception("invalid record size (too small)");
158 auto t = new Twit();
159 t.flags = fl.readNum!ubyte;
160 t.name = readBStr(fl);
161 t.title = readBStr(fl);
162 t.url = readBStr(fl);
163 t.msgid = readBStr(fl);
164 auto epos = fl.tell;
165 //conwriteln("rsz=", rsz, " (", epos-cpos-2, ")");
166 if (epos-cpos-2 != rsz) throw new Exception("invalid record size");
167 cpos = epos;
168 if (t.flags&t.Flag.NameMatch) {
169 nlist ~= t;
170 } else {
171 list[t.name] = t;
174 } catch (Exception e) {
175 conwriteln("ERROR loading twitlist: ", e.msg);
179 void save () {
180 try {
181 auto fo = VFile("./data/twit.dat", "w");
182 fo.rawWriteExact("KNTL");
183 fo.writeNum!uint(0); // version
184 foreach (Twit t; nlist) {
185 auto cpos = fo.tell;
186 fo.writeNum!ushort(0);
187 fo.writeNum!ubyte(t.flags);
188 writeBStr(fo, t.name);
189 writeBStr(fo, t.title);
190 writeBStr(fo, t.url);
191 writeBStr(fo, t.msgid);
192 auto epos = fo.tell;
193 if (epos-cpos-2 > ushort.max) throw new Exception("invalid record size");
194 fo.seek(cpos);
195 fo.writeNum!ushort(cast(ushort)(epos-cpos-2));
196 fo.seek(epos);
198 foreach (Twit t; list.byValue) {
199 auto cpos = fo.tell;
200 fo.writeNum!ushort(0);
201 fo.writeNum!ubyte(t.flags);
202 writeBStr(fo, t.name);
203 writeBStr(fo, t.title);
204 writeBStr(fo, t.url);
205 writeBStr(fo, t.msgid);
206 auto epos = fo.tell;
207 if (epos-cpos-2 > ushort.max) throw new Exception("invalid record size");
208 fo.seek(cpos);
209 fo.writeNum!ushort(cast(ushort)(epos-cpos-2));
210 fo.seek(epos);
212 } catch (Exception e) {
213 conwriteln("ERROR saving twitlist: ", e.msg);
217 static:
218 string readBStr (VFile fl) {
219 auto len = fl.readNum!ubyte;
220 if (len == 0) return null;
221 auto s = new char[](len);
222 fl.rawReadExact(s);
223 return cast(string)s; // it is safe to cast here
226 void writeBStr (VFile fo, const(char)[] s) {
227 if (s.length > 255) throw new Exception("wtf?!");
228 fo.writeNum!ubyte(cast(ubyte)s.length);
229 fo.rawWriteExact(s);
232 T getName(T : const(char)[]) (T s) {
233 while (s.length && s[0] <= ' ') s = s[1..$];
234 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
235 if (s.length >= 2 && s[$-1] == '>') {
236 while (s.length && s[$-1] != '<') s = s[0..$-1];
237 if (s.length) {
238 assert(s[$-1] == '<');
239 s = s[0..$-1];
241 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
243 return s;
248 // ////////////////////////////////////////////////////////////////////////// //
249 public final class ThreadTwitList {
250 public:
251 enum Action : ubyte {
252 None,
253 Ignore,
254 Hide,
257 private:
258 bool[string] list; // msgids
260 public:
261 this () {}
263 Action check (in ref Article art) {
264 synchronized(this) {
265 if (!art.valid) return Action.None;
266 if (art.msgid in list) return Action.Hide;
268 return Action.None;
271 void clear () {
272 synchronized(this) {
273 list.clear();
277 void add (string s) {
278 if (s.length == 0 || s.length > 255) return;
279 synchronized(this) {
280 list[s] = true;
281 save();
285 void load () {
286 import std.file : exists;
287 clear();
288 if (!"./data/twit_threads.dat".exists) return;
289 scope(failure) clear;
290 try {
291 auto fl = VFile("./data/twit_threads.dat");
292 char[4] sign;
293 fl.rawReadExact(sign[]);
294 if (sign != "KNTT") throw new Exception("invalid signature");
295 if (fl.readNum!uint != 0) throw new Exception("invalid version");
296 auto fsize = fl.size;
297 while (fl.tell != fsize) {
298 string s = TwitList.readBStr(fl);
299 if (s.length == 0) continue;
300 if (s in list) continue;
301 list[s] = true;
303 } catch (Exception e) {
304 conwriteln("ERROR loading twitlist: ", e.msg);
308 void save () {
309 try {
310 auto fo = VFile("./data/twit_threads.dat", "w");
311 fo.rawWriteExact("KNTT");
312 fo.writeNum!uint(0); // version
313 foreach (string s; list.byKey) {
314 TwitList.writeBStr(fo, s);
316 } catch (Exception e) {
317 conwriteln("ERROR saving twitlist: ", e.msg);