view pidgin-twitter.c @ 149:0594a81d967d

back to 64 pixel icon for wassr. 128 pixel ones are usually better, but sometimes would be smaller image with broad white border so that resulting image would be extremely small.
author Yoshiki Yazawa <yaz@honeyplanet.jp>
date Thu, 24 Jul 2008 21:07:11 +0900
parents 4e9d0fd93fb6
children f2748067a8c2
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_REGP 10
static GRegex *regp[NUM_REGP];
static gboolean suppress_oops = FALSE;
static GHashTable *icon_hash[3]; /* twitter, wassr, identica. */
static GHashTable *conv_hash = NULL;

static GList *statuseslist = NULL;
static GList *postedlist = NULL;

static gchar *wassr_post = NULL;
static gchar *identica_post = NULL;


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

    begin = strchr(startp, '<');
    if(begin)
        end = strchr(begin + 1, '>');
    if(!end) {
        tmp = g_strconcat(str, startp, NULL);
        g_free(str);
        str = tmp;
        g_free(html);
        return str; /* no corresponding >, we have done. */
    }

    /* 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
            if(0) {
            }
#endif
            else {
                /* anything else: discard whole <>. */
                startp = end + 1;
                goto loop;
            }
        }  /* 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 char *day_of_week_name[] = {
    "Sun",
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri",
    "Sat",
    "Sun",
    NULL
};

static char *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)
{
    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;
            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);
                }
            }
        }
     }

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

         if(st->id > lastid && !is_posted_message(st)) {
             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");

    PurpleConversation *conv = (PurpleConversation *)data;
    if(!conv)
        return FALSE; /* cease fetch */

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

    } 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, status, TWITTER_STATUS_TERMINATOR, NULL);

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

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

}

/***********************/
/* 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 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;
    case wassr_service:
        count = gtk_text_buffer_get_char_count(textbuffer) +
            (unsigned int)g_utf8_strlen(new_text, -1);
        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         count <= 255 ? "black" : "red", count);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

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

    g_free(markup);
}

static void
delete_text_cb(GtkTextBuffer *textbuffer, GtkTextIter *start_pos,
               GtkTextIter *end_pos, gpointer user_data)
{
    PurpleConversation *conv = (PurpleConversation *)user_data;
    PidginConversation *gtkconv = PIDGIN_CONVERSATION(conv);
    GtkWidget *box, *counter = NULL;
    gchar *markup = NULL;
    gint service = get_service_type(conv);
    guint count = 0;
    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 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;
    case wassr_service:
        count= gtk_text_buffer_get_char_count(textbuffer) -
            (gtk_text_iter_get_offset(end_pos) -
             gtk_text_iter_get_offset(start_pos));
        markup = g_markup_printf_escaped("<span color=\"%s\">%u</span>",
                                         count <= 255 ? "black" : "red", count);
        break;
    default:
        twitter_debug("unknown service\n");
        break;
    }

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

    g_free(markup);
}

static void
detach_from_window(void)
{
    GList *list;

    /* find twitter conv window out and detach from that */
    for(list = pidgin_conv_windows_get_list(); list; list = list->next) {
        PidginWindow *win = list->data;
        PurpleConversation *conv =
            pidgin_conv_window_get_active_conversation(win);
        gint service = get_service_type(conv);
        switch(service) {
        case twitter_service:
            detach_from_conv(conv, NULL);
            break;
        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:
            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
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:
        /* api based retrieve */ /* xxx should configurable */
        if(purple_prefs_get_bool(OPT_API_BASE_POST)) {
            get_status_with_api((gpointer)conv);
            g_timeout_add_seconds(
                purple_prefs_get_int(OPT_API_BASE_GET_INTERVAL),
                get_status_with_api, (gpointer)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);
        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 incoming IM message. XXX too wild? */
    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;
    gint icon_id;
    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);

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

    if(!icon_id) {
        return;
    }

    /* insert icon actually */
    if(purple_prefs_get_bool(OPT_SHOW_ICON)) {
        PurpleStoredImage *img = purple_imgstore_find_by_id(icon_id);
        const GdkPixbuf *pixbuf = purple_imgstore_get_data(img);
        gtk_text_buffer_insert_pixbuf(target_buffer,
                                      &insertion_point,
                                      (GdkPixbuf *)pixbuf);
    }
    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\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;
    }

    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 */
    g_regex_match(regp[regp_id], url_text, 0, &match_info);
    if(!g_match_info_matches(match_info)) {
        twitter_debug("no image url found\n");

        g_match_info_free(match_info);
        if(data) {
            data->requested = FALSE;
            data->fetch_data = NULL;
        }
        g_free(gotdata->user_name);
        g_free(gotdata);
        return;
    }
    url = g_match_info_fetch(match_info, 1);
    g_match_info_free(match_info);

    gchar *slash = strrchr(url, '/');
    *slash = '\0';
    gchar *tmp = g_strdup_printf("%s/%s", url,
                                 purple_url_encode(slash+1));

    gchar *startp = slash + 1;
    gchar *ext = NULL;
    do {
        ext = strrchr(startp, '.');
        if(ext) {
            if(!strcasecmp(ext, ".jpg") || !strcasecmp(ext, ".jpeg")) {
                data->img_type = "jpg";
                break;
            }
            else if(!strcasecmp(ext, ".png")) {
                data->img_type = "png";
                break;
            }
            else if(!strcasecmp(ext, ".gif")) {
                data->img_type = "gif";
                break;
            }
            startp = ext;
        }
    } while(ext);

    g_free(url);
    url = tmp;

    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 = 48; /* twitter 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;

    gint icon_id;
    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->icon_id > 0) {
            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;

    icon_id =
        purple_imgstore_add_with_id(pixbuf,
                                    gdk_pixbuf_get_rowstride(pixbuf) *
                                    gdk_pixbuf_get_height(pixbuf),
                                    user_name);

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

    data->icon_id = icon_id;

    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;

        switch(service) {
        case twitter_service:
            filename = g_strdup_printf("%s_twitter.%s",
                                       user_name, data->img_type);
            break;
        case wassr_service:
            filename = g_strdup_printf("%s_wassr.%s",
                                       user_name, data->img_type);
            break;
        case identica_service:
            filename = g_strdup_printf("%s_identica.%s",
                                       user_name, data->img_type);
            break;
        default:
            twitter_debug("unknown service\n");
            break;
        }

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

        g_file_set_contents(path, url_text, len, NULL);
    }

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

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

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

static void
request_icon(const char *user_name, gint service)
{
    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);
    g_hash_table_insert(hash, g_strdup(user_name), data);

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

    /* check if saved file exists */
    if(suffix) {
        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);

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

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

                pixbuf = make_scaled_pixbuf(imgdata, len);

                if(pixbuf) {
                    data->icon_id =
                        purple_imgstore_add_with_id(
                            pixbuf,
                            gdk_pixbuf_get_rowstride(pixbuf) *
                            gdk_pixbuf_get_height(pixbuf),
                            user_name);

                    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/profile_img.png.64",
                              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 == identica_service ||
           service == twitter_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 icon_id = 0;
    gint service = get_service_type(conv);
    icon_data *data = NULL;
    gint linenumber;
    GHashTable *hash = NULL;

    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)
        icon_id = data->icon_id;

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

    /* if we have icon for this user, insert icon immediately */
    if(purple_prefs_get_bool(OPT_SHOW_ICON)) {
        PurpleStoredImage *img = purple_imgstore_find_by_id(icon_id);
        const GdkPixbuf *pixbuf = purple_imgstore_get_data(img);
        gtk_text_buffer_insert_pixbuf(text_buffer,
                                      &insertion_point,
                                      (GdkPixbuf *)pixbuf);
    }
    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);
}



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_IDENTICA] = g_regex_new(P_IMAGE_IDENTICA, 0, 0, NULL);
    regp[IMAGE_TWITTER]  = g_regex_new(P_IMAGE_TWITTER, 0, 0, NULL);

    for(i = twitter_service; i <= identica_service; 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_REGP; 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 <= identica_service; 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_return_if_fail(data->icon_id >0);

    /* img->data is managed by gdkpixbuf, so we should not free here. */
#if 0
    PurpleStoredImage *img = purple_imgstore_find_by_id(data->icon_id);
    if(img)
        purple_imgstore_unref_by_id(data->icon_id);
#endif

    data->icon_id = 0;
}

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 <= identica_service; i++) {
        g_hash_table_foreach(icon_hash[i],
                             (GHFunc)invalidate_icon_data_func, NULL);
    }
}

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 GtkWidget *
prefs_get_frame(PurplePlugin *plugin)
{
    GtkBuilder *builder;
    GError *err = NULL;
    gchar *filename;
    GtkWidget *window, *notebook, *e;
    const gchar *text;

    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);
        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(gtk_widget_destroyed), &notebook);

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

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

    /* utility page */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "utility_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);

    /* setup spin */
    e = GTK_WIDGET(gtk_builder_get_object (builder, "utility_icon_size_spin"));
    g_object_set_data(G_OBJECT(e), "pref", OPT_ICON_SIZE);

    GtkSpinButton *spin = GTK_SPIN_BUTTON(e);

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

	GtkObject *adjust = gtk_adjustment_new(value, 16, 128, 4, 1, 1);
    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);



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


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

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

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


    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, mojin, \nthe pidging-twitter team",     /**< author	*/
    "http://www.honeyplanet.jp/",   /**< 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_SHOW_ICON, TRUE);

    purple_prefs_add_bool(OPT_API_BASE_POST, FALSE);
    purple_prefs_add_string(OPT_SCREEN_NAME_TWITTER, EMPTY);
    purple_prefs_add_string(OPT_PASSWORD_TWITTER, EMPTY);

    purple_prefs_add_int(OPT_ICON_SIZE, DEFAULT_ICON_SIZE);
    purple_prefs_add_string(OPT_SCREEN_NAME_WASSR, EMPTY);
    purple_prefs_add_string(OPT_SCREEN_NAME_IDENTICA, EMPTY);
    purple_prefs_add_int(OPT_API_BASE_GET_INTERVAL, TWITTER_DEFAULT_INTERVAL);

}

PURPLE_INIT_PLUGIN(pidgin_twitter, init_plugin, info)