even more modules!
[knntp.git] / twitlist.d
blobfbf7ed232922e5594d3953f3f94ba03ae34840c1
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[] list;
62 public:
63 this () {}
65 Twit check (in ref Article art) {
66 synchronized(this) {
67 if (!art.valid) return null;
68 const(char)[] qq = null;
69 foreach (Twit t; list) {
70 if (t.nameMatch) {
71 //conwriteln("|", getName(art.from), "|");
72 if (getName(art.from) == t.name) return t;
73 if (qq is null) qq = Article.decodeq(art.from);
74 if (getName(qq) == t.name) return t;
75 } else {
76 if (t.name == art.from) return t;
80 return null;
83 void clear () {
84 synchronized(this) {
85 list.length = 0;
86 list.assumeSafeAppend;
90 // msgid can be empty
91 // title can be empty
92 void update (string name, string msgid, string title, string fullname) {
93 if (name.length > 255 || fullname.length > 255 || msgid.length > 255 || title.length > 255) throw new Exception("invalid data");
94 if (name.length == 0) return;
95 synchronized(this) {
96 bool hardHide = false;
97 if (title.length && title[0] == '!') {
98 hardHide = true;
99 title = title[1..$];
100 if (title.length == 0) return;
102 if (title.length && title[$-1] == '!') {
103 hardHide = true;
104 title = title[0..$-1];
105 if (title.length == 0) return;
107 scope(exit) save();
108 foreach (immutable idx, Twit t; list) {
109 if (t.name == name) {
110 if (title.length == 0) {
111 // remove
112 import std.algorithm : remove;
113 list = list.remove(idx);
114 } else {
115 // replace
116 if (fullname.length) {
117 t.name = fullname;
118 t.flags &= ~t.Flag.NameMatch;
120 if (hardHide) t.flags |= t.Flag.HardIgnoreThreads; else t.flags &= ~t.Flag.HardIgnoreThreads;
121 t.title = title;
122 if (msgid.length) t.msgid = msgid;
124 return;
127 // add
128 if (title.length == 0) return;
129 auto t = new Twit();
130 if (fullname.length) {
131 t.name = fullname;
132 } else {
133 t.name = name;
134 t.flags |= t.Flag.NameMatch;
136 if (hardHide) t.flags |= t.Flag.HardIgnoreThreads;
137 t.title = title;
138 t.msgid = msgid;
139 list ~= t;
143 void load () {
144 import std.file : exists;
145 clear();
146 if (!"./data/twit.dat".exists) return;
147 scope(failure) clear;
148 try {
149 auto fl = VFile("./data/twit.dat");
150 char[4] sign;
151 fl.rawReadExact(sign[]);
152 if (sign != "KNTL") throw new Exception("invalid signature");
153 if (fl.readNum!uint != 0) throw new Exception("invalid version");
154 auto fsize = fl.size;
155 auto cpos = fl.tell;
156 while (cpos != fsize) {
157 auto rsz = fl.readNum!ushort;
158 if (rsz < 6) throw new Exception("invalid record size (too small)");
159 auto t = new Twit();
160 t.flags = fl.readNum!ubyte;
161 t.name = readBStr(fl);
162 t.title = readBStr(fl);
163 t.url = readBStr(fl);
164 t.msgid = readBStr(fl);
165 auto epos = fl.tell;
166 //conwriteln("rsz=", rsz, " (", epos-cpos-2, ")");
167 if (epos-cpos-2 != rsz) throw new Exception("invalid record size");
168 cpos = epos;
169 list ~= t;
171 } catch (Exception e) {
172 conwriteln("ERROR loading twitlist: ", e.msg);
176 void save () {
177 try {
178 auto fo = VFile("./data/twit.dat", "w");
179 fo.rawWriteExact("KNTL");
180 fo.writeNum!uint(0); // version
181 foreach (Twit t; list) {
182 auto cpos = fo.tell;
183 fo.writeNum!ushort(0);
184 fo.writeNum!ubyte(t.flags);
185 writeBStr(fo, t.name);
186 writeBStr(fo, t.title);
187 writeBStr(fo, t.url);
188 writeBStr(fo, t.msgid);
189 auto epos = fo.tell;
190 if (epos-cpos-2 > ushort.max) throw new Exception("invalid record size");
191 fo.seek(cpos);
192 fo.writeNum!ushort(cast(ushort)(epos-cpos-2));
193 fo.seek(epos);
195 } catch (Exception e) {
196 conwriteln("ERROR saving twitlist: ", e.msg);
200 static:
201 string readBStr (VFile fl) {
202 auto len = fl.readNum!ubyte;
203 if (len == 0) return null;
204 auto s = new char[](len);
205 fl.rawReadExact(s);
206 return cast(string)s; // it is safe to cast here
209 void writeBStr (VFile fo, const(char)[] s) {
210 if (s.length > 255) throw new Exception("wtf?!");
211 fo.writeNum!ubyte(cast(ubyte)s.length);
212 fo.rawWriteExact(s);
215 T getName(T : const(char)[]) (T s) {
216 while (s.length && s[0] <= ' ') s = s[1..$];
217 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
218 if (s.length >= 2 && s[$-1] == '>') {
219 while (s.length && s[$-1] != '<') s = s[0..$-1];
220 if (s.length) {
221 assert(s[$-1] == '<');
222 s = s[0..$-1];
224 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
226 return s;
231 // ////////////////////////////////////////////////////////////////////////// //
232 public final class ThreadTwitList {
233 public:
234 enum Action : ubyte {
235 None,
236 Ignore,
237 Hide,
240 private:
241 bool[string] list; // msgids
243 public:
244 this () {}
246 Action check (in ref Article art) {
247 synchronized(this) {
248 if (!art.valid) return Action.None;
249 if (art.msgid in list) return Action.Hide;
251 return Action.None;
254 void clear () {
255 synchronized(this) {
256 list.clear();
260 void add (string s) {
261 if (s.length == 0 || s.length > 255) return;
262 synchronized(this) {
263 list[s] = true;
264 save();
268 void load () {
269 import std.file : exists;
270 clear();
271 if (!"./data/twit_threads.dat".exists) return;
272 scope(failure) clear;
273 try {
274 auto fl = VFile("./data/twit_threads.dat");
275 char[4] sign;
276 fl.rawReadExact(sign[]);
277 if (sign != "KNTT") throw new Exception("invalid signature");
278 if (fl.readNum!uint != 0) throw new Exception("invalid version");
279 auto fsize = fl.size;
280 while (fl.tell != fsize) {
281 string s = TwitList.readBStr(fl);
282 if (s.length == 0) continue;
283 if (s in list) continue;
284 list[s] = true;
286 } catch (Exception e) {
287 conwriteln("ERROR loading twitlist: ", e.msg);
291 void save () {
292 try {
293 auto fo = VFile("./data/twit_threads.dat", "w");
294 fo.rawWriteExact("KNTT");
295 fo.writeNum!uint(0); // version
296 foreach (string s; list.byKey) {
297 TwitList.writeBStr(fo, s);
299 } catch (Exception e) {
300 conwriteln("ERROR saving twitlist: ", e.msg);