Logo Search packages:      
Sourcecode: pan version File versions  Download package

group.c

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Pan - A Newsreader for Gtk+
 * Copyright (C) 2002  Charles Kerr <charles@rebelbase.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <config.h>

#include <limits.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

#include <glib/gthread.h>

#include <gmime/gmime-stream-file.h>

#include <pan/base/acache.h>
#include <pan/base/article-thread.h>
#include <pan/base/base-prefs.h>
#include <pan/base/debug.h>
#include <pan/base/file-headers.h>
#include <pan/base/group.h>
#include <pan/base/log.h>
#include <pan/base/pan-i18n.h>
#include <pan/base/pan-glib-extensions.h>
#include <pan/base/newsrc.h>
#include <pan/base/pan-callback.h>
#include <pan/base/server.h>

static GHashTable* group_get_articles (Group *);
static GPtrArray* group_get_article_array (Group *);

/***
****
****  EVENTS
****
***/
 
/**
***  Groups Removed
**/
 
PanCallback*
group_get_groups_changed_callback (void)
{
      static PanCallback * cb = NULL;
      if (cb==NULL) cb = pan_callback_new ();
      return cb;
}

static void
fire_groups_changed (Group ** groups, int group_qty, guint change_type)
{
      struct GroupChangeEvent e;

      /* sanity clause */
      g_return_if_fail (groups!=NULL);
      g_return_if_fail (group_qty>0);
      g_return_if_fail (group_is_valid(groups[0]));

      /* fire the event */
      e.groups = groups;
      e.group_qty = group_qty;
      e.change_type = change_type;
      pan_callback_call (group_get_groups_changed_callback(), &e, NULL);
}

PanCallback*
group_get_articles_added_callback (void)
{
      static PanCallback * cb = NULL;
      if (cb==NULL) cb = pan_callback_new ();
      return cb;
}

PanCallback*
group_get_articles_removed_callback (void)
{
      static PanCallback * cb = NULL;
      if (cb==NULL) cb = pan_callback_new ();
      return cb;
}

/***
****
***/

Group*
group_new (Server * server, const char * name)
{
      Group * group;
      
      g_return_val_if_fail (server_is_valid (server), NULL);
      pan_warn_if_fail (is_nonempty_string(name));

      group = server_alloc_new_group (server);
      group_constructor (group, name);
      return group;
}

void
group_constructor (Group                * g,
               const char           * name)
{
      pan_warn_if_fail (g!=NULL);
      pan_warn_if_fail (is_nonempty_string(name));

      g->article_mutex = NULL;
      g->loaded_since_last_fetch = FALSE;
      g->articles_dirty = FALSE;
      g->flags = (gint8)0;
      g->old_sort_style = (gint8)0;
      g->new_sort_style = (gint8)0;
      g->filter_bits = ~((guint16)STATE_FILTER_SCORE_IGNORED);
      g->filter_show = FILTER_SHOW_MATCHES;
      g->article_qty = (gint32)0;
      g->article_read_qty = (gint32)0;
      g->article_low = (gulong)0;
      g->article_high_old = (gulong)0;
      g->permission = '?';
      g->article_high = (gulong)0;
      g->name = PSTRING_INIT;
      g->filter_name = NULL;
      g->identity_name = NULL;
      g->description = NULL;
      g->download_dir = NULL;
      g->default_charset = NULL;

      g->_articles_refcount = 0;
      g->_article_chunk = NULL;
      g->_newsrc = NULL;
      g->_purged = NULL;
      g->_articles = NULL;
      g->_deleted_articles = NULL;

      pstring_set (&g->name, name, -1);
}

static GMutex*
group_get_article_mutex (Group * g)
{
       g_return_val_if_fail (group_is_valid(g), NULL);

       if (g->article_mutex == NULL)
               g->article_mutex = g_mutex_new ();

       return g->article_mutex;
}

static void
destroy_value_article_ghfunc (gpointer key, gpointer value, gpointer user_data)
{
      article_destructor ((Article*)value);
}

static void
blow_article_memory (Group * g)
{
      g_return_if_fail (g!=NULL);
      pan_warn_if_fail (g->_articles_refcount >= 0);

      if (g->_articles != NULL) {
            g_hash_table_foreach (g->_articles, destroy_value_article_ghfunc, NULL);
            g_hash_table_destroy (g->_articles);
            g->_articles = NULL;
      }

      if (g->_deleted_articles != NULL) {
            pan_g_ptr_array_foreach (g->_deleted_articles, (GFunc)article_destructor, NULL);
            g_ptr_array_free (g->_deleted_articles, TRUE);
            g->_deleted_articles = NULL;
      }

      if (g->_article_chunk != NULL) {
            memchunk_destroy (g->_article_chunk);
            g->_article_chunk = NULL;
      }
}

void
group_destructor (Group * o)
{
      Group * g = GROUP(o);

      g_return_if_fail (o!=NULL);

      debug1 (DEBUG_PAN_OBJECT, "group_destructor: %p", g);

      /* strings */
      pstring_clear (&g->name);
      replace_gstr (&g->description, NULL);
      replace_gstr (&g->download_dir, NULL);
      replace_gstr (&g->default_charset, NULL);
      replace_gstr (&g->filter_name, NULL);
      replace_gstr (&g->identity_name, NULL);

      /* newsrc */
      if (g->_newsrc != NULL)
            pan_object_unref (PAN_OBJECT(g->_newsrc));
      if (g->_purged != NULL)
            pan_object_unref (PAN_OBJECT(g->_purged));

      /* articles */
      if (g->_articles_refcount > 0)
            blow_article_memory (g);
      if (g->article_mutex != NULL) {
            g_mutex_free (g->article_mutex);
            g->article_mutex = NULL;
      }
}

/***
****
****  PUBLIC ACCESSORS / MUTATORS
****
***/

void
group_set_new (Group * group, gboolean is_new)
{
      g_return_if_fail (group != NULL);

      if (is_new)
            group_set_flags (group, group->flags|GROUP_NEW);
      else
            group_set_flags (group, group->flags&~GROUP_NEW);
}

gboolean 
group_is_new (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), FALSE);

      return (group->flags & GROUP_NEW) ? 1 : 0;
}

void
groups_set_subscribed (Group ** groups, int qty, gboolean subscribed)
{
      Group ** changed;
      Server * server = NULL;
      const ServerGroupsType type = SERVER_GROUPS_SUBSCRIBED|SERVER_GROUPS_UNSUBSCRIBED;
      int changed_qty = 0;
      int i;
      debug_enter ("groups_set_subscribed");

      /* sanity clause */
      g_return_if_fail (groups != NULL);
      g_return_if_fail (qty > 0);

      /* change the subscription state of any group that needs it */
      changed = g_new (Group*, qty);
      for (i=0; i<qty; ++i)
      {
            Group * group = groups[i];
            const gboolean is_subscribed = (group->flags & GROUP_SUBSCRIBED) != 0;

            if (is_subscribed != subscribed)
            {
                  if (group->server != NULL)
                  {
                        server = group->server;
                        server_set_group_type_dirty (server, type);
                  }

                  if (subscribed)
                        group_set_flags (group, group->flags | GROUP_SUBSCRIBED);
                  else
                        group_set_flags (group, group->flags & ~GROUP_SUBSCRIBED);

                  changed[changed_qty++] = group;
            }
      }

      if (server != NULL)
            server_save_grouplist_if_dirty (server, NULL);

      if (changed_qty > 0)
            fire_groups_changed (changed, changed_qty, GROUP_CHANGED_SUBSCRIPTION_STATE);

      g_free (changed);
      debug_exit ("groups_set_subscribed");
}

gboolean 
group_is_subscribed (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), FALSE);

      return (group->flags & GROUP_SUBSCRIBED) ? 1 : 0;
}

void
group_set_dirty (Group * group)
{
      g_return_if_fail (group_is_valid(group));

      server_set_group_type_dirty (group->server, server_get_group_type (group));
}

void
group_set_download_dir (Group * group, const char* download_dir)
{
      g_return_if_fail (group_is_valid(group));

      replace_gstr (&group->download_dir, g_strdup(download_dir));
}

void
group_set_default_charset (Group * group,
                           const char * default_charset)
{
      debug_enter ("group_set_default_charset");

      g_return_if_fail (group_is_valid(group));

      if (!is_nonempty_string (default_charset))
            default_charset = NULL;

      replace_gstr (&group->default_charset, g_strdup(default_charset));

      group_set_dirty (group);

      debug_exit ("group_set_default_charset");
}

const char*
group_get_default_charset (const Group * group)
{
      const char * retval = NULL;

      g_return_val_if_fail (group_is_valid(group), retval);

      retval = group->default_charset;

      if (!is_nonempty_string (retval))
            retval = get_charset_from_locale ();

      return retval;
}

void
group_set_flags (Group * group, guint flags)
{
      g_return_if_fail (group_is_valid(group));

      group->flags = flags;
      group_set_dirty (group);
}

void
group_set_filter (Group         * group,
                  guint           filter_bits,
                  gulong          filter_show,
                  const char    * filter_name)
{
      g_return_if_fail (group_is_valid(group));

      group->filter_bits = filter_bits;
      group->filter_show = filter_show;
      replace_gstr (&group->filter_name, g_strdup(filter_name));

      group_set_dirty (group);
}

void
group_set_identity (Group       * group,
                    const char  * identity_name)
{
      g_return_if_fail (group!=NULL);

      replace_gstr (&group->identity_name, g_strdup (identity_name));

      group_set_dirty (group);
}

void
group_set_sort_style (Group * group, int sort_style)
{
      g_return_if_fail (group != NULL);

      if (abs(sort_style) != abs(group->new_sort_style))
            group->old_sort_style = group->new_sort_style;
      group->new_sort_style = sort_style;
      group_set_dirty (group);
}

static Newsrc* group_get_newsrc (Group *);
static Newsrc* group_get_purged (Group *);

void
group_set_article_range (Group    * g,
                         gulong     article_low,
                         gulong     article_high)
{
      g_return_if_fail (group_is_valid(g));

      g->article_low = article_low;
      g->article_high = article_high;
      newsrc_set_group_range (group_get_newsrc(g), g->article_low, g->article_high);
      newsrc_set_group_range (group_get_purged(g), g->article_low, g->article_high);

      group_set_dirty (g);
}

void
group_get_article_range (const Group   * g,
                         gulong        * article_low,
                         gulong        * article_high)
{
      g_return_if_fail (group_is_valid(g));
      g_return_if_fail (article_low!=NULL);
      g_return_if_fail (article_high!=NULL);

      *article_low = g->article_low;
      *article_high = g->article_high;
}


void
group_mark_new_article_number (Group         * group,
                               gulong          everything_above_this_is_new)
{
      g_return_if_fail (group_is_valid(group));

      group->article_high_old = everything_above_this_is_new;
}

static guint
group_set_article_qty_impl (Group * group, int article_qty)
{
      guint retval = 0;
      debug_enter ("group_set_article_qty_impl");

      g_return_val_if_fail (group_is_valid(group), retval);

      if (article_qty != group->article_qty)
      {
            group->article_qty = article_qty;
            group_set_dirty (group);
            retval |= GROUP_CHANGED_ARTICLE_QTY;
      }

      debug_exit ("group_set_article_qty_impl");
      return retval;
}

void
group_set_article_qty (Group * group, int article_qty)
{
      guint change_type;
      debug_enter ("group_set_article_qty");

      change_type = group_set_article_qty_impl (group, article_qty);
      if (change_type)
            fire_groups_changed (&group, 1, change_type);

      debug_exit ("group_set_article_qty");
}

static guint
group_set_article_read_qty_impl (Group * group, int article_read_qty)
{
      guint retval = 0;
      debug_enter ("group_set_article_read_qty_impl");

      g_return_val_if_fail (group_is_valid(group), retval);

      article_read_qty = MAX (article_read_qty, 0);
      if (article_read_qty != group->article_read_qty)
      {
            group->article_read_qty = article_read_qty;
            group_set_dirty (group);
            retval |= GROUP_CHANGED_ARTICLE_READ_QTY;
      }

      debug_exit ("group_set_article_read_qty_impl");
      return retval;
}

void
group_set_article_read_qty (Group * group, int article_read_qty)
{
      guint change_type;
      debug_enter ("group_set_article_read_qty");

      change_type = group_set_article_read_qty_impl (group, article_read_qty);
      if (change_type)
            fire_groups_changed (&group, 1, change_type);

      debug_exit ("group_set_article_read_qty");
}
void
group_inc_article_read_qty (Group * group, int inc)
{
      debug_enter ("group_inc_article_read_qty");

      g_return_if_fail (group_is_valid(group));

      if (inc != 0)
            group_set_article_read_qty (group, group->article_read_qty + inc);

      debug_exit ("group_inc_article_read_qty");
}

static void
group_mark_articles_read (Group * group, Article ** articles, guint article_qty, gpointer user_data)
{
      if (article_qty > 0u)
            articles_set_read (articles, article_qty, TRUE);
}

void
group_mark_all_read (Group * g, gboolean read)
{
      Newsrc * newsrc;
      debug_enter ("group_mark_all_read");

      /* sanity clause */
      g_return_if_fail (group_is_valid(g));

      /* ensure the articles are loaded */
      group_ref_articles (g, NULL);

      /* mark all the articles as read.  use articles_set_read for this 
         because that updates all the unread_children fields, marks 
         crossposts as read, etc... */

      g_mutex_lock (group_get_article_mutex (g));
      group_article_forall (g, group_mark_articles_read, NULL);
      g_mutex_unlock (group_get_article_mutex (g));

      /**
       * Wipe the entire newsgroup's article range as read.
       * This reduces the group's newsrc to a single range "low-high"
       * instead of thousands of smaller ranges.
       */
      newsrc = group_get_newsrc (g);
      newsrc_set_group_range (newsrc, g->article_low, g->article_high);
      newsrc_mark_all (newsrc, read);

      /* update group counts */
      group_set_article_read_qty (g, (read ? g->article_qty : 0));

      /* unref the articles */
      group_unref_articles (g, NULL);

      debug_exit ("group_mark_all_read");
}

gboolean
group_is_read_only (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), FALSE);

      return group->permission == 'n';
}

gboolean
group_is_moderated (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), FALSE);

      return group->permission == 'm';
}

gboolean
group_is_valid (const Group * group)
{
      g_return_val_if_fail (group!=NULL, FALSE);
      g_return_val_if_fail (server_is_valid (group->server), FALSE);
      g_return_val_if_fail (pstring_is_set (&group->name), FALSE);

      return TRUE;
}

gboolean
group_is_group (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), FALSE);

      return group->flags & GROUP_FOLDERS ? 0 : 1;
}

gboolean
group_is_folder (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), FALSE);

      return group->flags & GROUP_FOLDERS ? 1 : 0;
}

void
group_set_is_folder (Group * group, gboolean folder)
{
      g_return_if_fail (group_is_valid(group));

      if (!folder != !group_is_folder(group))
      {
            if (folder)
                  group->flags |= GROUP_FOLDERS;
            else
                  group->flags &= ~GROUP_FOLDERS;
            group_set_dirty (group);
      }
}

const char*
group_get_acache_key (const Group * group)
{
      const char * retval = ACACHE_DEFAULT_KEY;

      g_return_val_if_fail (group_is_valid(group), retval);

      if (group_is_folder (group))
            retval = group_get_name (group);

      return retval;
}


/***
****
****  DELETE GROUP
****
***/

void
group_empty (Group * group, gboolean clear_counts)
{
      guint change_type = 0;

      /* sanity clause */
      g_return_if_fail (group_is_valid(group));

      /* remove the articles from disk */
      file_headers_destroy (group);

      /* remove the articles from memory */
      group_article_forall (group, (GroupArticleFunc)group_remove_articles, NULL);

      /* reset the counters */
      change_type |= group_set_article_qty_impl (group, 0);
      change_type |= group_set_article_read_qty_impl (group, 0);
      newsrc_mark_all (group_get_purged(group), FALSE);

      /* if user wants to clear the range, do that too */
      if (clear_counts)
            group_set_article_range (group, 0, 0);

      if (change_type)
            fire_groups_changed (&group, 1, change_type);
}

/***
****
****   FOLDERS
****
***/

void
folder_add_message (Group            * folder,
                  GMimeMessage     * message)
{
      gboolean is_new;
      char * text;
      PString message_id;

      g_return_if_fail (group_is_valid(folder));
      g_return_if_fail (group_is_folder(folder));
      g_return_if_fail (GMIME_IS_MESSAGE(message));

      /* get the message-id */
      message_id = pstring_shallow (g_mime_message_get_message_id(message), -1);

      /* do we already have this message? */
      is_new = acache_has_message (group_get_acache_key(folder), &message_id);

      /* write the message */
      text = g_mime_message_to_string (message);
      acache_set_message (folder->name.str, &message_id, text, strlen(text));

      /* update articles */
      if (is_new)
      {
            if (group_ref_articles_if_loaded (folder))
            {
                  Article * a = article_new (folder);
                  article_set_from_g_mime_message (a, message);

                  if (article_is_valid (a))
                  {
                        GPtrArray * articles = g_ptr_array_new ();
                        g_ptr_array_add (articles, a);
                        group_add_articles (folder, articles, NULL, NULL, NULL);
                        g_ptr_array_free (articles, TRUE);
                  }

                  group_unref_articles (folder, NULL);
            }
            else
            {
                  group_set_article_qty (folder, folder->article_qty+1);
            }
      }

      /* cleanup */
      g_free (text);
}

void
folder_remove_message  (Group         * folder,
                        GMimeMessage  * message)
{
      PString message_id;
      const PString * pmessage_id;

      /* sanity clause */
      g_return_if_fail (group_is_valid (folder));
      g_return_if_fail (group_is_folder (folder));
      g_return_if_fail (GMIME_IS_MESSAGE(message));

      /* get the message-id */
      message_id = pstring_shallow (g_mime_message_get_message_id (message), -1);
      pmessage_id = &message_id;

      /* delete the message from our cache */
      acache_expire_messages (group_get_acache_key(folder), &pmessage_id, 1);

      /* delete the message from the group's tables */
      if (!group_ref_articles_if_loaded (folder))
            group_set_article_qty (folder, folder->article_qty-1);
      else {
            Article * a = group_get_article_by_message_id (folder, &message_id);
            if (a != NULL)
                  group_remove_articles (folder, &a, 1u);
            group_unref_articles (folder, NULL);
      }
}


/***
****
***/

static void
group_thread_articles (Group        * group,
                       Article     ** articles,
                       guint          article_qty,
                       gpointer       user_data)
{
      thread_articles (articles, article_qty);
}

static void
group_add_articles_impl (Group             * group,
                         const GPtrArray   * articles,
                         StatusItem        * status,
                         GPtrArray         * used,
                         GPtrArray         * ignored,
                         gboolean            lock_group)
{
      int old_qty;
      int new_read_qty;
      guint i;
      gulong low;
      gulong high;
      GPtrArray * tmp_used;
      GHashTable * ours;
      Newsrc * newsrc;
      Newsrc * purged;
      debug_enter ("group_add_articles");

      /* sanity clause */
      g_return_if_fail (group_is_valid(group));
      g_return_if_fail (group->_articles_refcount>=0);
      g_return_if_fail (articles!=NULL);
      if (!articles->len) return;
      g_return_if_fail (articles->len>0);
      g_return_if_fail (articles_are_valid_in_group ((const Article**)articles->pdata, articles->len));
      g_return_if_fail (ARTICLE(g_ptr_array_index(articles,0))->group == group);

      /**
      ***  Get a handle on our current stats...
      **/

      low = group->article_low;
      high = group->article_high;

      /**
      ***  Now try to add the articles that the client passed in
      **/

      new_read_qty = 0;
      tmp_used = g_ptr_array_sized_new (articles->len);
      newsrc = group_get_newsrc (group);
      purged = group_get_purged (group);

      if (lock_group)
            g_mutex_lock (group_get_article_mutex (group));

      ours = group_get_articles (group);
      old_qty = (int) g_hash_table_size (ours);
      for (i=0; i!=articles->len; ++i)
      {
            Article * a = ARTICLE(g_ptr_array_index(articles,i));
            const PString * message_id = &a->message_id;
            const gboolean is_purged = newsrc_is_article_read(purged, a->number);

            if (!is_purged && g_hash_table_lookup(ours,message_id)==NULL)
            {
                  a->group = group;
                  g_hash_table_insert (ours, &a->message_id, a);
                  g_ptr_array_add (tmp_used, a);

                  if (a->number<low)
                        low  = a->number;
                  if (a->number>high)
                        high = a->number;
                  if (newsrc_is_article_read (newsrc, a->number))
                        ++new_read_qty;
            }
            else if (ignored != NULL)
            {
                  g_ptr_array_add (ignored, a);
            }
      }

      /**
      ***  Rethread the articles
      **/

      if (tmp_used->len != 0)
            group_article_forall (group, group_thread_articles, NULL);

      /**
      ***  Unlock the article mutex
      **/

      if (lock_group) 
            g_mutex_unlock (group_get_article_mutex (group));

      /**
      ***  Now update our stats if articles were added
      **/

      if (tmp_used->len != 0)
      {
            guint change_type = 0u;

            /* maybe update the article range */
            if (high!=group->article_high || low!=group->article_low)
                  group_set_article_range (group, low, high);

            /* update the group article stats */
            change_type |= group_set_article_qty_impl (group, g_hash_table_size(ours));
            if (old_qty != 0)
                  new_read_qty += group->article_read_qty;
            change_type |= group_set_article_read_qty_impl (group, new_read_qty);
            group_set_articles_dirty (group);

            if (change_type)
                  fire_groups_changed (&group, 1, change_type);

            pan_callback_call (group_get_articles_added_callback(), group, tmp_used);
      }

      if (used != NULL)
            pan_g_ptr_array_assign (used, tmp_used->pdata, tmp_used->len);

      /* cleanup */
      g_ptr_array_free (tmp_used, TRUE);
            
      debug_exit ("group_add_articles");
}

static void
group_add_articles_remove_unused_impl (Group            * group,
                                       GPtrArray        * articles,
                                       StatusItem       * status,
                               gboolean           lock)
{
      GPtrArray * ignored;
      GPtrArray * used;

      /* entry assertions */
      g_return_if_fail (group_is_valid(group));
      g_return_if_fail (articles != NULL);
      g_return_if_fail (articles->len != 0);

      /* add the articles */
      ignored  = g_ptr_array_new ();
      used = g_ptr_array_new ();
      group_add_articles_impl (group, articles, status, used, ignored, lock);

      /* destroy unused articles & remove from the incoming array */
      pan_g_ptr_array_assign (articles, used->pdata, used->len);
      pan_g_ptr_array_foreach (ignored, (GFunc)article_destructor, NULL);

      /* cleanup */
      g_ptr_array_free (ignored, TRUE);
      g_ptr_array_free (used, TRUE);
}

void
group_add_articles_remove_unused (Group            * group,
                                  GPtrArray        * articles,
                                  StatusItem       * status)
{
      group_add_articles_remove_unused_impl (group, articles, status, TRUE);
}

void
group_init_articles (Group * group,
                     GPtrArray * articles,
                     StatusItem * status)
{
      group_add_articles_remove_unused_impl (group, articles, status, FALSE);
}

void
group_add_articles (Group             * group,
                    const GPtrArray   * articles,
                    StatusItem        * status,
                    GPtrArray         * used,
                    GPtrArray         * ignored)
{
      group_add_articles_impl (group, articles, status, used, ignored, TRUE);
}

void
group_set_articles_dirty (Group * group)
{
      g_return_if_fail (group!=NULL);

      group->articles_dirty = TRUE;
}

gpointer
group_get_article_by_message_id (Group * group, const PString * message_id)
{
      g_return_val_if_fail (group_is_valid (group), NULL);
      g_return_val_if_fail (pstring_is_set (message_id), NULL);

      return g_hash_table_lookup (group_get_articles(group), message_id);
}

static void
remove_articles_from_thread (Article    ** articles,
                             guint         article_qty,
                             gpointer      remove_articles_gptrarray)
{
      guint i;
      GPtrArray * remove = (GPtrArray*) remove_articles_gptrarray;
      GHashTable * rethread_hash;

      /* build a hash table of the articles to rethread.
       * this is for easy removal of "remove" articles */
            rethread_hash = g_hash_table_new (g_direct_hash, g_direct_equal);
      for (i=0; i!=article_qty; ++i)
            g_hash_table_insert (rethread_hash, articles[i], articles[i]);
      for (i=0; i!=remove->len; ++i)
            g_hash_table_remove (rethread_hash, g_ptr_array_index (remove, i));

      /* update the thread information in the "remove" articles */
      for (i=0; i!=remove->len; ++i) {
            Article * a = ARTICLE(g_ptr_array_index(remove, i));
            a->parent = NULL;
            if (a->threads != NULL) {
                  g_slist_free (a->threads);
                  a->threads = NULL;
            }
      }

      /* update the thread information in "keep" articles */
      if (g_hash_table_size(rethread_hash) > 0) {
            GPtrArray * rethread_a = g_ptr_array_new ();
            pan_hash_to_ptr_array (rethread_hash, rethread_a);
            thread_articles ((Article**)rethread_a->pdata, rethread_a->len);
            g_ptr_array_free (rethread_a, TRUE);
      }

      /* cleanup */
      g_hash_table_destroy (rethread_hash);
}

void
group_remove_articles (Group * group, Article ** articles, guint article_qty)
{
      debug_enter ("group_remove_articles");

      /* sanity clause */
      g_return_if_fail (group_is_valid (group));
      g_return_if_fail (group->_articles_refcount>=0);

      if (article_qty > 0)
      {
            guint i;
            int removed_read;
            GPtrArray * tmp_remove;
            GPtrArray * removed;
            GHashTable * ours;

            g_return_if_fail (articles!=NULL);
            g_return_if_fail (article_qty > 0);
            g_return_if_fail (articles_are_valid_in_group ((const Article **)articles, article_qty));
            g_return_if_fail (articles[0]->group == group);

            /* let everyone know that articles are going to be removed */
            tmp_remove = g_ptr_array_new ();
            pan_g_ptr_array_assign (tmp_remove, (gpointer*)articles, article_qty);
            sort_articles ((Article**)tmp_remove->pdata, tmp_remove->len, ARTICLE_SORT_MSG_ID, TRUE);
            pan_callback_call (group_get_articles_removed_callback(), group, tmp_remove);

            /**
            ***  Try to remove the articles
            **/

            g_mutex_lock (group_get_article_mutex (group));
            ours = group_get_articles (group);
            removed_read = 0;
            removed = g_ptr_array_sized_new (article_qty);
            for (i=0; i!=tmp_remove->len; ++i)
            {
                  const Article * a = ARTICLE (g_ptr_array_index(tmp_remove,i));
                  const PString * message_id = &a->message_id;
                  Article * our_a = ARTICLE (g_hash_table_lookup (ours, message_id));

                  if (our_a != NULL)
                  {
                        g_hash_table_remove (ours, message_id);

                        /* add to the 'removed' array */
                        g_ptr_array_add (removed, our_a);

                        /* count how many removed articles were read */
                        if (article_is_read (our_a))
                              ++removed_read;
                  }
            }

            /**
            ***  Rethread the affected articles
            **/

            if (removed->len)
            {
                  /* add removed articles to the 'deleted' list */
                  if (group->_deleted_articles == NULL)
                        group->_deleted_articles = g_ptr_array_new ();
                  pan_g_ptr_array_append (group->_deleted_articles, removed->pdata, removed->len);

                  /* rethread the affected branches */
                  article_forall_in_threads ((Article**)removed->pdata, removed->len, GET_WHOLE_THREAD,
                                       remove_articles_from_thread, removed);

                  group_set_articles_dirty (group);
            }

            /**
            ***  Unlock the lock
            **/

            g_mutex_unlock (group_get_article_mutex (group));

            /**
            ***  Update the stats
            **/

            if (removed->len != 0) {
                  guint change_type = 0u;
                  change_type |= group_set_article_qty_impl (group, group->article_qty - removed->len);
                  change_type |= group_set_article_read_qty_impl (group, group->article_read_qty - removed_read);
                  if (change_type)
                        fire_groups_changed (&group, 1, change_type);
            }

            /* cleanup */
            g_ptr_array_free (removed, TRUE);
            g_ptr_array_free (tmp_remove, TRUE);
      }

      debug_exit ("group_remove_articles");
}

void
group_expire_articles_not_in_range (Group * g, gulong low, gulong high)
{
      guint i;
      GPtrArray * tmp;
      GPtrArray * removeme;
      debug_enter ("group_expire_articles_not_in_range");

      /* sanity clause */
      g_return_if_fail (group_is_valid(g));

      /* find old articles to remove */
      tmp = group_get_article_array (g);
      removeme = g_ptr_array_sized_new (tmp->len);
      for (i=0; i!=tmp->len; ++i) {
            Article * a = ARTICLE(g_ptr_array_index(tmp,i));
            if ((a->number<low) || (a->number>high))
                  g_ptr_array_add (removeme, a);
      }

      /* remove the old articles */
      if (removeme->len != 0)
      {
            log_add_va (LOG_INFO, _("Expired %u articles from `%s'"),
                  removeme->len,
                  group_get_name(g));

            group_remove_articles (g, (Article**)removeme->pdata, removeme->len);
      }

      /* cleanup */
      g_ptr_array_free (tmp, TRUE);
      g_ptr_array_free (removeme, TRUE);
      debug_exit ("group_expire_articles_not_in_range");
}


/***
****
***/

static GStaticRecMutex article_refcount_mutex = G_STATIC_REC_MUTEX_INIT;

gboolean
group_ref_articles_if_loaded (Group * group)
{
      gboolean retval = FALSE;
      debug_enter ("group_ref_articles_if_loaded");

      /* sanity clause */
      g_return_val_if_fail (group_is_valid(group), FALSE);
      g_return_val_if_fail (group->_articles_refcount>=0, FALSE);

      /* ref if reffed */
      g_static_rec_mutex_lock (&article_refcount_mutex);
      if (group->_articles_refcount >= 1) {
            ++group->_articles_refcount;
            retval = TRUE;
      }
      g_static_rec_mutex_unlock (&article_refcount_mutex);

      debug_exit ("group_ref_articles_if_loaded");
      return retval;
}

void
group_ref_articles (Group * group, StatusItem * status)
{
      int refcount;
      debug_enter ("group_ref_articles");

      /* sanity clause */
      g_return_if_fail (group_is_valid(group));
      g_return_if_fail (group->_articles_refcount >= 0);

      /* increment the refcount and load from disk if necessary */
      g_static_rec_mutex_lock (&article_refcount_mutex);
      refcount = ++group->_articles_refcount;
      if (refcount == 1)
      {
            g_mutex_lock (group_get_article_mutex (group));
            file_headers_load (group, status);
            g_mutex_unlock (group_get_article_mutex (group));
      }
      g_static_rec_mutex_unlock (&article_refcount_mutex);

      debug_exit ("group_ref_articles");
}

void
group_unref_articles (Group * group, StatusItem * status)
{
      debug_enter ("group_unref_articles");

      /* sanity clause */
      g_return_if_fail (group_is_valid(group));
      g_return_if_fail (group->_articles_refcount > 0);

      /* unref */
      g_static_rec_mutex_lock (&article_refcount_mutex);
      if (group->_articles_refcount == 1)
      {
            g_mutex_lock (group_get_article_mutex (group));
            file_headers_save_noref (group, status);
            server_save_grouplist_if_dirty (group->server, status);
            blow_article_memory (group);
            g_mutex_unlock (group_get_article_mutex (group));
      }
      --group->_articles_refcount;
      g_static_rec_mutex_unlock (&article_refcount_mutex);

      /* cleanup */
      debug_exit ("group_unref_articles");
}

/**
***
**/


const char*
group_get_name (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), _("(No Group)"));

      return group->name.str;
}

void
group_get_collapsed_name (const Group   * group,
                          char          * buf,
                          size_t          bufsize)
{
      static const PString moderated = { "moderated", 9, 0u };
      const char * name_str = group_get_name (group);
      const char * bufend = buf + bufsize;
      const char * bufbegin = buf;
      const char * pch;
      const PString * name = &group->name;
      PString token = PSTRING_INIT;
      PString long_token = PSTRING_INIT;

      *buf = '\0';

      /* find the long token -- use the last, unless that's "moderated" */
      pch = pan_strrchr_len (name->str, name->len, '.');
      if (pch == NULL)
            long_token = pstring_shallow (name->str, name->len);
      else {
            token = pstring_substr_shallow (name, pch+1, NULL);
            if (!pstring_equal (&moderated, &token))
                  long_token = token;
            else {
                  token = pstring_substr_shallow (name, name->str, pch);
                  pch = pan_strrchr_len (token.str, token.len, '.');
                  if (pch != NULL)
                        long_token = pstring_substr_shallow (&token, pch+1, NULL);
            }
      }

      pch = name_str;
      while (get_next_token_pstring (pch, '.', &pch, &token)) {
            const int len = pstring_equal (&token, &long_token) ? token.len : 1;
            buf += pan_strncpy_len (buf, bufend-buf, token.str, len);
            buf += pan_strncpy_len (buf, bufend-buf, ".", 1);
      }

      if (buf != bufbegin)
            *--buf = '\0';
}

/***
****
***/

static Newsrc*
group_get_newsrc (Group * group)
{
      g_return_val_if_fail (group_is_valid(group), NULL);

      if (group->_newsrc == NULL)
            group->_newsrc = newsrc_new (NULL, group->article_low, group->article_high);

      return group->_newsrc;
}

void
group_set_newsrc_read_string (Group            * group,
                              const char       * range_string)
{
      g_return_if_fail (group_is_valid (group));

      if (is_nonempty_string (range_string))
      {
            gulong newsrc_low = 0ul;
            gulong newsrc_high = 0ul;
            Newsrc * newsrc = group_get_newsrc (group);

            /* initialize the newsrc */
            newsrc_init_from_range_str (newsrc, range_string);

            /* sync the newsrc and group ranges.*/
            newsrc_get_group_range (newsrc, &newsrc_low,  &newsrc_high);
            group_set_article_range (group,
                                     MIN (newsrc_low, group->article_low),
                                     MAX (newsrc_high, group->article_high));
      }
}

gboolean
group_is_article_read (const Group      * group,
                       gulong             article_number)
{
      return newsrc_is_article_read (group_get_newsrc ((Group*)group), article_number);
}
                                                                                                                               
gboolean
group_mark_article_read (Group            * group,
                         gulong             article_number,
                         gboolean           read)
{
      return newsrc_mark_article (group_get_newsrc (group), article_number, read);
}

char*
group_get_newsrc_export_string (const Group * group)
{
      char * retval = NULL;

      g_return_val_if_fail (group_is_valid(group), NULL);

      if (group->_newsrc == NULL)
            retval = g_strdup ("");
      else
            retval = newsrc_export_line (group->_newsrc, group_get_name(group), group_is_subscribed(group));

      return retval;
}

static char*
get_newsrc_read_string (const Newsrc * newsrc)
{
      char * retval;

      if (newsrc == NULL)
            retval = g_strdup ("");
      else
            retval = newsrc_get_read_str (newsrc);

      return retval;
}

char*
group_get_newsrc_read_string (const Group * group)
{
      g_return_val_if_fail (group_is_valid(group), NULL);

      return get_newsrc_read_string (group->_newsrc);
}

/**
 * When crossposts are deleted, we don't want to load in all the groups
 * containing a crosspost just to delete the article and save the whole
 * group back to disk again.  Likewise the crosspost may not even have been
 * downloaded yet, which would thwart the deletion.
 *
 * So the solution Pan uses is to have a newsrc-like range of article
 * numbers of these deleted articles.  When a client tries to add articles
 * to the group, group_add_articles() checks them against the deleted list
 * before allowing them to be added.
 * 
 * Also important is that we update the deleted list's bounds whenever
 * the group's own low/high numbers are updated, in group_set_article_range(),
 * so that the deleted article list doesn't get too long when saved to disk.
 */
static Newsrc*
group_get_purged (Group * group)
{
      g_return_val_if_fail (group_is_valid(group), NULL);

      if (group->_purged == NULL)
            group->_purged = newsrc_new (NULL, group->article_low, group->article_high);

      return group->_purged;
}

char*
group_get_purged_string (const Group * group)
{
      g_return_val_if_fail (group_is_valid (group), NULL);

      return get_newsrc_read_string (group->_purged);
}

                                                                                                                               
void
group_set_purged_string (Group            * group,
                         const char       * range_string)
{
      g_return_if_fail (group_is_valid (group));

      if (is_nonempty_string (range_string))
      {
            Newsrc * newsrc = group_get_purged (group);

            newsrc_init_from_range_str (newsrc, range_string);

            newsrc_set_group_range (newsrc, group->article_low, group->article_high);
      }
}

void
group_mark_article_purged (Group            * group,
                           gulong             article_number)
{
      newsrc_mark_article (group_get_purged (group), article_number, TRUE);
}



/**
***
**/

struct _Article*
group_alloc_new_article (Group * group)
{
      Article * a;
      g_return_val_if_fail (group_is_valid(group), NULL);

      if (group->_article_chunk == NULL)
            group->_article_chunk = memchunk_new (sizeof(Article), 1024, FALSE);

      a = (Article*) memchunk_alloc (group->_article_chunk);
      a->group = group;
      return a;
}

static GHashTable*
group_get_articles (Group * group)
{
      g_return_val_if_fail (group_is_valid(group), NULL);

      if (group->_articles == NULL)
            group->_articles = g_hash_table_new (pstring_hash, pstring_equal);

      return group->_articles;
}

static GPtrArray*
group_get_article_array (Group * group)
{
      GHashTable * hash;
      GPtrArray * retval;

      /* sanity clause */
      retval = g_ptr_array_new ();
      g_return_val_if_fail (group_is_valid(group), retval);

      /* populate retval */
      hash = group_get_articles (group);
      pan_hash_to_ptr_array (hash, retval);
      pan_warn_if_fail (articles_are_valid ((const Article**)retval->pdata, retval->len));

      return retval;
}

void
group_article_forall      (Group               * group,
                           GroupArticleFunc      func,
                           gpointer              func_user_data)
{
      g_return_if_fail (group_is_valid (group));

      if (group_ref_articles_if_loaded (group))
      {
            GPtrArray * a = group_get_article_array (group);
            (func)(group, (Article**)a->pdata, a->len, func_user_data);
            g_ptr_array_free (a, TRUE);

            group_unref_articles (group, NULL);
      }
      else
      {
            (func)(group, NULL, 0u, func_user_data);
      }
}

Generated by  Doxygen 1.6.0   Back to index