view pidgin-twitter.c @ 91:2b7ef3538cd3

- Fix type of 'flag' of writing_im_cb - Support a function which displays icons of sent messages via IM.
author Konosuke Watanabe <sasugaanija@gmail.com>
date Sun, 06 Jul 2008 17:43:44 +0900
parents fb9831fae969
children 7f20a61e98ad
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 "pidgin-twitter.h"

/* globals */
static GRegex *regp[7];
static gboolean suppress_oops = FALSE;
static GHashTable *icon_data_by_user = NULL;
static GHashTable *icon_data_by_user2 = NULL;

#define WASSR_POST_LEN (255 * 4)
static gchar *wassr_post = NULL;

typedef struct _icon_data {
    gint icon_id;        // image id
    gboolean requested;  // TRUE if download icon has been requested
    GList *request_list; // marker list
    PurpleUtilFetchUrlData *fetch_data;          // icon fetch data
} icon_data;

enum {
    unknown_service,
    twitter_service,
    wassr_service
};

typedef struct _eval_data {
    gint which;
    gint service;
} eval_data;

typedef struct _got_icon_data {
    gchar *user_name;
    gint service;
} got_icon_data;

/* 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;
}

/* this function has been taken from autoaccept plugin */
static gboolean
ensure_path_exists(const char *dir)
{
	if (!g_file_test(dir, G_FILE_TEST_IS_DIR)) {
		if (purple_build_dir(dir, S_IRUSR | S_IWUSR | S_IXUSR))
			return FALSE;
	}

	return TRUE;
}


/**********************/
/* 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;
}

#define TWITTER_STATUS_POST "POST /statuses/update.xml HTTP/1.0\r\n" \
    "Host: twitter.com\r\n"                                          \
    "User-Agent: Pidgin-Twitter\r\n"                                 \
    "Authorization: Basic %s\r\n"                                    \
    "Content-Length: %d\r\n\r\n"

#define TWITTER_STATUS_FORMAT "status=%s"
#define TWITTER_STATUS_TERMINATOR "\r\n\r\n"

#define TWITTER_BASE_URL "http://twitter.com"

typedef struct twitter_message {
    PurpleAccount *account;
    char *status;
    time_t time;
} twitter_message_t;

static void
post_status_with_api_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data,
                        const gchar *url_text, size_t len,
                        const gchar *error_message)
{
    twitter_message_t *tm = (struct twitter_message *)user_data;
    gchar *msg = NULL;
    char *p1 = NULL, *p2 = NULL;
    int error = 1;
    PurpleConversation *conv;

    conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY,
                                                 "twitter@twitter.com",
                                                 tm->account);
    if (!conv) {
        twitter_debug("failed to get conversation\n");
        goto fin;
    }

    if (error_message) {
        /* connection failed or something */
        msg = g_strdup_printf("Local error: %s", error_message);
    } else {
        int code = -1;

        if ((strncmp(url_text, "HTTP/1.0", strlen("HTTP/1.0")) == 0
             || strncmp(url_text, "HTTP/1.1", strlen("HTTP/1.1")) == 0)) {

            p1 = strchr(url_text, ' ');

            if (p1) {
                p1++;
                p2 = strchr(p1, ' ');
                if (p2)
                    p2++;
                else
                    p2 = NULL;
            }
        }

        code = atoi(p1);

        if (code == 200) {
            error = 0;
        } else {
            switch (code) {
            case 400:
                msg = g_strdup("Invalid request. Too many updates?");
                break;
            case 401:
                msg = g_strdup("Authorization failed.");
                break;
            case 403:
                msg = g_strdup("Your update has been refused by Twitter server "
                               "for some reason.");
                break;
            case 404:
                msg = g_strdup("Requested URI is not found.");
                break;
            case 500:
                msg = g_strdup("Server error.");
                break;
            case 502:
                msg = g_strdup("Twitter is down or under maintenance.");
                break;
            case 503:
                msg = g_strdup("Twitter is extremely crowded. "
                               "Try again later.");
                break;
            default:
                msg = g_strdup_printf("Unknown error. (%d %s)",
                                      code, p2 ? p2 : "");
                break;
            }
        }
    }

    if (!error) {
        purple_conv_im_write(conv->u.im,
                             purple_account_get_username(tm->account),
                             tm->status, PURPLE_MESSAGE_SEND, tm->time);
    } else {
        gchar *m;
        m = g_strdup_printf("%s<BR>%s",
                            msg, tm->status);
        /* FIXME: too strong. it should be more smart */
        purple_conv_im_write(conv->u.im,
                             purple_account_get_username(tm->account),
                             m, PURPLE_MESSAGE_ERROR, time(NULL));
        g_free(m);
    }

 fin:
    if (msg)
        g_free(msg);

    if (tm) {
        if (tm->status)
            g_free(tm->status);
        g_free(tm);
    }

}

static void
post_status_with_api(PurpleAccount *account, char **buffer)
{
    char *request, *status, *header;
    const char *url_encoded = purple_url_encode(*buffer);
    char *basic_auth, *basic_auth_encoded;

    twitter_message_t *tm;

    const char *screen_name = purple_prefs_get_string(OPT_SCREEN_NAME);
    const char *password = purple_prefs_get_string(OPT_PASSWORD);

    twitter_debug("tm.account: %s\n",
                  purple_account_get_username(account));

    if (!screen_name || !password || !screen_name[0] || !password[0]) {
        twitter_debug("screen_name or password is empty\n");
        return;
    }

    tm = g_new(twitter_message_t, 1);
    tm->account = account;
    tm->status = g_strdup(*buffer);
    tm->time = time(NULL);

    basic_auth = g_strdup_printf("%s:%s", screen_name, password);
    basic_auth_encoded = purple_base64_encode((unsigned char *)basic_auth,
                                              strlen(basic_auth));
    g_free(basic_auth);

    status = g_strdup_printf(TWITTER_STATUS_FORMAT, url_encoded);

    header = g_strdup_printf(TWITTER_STATUS_POST, basic_auth_encoded,
                             (int)strlen(status));

    request = g_strconcat(header, status, TWITTER_STATUS_TERMINATOR, NULL);

    purple_util_fetch_url_request(TWITTER_BASE_URL, FALSE,
                                  NULL, TRUE, request, TRUE,
                                  post_status_with_api_cb, tm);

    g_free(header);
    g_free(basic_auth_encoded);
    g_free(status);
    g_free(request);

}

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

    twitter_debug("called\n");

    if(is_wassr_account(account, recipient)) {
        /* store sending message to address parrot problem */
        g_strlcpy(wassr_post, *buffer, WASSR_POST_LEN);
        twitter_debug("parrot pushed:%s\n", *buffer);
    }

    /* 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(is_twitter_account(account, recipient) &&
       purple_prefs_get_bool(OPT_ESCAPE_PSEUDO)) {
        escape(buffer);
    }

    /* update status with Twitter API instead of IM protocol */
    if (purple_prefs_get_bool(OPT_API_BASE_POST)) {
        if (buffer && *buffer) {
            post_status_with_api(account, buffer);
            (*buffer)[0] = '\0';
        }
        return FALSE;
    }

    /* 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)
{
    eval_data *data = (eval_data *)user_data;
    gint which = data->which;
    gint service = data->service;
    gchar sub[128];

    twitter_debug("which = %d service = %d\n", which, service);

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

        switch(service) {
        case twitter_service:
            snprintf(sub, 128, RECIPIENT_FORMAT, match, match);
            break;
        case wassr_service:
            snprintf(sub, 128, RECIPIENT_FORMAT_WASSR, match, match);
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }

        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

        switch(service) {
        case twitter_service:
            snprintf(sub, 128, SENDER_FORMAT, match1 ? match1: "",
                     match2, match2);
            break;
        case wassr_service:
            snprintf(sub, 128, SENDER_FORMAT_WASSR, match1 ? match1: "",
                     match2, match2);
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }

        g_free(match1);
        g_free(match2);
    }

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

    return FALSE;
}

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

    eval_data *data = g_new0(eval_data, 1);

    data->which = which;
    data->service = service;

    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
                                  data,   // user data
                                  NULL);    // error handler

    g_free(data); data = NULL;

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

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

static void
playsound(gchar **str, gint 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");

    gint service = get_service_type(conv);

    /* check if the conversation is between twitter */
    if(service == unknown_service)
        return FALSE;

    /* Add screen_name if the current message is posted by owner */
    if (flags & PURPLE_MESSAGE_SEND) {
        gchar *m = NULL;
        const char *screen_name = purple_prefs_get_string(OPT_SCREEN_NAME);
        if (screen_name) {
            m = g_strdup_printf("%s: %s", screen_name, *buffer);
            g_free(*buffer);
            *buffer = m;
        }
    }

    /* 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, service);
    }
    if(purple_prefs_get_bool(OPT_TRANSLATE_RECIPIENT)) {
        translate(buffer, RECIPIENT, service);
    }

    /* escape pseudo command (to show same result to sending message) */
    if(is_twitter_conv(conv) && 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)
{
    PurpleConversation *conv = (PurpleConversation *)user_data;
    PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);

    GtkWidget *box, *counter = NULL;
    gchar *markup = NULL;
    guint count;

    gint service = get_service_type(conv);

    g_return_if_fail(gtkconv != NULL);

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

    switch(service) {
    case twitter_service:
        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         count <= 140 ? "black" : "red", count);
        break;
    case wassr_service:
        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         count <= 255 ? "black" : "red", count);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    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)
{
    PurpleConversation *conv = (PurpleConversation *)user_data;
    PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
    GtkWidget *box, *counter = NULL;
    gchar *markup = NULL;
    gint service = get_service_type(conv);

    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));

    switch(service) {
    case twitter_service:
        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         count <= 140 ? "black" : "red", count);
        break;
    case wassr_service:
        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         count <= 255 ? "black" : "red", count);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    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);
        gint service = get_service_type(conv);
        switch(service) {
        case twitter_service:
            detach_from_conv(conv, NULL);
            break;
        case wassr_service:
            detach_from_conv(conv, NULL);
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }
    }
}

static void
detach_from_conv(PurpleConversation *conv, gpointer null)
{
    PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
    GtkWidget *box, *counter = NULL, *sep = NULL;

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

    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
remove_marks_func(gpointer key, gpointer value, gpointer user_data)
{
    icon_data *data = (icon_data *)value;
    GtkTextBuffer *text_buffer = (GtkTextBuffer *)user_data;
    GList *mark_list = NULL;
    GList *current;

    if(data && data->request_list)
        mark_list = data->request_list;

    /* remove the marks in its GtkTextBuffers */
    for(current = g_list_first(mark_list); current;
        current = g_list_next(current)) {
        GtkTextMark *current_mark = current->data;
        GtkTextBuffer *current_text_buffer = gtk_text_mark_get_buffer(
            current_mark);

        if(!current_text_buffer)
            continue;

        if(text_buffer) {
            if(current_text_buffer == text_buffer) {
                /* the mark will be freed in this function */
                gtk_text_buffer_delete_mark(current_text_buffer,
                                            current_mark);
                current->data = NULL;
            }
        }
        else {
            gtk_text_buffer_delete_mark(current_text_buffer, current_mark);
            current->data = NULL;
        }
    } /* end of for */

    mark_list = g_list_remove_all(mark_list, NULL);
    data->request_list = mark_list;
}

static void
delete_requested_icon_marks(PidginConversation *conv, GHashTable *table) {
    GtkTextBuffer *text_buffer = gtk_text_view_get_buffer(
        GTK_TEXT_VIEW(conv->imhtml));

    g_hash_table_foreach(table,
                         (GHFunc)remove_marks_func,
                         (gpointer)text_buffer);
}

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);
        gint service = get_service_type(conv);
        /* only attach to twitter conversation window */
        switch(service) {
        case twitter_service:
        case wassr_service:
            attach_to_conv(conv, NULL);
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }
    }
}

static void
attach_to_conv(PurpleConversation *conv, gpointer null)
{
    PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
    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), conv);
    g_signal_connect(G_OBJECT(gtkconv->entry_buffer), "delete_range",
                     G_CALLBACK(delete_text_cb), conv);

    /* 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 gboolean
is_wassr_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(strstr(name, "wassr-bot@wassr.jp") &&
       !strcmp(proto, "prpl-jabber")) {
        return TRUE;
    }

    return FALSE;
}

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

    return is_wassr_account(account, name);
}

static gint
get_service_type(PurpleConversation *conv)
{
    gint service = unknown_service;

    g_return_val_if_fail(conv != NULL, unknown_service);

    if(is_twitter_conv(conv))
        service = twitter_service;
    else if(is_wassr_conv(conv))
        service = wassr_service;

    return service;
}

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

    gint service = get_service_type(conv);
    /* only attach to twitter conversation window */
    switch(service) {
    case twitter_service:
    case wassr_service:
        attach_to_conv(conv, NULL);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }
}

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

    gint service = get_service_type(conv);
    /* only attach to twitter conversation window */
    switch(service) {
    case twitter_service:
        delete_requested_icon_marks(gtkconv, icon_data_by_user);
        break;
    case wassr_service:
        delete_requested_icon_marks(gtkconv, icon_data_by_user2);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }
}

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 if the conv is not NULL to avoid a clash.
     * conv is null when the conversation window has not opened yet.
     * And if check is a twitter conv. */

    /* quick hack to suppress annoying completion message from wassr */
    if(conv && is_wassr_conv(conv)) {
        if(strstr(*buffer, "<body>投稿完了:") ||
           strstr(*buffer, "<body>チャンネル投稿完了:")) {
            twitter_debug("clearing sender and buffer\n");
            g_free(*sender); *sender = NULL;
            g_free(*buffer); *buffer = NULL;
        }
        /* fix for parrot problem during post to a channel */
        else if(strlen(wassr_post) && strstr(*buffer, wassr_post)) {
            twitter_debug("parrot clearing: buf = %s post = %s\n", *buffer, wassr_post);
            g_free(*sender); *sender = NULL;
            g_free(*buffer); *buffer = NULL;
        }
    }

    if(!(conv && is_twitter_conv(conv))) {
        return FALSE;
    }

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

    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_icon_at_mark(GtkTextMark *requested_mark, gpointer user_data)
{
    got_icon_data *gotdata = (got_icon_data *)user_data;

    gchar *user_name = gotdata->user_name;
    int service = gotdata->service;

    GList *win_list;
    GtkIMHtml *target_imhtml = NULL;
    GtkTextBuffer *target_buffer = NULL;
    GtkTextIter inserting_point;
    int icon_id;
    icon_data *data = NULL;

    twitter_debug("called: service = %d\n", service);

    /* 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;

            gint service = get_service_type(purple_conv);

            if(service != unknown_service) {
                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);

    /* insert icon */
    switch(service) {
    case twitter_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user, user_name);
        break;
    case wassr_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user2, user_name);
        break;
    default:
        twitter_debug("unknown service\n");
    }

    if(data)
        icon_id = data->icon_id;
    else
        icon_id = 0;

    if(!icon_id) {
        return;
    }

    /* insert icon actually */
    gtk_imhtml_insert_image_at_iter(target_imhtml, icon_id, &inserting_point);
    gtk_text_buffer_delete_mark(target_buffer, requested_mark);
    requested_mark = NULL;
}

static void
insert_requested_icon(const gchar *user_name, int service)
{
    icon_data *data = NULL;
    GList *mark_list = NULL;

    switch(service) {
    case twitter_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user, user_name);
        break;
    case wassr_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user2, user_name);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(!data)
        return;

    mark_list = data->request_list;

    got_icon_data *gotdata = g_new0(got_icon_data, 1);
    gotdata->user_name = g_strdup(user_name);
    gotdata->service = service;

    if(mark_list) {
        g_list_foreach(mark_list, (GFunc) insert_icon_at_mark, gotdata);
        mark_list = g_list_remove_all(mark_list, NULL);
        g_list_free(mark_list);
        data->request_list = NULL;
    }
    g_free(gotdata->user_name);
    g_free(gotdata);
}

static void
got_icon_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data,
                   const gchar *url_text, gsize len, const gchar *error_message)
{
    got_icon_data *gotdata = (got_icon_data *)user_data;
    gchar *user_name = gotdata->user_name;
    int service = gotdata->service;

    int icon_id;
    icon_data *data = NULL;

    twitter_debug("called: service = %d\n", service);

    switch(service) {
    case twitter_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user, user_name);
        break;
    case wassr_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user2, user_name);
        break;
    default:
        twitter_debug("unknown service\n");
    }

    if(data && data->fetch_data) {
        data->fetch_data = NULL;
    }

    /* Return if user's icon had already been downloaded or
     * the download is failure. */
    if((data && data->icon_id) || !url_text) { //xxx
        if(!url_text) {
            twitter_debug("downloading %s's icon failed : %s\n",
                          user_name, error_message);
        }
        else {
            twitter_debug("%s's icon has already been downloaded\n", user_name);
        }

        data->requested = FALSE;

        g_free(user_name);
        g_free(data);
        return;
    }

    icon_id = purple_imgstore_add_with_id(g_memdup(url_text, len), len,
                                          user_name);
    if(!data) {
        data = g_new0(icon_data, 1);
    }

    data->icon_id = icon_id;

    switch(service) {
    case twitter_service:
        g_hash_table_insert(icon_data_by_user,
                            g_strdup(user_name), data);
        break;
    case wassr_service:
        g_hash_table_insert(icon_data_by_user2,
                            g_strdup(user_name), data);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    const gchar *dirname = purple_prefs_get_string(OPT_ICON_DIR);

    /* store retrieved image to a file in icon dir */
    if(ensure_path_exists(dirname)) {
        gchar *filename = NULL;
        gchar *path = NULL;
        FILE *fp = NULL;

        switch(service) {
        case twitter_service:
            filename = g_strdup_printf("%s.gif", user_name);
            break;
        case wassr_service:
            filename = g_strdup_printf("%s_wassr.png", user_name);
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }

        path = g_build_filename(dirname, filename, NULL);
        g_free(filename); filename = NULL;

        fp = fopen(path, "wb");
        g_free(path); path = NULL;

        if(fp) {
            int wrotelen;
            wrotelen = fwrite(url_text, 1, len, fp);
        }

        fclose(fp); fp = NULL;
    }

    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. */
    insert_requested_icon(user_name, service);
    g_free(user_name);
    g_free(data);
}

static void
request_icon(const char *user_name, int service)
{
    gchar *url = NULL;

    /* look local icon cache for the requested icon */
    gchar *filename = NULL;
    gchar *path = NULL;
    icon_data *data = NULL;

    switch(service) {
    case twitter_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user, user_name);
        break;
    case wassr_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user2, user_name);
        break;
    default:
        twitter_debug("unknown service\n");
        return;
        break;
    }

    if(!data) {
        data = g_new0(icon_data, 1);
        switch(service) {
        case twitter_service:
            g_hash_table_insert(icon_data_by_user,
                                g_strdup(user_name), data);
            break;
        case wassr_service:
            g_hash_table_insert(icon_data_by_user2,
                                g_strdup(user_name), data);
            break;
        default:
            twitter_debug("unknown service\n");
            return;
            break;
        }
    }

    /* if img has been registerd, just return */
    if(data->icon_id)
        return;

    /* check if saved file exists */
    switch(service) {
    case twitter_service:
        filename = g_strdup_printf("%s.gif", user_name);
        path = g_build_filename(
            purple_prefs_get_string(OPT_ICON_DIR), filename, NULL);

        if(!g_file_test(path, G_FILE_TEST_EXISTS)) {
            g_free(path);
            filename = g_strdup_printf("%s.png", user_name);
            path = g_build_filename(
                purple_prefs_get_string(OPT_ICON_DIR), filename, NULL);
        }
        break;
    case wassr_service:
        filename = g_strdup_printf("%s_wassr.gif", user_name);
        path = g_build_filename(
            purple_prefs_get_string(OPT_ICON_DIR), filename, NULL);

        if(!g_file_test(path, G_FILE_TEST_EXISTS)) {
            g_free(path);
            filename = g_strdup_printf("%s_wassr.png", user_name);
            path = g_build_filename(
                purple_prefs_get_string(OPT_ICON_DIR), filename, NULL);
        }
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    /* build image from file, if file exists */
    if(g_file_test(path, G_FILE_TEST_EXISTS)) {
        gchar *imgdata = NULL;
        size_t len;
        GError *err = NULL;

        if (!g_file_get_contents(path, &imgdata, &len, &err)) {
            twitter_debug("Error reading %s: %s\n",
                               path, err->message);
            g_error_free(err);
        }

        data->icon_id = purple_imgstore_add_with_id(imgdata, len, path);
        g_free(filename);
        g_free(path);

        twitter_debug("icon data has been loaded from file\n");

        insert_requested_icon(user_name, service);
        return;
    }

    /* Return if user's icon has been requested already. */
    if(data->requested)
        return;
    else
        data->requested = TRUE;


    /* Create the URL of the user's icon.
     * See http://twitter.g.hatena.ne.jp/ikko615/20080107/1199703400
     */
    switch(service) {
    case twitter_service:
        url = g_strdup_printf("http://img.twitty.jp/twitter/user/%s/m.gif",
                              user_name);
        break;
    case wassr_service:
        url = g_strdup_printf("http://wassr.jp/user/%s/profile_img.png.64",
                              user_name);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    got_icon_data *gotdata = g_new0(got_icon_data, 1);
    gotdata->user_name = g_strdup(user_name);
    gotdata->service = service;

    /* gotdata will be released in got_icon_cb */
    data->fetch_data = purple_util_fetch_url(url, TRUE, NULL, TRUE,
                                       got_icon_cb, gotdata);
    g_free(url); url = NULL;

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

static void
mark_icon_for_user(GtkTextMark *mark, const gchar *user_name, int service)
{
    icon_data *data = NULL;

    twitter_debug("called\n");

    switch(service) {
    case twitter_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user, user_name);
        break;
    case wassr_service:
        data = (icon_data *)g_hash_table_lookup(icon_data_by_user2, user_name);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(!data) {
        data = g_new0(icon_data, 1);
        switch(service) {
        case twitter_service:
            g_hash_table_insert(icon_data_by_user,
                                g_strdup(user_name), data);
            break;
        case wassr_service:
            g_hash_table_insert(icon_data_by_user2,
                                g_strdup(user_name), data);
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }
    }

    data->request_list = g_list_append(data->request_list, mark);
}

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

    twitter_debug("called\n");

    if(service == unknown_service) {
        twitter_debug("neither twitter or wassr conv\n");
        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);

    switch(service) {
    case twitter_service:
        data = g_hash_table_lookup(icon_data_by_user, user_name);
        break;
    case wassr_service:
        data = g_hash_table_lookup(icon_data_by_user2, user_name);
        break;
    }

    if(data)
        icon_id = data->icon_id;
    else
        icon_id = 0;

    /* If the user's icon has not been downloaded,
     * mark the message and request the icon. */
    if(!icon_id) {
        twitter_debug("%s's icon is not in memory.\n", user_name);
        mark_icon_for_user(gtk_text_buffer_create_mark(
                                text_buffer, NULL, &inserting_point, FALSE),
                           user_name, service);
        /* request to attach icon to the buffer */
        request_icon(user_name, service);
        g_free(user_name); user_name = NULL;
        return;
    }

    gtk_imhtml_insert_image_at_iter(imhtml, icon_id, &inserting_point);
    g_free(user_name); user_name = NULL;

    twitter_debug("reach end of function\n");
}

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[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);

    icon_data_by_user = g_hash_table_new_full(g_str_hash, g_str_equal,
                                              g_free, NULL);
    icon_data_by_user2 = g_hash_table_new_full(g_str_hash, g_str_equal,
                                              g_free, NULL);

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

    /* allocate wassr_post */
    wassr_post = g_new0(gchar, WASSR_POST_LEN + 1);

    return TRUE;
}

static void
cancel_fetch_func(gpointer key, gpointer value, gpointer user_data)
{
    icon_data *data = (icon_data *)value;

    if(!data)
        return;

    if(data->fetch_data) {
        purple_util_fetch_url_cancel(data->fetch_data);
        data->fetch_data = NULL;
    }

    if(data->request_list) {
        twitter_debug("somehow, request_list != NULL\n");
    }

    return;
}

static gboolean
unload_plugin(PurplePlugin *plugin)
{
    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[USER]);
    g_regex_unref(regp[USER_FIRST_LINE]);
    g_regex_unref(regp[USER_FORMATTED]);

    /* remove mark list in each hash entry */
    g_hash_table_foreach(icon_data_by_user, (GHFunc)remove_marks_func, NULL);
    g_hash_table_foreach(icon_data_by_user2, (GHFunc)remove_marks_func, NULL);

    /* cancel request that has not been finished yet */
    g_hash_table_foreach(icon_data_by_user, (GHFunc)cancel_fetch_func, NULL);
    g_hash_table_foreach(icon_data_by_user2, (GHFunc)cancel_fetch_func, NULL);

    /* destroy hash table for icon_data */
    g_hash_table_destroy(icon_data_by_user); //XXX all memory freed? --yaz
    g_hash_table_destroy(icon_data_by_user2); //XXX all memory freed? --yaz

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

    /* free wassr_post */
    g_free(wassr_post);
    wassr_post = NULL;

    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);


    /* API based post */

    pref = purple_plugin_pref_new_with_label("API Based Post");
    purple_plugin_pref_frame_add(frame, pref);

    pref = purple_plugin_pref_new_with_name_and_label(OPT_API_BASE_POST, 
                                             "Post Status with API");
    purple_plugin_pref_frame_add(frame, pref);

    pref = purple_plugin_pref_new_with_name_and_label(OPT_SCREEN_NAME,
                                                      "Screen Name");
    purple_plugin_pref_frame_add(frame, pref);

    pref = purple_plugin_pref_new_with_name_and_label(OPT_PASSWORD,
                                                      "Password");
    purple_plugin_pref_set_masked(pref, TRUE);

    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.7.0 alpha2d1",                    /**< version	*/
    "provides useful features for twitter", /**  summary	*/
    "provides useful features for twitter", /**  desc	*/
    "Yoshiki Yazawa, mikanbako, \nKonosuke Watanabe, IWATA Ray, mojin, \nthe 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)
{
    char *dirname = NULL;

    g_type_init();
    dirname = g_build_filename(purple_user_dir(), "pidgin-twitter", "icons", NULL);
    if(dirname)
        purple_prefs_add_string(OPT_ICON_DIR, dirname);
    g_free(dirname);

    /* 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_prefs_add_bool(OPT_API_BASE_POST, FALSE);
    purple_prefs_add_string(OPT_SCREEN_NAME, EMPTY);
    purple_prefs_add_string(OPT_PASSWORD, EMPTY);
}

PURPLE_INIT_PLUGIN(pidgin_twitter, init_plugin, info)