view pidgin-twitter.c @ 222:b168502b73c3

expanded size of substitution buffer to 32KB. 128 bytes were not sufficient if both user name and channel name are rather long. tenforward reported this problem and umq pointed out the cause.
author Yoshiki Yazawa <yaz@honeyplanet.jp>
date Wed, 03 Sep 2008 18:39:39 +0900
parents b5fa0c295ff5
children c3efae72f72a
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 */
/***********/
#define NUM_REGPS 13
#define NUM_SERVICES 3          /* twitter, wassr, identica. */
static GRegex *regp[NUM_REGPS];
static gboolean suppress_oops = FALSE;
static GHashTable *icon_hash[NUM_SERVICES];
static GHashTable *conv_hash = NULL;
static GList *statuseslist = NULL;
static GList *postedlist = NULL;
static GList *wassr_parrot_list = NULL;
static GList *identica_parrot_list = NULL;
#ifdef _WIN32
static gboolean blink_state = FALSE;
static gboolean blink_modified = FALSE;
#endif

static struct _source {
    guint id;
    PurpleConversation *conv;
} source;

#ifndef _WIN32
extern gchar *sanitize_utf(const gchar *msg, gsize len, gsize *newlen) __attribute__ ((weak));
#endif

/*************/
/* functions */
/*************/

/* 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 gchar *
twitter_memrchr(const gchar *s, int c, size_t n)
{
    int nn = n;

    g_return_val_if_fail(s != NULL, NULL);

    while(nn+1) {
        if((int)*(s+nn) == c)
            return (gchar *)(s+nn);
        nn--;
    }
    return NULL;
}

static gchar *html_tags[] = {
    "<a href=",
    "</a>",
    "<b>",
    "</b>",
    "<p>",
    "</p>",
    "<div ",
    "</div>",
    "<span ",
    "</span>",
    "<body>",
    "<body ",
    "</body>",
    "<i>",
    "</i>",
    "<font ",
    "</font>",
    "<br>",
    "<br/>",
    "<img ",
    "<html>",
    "<html ",
    "</html>",
    NULL
};

static gchar *
strip_html_markup(const gchar *src)
{
    gchar *head, *tail;     /* head and tail of html */
    gchar *begin, *end;     /* begin:<  end:> */
    gchar *html, *str;      /* copied src and string to be returned */
/*    gchar *vis1, *vis2; */     /* begin and end of address part */
    gchar *startp;          /* starting point marker */
    gchar **tagp;           /* tag iterator */
    gchar *tmp, *tmp2;      /* scratches */

    g_return_val_if_fail(src != NULL, NULL);

    const gchar *ptr, *ent;
    gchar *ptr2;
    gint entlen;

    /* unescape &x; */
    html = g_malloc0(strlen(src) + 1);
    ptr2 = html;
    for(ptr = src; *ptr; ) {
        if(*ptr == '&') {
            ent = purple_markup_unescape_entity(ptr, &entlen);
            if(ent != NULL) {
                while(*ent) {
                    *ptr2++ = *ent++;
                }
                ptr += entlen;
            }
            else {
                *ptr2++ = *ptr++;
            }
        }
        else {
            *ptr2++ = *ptr++;
        }
    } /* for */

    str = g_strdup("\0");

    head = html;
    tail = head + strlen(html);
    startp = head;

loop:
    begin = NULL;
    end = NULL;

    if(startp >= tail) {
        g_free(html);
        return str;
    }

    end = strchr(startp, '>');
    if(end) {
        begin = twitter_memrchr(startp, '<', end - startp);
        if(begin < startp)
            begin = NULL;

        if(!begin) { /* '>' found but no corresponding '<' */
            tmp = g_strndup(startp, end - startp + 1); /* concat until '>' */
            tmp2 = g_strconcat(str, tmp, NULL);
            g_free(str);
            str = tmp2;
            startp = end + 1;
            goto loop;
        }
    }
    else { /* neither '>' nor '<' were found */
        tmp = g_strconcat(str, startp, NULL); /* concat the rest */
        g_free(str);
        str = tmp;
        g_free(html);
        return str;
    }

    /* here, both < and > are found */
    /* concatenate leading part to dest */
    tmp = g_strndup(startp, begin - startp);
    tmp2 = g_strconcat(str, tmp, NULL);
    g_free(tmp);
    g_free(str);
    str = tmp2;

    /* find tag */
    for(tagp = html_tags; *tagp; tagp++) {
        if(!g_ascii_strncasecmp(begin, *tagp, strlen(*tagp))) {
            /* we found a valid tag */
            /* if tag is <a href=, extract address. */
#if 0
            if(!strcmp(*tagp, "<a href=")) {
                vis1 = NULL; vis2 = NULL;

                vis1 = strchr(begin, '\'');
                if(vis1)
                    vis2 = strchr(vis1+1, '\'');
                if(!vis1) {
                    vis1 = strchr(begin, '\"');
                    if(vis1)
                        vis2 = strchr(vis1+1, '\"');
                }
                if(vis1 && vis2) {
                    *vis2 = '\0';
                    /* generate "[ http://example.com/ ] anchor " */
                    tmp = g_strconcat(str, "[ ", vis1+1, " ]", " ", NULL);
                    g_free(str);
                    str = tmp;
                }
                startp = end + 1;
                goto loop;
            } /* <a href= */
            else {
                /* anything else: discard whole <>. */
                startp = end + 1;
                goto loop;
            }
#else
            /* anything else: discard whole <>. */
            startp = end + 1;
            goto loop;
#endif
        }  /* valid tag */
    }

    /* no valid tag was found: copy <brabra> */
    tmp = g_strndup(begin, end - begin + 1);
    tmp2 = g_strconcat(str, tmp, NULL);
    g_free(tmp);
    g_free(str);
    str = tmp2;
    startp = end + 1;
    goto loop;
}

/* string utilities */
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, gboolean escape)
{
    gchar *plain;

    plain = strip_html_markup(*str);
    g_free(*str);
    if(escape) {
        *str = g_markup_escape_text(plain, -1);
        g_free(plain);
    }
    else {
        *str = plain;
    }
    twitter_debug("result=%s\n", *str);
}


/**************************/
/* API base get functions */
/**************************/
/* xml parser */
static void
parse_user(xmlNode *user, status_t *st)
{
    xmlNode *nptr;

    for(nptr = user->children; nptr != NULL; nptr = nptr->next) {
        if(nptr->type == XML_ELEMENT_NODE) {
            if(!xmlStrcmp(nptr->name, (xmlChar *)"screen_name")) {
                gchar *str = (gchar *)xmlNodeGetContent(nptr);
                st->screen_name = g_strdup(str);
                xmlFree(str);
            }
            else if(!xmlStrcmp(nptr->name, (xmlChar *)"profile_image_url")) {
                gchar *str = (gchar *)xmlNodeGetContent(nptr);
                st->profile_image_url = g_strdup(str);
                xmlFree(str);
            }
        }
    }
}

static gchar *day_of_week_name[] = {
    "Sun",
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri",
    "Sat",
    NULL
};

static gchar *month_name[] = {
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec",
    NULL
};

static void
read_timestamp(const char *str, struct tm *res)
{
    char day_of_week[4];
    char month[4];
    char time_offset[6];
    int day, hour, minute, second, year;
    int i;

    if(str == NULL || res == NULL)
        return;

    sscanf(str, "%s %s %d %d:%d:%d %s %d",
           day_of_week, month, &day,
           &hour, &minute, &second,
           time_offset, &year);

    for(i=0; i<7; i++) {
        if(!strcmp(day_of_week_name[i], day_of_week)) {
            res->tm_wday = i;
        }
    }
    for(i=0; i<12; i++) {
        if(!strcmp(month_name[i], month)) {
            res->tm_mon = i;
        }
    }

    res->tm_mday = day;
    res->tm_hour = hour;
    res->tm_min  = minute;
    res->tm_sec  = second;
    res->tm_year = year - 1900;
#ifndef _WIN32
    int offset   = atoi(time_offset);
    res->tm_gmtoff = -1 * (60 * 60 * offset / 100);
#endif

}

static void
parse_status(xmlNode *status, status_t *st)
{
    xmlNode *nptr;

    for(nptr = status->children; nptr != NULL; nptr = nptr->next) {
        if(nptr->type == XML_ELEMENT_NODE) {
            if(!xmlStrcmp(nptr->name, (xmlChar *)"created_at")) {
                gchar *str = (gchar *)xmlNodeGetContent(nptr);
                st->created_at = g_strdup(str);

                /* read time stamp */
                struct tm res;
                memset(&res, 0x00, sizeof(struct tm));
                read_timestamp(str, &res);
                tzset();
#ifdef _WIN32
                st->time = mktime(&res) - timezone;
#else
                st->time = mktime(&res) + res.tm_gmtoff;
#endif

                xmlFree(str);
            }
            else if(!xmlStrcmp(nptr->name, (xmlChar *)"id")) {
                gchar *str = (gchar *)xmlNodeGetContent(nptr);
                st->id = atoi(str);
                xmlFree(str);
            }
            else if(!xmlStrcmp(nptr->name, (xmlChar *)"text")) {
                gchar *str = (gchar *)xmlNodeGetContent(nptr);
                st->text = g_strdup(str);
                xmlFree(str);
            }
            else if(!xmlStrcmp(nptr->name, (xmlChar *)"user")) {
                parse_user(nptr, st);
            }
        }
    }
}

static void
free_status(status_t *st)
{
    g_free(st->created_at);
    g_free(st->text);
    g_free(st->screen_name);
    g_free(st->profile_image_url);
}

static gboolean
is_posted_message(status_t *status, guint lastid)
{
    GList *pp = g_list_first(postedlist);
    gboolean rv = FALSE;

    while(pp) {
        GList *next;
        status_t *posted = (status_t *)pp->data;

        next = g_list_next(pp);

        if(posted->id == status->id) {
            rv = TRUE;
        }

        if(posted->id <= lastid) {
            free_status(posted);
            g_free(pp->data);
            postedlist = g_list_delete_link(postedlist, pp);
        }

        pp = next;
    }

    return rv;
}

static void
get_status_with_api_cb(PurpleUtilFetchUrlData *url_data, gpointer user_data,
                        const gchar *url_text, size_t len,
                        const gchar *error_message)

{
    xmlDocPtr doc;
    xmlNode *nptr, *nptr2;
    static guint lastid = 0;
    PurpleConversation *conv;
    GList *stp;
    const gchar *start;

    g_return_if_fail(url_text != NULL);

    conv = (PurpleConversation *)user_data;
    if(!conv)
        return;

    /* skip to the beginning of xml */
    start = strstr(url_text, "<?xml");

    doc = xmlRecoverMemory(start, len - (start - url_text));
    if(doc == NULL)
        return;

#ifdef _WIN32
    /* suppress notification of incoming messages. */
    if(purple_prefs_get_bool(OPT_PREVENT_NOTIFICATION)) {
        if(!blink_modified) {
            blink_modified = TRUE;
            blink_state = purple_prefs_get_bool(OPT_PIDGIN_BLINK_IM);
            purple_prefs_set_bool(OPT_PIDGIN_BLINK_IM, FALSE);
        }
    }
#endif

     for(nptr = doc->children; nptr != NULL; nptr = nptr->next) {
        if(nptr->type == XML_ELEMENT_NODE &&
           !xmlStrcmp(nptr->name, (xmlChar *)"statuses")) {

            for(nptr2 = nptr->children; nptr2 != NULL; nptr2 = nptr2->next) {
                if(nptr2->type == XML_ELEMENT_NODE &&
                    !xmlStrcmp(nptr2->name, (xmlChar *)"status")) {
                    status_t *st = g_new0(status_t, 1);
                    statuseslist = g_list_prepend(statuseslist, st);
                    parse_status(nptr2, st);
                }
            }
        }
     }

     xmlFreeDoc(doc);
     xmlCleanupParser();

     /* process statuseslist */
     stp = g_list_first(statuseslist);
     while(stp) {
         GList *next;
         status_t *st = (status_t *)stp->data;

         next = g_list_next(stp);

         if(st->id > lastid && !is_posted_message(st, lastid)) {
             gchar *msg = NULL;
             gchar *sender = NULL;

             sender = g_strdup("twitter@twitter.com");

             PurpleMessageFlags flag = PURPLE_MESSAGE_RECV;

             msg = g_strdup_printf("%s: %s", st->screen_name, st->text);

             /* apply filter*/
             if(purple_prefs_get_bool(OPT_FILTER)) {
                 apply_filter(&sender, &msg, &flag, twitter_service);
             }
             if(sender && msg) {
                 purple_conv_im_write(conv->u.im,
                                      sender,
                                      msg,
                                      flag,
                                      st->time);
             }
             lastid = st->id;

             g_free(msg);
         }

         free_status(st);
         g_free(stp->data);
         statuseslist = g_list_delete_link(statuseslist, stp);

         stp = next;
     }
}

/* status fetching function. it will be called periodically. */
static gboolean
get_status_with_api(gpointer data)
{
    /* fetch friends time line */
    char *request, *header;
    char *basic_auth, *basic_auth_encoded;

    twitter_debug("called\n");

    /* if disabled, just return */
    if(!purple_prefs_get_bool(OPT_API_BASE_POST))
        return TRUE;

    const char *screen_name =
        purple_prefs_get_string(OPT_SCREEN_NAME_TWITTER);
    const char *password =
        purple_prefs_get_string(OPT_PASSWORD_TWITTER);

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

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

    /* header */
    header = g_strdup_printf(TWITTER_STATUS_GET, basic_auth_encoded);
    request = g_strconcat(header, "\r\n", NULL);

    /* invoke fetch */
    purple_util_fetch_url_request(TWITTER_BASE_URL, FALSE,
                                  NULL, TRUE, request, TRUE,
                                  get_status_with_api_cb, data);

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

    return TRUE;
}

/****************************/
/* API based post functions */
/****************************/
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 = (twitter_message_t *)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);

        /* cache message ID that posted via API */
        gchar *start = NULL;
        xmlDocPtr doc;
        xmlNode *nptr;

        start = strstr(url_text, "<?xml");

        if(!start)
            goto fin;

        doc = xmlRecoverMemory(start, len - (start - url_text));
        if(doc == NULL)
            return;

        /* enqueue posted message to postedlist */
        for(nptr = doc->children; nptr != NULL; nptr = nptr->next) {
            if(nptr->type == XML_ELEMENT_NODE &&
               !xmlStrcmp(nptr->name, (xmlChar *)"status")) {
                status_t *st = g_new0(status_t, 1);
                postedlist = g_list_prepend(postedlist, st);
                parse_status(nptr, st);
            }
        }

        xmlFreeDoc(doc);
        xmlCleanupParser();

    } 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_TWITTER);
    const char *password = purple_prefs_get_string(OPT_PASSWORD_TWITTER);

    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, "\r\n", status, "\r\n", 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);

}

/***********************/
/* intrinsic functions */
/***********************/
static gboolean
sending_im_cb(PurpleAccount *account, char *recipient, char **buffer,
              void *data)
{
    int utflen, bytes;
    gboolean twitter_ac = FALSE, wassr_ac = FALSE, identica_ac = FALSE;
    twitter_debug("called\n");

    twitter_ac = is_twitter_account(account, recipient);
    wassr_ac   = is_wassr_account(account, recipient);
    identica_ac = is_identica_account(account, recipient);

    /* strip all markups */
    if(twitter_ac || wassr_ac || identica_ac) {
        gchar *tmp, *plain;
        gsize dummy;

        tmp = strip_html_markup(*buffer);

#ifndef _WIN32
        if(sanitize_utf) {
            plain = sanitize_utf(tmp, -1, &dummy);
            g_free(tmp);
        }
        else
#endif
            plain = tmp;

        if(wassr_ac) {
            /* store sending message to address parrot problem */
            wassr_parrot_list =
                g_list_prepend(wassr_parrot_list, g_strdup(plain));
            twitter_debug("wassr parrot pushed:%s\n", plain);
        }
        else if(identica_ac) {
            /* store sending message to address parrot problem */
            identica_parrot_list =
                g_list_prepend(identica_parrot_list, g_strdup(plain));
            twitter_debug("identica parrot pushed:%s\n", plain);
        }

        g_free(*buffer);
        *buffer = g_markup_escape_text(plain, -1);
        g_free(plain);
    }

    /* return here if the message is not to twitter */
    if(!twitter_ac)
        return FALSE;

    /* escape pseudo command */
    if(twitter_ac &&
       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[SUBST_BUF_SIZE];

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

    if(which == RECIPIENT) {
        gchar *match = g_match_info_fetch(match_info, 1);
        const gchar *format = NULL;
        switch(service) {
        case twitter_service:
            format = RECIPIENT_FORMAT_TWITTER;
            break;
        case wassr_service:
            format = RECIPIENT_FORMAT_WASSR;
            break;
        case identica_service:
            format = RECIPIENT_FORMAT_IDENTICA;
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }
        g_snprintf(sub, SUBST_BUF_SIZE, 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 */
        const gchar *format = NULL;

        switch(service) {
        case twitter_service:
            format = SENDER_FORMAT_TWITTER;
            break;
        case wassr_service:
            format = SENDER_FORMAT_WASSR;
            break;
        case identica_service:
            format = SENDER_FORMAT_IDENTICA;
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }

        g_snprintf(sub, SUBST_BUF_SIZE, format, match1 ? match1: "", match2, match2);

        g_free(match1);
        g_free(match2);
    }
    else if(which == CHANNEL_WASSR && service == wassr_service) {
        gchar *match1 = g_match_info_fetch(match_info, 1); /*before channel*/
        gchar *match2 = g_match_info_fetch(match_info, 2); /* channel */
        const gchar *format = CHANNEL_FORMAT_WASSR;

        g_snprintf(sub, SUBST_BUF_SIZE, format, match1 ? match1: "", match2, match2);

        g_free(match1);
        g_free(match2);
    }
    else if(which == TAG_IDENTICA && service == identica_service) {
        gchar *match = g_match_info_fetch(match_info, 1);
        const gchar *format = TAG_FORMAT_IDENTICA;
        g_snprintf(sub, SUBST_BUF_SIZE, format, match, match);
        g_free(match);
    }

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

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

    regp_id = which; /* for future use --yaz */
    newstr = g_regex_replace_eval(regp[regp_id],  /* 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 = NULL;
    gchar **candidates = NULL, **candidate = NULL;

    list = purple_prefs_get_string(which ? OPT_USERLIST_SENDER :
                                   OPT_USERLIST_RECIPIENT);
    g_return_if_fail(list != NULL);
    if(strstr(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 myself */
    if (flags & PURPLE_MESSAGE_SEND) {
        gchar *m = NULL;
        const char *screen_name = NULL;

        switch(service) {
        case twitter_service:
            screen_name = purple_prefs_get_string(OPT_SCREEN_NAME_TWITTER);
            break;
        case wassr_service:
            screen_name = purple_prefs_get_string(OPT_SCREEN_NAME_WASSR);
            break;
        case identica_service:
            screen_name = purple_prefs_get_string(OPT_SCREEN_NAME_IDENTICA);
            break;
        }

        if (screen_name) {
            m = g_strdup_printf("%s: %s", screen_name, *buffer);
            g_free(*buffer);
            *buffer = m;
        }
    }

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

    /* 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);
    }
    if(service == wassr_service &&
       purple_prefs_get_bool(OPT_TRANSLATE_CHANNEL)) {
        translate(buffer, CHANNEL_WASSR, service);
    }
    if(service == identica_service &&
       purple_prefs_get_bool(OPT_TRANSLATE_CHANNEL)) {
        translate(buffer, TAG_IDENTICA, service);
    }

    /* escape pseudo command (to show the same result as sending message) */
    if(service == twitter_service &&
       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;
    gint service = get_service_type(conv);
    guint count;

    g_return_if_fail(gtkconv != NULL);

    switch(service) {
    case twitter_service:
    case identica_service:
        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);
        break;
    case wassr_service:
        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 <= 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);
    guint count = 0;

    g_return_if_fail(gtkconv != NULL);

    switch(service) {
    case twitter_service:
    case identica_service:
        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);
        break;
    case wassr_service:
        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 <= 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:
        case wassr_service:
        case identica_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)
        return;

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

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

        next = g_list_next(current);

        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;
                mark_list = g_list_delete_link(mark_list, current);
            }
        }
        else {
            gtk_text_buffer_delete_mark(current_text_buffer, current_mark);
            current->data = NULL;
            mark_list = g_list_delete_link(mark_list, current);
        }

        current = next;
    }

    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;

    twitter_debug("called\n");

    /* 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:
        case identica_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);

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

    return FALSE;
}

static gboolean
is_twitter_conv(PurpleConversation *conv)
{
    g_return_val_if_fail(conv != NULL, FALSE);

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

    if(g_strstr_len(name,  18, "wassr-bot@wassr.jp") &&
       g_strstr_len(proto, 11, "prpl-jabber")) {
        return TRUE;
    }

    return FALSE;
}

static gboolean
is_wassr_conv(PurpleConversation *conv)
{
    g_return_val_if_fail(conv != NULL, FALSE);

    const char *name = purple_conversation_get_name(conv);
    PurpleAccount *account = purple_conversation_get_account(conv);

    return is_wassr_account(account, name);
}

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

    if(g_strstr_len(name,  16, "update@identi.ca") &&
       g_strstr_len(proto, 11, "prpl-jabber")) {
        return TRUE;
    }

    return FALSE;
}

static gboolean
is_identica_conv(PurpleConversation *conv)
{
    g_return_val_if_fail(conv != NULL, FALSE);

    const char *name = purple_conversation_get_name(conv);
    PurpleAccount *account = purple_conversation_get_account(conv);

    return is_identica_account(account, name);
}

static gint
get_service_type_by_account(PurpleAccount *account, const char *sender)
{
    gint service = unknown_service;

    g_return_val_if_fail(account != NULL, unknown_service);
    g_return_val_if_fail(sender != NULL, unknown_service);

    if(is_twitter_account(account, sender))
        service = twitter_service;
    else if(is_wassr_account(account, sender))
        service = wassr_service;
    else if(is_identica_account(account, sender))
        service = identica_service;

    return service;
}

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;
    else if(is_identica_conv(conv))
        service = identica_service;

    return service;
}

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

    twitter_debug("called\n");

    g_return_if_fail(gtkconv != NULL);

    gint service = get_service_type(conv);
    /* only attach to twitter conversation window */
    switch(service) {
    case twitter_service:
        get_status_with_api((gpointer)conv);
        source.id = g_timeout_add_seconds(
            purple_prefs_get_int(OPT_API_BASE_GET_INTERVAL),
            get_status_with_api, (gpointer)conv);
        source.conv = conv;
        attach_to_conv(conv, NULL);
        break;
    case wassr_service:
    case identica_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);

    twitter_debug("called\n");

    g_return_if_fail(gtkconv != NULL);

    gint service = get_service_type(conv);
    GHashTable *hash = NULL;

    /* only attach to twitter conversation window */
    switch(service) {
    case twitter_service:
        if(purple_prefs_get_bool(OPT_API_BASE_POST)) {
            g_source_remove_by_user_data((gpointer)conv);
            source.id = 0;
            source.conv = NULL;
        }
        hash = icon_hash[twitter_service];
        break;
    case wassr_service:
        hash = icon_hash[wassr_service];
        break;
    case identica_service:
        hash = icon_hash[identica_service];
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(hash)
        delete_requested_icon_marks(gtkconv, hash);
}

static void
apply_filter(gchar **sender, gchar **buffer, PurpleMessageFlags *flags, int service)
{
    GMatchInfo *match_info;
    const gchar *list = NULL;
    gchar *screen_name = NULL;
    gchar **candidates = NULL, **candidate = NULL;

    g_return_if_fail(*sender != NULL);
    g_return_if_fail(*buffer != NULL);

    gchar *plain = strip_html_markup(*buffer);

    switch(service) {
    case twitter_service:
    default:
        list = purple_prefs_get_string(OPT_FILTER_TWITTER);
        screen_name = g_strdup_printf("@%s", purple_prefs_get_string(OPT_SCREEN_NAME_TWITTER));
        break;
    case wassr_service:
        list = purple_prefs_get_string(OPT_FILTER_WASSR);
        screen_name = g_strdup_printf("@%s", purple_prefs_get_string(OPT_SCREEN_NAME_WASSR));
        break;
    case identica_service:
        list = purple_prefs_get_string(OPT_FILTER_IDENTICA);
        screen_name = g_strdup_printf("@%s", purple_prefs_get_string(OPT_SCREEN_NAME_IDENTICA));
        break;
    }
    g_return_if_fail(list != NULL);
    if(strstr(list, DEFAULT_LIST))
        return;

    /* find @myself */
    if(purple_prefs_get_bool(OPT_FILTER_EXCLUDE_REPLY) &&
       strstr(plain, screen_name)) {
        g_free(plain);
        g_free(screen_name);
        return;
    }
    else
        g_free(screen_name);

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

    g_regex_match(regp[SENDER], plain, 0, &match_info);
    while(g_match_info_matches(match_info)) {
        gchar *user = NULL;
        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. filter %s\n", user);
                /* pidgin should handle this flag properly --yaz */
//                *flags |= PURPLE_MESSAGE_INVISIBLE;

                /* temporal workaround */
                g_free(*sender); *sender = NULL;
                g_free(*buffer); *buffer = NULL;
                break;
            }
        }

        g_free(user);
        g_match_info_next(match_info, NULL);
    }

    g_free(plain);
    g_strfreev(candidates);
    g_match_info_free(match_info);
}


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

    gint service;

    service = get_service_type_by_account(account, *sender);
    twitter_debug("service = %d\n", service);

#ifdef _WIN32
    /* suppress notification of incoming messages. */
    if(service != unknown_service &&
       purple_prefs_get_bool(OPT_PREVENT_NOTIFICATION)) {
        if(!blink_modified) {
            blink_modified = TRUE;
            blink_state = purple_prefs_get_bool(OPT_PIDGIN_BLINK_IM);
            purple_prefs_set_bool(OPT_PIDGIN_BLINK_IM, FALSE);
        }
    }
    else {
        if(blink_modified) {
            purple_prefs_set_bool(OPT_PIDGIN_BLINK_IM, blink_state);
            blink_modified = FALSE;
        }
    }
#endif

    if(service == wassr_service) {
        gchar *stripped = strip_html_markup(*buffer);
        /* suppress annoying completion message from wassr */
        if(strstr(*buffer, "<body>チャンネル投稿完了:")) {
            twitter_debug("clearing channel parrot message\n");
            g_free(*sender); *sender = NULL;
            g_free(*buffer); *buffer = NULL;
        }
        /* discard parrot message */
        else {
            GList *current = g_list_first(wassr_parrot_list);
            while(current) {
                GList *next = g_list_next(current);

                if(strstr(stripped, current->data)) {
                    twitter_debug("parrot clearing: buf = %s post = %s\n",
                                  *buffer, (char *)current->data);
                    g_free(*sender); *sender = NULL;
                    g_free(*buffer); *buffer = NULL;
                    g_free(current->data);
                    current->data = NULL;
                    wassr_parrot_list =
                        g_list_delete_link(wassr_parrot_list, current);
                    break;
                }

                current = next;
            }
        }
        g_free(stripped);
    }

    if(service == identica_service) {
        /* discard parrot message */
        gchar *stripped = strip_html_markup(*buffer);
        GList *current = g_list_first(identica_parrot_list);
        while(current) {
            GList *next = g_list_next(current);

            if(strstr(stripped, current->data)) {
                twitter_debug("identica parrot clearing: buf = %s post = %s\n",
                              *buffer, (char *)current->data);
                g_free(*sender); *sender = NULL;
                g_free(*buffer); *buffer = NULL;
                g_free(current->data);
                current->data = NULL;
                identica_parrot_list =
                    g_list_delete_link(identica_parrot_list, current);
                break;
            }

            current = next;
        }
        g_free(stripped);
    }

    /* filtering */
    if(purple_prefs_get_bool(OPT_FILTER)) {
        apply_filter(sender, buffer, flags, service);
    }

    /* return here if it is not twitter */
    if(service != twitter_service) {
        return FALSE;
    }

    /* if we use api, discard all incoming IM messages. */
    if(purple_prefs_get_bool(OPT_API_BASE_POST)) {
        g_free(*sender); *sender = NULL;
        g_free(*buffer); *buffer = NULL;
    }

    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;
    gint service = gotdata->service;

    GList *win_list;
    GtkIMHtml *target_imhtml = NULL;
    GtkTextBuffer *target_buffer = NULL;
    GtkTextIter insertion_point;
    icon_data *data = NULL;
    GHashTable *hash = 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,
                                     &insertion_point, requested_mark);

    /* insert icon */
    switch(service) {
    case twitter_service:
        hash = icon_hash[twitter_service];
        break;
    case wassr_service:
        hash = icon_hash[wassr_service];
        break;
    case identica_service:
        hash = icon_hash[identica_service];
        break;
    default:
        twitter_debug("unknown service\n");
    }

    if(hash)
        data = (icon_data *)g_hash_table_lookup(hash, user_name);


    /* in this function, we put an icon for pending marks. we should
     * not invalidate the icon here, otherwise it may result in
     * thrashing. --yaz */

    if(!data || !data->pixbuf) {
        return;
    }

    /* insert icon actually */
    if(purple_prefs_get_bool(OPT_SHOW_ICON)) {
        gtk_text_buffer_insert_pixbuf(target_buffer,
                                      &insertion_point,
                                      data->pixbuf);
        data->use_count++;
    }
    gtk_text_buffer_delete_mark(target_buffer, requested_mark);
    requested_mark = NULL;
}

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

    twitter_debug("called\n");

    switch(service) {
    case twitter_service:
        hash = icon_hash[twitter_service];
        break;
    case wassr_service:
        hash = icon_hash[wassr_service];
        break;
    case identica_service:
        hash = icon_hash[identica_service];
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(hash)
        data = (icon_data *)g_hash_table_lookup(hash, user_name);

    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;

    twitter_debug("about to insert icon for pending requests\n");

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

/* this function will be called when profile page has been retrieved */
static void
got_page_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;
    gint service = gotdata->service;
    GMatchInfo *match_info = NULL;
    icon_data *data = NULL;
    gchar *url = NULL;
    gint regp_id = -1;

    if(service == twitter_service) {
        data = (icon_data *)g_hash_table_lookup(
            icon_hash[twitter_service], user_name);
        regp_id = IMAGE_TWITTER;
    }
    else if(service == wassr_service) {
        data = (icon_data *)g_hash_table_lookup(
            icon_hash[wassr_service], user_name);
        regp_id = IMAGE_WASSR;
    }
    else if(service == identica_service) {
        data = (icon_data *)g_hash_table_lookup(
            icon_hash[identica_service], user_name);
        regp_id = IMAGE_IDENTICA;
    }

    if(!url_text) {
        if(data) {
            data->requested = FALSE;
            data->fetch_data = NULL;
        }
        g_free(gotdata->user_name);
        g_free(gotdata);
        return;
    }

    /* setup image url */ /* xxx need simplify --yaz */
    g_regex_match(regp[regp_id], url_text, 0, &match_info);
    if(!g_match_info_matches(match_info)) {
        g_match_info_free(match_info);

        if(service == twitter_service) {
            twitter_debug("fall back to twitter default icon\n");
            url = g_strdup(TWITTER_DEFAULT_ICON_URL);
        }
        else {
            twitter_debug("no image url found\n");
            if(data) {
                data->requested = FALSE;
                data->fetch_data = NULL;
            }
            g_free(gotdata->user_name);
            g_free(gotdata);
            return;
        }
    }
    else {
        url = g_match_info_fetch(match_info, 1);
        g_match_info_free(match_info);
    }

    /* find out basename */
    gchar *slash = strrchr(url, '/');
    *slash = '\0';

    gchar *lower = g_ascii_strdown(slash+1, -1);

    if(strstr(lower, ".png"))
        data->img_type = "png";
    else if(strstr(lower, ".gif"))
        data->img_type = "gif";
    else if(strstr(lower, ".jpg") || strstr(lower, ".jpeg"))
        data->img_type = "jpg";

    g_free(lower);

    gchar *tmp;
    /* url encode basename. twitter needs this. */
    if(service == twitter_service)
        tmp = g_strdup_printf("%s/%s", url,
                              purple_url_encode(slash+1));
    else if(service == wassr_service) {
        gchar *tmp0 = NULL;
        tmp0 = g_regex_replace(regp[SIZE_128_WASSR], slash+1,
                                     -1, 0, ".64.", 0, NULL);
        tmp = g_strdup_printf("http://wassr.jp%s/%s", url,
                              tmp0 ? tmp0 : slash+1);
        g_free(tmp0);
    }
    else {
        tmp = g_strdup_printf("%s/%s", url, slash+1);
    }

    g_free(url);
    url = tmp;

    /* if requesting icon url is the same as old, return. */
    if(url && data->icon_url && !strcmp(data->icon_url, url)) {
        twitter_debug("old url = %s new url = %s\n", data->icon_url, url);
        data->requested = FALSE;
        data->fetch_data = NULL;
        g_free(url);
        return;
    }

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

    g_free(data->icon_url);
    data->icon_url = g_strdup(url);

    data->use_count = 0;
    data->mtime = time(NULL); /* xxx is there a better way? */

    twitter_debug("requested url=%s\n", url);

    /* request fetch image */
    if(url) {
        /* reuse gotdata. just pass given one */
        /* gotdata will be released in got_icon_cb */
        data->fetch_data = purple_util_fetch_url(url,
                                                 TRUE, NULL, TRUE,
                                                 got_icon_cb, gotdata);
        twitter_debug("request %s's icon\n", user_name);
        g_free(url);
    }
}

static GdkPixbuf *
make_scaled_pixbuf(const gchar *url_text, gsize len)
{
    /* make pixbuf */
    GdkPixbufLoader *loader;
    GdkPixbuf *src = NULL, *dest = NULL;
    gint size;

    g_return_val_if_fail(url_text != NULL, NULL);
    g_return_val_if_fail(len > 0, NULL);

    loader = gdk_pixbuf_loader_new();
    gdk_pixbuf_loader_write(loader, (guchar *)url_text, len, NULL);
    gdk_pixbuf_loader_close(loader, NULL);

    src = gdk_pixbuf_loader_get_pixbuf(loader);
    if(!src)
        return NULL;

    size = purple_prefs_get_int(OPT_ICON_SIZE);
    if(size == 0)
        size = DEFAULT_ICON_SIZE;

    dest = gdk_pixbuf_scale_simple(src, size, size, GDK_INTERP_HYPER);
    gdk_pixbuf_unref(src);

    return dest;
}

static gchar *ext_list[] = {
    "png",
    "gif",
    "jpg",
    NULL
};

/* this function will be called when requested icon has been retrieved */
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;
    gint service = gotdata->service;

    icon_data *data = NULL;
    GHashTable *hash = NULL;
    GdkPixbuf *pixbuf = NULL;
    const gchar *dirname = NULL;

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

    switch(service) {
    case twitter_service:
        hash = icon_hash[twitter_service];
        break;
    case wassr_service:
        hash = icon_hash[wassr_service];
        break;
    case identica_service:
        hash = icon_hash[identica_service];
        break;
    default:
        twitter_debug("unknown service\n");
    }

    if(hash)
        data = (icon_data *)g_hash_table_lookup(hash, user_name);

    /* return if download failed */
    if(!url_text) {
        twitter_debug("downloading %s's icon failed : %s\n",
                      user_name, error_message);
        if(data)
            data->requested = FALSE;

        goto fin_got_icon_cb;
    }

    if(data) {
        /* remove download request */
        data->requested = FALSE;
        data->fetch_data = NULL;

        /* return if user's icon had been downloaded */
        if(data->pixbuf) {
            twitter_debug("%s's icon has already been downloaded\n",
                          user_name);

            goto fin_got_icon_cb;
        }
    }

    pixbuf = make_scaled_pixbuf(url_text, len);

    if(!pixbuf)
        goto fin_got_icon_cb;


    if(!data) {
        twitter_debug("allocate icon_data (shouldn't be called)\n");
        data = g_new0(icon_data, 1);
    }

    data->pixbuf = pixbuf;

    twitter_debug("new icon pixbuf = %p size = %d\n",
                  pixbuf,
                  gdk_pixbuf_get_rowstride(pixbuf) *
                  gdk_pixbuf_get_height(pixbuf));

    if(hash)
        g_hash_table_insert(hash, g_strdup(user_name), data);

    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;
        const gchar *suffix = NULL;
        gchar **extp;

        switch(service) {
        case twitter_service:
            suffix = "twitter";
            break;
        case wassr_service:
            suffix = "wassr";
            break;
        case identica_service:
            suffix = "identica";
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }

        /* remove old file first */
        for(extp = ext_list; *extp; extp++) {
            filename = g_strdup_printf("%s_%s.%s",
                                       user_name, suffix, *extp);
            path = g_build_filename(dirname, filename, NULL);
            g_remove(path);

            g_free(filename);
            g_free(path);
        }

        /* setup path */
        filename = g_strdup_printf("%s_%s.%s",
                                   user_name, suffix, data->img_type);

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

        g_file_set_contents(path, url_text, len, NULL);
        g_free(path); path = NULL;

        data->mtime = time(NULL);
    }

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

    /* Insert the icon to messages that had been received. */
    insert_requested_icon(user_name, service);

fin_got_icon_cb:
    g_free(gotdata->user_name);
    g_free(gotdata);
}

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

    /* look local icon cache for the requested icon */
    gchar *path = NULL;
    icon_data *data = NULL;
    GHashTable *hash = NULL;
    const gchar *suffix = NULL;

    switch(service) {
    case twitter_service:
        hash = icon_hash[twitter_service];
        suffix = "twitter";
        break;
    case wassr_service:
        hash = icon_hash[wassr_service];
        suffix = "wassr";
        break;
    case identica_service:
        suffix = "identica";
        hash = icon_hash[identica_service];
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(!hash)
        return;

    /* since this function is called after mark_icon_for_user(), data
     * must exist here. */
    data = (icon_data *)g_hash_table_lookup(hash, user_name);

    /* if img has been registerd, just return */
    if(data && data->pixbuf && !renew)
        return;

    /* check if saved file exists */
    if(suffix && !renew) {
        gchar *filename = NULL;
        gchar **extp;

        for(extp = ext_list; *extp; extp++) {
            filename = g_strdup_printf("%s_%s.%s", user_name, suffix, *extp);
            path = g_build_filename(purple_prefs_get_string(OPT_ICON_DIR),
                                    filename, NULL);

            twitter_debug("path = %s\n", path);

            /* 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;
                GdkPixbuf *pixbuf = NULL;
                struct stat buf;

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

                if(stat(path, &buf))
                    data->mtime = buf.st_mtime;

                pixbuf = make_scaled_pixbuf(imgdata, len);
                g_free(imgdata);

                if(pixbuf) {
                    data->pixbuf = pixbuf;

                    twitter_debug("new icon pixbuf = %p size = %d\n",
                                  pixbuf,
                                  gdk_pixbuf_get_rowstride(pixbuf) *
                                  gdk_pixbuf_get_height(pixbuf));

                    data->img_type = *extp;

                    twitter_debug("icon data has been loaded from file\n");
                    insert_requested_icon(user_name, service);
                }

                g_free(path);
                return;
            }
        } /* for */
    } /* suffix */

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

    /* Create the URL for an user's icon. */
    switch(service) {
    case twitter_service:
        url = g_strdup_printf("http://twitter.com/%s", user_name);
        break;
    case wassr_service:
        url = g_strdup_printf("http://wassr.jp/user/%s", user_name);
        break;
    case identica_service:
        url = g_strdup_printf("http://identi.ca/%s", user_name);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(url) {
        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 */
        if(service == twitter_service ||
           service == wassr_service ||
           service == identica_service) {
            data->fetch_data = purple_util_fetch_url(url, TRUE, NULL, TRUE,
                                                     got_page_cb, gotdata);
        }
        else {
            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, gint service)
{
    icon_data *data = NULL;
    GHashTable *hash = NULL;

    twitter_debug("called\n");

    switch(service) {
    case twitter_service:
        hash = icon_hash[twitter_service];
        break;
    case wassr_service:
        hash = icon_hash[wassr_service];
        break;
    case identica_service:
        hash = icon_hash[identica_service];
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(hash)
        data = (icon_data *)g_hash_table_lookup(hash, user_name);

    /* proper place to allocate icon_data */
    if(!data) {
        data = g_new0(icon_data, 1);
        g_hash_table_insert(hash, g_strdup(user_name), data);
    }

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

static gboolean
displaying_im_cb(PurpleAccount *account, const char *who, char **message,
                 PurpleConversation *conv, PurpleMessageFlags flags,
                 void *unused)
{
    GtkIMHtml *imhtml;
    GtkTextBuffer *text_buffer;
    gint service = get_service_type(conv);
    gint linenumber = 0;

    twitter_debug("called\n");

    if(service == unknown_service) {
        twitter_debug("neither twitter nor wassr conv\n");
        return FALSE;
    }

    /* get text buffer */
    imhtml = GTK_IMHTML(PIDGIN_CONVERSATION(conv)->imhtml);
    text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(imhtml));

    /* store number of lines */
    linenumber = gtk_text_buffer_get_line_count(text_buffer);
    g_hash_table_insert(conv_hash, conv, GINT_TO_POINTER(linenumber));
    twitter_debug("conv = %p linenumber = %d\n", conv, linenumber);

    return FALSE;
}

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 insertion_point;
    gint service = get_service_type(conv);
    icon_data *data = NULL;
    gint linenumber;
    GHashTable *hash = NULL;
    gboolean renew = FALSE;

    twitter_debug("called\n");

    if(service == unknown_service) {
        twitter_debug("unknown service\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 in the target line */
    linenumber = GPOINTER_TO_INT(g_hash_table_lookup(conv_hash, conv));
    gtk_text_buffer_get_iter_at_line(text_buffer,
                                     &insertion_point,
                                     linenumber);

    switch(service) {
    case twitter_service:
        hash = icon_hash[twitter_service];
        break;
    case wassr_service:
        hash = icon_hash[wassr_service];
        break;
    case identica_service:
        hash = icon_hash[identica_service];
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

    if(hash)
        data = g_hash_table_lookup(hash, user_name);

    if(data) {
        /* check validity of icon */
        int count_thres = purple_prefs_get_int(OPT_ICON_MAX_COUNT);
        int days_thres = DAYS_TO_SECONDS(
            purple_prefs_get_int(OPT_ICON_MAX_DAYS));

        if(data->use_count > count_thres ||
           (data->mtime && ((time(NULL) - data->mtime)) > days_thres)) {
            twitter_debug("count=%d mtime=%d\n",
                          data->use_count, (int)(data->mtime));
            renew = TRUE;
            request_icon(user_name, service, renew);
        }
    }

    /* if we don't have the icon for this user, put a mark instead and
     * request the icon */
    if(!data || !data->pixbuf) {
        twitter_debug("%s's icon is not in memory.\n", user_name);
        mark_icon_for_user(gtk_text_buffer_create_mark(
                               text_buffer, NULL, &insertion_point, FALSE),
                           user_name, service);
        /* request to attach icon to the buffer */
        request_icon(user_name, service, renew);
        g_free(user_name); user_name = NULL;
        return;
    }

    /* if we have icon for this user, insert icon immediately */
    if(purple_prefs_get_bool(OPT_SHOW_ICON)) {
        gtk_text_buffer_insert_pixbuf(text_buffer,
                                      &insertion_point,
                                      data->pixbuf);
        data->use_count++;
    }
    g_free(user_name); user_name = NULL;

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


static void
signed_on_cb(PurpleConnection *gc)
{
    PurpleBuddyList *list = purple_get_blist();
	PurpleBlistNode *gnode, *cnode, *bnode;
	PurpleBuddy *b;

    twitter_debug("called\n");

    if(!purple_prefs_get_bool(OPT_API_BASE_POST))
        return;

    if (!list)
        return;

    twitter_debug("scan list\n");

	for (gnode = list->root; gnode; gnode = gnode->next) {
		if(!PURPLE_BLIST_NODE_IS_GROUP(gnode))
			continue;

        for(cnode = gnode->child; cnode; cnode = cnode->next) {

			if(!PURPLE_BLIST_NODE_IS_CONTACT(cnode))
				continue;

			for(bnode = cnode->child; bnode; bnode = bnode->next) {
				if(!PURPLE_BLIST_NODE_IS_BUDDY(bnode))
					continue;

				b = (PurpleBuddy *)bnode;

				if(!PURPLE_BUDDY_IS_ONLINE(b)) {
                    const char *name;
                    PurpleAccount *account;
                    name = purple_buddy_get_name(b);
                    account = purple_buddy_get_account(b);
                    if (is_twitter_account(account, name)) {
                        PurpleConversation *gconv;
                        gconv = purple_find_conversation_with_account(
                            PURPLE_CONV_TYPE_IM, name, account);
                        if (!gconv) {
                            gconv = purple_conversation_new(
                                PURPLE_CONV_TYPE_IM, account, name);
                        }
                    }
                }
			}
		}
	}
}

static void
api_base_post_cb(const char *name, PurplePrefType type, gconstpointer value,
                 gpointer data)
{
    signed_on_cb(NULL);
    get_status_with_api((gpointer)(source.conv));
}

static gboolean
load_plugin(PurplePlugin *plugin)
{
    int i;

    /* 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(), "displaying-im-msg",
                          plugin, PURPLE_CALLBACK(displaying_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);
    purple_signal_connect(purple_connections_get_handle(), "signed-on",
                          plugin, PURPLE_CALLBACK(signed_on_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);
    regp[CHANNEL_WASSR]  = g_regex_new(P_CHANNEL, 0, 0, NULL);
    regp[IMAGE_TWITTER]  = g_regex_new(P_IMAGE_TWITTER, 0, 0, NULL);
    regp[IMAGE_WASSR]    = g_regex_new(P_IMAGE_WASSR, 0, 0, NULL);
    regp[IMAGE_IDENTICA] = g_regex_new(P_IMAGE_IDENTICA, 0, 0, NULL);
    regp[TAG_IDENTICA]   = g_regex_new(P_TAG_IDENTICA, 0, 0, NULL);
    regp[SIZE_128_WASSR] = g_regex_new(P_SIZE_128_WASSR, 0, 0, NULL);

    for(i = twitter_service; i < NUM_SERVICES; i++) {
        icon_hash[i] = g_hash_table_new_full(g_str_hash, g_str_equal,
                                              g_free, NULL);
    }

    conv_hash = g_hash_table_new_full(g_direct_hash, g_direct_equal,
                                              NULL, NULL);


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

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

static void
cleanup_hash_entry_func(gpointer key, gpointer value, gpointer user_data)
{
    remove_marks_func(key, value, user_data);
    cancel_fetch_func(key, value, user_data);
}

static gboolean
unload_plugin(PurplePlugin *plugin)
{
    int i;
    GList *current;

    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(),
                             "displaying-im-msg",
                             plugin, PURPLE_CALLBACK(displaying_im_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));
    purple_signal_disconnect(purple_connections_get_handle(), 
                             "signed-on",
                             plugin, PURPLE_CALLBACK(signed_on_cb));

    /* unreference regp */
    for(i = 0; i < NUM_REGPS; i++) {
        g_regex_unref(regp[i]);
    }

    /* remove mark list in each hash entry */
    /* cancel request that has not been finished yet */
    for(i = twitter_service; i < NUM_SERVICES; i++) {
        /* delete mark list and stop requeset for each hash table */
        g_hash_table_foreach(icon_hash[i],
                             (GHFunc)cleanup_hash_entry_func, NULL);
        /* destroy hash table for icon_data */
        g_hash_table_destroy(icon_hash[i]);
    }

    g_hash_table_destroy(conv_hash);

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

    /* free wassr_parrot_list */
    current = g_list_first(wassr_parrot_list);
    while(current) {
        GList *next;

        next = g_list_next(current);
        g_free(current->data);
        wassr_parrot_list =
            g_list_delete_link(wassr_parrot_list, current);

        current = next;
    }
    g_list_free(wassr_parrot_list);
    wassr_parrot_list = NULL;

    /* free identica_parot_list */
    current = g_list_first(identica_parrot_list);
    while(current) {
        GList *next;

        next = g_list_next(current);
        g_free(current->data);
        identica_parrot_list =
            g_list_delete_link(identica_parrot_list, current);

        current = next;
    }
    g_list_free(identica_parrot_list);
    identica_parrot_list = 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 void
invalidate_icon_data_func(gpointer key, gpointer value, gpointer user_data)
{
    icon_data *data = (icon_data *)value;

    g_return_if_fail(data != NULL);

    g_object_unref(data->pixbuf);
    data->pixbuf = NULL;
}

static void
icon_size_prefs_cb(const char *name, PurplePrefType type,
                   gconstpointer val, gpointer data)
{
    int i;

    /* invalidate icon cache */
    for(i = twitter_service; i < NUM_SERVICES; i++) {
        g_hash_table_foreach(icon_hash[i],
                             (GHFunc)invalidate_icon_data_func, NULL);
    }
}

static void
interval_prefs_cb(const char *name, PurplePrefType type,
                   gconstpointer val, gpointer data)
{
    /* remove idle func */
    g_source_remove_by_user_data((gpointer)(source.conv));

    /* add idle func */
    if(purple_prefs_get_bool(OPT_API_BASE_POST)) {
        source.id = g_timeout_add_seconds(
            purple_prefs_get_int(OPT_API_BASE_GET_INTERVAL),
            get_status_with_api, (gpointer)(source.conv));
    }
}

static void
text_changed_cb(gpointer *data)
{
    const gchar *text;
    gchar *pref = (gchar *)g_object_get_data(G_OBJECT(data), "pref");
    text = gtk_entry_get_text(GTK_ENTRY(data));
    purple_prefs_set_string(pref, text);
}

static void
bool_toggled_cb(gpointer *data)
{
    gchar *pref = (gchar *)g_object_get_data(G_OBJECT(data), "pref");
    gboolean value = purple_prefs_get_bool(pref);
    purple_prefs_set_bool(pref, !value);
}

static void
spin_changed_cb(gpointer *data)
{
    gchar *pref = (gchar *)g_object_get_data(G_OBJECT(data), "pref");

    twitter_debug("called\n");

    purple_prefs_set_int(pref,
        gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(data)));
}

static void
combo_changed_cb(gpointer *data)
{
    gint position;
    gchar *pref = (gchar *)g_object_get_data(G_OBJECT(data), "pref");
    position = gtk_combo_box_get_active(GTK_COMBO_BOX(data));
    purple_prefs_set_int(pref, position);
}

static void
disconnect_prefs_cb(GtkObject *object, gpointer data)
{
	PurplePlugin *plugin = (PurplePlugin *)data;

	purple_prefs_disconnect_by_handle(plugin);
}

static GtkWidget *
prefs_get_frame(PurplePlugin *plugin)
{
    GtkBuilder *builder;
    GError *err = NULL;
    gchar *filename;
    GtkWidget *window, *notebook, *e;
    const gchar *text;
    GtkSpinButton *spin;
    GtkObject *adjust;
    gint value;

    builder = gtk_builder_new();

#ifdef _WIN32
    filename = g_build_filename(purple_user_dir(),
                                "pidgin-twitter", "prefs.ui", NULL);
#else
    filename = g_build_filename(DATADIR,
                                "pidgin-twitter", "prefs.ui", NULL);
#endif
    gtk_builder_add_from_file(builder, filename, &err);
    if(err) {
        twitter_debug("%s\n", err->message);
        g_free(filename);
        return NULL;
    }

    g_free(filename);

    gtk_builder_connect_signals(builder, NULL);

    window = GTK_WIDGET(gtk_builder_get_object(builder, "prefswindow"));
    notebook = GTK_WIDGET(gtk_builder_get_object(builder, "prefsnotebook"));

    gtk_container_remove(GTK_CONTAINER(window), notebook);

    g_signal_connect(notebook, "destroy",
                     G_CALLBACK(disconnect_prefs_cb), plugin);


    /**********************/
    /* connect to signals */
    /**********************/

    /****************/
    /* account page */
    /****************/
    e = GTK_WIDGET(gtk_builder_get_object (builder, "account_twitter"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_SCREEN_NAME_TWITTER);
    text = purple_prefs_get_string(OPT_SCREEN_NAME_TWITTER);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "account_wassr"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_SCREEN_NAME_WASSR);
    text = purple_prefs_get_string(OPT_SCREEN_NAME_WASSR);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "account_identica"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_SCREEN_NAME_IDENTICA);
    text = purple_prefs_get_string(OPT_SCREEN_NAME_IDENTICA);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "account_api"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_API_BASE_POST);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_API_BASE_POST));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);
    purple_prefs_connect_callback(plugin, OPT_API_BASE_POST, /* xxx divide? */
                                  api_base_post_cb, NULL);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "account_api_password"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_PASSWORD_TWITTER);

    gtk_entry_set_visibility(GTK_ENTRY(e), FALSE);
    if (gtk_entry_get_invisible_char(GTK_ENTRY(e)) == '*')
        gtk_entry_set_invisible_char(GTK_ENTRY(e), PIDGIN_INVISIBLE_CHAR);

    text = purple_prefs_get_string(OPT_PASSWORD_TWITTER);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);


    /* interval spin */
    e = GTK_WIDGET(gtk_builder_get_object (builder,
                       "account_api_get_interval_spin"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_API_BASE_GET_INTERVAL);

    spin = GTK_SPIN_BUTTON(e);

    value = purple_prefs_get_int(OPT_API_BASE_GET_INTERVAL);
    twitter_debug("spin value = %d\n", value);

	adjust = gtk_adjustment_new(value, 40, 3600, 10, 100, 100);
    gtk_spin_button_set_adjustment(spin, GTK_ADJUSTMENT(adjust));
    gtk_widget_set_size_request(GTK_WIDGET(spin), 50, -1);

    if(value == 0) {
        value = TWITTER_DEFAULT_INTERVAL;
        purple_prefs_set_int(OPT_API_BASE_GET_INTERVAL, value);
    }
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(e), (gdouble)value);
    g_signal_connect(e, "value-changed",
                     G_CALLBACK(spin_changed_cb), &e);
    purple_prefs_connect_callback(plugin, OPT_API_BASE_GET_INTERVAL,
                                  interval_prefs_cb, NULL);



    /********************/
    /* translation page */
    /********************/
    e = GTK_WIDGET(gtk_builder_get_object (builder, "translation_recipient"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_TRANSLATE_RECIPIENT);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_TRANSLATE_RECIPIENT));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "translation_sender"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_TRANSLATE_SENDER);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_TRANSLATE_SENDER));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "translation_channel"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_TRANSLATE_CHANNEL);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_TRANSLATE_CHANNEL));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);



    /***************/
    /* filter page */
    /***************/
    e = GTK_WIDGET(gtk_builder_get_object (builder, "filter_filter_check"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_FILTER);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_FILTER));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "filter_exclude_reply_check"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_FILTER_EXCLUDE_REPLY);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_FILTER_EXCLUDE_REPLY));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "filter_twitter"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_FILTER_TWITTER);
    text = purple_prefs_get_string(OPT_FILTER_TWITTER);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "filter_wassr"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_FILTER_WASSR);
    text = purple_prefs_get_string(OPT_FILTER_WASSR);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "filter_identica"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_FILTER_IDENTICA);
    text = purple_prefs_get_string(OPT_FILTER_IDENTICA);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);



    /*************/
    /* icon page */
    /*************/
    e = GTK_WIDGET(gtk_builder_get_object (builder, "icon_show_icon"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_SHOW_ICON);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_SHOW_ICON));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    /* icon size spin */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "icon_icon_size_spin"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_ICON_SIZE);

    spin = GTK_SPIN_BUTTON(e);

    value = purple_prefs_get_int(OPT_ICON_SIZE);
    twitter_debug("spin value = %d\n", value);

	adjust = gtk_adjustment_new(value, 16, 128, 4, 4, 4);
    gtk_spin_button_set_adjustment(spin, GTK_ADJUSTMENT(adjust));
    gtk_widget_set_size_request(GTK_WIDGET(spin), 50, -1);

    if(value == 0) {
        value = DEFAULT_ICON_SIZE;
        purple_prefs_set_int(OPT_ICON_SIZE, value);
    }
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(e), (gdouble)value);
    g_signal_connect(e, "value-changed",
                     G_CALLBACK(spin_changed_cb), &e);
    purple_prefs_connect_callback(plugin, OPT_ICON_SIZE,
                                  icon_size_prefs_cb, NULL);

    /* enable update */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "icon_enable_update"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_UPDATE_ICON);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_UPDATE_ICON));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    /* max count spin */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "icon_max_count_spin"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_ICON_MAX_COUNT);

    spin = GTK_SPIN_BUTTON(e);

    value = purple_prefs_get_int(OPT_ICON_MAX_COUNT);
    twitter_debug("spin value = %d\n", value);

	adjust = gtk_adjustment_new(value, 2, 10000, 1, 10, 10);
    gtk_spin_button_set_adjustment(spin, GTK_ADJUSTMENT(adjust));
    gtk_widget_set_size_request(GTK_WIDGET(spin), 50, -1);

    if(value == 0) {
        value = DEFAULT_ICON_MAX_COUNT;
        purple_prefs_set_int(OPT_ICON_MAX_COUNT, value);
    }
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(e), (gdouble)value);
    g_signal_connect(e, "value-changed",
                     G_CALLBACK(spin_changed_cb), &e);


    /* max days spin */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "icon_max_days_spin"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_ICON_MAX_DAYS);

    spin = GTK_SPIN_BUTTON(e);

    value = purple_prefs_get_int(OPT_ICON_MAX_DAYS);
    twitter_debug("spin value = %d\n", value);

	adjust = gtk_adjustment_new(value, 1, 180, 1, 10, 10);
    gtk_spin_button_set_adjustment(spin, GTK_ADJUSTMENT(adjust));
    gtk_widget_set_size_request(GTK_WIDGET(spin), 50, -1);

    if(value == 0) {
        value = DEFAULT_ICON_MAX_DAYS;
        purple_prefs_set_int(OPT_ICON_MAX_DAYS, value);
    }
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(e), (gdouble)value);
    g_signal_connect(e, "value-changed",
                     G_CALLBACK(spin_changed_cb), &e);



    /**************/
    /* sound page */
    /**************/
    e = GTK_WIDGET(gtk_builder_get_object (builder, "sound_recip_check"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_PLAYSOUND_RECIPIENT);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_PLAYSOUND_RECIPIENT));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "sound_recip_list"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_USERLIST_RECIPIENT);
    text = purple_prefs_get_string(OPT_USERLIST_RECIPIENT);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);

    /* recipient combobox */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "sound_recip_combo"));
    gtk_combo_box_set_active(GTK_COMBO_BOX(e),
                             purple_prefs_get_int(OPT_SOUNDID_RECIPIENT));
    g_object_set_data(G_OBJECT(e), "pref", OPT_SOUNDID_RECIPIENT);
    g_signal_connect(e, "changed",
                     G_CALLBACK(combo_changed_cb), &e);



    e = GTK_WIDGET(gtk_builder_get_object (builder, "sound_send_check"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_PLAYSOUND_SENDER);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_PLAYSOUND_SENDER));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "sound_send_list"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_USERLIST_SENDER);
    text = purple_prefs_get_string(OPT_USERLIST_SENDER);
    gtk_entry_set_text(GTK_ENTRY(e), text);
    g_signal_connect(e, "changed",
                     G_CALLBACK(text_changed_cb), &e);

    /* sender combobox */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "sound_send_combo"));
    gtk_combo_box_set_active(GTK_COMBO_BOX(e),
                             purple_prefs_get_int(OPT_SOUNDID_RECIPIENT));
    g_object_set_data(G_OBJECT(e), "pref", OPT_SOUNDID_SENDER);
    g_signal_connect(e, "changed",
                     G_CALLBACK(combo_changed_cb), &e);




    /****************/
    /* utility page */
    /****************/
    e = GTK_WIDGET(gtk_builder_get_object (builder, "utility_counter"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_COUNTER);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_COUNTER));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);
    purple_prefs_connect_callback(plugin, OPT_COUNTER,
                                  counter_prefs_cb, NULL);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "utility_pseudo"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_ESCAPE_PSEUDO);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_ESCAPE_PSEUDO));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "utility_oops"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_SUPPRESS_OOPS);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_SUPPRESS_OOPS));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "utility_notify"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_PREVENT_NOTIFICATION);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_PREVENT_NOTIFICATION));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);

    e = GTK_WIDGET(gtk_builder_get_object (builder, "utility_log_output"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_LOG_OUTPUT);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(e),
                                 purple_prefs_get_bool(OPT_LOG_OUTPUT));
    g_signal_connect(e, "toggled",
                     G_CALLBACK(bool_toggled_cb), &e);


    /* all done */
    gtk_widget_show_all(notebook);
    return notebook;
}

static PidginPluginUiInfo ui_info = {
    prefs_get_frame,
	0,									/* page number - reserved	*/
	NULL,								/* reserved 1	*/
	NULL,								/* reserved 2	*/
	NULL,								/* reserved 3	*/
	NULL								/* reserved 4	*/
};

static PurplePluginInfo info = {
    PURPLE_PLUGIN_MAGIC,
    PURPLE_MAJOR_VERSION,
    PURPLE_MINOR_VERSION,
    PURPLE_PLUGIN_STANDARD,     /**< type	*/
    PIDGIN_PLUGIN_TYPE,         /**< ui_req	*/
    0,                          /**< flags	*/
    NULL,                       /**< deps	*/
    PURPLE_PRIORITY_DEFAULT,    /**< priority	*/
    PLUGIN_ID,                  /**< id		*/
    "Pidgin-Twitter",           /**< name	*/
    "0.8.0d1",                  /**< version	*/
    "provides useful features for twitter", /**  summary	*/
    "provides useful features for twitter", /**  desc	*/
    "Yoshiki Yazawa, mikanbako, \nKonosuke Watanabe, IWATA Ray, \nmojin, umq, \nthe pidging-twitter team",     /**< author	*/
    "http://www.honeyplanet.jp/pidgin-twitter/",   /**< homepage	*/
    load_plugin,                /**< load	*/
    unload_plugin,              /**< unload	*/
    NULL,                       /**< destroy	*/
    &ui_info,                    /**< ui_info	*/
    NULL,                       /**< extra_info	*/
    NULL,                       /**< 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_TRANSLATE_CHANNEL, 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_int(OPT_API_BASE_GET_INTERVAL, TWITTER_DEFAULT_INTERVAL);
    purple_prefs_add_string(OPT_SCREEN_NAME_TWITTER, EMPTY);
    purple_prefs_add_string(OPT_PASSWORD_TWITTER, EMPTY);
    purple_prefs_add_string(OPT_SCREEN_NAME_WASSR, EMPTY);
    purple_prefs_add_string(OPT_SCREEN_NAME_IDENTICA, EMPTY);

    purple_prefs_add_bool(OPT_SHOW_ICON, TRUE);
    purple_prefs_add_int(OPT_ICON_SIZE, DEFAULT_ICON_SIZE);
    purple_prefs_add_bool(OPT_UPDATE_ICON, TRUE);
    purple_prefs_add_int(OPT_ICON_MAX_COUNT, DEFAULT_ICON_MAX_COUNT);
    purple_prefs_add_int(OPT_ICON_MAX_DAYS, DEFAULT_ICON_MAX_DAYS);
    purple_prefs_add_bool(OPT_LOG_OUTPUT, FALSE);

    purple_prefs_add_bool(OPT_FILTER, TRUE);
    purple_prefs_add_bool(OPT_FILTER_EXCLUDE_REPLY, TRUE);
    purple_prefs_add_string(OPT_FILTER_TWITTER, DEFAULT_LIST);
    purple_prefs_add_string(OPT_FILTER_WASSR, DEFAULT_LIST);
    purple_prefs_add_string(OPT_FILTER_IDENTICA, DEFAULT_LIST);
}

PURPLE_INIT_PLUGIN(pidgin_twitter, init_plugin, info)