view pidgin-twitter.c @ 200:9a2d727f39b4

removed jisko support as a protest to their expulsion policy.
author Yoshiki Yazawa <yaz@honeyplanet.jp>
date Mon, 11 Aug 2008 14:31:54 +0900
parents 5ddf8bee768d
children 0aeeafe37ef7
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 11
#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 gchar *wassr_post = NULL;
static gchar *identica_post = NULL;

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


/*************/
/* 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;
    gboolean rv = FALSE;

    for(pp = postedlist; pp; pp=pp->next) {
        status_t *posted = (status_t *)pp->data;
        if(posted->id == status->id) {
            rv = TRUE;
        }

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

    postedlist = g_list_remove_all(postedlist, NULL);

    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;

     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 */
     for(stp = statuseslist; stp; stp=stp->next) {
         status_t *st = (status_t *)stp->data;

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

             msg = g_strdup_printf("%s: %s", st->screen_name, st->text);
             purple_conv_im_write(conv->u.im,
                                  "twitter@twitter.com",
                                  msg,
                                  PURPLE_MESSAGE_RECV,
                                  st->time);
             lastid = st->id;

             g_free(msg);
         }

         free_status(st);
         g_free(stp->data);
         stp->data = NULL;
     }

     statuseslist = g_list_remove_all(statuseslist, NULL);
}

/* 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 base 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)
        strip_markup(buffer, TRUE);

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

    if(identica_ac) {
        /* store sending message to address parrot problem */
        g_strlcpy(identica_post, *buffer, IDENTICA_POST_LEN);
        twitter_debug("identica parrot pushed:%s\n", *buffer);
    }

    /* 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[128];

    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, 128, 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, 128, format, match1 ? match1: "", match2, match2);

        g_free(match1);
        g_free(match2);
    }
    else if(service == wassr_service && which == CHANNEL) {
        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, 128, format, match1 ? match1: "", match2, match2);

        g_free(match1);
        g_free(match2);
    }

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

    return FALSE;
}

static void
translate(gchar **str, 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;
    gchar **candidates = NULL, **candidate = NULL;

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

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

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

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

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

    gint service = get_service_type(conv);

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

    /* Add screen_name if the current message is posted by owner */
    if (flags & PURPLE_MESSAGE_SEND) {
        gchar *m = NULL;
        const char *screen_name = 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(service == wassr_service &&
       purple_prefs_get_bool(OPT_TRANSLATE_CHANNEL)) {
        translate(buffer, CHANNEL, service);
    }
    if(purple_prefs_get_bool(OPT_TRANSLATE_RECIPIENT)) {
        translate(buffer, RECIPIENT, service);
    }

    /* escape pseudo command (to show same result to 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;
    gchar *text = NULL;
    GtkTextIter head, tail;
    guint bytes = 0;

    g_return_if_fail(gtkconv != NULL);

    switch(service) {
    case twitter_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;
    case identica_service:
        gtk_text_buffer_get_start_iter(textbuffer, &head);
        gtk_text_buffer_get_end_iter(textbuffer, &tail);

        text = gtk_text_buffer_get_text(textbuffer, &head, &tail, TRUE);
        if(text)
            bytes = strlen(text) + new_text_length;
        g_free(text);
        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         bytes <= 140 ? "black" : "red", bytes);
        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;
    gchar *text = NULL;
    GtkTextIter head, tail;
    guint bytes = 0;

    g_return_if_fail(gtkconv != NULL);

    switch(service) {
    case twitter_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;
    case identica_service:
        gtk_text_buffer_get_start_iter(textbuffer, &head);
        gtk_text_buffer_get_end_iter(textbuffer, &tail);

        text = gtk_text_buffer_get_text(textbuffer,
                                        &head, &tail, TRUE);
        if(text)
            bytes = strlen(text);
        g_free(text);

        text = gtk_text_buffer_get_text(textbuffer,
                                        start_pos, end_pos, TRUE);
        if(text)
            bytes -= strlen(text);
        g_free(text);

        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         bytes <= 140 ? "black" : "red", bytes);
        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 */
    for(current = g_list_first(mark_list); current;
        current = g_list_next(current)) {
        GtkTextMark *current_mark = current->data;
        GtkTextBuffer *current_text_buffer = gtk_text_mark_get_buffer(
            current_mark);

        if(!current_text_buffer)
            continue;

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

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

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

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

static void
attach_to_window(void)
{
    GList *list;

    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(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 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 = get_service_type(conv);

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

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

    if(service == identica_service) {
        if(identica_post &&
           strlen(identica_post) &&
           strstr(*buffer, identica_post)) {
            twitter_debug("identica parrot clearing: buf = %s post = %s\n",
                          *buffer, identica_post);
            g_free(*sender); *sender = NULL;
            g_free(*buffer); *buffer = NULL;
        }
    }

    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) {
        tmp = g_strdup_printf("http://wassr.jp%s/%s", url, slash+1);
    }
    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_append(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 or 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);
            g_free(user_name); user_name = NULL;
        }
    }

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

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

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

    /* allocate identica_post */
    identica_post = g_new0(gchar, IDENTICA_POST_LEN + 1);

    return TRUE;
}

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

    if(!data)
        return;

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

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

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;

    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_post */
    g_free(wassr_post);
    wassr_post = NULL;

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

    return TRUE;
}

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

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

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



    /*************/
    /* 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.7.0",                    /**< 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_INIT_PLUGIN(pidgin_twitter, init_plugin, info)