view pidgin-twitter.c @ 62:6843c3e6477f

fixed the size of regp array
author mikanbako <maoutwo@gmail.com>
date Sat, 21 Jun 2008 14:47:35 +0900
parents a44d15cfd8a2
children 760006015519
line wrap: on
line source

/*
 * Pidgin-Twitter plugin.
 *
 * 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; either version 2 of the
 * License, or (at your option) any later version.
 *
 * 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.
 */
#define PURPLE_PLUGINS 1

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

#include "gtkplugin.h"
#include "util.h"
#include "debug.h"
#include "connection.h"
#include "version.h"
#include "sound.h"
#include "gtkconv.h"
#include "gtkimhtml.h"

#define RECIPIENT        0
#define SENDER           1
#define COMMAND          2
#define PSEUDO           3
#define MESSAGE          4
#define USER             5
#define USER_FIRST_LINE  6
#define USER_FORMATTED   7

#define PLUGIN_ID	            "gtk-honeyplanet-pidgin_twitter"
#define PLUGIN_NAME	            "pidgin-twitter"

/* options */
#define OPT_PIDGINTWITTER 		"/plugins/pidgin_twitter"
#define OPT_TRANSLATE_RECIPIENT OPT_PIDGINTWITTER "/translate_recipient"
#define OPT_TRANSLATE_SENDER    OPT_PIDGINTWITTER "/translate_sender"
#define OPT_PLAYSOUND_RECIPIENT OPT_PIDGINTWITTER "/playsound_recipient"
#define OPT_PLAYSOUND_SENDER    OPT_PIDGINTWITTER "/playsound_sender"
#define OPT_SOUNDID_RECIPIENT   OPT_PIDGINTWITTER "/soundid_recipient"
#define OPT_SOUNDID_SENDER      OPT_PIDGINTWITTER "/soundid_sender"
#define OPT_ESCAPE_PSEUDO       OPT_PIDGINTWITTER "/escape_pseudo"
#define OPT_USERLIST_RECIPIENT  OPT_PIDGINTWITTER "/userlist_recipient"
#define OPT_USERLIST_SENDER     OPT_PIDGINTWITTER "/userlist_sender"
#define OPT_COUNTER             OPT_PIDGINTWITTER "/counter"
#define OPT_SUPPRESS_OOPS       OPT_PIDGINTWITTER "/suppress_oops"
#define OPT_PREVENT_NOTIFICATION OPT_PIDGINTWITTER "/prevent_notification"

/* formats and templates */
#define RECIPIENT_FORMAT        "@<a href='http://twitter.com/%s'>%s</a>"
#define SENDER_FORMAT           "%s<a href='http://twitter.com/%s'>%s</a>: "
#define DEFAULT_LIST            "(list of users: separated with ' ,:;')"
#define OOPS_MESSAGE            "<body>Oops! Your update was over 140 characters. We sent the short version to your friends (they can view the entire update on the web).<BR></body>"

/* patterns */
#define P_RECIPIENT        "@([A-Za-z0-9_]+)"
#define P_SENDER           "^(\\r?\\n?)([A-Za-z0-9_]+): "
#define P_COMMAND          "^(?:\\s*)([dDfFgGlLmMnNtTwW]{1}\\s+[A-Za-z0-9_]+)(?:\\s*\\Z)"
#define P_PSEUDO           "^\\s*(?:[\"#$%&'()*+,\\-./:;<=>?\\[\\\\\\]_`{|}~]|[^\\s\\x21-\\x7E])*([dDfFgGlLmMnNtTwW]{1})(?:\\Z|\\s+|[^\\x21-\\x7E]+\\Z)"
#define P_MESSAGE          "^<body>(.*)</body>"
#define P_USER             "^\\(.+?\\)\\s*([A-Za-z0-9_]+):"
#define P_USER_FIRST_LINE  "^\\(.+?\\)\\s*.+:\\s*([A-Za-z0-9_]+):"
#define P_USER_FORMATTED   "^.*?<a .+?>([A-Za-z0-9_]+)</a>:"

/* debug macros */
#define twitter_debug(fmt, ...)	purple_debug(PURPLE_DEBUG_INFO, PLUGIN_NAME, "%s():%4d:  " fmt, __FUNCTION__, (int)__LINE__, ## __VA_ARGS__);
#define twitter_error(fmt, ...)	purple_debug(PURPLE_DEBUG_ERROR, PLUGIN_NAME, "%s():%4d:  " fmt, __FUNCTION__, (int)__LINE__, ## __VA_ARGS__);


/* globals */
static GRegex *regp[8];
static gboolean suppress_oops = FALSE;
static GHashTable *icon_id_by_user;
static GList *requested_users = NULL;
static GList *requestings = NULL;
static GList *requested_icon_marks = NULL;

/* prototypes */
static void escape(gchar **str);
static gboolean sending_im_cb(PurpleAccount *account, char *recipient, char **buffer, void *data);
static gboolean eval(const GMatchInfo *match_info, GString *result, gpointer user_data);
static void translate(gchar **str, int which);
static void playsound(gchar **str, int which);
static gboolean writing_im_cb(PurpleAccount *account, char *sender, char **buffer, PurpleConversation *conv, int *flags, void *data);
static void insert_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *position, gchar *new_text, gint new_text_length, gpointer user_data);
static void delete_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *start_pos, GtkTextIter *end_pos, gpointer user_data);
static void detach_from_window(void);
static void detach_from_gtkconv(PidginConversation *gtkconv, gpointer null);
static void delete_requested_icon_marks(PidginConversation *gtkconv);
static void attach_to_window(void);
static void attach_to_gtkconv(PidginConversation *gtkconv, gpointer null);
static gboolean is_twitter_account(PurpleAccount *account, const char *name);
static gboolean is_twitter_conv(PurpleConversation *conv);
static void conv_created_cb(PurpleConversation *conv, gpointer null);
static void deleting_conv_cb(PurpleConversation *conv);
static gboolean receiving_im_cb(PurpleAccount *account, char **sender, char **buffer, PurpleConversation *conv, PurpleMessageFlags *flags, void *data);
static void insert_requested_icon(gpointer data, gpointer user_data);
static void downloaded_icon_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text, gsize len, const gchar *error_message);
static void request_icon(const char *buffer);
static void displayed_im_cb(PurpleAccount *account, const char *who, char *message, PurpleConversation *conv, PurpleMessageFlags flags);
static gboolean load_plugin(PurplePlugin *plugin);
static gboolean unload_plugin(PurplePlugin *plugin);
static void counter_prefs_cb(const char *name, PurplePrefType type, gconstpointer val, gpointer data);
static PurplePluginPrefFrame *get_plugin_pref_frame(PurplePlugin *plugin);
static void init_plugin(PurplePlugin *plugin);


/* this function is a modified clone of purple_markup_strip_html() */
static char *
strip_html_markup(const char *str)
{
	int i, j, k, entlen;
	gboolean visible = TRUE;
	gboolean closing_td_p = FALSE;
	gchar *str2;
	const gchar *cdata_close_tag = NULL, *ent;
	gchar *href = NULL;
	int href_st = 0;

	if(!str)
		return NULL;

	str2 = g_strdup(str);

	for (i = 0, j = 0; str2[i]; i++)
	{
		if (str2[i] == '<')
		{
			if (cdata_close_tag)
			{
				/* Note: Don't even assume any other tag is a tag in CDATA */
				if (g_ascii_strncasecmp(str2 + i, cdata_close_tag,
                                        strlen(cdata_close_tag)) == 0)
				{
					i += strlen(cdata_close_tag) - 1;
					cdata_close_tag = NULL;
				}
				continue;
			}
			else if (g_ascii_strncasecmp(str2 + i, "<td", 3) == 0 && closing_td_p)
			{
				str2[j++] = '\t';
				visible = TRUE;
			}
			else if (g_ascii_strncasecmp(str2 + i, "</td>", 5) == 0)
			{
				closing_td_p = TRUE;
				visible = FALSE;
			}
			else
			{
				closing_td_p = FALSE;
				visible = TRUE;
			}

			k = i + 1;

			if(g_ascii_isspace(str2[k]))
				visible = TRUE;
			else if (str2[k])
			{
				/* Scan until we end the tag either implicitly (closed start
				 * tag) or explicitly, using a sloppy method (i.e., < or >
				 * inside quoted attributes will screw us up)
				 */
				while (str2[k] && str2[k] != '<' && str2[k] != '>')
				{
					k++;
				}

				/* If we've got an <a> tag with an href, save the address
				 * to print later. */
				if (g_ascii_strncasecmp(str2 + i, "<a", 2) == 0 &&
				    g_ascii_isspace(str2[i+2]))
				{
					int st; /* start of href, inclusive [ */
					int end; /* end of href, exclusive ) */
					char delim = ' ';
					/* Find start of href */
					for (st = i + 3; st < k; st++)
					{
						if (g_ascii_strncasecmp(str2+st, "href=", 5) == 0)
						{
							st += 5;
							if (str2[st] == '"' || str2[st] == '\'')
							{
								delim = str2[st];
								st++;
							}
							break;
						}
					}
					/* find end of address */
					for (end = st; end < k && str2[end] != delim; end++)
					{
						/* All the work is done in the loop construct above. */
					}

					/* If there's an address, save it.  If there was
					 * already one saved, kill it. */
					if (st < k)
					{
						char *tmp;
						g_free(href);
						tmp = g_strndup(str2 + st, end - st);
						href = purple_unescape_html(tmp);
						g_free(tmp);
						href_st = j;
					}
				}

				/* Check for tags which should be mapped to newline */
				else if (g_ascii_strncasecmp(str2 + i, "<p>", 3) == 0
                         || g_ascii_strncasecmp(str2 + i, "<tr", 3) == 0
                         || g_ascii_strncasecmp(str2 + i, "<br", 3) == 0
                         || g_ascii_strncasecmp(str2 + i, "<hr", 3) == 0
                         || g_ascii_strncasecmp(str2 + i, "<li", 3) == 0
                         || g_ascii_strncasecmp(str2 + i, "<div", 4) == 0
                         || g_ascii_strncasecmp(str2 + i, "</table>", 8) == 0)
				{
					str2[j++] = '\n';
				}
				else if (g_ascii_strncasecmp(str2 + i, "<script", 7) == 0)
				{
					cdata_close_tag = "</script>";
				}
				else if (g_ascii_strncasecmp(str2 + i, "<style", 6) == 0)
				{
					cdata_close_tag = "</style>";
				}
				/* Update the index and continue checking after the tag */
				i = (str2[k] == '<' || str2[k] == '\0')? k - 1: k;
				continue;
			}
		}
		else if (cdata_close_tag)
		{
			continue;
		}
		else if (!g_ascii_isspace(str2[i]))
		{
			visible = TRUE;
		}

		if (str2[i] == '&' &&
            (ent = purple_markup_unescape_entity(str2 + i, &entlen)) != NULL)
		{
			while (*ent)
				str2[j++] = *ent++;
			i += entlen - 1;
			continue;
		}

		if (visible)
			str2[j++] = g_ascii_isspace(str2[i])? ' ': str2[i];
	}

	g_free(href);

	str2[j] = '\0';

	return str2;
}


/* our implementation */

static void
escape(gchar **str)
{
    GMatchInfo *match_info = NULL;
    gchar *newstr = NULL, *match = NULL;
    gboolean flag = FALSE;

    /* search genuine command */
    g_regex_match(regp[COMMAND], *str, 0, &match_info);
    while(g_match_info_matches(match_info)) {
        match = g_match_info_fetch(match_info, 1);
        twitter_debug("command = %s\n", match);
        g_free(match);
        g_match_info_next(match_info, NULL);
        flag = TRUE;
    }
    g_match_info_free(match_info);
    match_info = NULL;

    if(flag)
        return;

    /* if not found, check pseudo command */
    g_regex_match(regp[PSEUDO], *str, 0, &match_info);
    while(g_match_info_matches(match_info)) {
        match = g_match_info_fetch(match_info, 1);
        twitter_debug("pseudo = %s\n", match);
        g_free(match);
        g_match_info_next(match_info, NULL);
        flag = TRUE;
    }
    g_match_info_free(match_info);
    match_info = NULL;

    /* if there is pseudo one, escape it */
    if(flag) {
        /* put ". " to the beginning of buffer */
        newstr = g_strdup_printf(". %s", *str);
        twitter_debug("*str = %s newstr = %s\n", *str, newstr);
        g_free(*str);
        *str = newstr;
    }
}

static void
strip_markup(gchar **str)
{
    char *plain;

    plain = strip_html_markup(*str);
    g_free(*str);
    *str = plain;
}

static gboolean
sending_im_cb(PurpleAccount *account, char *recipient, char **buffer,
              void *data)
{
    int utflen, bytes;

    twitter_debug("called\n");

    /* check if the message is from twitter */
    if(!is_twitter_account(account, recipient))
        return FALSE;

    /* strip all markups */
    strip_markup(buffer);

    /* escape pseudo command */
    if(purple_prefs_get_bool(OPT_ESCAPE_PSEUDO)) {
        escape(buffer);
    }

    /* try to suppress oops message */
    utflen = g_utf8_strlen(*buffer, -1);
    bytes = strlen(*buffer);
    twitter_debug("utflen = %d bytes = %d\n", utflen, bytes);
    if(bytes > 140 && utflen <= 140)
        suppress_oops = TRUE;

    return FALSE;
}

static gboolean
eval(const GMatchInfo *match_info, GString *result, gpointer user_data)
{
    int which = *(int *)user_data;
    gchar sub[128];

    if(which == RECIPIENT) {
        gchar *match = g_match_info_fetch(match_info, 1);

        snprintf(sub, 128, RECIPIENT_FORMAT, match, match);
        g_free(match);
    }
    else if(which == SENDER) {
        gchar *match1 = g_match_info_fetch(match_info, 1); //preceding CR|LF
        gchar *match2 = g_match_info_fetch(match_info, 2); //sender

        snprintf(sub, 128, SENDER_FORMAT, match1 ? match1: "",
                 match2, match2);
        g_free(match1);
        g_free(match2);
    }

    g_string_append(result, sub);
    twitter_debug("sub = %s\n", sub);

    return FALSE;
}

static void
translate(gchar **str, int which)
{
    gchar *newstr;

    newstr = g_regex_replace_eval(regp[which],  // compiled regex
                                  *str, // subject string
                                  -1,   // length of the subject string
                                  0,    // start position
                                  0,    // match options
                                  eval, // function to be called for each match
                                  &which,   // user data
                                  NULL);    // error handler

    twitter_debug("which = %d *str = %s newstr = %s\n", which, *str, newstr);

    g_free(*str);
    *str = newstr;
}

static void
playsound(gchar **str, int which)
{
    GMatchInfo *match_info;
    const gchar *list;
    gchar **candidates = NULL, **candidate = NULL;

    list = purple_prefs_get_string(which ? OPT_USERLIST_SENDER :
                                   OPT_USERLIST_RECIPIENT);
    g_return_if_fail(list != NULL);
    if(!strcmp(list, DEFAULT_LIST))
        return;

    candidates = g_strsplit_set(list, " ,:;", 0);
    g_return_if_fail(candidates != NULL);

    g_regex_match(regp[which], *str, 0, &match_info);
    while(g_match_info_matches(match_info)) {
        gchar *user = NULL;
        if(which == RECIPIENT)
            user = g_match_info_fetch(match_info, 1);
        else if(which == SENDER)
            user = g_match_info_fetch(match_info, 2);
        twitter_debug("user = %s\n", user);

        for(candidate = candidates; *candidate; candidate++) {
            if(!strcmp(*candidate, ""))
                continue;
            twitter_debug("candidate = %s\n", *candidate);
            if(!strcmp(user, *candidate)) {
                twitter_debug("match. play sound\n");
                purple_sound_play_event(purple_prefs_get_int
                                        (which ? OPT_SOUNDID_SENDER :
                                         OPT_SOUNDID_RECIPIENT), NULL);
                break;
            }
        }
        g_free(user);
        g_match_info_next(match_info, NULL);
    }
    g_strfreev(candidates);
    g_match_info_free(match_info);
}

static gboolean
writing_im_cb(PurpleAccount *account, char *sender, char **buffer,
              PurpleConversation *conv, int *flags, void *data)
{
    twitter_debug("called\n");

    /* check if the message is from twitter */
    if(!is_twitter_account(account, sender))
        return FALSE;

    /* strip all markups */
    strip_markup(buffer);

    /* playsound */
    if(purple_prefs_get_bool(OPT_PLAYSOUND_SENDER)) {
        playsound(buffer, SENDER);
    }
    if(purple_prefs_get_bool(OPT_PLAYSOUND_RECIPIENT)) {
        playsound(buffer, RECIPIENT);
    }

    /* translate */
    if(purple_prefs_get_bool(OPT_TRANSLATE_SENDER)) {
        translate(buffer, SENDER);
    }
    if(purple_prefs_get_bool(OPT_TRANSLATE_RECIPIENT)) {
        translate(buffer, RECIPIENT);
    }

    /* escape pseudo command (to show same result to sending message) */
    if(purple_prefs_get_bool(OPT_ESCAPE_PSEUDO)) {
        escape(buffer);
    }

    return FALSE;
}

static void
insert_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *position,
               gchar *new_text, gint new_text_length, gpointer user_data)
{
    PidginConversation *gtkconv = (PidginConversation *)user_data;
    GtkWidget *box, *counter = NULL;
    gchar *markup = NULL;
    guint count;

    g_return_if_fail(gtkconv != NULL);

    count = gtk_text_buffer_get_char_count(textbuffer) +
        (unsigned int)g_utf8_strlen(new_text, -1);

    markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                     count <= 140 ? "black" : "red", count);

    box = gtkconv->toolbar;
    counter = g_object_get_data(G_OBJECT(box), PLUGIN_ID "-counter");
    if(counter)
        gtk_label_set_markup(GTK_LABEL(counter), markup);

    g_free(markup);
}

static void
delete_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *start_pos,
               GtkTextIter *end_pos, gpointer user_data)
{
    PidginConversation *gtkconv = (PidginConversation *)user_data;
    GtkWidget *box, *counter = NULL;
    gchar *markup = NULL;

    g_return_if_fail(gtkconv != NULL);

    guint count = gtk_text_buffer_get_char_count(textbuffer) -
        (gtk_text_iter_get_offset(end_pos) -
         gtk_text_iter_get_offset(start_pos));

    markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                     count <= 140 ? "black" : "red", count);

    box = gtkconv->toolbar;
    counter = g_object_get_data(G_OBJECT(box), PLUGIN_ID "-counter");
    if(counter)
        gtk_label_set_markup(GTK_LABEL(counter), markup);

    g_free(markup);
}

static void
detach_from_window(void)
{
    GList *list;

    /* find twitter conv window out and detach from that */
    for(list = pidgin_conv_windows_get_list(); list; list = list->next) {
        PidginWindow *win = list->data;
        PurpleConversation *conv =
            pidgin_conv_window_get_active_conversation(win);
        if(is_twitter_conv(conv))
            detach_from_gtkconv(PIDGIN_CONVERSATION(conv), NULL);
    }
}

static void
detach_from_gtkconv(PidginConversation *gtkconv, gpointer null)
{
    GtkWidget *box, *counter = NULL, *sep = NULL;

    g_signal_handlers_disconnect_by_func(G_OBJECT(gtkconv->entry_buffer),
                                         (GFunc) insert_text_cb, gtkconv);
    g_signal_handlers_disconnect_by_func(G_OBJECT(gtkconv->entry_buffer),
                                         (GFunc) delete_text_cb, gtkconv);

    box = gtkconv->toolbar;

    /* remove counter */
    counter = g_object_get_data(G_OBJECT(box), PLUGIN_ID "-counter");
    if(counter) {
        gtk_container_remove(GTK_CONTAINER(box), counter);
        g_object_unref(counter);
        g_object_set_data(G_OBJECT(box), PLUGIN_ID "-counter", NULL);
    }

    /* remove separator */
    sep = g_object_get_data(G_OBJECT(box), PLUGIN_ID "-sep");
    if(sep) {
        gtk_container_remove(GTK_CONTAINER(box), sep);
        g_object_unref(sep);
        g_object_set_data(G_OBJECT(box), PLUGIN_ID "-sep", NULL);
    }

    gtk_widget_queue_draw(pidgin_conv_get_window(gtkconv)->window);
}

static void
delete_requested_icon_marks(PidginConversation* conv) {
    GtkTextBuffer *text_buffer = gtk_text_view_get_buffer(
        GTK_TEXT_VIEW(conv->imhtml));
    GList *mark_list = g_list_first(requested_icon_marks);
    
    /* delete the marks in the buffer that will be closed. */
    while(mark_list) {
        GtkTextMark *mark = mark_list->data;
        GList *next = g_list_next(mark_list);

        if(gtk_text_mark_get_buffer(mark) == text_buffer) {
            /* the mark will be freed in the function */
            gtk_text_buffer_delete_mark(text_buffer, mark);
            requested_icon_marks = g_list_delete_link(
                requested_icon_marks, mark_list);
        }

        mark_list = next;
    }
}

static void
attach_to_window(void)
{
    GList *list;

    /* find twitter conv window out and attach to that */
    for(list = pidgin_conv_windows_get_list(); list; list = list->next) {
        PidginWindow *win = list->data;
        PurpleConversation *conv =
            pidgin_conv_window_get_active_conversation(win);
        /* only attach to twitter conversation window */
        if(is_twitter_conv(conv))
            attach_to_gtkconv(PIDGIN_CONVERSATION(conv), NULL);
    }
}

static void
attach_to_gtkconv(PidginConversation *gtkconv, gpointer null)
{
    GtkWidget *box, *sep, *counter, *menus;
    GtkIMHtml *imhtml;

    box = gtkconv->toolbar;
    imhtml = GTK_IMHTML(gtkconv->imhtml);

    /* Disable widgets that decorate or add link to composing text
     * because Twitter cannot receive marked up string. For lean-view
     * and wide-view, see pidgin/gtkimhtmltoolbar.c.
     */
    menus = g_object_get_data(G_OBJECT(box), "lean-view");
    if(menus) {
        gtk_widget_set_sensitive(GTK_WIDGET(menus), FALSE);
    }
    menus = g_object_get_data(G_OBJECT(box), "wide-view");
    if(menus) {
        gtk_widget_set_sensitive(GTK_WIDGET(menus), FALSE);
    }

    purple_conversation_set_features(
        gtkconv->active_conv,
        purple_conversation_get_features(gtkconv->active_conv) &
        ~PURPLE_CONNECTION_HTML); 

    /* check if the counter is enabled */
    if(!purple_prefs_get_bool(OPT_COUNTER))
        return;

    /* get counter object */
    counter = g_object_get_data(G_OBJECT(box), PLUGIN_ID "-counter");
    g_return_if_fail(counter == NULL);

    /* make counter object */
    counter = gtk_label_new(NULL);
    gtk_widget_set_name(counter, "counter_label");
    gtk_label_set_text(GTK_LABEL(counter), "0");
    gtk_box_pack_end(GTK_BOX(box), counter, FALSE, FALSE, 0);
    gtk_widget_show_all(counter);
    g_object_set_data(G_OBJECT(box), PLUGIN_ID "-counter", counter);

    /* make separator object */
    sep = gtk_vseparator_new();
    gtk_box_pack_end(GTK_BOX(box), sep, FALSE, FALSE, 0);
    gtk_widget_show_all(sep);
    g_object_set_data(G_OBJECT(box), PLUGIN_ID "-sep", sep);

    /* connect to signals */
    g_signal_connect(G_OBJECT(gtkconv->entry_buffer), "insert_text",
                     G_CALLBACK(insert_text_cb), gtkconv);
    g_signal_connect(G_OBJECT(gtkconv->entry_buffer), "delete_range",
                     G_CALLBACK(delete_text_cb), gtkconv);

    /* redraw window */
    gtk_widget_queue_draw(pidgin_conv_get_window(gtkconv)->window);
}

static gboolean
is_twitter_account(PurpleAccount *account, const char *name)
{
    const gchar *proto = purple_account_get_protocol_id(account);

    twitter_debug("name  = %s proto = %s\n", name, proto);

    if(!strcmp(name, "twitter@twitter.com") &&
       !strcmp(proto, "prpl-jabber")) {
        return TRUE;
    }

    return FALSE;
}

static gboolean
is_twitter_conv(PurpleConversation *conv)
{
    const char *name = purple_conversation_get_name(conv);
    PurpleAccount *account = purple_conversation_get_account(conv);

    return is_twitter_account(account, name);
}

static void
conv_created_cb(PurpleConversation *conv, gpointer null)
{
    PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
    g_return_if_fail(gtkconv != NULL);

    /* only attach to twitter conversation window */
    if(is_twitter_conv(conv))
        attach_to_gtkconv(gtkconv, NULL);
}

static void
deleting_conv_cb(PurpleConversation *conv)
{
    PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
    g_return_if_fail(gtkconv != NULL);

    /* only attach to twitter conversation window */
    if(is_twitter_conv(conv))
        delete_requested_icon_marks(gtkconv);
}

static gboolean
receiving_im_cb(PurpleAccount *account, char **sender, char **buffer,
                PurpleConversation *conv, PurpleMessageFlags *flags, void *data)
{
    twitter_debug("called\n");
    twitter_debug("buffer = %s suppress_oops = %d\n", *buffer, suppress_oops);

    /* Check that conv is not null to avoid a clash.
     * conv is null when the conversation window has not opened yet.
     */
    if(!(conv && is_twitter_conv(conv))) {
        return FALSE;
    }

    /* Prevent notifications of incoming messages. */
    if(purple_prefs_get_bool(OPT_PREVENT_NOTIFICATION)) {
        *flags |= PURPLE_MESSAGE_SYSTEM;
    }

    request_icon(*buffer);

    if(!suppress_oops || !purple_prefs_get_bool(OPT_SUPPRESS_OOPS))
        return FALSE;

    if(strstr(*buffer, OOPS_MESSAGE)) {
        twitter_debug("clearing sender and buffer\n");
        g_free(*sender);
        *sender = NULL;
        g_free(*buffer);
        *buffer = NULL;
        suppress_oops = FALSE;
    }
    return FALSE;
}

static void
insert_requested_icon(gpointer data, gpointer user_data)
{
    GtkTextMark *requested_mark = (GtkTextMark *)data;
    gchar *user_name = (gchar *)user_data;
    GList *win_list;
    GtkIMHtml *target_imhtml = NULL;
    GtkTextBuffer *target_buffer = NULL;
    GtkTextIter inserting_point, next_line;
    gchar *message;
    GMatchInfo *match_info = NULL;
    gchar *user_name_in_message;
    int icon_id;
    GList *entry_of_mark;

    /* find the conversation that contains the mark  */
    for(win_list = pidgin_conv_windows_get_list(); win_list;
            win_list = win_list->next) {
        PidginWindow *win = win_list->data;
        GList *conv_list;

        for(conv_list = pidgin_conv_window_get_gtkconvs(win); conv_list;
                conv_list = conv_list->next) {
            PidginConversation *conv = conv_list->data;
            PurpleConversation *purple_conv = conv->active_conv;

            if(purple_conv && is_twitter_conv(purple_conv)) {
                GtkIMHtml *current_imhtml = GTK_IMHTML(conv->imhtml); 
                GtkTextBuffer *current_buffer = gtk_text_view_get_buffer(
                     GTK_TEXT_VIEW(current_imhtml));
    
                if(current_buffer == gtk_text_mark_get_buffer(requested_mark)) {
                     target_imhtml = current_imhtml;
                     target_buffer = current_buffer;
                     break;
                }
            }
        }
    }
    if(!(target_imhtml && target_buffer)) {
        return;
    }

    /* insert icon to the mark */

    gtk_text_buffer_get_iter_at_mark(target_buffer,
                                     &inserting_point, requested_mark);
    next_line = inserting_point;
    gtk_text_iter_forward_line(&next_line);

    message = gtk_text_buffer_get_text(target_buffer, &inserting_point, &next_line, FALSE);

    g_regex_match(regp[USER], message, 0, &match_info);
    if(!g_match_info_matches(match_info)) {
        g_match_info_free(match_info);
        g_regex_match(regp[USER_FIRST_LINE], message, 0, &match_info);
    }
    if(!g_match_info_matches(match_info)) {
        twitter_debug("user's name was not matched : %s\n", message);
        g_match_info_free(match_info);
        g_free(message);
        return;
    }

    user_name_in_message = g_match_info_fetch(match_info, 1);
    g_match_info_free(match_info);
    g_free(message);

    /* Return if the message is not by the user that has the icon. */
    if(!g_str_equal(user_name, user_name_in_message)) {
        g_free(user_name_in_message);
        return;
    }
    g_free(user_name_in_message);

    /* insert icon */
    icon_id = GPOINTER_TO_INT(g_hash_table_lookup(icon_id_by_user, user_name));
    if(!icon_id) {
        return;
    }

    gtk_imhtml_insert_image_at_iter(target_imhtml, icon_id, &inserting_point);

    /* mark the entry contains the deleted mark with NULL */
    entry_of_mark = g_list_find(requested_icon_marks, requested_mark);
    entry_of_mark->data = NULL;

    gtk_text_buffer_delete_mark(target_buffer, requested_mark);
}

static void
downloaded_icon_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data,
                   const gchar *url_text, gsize len, const gchar *error_message) 
{
    gchar *user_name = (gchar *)user_data;
    int icon_id;

    requestings = g_list_remove(requestings, url_data);

    /* Return if user's icon had already been downloaded or
     * the download is failure. */
    if(g_hash_table_lookup(icon_id_by_user, user_name) || !url_text) {
        if(!url_text) {
            twitter_debug("downloading %s's icon was failure : %s\n",
                          user_name, error_message);
        }
        else {
            twitter_debug("%s's icon has already been downloaded\n", user_name);
        }

        requested_users = g_list_remove(requested_users, user_name);
        g_free(user_name);
        return;
    }

    icon_id = purple_imgstore_add_with_id(g_memdup(url_text, len), len,
                                          user_name);
    g_hash_table_insert(icon_id_by_user, user_name, GINT_TO_POINTER(icon_id));

    twitter_debug("Downloading %s's icon has been complete.(icon_id = %d)\n",
        user_name, icon_id);

    /* Insert the icon to messages that had been received. */
    g_list_foreach(requested_icon_marks, insert_requested_icon, user_name);
    /* Remove the entries of the mark that inserted the icon. */
    requested_icon_marks = g_list_remove_all(requested_icon_marks, NULL);
}

static void
request_icon(const char *buffer)
{
    GMatchInfo *match_info = NULL;
    gchar *user_name = NULL;
    gchar *message = NULL;
    PurpleUtilFetchUrlData *fetch_data = NULL;

    /* get user's name */

    g_regex_match(regp[MESSAGE], buffer, 0, &match_info);
    if(!g_match_info_matches(match_info)) {
        twitter_debug("Message was not matched : %s\n", buffer);
        g_match_info_free(match_info);
        return;
    }

    message = g_match_info_fetch(match_info, 1);
    g_match_info_free(match_info);
    match_info = NULL;

    g_regex_match(regp[SENDER], message, 0, &match_info);
    if(!g_match_info_matches(match_info)) {
        twitter_debug("user's name was not matched : %s\n", message);
        g_match_info_free(match_info);
        g_free(message);
        return;
    }

    user_name = g_match_info_fetch(match_info, 2);
    g_match_info_free(match_info);
    g_free(message);

    /* request user's icon */

    /* Return if user's icon had already been requested. */
    if(g_list_find_custom(requested_users, user_name, (GCompareFunc)strcmp)) {
        g_free(user_name);
        return;
    }

    /* The string object are owned by the list. */
    requested_users = g_list_append(requested_users, user_name);
    
    fetch_data = purple_util_fetch_url(
        "http://static.twitter.com/images/default_profile_normal.png",
        TRUE, NULL, TRUE, downloaded_icon_cb, user_name);
    requestings = g_list_append(requestings, fetch_data);

    twitter_debug("request %s's icon\n", user_name);
}

static void
displayed_im_cb(PurpleAccount *account, const char *who, char *message,
                PurpleConversation *conv, PurpleMessageFlags flags)
{
    GMatchInfo *match_info = NULL;
    gchar *user_name;
    GtkIMHtml *imhtml;
    GtkTextBuffer *text_buffer;
    GtkTextIter inserting_point;
    int icon_id;

    /* Check that conv is not null to avoid a clash.
     * conv is null when the conversation window has not opened yet.
     */
    if(!(conv && is_twitter_conv(conv))) {
        return;
    }

    /* get user's name */
    g_regex_match(regp[USER_FORMATTED], message, 0, &match_info);
    if(!g_match_info_matches(match_info)) {
        twitter_debug("message was not matched : %s\n", message);
        g_match_info_free(match_info);
        return;
    }

    user_name = g_match_info_fetch(match_info, 1);
    g_match_info_free(match_info);

    /* insert icon */

    imhtml = GTK_IMHTML(PIDGIN_CONVERSATION(conv)->imhtml);
    text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imhtml));

    /* get GtkTextIter of the last line */
    gtk_text_buffer_get_iter_at_line(text_buffer, &inserting_point,
        gtk_text_buffer_get_line_count(text_buffer) - 1);

    icon_id = GPOINTER_TO_INT(g_hash_table_lookup(icon_id_by_user, user_name));

    /* If the user's icon has not been downloaded, mark the message */
    if(!icon_id) {
        requested_icon_marks = g_list_append(requested_icon_marks,
                                   gtk_text_buffer_create_mark(
                                       text_buffer, NULL,
                                       &inserting_point, FALSE));
        twitter_debug("%s's icon has not been downloaded.", user_name);
        g_free(user_name);
        return;
    }

    gtk_imhtml_insert_image_at_iter(imhtml, icon_id, &inserting_point);
    g_free(user_name);
}

static gboolean
load_plugin(PurplePlugin *plugin)
{
    /* connect to signal */
    purple_signal_connect(purple_conversations_get_handle(), "writing-im-msg",
                          plugin, PURPLE_CALLBACK(writing_im_cb), NULL);
    purple_signal_connect(purple_conversations_get_handle(), "sending-im-msg",
                          plugin, PURPLE_CALLBACK(sending_im_cb), NULL);
    purple_signal_connect(purple_conversations_get_handle(),
                          "conversation-created",
                          plugin, PURPLE_CALLBACK(conv_created_cb), NULL);
    purple_signal_connect(purple_conversations_get_handle(), "receiving-im-msg",
                          plugin, PURPLE_CALLBACK(receiving_im_cb), NULL);
    purple_signal_connect(pidgin_conversations_get_handle(), "displayed-im-msg",
                          plugin, PURPLE_CALLBACK(displayed_im_cb), NULL);
    purple_signal_connect(purple_conversations_get_handle(),
                          "deleting-conversation",
                           plugin, PURPLE_CALLBACK(deleting_conv_cb), NULL);

    /* compile regex */
    regp[RECIPIENT] = g_regex_new(P_RECIPIENT, 0, 0, NULL);
    regp[SENDER]    = g_regex_new(P_SENDER,    0, 0, NULL);
    regp[COMMAND]   = g_regex_new(P_COMMAND, G_REGEX_RAW, 0, NULL);
    regp[PSEUDO]    = g_regex_new(P_PSEUDO,  G_REGEX_RAW, 0, NULL);
    regp[MESSAGE]   = g_regex_new(P_MESSAGE, 0, 0, NULL);
    regp[USER]      = g_regex_new(P_USER, 0, 0, NULL);
    regp[USER_FIRST_LINE] = g_regex_new(P_USER_FIRST_LINE, 0, 0, NULL);
    regp[USER_FORMATTED]  = g_regex_new(P_USER_FORMATTED, G_REGEX_RAW, 0, NULL);

    /* hash table for user's icons 
     * the key is owned by requested_user */
    icon_id_by_user = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, NULL);

    /* attach counter to the existing twitter window */
    if(purple_prefs_get_bool(OPT_COUNTER)) {
        attach_to_window();
    }

    return TRUE;
}

static gboolean
unload_plugin(PurplePlugin *plugin)
{
    GList *list;

    twitter_debug("called\n");

    /* disconnect from signal */
    purple_signal_disconnect(purple_conversations_get_handle(),
                             "writing-im-msg",
                             plugin, PURPLE_CALLBACK(writing_im_cb));
    purple_signal_disconnect(purple_conversations_get_handle(),
                             "sending-im-msg",
                             plugin, PURPLE_CALLBACK(sending_im_cb));
    purple_signal_disconnect(purple_conversations_get_handle(),
                             "conversation-created",
                             plugin, PURPLE_CALLBACK(conv_created_cb));
    purple_signal_disconnect(pidgin_conversations_get_handle(),
                             "displayed-im-msg",
                             plugin, PURPLE_CALLBACK(displayed_im_cb));
    purple_signal_disconnect(purple_conversations_get_handle(),
                             "receiving-im-msg",
                             plugin, PURPLE_CALLBACK(receiving_im_cb));
    purple_signal_disconnect(purple_conversations_get_handle(),
                             "deleting-conversation",
                             plugin, PURPLE_CALLBACK(deleting_conv_cb));

    /* unreference regp */
    g_regex_unref(regp[RECIPIENT]);
    g_regex_unref(regp[SENDER]);
    g_regex_unref(regp[COMMAND]);
    g_regex_unref(regp[PSEUDO]);
    g_regex_unref(regp[MESSAGE]);
    g_regex_unref(regp[USER]);
    g_regex_unref(regp[USER_FIRST_LINE]);
    g_regex_unref(regp[USER_FORMATTED]);

    g_list_free(requested_icon_marks);
    requested_icon_marks = NULL;

    /* cancel request that has not been finished yet */
    for(list = g_list_first(requestings); list; list = g_list_next(list)) {
        purple_util_fetch_url_cancel(list->data);
    }
    g_list_free(requestings);
    requestings = NULL;

    /* destroy hash table for icons */
    g_hash_table_destroy(icon_id_by_user);
    for(list = g_list_first(requested_users); list; list = g_list_next(list)) {
        g_free(list->data);
    }
    g_list_free(requested_users);
    requested_users = NULL;

    /* detach from twitter window */
    detach_from_window();

    return TRUE;
}

static void
counter_prefs_cb(const char *name, PurplePrefType type,
                 gconstpointer val, gpointer data)
{
    gboolean enabled = purple_prefs_get_bool(OPT_COUNTER);

    if(enabled) {
        attach_to_window();
    }
    else {
        detach_from_window();
    }
}

static PurplePluginPrefFrame *
get_plugin_pref_frame(PurplePlugin *plugin)
{
    /* create gtk elements for the plugin preferences */
    PurplePluginPref *pref;
    PurplePluginPrefFrame *frame = purple_plugin_pref_frame_new();

    /************************/
    /* translatione heading */
    /************************/
    pref = purple_plugin_pref_new_with_label("Translation Configurations");
    purple_plugin_pref_frame_add(frame, pref);

    /* translation settings */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_TRANSLATE_RECIPIENT,
                                                      "Translate @username to link");
    purple_plugin_pref_frame_add(frame, pref);

    pref = purple_plugin_pref_new_with_name_and_label(OPT_TRANSLATE_SENDER,
                                                      "Translate sender name to link");
    purple_plugin_pref_frame_add(frame, pref);


    /*************************/
    /* miscellaneous heading */
    /*************************/
    pref = purple_plugin_pref_new_with_label("Miscellaneous Configurations");
    purple_plugin_pref_frame_add(frame, pref);

    /* escape pseudo command setting */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_ESCAPE_PSEUDO,
                                                      "Escape pseudo command string");
    purple_plugin_pref_frame_add(frame, pref);

    /* show text counter  */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_COUNTER,
                                                      "Show text counter");
    purple_plugin_pref_frame_add(frame, pref);

    purple_prefs_connect_callback(plugin, OPT_COUNTER, counter_prefs_cb, NULL);

    /* suppress oops message */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_SUPPRESS_OOPS,
                                                      "Suppress oops message");
    purple_plugin_pref_frame_add(frame, pref);


    /*****************/
    /* sound heading */
    /*****************/
    pref = purple_plugin_pref_new_with_label("Sound Configurations");
    purple_plugin_pref_frame_add(frame, pref);

    /* sound settings for recipient */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_PLAYSOUND_RECIPIENT,
                                                      "Play sound on a reply to the user in the recipient list");
    purple_plugin_pref_frame_add(frame, pref);

    /* recipient list */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_USERLIST_RECIPIENT,
                                                      "Recipient List");
    purple_plugin_pref_frame_add(frame, pref);

    /* sound id selector */
    pref =
        purple_plugin_pref_new_with_name_and_label(OPT_SOUNDID_RECIPIENT,
                                                   "Recipient Sound");

    purple_plugin_pref_set_type(pref, PURPLE_PLUGIN_PREF_CHOICE);
    purple_plugin_pref_add_choice(pref, "Arrive", GINT_TO_POINTER(0));
    purple_plugin_pref_add_choice(pref, "Leave", GINT_TO_POINTER(1));
    purple_plugin_pref_add_choice(pref, "Receive", GINT_TO_POINTER(2));
    purple_plugin_pref_add_choice(pref, "Fist Receive", GINT_TO_POINTER(3));
    purple_plugin_pref_add_choice(pref, "Send", GINT_TO_POINTER(4));
    purple_plugin_pref_add_choice(pref, "Chat Join", GINT_TO_POINTER(5));
    purple_plugin_pref_add_choice(pref, "Chat Leave", GINT_TO_POINTER(6));
    purple_plugin_pref_add_choice(pref, "Chat You Say", GINT_TO_POINTER(7));
    purple_plugin_pref_add_choice(pref, "Chat Someone Say", GINT_TO_POINTER(8));
    purple_plugin_pref_add_choice(pref, "Pounce Default", GINT_TO_POINTER(9));
    purple_plugin_pref_add_choice(pref, "Chat Nick Said", GINT_TO_POINTER(10));

    purple_plugin_pref_frame_add(frame, pref);

    /* sound setting for sender */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_PLAYSOUND_SENDER,
                                                      "Play sound if sender of a message is in the sender list");
    purple_plugin_pref_frame_add(frame, pref);

    /* sender list */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_USERLIST_SENDER,
                                                      "Sender List");
    purple_plugin_pref_frame_add(frame, pref);

    /* sound id selector */
    pref =
        purple_plugin_pref_new_with_name_and_label(OPT_SOUNDID_SENDER,
                                                   "Sender Sound");

    purple_plugin_pref_set_type(pref, PURPLE_PLUGIN_PREF_CHOICE);
    purple_plugin_pref_add_choice(pref, "Arrive", GINT_TO_POINTER(0));
    purple_plugin_pref_add_choice(pref, "Leave", GINT_TO_POINTER(1));
    purple_plugin_pref_add_choice(pref, "Receive", GINT_TO_POINTER(2));
    purple_plugin_pref_add_choice(pref, "Fist Receive", GINT_TO_POINTER(3));
    purple_plugin_pref_add_choice(pref, "Send", GINT_TO_POINTER(4));
    purple_plugin_pref_add_choice(pref, "Chat Join", GINT_TO_POINTER(5));
    purple_plugin_pref_add_choice(pref, "Chat Leave", GINT_TO_POINTER(6));
    purple_plugin_pref_add_choice(pref, "Chat You Say", GINT_TO_POINTER(7));
    purple_plugin_pref_add_choice(pref, "Chat Someone Say", GINT_TO_POINTER(8));
    purple_plugin_pref_add_choice(pref, "Pounce Default", GINT_TO_POINTER(9));
    purple_plugin_pref_add_choice(pref, "Chat Nick Said", GINT_TO_POINTER(10));

    purple_plugin_pref_frame_add(frame, pref);

    /************************/
    /* notification heading */
    /************************/
    pref = purple_plugin_pref_new_with_label("Notification Configuration");
    purple_plugin_pref_frame_add(frame, pref);

    /* notification setting */
    pref = purple_plugin_pref_new_with_name_and_label(OPT_PREVENT_NOTIFICATION,
                                "Prevent notifications of incoming messages");
    purple_plugin_pref_frame_add(frame, pref);

    return frame;
}

static PurplePluginUiInfo pref_info = {
    get_plugin_pref_frame
};

static PurplePluginInfo info = {
    PURPLE_PLUGIN_MAGIC,
    PURPLE_MAJOR_VERSION,
    PURPLE_MINOR_VERSION,
    PURPLE_PLUGIN_STANDARD,     /**< type	*/
    NULL,                       /**< ui_req	*/
    0,                          /**< flags	*/
    NULL,                       /**< deps	*/
    PURPLE_PRIORITY_DEFAULT,    /**< priority	*/
    PLUGIN_ID,                  /**< id		*/
    "Pidgin-Twitter",           /**< name	*/
    "0.6.0",                    /**< version	*/
    "replaces usernames with links and plays sounds", /**  summary	*/
    "replaces usernames with links and plays sounds", /**  desc	*/
    "Yoshiki Yazawa and the pidging-twitter team",     /**< author	*/
    "http://www.honeyplanet.jp/",   /**< homepage	*/
    load_plugin,                /**< load	*/
    unload_plugin,              /**< unload	*/
    NULL,                       /**< destroy	*/
    NULL,                       /**< ui_info	*/
    NULL,                       /**< extra_info	*/
    &pref_info,                 /**< pref info	*/
    NULL
};

static void
init_plugin(PurplePlugin *plugin)
{
    g_type_init();

    /* add plugin preferences */
    purple_prefs_add_none(OPT_PIDGINTWITTER);
    purple_prefs_add_bool(OPT_TRANSLATE_RECIPIENT, TRUE);
    purple_prefs_add_bool(OPT_TRANSLATE_SENDER, TRUE);
    purple_prefs_add_bool(OPT_ESCAPE_PSEUDO, TRUE);

    purple_prefs_add_bool(OPT_PLAYSOUND_RECIPIENT, TRUE);
    purple_prefs_add_bool(OPT_PLAYSOUND_SENDER, TRUE);
    purple_prefs_add_int(OPT_SOUNDID_RECIPIENT, PURPLE_SOUND_POUNCE_DEFAULT);
    purple_prefs_add_string(OPT_USERLIST_RECIPIENT, DEFAULT_LIST);
    purple_prefs_add_int(OPT_SOUNDID_SENDER, PURPLE_SOUND_POUNCE_DEFAULT);
    purple_prefs_add_string(OPT_USERLIST_SENDER, DEFAULT_LIST);

    purple_prefs_add_bool(OPT_COUNTER, TRUE);
    purple_prefs_add_bool(OPT_SUPPRESS_OOPS, TRUE);

    purple_prefs_add_bool(OPT_PREVENT_NOTIFICATION, FALSE);
}

PURPLE_INIT_PLUGIN(pidgin_twitter, init_plugin, info)