Mercurial > pidgin.yaz
view src/protocols/msn/msn.c @ 10732:c4cb90065e1d
[gaim-migrate @ 12334]
"gaim_escape_html (according to Ethan) predates
g_markup_escape_text. Current code in Gaim uses both
functions. This patch removes gaim_escape_html from the
API and replaces all calls in the Gaim tree with
g_markup_escape_text. I included a ChangeLog.API note.
As far as I can tell, this still works perfectly.
This is obviously intended for HEAD only, as it removes a
public function." --rlaager
this was discussed extensively this morning
committer: Tailor Script <tailor@pidgin.im>
author | Luke Schierer <lschiere@pidgin.im> |
---|---|
date | Sat, 26 Mar 2005 02:43:49 +0000 |
parents | 41a2c35c0941 |
children | 55af3fa46329 |
line wrap: on
line source
/** * @file msn.c The MSN protocol plugin * * gaim * * Gaim is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * 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 PHOTO_SUPPORT 1 #include <glib.h> #include "msn.h" #include "accountopt.h" #include "msg.h" #include "page.h" #include "pluginpref.h" #include "prefs.h" #include "session.h" #include "state.h" #include "utils.h" #include "prpl.h" #include "util.h" #include "version.h" #include "switchboard.h" #include "notification.h" #include "sync.h" #include "slplink.h" #if PHOTO_SUPPORT #include "imgstore.h" #endif typedef struct { GaimConnection *gc; const char *passport; } MsnMobileData; typedef struct { GaimConnection *gc; char *name; } MsnGetInfoData; typedef struct { MsnGetInfoData *info_data; char *stripped; char *url_buffer; GString *s; char *photo_url_text; char *tooltip_text; const char *title; } MsnGetInfoStepTwoData; static const char * msn_normalize(const GaimAccount *account, const char *str) { static char buf[BUF_LEN]; char *tmp; g_return_val_if_fail(str != NULL, NULL); g_snprintf(buf, sizeof(buf), "%s%s", str, (strchr(str, '@') ? "" : "@hotmail.com")); tmp = g_utf8_strdown(buf, -1); strncpy(buf, tmp, sizeof(buf)); g_free(tmp); return buf; } static void msn_act_id(GaimConnection *gc, const char *entry) { MsnCmdProc *cmdproc; MsnSession *session; GaimAccount *account; const char *alias; session = gc->proto_data; cmdproc = session->notification->cmdproc; account = gaim_connection_get_account(gc); if(entry && strlen(entry)) alias = gaim_url_encode(entry); else alias = ""; if (strlen(alias) > BUDDY_ALIAS_MAXLEN) { gaim_notify_error(gc, NULL, _("Your new MSN friendly name is too long."), NULL); return; } msn_cmdproc_send(cmdproc, "REA", "%s %s", gaim_account_get_username(account), alias); } static void msn_set_prp(GaimConnection *gc, const char *type, const char *entry) { MsnCmdProc *cmdproc; MsnSession *session; session = gc->proto_data; cmdproc = session->notification->cmdproc; if (entry == NULL || *entry == '\0') { msn_cmdproc_send(cmdproc, "PRP", "%s", type); } else { msn_cmdproc_send(cmdproc, "PRP", "%s %s", type, gaim_url_encode(entry)); } } static void msn_set_home_phone_cb(GaimConnection *gc, const char *entry) { msn_set_prp(gc, "PHH", entry); } static void msn_set_work_phone_cb(GaimConnection *gc, const char *entry) { msn_set_prp(gc, "PHW", entry); } static void msn_set_mobile_phone_cb(GaimConnection *gc, const char *entry) { msn_set_prp(gc, "PHM", entry); } static void enable_msn_pages_cb(GaimConnection *gc) { msn_set_prp(gc, "MOB", "Y"); } static void disable_msn_pages_cb(GaimConnection *gc) { msn_set_prp(gc, "MOB", "N"); } static void send_to_mobile(GaimConnection *gc, const char *who, const char *entry) { MsnTransaction *trans; MsnSession *session; MsnCmdProc *cmdproc; MsnPage *page; char *payload; size_t payload_len; session = gc->proto_data; cmdproc = session->notification->cmdproc; page = msn_page_new(); msn_page_set_body(page, entry); payload = msn_page_gen_payload(page, &payload_len); trans = msn_transaction_new(cmdproc, "PGD", "%s 1 %d", who, payload_len); msn_transaction_set_payload(trans, payload, payload_len); msn_page_destroy(page); msn_cmdproc_send_trans(cmdproc, trans); } static void send_to_mobile_cb(MsnMobileData *data, const char *entry) { send_to_mobile(data->gc, data->passport, entry); g_free(data); } static void close_mobile_page_cb(MsnMobileData *data, const char *entry) { g_free(data); } /* -- */ static void msn_show_set_friendly_name(GaimPluginAction *action) { GaimConnection *gc; gc = (GaimConnection *) action->context; gaim_request_input(gc, NULL, _("Set your friendly name."), _("This is the name that other MSN buddies will " "see you as."), gaim_connection_get_display_name(gc), FALSE, FALSE, NULL, _("OK"), G_CALLBACK(msn_act_id), _("Cancel"), NULL, gc); } static void msn_show_set_home_phone(GaimPluginAction *action) { GaimConnection *gc; MsnSession *session; gc = (GaimConnection *) action->context; session = gc->proto_data; gaim_request_input(gc, NULL, _("Set your home phone number."), NULL, msn_user_get_home_phone(session->user), FALSE, FALSE, NULL, _("OK"), G_CALLBACK(msn_set_home_phone_cb), _("Cancel"), NULL, gc); } static void msn_show_set_work_phone(GaimPluginAction *action) { GaimConnection *gc; MsnSession *session; gc = (GaimConnection *) action->context; session = gc->proto_data; gaim_request_input(gc, NULL, _("Set your work phone number."), NULL, msn_user_get_work_phone(session->user), FALSE, FALSE, NULL, _("OK"), G_CALLBACK(msn_set_work_phone_cb), _("Cancel"), NULL, gc); } static void msn_show_set_mobile_phone(GaimPluginAction *action) { GaimConnection *gc; MsnSession *session; gc = (GaimConnection *) action->context; session = gc->proto_data; gaim_request_input(gc, NULL, _("Set your mobile phone number."), NULL, msn_user_get_mobile_phone(session->user), FALSE, FALSE, NULL, _("OK"), G_CALLBACK(msn_set_mobile_phone_cb), _("Cancel"), NULL, gc); } static void msn_show_set_mobile_pages(GaimPluginAction *action) { GaimConnection *gc; gc = (GaimConnection *) action->context; gaim_request_action(gc, NULL, _("Allow MSN Mobile pages?"), _("Do you want to allow or disallow people on " "your buddy list to send you MSN Mobile pages " "to your cell phone or other mobile device?"), -1, gc, 3, _("Allow"), G_CALLBACK(enable_msn_pages_cb), _("Disallow"), G_CALLBACK(disable_msn_pages_cb), _("Cancel"), NULL); } static void show_send_to_mobile_cb(GaimBlistNode *node, gpointer ignored) { GaimBuddy *buddy; GaimConnection *gc; MsnSession *session; MsnMobileData *data; g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node)); buddy = (GaimBuddy *) node; gc = gaim_account_get_connection(buddy->account); session = gc->proto_data; data = g_new0(MsnMobileData, 1); data->gc = gc; data->passport = buddy->name; gaim_request_input(gc, NULL, _("Send a mobile message."), NULL, NULL, TRUE, FALSE, NULL, _("Page"), G_CALLBACK(send_to_mobile_cb), _("Close"), G_CALLBACK(close_mobile_page_cb), data); } static void initiate_chat_cb(GaimBlistNode *node, gpointer data) { GaimBuddy *buddy; GaimConnection *gc; MsnSession *session; MsnSwitchBoard *swboard; g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node)); buddy = (GaimBuddy *) node; gc = gaim_account_get_connection(buddy->account); session = gc->proto_data; swboard = msn_switchboard_new(session); msn_switchboard_request(swboard); msn_switchboard_request_add_user(swboard, buddy->name); /* TODO: This might move somewhere else, after USR might be */ swboard->chat_id = session->conv_seq++; swboard->conv = serv_got_joined_chat(gc, swboard->chat_id, "MSN Chat"); gaim_conv_chat_add_user(GAIM_CONV_CHAT(swboard->conv), gaim_account_get_username(buddy->account), NULL, GAIM_CBFLAGS_NONE, TRUE); } static void t_msn_xfer_init(GaimXfer *xfer) { MsnSlpLink *slplink; const char *filename; FILE *fp; filename = gaim_xfer_get_local_filename(xfer); slplink = xfer->data; if ((fp = g_fopen(filename, "rb")) == NULL) { GaimAccount *account; GaimConnection *gc; const char *who; char *msg; account = slplink->session->account; gc = gaim_account_get_connection(account); who = slplink->remote_user; msg = g_strdup_printf(_("Error reading %s: \n%s.\n"), filename, strerror(errno)); gaim_xfer_error(gaim_xfer_get_type(xfer), account, xfer->who, msg); gaim_xfer_cancel_local(xfer); g_free(msg); return; } fclose(fp); msn_slplink_request_ft(slplink, xfer); } static void msn_send_file(GaimConnection *gc, const char *who, const char *file) { MsnSession *session; MsnSlpLink *slplink; GaimXfer *xfer; session = gc->proto_data; xfer = gaim_xfer_new(gc->account, GAIM_XFER_SEND, who); slplink = msn_session_get_slplink(session, who); xfer->data = slplink; gaim_xfer_set_init_fnc(xfer, t_msn_xfer_init); if (file) gaim_xfer_request_accepted(xfer, file); else gaim_xfer_request(xfer); } static gboolean msn_can_receive_file(GaimConnection *gc, const char *who) { GaimAccount *account; char *normal; gboolean ret; account = gaim_connection_get_account(gc); normal = g_strdup(msn_normalize(account, gaim_account_get_username(account))); ret = strcmp(normal, msn_normalize(account, who)); g_free(normal); return ret; } /************************************************************************** * Protocol Plugin ops **************************************************************************/ static const char * msn_list_icon(GaimAccount *a, GaimBuddy *b) { return "msn"; } static void msn_list_emblems(GaimBuddy *b, const char **se, const char **sw, const char **nw, const char **ne) { MsnUser *user; GaimPresence *presence; const char *emblems[4] = { NULL, NULL, NULL, NULL }; int i = 0; user = b->proto_data; presence = gaim_buddy_get_presence(b); if (!gaim_presence_is_online(presence)) emblems[i++] = "offline"; else if (gaim_presence_is_status_active(presence, "busy") || gaim_presence_is_status_active(presence, "phone")) emblems[i++] = "occupied"; else if (!gaim_presence_is_available(presence)) emblems[i++] = "away"; if (user == NULL) { emblems[0] = "offline"; } else { if (user->mobile) emblems[i++] = "wireless"; if (user->list_op & (1 << MSN_LIST_BL)) emblems[i++] = "blocked"; if (!(user->list_op & (1 << MSN_LIST_RL))) emblems[i++] = "nr"; } *se = emblems[0]; *sw = emblems[1]; *nw = emblems[2]; *ne = emblems[3]; } static char * msn_status_text(GaimBuddy *buddy) { GaimPresence *presence; GaimStatus *status; presence = gaim_buddy_get_presence(buddy); status = gaim_presence_get_active_status(presence); if (!gaim_presence_is_available(presence) && !gaim_presence_is_idle(presence)) { return g_strdup(gaim_status_get_name(status)); } return NULL; } static char * msn_tooltip_text(GaimBuddy *buddy) { MsnUser *user; GaimPresence *presence = gaim_buddy_get_presence(buddy); GaimStatus *status = gaim_presence_get_active_status(presence); GString *s; user = buddy->proto_data; s = g_string_new(""); if (gaim_presence_is_online(presence)) { g_string_append_printf(s, _("\n<b>%s:</b> %s"), _("Status"), gaim_presence_is_idle(presence) ? _("Idle") : gaim_status_get_name(status)); } if (user) { g_string_append_printf(s, _("\n<b>%s:</b> %s"), _("Has you"), (user->list_op & (1 << MSN_LIST_RL)) ? _("Yes") : _("No")); g_string_append_printf(s, _("\n<b>%s:</b> %s"), _("Blocked"), (user->list_op & (1 << MSN_LIST_BL)) ? _("Yes") : _("No")); } return g_string_free(s, FALSE); } static GList * msn_status_types(GaimAccount *account) { GaimStatusType *status; GList *types = NULL; status = gaim_status_type_new_full(GAIM_STATUS_OFFLINE, "offline", _("Offline"), FALSE, FALSE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_ONLINE, "online", _("Online"), FALSE, FALSE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_AVAILABLE, "available", _("Available"), FALSE, TRUE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_AWAY, "away", _("Away"), FALSE, TRUE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_AWAY, "brb", _("Be Right Back"), FALSE, TRUE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_AWAY, "busy", _("Busy"), FALSE, TRUE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_AWAY, "phone", _("On The Phone"), FALSE, TRUE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_AWAY, "lunch", _("Out To Lunch"), FALSE, TRUE, FALSE); types = g_list_append(types, status); status = gaim_status_type_new_full(GAIM_STATUS_HIDDEN, "hidden", _("Hidden"), FALSE, TRUE, FALSE); types = g_list_append(types, status); return types; } static GList * msn_actions(GaimPlugin *plugin, gpointer context) { GList *m = NULL; GaimPluginAction *act; act = gaim_plugin_action_new(_("Set Friendly Name"), msn_show_set_friendly_name); m = g_list_append(m, act); m = g_list_append(m, NULL); act = gaim_plugin_action_new(_("Set Home Phone Number"), msn_show_set_home_phone); m = g_list_append(m, act); act = gaim_plugin_action_new(_("Set Work Phone Number"), msn_show_set_work_phone); m = g_list_append(m, act); act = gaim_plugin_action_new(_("Set Mobile Phone Number"), msn_show_set_mobile_phone); m = g_list_append(m, act); m = g_list_append(m, NULL); #if 0 act = gaim_plugin_action_new(_("Enable/Disable Mobile Devices"), msn_show_set_mobile_support); m = g_list_append(m, act); #endif act = gaim_plugin_action_new(_("Allow/Disallow Mobile Pages"), msn_show_set_mobile_pages); m = g_list_append(m, act); return m; } static GList * msn_buddy_menu(GaimBuddy *buddy) { MsnUser *user; GList *m = NULL; GaimBlistNodeAction *act; g_return_val_if_fail(buddy != NULL, NULL); user = buddy->proto_data; if (user != NULL) { if (user->mobile) { act = gaim_blist_node_action_new(_("Send to Mobile"), show_send_to_mobile_cb, NULL, NULL); m = g_list_append(m, act); } } if (g_ascii_strcasecmp(buddy->name, gaim_account_get_username(buddy->account))) { act = gaim_blist_node_action_new(_("Initiate _Chat"), initiate_chat_cb, NULL, NULL); m = g_list_append(m, act); } return m; } static GList * msn_blist_node_menu(GaimBlistNode *node) { if(GAIM_BLIST_NODE_IS_BUDDY(node)) { return msn_buddy_menu((GaimBuddy *) node); } else { return NULL; } } static void msn_login(GaimAccount *account, GaimStatus *status) { GaimConnection *gc; MsnSession *session; const char *username; const char *host; gboolean http_method = FALSE; int port; gc = gaim_account_get_connection(account); if (!gaim_ssl_is_supported()) { gaim_connection_error(gc, _("SSL support is needed for MSN. Please install a supported " "SSL library. See http://gaim.sf.net/faq-ssl.php for more " "information.")); return; } if (gaim_account_get_bool(account, "http_method", FALSE)) http_method = TRUE; host = gaim_account_get_string(account, "server", MSN_SERVER); port = gaim_account_get_int(account, "port", MSN_PORT); session = msn_session_new(account); gc->proto_data = session; gc->flags |= GAIM_CONNECTION_HTML | GAIM_CONNECTION_FORMATTING_WBFO | GAIM_CONNECTION_NO_BGCOLOR | GAIM_CONNECTION_NO_FONTSIZE | GAIM_CONNECTION_NO_URLDESC; msn_session_set_login_step(session, MSN_LOGIN_STEP_START); /* Hmm, I don't like this. */ /* XXX shx: Me neither */ username = msn_normalize(account, gaim_account_get_username(account)); if (strcmp(username, gaim_account_get_username(account))) gaim_account_set_username(account, username); if (!msn_session_connect(session, host, port, http_method)) gaim_connection_error(gc, _("Failed to connect to server.")); } static void msn_close(GaimConnection *gc) { MsnSession *session; session = gc->proto_data; g_return_if_fail(session != NULL); msn_session_destroy(session); gc->proto_data = NULL; } static int msn_send_im(GaimConnection *gc, const char *who, const char *message, GaimConvImFlags flags) { GaimAccount *account; MsnMessage *msg; char *msgformat; char *msgtext; account = gaim_connection_get_account(gc); msn_import_html(message, &msgformat, &msgtext); if (strlen(msgtext) + strlen(msgformat) + strlen(VERSION) > 1564) { g_free(msgformat); g_free(msgtext); return -E2BIG; } msg = msn_message_new_plain(msgtext); msn_message_set_attr(msg, "X-MMS-IM-Format", msgformat); g_free(msgformat); g_free(msgtext); if (g_ascii_strcasecmp(who, gaim_account_get_username(account))) { MsnSession *session; MsnSwitchBoard *swboard; session = gc->proto_data; swboard = msn_session_get_swboard(session, who); msn_switchboard_send_msg(swboard, msg, TRUE); } else { char *body_str, *body_enc, *pre, *post; const char *format; /* * In MSN, you can't send messages to yourself, so * we'll fake like we received it ;) */ body_str = msn_message_to_string(msg); body_enc = g_markup_escape_text(body_str, -1); g_free(body_str); format = msn_message_get_attr(msg, "X-MMS-IM-Format"); msn_parse_format(format, &pre, &post); body_str = g_strdup_printf("%s%s%s", pre ? pre : "", body_enc ? body_enc : "", post ? post : ""); g_free(body_enc); g_free(pre); g_free(post); serv_got_typing_stopped(gc, who); serv_got_im(gc, who, body_str, flags, time(NULL)); g_free(body_str); } msn_message_destroy(msg); return 1; } static int msn_send_typing(GaimConnection *gc, const char *who, int typing) { GaimAccount *account; MsnSession *session; MsnSwitchBoard *swboard; MsnMessage *msg; account = gaim_connection_get_account(gc); session = gc->proto_data; if (!typing) return 0; if (!g_ascii_strcasecmp(who, gaim_account_get_username(account))) { /* We'll just fake it, since we're sending to ourself. */ serv_got_typing(gc, who, MSN_TYPING_RECV_TIMEOUT, GAIM_TYPING); return MSN_TYPING_SEND_TIMEOUT; } swboard = msn_session_find_swboard(session, who); if (swboard == NULL || !msn_switchboard_can_send(swboard)) return 0; msg = msn_message_new(MSN_MSG_TYPING); msn_message_set_content_type(msg, "text/x-msmsgscontrol"); msn_message_set_flag(msg, 'U'); msn_message_set_attr(msg, "TypingUser", gaim_account_get_username(account)); msn_message_set_bin_data(msg, "\r\n", 2); msn_switchboard_send_msg(swboard, msg, FALSE); msn_message_destroy(msg); return MSN_TYPING_SEND_TIMEOUT; } static void msn_set_status(GaimAccount *account, GaimStatus *status) { GaimConnection *gc; MsnSession *session; const char *state; int msnstatus; gc = gaim_account_get_connection(account); if (gc) session = gc->proto_data; state = gaim_status_get_id(status); gaim_debug_info("msn", "Set status to %s\n", gaim_status_get_name(status)); if (strcmp(state, "offline") && !gc) { gaim_account_connect(account, status); return; } else if (!strcmp(state, "away")) msnstatus = MSN_AWAY; else if (!strcmp(state, "brb")) msnstatus = MSN_BRB; else if (!strcmp(state, "busy")) msnstatus = MSN_BUSY; else if (!strcmp(state, "phone")) msnstatus = MSN_PHONE; else if (!strcmp(state, "lunch")) msnstatus = MSN_LUNCH; else if (!strcmp(state, "hidden")) msnstatus = MSN_HIDDEN; else if (0) /* how do we detect idle with new status? */ msnstatus = MSN_IDLE; else msnstatus = MSN_ONLINE; if (gc) msn_change_status(session, msnstatus); } static void msn_set_idle(GaimConnection *gc, int idle) { MsnSession *session; session = gc->proto_data; msn_change_status(session, (idle ? MSN_IDLE : MSN_ONLINE)); } static void fake_userlist_add_buddy(MsnUserList *userlist, const char *who, int list_id, const char *group_name) { MsnUser *user; static int group_id_c = 1; int group_id; group_id = -1; if (group_name != NULL) { MsnGroup *group; group = msn_group_new(userlist, group_id_c, group_name); group_id = group_id_c++; } user = msn_userlist_find_user(userlist, who); if (user == NULL) { user = msn_user_new(userlist, who, NULL); msn_userlist_add_user(userlist, user); } else if (user->list_op & (1 << list_id)) { if (list_id == MSN_LIST_FL) { if (group_id >= 0) if (g_list_find(user->group_ids, GINT_TO_POINTER(group_id))) return; } else return; } if (group_id >= 0) { user->group_ids = g_list_append(user->group_ids, GINT_TO_POINTER(group_id)); } user->list_op |= (1 << list_id); } static void msn_add_buddy(GaimConnection *gc, GaimBuddy *buddy, GaimGroup *group) { MsnSession *session; MsnUserList *userlist; const char *who; session = gc->proto_data; userlist = session->userlist; who = msn_normalize(gc->account, buddy->name); if (!session->logged_in) { fake_userlist_add_buddy(session->sync_userlist, who, MSN_LIST_FL, group ? group->name : NULL); return; } #if 0 if (group != NULL && group->name != NULL) gaim_debug_info("msn", "msn_add_buddy: %s, %s\n", who, group->name); else gaim_debug_info("msn", "msn_add_buddy: %s\n", who); #endif #if 0 /* Which is the max? */ if (session->fl_users_count >= 150) { gaim_debug_info("msn", "Too many buddies\n"); /* Buddy list full */ /* TODO: gaim should be notified of this */ return; } #endif /* XXX - Would group ever be NULL here? I don't think so... * shx: Yes it should; MSN handles non-grouped buddies, and this is only * internal. */ msn_userlist_add_buddy(userlist, who, MSN_LIST_FL, group ? group->name : NULL); } static void msn_rem_buddy(GaimConnection *gc, GaimBuddy *buddy, GaimGroup *group) { MsnSession *session; MsnUserList *userlist; session = gc->proto_data; userlist = session->userlist; if (!session->logged_in) return; /* XXX - Does buddy->name need to be msn_normalize'd here? --KingAnt */ msn_userlist_rem_buddy(userlist, buddy->name, MSN_LIST_FL, group->name); } static void msn_add_permit(GaimConnection *gc, const char *who) { MsnSession *session; MsnUserList *userlist; MsnUser *user; session = gc->proto_data; userlist = session->userlist; user = msn_userlist_find_user(userlist, who); if (!session->logged_in) return; if (user != NULL && user->list_op & MSN_LIST_BL_OP) msn_userlist_rem_buddy(userlist, who, MSN_LIST_BL, NULL); msn_userlist_add_buddy(userlist, who, MSN_LIST_AL, NULL); } static void msn_add_deny(GaimConnection *gc, const char *who) { MsnSession *session; MsnUserList *userlist; MsnUser *user; session = gc->proto_data; userlist = session->userlist; user = msn_userlist_find_user(userlist, who); if (!session->logged_in) return; if (user != NULL && user->list_op & MSN_LIST_AL_OP) msn_userlist_rem_buddy(userlist, who, MSN_LIST_AL, NULL); msn_userlist_add_buddy(userlist, who, MSN_LIST_BL, NULL); } static void msn_rem_permit(GaimConnection *gc, const char *who) { MsnSession *session; MsnUserList *userlist; MsnUser *user; session = gc->proto_data; userlist = session->userlist; if (!session->logged_in) return; user = msn_userlist_find_user(userlist, who); msn_userlist_rem_buddy(userlist, who, MSN_LIST_AL, NULL); if (user != NULL && user->list_op & MSN_LIST_RL_OP) msn_userlist_add_buddy(userlist, who, MSN_LIST_BL, NULL); } void msn_rem_deny(GaimConnection *gc, const char *who) { MsnSession *session; MsnUserList *userlist; MsnUser *user; session = gc->proto_data; userlist = session->userlist; if (!session->logged_in) return; user = msn_userlist_find_user(userlist, who); msn_userlist_rem_buddy(userlist, who, MSN_LIST_BL, NULL); if (user != NULL && user->list_op & MSN_LIST_RL_OP) msn_userlist_add_buddy(userlist, who, MSN_LIST_AL, NULL); } static void msn_set_permit_deny(GaimConnection *gc) { GaimAccount *account; MsnSession *session; MsnCmdProc *cmdproc; account = gaim_connection_get_account(gc); session = gc->proto_data; cmdproc = session->notification->cmdproc; if (account->perm_deny == GAIM_PRIVACY_ALLOW_ALL || account->perm_deny == GAIM_PRIVACY_DENY_USERS) { msn_cmdproc_send(cmdproc, "BLP", "%s", "AL"); } else { msn_cmdproc_send(cmdproc, "BLP", "%s", "BL"); } } static void msn_chat_invite(GaimConnection *gc, int id, const char *msg, const char *who) { MsnSession *session; MsnSwitchBoard *swboard; session = gc->proto_data; swboard = msn_session_find_swboard_with_id(session, id); if (swboard == NULL) { /* if we have no switchboard, everyone else left the chat already */ swboard = msn_switchboard_new(session); msn_switchboard_request(swboard); swboard->chat_id = id; swboard->conv = gaim_find_chat(gc, id); } msn_switchboard_request_add_user(swboard, who); } static void msn_chat_leave(GaimConnection *gc, int id) { MsnSession *session; MsnSwitchBoard *swboard; GaimConversation *conv; session = gc->proto_data; swboard = msn_session_find_swboard_with_id(session, id); /* if swboard is NULL we were the only person left anyway */ if (swboard == NULL) return; conv = swboard->conv; msn_switchboard_close(swboard); /* If other switchboards managed to associate themselves with this * conv, make sure they know it's gone! */ if (conv != NULL) { while ((swboard = msn_session_find_swboard_with_conv(session, conv)) != NULL) swboard->conv = NULL; } } static int msn_chat_send(GaimConnection *gc, int id, const char *message) { GaimAccount *account; MsnSession *session; MsnSwitchBoard *swboard; MsnMessage *msg; char *msgformat; char *msgtext; account = gaim_connection_get_account(gc); session = gc->proto_data; swboard = msn_session_find_swboard_with_id(session, id); if (swboard == NULL) return -EINVAL; if (!swboard->ready) return 0; msn_import_html(message, &msgformat, &msgtext); if (strlen(msgtext) + strlen(msgformat) + strlen(VERSION) > 1564) { g_free(msgformat); g_free(msgtext); return -E2BIG; } msg = msn_message_new_plain(msgtext); msn_message_set_attr(msg, "X-MMS-IM-Format", msgformat); msn_switchboard_send_msg(swboard, msg, FALSE); msn_message_destroy(msg); g_free(msgformat); g_free(msgtext); serv_got_chat_in(gc, id, gaim_account_get_username(account), 0, message, time(NULL)); return 0; } static void msn_keepalive(GaimConnection *gc) { MsnSession *session; session = gc->proto_data; if (!session->http_method) { MsnCmdProc *cmdproc; cmdproc = session->notification->cmdproc; msn_cmdproc_send_quick(cmdproc, "PNG", NULL, NULL); } } static void msn_group_buddy(GaimConnection *gc, const char *who, const char *old_group_name, const char *new_group_name) { MsnSession *session; MsnUserList *userlist; session = gc->proto_data; userlist = session->userlist; msn_userlist_move_buddy(userlist, who, old_group_name, new_group_name); } static void msn_rename_group(GaimConnection *gc, const char *old_name, GaimGroup *group, GList *moved_buddies) { MsnSession *session; MsnCmdProc *cmdproc; int old_gid; const char *enc_new_group_name; session = gc->proto_data; cmdproc = session->notification->cmdproc; enc_new_group_name = gaim_url_encode(group->name); old_gid = msn_userlist_find_group_id(session->userlist, old_name); if (old_gid >= 0) { msn_cmdproc_send(cmdproc, "REG", "%d %s 0", old_gid, enc_new_group_name); } else { msn_cmdproc_send(cmdproc, "ADG", "%s 0", enc_new_group_name); } } static void msn_convo_closed(GaimConnection *gc, const char *who) { MsnSession *session; MsnSwitchBoard *swboard; GaimConversation *conv; session = gc->proto_data; swboard = msn_session_find_swboard(session, who); /* * Don't perform an assertion here. If swboard is NULL, then the * switchboard was either closed by the other party, or the person * is talking to himself. */ if (swboard == NULL) return; conv = swboard->conv; if (!(swboard->flag & MSN_SB_FLAG_FT)) msn_switchboard_close(swboard); else swboard->conv = NULL; /* If other switchboards managed to associate themselves with this * conv, make sure they know it's gone! */ if (conv != NULL) { while ((swboard = msn_session_find_swboard_with_conv(session, conv)) != NULL) swboard->conv = NULL; } } static void msn_set_buddy_icon(GaimConnection *gc, const char *filename) { MsnSession *session; MsnUser *user; session = gc->proto_data; user = session->user; msn_user_set_buddy_icon(user, filename); msn_change_status(session, session->state); } static void msn_remove_group(GaimConnection *gc, GaimGroup *group) { MsnSession *session; MsnCmdProc *cmdproc; int group_id; session = gc->proto_data; cmdproc = session->notification->cmdproc; if ((group_id = msn_userlist_find_group_id(session->userlist, group->name)) >= 0) { msn_cmdproc_send(cmdproc, "RMG", "%d", group_id); } } static char * msn_tooltip_info_text(MsnGetInfoData *info_data) { GString *s; GString *name; GaimBuddy *b; const char *p; s = g_string_sized_new(80); /* wild guess */ /* Try to not display the MSN screen name as an email address */ p = strrchr(info_data->name, '@'); if (p) { name = g_string_new_len(info_data->name, p - info_data->name); g_string_append_printf(name, "@%s", p + 1); } else { /* This should never happen */ name = g_string_new(info_data->name); } g_string_printf(s, "<span style=\"font-size: larger\"><b>%s</b></span><br>", name->str); g_string_free(name, TRUE); b = gaim_find_buddy(gaim_connection_get_account(info_data->gc), info_data->name); if (b) { GaimPresence *presence; char *statustext = msn_tooltip_text(b); presence = gaim_buddy_get_presence(b); if (b->alias && b->alias[0]) { char *aliastext = g_markup_escape_text(b->alias, -1); g_string_append_printf(s, _("<b>Alias:</b> %s<br>"), aliastext); g_free(aliastext); } if (b->server_alias) { char *nicktext = g_markup_escape_text(b->server_alias, -1); g_string_append_printf(s, _("<b>%s:</b> "), _("Nickname")); g_string_append_printf(s, "<font sml=\"msn\">%s</font><br>", nicktext); g_free(nicktext); } if (statustext) { char *tmp; tmp = gaim_strreplace((*statustext == '\n' ? statustext + 1 : statustext), "\n", "<br>"); g_free(statustext); g_string_append_printf(s, "%s<br>", tmp); g_free(tmp); } } return g_string_free(s, FALSE); } #if PHOTO_SUPPORT static char * msn_get_photo_url(const char *url_text) { char *p; char *it = NULL; p = strstr(url_text, " title=\"Click to see the full-size photo.\">"); if (p) { /* Search backwards for "http://". This is stupid, but it works. */ for (; !it && p > url_text; p -= 1) { if (strncmp(p, "\"http://", 8) == 0) { char *q; p += 1; /* skip only the " */ q = strchr(p, '"'); if (q) { it = g_strndup(p, q - p); } } } } return it; } static void msn_got_photo(void *data, const char *url_text, size_t len); #endif static void msn_got_info(void *data, const char *url_text, size_t len) { MsnGetInfoData *info_data = (MsnGetInfoData *)data; char *stripped, *p, *q; char buf[1024]; char *tooltip_text = NULL; char *user_url = NULL; gboolean found; gboolean has_info = FALSE; const char* title = NULL; char *url_buffer; GString *s; int stripped_len; #if PHOTO_SUPPORT char *photo_url_text = NULL; MsnGetInfoStepTwoData *info2_data = NULL; #endif gaim_debug_info("msn", "In msn_got_info\n"); tooltip_text = msn_tooltip_info_text(info_data); title = _("MSN Profile"); if (url_text == NULL || strcmp(url_text, "") == 0) { g_snprintf(buf, 1024, "<html><body>%s<b>%s</b></body></html>", tooltip_text, _("Error retrieving profile")); gaim_notify_userinfo(info_data->gc, info_data->name, NULL, title, NULL, buf, NULL, NULL); g_free(tooltip_text); return; } url_buffer = g_strdup(url_text); /* If they have a homepage link, MSN masks it such that we need to * fetch the url out before gaim_markup_strip_html() nukes it */ if ((p = strstr(url_text, "Take a look at my </font><A class=viewDesc title=\"")) != NULL) { p += 50; if ((q = strchr(p, '"')) != NULL) user_url = g_strndup(p, q - p); } /* * gaim_markup_strip_html() doesn't strip out character entities like * and · */ while ((p = strstr(url_buffer, " ")) != NULL) { *p = ' '; /* Turn 's into ordinary blanks */ p += 1; memmove(p, p + 5, strlen(p + 5)); url_buffer[strlen(url_buffer) - 5] = '\0'; } while ((p = strstr(url_buffer, "·")) != NULL) { memmove(p, p + 6, strlen(p + 6)); url_buffer[strlen(url_buffer) - 6] = '\0'; } /* Nuke the nasty \r's that just get in the way */ gaim_str_strip_cr(url_buffer); /* MSN always puts in ' for apostrophes...replace them */ while ((p = strstr(url_buffer, "'")) != NULL) { *p = '\''; memmove(p + 1, p + 5, strlen(p + 5)); url_buffer[strlen(url_buffer) - 4] = '\0'; } /* Nuke the html, it's easier than trying to parse the horrid stuff */ stripped = gaim_markup_strip_html(url_buffer); stripped_len = strlen(stripped); gaim_debug_misc("msn", "stripped = %p\n", stripped); gaim_debug_misc("msn", "url_buffer = %p\n", url_buffer); /* Gonna re-use the memory we've already got for url_buffer */ /* No we're not. */ s = g_string_sized_new(strlen(url_buffer)); /* Extract their Name and put it in */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, "\nName\n", 0, "\t", 0, "Undisclosed", _("Name"), 0, NULL); if (found) has_info = TRUE; /* Extract their Age and put it in */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, "\tAge\n", 0, "\n", 0, "Undisclosed", _("Age"), 0, NULL); if (found) has_info = TRUE; /* Extract their Gender and put it in */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, "\nGender\n", 0, "\t", 0, "Undisclosed", _("Gender"), 0, NULL); if (found) has_info = TRUE; /* Extract their MaritalStatus and put it in */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, "\tMarital Status\n", 0, "\n", 0, "Undisclosed", _("Marital Status"), 0, NULL); if (found) has_info = TRUE; /* Extract their Location and put it in */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, "\nLocation\n", 0, "\n", 0, "Undisclosed", _("Location"), 0, NULL); if (found) has_info = TRUE; /* Extract their Occupation and put it in */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, " Occupation\n", 6, "\n", 0, "Undisclosed", _("Occupation"), 0, NULL); if (found) has_info = TRUE; /* * The fields, 'A Little About Me', 'Favorite Things', 'Hobbies * and Interests', 'Favorite Quote', and 'My Homepage' may or may * not appear, in any combination. However, they do appear in * certain order, so we can successively search to pin down the * distinct values. */ /* Check if they have A Little About Me */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, " A Little About Me \n\n", 0, "Favorite Things", '\n', NULL, _("A Little About Me"), 0, NULL); if (!found) { found = gaim_markup_extract_info_field(stripped, stripped_len, s, " A Little About Me \n\n", 0, "Hobbies and Interests", '\n', NULL, _("A Little About Me"), 0, NULL); } if (!found) { found = gaim_markup_extract_info_field(stripped, stripped_len, s, " A Little About Me \n\n", 0, "Favorite Quote", '\n', NULL, _("A Little About Me"), 0, NULL); } if (!found) { found = gaim_markup_extract_info_field(stripped, stripped_len, s, " A Little About Me \n\n", 0, "My Homepage \n\nTake a look", '\n', NULL, _("A Little About Me"), 0, NULL); } if (!found) { gaim_markup_extract_info_field(stripped, stripped_len, s, " A Little About Me \n\n", 0, "last updated", '\n', NULL, _("A Little About Me"), 0, NULL); } if (found) has_info = TRUE; /* Check if they have Favorite Things */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, " Favorite Things \n\n", 0, "Hobbies and Interests", '\n', NULL, _("Favorite Things"), 0, NULL); if (!found) { found = gaim_markup_extract_info_field(stripped, stripped_len, s, " Favorite Things \n\n", 0, "Favorite Quote", '\n', NULL, _("Favorite Things"), 0, NULL); } if (!found) { found = gaim_markup_extract_info_field(stripped, stripped_len, s, " Favorite Things \n\n", 0, "My Homepage \n\nTake a look", '\n', NULL, _("Favorite Things"), 0, NULL); } if (!found) { gaim_markup_extract_info_field(stripped, stripped_len, s, " Favorite Things \n\n", 0, "last updated", '\n', NULL, _("Favorite Things"), 0, NULL); } if (found) has_info = TRUE; /* Check if they have Hobbies and Interests */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, " Hobbies and Interests \n\n", 0, "Favorite Quote", '\n', NULL, _("Hobbies and Interests"), 0, NULL); if (!found) { found = gaim_markup_extract_info_field(stripped, stripped_len, s, " Hobbies and Interests \n\n", 0, "My Homepage \n\nTake a look", '\n', NULL, _("Hobbies and Interests"), 0, NULL); } if (!found) { gaim_markup_extract_info_field(stripped, stripped_len, s, " Hobbies and Interests \n\n", 0, "last updated", '\n', NULL, _("Hobbies and Interests"), 0, NULL); } if (found) has_info = TRUE; /* Check if they have Favorite Quote */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, "Favorite Quote \n\n", 0, "My Homepage \n\nTake a look", '\n', NULL, _("Favorite Quote"), 0, NULL); if (!found) { gaim_markup_extract_info_field(stripped, stripped_len, s, "Favorite Quote \n\n", 0, "last updated", '\n', NULL, _("Favorite Quote"), 0, NULL); } if (found) has_info = TRUE; /* Extract the last updated date and put it in */ found = gaim_markup_extract_info_field(stripped, stripped_len, s, " last updated:", 1, "\n", 0, NULL, _("Last Updated"), 0, NULL); if (found) has_info = TRUE; /* If we were able to fetch a homepage url earlier, stick it in there */ if (user_url != NULL) { g_snprintf(buf, sizeof(buf), "<b>%s:</b><br><a href=\"%s\">%s</a><br>\n", _("Homepage"), user_url, user_url); g_string_append(s, buf); g_free(user_url); has_info = TRUE; } if (!has_info) { /* MSN doesn't actually distinguish between "unknown member" and * a known member with an empty profile. Try to explain this fact. * Note that if we have a nonempty tooltip_text, we know the user * exists. */ char *p = strstr(url_buffer, "Unknown Member </TITLE>"); GaimBuddy *b = gaim_find_buddy (gaim_connection_get_account(info_data->gc), info_data->name); g_string_append_printf(s, "<br><b>%s</b><br>%s<br><br>", _("Error retrieving profile"), ((p && b)? _("The user has not created a public profile."): p? _("MSN reported not being able to find the user's profile. " "This either means that the user does not exist, " "or that the user exists " "but has not created a public profile."): _("Gaim could not find " /* This should never happen */ "any information in the user's profile. " "The user most likely does not exist."))); } /* put a link to the actual profile URL */ g_string_append_printf(s, _("<b>%s:</b> "), _("Profile URL")); g_string_append_printf(s, "<br><a href=\"%s%s\">%s%s</a><br>", PROFILE_URL, info_data->name, PROFILE_URL, info_data->name); /* Finish it off, and show it to them */ g_string_append(s, "</body></html>\n"); #if PHOTO_SUPPORT /* Find the URL to the photo; must be before the marshalling [Bug 994207] */ photo_url_text = msn_get_photo_url(url_text); /* Marshall the existing state */ info2_data = g_malloc0(sizeof(MsnGetInfoStepTwoData)); info2_data->info_data = info_data; info2_data->stripped = stripped; info2_data->url_buffer = url_buffer; info2_data->s = s; info2_data->photo_url_text = photo_url_text; info2_data->tooltip_text = tooltip_text; info2_data->title = title; /* Try to put the photo in there too, if there's one */ if (photo_url_text) { gaim_url_fetch(photo_url_text, FALSE, NULL, FALSE, msn_got_photo, info2_data); } else { /* Emulate a callback */ msn_got_photo(info2_data, NULL, 0); } } static void msn_got_photo(void *data, const char *url_text, size_t len) { MsnGetInfoStepTwoData *info2_data = (MsnGetInfoStepTwoData *)data; int id = -1; /* Unmarshall the saved state */ MsnGetInfoData *info_data = info2_data->info_data; char *stripped = info2_data->stripped; char *url_buffer = info2_data->url_buffer; GString *s = info2_data->s; char *photo_url_text = info2_data->photo_url_text; char *tooltip_text = info2_data->tooltip_text; const char *title = info2_data->title; /* Try to put the photo in there too, if there's one and is readable */ if (data && url_text && len != 0) { if (strstr(url_text, "400 Bad Request") || strstr(url_text, "403 Forbidden") || strstr(url_text, "404 Not Found")) { gaim_debug_info("msn", "Error getting %s: %s\n", photo_url_text, url_text); } else { char buf[1024]; gaim_debug_info("msn", "%s is %d bytes\n", photo_url_text, len); id = gaim_imgstore_add(url_text, len, NULL); g_snprintf(buf, sizeof(buf), "<img id=\"%d\"><br>", id); g_string_prepend(s, buf); } } /* We continue here from msn_got_info, as if nothing has happened */ #endif g_string_prepend(s, tooltip_text); gaim_notify_userinfo(info_data->gc, info_data->name, NULL, title, NULL, s->str, NULL, NULL); g_free(stripped); g_free(url_buffer); g_string_free(s, TRUE); g_free(tooltip_text); g_free(info_data->name); g_free(info_data); #if PHOTO_SUPPORT g_free(photo_url_text); g_free(info2_data); if (id != -1) gaim_imgstore_unref(id); #endif } static void msn_get_info(GaimConnection *gc, const char *name) { MsnGetInfoData *data; char *url; data = g_new0(MsnGetInfoData, 1); data->gc = gc; data->name = g_strdup(name); url = g_strdup_printf("%s%s", PROFILE_URL, name); gaim_url_fetch(url, FALSE, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)", TRUE, msn_got_info, data); g_free(url); } static gboolean msn_load(GaimPlugin *plugin) { msn_notification_init(); msn_switchboard_init(); msn_sync_init(); return TRUE; } static gboolean msn_unload(GaimPlugin *plugin) { msn_notification_end(); msn_switchboard_end(); msn_sync_end(); return TRUE; } static GaimPluginProtocolInfo prpl_info = { OPT_PROTO_MAIL_CHECK, NULL, /* user_splits */ NULL, /* protocol_options */ {"png", 0, 0, 96, 96, GAIM_ICON_SCALE_SEND}, /* icon_spec */ msn_list_icon, /* list_icon */ msn_list_emblems, /* list_emblems */ msn_status_text, /* status_text */ msn_tooltip_text, /* tooltip_text */ msn_status_types, /* away_states */ msn_blist_node_menu, /* blist_node_menu */ NULL, /* chat_info */ NULL, /* chat_info_defaults */ msn_login, /* login */ msn_close, /* close */ msn_send_im, /* send_im */ NULL, /* set_info */ msn_send_typing, /* send_typing */ msn_get_info, /* get_info */ msn_set_status, /* set_away */ msn_set_idle, /* set_idle */ NULL, /* change_passwd */ msn_add_buddy, /* add_buddy */ NULL, /* add_buddies */ msn_rem_buddy, /* remove_buddy */ NULL, /* remove_buddies */ msn_add_permit, /* add_permit */ msn_add_deny, /* add_deny */ msn_rem_permit, /* rem_permit */ msn_rem_deny, /* rem_deny */ msn_set_permit_deny, /* set_permit_deny */ NULL, /* warn */ NULL, /* join_chat */ NULL, /* reject chat invite */ NULL, /* get_chat_name */ msn_chat_invite, /* chat_invite */ msn_chat_leave, /* chat_leave */ NULL, /* chat_whisper */ msn_chat_send, /* chat_send */ msn_keepalive, /* keepalive */ NULL, /* register_user */ NULL, /* get_cb_info */ NULL, /* get_cb_away */ NULL, /* alias_buddy */ msn_group_buddy, /* group_buddy */ msn_rename_group, /* rename_group */ NULL, /* buddy_free */ msn_convo_closed, /* convo_closed */ msn_normalize, /* normalize */ msn_set_buddy_icon, /* set_buddy_icon */ msn_remove_group, /* remove_group */ NULL, /* get_cb_real_name */ NULL, /* set_chat_topic */ NULL, /* find_blist_chat */ NULL, /* roomlist_get_list */ NULL, /* roomlist_cancel */ NULL, /* roomlist_expand_category */ msn_can_receive_file, /* can_receive_file */ msn_send_file /* send_file */ }; static GaimPluginInfo info = { GAIM_PLUGIN_MAGIC, GAIM_MAJOR_VERSION, GAIM_MINOR_VERSION, GAIM_PLUGIN_PROTOCOL, /**< type */ NULL, /**< ui_requirement */ 0, /**< flags */ NULL, /**< dependencies */ GAIM_PRIORITY_DEFAULT, /**< priority */ "prpl-msn", /**< id */ "MSN", /**< name */ VERSION, /**< version */ /** summary */ N_("MSN Protocol Plugin"), /** description */ N_("MSN Protocol Plugin"), "Christian Hammond <chipx86@gnupdate.org>", /**< author */ GAIM_WEBSITE, /**< homepage */ msn_load, /**< load */ msn_unload, /**< unload */ NULL, /**< destroy */ NULL, /**< ui_info */ &prpl_info, /**< extra_info */ NULL, /**< prefs_info */ msn_actions }; static void init_plugin(GaimPlugin *plugin) { GaimAccountOption *option; option = gaim_account_option_string_new(_("Login server"), "server", MSN_SERVER); prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); option = gaim_account_option_int_new(_("Port"), "port", 1863); prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); option = gaim_account_option_bool_new(_("Use HTTP Method"), "http_method", FALSE); prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option); gaim_prefs_remove("/plugins/prpl/msn"); } GAIM_INIT_PLUGIN(msn, init_plugin, info);