don't center cursor on message tree rebuild
[knntp.git] / twitlist.d
bloba41bf57a7d87058452dad807c91af824c2f02d16
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;
63 public:
64 this () {}
66 Twit check (in ref Article art) {
67 synchronized(this) {
68 if (!art.valid) return null;
69 if (auto tp = art.from in list) return *tp;
70 foreach (Twit t; nlist) {
71 //conwriteln("|", getName(art.from), "|");
72 if (getName(art.from) == t.name) return t;
73 if (getName(art.fromDC) == t.name) return t;
76 return null;
79 void clear () {
80 synchronized(this) {
81 list.clear();
82 nlist.length = 0;
83 nlist.assumeSafeAppend;
87 // msgid can be empty
88 // title can be empty
89 void update (string name, string msgid, string title, string fullname) {
90 if (name.length > 255 || fullname.length > 255 || msgid.length > 255 || title.length > 255) throw new Exception("invalid data");
91 if (name.length == 0) return;
92 synchronized(this) {
93 bool hardHide = false;
94 if (title.length && title[0] == '!') {
95 hardHide = true;
96 title = title[1..$];
97 if (title.length == 0) return;
99 if (title.length && title[$-1] == '!') {
100 hardHide = true;
101 title = title[0..$-1];
102 if (title.length == 0) return;
104 scope(exit) save();
105 string aurl, aid;
106 foreach (immutable idx, Twit t; nlist) {
107 if (t.name == name) {
108 aurl = t.url;
109 aid = t.msgid;
110 // remove
111 import std.algorithm : remove;
112 nlist = nlist.remove(idx);
115 if (msgid.length == 0 && aid.length > 0) msgid = aid;
116 // add
117 if (title.length == 0) return;
119 auto t = new Twit();
121 if (fullname.length) {
122 t.name = fullname;
123 } else {
124 t.name = name;
125 t.flags |= t.Flag.NameMatch;
127 if (hardHide) t.flags |= t.Flag.HardIgnoreThreads;
128 t.title = title;
129 t.msgid = msgid;
130 t.url = aurl;
131 if (t.flags&t.Flag.NameMatch) {
132 nlist ~= t;
133 } else {
134 list[t.name] = t;
139 void load () {
140 import std.file : exists;
141 clear();
142 if (!"./data/twit.dat".exists) return;
143 scope(failure) clear;
144 try {
145 auto fl = VFile("./data/twit.dat");
146 char[4] sign;
147 fl.rawReadExact(sign[]);
148 if (sign != "KNTL") throw new Exception("invalid signature");
149 if (fl.readNum!uint != 0) throw new Exception("invalid version");
150 auto fsize = fl.size;
151 auto cpos = fl.tell;
152 while (cpos != fsize) {
153 auto rsz = fl.readNum!ushort;
154 if (rsz < 6) throw new Exception("invalid record size (too small)");
155 auto t = new Twit();
156 t.flags = fl.readNum!ubyte;
157 t.name = readBStr(fl);
158 t.title = readBStr(fl);
159 t.url = readBStr(fl);
160 t.msgid = readBStr(fl);
161 auto epos = fl.tell;
162 //conwriteln("rsz=", rsz, " (", epos-cpos-2, ")");
163 if (epos-cpos-2 != rsz) throw new Exception("invalid record size");
164 cpos = epos;
165 if (t.flags&t.Flag.NameMatch) {
166 nlist ~= t;
167 } else {
168 list[t.name] = 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; nlist) {
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 foreach (Twit t; list.byValue) {
196 auto cpos = fo.tell;
197 fo.writeNum!ushort(0);
198 fo.writeNum!ubyte(t.flags);
199 writeBStr(fo, t.name);
200 writeBStr(fo, t.title);
201 writeBStr(fo, t.url);
202 writeBStr(fo, t.msgid);
203 auto epos = fo.tell;
204 if (epos-cpos-2 > ushort.max) throw new Exception("invalid record size");
205 fo.seek(cpos);
206 fo.writeNum!ushort(cast(ushort)(epos-cpos-2));
207 fo.seek(epos);
209 } catch (Exception e) {
210 conwriteln("ERROR saving twitlist: ", e.msg);
214 static:
215 string readBStr (VFile fl) {
216 auto len = fl.readNum!ubyte;
217 if (len == 0) return null;
218 auto s = new char[](len);
219 fl.rawReadExact(s);
220 return cast(string)s; // it is safe to cast here
223 void writeBStr (VFile fo, const(char)[] s) {
224 if (s.length > 255) throw new Exception("wtf?!");
225 fo.writeNum!ubyte(cast(ubyte)s.length);
226 fo.rawWriteExact(s);
229 T getName(T : const(char)[]) (T s) {
230 while (s.length && s[0] <= ' ') s = s[1..$];
231 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
232 if (s.length >= 2 && s[$-1] == '>') {
233 while (s.length && s[$-1] != '<') s = s[0..$-1];
234 if (s.length) {
235 assert(s[$-1] == '<');
236 s = s[0..$-1];
238 while (s.length && s[$-1] <= ' ') s = s[0..$-1];
240 return s;
245 // ////////////////////////////////////////////////////////////////////////// //
246 public final class ThreadTwitList {
247 public:
248 enum Action : ubyte {
249 None,
250 Ignore,
251 Hide,
254 private:
255 bool[string] list; // msgids
257 public:
258 this () {}
260 Action check (in ref Article art) {
261 synchronized(this) {
262 if (!art.valid) return Action.None;
263 if (art.msgid in list) return Action.Hide;
265 return Action.None;
268 void clear () {
269 synchronized(this) {
270 list.clear();
274 void add (string s) {
275 if (s.length == 0 || s.length > 255) return;
276 synchronized(this) {
277 list[s] = true;
278 save();
282 void load () {
283 import std.file : exists;
284 clear();
285 if (!"./data/twit_threads.dat".exists) return;
286 scope(failure) clear;
287 try {
288 auto fl = VFile("./data/twit_threads.dat");
289 char[4] sign;
290 fl.rawReadExact(sign[]);
291 if (sign != "KNTT") throw new Exception("invalid signature");
292 if (fl.readNum!uint != 0) throw new Exception("invalid version");
293 auto fsize = fl.size;
294 while (fl.tell != fsize) {
295 string s = TwitList.readBStr(fl);
296 if (s.length == 0) continue;
297 if (s in list) continue;
298 list[s] = true;
300 } catch (Exception e) {
301 conwriteln("ERROR loading twitlist: ", e.msg);
305 void save () {
306 try {
307 auto fo = VFile("./data/twit_threads.dat", "w");
308 fo.rawWriteExact("KNTT");
309 fo.writeNum!uint(0); // version
310 foreach (string s; list.byKey) {
311 TwitList.writeBStr(fo, s);
313 } catch (Exception e) {
314 conwriteln("ERROR saving twitlist: ", e.msg);