1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3 * Pan - A Newsreader for Gtk+
4 * Copyright (C) 2002-2006 Charles Kerr <charles@rebelbase.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; version 2 of the License.
9 *
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.
14 *
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/>.
17 *
18 */
19
20 /**************
21 ***************
22 **************/
23
24 #include <config.h>
25 #include <cassert>
26 #include <iostream>
27 #include <map>
28 #include <set>
29 #include <vector>
30
31 #include <glib.h>
32 extern "C" {
33 #include <unistd.h>
34 #include <glib/gi18n.h>
35 }
36
37 #include <pan/general/debug.h>
38 #include <pan/general/file-util.h>
39 #include <pan/general/log.h>
40 #include <pan/general/macros.h>
41 #include <pan/general/messages.h>
42 #include <pan/general/time-elapsed.h>
43
44 #include <pan/usenet-utils/numbers.h>
45
46 #include "data-impl.h"
47
48 using namespace pan;
49
50 /**
51 ***
52 **/
53
54 namespace
55 {
56 bool
parse_newsrc_line(const StringView & line,StringView & setme_group_name,bool & setme_subscribed,StringView & setme_number_ranges)57 parse_newsrc_line (const StringView & line,
58 StringView & setme_group_name,
59 bool & setme_subscribed,
60 StringView & setme_number_ranges)
61 {
62 bool ok (false);
63 // since most groups will be unread, it's faster to test the end of the line before calling strpbrk
64 const char * delimiter ((!line.empty() && (line.back()=='!' || line.back()==':')) ? &line.back() : line.strpbrk("!:"));
65 if (delimiter)
66 {
67 StringView myline (line);
68
69 setme_subscribed = *delimiter == ':';
70
71 myline.substr (NULL, delimiter, setme_group_name);
72 setme_group_name.trim ();
73
74 myline.substr (delimiter+1, NULL, setme_number_ranges);
75 setme_number_ranges.trim ();
76
77 ok = !setme_group_name.empty();
78 }
79
80 return ok;
81 }
82 }
83
84 // detect std::lib
85 #include <ciso646>
86 #ifdef _LIBCPP_VERSION
87 // using libc++
88 #include <algorithm>
89 #else
90 // using libstdc++
91 #include <ext/algorithm>
92 #endif
93
94 void
load_newsrc(const Quark & server,LineReader * in,alpha_groups_t & sub,alpha_groups_t & unsub)95 DataImpl :: load_newsrc (const Quark & server,
96 LineReader * in,
97 alpha_groups_t & sub,
98 alpha_groups_t & unsub)
99 {
100 Server * s = find_server (server);
101 if (!s) {
102 Log::add_err_va (_("Skipping newsrc file for server “%s”"), server.c_str());
103 return;
104 }
105
106 std::vector<Quark>& groups (s->groups.get_container());
107
108 AlphabeticalQuarkOrdering o;
109 StringView line, name, numbers;
110 bool needs_sort (false);
111 Quark prev;
112 std::vector<Quark> tmp_sub, tmp_unsub;
113 tmp_sub.reserve (1000);
114 tmp_unsub.reserve (120000); // giganews has ~100k; anyone have more?
115 while (!in->fail() && in->getline (line))
116 {
117 bool subscribed;
118 if (parse_newsrc_line (line, name, subscribed, numbers))
119 {
120 const Quark& group (name);
121
122 needs_sort |= (!prev.empty() && !o(prev,group));
123 groups.push_back (group);
124
125 if (subscribed)
126 tmp_sub.push_back (group);
127 else
128 tmp_unsub.push_back (group);
129
130 if (!numbers.empty())
131 _read_groups[group][server]._read.mark_str (numbers);
132
133 prev = group;
134 }
135 }
136
137 // sub += tmp_sub
138 if (needs_sort)
139 std::sort (tmp_sub.begin(), tmp_sub.end(), AlphabeticalQuarkOrdering());
140 if (sub.empty())
141 sub.get_container().swap (tmp_sub);
142 else {
143 std::vector<Quark> tmp;
144 tmp.reserve (sub.size() + tmp_sub.size());
145 std::set_union (sub.begin(), sub.end(),
146 tmp_sub.begin(), tmp_sub.end(),
147 std::inserter (tmp, tmp.begin()), o);
148 sub.get_container().swap (tmp);
149 }
150
151 // unsub += tmp_unsub
152 if (needs_sort)
153 std::sort (tmp_unsub.begin(), tmp_unsub.end(), AlphabeticalQuarkOrdering());
154 if (unsub.empty())
155 unsub.get_container().swap (tmp_unsub);
156 else {
157 std::vector<Quark> tmp;
158 tmp.reserve (unsub.size() + tmp_unsub.size());
159 std::set_union (unsub.begin(), unsub.end(),
160 tmp_unsub.begin(), tmp_unsub.end(),
161 std::inserter (tmp, tmp.begin()), o);
162 unsub.get_container().swap (tmp);
163 }
164 }
165
166 void
load_newsrc_files(const DataIO & data_io)167 DataImpl :: load_newsrc_files (const DataIO& data_io)
168 {
169 alpha_groups_t& s(_subscribed);
170 alpha_groups_t& u(_unsubscribed);
171
172 s.clear ();
173 u.clear ();
174
175 foreach_const (servers_t, _servers, sit) {
176 const Quark key (sit->first);
177 const std::string filename = file::absolute_fn("", sit->second.newsrc_filename);
178 if (file::file_exists (filename.c_str())) {
179 LineReader * in (data_io.read_file (filename));
180 load_newsrc (key, in, s, u);
181 delete in;
182 }
183 }
184
185 // remove duplicates
186 s.erase (std::unique(s.begin(), s.end()), s.end());
187 u.erase (std::unique(u.begin(), u.end()), u.end());
188
189 // unsub -= sub
190 AlphabeticalQuarkOrdering o;
191 std::vector<Quark> tmp;
192 tmp.reserve (u.size());
193 std::set_difference (u.begin(), u.end(), s.begin(), s.end(), inserter (tmp, tmp.begin()), o);
194 u.get_container().swap (tmp);
195
196 // shrink-to-fit
197 alpha_groups_t (s).swap(s);
198 alpha_groups_t (u).swap(u);
199 fire_grouplist_rebuilt ();
200 }
201
202 void
save_newsrc_files(DataIO & data_io) const203 DataImpl :: save_newsrc_files (DataIO& data_io) const
204 {
205 if (newsrc_autosave_id) {
206 g_source_remove( newsrc_autosave_id );
207 newsrc_autosave_id = 0;
208 }
209
210 if (_unit_test)
211 return;
212
213 // overly-complex optimization: both sit->second.groups and _subscribed
214 // are both ordered by AlphabeticalQuarkOrdering.
215 // Where N==sit->second.groups.size() and M==_subscribed.size(),
216 // "bool subscribed = _subscribed.count (group)" is N*log(M),
217 // but a sorted set comparison is M+N comparisons.
218 AlphabeticalQuarkOrdering o;
219
220 // save all the servers' newsrc files
221 foreach_const (servers_t, _servers, sit)
222 {
223 const Quark& server (sit->first);
224
225 // write this server's newsrc
226 const std::string filename = file::absolute_fn("", sit->second.newsrc_filename);
227
228 std::ostream& out (*data_io.write_file (filename));
229 std::string newsrc_string;
230 alpha_groups_t::const_iterator sub_it (_subscribed.begin());
231 const alpha_groups_t::const_iterator sub_end(_subscribed.end());
232 foreach_const (Server::groups_t, sit->second.groups, git) // for the groups in this server...
233 {
234 const Quark& group (*git);
235
236 //const bool subscribed (_subscribed.count (group));
237 while (sub_it!=sub_end && o (*sub_it, group)) ++sub_it; // see comment for 'o' above
238 const bool subscribed (sub_it!=sub_end && *sub_it==group);
239 out << group;
240 out.put (subscribed ? ':' : '!');
241
242 // if the group's been read, save its read number ranges...
243 const ReadGroup::Server * rgs (find_read_group_server (group, server));
244 if (rgs != 0) {
245 newsrc_string.clear ();
246 rgs->_read.to_string (newsrc_string);
247 if (!newsrc_string.empty()) {
248 out.put (' ');
249 out << newsrc_string;
250 }
251 }
252
253 out.put ('\n');
254 }
255 data_io.write_done (&out);
256 }
257 }
258
259 /***
260 ****
261 ***/
262
263 void
load_group_permissions(const DataIO & data_io)264 DataImpl :: load_group_permissions (const DataIO& data_io)
265 {
266 std::vector<Quark> m, n;
267
268 LineReader * in (data_io.read_group_permissions ());
269 StringView s, line;
270 while (in && !in->fail() && in->getline(line))
271 {
272 if (line.len && *line.str=='#')
273 continue;
274
275 else if (!line.pop_last_token (s, ':') || !s.len || (*s.str!='y' && *s.str!='n' && *s.str!='m')) {
276 std::cerr << LINE_ID << " Group permissions: Can't parse line `" << line << std::endl;
277 continue;
278 }
279
280 const Quark group (line);
281 const char ch = *s.str;
282
283 if (ch == 'm')
284 m.push_back (group);
285 else if (ch == 'n')
286 n.push_back (group);
287 }
288
289 std::sort (m.begin(), m.end());
290 m.erase (std::unique(m.begin(), m.end()), m.end());
291 _moderated.get_container().swap (m);
292
293 std::sort (n.begin(), n.end());
294 n.erase (std::unique(n.begin(), n.end()), n.end());
295 _nopost.get_container().swap (n);
296
297 delete in;
298 }
299
300 void
save_group_permissions(DataIO & data_io) const301 DataImpl :: save_group_permissions (DataIO& data_io) const
302 {
303 if (_unit_test)
304 return;
305
306 std::ostream& out (*data_io.write_group_permissions ());
307
308 typedef std::map<Quark, char, AlphabeticalQuarkOrdering> tmp_t;
309 tmp_t tmp;
310 foreach_const (groups_t, _moderated, it) tmp[*it] = 'm';
311 foreach_const (groups_t, _nopost, it) tmp[*it] = 'n';
312
313 out << "# Permissions: y means posting ok; n means posting not okay; m means moderated.\n"
314 << "# Since almost all groups allow posting, Pan assumes that as the default.\n"
315 << "# Only moderated or no-posting groups are listed here.\n";
316 foreach_const (tmp_t, tmp, it) {
317 out << it->first;
318 out.put (':');
319 out << it->second;
320 out.put ('\n');
321 }
322 data_io.write_done (&out);
323 }
324
325 /***
326 ****
327 ***/
328
329 void
ensure_descriptions_are_loaded() const330 DataImpl :: ensure_descriptions_are_loaded () const
331 {
332 if (!_descriptions_loaded)
333 {
334 _descriptions_loaded = true;
335 load_group_descriptions (*_data_io);
336 }
337 }
338
339 void
load_group_descriptions(const DataIO & data_io) const340 DataImpl :: load_group_descriptions (const DataIO& data_io) const
341 {
342 _descriptions.clear ();
343
344 LineReader * in (data_io.read_group_descriptions ());
345 StringView s, group;
346 while (in && !in->fail() && in->getline(group))
347 if (group.pop_last_token (s, ':'))
348 _descriptions[group].assign (s.str, s.len);
349 delete in;
350 }
351
352 void
load_group_xovers(const DataIO & data_io)353 DataImpl :: load_group_xovers (const DataIO& data_io)
354 {
355 LineReader * in (data_io.read_group_xovers ());
356 if (in && !in->fail())
357 {
358 StringView line;
359 StringView groupname, total, unread;
360 StringView xover, servername, low;
361
362 // walk through the groups line-by-line...
363 while (in->getline (line))
364 {
365 // skip comments and blank lines
366 line.trim();
367 if (line.empty() || *line.str=='#')
368 continue;
369
370 if (line.pop_token(groupname) && line.pop_token(total) && line.pop_token(unread))
371 {
372 ReadGroup& g (_read_groups[groupname]);
373 g._article_count = strtoul (total.str, NULL, 10);
374 g._unread_count = strtoul (unread.str, NULL, 10);
375
376 while (line.pop_token (xover))
377 if (xover.pop_token(servername,':'))
378 g[servername]._xover_high = g_ascii_strtoull (xover.str, NULL, 10);
379 }
380 }
381 }
382 delete in;
383 }
384
385 void
save_group_descriptions(DataIO & data_io) const386 DataImpl :: save_group_descriptions (DataIO& data_io) const
387 {
388 if (_unit_test)
389 return;
390
391 assert (_descriptions_loaded);
392
393 typedef std::map<Quark, std::string, AlphabeticalQuarkOrdering> tmp_t;
394 tmp_t tmp;
395 foreach_const (descriptions_t, _descriptions, it)
396 tmp[it->first] = it->second;
397
398 std::ostream& out (*data_io.write_group_descriptions ());
399 foreach_const (tmp_t, tmp, it) {
400 out << it->first;
401 out.put (':');
402 out << it->second;
403 out.put ('\n');
404 }
405 data_io.write_done (&out);
406 }
407
408 namespace
409 {
410 typedef std::map < pan::Quark, std::string > quark_to_symbol_t;
411
412 struct QuarkToSymbol {
413 const quark_to_symbol_t& _map;
~QuarkToSymbol__anon3fde3de10211::QuarkToSymbol414 virtual ~QuarkToSymbol () {}
QuarkToSymbol__anon3fde3de10211::QuarkToSymbol415 QuarkToSymbol (const quark_to_symbol_t& map): _map(map) { }
operator ()__anon3fde3de10211::QuarkToSymbol416 virtual std::string operator() (const Quark& quark) const {
417 quark_to_symbol_t::const_iterator it (_map.find (quark));
418 return it!=_map.end() ? it->second : quark.to_string();
419 }
420 };
421 }
422
423 void
save_group_xovers(DataIO & data_io) const424 DataImpl :: save_group_xovers (DataIO& data_io) const
425 {
426 if (_unit_test)
427 return;
428
429 std::ostream& out (*data_io.write_group_xovers ());
430
431 // find the set of groups that have had an xover
432 typedef std::set<Quark, AlphabeticalQuarkOrdering> xgroups_t;
433 xgroups_t xgroups;
434 foreach_const (read_groups_t, _read_groups, git) {
435 const ReadGroup& group (git->second);
436 bool is_xgroup (group._article_count || group._unread_count);
437 if (!is_xgroup)
438 foreach_const (ReadGroup::servers_t, group._servers, sit)
439 if ((is_xgroup = (sit->second._xover_high!=0)))
440 break;
441 if (is_xgroup)
442 xgroups.insert (git->first);
443 }
444
445 out << "# groupname totalArticleCount unreadArticleCount [server:latestXoverHigh]*\n";
446
447 // foreach xgroup
448 foreach_const (xgroups_t, xgroups, it)
449 {
450 const Quark groupname (*it);
451 const ReadGroup& g (*find_read_group (groupname));
452 out << groupname;
453 out.put (' ');
454 out << g._article_count;
455 out.put (' ');
456 out << g._unread_count;
457 foreach_const (ReadGroup::servers_t, g._servers, i) {
458 if (i->second._xover_high) {
459 out.put (' ');
460 out << i->first;
461 out.put (':');
462 out << i->second._xover_high;
463 }
464 }
465 out.put ('\n');
466 }
467
468 data_io.write_done (&out);
469 }
470
471 /****
472 *****
473 ****/
474
475 uint64_t
get_xover_high(const Quark & groupname,const Quark & servername) const476 DataImpl :: get_xover_high (const Quark & groupname,
477 const Quark & servername) const
478 {
479 uint64_t high (0ul);
480 const ReadGroup::Server * rgs (find_read_group_server (groupname, servername));
481 if (rgs)
482 high = rgs->_xover_high;
483 return high;
484 }
485
486 void
set_xover_high(const Quark & group,const Quark & server,const uint64_t high)487 DataImpl :: set_xover_high (const Quark & group,
488 const Quark & server,
489 const uint64_t high)
490 {
491 //std::cerr << LINE_ID << "setting " << get_server_address(server) << ':' << group << " xover high to " << high << std::endl;
492 ReadGroup::Server& rgs (_read_groups[group][server]);
493 rgs._xover_high = high;
494 }
495
496 void
add_groups(const Quark & server,const NewGroup * newgroups,size_t count)497 DataImpl :: add_groups (const Quark & server,
498 const NewGroup * newgroups,
499 size_t count)
500 {
501 ensure_descriptions_are_loaded ();
502
503 Server * s (find_server (server));
504 assert (s != 0);
505
506 {
507 AlphabeticalQuarkOrdering o;
508 Server::groups_t groups;
509 std::vector<Quark> tmp;
510
511 // make a groups_t from the added groups,
512 // and merge it with the server's list of groups
513 groups.get_container().reserve (count);
514 for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it)
515 groups.get_container().push_back (it->group);
516 groups.sort ();
517 std::set_union (s->groups.begin(), s->groups.end(),
518 groups.begin(), groups.end(),
519 std::back_inserter (tmp), o);
520 tmp.erase (std::unique(tmp.begin(), tmp.end()), tmp.end());
521 s->groups.get_container().swap (tmp);
522
523 // make a groups_t of groups we didn't already have,
524 // and merge it with _unsubscribed (i.e., groups we haven't seen before become unsubscribed)
525 groups.clear ();
526 for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it)
527 if (!_subscribed.count (it->group))
528 groups.get_container().push_back (it->group);
529 groups.sort ();
530 tmp.clear ();
531 std::set_union (groups.begin(), groups.end(),
532 _unsubscribed.begin(), _unsubscribed.end(),
533 std::back_inserter (tmp), o);
534 tmp.erase (std::unique(tmp.begin(), tmp.end()), tmp.end());
535 _unsubscribed.get_container().swap (tmp);
536 }
537
538 {
539 // build lists of the groups that should and should not be in _moderated and _nopost.t
540 // this is pretty cumbersome, but since these lists almost never change it's still
541 // a worthwhile tradeoff to get the speed/memory wins of a sorted_vector
542 groups_t mod, notmod, post, nopost, tmp;
543 for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it) {
544 if (it->permission == 'm') mod.get_container().push_back (it->group);
545 if (it->permission != 'm') notmod.get_container().push_back (it->group);
546 if (it->permission == 'n') nopost.get_container().push_back (it->group);
547 if (it->permission != 'n') post.get_container().push_back (it->group);
548 }
549 mod.sort (); notmod.sort ();
550 post.sort (); nopost.sort ();
551
552 // _moderated -= notmod
553 tmp.clear ();
554 std::set_difference (_moderated.begin(), _moderated.end(), notmod.begin(), notmod.end(), inserter (tmp, tmp.begin()));
555 _moderated.swap (tmp);
556 // _moderated += mod
557 tmp.clear ();
558 std::set_union (_moderated.begin(), _moderated.end(), mod.begin(), mod.end(), inserter (tmp, tmp.begin()));
559 _moderated.swap (tmp);
560
561 // _nopost -= post
562 tmp.clear ();
563 std::set_difference (_nopost.begin(), _nopost.end(), post.begin(), post.end(), inserter (tmp, tmp.begin()));
564 _nopost.swap (tmp);
565 // _nopost += nopost
566 tmp.clear ();
567 std::set_union (_nopost.begin(), _nopost.end(), nopost.begin(), nopost.end(), inserter (tmp, tmp.begin()));
568 _nopost.swap (tmp);
569 }
570
571 // keep any descriptions worth keeping that we don't already have...
572 for (const NewGroup *it=newgroups, *end=newgroups+count; it!=end; ++it) {
573 const NewGroup& ng (*it);
574 if (!ng.description.empty() && ng.description!="?")
575 _descriptions[ng.group] = ng.description;
576 }
577
578 save_group_descriptions (*_data_io);
579 save_group_permissions (*_data_io);
580 fire_grouplist_rebuilt ();
581 }
582
583 void
mark_group_read(const Quark & groupname)584 DataImpl :: mark_group_read (const Quark& groupname)
585 {
586 ReadGroup * rg (find_read_group (groupname));
587 if (rg != 0) {
588 foreach (ReadGroup::servers_t, rg->_servers, it) {
589 //std::cerr << LINE_ID << " marking read range [0..." << it->second._xover_high << "] in " << get_server_address(it->first) << ']' << std::endl;
590 it->second._read.mark_range (0, it->second._xover_high, true);
591 }
592 rg->_unread_count = 0;
593 save_group_xovers (*_data_io);
594 fire_group_read (groupname);
595 }
596 }
597
598 void
set_group_subscribed(const Quark & group,bool subscribed)599 DataImpl :: set_group_subscribed (const Quark& group, bool subscribed)
600 {
601 if (subscribed) {
602 _unsubscribed.erase (group);
603 _subscribed.insert (group);
604 } else {
605 _subscribed.erase (group);
606 _unsubscribed.insert (group);
607 }
608
609 fire_group_subscribe (group, subscribed);
610 }
611
612 const std::string&
get_group_description(const Quark & group) const613 DataImpl :: get_group_description (const Quark& group) const
614 {
615 ensure_descriptions_are_loaded ();
616 static const std::string nil;
617 descriptions_t::const_iterator it (_descriptions.find (group));
618 return it == _descriptions.end() ? nil : it->second;
619 }
620
621 void
get_group_counts(const Quark & groupname,unsigned long & unread_count,unsigned long & article_count) const622 DataImpl :: get_group_counts (const Quark & groupname,
623 unsigned long & unread_count,
624 unsigned long & article_count) const
625 {
626 const ReadGroup * g (find_read_group (groupname));
627 if (!g)
628 unread_count = article_count = 0ul;
629 else {
630 unread_count = g->_unread_count;
631 article_count = g->_article_count;
632 }
633 }
634
635 char
get_group_permission(const Quark & group) const636 DataImpl :: get_group_permission (const Quark & group) const
637 {
638 if (_moderated.count (group))
639 return 'm';
640 else if (_nopost.count (group))
641 return 'n';
642 else
643 return 'y';
644 }
645
646
647 void
group_get_servers(const Quark & groupname,quarks_t & addme) const648 DataImpl :: group_get_servers (const Quark& groupname, quarks_t& addme) const
649 {
650 foreach_const (servers_t, _servers, it)
651 if (it->second.groups.count (groupname))
652 addme.insert (it->first);
653 }
654
655 void
server_get_groups(const Quark & servername,quarks_t & addme) const656 DataImpl :: server_get_groups (const Quark& servername, quarks_t& addme) const
657 {
658 const Server * server (find_server (servername));
659 if (server)
660 addme.insert (server->groups.begin(), server->groups.end());
661 }
662
663 void
get_subscribed_groups(std::vector<Quark> & setme) const664 DataImpl :: get_subscribed_groups (std::vector<Quark>& setme) const
665 {
666 setme.assign (_subscribed.begin(), _subscribed.end());
667 }
668
669 void
get_other_groups(std::vector<Quark> & setme) const670 DataImpl :: get_other_groups (std::vector<Quark>& setme) const
671 {
672 setme.assign (_unsubscribed.begin(), _unsubscribed.end());
673 }
674