pidgin/purple-plugin-pack

Merge
pp_2_6_0
2009-08-30, John Bailey
93089a7ce7f6
Merge
/*
* IRC Helper Plugin for libpurple
*
* Copyright (C) 2005-2008, Richard Laager <rlaager@pidgin.im>
* Copyright (C) 2004-2005, Mathias Hasselmann <mathias@taschenorakel.de>
* Copyright (C) 2005, Daniel Beardsmore <uilleann@users.sf.net>
* Copyright (C) 2005, Björn Nilsson <BNI on irc.freenode.net>
* Copyright (C) 2005, Anthony Sofocleous <itchysoft_ant@users.sf.net>
*
* 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., 51 Franklin Street, Fifth Floor, Boston, MA
* 02111-1301, USA.
*/
/* If you can't figure out what this line is for, DON'T TOUCH IT. */
#include "../common/pp_internal.h"
#include <string.h>
#include <account.h>
#include <accountopt.h>
#include <cmds.h>
#include <connection.h>
#include <conversation.h>
#include <debug.h>
#include <notify.h>
#include <plugin.h>
#include <pluginpref.h>
#include <prefs.h>
#include <util.h>
#define PLUGIN_STATIC_NAME "irchelper"
#define PLUGIN_ID "core-rlaager-" PLUGIN_STATIC_NAME
#define PLUGIN_AUTHOR "Richard Laager <rlaager@guifications.org>"
/*****************************************************************************
* Constants *
*****************************************************************************/
#define IRC_PLUGIN_ID "prpl-irc"
/* Copied from SECS_BEFORE_RESENDING_AUTORESPONSE in src/server.c */
#define AUTO_RESPONSE_INTERVAL 600
#define DOMAIN_SUFFIX_FREENODE ".freenode.net"
#define DOMAIN_SUFFIX_FUNCOM ".funcom.com"
#define DOMAIN_SUFFIX_GAMESURGE ".gamesurge.net"
#define DOMAIN_SUFFIX_THUNDERCITY ".thundercity.org"
#define DOMAIN_SUFFIX_DALNET ".dal.net"
#define DOMAIN_SUFFIX_JEUX ".jeux.fr"
#define DOMAIN_SUFFIX_QUAKENET ".quakenet.org"
#define DOMAIN_SUFFIX_UNDERNET ".undernet.org"
#define MESSAGE_CHANSERV_ACCESS_LIST_ADD "You have been added to the access list for "
/* Omit the first character of the portion after the channel name. */
#define MESSAGE_CHANSERV_ACCESS_LIST_ADD_WITH_LEVEL "with level ["
#define MESSAGE_CHANSERV_ACCESS_LIST_DEL "You have been deleted from the access list for ["
#define MESSAGE_CHANSERV_NO_OP_ACCESS "You do not have channel operator access to"
#define MESSAGE_FRENCH_ADVERTISEMENT_START "<B>Avertissement</B> : Le pseudo <B>"
#define MESSAGE_FRENCH_ADVERTISEMENT_END "&lt;votre pass&gt;"
#define MESSAGE_FRENCH_LOGIN "Login <B>r?ussi</B>"
#define MESSAGE_FRENCH_MAXIMUM_CONNECTION_COUNT "Maximum de connexion"
#define MESSAGE_FRENCH_MOTD "Message du Jour :"
#define MESSAGE_PURPLE_NOTICE_PREFIX "(notice) "
#define MESSAGE_GAMESURGE_AUTHSERV_IDENTIFIED "I recognize you."
#define MESSAGE_GAMESURGE_AUTHSERV_ID_FAILURE "Incorrect password; please try again."
#define MESSAGE_GHOST_KILLED " has been killed"
#define MESSAGE_INVITED " invited "
#define MESSAGE_LOGIN_CONNECTION_COUNT "Highest connection count"
#define MESSAGE_MEMOSERV_NO_NEW_MEMOS "You have no new memos"
#define MESSAGE_MODE_NOTICE_PREFIX "mode (+"
#define MESSAGE_MODE_NOTICE_SUFFIX " ) by "
#define MESSAGE_NICKSERV_CLOAKED " set your hostname to"
#define MESSAGE_NICKSERV_ID_FAILURE "Password Incorrect"
#define MESSAGE_NICKSERV_IDENTIFIED "Password accepted - you are now recognized"
#define MESSAGE_SPOOFING_YOUR_IP "*** Spoofing your IP. congrats."
#define MESSAGE_QUAKENET_Q_CRUFT \
"Remember: NO-ONE from QuakeNet will ever ask for your password. " \
"NEVER send your password to ANYONE except Q@CServe.quakenet.org."
#define MESSAGE_QUAKENET_Q_ID_FAILURE \
"Lastly, When you do recover your password, please choose a NEW PASSWORD, not your old one! " \
"See the above URL for details."
#define MESSAGE_QUAKENET_Q_IDENTIFIED "AUTH&apos;d successfully."
#define MESSAGE_UNREAL_IRCD_HOSTNAME_FOUND "*** Found your hostname"
#define MESSAGE_UNREAL_IRCD_HOSTNAME_LOOKUP "*** Looking up your hostname..."
#define MESSAGE_UNREAL_IRCD_IDENT_LOOKUP "*** Checking ident..."
#define MESSAGE_UNREAL_IRCD_IDENT_NO_RESPONSE "*** No ident response; username prefixed with ~"
#define MESSAGE_UNREAL_IRCD_PONG_CRUFT \
"*** If you are having problems connecting due to ping timeouts, please type /quote pong"
/* Generic AuthServ, not currently used for any networks. */
#define NICK_AUTHSERV "AuthServ"
#define NICK_CHANSERV "ChanServ"
#define NICK_FREENODE_CONNECT "freenode-connect"
#define NICK_FUNCOM_Q_SERVICE NICK_QUAKENET_Q "@cserve.funcom.com"
#define NICK_GAMESURGE_AUTHSERV "AuthServ"
#define NICK_GAMESURGE_AUTHSERV_SERVICE NICK_GAMESURGE_AUTHSERV "@Services.GameSurge.net"
#define NICK_GAMESURGE_GLOBAL "Global"
#define NICK_JEUX_Z "Z"
#define NICK_JEUX_WELCOME "[Welcome]"
#define NICK_MEMOSERV "MemoServ"
#define NICK_NICKSERV "NickServ"
#define NICK_DALNET_AUTHSERV_SERVICE NICK_NICKSERV "@services.dal.net"
#define NICK_QUAKENET_L "L"
#define NICK_QUAKENET_Q "Q"
#define NICK_QUAKENET_Q_SERVICE NICK_QUAKENET_Q "@CServe.quakenet.org"
#define NICK_UNDERNET_X "x@channels.undernet.org"
/* The %c exists so we can tell if a match occurred. */
#define PATTERN_WEIRD_LOGIN_CRUFT "o%c %*u ca %*u(%*u) ft %*u(%*u)"
#define TIMEOUT_IDENTIFY 4000
#define TIMEOUT_KILLING_GHOST 4000
typedef enum {
IRC_NONE = 0x0000,
IRC_KILLING_GHOST = 0x0001,
IRC_WILL_ID = 0x0002,
IRC_DID_ID = 0x0004,
IRC_ID_FAILED = 0x0008,
IRC_NETWORK_TYPE_UNKNOWN = 0x0010,
IRC_NETWORK_TYPE_GAMESURGE = 0x0020,
IRC_NETWORK_TYPE_NICKSERV = 0x0040,
IRC_NETWORK_TYPE_QUAKENET = 0x0080,
IRC_NETWORK_TYPE_JEUX = 0x0100,
IRC_NETWORK_TYPE_UNDERNET = 0x0200,
IRC_NETWORK_TYPE_THUNDERCITY = 0x0400,
IRC_NETWORK_TYPE_DALNET = 0x0800,
IRC_NETWORK_TYPE_FUNCOM = 0x1000,
} IRCHelperStateFlags;
struct proto_stuff
{
gpointer *proto_data;
PurpleAccount *account;
};
GHashTable *states;
/*****************************************************************************
* Prototypes *
*****************************************************************************/
static gboolean plugin_load(PurplePlugin *plugin);
static gboolean plugin_unload(PurplePlugin *plugin);
/*****************************************************************************
* Plugin Info *
*****************************************************************************/
static PurplePluginInfo info =
{
PURPLE_PLUGIN_MAGIC,
PURPLE_MAJOR_VERSION,
0,
PURPLE_PLUGIN_STANDARD, /**< type */
NULL, /**< ui_requirement */
0, /**< flags */
NULL, /**< dependencies */
PURPLE_PRIORITY_DEFAULT, /**< priority */
PLUGIN_ID, /**< id */
NULL, /**< name */
PP_VERSION, /**< version */
NULL, /**< summary */
NULL, /**< description */
PLUGIN_AUTHOR, /**< author */
PP_WEBSITE, /**< homepage */
plugin_load, /**< load */
plugin_unload, /**< unload */
NULL, /**< destroy */
NULL, /**< ui_info */
NULL, /**< extra_info */
NULL, /**< prefs_info */
NULL, /**< actions */
NULL, /**< reserved 1 */
NULL, /**< reserved 2 */
NULL, /**< reserved 3 */
NULL /**< reserved 4 */
};
/* XXX: This is a dirty hack. It's better than what I was doing before, though. */
static PurpleConversation *get_conversation(PurpleAccount *account)
{
PurpleConversation *conv;
conv = g_new0(PurpleConversation, 1);
conv->type = PURPLE_CONV_TYPE_IM;
/* If we use this then the conversation updated signal is fired and
* other plugins might start doing things to our conversation, such as
* setting data on it which we would then need to free etc. It's easier
* just to be more hacky by setting account directly. */
/* purple_conversation_set_account(conv, account); */
conv->account = account;
return conv;
}
static IRCHelperStateFlags get_connection_type(PurpleConnection *connection)
{
PurpleAccount *account;
const gchar *protocol;
gchar *username;
IRCHelperStateFlags type = IRC_NETWORK_TYPE_UNKNOWN;
g_return_val_if_fail(NULL != connection, IRC_NETWORK_TYPE_UNKNOWN);
account = purple_connection_get_account(connection);
protocol = purple_account_get_protocol_id(account);
g_return_val_if_fail(g_str_equal(protocol, IRC_PLUGIN_ID), IRC_NETWORK_TYPE_UNKNOWN);
username = g_utf8_strdown(purple_account_get_username(account), -1);
if (g_str_has_suffix(username, DOMAIN_SUFFIX_GAMESURGE))
type = IRC_NETWORK_TYPE_GAMESURGE;
else if (g_str_has_suffix(username, DOMAIN_SUFFIX_THUNDERCITY))
type = IRC_NETWORK_TYPE_THUNDERCITY;
else if (g_str_has_suffix(username, DOMAIN_SUFFIX_DALNET))
type = IRC_NETWORK_TYPE_DALNET;
else if (g_str_has_suffix(username, DOMAIN_SUFFIX_QUAKENET))
type = IRC_NETWORK_TYPE_QUAKENET;
else if (g_str_has_suffix(username, DOMAIN_SUFFIX_FUNCOM))
type = IRC_NETWORK_TYPE_FUNCOM;
else if (g_str_has_suffix(username, DOMAIN_SUFFIX_JEUX))
type = IRC_NETWORK_TYPE_JEUX;
else if (g_str_has_suffix(username, DOMAIN_SUFFIX_UNDERNET))
type = IRC_NETWORK_TYPE_UNDERNET;
g_free(username);
return type;
}
static gboolean auth_timeout(gpointer proto_data)
{
IRCHelperStateFlags state;
state = GPOINTER_TO_INT(g_hash_table_lookup(states, proto_data));
if (state & IRC_WILL_ID)
{
purple_debug_info(PLUGIN_STATIC_NAME, "Authentication failed: timeout expired\n");
g_hash_table_insert(states, proto_data,
GINT_TO_POINTER((state & ~IRC_WILL_ID) | IRC_DID_ID));
}
return FALSE;
}
/*****************************************************************************
* AuthServ Helper Functions *
*****************************************************************************/
static void authserv_identify(const char *command, PurpleConnection *connection, IRCHelperStateFlags state)
{
PurpleAccount *account;
gchar **userparts = NULL;
const gchar *username;
const gchar *password;
g_return_if_fail(NULL != connection);
account = purple_connection_get_account(connection);
username = purple_account_get_string(account, PLUGIN_ID "_authname", "");
if (NULL == username || '\0' == *username)
{
userparts = g_strsplit(purple_account_get_username(account), "@", 2);
username = userparts[0];
}
password = purple_account_get_string(account, PLUGIN_ID "_nickpassword", "");
if (NULL != username && '\0' != *username &&
NULL != password && '\0' != *password)
{
const gchar *authserv = NICK_AUTHSERV;
gchar *authentication = g_strconcat(command, " ", username, " ", password, NULL);
purple_debug_misc(PLUGIN_STATIC_NAME, "Sending authentication: %s %s <PASSWORD>\n", command, username);
g_hash_table_insert(states,
connection->proto_data,
GINT_TO_POINTER(state | IRC_WILL_ID));
if (state & IRC_NETWORK_TYPE_GAMESURGE)
authserv = NICK_GAMESURGE_AUTHSERV_SERVICE;
else if (state & IRC_NETWORK_TYPE_DALNET)
authserv = NICK_DALNET_AUTHSERV_SERVICE;
else if (state & IRC_NETWORK_TYPE_QUAKENET)
authserv = NICK_QUAKENET_Q_SERVICE;
else if (state & IRC_NETWORK_TYPE_FUNCOM)
authserv = NICK_FUNCOM_Q_SERVICE;
else if (state & IRC_NETWORK_TYPE_UNDERNET)
authserv = NICK_UNDERNET_X;
serv_send_im(connection, authserv, authentication, 0);
g_free(authentication);
/* Register a timeout... If we don't get the expected response from AuthServ,
* we need to stop suppressing messages from it at some point or the user
* could be very confused.
*/
purple_timeout_add(TIMEOUT_IDENTIFY, (GSourceFunc)auth_timeout,
(gpointer)connection->proto_data);
}
g_strfreev(userparts);
}
/*****************************************************************************
* Operator Helper Functions *
*****************************************************************************/
static void jeux_identify(PurpleConnection *connection, IRCHelperStateFlags state)
{
PurpleAccount *account;
gchar **userparts;
const gchar *username;
const gchar *password;
g_return_if_fail(NULL != connection);
account = purple_connection_get_account(connection);
userparts = g_strsplit(purple_account_get_username(account), "@", 2);
username = userparts[0];
password = purple_account_get_string(account, PLUGIN_ID "_nickpassword", "");
if (NULL != username && '\0' != *username &&
NULL != password && '\0' != *password)
{
gchar *authentication = g_strdup_printf("quote %s login %s %s", NICK_JEUX_Z, username, password);
PurpleConversation *conv = get_conversation(account);
gchar *error;
purple_debug_misc(PLUGIN_STATIC_NAME, "Sending authentication: quote %s login %s <PASSWORD>\n", NICK_JEUX_Z, username);
g_hash_table_insert(states,
connection->proto_data,
GINT_TO_POINTER(state | IRC_WILL_ID));
if (purple_cmd_do_command(conv, authentication, authentication, &error) != PURPLE_CMD_STATUS_OK)
{
/* TODO: PRINT ERROR MESSAGE */
g_free(error);
}
g_free(conv);
g_free(authentication);
/* Register a timeout... If we don't get the expected response from AuthServ,
* we need to stop suppressing messages from it at some point or the user
* could be very confused.
*/
purple_timeout_add(TIMEOUT_IDENTIFY, (GSourceFunc)auth_timeout,
(gpointer)connection->proto_data);
}
g_strfreev(userparts);
}
/*****************************************************************************
* Operator Helper Functions *
*****************************************************************************/
static void oper_identify(PurpleAccount *account)
{
const char *operpassword;
operpassword = purple_account_get_string(account, PLUGIN_ID "_operpassword", "");
if ('\0' != *operpassword)
{
PurpleConversation *conv = get_conversation(account);
PurpleConnection *connection = purple_account_get_connection(account);
const gchar *name = purple_connection_get_display_name(connection);
char *command = g_strdup_printf("quote OPER %s %s", name, operpassword);
gchar *error;
if (purple_cmd_do_command(conv, command, command, &error) != PURPLE_CMD_STATUS_OK)
{
/* TODO: PRINT ERROR MESSAGE */
g_free(error);
}
g_free(command);
g_free(conv);
}
}
/*****************************************************************************
* NickServ Helper Functions *
*****************************************************************************/
static void nickserv_do_identify(char *authentication, gpointer proto_data, PurpleConnection *gc, const char *nickpassword)
{
PurpleConversation *conv = get_conversation(purple_connection_get_account(gc));
gchar *error;
gchar *tmp;
purple_debug_misc(PLUGIN_STATIC_NAME, "Sending authentication: %s <PASSWORD>\n", authentication);
tmp = g_strconcat(authentication, " ", nickpassword, NULL);
g_free(authentication);
authentication = tmp;
if (purple_cmd_do_command(conv, authentication, authentication, &error) != PURPLE_CMD_STATUS_OK)
{
/* TODO: PRINT ERROR MESSAGE */
g_free(error);
}
g_free(authentication);
g_free(conv);
/* Register a timeout... If we don't get the expected response from NickServ,
* we need to stop suppressing messages from it at some point or the user
* could be very confused.
*/
purple_timeout_add(TIMEOUT_IDENTIFY, (GSourceFunc)auth_timeout,
(gpointer)proto_data);
}
static void nickserv_identify(gpointer proto_data, PurpleConnection *gc, const char *nickpassword)
{
char *authentication = g_strdup_printf("quote %s IDENTIFY", NICK_NICKSERV);
nickserv_do_identify(authentication, proto_data, gc, nickpassword);
}
static void nickserv_msg_identify(const char *command, gpointer proto_data, PurpleConnection *gc, const char *nickpassword)
{
char *authentication = g_strdup_printf("quote PRIVMSG %s : %s", NICK_NICKSERV, command);
nickserv_do_identify(authentication, proto_data, gc, nickpassword);
}
static gboolean ghosted_nickname_killed_cb(struct proto_stuff *stuff)
{
PurpleConnection *gc;
char **userparts;
IRCHelperStateFlags state;
PurpleConversation *conv;
char *command;
gchar *error;
state = GPOINTER_TO_INT(g_hash_table_lookup(states, stuff->proto_data));
/* We only want this function to act once. Under normal circumstances,
* it's going to get called twice: Once when NickServ tells us the ghost
* has been killed and once when the timeout expires. Under abnormal
* conditions, it will only be called when the timeout expires.
*/
if (!(state & IRC_KILLING_GHOST))
{
g_free(stuff);
return FALSE;
}
g_hash_table_insert(states, stuff->proto_data,
GINT_TO_POINTER((state & ~IRC_KILLING_GHOST) | IRC_WILL_ID));
gc = purple_account_get_connection(stuff->account);
if (NULL == gc)
{
g_free(stuff);
return FALSE;
}
userparts = g_strsplit(purple_account_get_username(stuff->account), "@", 2);
/* Switch back to the normal nickname. */
conv = get_conversation(stuff->account);
command = g_strdup_printf("nick %s", userparts[0]);
if (purple_cmd_do_command(conv, command, command, &error) != PURPLE_CMD_STATUS_OK)
{
/* TODO: PRINT ERROR MESSAGE */
g_free(error);
}
g_free(command);
g_free(conv);
nickserv_identify(stuff->proto_data, gc, purple_account_get_string(stuff->account,
PLUGIN_ID "_nickpassword", ""));
g_strfreev(userparts);
g_free(stuff);
oper_identify(stuff->account);
return FALSE;
}
/*****************************************************************************
* Callbacks *
*****************************************************************************/
static void signed_on_cb(PurpleConnection *connection)
{
PurpleAccount *account;
IRCHelperStateFlags state;
const char *nickpassword;
g_return_if_fail(NULL != connection);
g_return_if_fail(NULL != connection->proto_data);
account = purple_connection_get_account(connection);
g_return_if_fail(NULL != account);
if (!g_str_equal(purple_account_get_protocol_id(account), IRC_PLUGIN_ID))
return;
state = get_connection_type(connection);
if (state & IRC_NETWORK_TYPE_GAMESURGE)
{
purple_debug_info(PLUGIN_STATIC_NAME, "Connected with GameSurge: %s\n",
purple_connection_get_display_name(connection));
authserv_identify("AUTH", connection, state);
}
if (state & IRC_NETWORK_TYPE_DALNET)
{
purple_debug_info(PLUGIN_STATIC_NAME, "Connected with DalNet: %s\n",
purple_connection_get_display_name(connection));
authserv_identify("IDENTIFY", connection, state);
}
else if (state & IRC_NETWORK_TYPE_JEUX)
{
purple_debug_info(PLUGIN_STATIC_NAME, "Connected with Jeux.fr: %s\n",
purple_connection_get_display_name(connection));
jeux_identify(connection, state);
}
else if (state & IRC_NETWORK_TYPE_QUAKENET)
{
purple_debug_info(PLUGIN_STATIC_NAME, "Connected with QuakeNet: %s\n",
purple_connection_get_display_name(connection));
authserv_identify("AUTH", connection, state);
}
else if (state & IRC_NETWORK_TYPE_UNDERNET)
{
purple_debug_info(PLUGIN_STATIC_NAME, "Connected with UnderNet: %s\n",
purple_connection_get_display_name(connection));
authserv_identify("login ", connection, state);
}
else if (state & IRC_NETWORK_TYPE_FUNCOM)
{
purple_debug_info(PLUGIN_STATIC_NAME, "Connected with Funcom: %s\n",
purple_connection_get_display_name(connection));
authserv_identify("AUTH", connection, state);
}
else
{
nickpassword = purple_account_get_string(account, PLUGIN_ID "_nickpassword", "");
if ('\0' != *nickpassword)
{
char **userparts;
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER(IRC_NETWORK_TYPE_NICKSERV | IRC_WILL_ID));
userparts = g_strsplit(purple_account_get_username(account), "@", 2);
if (purple_account_get_bool(account, PLUGIN_ID "_disconnectghosts", 0) &&
strcmp(userparts[0], purple_connection_get_display_name(connection)))
{
struct proto_stuff *stuff = g_new0(struct proto_stuff, 1);
char *command;
PurpleConversation *conv;
gchar *error;
stuff->proto_data = connection->proto_data;
stuff->account = account;
/* Disconnect the ghosted connection. */
command = g_strdup_printf("quote %s GHOST %s %s", NICK_NICKSERV, userparts[0], nickpassword);
conv = get_conversation(account);
purple_debug_misc(PLUGIN_STATIC_NAME, "Sending command: quote %s GHOST %s <PASSWORD>\n", NICK_NICKSERV, userparts[0]);
if (purple_cmd_do_command(conv, command, command, &error) != PURPLE_CMD_STATUS_OK)
{
/* TODO: PRINT ERROR MESSAGE */
g_free(error);
}
g_free(command);
g_free(conv);
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER(IRC_NETWORK_TYPE_NICKSERV | IRC_KILLING_GHOST));
/* We have to wait for the server to disconnect the
* other username before we can reclaim it. This
* timeout sets an upper bound on the length of time
* we'll wait for the ghost to be killed.
*/
purple_timeout_add(TIMEOUT_KILLING_GHOST,
(GSourceFunc)ghosted_nickname_killed_cb,
(gpointer)stuff);
g_strfreev(userparts);
return;
}
g_strfreev(userparts);
if (state & IRC_NETWORK_TYPE_THUNDERCITY)
nickserv_msg_identify("AUTH", connection->proto_data, connection, nickpassword);
else
nickserv_identify(connection->proto_data, connection, nickpassword);
}
}
oper_identify(account);
}
static void conversation_created_cb(PurpleConversation *conv)
{
purple_conversation_set_data(conv, PLUGIN_ID "_start_time",
GINT_TO_POINTER((int)time(NULL)));
}
static gboolean writing_chat_msg_cb(PurpleAccount *account, const char *who,
char **message, PurpleConversation *conv,
PurpleMessageFlags flags)
{
const gchar *name;
const gchar *topic;
if (!g_str_equal(purple_account_get_protocol_id(account), IRC_PLUGIN_ID))
return FALSE;
if (NULL == *message)
return FALSE;
g_return_val_if_fail(purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT, FALSE);
if (flags & PURPLE_MESSAGE_SYSTEM &&
(g_str_has_prefix(*message, MESSAGE_MODE_NOTICE_PREFIX "v ") ||
g_str_has_prefix(*message, MESSAGE_MODE_NOTICE_PREFIX "o ")))
{
const char *tmp = *message + sizeof(MESSAGE_MODE_NOTICE_PREFIX "X ") - 1;
const char *name = purple_connection_get_display_name(purple_account_get_connection(account));
if (g_str_has_prefix(tmp, name) &&
g_str_has_prefix(tmp + strlen(name), MESSAGE_MODE_NOTICE_SUFFIX NICK_CHANSERV))
{
if (time(NULL) < (time_t)GPOINTER_TO_INT(purple_conversation_get_data(conv, PLUGIN_ID "_start_time")) + 10)
return TRUE;
}
}
if (flags & PURPLE_MESSAGE_SYSTEM &&
(topic = purple_conv_chat_get_topic(PURPLE_CONV_CHAT(conv))) != NULL &&
(name = purple_conversation_get_name(conv)) != NULL)
{
char *name_escaped = g_markup_escape_text(name, -1);
char *topic_escaped = g_markup_escape_text(topic, -1);
char *topic_linkified = purple_markup_linkify(topic_escaped);
if (strstr(*message, name_escaped) != NULL &&
strstr(*message, topic_linkified) != NULL)
{
/* We've got a topic notice. */
PurpleBlistNode *node;
if ((node = (PurpleBlistNode *)purple_blist_find_chat(account, name)) != NULL)
{
const char *last_topic = purple_blist_node_get_string(node, PLUGIN_ID "_topic");
/* If we saw this the last time we joined, suppress it. */
if (last_topic != NULL && strcmp(topic, last_topic) == 0)
{
g_free(name_escaped);
g_free(topic_escaped);
g_free(topic_linkified);
return TRUE;
}
else
purple_blist_node_set_string(node, PLUGIN_ID "_topic", topic);
}
}
g_free(name_escaped);
g_free(topic_escaped);
g_free(topic_linkified);
}
return FALSE;
}
static GSList *auto_responses = NULL;
struct auto_response {
PurpleConnection *gc;
char *name;
time_t received;
char *message;
};
static gboolean
expire_auto_responses(gpointer data)
{
GSList *tmp, *cur;
struct auto_response *ar;
tmp = auto_responses;
while (tmp) {
cur = tmp;
tmp = tmp->next;
ar = (struct auto_response *)cur->data;
if ((time(NULL) - ar->received) > AUTO_RESPONSE_INTERVAL) {
auto_responses = g_slist_remove(auto_responses, ar);
g_free(ar->message);
g_free(ar);
}
}
return FALSE; /* do not run again */
}
static struct auto_response *
get_auto_response(PurpleConnection *gc, const char *name)
{
GSList *tmp;
struct auto_response *ar;
purple_timeout_add((AUTO_RESPONSE_INTERVAL + 1) * 1000, expire_auto_responses, NULL);
tmp = auto_responses;
while (tmp) {
ar = (struct auto_response *)tmp->data;
if (gc == ar->gc && !strncmp(name, ar->name, sizeof(ar->name)))
return ar;
tmp = tmp->next;
}
ar = (struct auto_response *)g_new0(struct auto_response, 1);
ar->name = g_strdup(name);
ar->gc = gc;
ar->received = 0;
auto_responses = g_slist_prepend(auto_responses, ar);
return ar;
}
static gboolean receiving_im_msg_cb(PurpleAccount *account, gchar **sender, gchar **buffer,
PurpleConversation *conv,
gint *flags, gpointer data)
{
gchar *msg;
gchar *nick;
PurpleConversation *chat;
PurpleConnection *connection;
IRCHelperStateFlags state;
gchar *invite_prefix;
if (!g_str_equal(purple_account_get_protocol_id(account), IRC_PLUGIN_ID))
return FALSE;
msg = *buffer;
nick = *sender;
connection = purple_account_get_connection(account);
g_return_val_if_fail(NULL != connection, FALSE);
state = GPOINTER_TO_INT(g_hash_table_lookup(states, connection->proto_data));
/* SUPPRESS EXTRA AUTO-RESPONSES */
if (*flags & PURPLE_MESSAGE_AUTO_RESP)
{
/* The same idea as the code in src/server.c. */
struct auto_response *ar = get_auto_response(connection, nick);
time_t now = time(NULL);
if ((now - ar->received) <= AUTO_RESPONSE_INTERVAL &&
!strcmp(msg, ar->message))
{
/* We've recently received an auto-response
* and it's the same as the one we've just received.
* Drop it!
*/
ar->received = now;
return TRUE;
}
ar->received = now;
g_free(ar->message);
ar->message = g_strdup(msg);
/* None of the other rules are for auto-responses. */
return FALSE;
}
/* SIMPLE SUPPRESSION RULES */
/* Suppress the FreeNode stats collection bot. */
if (g_str_equal(nick, NICK_FREENODE_CONNECT))
{
return TRUE;
}
/* Suppress useless ChanServ notification. */
if (g_str_equal(nick, NICK_CHANSERV) &&
g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_CHANSERV_NO_OP_ACCESS))
{
return TRUE;
}
/* Suppress GameSurge message(s) of the day. */
if (state & IRC_NETWORK_TYPE_GAMESURGE &&
g_str_equal(nick, NICK_GAMESURGE_GLOBAL))
{
return TRUE;
}
/* Suppress useless Jeux welcome. */
if (g_str_equal(nick, NICK_JEUX_WELCOME))
{
return TRUE;
}
/* Suppress useless MemoServ notification. */
if (g_str_equal(nick, NICK_MEMOSERV) &&
g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_MEMOSERV_NO_NEW_MEMOS))
{
return TRUE;
}
/* Suppress QuakeNet Q password warning. */
if (g_str_equal(nick, NICK_QUAKENET_Q) &&
g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_QUAKENET_Q_CRUFT))
{
return TRUE;
}
/* Suppress Z registration notice. */
if (g_str_equal(nick, "Z") &&
g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_FRENCH_ADVERTISEMENT_START) &&
g_str_has_suffix(msg, MESSAGE_FRENCH_ADVERTISEMENT_END))
{
return TRUE;
}
/* Suppress Z's successful login notice. */
if (g_str_equal(nick, "Z") &&
(g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_FRENCH_LOGIN) ||
g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_FRENCH_MOTD)))
{
return TRUE;
}
/* Suppress login message for the highest connection count. */
if (g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX
MESSAGE_LOGIN_CONNECTION_COUNT) ||
g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX
MESSAGE_FRENCH_MAXIMUM_CONNECTION_COUNT))
{
return TRUE;
}
/* Suppress UnrealIRCd login cruft. */
if (g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_UNREAL_IRCD_HOSTNAME_FOUND) ||
g_str_equal( msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_UNREAL_IRCD_HOSTNAME_LOOKUP) ||
g_str_equal( msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_UNREAL_IRCD_IDENT_LOOKUP) ||
g_str_equal( msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_UNREAL_IRCD_IDENT_NO_RESPONSE) ||
g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_UNREAL_IRCD_PONG_CRUFT))
{
return TRUE;
}
/* Suppress hostname cloak notification on Freenode. */
if (g_str_has_suffix(nick, DOMAIN_SUFFIX_FREENODE) &&
g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX NICK_NICKSERV MESSAGE_NICKSERV_CLOAKED))
{
return TRUE;
}
/* Suppress "IP spoofing" notice on worldforge.org:
* http://plugins.guifications.org/trac/ticket/524 */
if (g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_SPOOFING_YOUR_IP))
{
return TRUE;
}
/* SLIGHTLY COMPLICATED SUPPRESSION RULES */
/* Supress QuakeNet and UnderNet Weird Login Cruft */
{
char temp;
if (sscanf(msg, MESSAGE_PURPLE_NOTICE_PREFIX PATTERN_WEIRD_LOGIN_CRUFT, &temp) == 1)
{
return TRUE;
}
}
/* Suppress silly notices of my own invites. */
invite_prefix = g_strconcat(MESSAGE_PURPLE_NOTICE_PREFIX,
purple_connection_get_display_name(connection), MESSAGE_INVITED, NULL);
if (g_str_has_prefix(msg, invite_prefix))
{
g_free(invite_prefix);
return TRUE;
}
g_free(invite_prefix);
/* SERVICE MAGIC */
/* Display ChanServ Access List Add Notifications in the chat window. */
if (g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_CHANSERV_ACCESS_LIST_ADD) &&
g_str_equal(nick, NICK_CHANSERV))
{
gchar *tmp;
gchar *channel;
gchar *level = NULL;
gchar *msg2;
channel = purple_markup_strip_html(msg + sizeof(MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_CHANSERV_ACCESS_LIST_ADD) - 1);
if ((tmp = strchr(channel, ' ')) != NULL)
{
*tmp = '\0';
if (g_str_has_prefix(tmp + 1, MESSAGE_CHANSERV_ACCESS_LIST_ADD_WITH_LEVEL))
{
/* The + 1 and - 1 are both kept to make it clear how that relates with other code. */
level = tmp + 1 + sizeof(MESSAGE_CHANSERV_ACCESS_LIST_ADD_WITH_LEVEL) - 1;
if ((tmp = strchr(level, ']')) != NULL)
*tmp = '\0';
}
}
if (NULL == level)
msg2 = g_strdup(_("You have been added to the access list."));
else
msg2 = g_strdup_printf(_("You have been added to the access list with an access level of %s."), level);
chat = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, channel, account);
if (chat)
{
purple_conv_chat_write(PURPLE_CONV_CHAT(chat), nick,
msg2, PURPLE_MESSAGE_SYSTEM, time(NULL));
g_free(channel);
g_free(msg2);
return TRUE;
}
g_free(channel);
g_free(msg2);
return FALSE;
}
/* Display ChanServ Access List Delete Notifications in the chat window. */
if (g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_CHANSERV_ACCESS_LIST_DEL) &&
g_str_equal(nick, NICK_CHANSERV))
{
gchar *tmp;
gchar *channel;
channel = purple_markup_strip_html(msg + sizeof(MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_CHANSERV_ACCESS_LIST_DEL) - 1);
if ((tmp = strchr(channel, ']')) != NULL)
*tmp = '\0';
chat = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, channel, account);
if (chat)
{
purple_conv_chat_write(PURPLE_CONV_CHAT(chat), nick,
_("You have been removed from the access list."),
PURPLE_MESSAGE_SYSTEM, time(NULL));
g_free(channel);
return TRUE;
}
g_free(channel);
return FALSE;
}
/* Display ChanServ channel join messages in the chat window. */
if (g_str_has_prefix(msg, MESSAGE_PURPLE_NOTICE_PREFIX "[#") &&
(g_str_equal(nick, NICK_CHANSERV) || g_str_equal(nick, NICK_QUAKENET_L)))
{
gchar *msg2;
gchar *msg3;
gchar *channel;
/* Duplicate the message so we can modify the string in place. */
msg2 = g_strdup(msg);
/* We need to keep a pointer to the beginning of the string so we can free it later. */
msg3 = msg2;
/* channel needs to start with the # */
channel = msg3;
channel += sizeof(MESSAGE_PURPLE_NOTICE_PREFIX);
/* Find the "]", set it to the null character.
* This makes chan contain just the channel.
* Then, increment msg3 two spots so it contains
* just the message.
*/
if ((msg3 = g_strstr_len(msg3, strlen(msg3), "]")))
{
PurpleBlistNode *node;
*msg3 = '\0';
/* Make sure it's safe to increment the pointer */
if ('\0' == msg3[1] || '\0' == msg3[2])
{
g_free(msg2);
return FALSE;
}
msg3 += 2;
if ((node = (PurpleBlistNode *)purple_blist_find_chat(account, channel)) != NULL)
{
const char *last_msg = purple_blist_node_get_string(node, PLUGIN_ID "_chanserv_join_msg");
/* If we saw this the last time we joined, suppress it. */
if (last_msg != NULL && strcmp(msg3, last_msg) == 0)
{
g_free(msg2);
return TRUE;
}
else
purple_blist_node_set_string(node, PLUGIN_ID "_chanserv_join_msg", msg3);
}
/* Write the message to the chat as a system message. */
chat = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, channel, account);
if (chat)
{
purple_conv_chat_write(PURPLE_CONV_CHAT(chat), nick,
msg3, PURPLE_MESSAGE_SYSTEM, time(NULL));
g_free(msg2);
return TRUE;
}
}
g_free(msg2);
return FALSE;
}
/* Suppress useless NickServ notifications if we're identifying automatically. */
if (state & IRC_NETWORK_TYPE_NICKSERV &&
(state & IRC_WILL_ID || state & IRC_KILLING_GHOST) &&
g_str_equal(nick, NICK_NICKSERV))
{
/* Track that the identification is finished. */
if (g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_NICKSERV_IDENTIFIED))
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER((state & ~IRC_KILLING_GHOST & ~IRC_WILL_ID)
| IRC_DID_ID));
/* The ghost has been killed, continue the signon. */
if (state & IRC_KILLING_GHOST && strstr(msg, MESSAGE_GHOST_KILLED))
{
struct proto_stuff *stuff = g_new0(struct proto_stuff, 1);
stuff->proto_data = connection->proto_data;
stuff->account = account;
ghosted_nickname_killed_cb(stuff);
}
/* The identification has failed. */
if (g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_NICKSERV_ID_FAILURE))
{
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER((state & ~IRC_KILLING_GHOST & ~IRC_WILL_ID)
| IRC_ID_FAILED));
purple_notify_error(NULL,
_("NickServ Authentication Error"),
_("Error authenticating with NickServ"),
_("Check your password."));
}
return TRUE;
}
/* Suppress useless AuthServ notifications if we're identifying automatically. */
if (state & IRC_NETWORK_TYPE_GAMESURGE &&
state & IRC_WILL_ID &&
g_str_equal(nick, NICK_GAMESURGE_AUTHSERV))
{
/* Track that the identification is finished. */
if (g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_GAMESURGE_AUTHSERV_IDENTIFIED))
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER((state & ~IRC_WILL_ID) | IRC_DID_ID));
/* The identification has failed. */
if (g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_GAMESURGE_AUTHSERV_ID_FAILURE))
{
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER((state & ~IRC_WILL_ID) | IRC_ID_FAILED));
purple_notify_error(NULL,
_("GameSurge Authentication Error"),
_("Error authenticating with AuthServ"),
_("Check your password."));
}
return TRUE;
}
/* Suppress useless Q notifications if we're identifying automatically. */
if ((state & IRC_NETWORK_TYPE_QUAKENET ||
state & IRC_NETWORK_TYPE_FUNCOM) &&
state & IRC_WILL_ID &&
g_str_equal(nick, NICK_QUAKENET_Q))
{
/* Track that the identification is finished. */
if (g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_QUAKENET_Q_IDENTIFIED))
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER((state & ~IRC_WILL_ID) | IRC_DID_ID));
/* The identification has failed. */
if (g_str_equal(msg, MESSAGE_PURPLE_NOTICE_PREFIX MESSAGE_QUAKENET_Q_ID_FAILURE))
{
g_hash_table_insert(states, connection->proto_data,
GINT_TO_POINTER((state & ~IRC_WILL_ID) | IRC_ID_FAILED));
purple_notify_error(NULL,
_("QuakeNet Authentication Error"),
_("Error authenticating with Q"),
_("Check your password."));
}
return TRUE;
}
return FALSE;
}
/*****************************************************************************
* Plugin Code *
*****************************************************************************/
static gboolean plugin_load(PurplePlugin *plugin)
{
PurplePlugin *irc_prpl;
PurplePluginProtocolInfo *prpl_info;
PurpleAccountOption *option;
void *conn_handle;
void *conv_handle;
irc_prpl = purple_plugins_find_with_id(IRC_PLUGIN_ID);
if (NULL == irc_prpl)
return FALSE;
prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(irc_prpl);
if (NULL == prpl_info)
return FALSE;
/* Create hash table. */
states = g_hash_table_new(g_direct_hash, g_direct_equal);
/* Register protocol preferences. */
option = purple_account_option_string_new(_("Auth name"), PLUGIN_ID "_authname", "");
prpl_info->protocol_options = g_list_append(prpl_info->protocol_options, option);
option = purple_account_option_string_new(_("Nick password"), PLUGIN_ID "_nickpassword", "");
purple_account_option_set_masked(option, TRUE);
prpl_info->protocol_options = g_list_append(prpl_info->protocol_options, option);
option = purple_account_option_bool_new(_("Disconnect ghosts (Duplicate nicknames)"),
PLUGIN_ID "_disconnectghosts", 0);
prpl_info->protocol_options = g_list_append(prpl_info->protocol_options, option);
option = purple_account_option_string_new(_("Operator password"), PLUGIN_ID "_operpassword", "");
purple_account_option_set_masked(option, TRUE);
prpl_info->protocol_options = g_list_append(prpl_info->protocol_options, option);
/* Register callbacks. */
conn_handle = purple_connections_get_handle();
conv_handle = purple_conversations_get_handle();
purple_signal_connect(conn_handle, "signed-on",
plugin, PURPLE_CALLBACK(signed_on_cb), NULL);
purple_signal_connect(conv_handle, "conversation-created",
plugin, PURPLE_CALLBACK(conversation_created_cb), NULL);
purple_signal_connect(conv_handle, "receiving-im-msg",
plugin, PURPLE_CALLBACK(receiving_im_msg_cb), NULL);
purple_signal_connect(conv_handle, "writing-chat-msg",
plugin, PURPLE_CALLBACK(writing_chat_msg_cb), NULL);
return TRUE;
}
static gboolean plugin_unload(PurplePlugin *plugin)
{
PurplePlugin *irc_prpl;
PurplePluginProtocolInfo *prpl_info;
GList *list;
irc_prpl = purple_plugins_find_with_id(IRC_PLUGIN_ID);
if (NULL == irc_prpl)
return FALSE;
prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(irc_prpl);
if (NULL == prpl_info)
return FALSE;
list = prpl_info->protocol_options;
/* Remove protocol preferences. */
while (NULL != list)
{
PurpleAccountOption *option = (PurpleAccountOption *) list->data;
if (g_str_has_prefix(purple_account_option_get_setting(option), PLUGIN_ID "_"))
{
GList *llist = list;
/* Remove this element from the list. */
if (llist->prev)
llist->prev->next = llist->next;
if (llist->next)
llist->next->prev = llist->prev;
purple_account_option_destroy(option);
list = g_list_next(list);
g_list_free_1(llist);
}
else
list = g_list_next(list);
}
return TRUE;
}
static void plugin_init(PurplePlugin *plugin)
{
info.dependencies = g_list_append(info.dependencies, IRC_PLUGIN_ID);
#ifdef ENABLE_NLS
bindtextdomain(GETTEXT_PACKAGE, PP_LOCALEDIR);
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
#endif /* ENABLE_NLS */
info.name = _("IRC Helper");
info.summary = _("Handles the rough edges of the IRC protocol.");
info.description = _("- Transparent authentication with a variety of "
"services.\n- Suppression of various useless messages");
}
PURPLE_INIT_PLUGIN(PLUGIN_STATIC_NAME, plugin_init, info)