grim/purple-spasm

First stab at emote support. It kind of works...
draft emotes
2020-06-18, Gary Kramlich
1eb66b376722
First stab at emote support. It kind of works...
/*
* Spasm - A Twitch Protocol Plugin
* Copyright (C) 2017-2019 Gary Kramlich <grim@reaperworld.com>
*
* 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 02110-1301, USA.
*/
#include "spasm-chat.h"
#include "spasm-const.h"
#include <glib/gi18n-lib.h>
#include <stdarg.h>
#include <purple.h>
/******************************************************************************
* Structs
*****************************************************************************/
struct _SpasmChatService {
SpasmAccount *sa;
GSocketClient *socket_client;
GSocketConnection *socket_connection;
GOutputStream *output_stream;
GDataInputStream *input_stream;
GHashTable *handlers;
GRegex *regex_message;
GRegex *regex_target;
GRegex *regex_names_reply;
GRegex *regex_tags;
GRegex *regex_emotes;
GRegex *regex_emote_ranges;
/* This table keeps track of which emotes we are currently requesting. */
GHashTable *emote_lookups;
};
typedef struct {
SpasmChatService *chat;
PurpleConversation *conversation;
gchar *text;
} SpasmChatEmoteFetchData;
typedef void (*SpasmChatMessageHandler)(SpasmChatService *sa,
const gchar *tags,
const gchar *prefix,
const gchar *command,
const gchar *middle,
const gchar *trailing);
/******************************************************************************
* Prototypes
*****************************************************************************/
static void spasm_chat_read(SpasmChatService *chat);
/******************************************************************************
* Helpers
*****************************************************************************/
static void
spasm_chat_fetch_emote_cb(GObject *source, GAsyncResult *result, gpointer d) {
SpasmChatEmoteFetchData *data = (SpasmChatEmoteFetchData *)d;
GError *error = NULL;
GFileInputStream *istream = NULL;
istream = g_file_read_finish(G_FILE(source), result, &error);
if(G_IS_FILE_INPUT_STREAM(istream)) {
PurpleSmiley *smiley = NULL;
smiley = purple_smileys_find_by_shortcut(data->text);
if(smiley == NULL) {
GFile *file = NULL;
GFileIOStream *iostream = NULL;
GOutputStream *ostream = NULL;
GError *error = NULL;
gchar *path = NULL;
gboolean added = FALSE;
file = g_file_new_tmp("spasm-emote-XXXXXX.png", &iostream, &error);
if(file == NULL) {
purple_debug_misc("spasm-chat", "emote_fetch_cb: %s",
(error) ? error->message : "unknown error");
} else {
ostream = g_io_stream_get_output_stream(G_IO_STREAM(iostream));
/* Copy the data into a temporary file. */
g_output_stream_splice(ostream, G_INPUT_STREAM(istream),
G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE |
G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
NULL, &error);
path = g_file_get_path(file);
smiley = purple_smiley_new_from_file(data->text, path);
g_free(path);
g_file_delete(file, NULL, NULL);
g_object_unref(G_OBJECT(file));
/* now write the smiley to the conversation */
added = purple_conv_custom_smiley_add(data->conversation,
data->text, NULL, NULL,
FALSE);
if(added) {
gconstpointer smiley_data = NULL;
size_t smiley_len = 0;
smiley_data = purple_smiley_get_data(smiley, &smiley_len);
purple_conv_custom_smiley_write(data->conversation,
data->text, smiley_data,
smiley_len);
purple_conv_custom_smiley_close(data->conversation,
data->text);
}
}
}
g_object_unref(G_OBJECT(istream));
} else {
purple_debug_misc("spasm-chat", "failed to fetch emote %s: %s\n",
data->text,
error ? error->message : "unknown error");
}
g_hash_table_remove(data->chat->emote_lookups, data->text);
g_clear_error(&error);
g_free(data->text);
g_free(data);
}
static void
spasm_chat_service_extract_emote(SpasmChatService *chat,
PurpleConversation *conversation,
const gchar *msg, const gchar *id,
const gchar *ranges)
{
PurpleSmiley *smiley = NULL;
GMatchInfo *info = NULL;
gchar *s_start = NULL, *s_end = NULL, *text = NULL;
gulong start = 0, end = 0, len = 0;
const guchar *data = NULL;
gsize data_len = 0;
if(!g_regex_match(chat->regex_emote_ranges, ranges, 0, &info)) {
purple_debug_misc("spasm-chat",
"failed to match emote range for %s:%s\n",
id, ranges);
g_match_info_unref(info);
return;
}
s_start = g_match_info_fetch_named(info, "start");
s_end = g_match_info_fetch_named(info, "end");
start = atol(s_start);
end = atol(s_end) + 1;
g_match_info_unref(info);
g_free(s_start);
g_free(s_end);
len = g_utf8_strlen(msg, -1);
if(start >= end || end <= start || start < 0 || end > len) {
purple_debug_misc("spasm-chat", "invalid range %s\n", ranges);
return;
}
text = g_utf8_substring(msg, start, end);
smiley = purple_smileys_find_by_shortcut(text);
/* Check if we already have the smiley. */
if(smiley == NULL) {
/* Check if we're already looking up the smiley. */
if(!g_hash_table_contains(chat->emote_lookups, text)) {
SpasmChatEmoteFetchData *data = NULL;
GFile *remote_file = NULL;
gchar *uri = NULL;
g_hash_table_insert(chat->emote_lookups, g_strdup(text), NULL);
/* now setup the async download of the actual data */
uri = g_strdup_printf(SPASM_EMOTE_URI_FORMAT, id);
remote_file = g_file_new_for_uri(uri);
g_free(uri);
/* the ready function will free data and text */
data = g_new(SpasmChatEmoteFetchData, 1);
data->chat = chat;
data->conversation = conversation;
data->text = g_strdup(text);
g_file_read_async(remote_file, G_PRIORITY_DEFAULT, NULL,
spasm_chat_fetch_emote_cb, data);
}
}
g_free(text);
}
static void
spasm_chat_service_extract_emotes(SpasmChatService *chat,
PurpleConversation *conversation,
const gchar *msg, const gchar *emotes)
{
GMatchInfo *info = NULL;
if(emotes == NULL) {
return;
}
if(!g_regex_match(chat->regex_emotes, emotes, 0, &info)) {
g_match_info_unref(info);
return;
}
while(g_match_info_matches(info)) {
GError *error = NULL;
gchar *id = NULL, *ranges = NULL;
id = g_match_info_fetch_named(info, "id");
ranges = g_match_info_fetch_named(info, "ranges");
spasm_chat_service_extract_emote(chat, conversation, msg, id, ranges);
g_free(id);
g_free(ranges);
g_match_info_next(info, &error);
if(error != NULL) {
purple_debug_misc("spasm-chat", "failed to parse emotes: %s\n",
error ? error->message: "unknown error");
g_error_free(error);
break;
}
}
g_match_info_unref(info);
}
static GHashTable *
spasm_chat_service_parse_tags(SpasmChatService *chat, const gchar *tags) {
GError *error = NULL;
GHashTable *ret = NULL;
GMatchInfo *info = NULL;
ret = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
if(!g_regex_match_full(chat->regex_tags, tags, -1, 0, 0, &info, &error)) {
purple_debug_misc("spasm-chat", "tag regex failed to match: %s\n",
error ? error->message : "unknown error");
g_clear_error(&error);
g_match_info_unref(info);
return ret;
}
while(g_match_info_matches(info)) {
gchar *key = NULL, *value = NULL;
key = g_match_info_fetch_named(info, "key");
value = g_match_info_fetch_named(info, "value");
/* the hash table is created with destroy notifies for both key and
* value, so there's no need to free the allocated memory right now.
*/
g_hash_table_insert(ret, key, value);
g_match_info_next(info, &error);
if(error != NULL) {
purple_debug_misc("spasm-chat", "tag regex failed to match: %s\n",
error ? error->message : "unknown error");
break;
}
}
g_clear_error(&error);
g_match_info_unref(info);
return ret;
}
static void
spasm_chat_service_regex_init(SpasmChatService *chat) {
chat->regex_message = g_regex_new("(?:@(?<tags>[^ ]+) )?"
"(?::(?<prefix>[^ ]+) +)?"
"(?<command>[^ :]+)"
"(?: (?<middle>(?:[^ :]+(?: [^ :]+)*)))*"
"(?: +:(?<trailing>.*)?)?",
0, 0, NULL);
g_assert(chat->regex_message != NULL);
chat->regex_target = g_regex_new("(?:#(?<target>[^\\s]+))", 0, 0, NULL);
g_assert(chat->regex_target != NULL);
chat->regex_names_reply = g_regex_new("(?:[^ ]+) (?<scope>[=*@]) "
"#(?<target>[^ ]+)",
0, 0, NULL);
g_assert(chat->regex_names_reply != NULL);
chat->regex_tags = g_regex_new("(?:(?<key>[^=]+)=(?<value>[^;]+)?);?",
0, 0, NULL);
g_assert(chat->regex_tags != NULL);
chat->regex_emotes = g_regex_new("(?:(?<id>[^:/]+)):(?<ranges>[^/]+)/?",
0, 0, NULL);
g_assert(chat->regex_emotes != NULL);
chat->regex_emote_ranges = g_regex_new("(?<start>[^-,]+)-(?<end>[^,]+),?",
0, 0, NULL);
g_assert(chat->regex_emote_ranges != NULL);
}
static gchar *
spasm_chat_service_nick_from_mask(const gchar *mask) {
gchar *nick = NULL, *bang = NULL;
bang = strchr(mask, '!');
if(bang == NULL) {
nick = g_strdup(mask);
} else {
nick = g_strndup(mask, bang - mask); /* eww pointer math... */
}
return nick;
}
/******************************************************************************
* Sending
*****************************************************************************/
static void
spasm_chat_service_real_send(SpasmChatService *chat,
const gchar *format, ...)
{
GCancellable *cancellable = NULL;
GError *error = NULL;
gchar *buffer = NULL;
gboolean success;
va_list vargs;
cancellable = spasm_account_get_cancellable(chat->sa);
va_start(vargs, format);
buffer = g_strdup_vprintf(format, vargs);
va_end(vargs);
if(g_str_has_prefix(buffer, "PASS")) {
purple_debug_info("spasm-chat", "send buffer: 'PASS <redacted>'\n");
} else {
purple_debug_info("spasm-chat", "send buffer: '%s'\n", buffer);
}
success = g_output_stream_printf(
chat->output_stream,
NULL,
cancellable,
&error,
"%s\r\n",
buffer
);
g_free(buffer);
if(!success) {
PurpleConnection *purple_connection = spasm_account_get_connection(chat->sa);
if(error) {
purple_connection_error(purple_connection, error->message);
g_error_free(error);
} else {
purple_connection_error(purple_connection, _("unknown error"));
}
return;
}
g_output_stream_flush(chat->output_stream, NULL, NULL);
}
/******************************************************************************
* Handlers
*****************************************************************************/
static void
spasm_chat_service_handle_ping(SpasmChatService *chat, const gchar *tags,
const gchar *prefix, const gchar *command,
const gchar *middle, const gchar *trailing)
{
spasm_chat_service_real_send(chat, "PONG :%s", trailing);
}
static void
spasm_chat_service_handle_join(SpasmChatService *chat, const gchar *tags,
const gchar *prefix, const gchar *command,
const gchar *middle, const gchar *trailing)
{
PurpleAccount *account = spasm_account_get_account(chat->sa);
GMatchInfo *info = NULL;
gchar *nick= NULL, *target = NULL;
if(!g_regex_match(chat->regex_target, middle, 0, &info)) {
purple_debug_misc("spasm-chat", "JOIN failed to find a target");
g_match_info_unref(info);
return;
}
target = g_match_info_fetch_named(info, "target");
g_match_info_unref(info);
nick = spasm_chat_service_nick_from_mask(prefix);
if(purple_utf8_strcasecmp(nick, purple_account_get_username(account)) == 0) {
PurpleConnection *connection = spasm_account_get_connection(chat->sa);
gchar *lower = g_utf8_casefold(target, -1);
/* We initiated the join. */
serv_got_joined_chat(connection, g_str_hash(lower), lower);
g_free(nick);
g_free(target);
g_free(lower);
return;
} else {
/* Someone else is joining a channel we're in. */
PurpleConvChat *chat = NULL;
PurpleConversation *conversation = NULL;
conversation = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
target, account);
if(conversation == NULL) {
purple_debug_misc("spasm-chat",
"ignoring JOIN for non-existant channel %s\n",
target);
g_free(nick);
g_free(target);
return;
}
chat = PURPLE_CONV_CHAT(conversation);
purple_conv_chat_add_user(chat, nick, NULL, PURPLE_CBFLAGS_NONE, TRUE);
g_free(nick);
g_free(target);
}
}
static void
spasm_chat_service_handle_names(SpasmChatService *chat, const gchar *tags,
const gchar *prefix, const gchar *command,
const gchar *middle, const gchar *trailing)
{
PurpleAccount *account = NULL;
PurpleConvChat *chat_conversation = NULL;
PurpleConversation *conversation = NULL;
GMatchInfo *info = NULL;
gchar **names = NULL;
gchar *target = NULL;
gint i = 0;
if(!g_regex_match(chat->regex_names_reply, middle, 0, &info)) {
purple_debug_misc("spasm-chat", "failed to parse names reply '%s'\n",
middle);
g_match_info_unref(info);
return;
}
target = g_match_info_fetch_named(info, "target");
g_match_info_unref(info);
account = spasm_account_get_account(chat->sa);
conversation = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
target, account);
if(conversation == NULL) {
purple_debug_misc("spasm-chat",
"Ignoring 353 for non-existent channel %s\n",
target);
g_free(target);
return;
}
g_free(target);
chat_conversation = PURPLE_CONV_CHAT(conversation);
names = g_strsplit(trailing, " ", -1);
for(i = 0; names[i] != NULL; i++) {
purple_conv_chat_add_user(chat_conversation, names[i], NULL,
PURPLE_CBFLAGS_NONE, TRUE);
}
g_strfreev(names);
}
static void
spasm_chat_service_handle_part(SpasmChatService *chat, const gchar *tags,
const gchar *prefix, const gchar *command,
const gchar *middle, const gchar *trailing)
{
PurpleAccount *account = spasm_account_get_account(chat->sa);
PurpleConnection *connection = NULL;
PurpleConvChat *chat_conversation = NULL;
PurpleConversation *conversation = NULL;
GMatchInfo *info = NULL;
gchar *nick= NULL, *target = NULL;
if(!g_regex_match(chat->regex_target, middle, 0, &info)) {
purple_debug_misc("spasm-chat", "PART failed to find a target");
g_match_info_unref(info);
return;
}
target = g_match_info_fetch_named(info, "target");
g_match_info_unref(info);
connection = spasm_account_get_connection(chat->sa);
nick = spasm_chat_service_nick_from_mask(prefix);
conversation = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
target, account);
if(conversation == NULL) {
purple_debug_misc("spasm-chat",
"Ignoring PART for non-existent channel %s\n",
target);
g_free(nick);
g_free(target);
return;
}
g_free(target);
chat_conversation = PURPLE_CONV_CHAT(conversation);
if(purple_utf8_strcasecmp(nick, purple_account_get_username(account)) == 0) {
/* We initiated the part. */
serv_got_chat_left(connection,
purple_conv_chat_get_id(chat_conversation));
} else {
/* Someone else is parting a channel we're in. */
purple_conv_chat_remove_user(chat_conversation, nick, NULL);
}
g_free(nick);
}
static void
spasm_chat_service_handle_privmsg(SpasmChatService *chat, const gchar *tags,
const gchar *prefix, const gchar *command,
const gchar *middle, const gchar *trailing)
{
GMatchInfo *info = NULL;
if(g_regex_match(chat->regex_target, middle, 0, &info)) {
PurpleAccount *account = NULL;
PurpleConversation *conversation = NULL;
PurpleMessageFlags flags = PURPLE_MESSAGE_RECV;
gchar *nick = NULL, *target = NULL;
target = g_match_info_fetch_named(info, "target");
nick = spasm_chat_service_nick_from_mask(prefix);
account = spasm_account_get_account(chat->sa);
conversation =
purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
target, account);
if(purple_strequal(command, "NOTICE") || purple_strequal(command, "USERNOTICE")) {
flags |= PURPLE_MESSAGE_NOTIFY | PURPLE_MESSAGE_SYSTEM;
}
if(conversation != NULL) {
GHashTable *tags_table = spasm_chat_service_parse_tags(chat, tags);
PurpleMessageFlags flags = PURPLE_MESSAGE_RECV;
gchar *emotes = NULL;
emotes = g_hash_table_lookup(tags_table, "emotes");
spasm_chat_service_extract_emotes(chat, conversation, trailing,
emotes);
gint id = purple_conv_chat_get_id(PURPLE_CONV_CHAT(conversation));
serv_got_chat_in(spasm_account_get_connection(chat->sa), id,
nick, flags, trailing, time(NULL));
g_hash_table_unref(tags_table);
}
g_free(target);
g_free(nick);
} else {
purple_debug_misc("spasm-chat",
"failed to to find a target in \"%s\"\n", middle);
}
g_match_info_unref(info);
}
static void
spasm_chat_service_handle_cap(SpasmChatService *chat, const gchar *tags,
const gchar *prefix, const gchar *command,
const gchar *middle, const gchar *trailing)
{
if(g_ascii_strcasecmp(middle, "ACK") == 0) {
spasm_chat_service_real_send(chat, "CAP END");
}
}
static GHashTable *
spasm_chat_service_init_handlers(void) {
GHashTable *handlers = NULL;
handlers = g_hash_table_new(g_str_hash, g_str_equal);
g_hash_table_insert(handlers, "001", NULL); /* Ignore RPL_WELCOME */
g_hash_table_insert(handlers, "002", NULL); /* Ignore RPL_YOURHOST */
g_hash_table_insert(handlers, "003", NULL); /* Ignore RPL_CREATED */
g_hash_table_insert(handlers, "004", NULL); /* Ignore RPL_MYINFO */
g_hash_table_insert(handlers, "353", spasm_chat_service_handle_names);
g_hash_table_insert(handlers, "366", NULL); /* Ignore RPL_ENDOFNAMES */
g_hash_table_insert(handlers, "372", NULL); /* Ignore RPL_MOTD */
g_hash_table_insert(handlers, "375", NULL); /* Ignore RPL_MOTDSTART */
g_hash_table_insert(handlers, "376", NULL); /* Ignore RPL_ENDOFMOTD */
g_hash_table_insert(handlers, "CAP", spasm_chat_service_handle_cap);
g_hash_table_insert(handlers, "JOIN", spasm_chat_service_handle_join);
g_hash_table_insert(handlers, "NOTICE", spasm_chat_service_handle_privmsg);
g_hash_table_insert(handlers, "PART", spasm_chat_service_handle_part);
g_hash_table_insert(handlers, "PING", spasm_chat_service_handle_ping);
g_hash_table_insert(handlers, "PRIVMSG",
spasm_chat_service_handle_privmsg);
g_hash_table_insert(handlers, "USERNOTICE",
spasm_chat_service_handle_privmsg);
return handlers;
}
/******************************************************************************
* Read Loop
*****************************************************************************/
static void
spasm_chat_service_parse(SpasmChatService *chat,
const gchar *buffer)
{
GMatchInfo *info = NULL;
gchar *command = NULL, *middle = NULL, *prefix = NULL, *tags = NULL;
gchar *trailing = NULL;
gboolean matches = FALSE;
gpointer value;
g_return_if_fail(buffer != NULL);
matches = g_regex_match(chat->regex_message, buffer, 0, &info);
if(!matches) {
purple_debug_misc("spasm-chat", "failed to parse \"%s\"\n", buffer);
g_match_info_unref(info);
return;
}
tags = g_match_info_fetch_named(info, "tags");
prefix = g_match_info_fetch_named(info, "prefix");
command = g_match_info_fetch_named(info, "command");
middle = g_match_info_fetch_named(info, "middle");
trailing = g_match_info_fetch_named(info, "trailing");
g_match_info_unref(info);
/* Look up the command handler. */
if(g_hash_table_lookup_extended(chat->handlers, command, NULL, &value)) {
/* Ignored replies have a NULL handler. */
if(value != NULL) {
SpasmChatMessageHandler handler = (SpasmChatMessageHandler)value;
handler(chat, tags, prefix, command, middle, trailing);
}
} else {
purple_debug_misc("spasm-chat", "no handler found for \"%s\"\n",
buffer);
purple_debug_misc("spasm-chat", "tags: \"%s\"\n", tags);
purple_debug_misc("spasm-chat", "prefix: \"%s\"\n", prefix);
purple_debug_misc("spasm-chat", "command: \"%s\"\n", command);
purple_debug_misc("spasm-chat", "middle: \"%s\"\n", middle);
purple_debug_misc("spasm-chat", "trailing: \"%s\"\n", trailing);
purple_debug_misc("spasm-chat", "----\n");
}
g_free(tags);
g_free(prefix);
g_free(command);
g_free(middle);
g_free(trailing);
}
static void
spasm_chat_read_cb(GObject *obj, GAsyncResult *res, gpointer data) {
GError *error = NULL;
gchar *buffer = NULL;
gsize buffer_len;
SpasmChatService *chat = (SpasmChatService *)data;
buffer = g_data_input_stream_read_line_finish(
G_DATA_INPUT_STREAM(obj),
res,
&buffer_len,
&error
);
/* g_data_input_stream_read_line_finish, will return null with error set
* on connection error. It will also return null with error not set if
* there is no content to read.
*/
if(buffer == NULL) {
gchar *error_msg = NULL;
if(error != NULL) {
error_msg = g_strdup_printf(
"spasm lost connection with server : %s",
error->message
);
g_error_free(error);
} else {
error_msg = g_strdup_printf("spasm server closed connection");
}
purple_connection_error(spasm_account_get_connection(chat->sa), error_msg);
g_free(error_msg);
return;
}
if(purple_account_is_disconnected(spasm_account_get_account(chat->sa))) {
g_free(buffer);
return;
}
spasm_chat_service_parse(chat, buffer);
g_free(buffer);
spasm_chat_read(chat);
}
static void
spasm_chat_read(SpasmChatService *chat) {
g_data_input_stream_read_line_async(
chat->input_stream,
G_PRIORITY_DEFAULT,
spasm_account_get_cancellable(chat->sa),
spasm_chat_read_cb,
chat
);
}
/******************************************************************************
* chat login flow
*****************************************************************************/
static void
spasm_chat_login_cb(GObject *obj, GAsyncResult *res, gpointer data) {
GError *error = NULL;
PurpleConnection *purple_connection = NULL;
SpasmChatService *chat = (SpasmChatService *)data;
purple_connection = spasm_account_get_connection(chat->sa);
chat->socket_connection = g_socket_client_connect_to_host_finish(
G_SOCKET_CLIENT(obj),
res,
&error
);
if(chat->socket_connection == NULL) {
if(error) {
g_prefix_error(&error, "failed to connect: ");
purple_connection_error(purple_connection, error->message);
g_error_free(error);
} else {
purple_connection_error(purple_connection, _("unknown error"));
}
return;
}
chat->output_stream = g_io_stream_get_output_stream(G_IO_STREAM(chat->socket_connection));
/* first check that we have the right capabilities */
spasm_chat_service_real_send(chat, "CAP REQ :twitch.tv/commands twitch.tv/membership twitch.tv/tags");
/* now do the login */
spasm_chat_service_real_send(
chat,
"PASS oauth:%s",
spasm_account_get_access_token(chat->sa)
);
/* now try to use our nick */
spasm_chat_service_real_send(
chat,
"NICK %s",
spasm_account_get_login(chat->sa)
);
purple_connection_set_state(purple_connection, PURPLE_CONNECTED);
chat->input_stream = g_data_input_stream_new(
g_io_stream_get_input_stream(G_IO_STREAM(chat->socket_connection))
);
spasm_chat_read(chat);
}
SpasmChatService *
spasm_chat_service_new(SpasmAccount *sa) {
SpasmChatService *chat = NULL;
g_return_val_if_fail(sa, NULL);
chat = g_slice_new0(SpasmChatService);
chat->sa = sa;
chat->handlers = spasm_chat_service_init_handlers();
spasm_chat_service_regex_init(chat);
chat->emote_lookups = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
NULL);
return chat;
}
void
spasm_chat_service_free(SpasmChatService *chat) {
g_object_unref(chat->input_stream);
g_object_unref(chat->output_stream);
g_object_unref(chat->socket_client);
g_object_unref(chat->socket_connection);
g_regex_unref(chat->regex_message);
g_regex_unref(chat->regex_target);
g_regex_unref(chat->regex_names_reply);
g_regex_unref(chat->regex_tags);
g_regex_unref(chat->regex_emotes);
g_regex_unref(chat->regex_emote_ranges);
g_hash_table_destroy(chat->emote_lookups);
g_slice_free(SpasmChatService, chat);
}
void
spasm_chat_service_connect(SpasmChatService *chat) {
g_return_if_fail(chat);
chat->socket_client = g_socket_client_new();
g_socket_client_set_tls(chat->socket_client, TRUE);
g_socket_client_connect_to_host_async(
chat->socket_client,
SPASM_CHAT_HOSTNAME,
SPASM_CHAT_PORT,
spasm_account_get_cancellable(chat->sa),
spasm_chat_login_cb,
chat
);
}
GList *
spasm_chat_service_info(PurpleConnection *connection) {
GList *entries = NULL;
struct proto_chat_entry *pce = NULL;
pce = g_new0(struct proto_chat_entry, 1);
pce->label = _("Channel");
pce->identifier = "channel";
entries = g_list_append(entries, pce);
return entries;
}
GHashTable *
spasm_chat_service_info_default(PurpleConnection *connection,
const gchar *name)
{
GHashTable *defaults = g_hash_table_new_full(
g_str_hash,
g_str_equal,
NULL,
g_free
);
if(name != NULL) {
g_hash_table_insert(defaults, "channel", g_strdup(name));
}
return defaults;
}
void
spasm_chat_service_join(PurpleConnection *connection,
GHashTable *components)
{
SpasmAccount *sa = NULL;
PurpleConversation *conv = NULL;
gchar *lower = NULL;
const gchar *channel = NULL;
channel = g_hash_table_lookup(components, "channel");
lower = g_utf8_casefold(channel, -1);
sa = purple_connection_get_protocol_data(connection);
conv = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT, lower,
spasm_account_get_account(sa));
if(conv == NULL) {
SpasmChatService *chat = NULL;
chat = spasm_account_get_chat_service(sa);
spasm_chat_service_real_send(chat, "JOIN #%s", lower);
}
g_free(lower);
}
gchar *
spasm_chat_service_name(GHashTable *components) {
return g_strdup(g_hash_table_lookup(components, "channel"));
}
void
spasm_chat_service_leave(PurpleConnection *connection, gint id) {
PurpleConversation *conversation = NULL;
const gchar *channel = NULL;
conversation = purple_find_chat(connection, id);
if(conversation != NULL) {
SpasmAccount *sa = purple_connection_get_protocol_data(connection);
SpasmChatService *chat = spasm_account_get_chat_service(sa);
channel = purple_conversation_get_name(conversation);
spasm_chat_service_real_send(chat, "PART #%s", channel);
}
}
gint
spasm_chat_service_send(PurpleConnection *connection,
gint id,
const gchar *message,
PurpleMessageFlags flags)
{
SpasmAccount *sa = NULL;
SpasmChatService *chat = NULL;
PurpleConversation *conversation = purple_find_chat(connection, id);
if(conversation == NULL) {
return -1;
}
sa = purple_connection_get_protocol_data(connection);
chat = spasm_account_get_chat_service(sa);
char *stripped = purple_markup_strip_html(message);
spasm_chat_service_real_send(chat, "PRIVMSG #%s :%s",
purple_conversation_get_name(conversation),
stripped);
g_free(stripped);
serv_got_chat_in(connection, id, spasm_account_get_display_name(sa), flags,
message, time(NULL));
return 0;
}
void
spasm_chat_service_set_topic(PurpleConnection *connection,
gint id,
const gchar *topic)
{
}
void
spasm_chat_service_send_raw(SpasmChatService *chat, const gchar *raw) {
g_return_if_fail(chat != NULL);
spasm_chat_service_real_send(chat, "%s", raw);
}