grim/purple-spasm

Basic chat support is working

2020-04-19, Gary Kramlich
a1e6bcaf27c3
Basic chat support is working
/*
* 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;
gint id; /* used to track ids for this service */
};
typedef struct {
gchar *name;
gint n_args;
void (*callback)(SpasmChatService *sa, const gchar *from, gchar **args);
} SpasmChatMessageHandler;
/******************************************************************************
* Helpers
*****************************************************************************/
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;
}
/******************************************************************************
* Handlers
*****************************************************************************/
static void
spasm_chat_service_handle_join(SpasmChatService *chat, const gchar *from,
gchar **args)
{
PurpleConnection *connection = NULL;
const gchar *name = args[0] + 1; /* we want to ignore the leading # */
connection = spasm_account_get_connection(chat->sa);
serv_got_joined_chat(connection, chat->id++, name);
}
static void
spasm_chat_service_handle_privmsg(SpasmChatService *chat, const gchar *from,
gchar **args)
{
PurpleAccount *account = NULL;
PurpleConversation *conversation = NULL;
gchar *nick = spasm_chat_service_nick_from_mask(from);
gint id = 0;
account = spasm_account_get_account(chat->sa);
conversation = purple_find_conversation_with_account(PURPLE_CONV_TYPE_CHAT,
args[0] + 1, account);
if(conversation != NULL) {
id = purple_conv_chat_get_id(PURPLE_CONV_CHAT(conversation));
serv_got_chat_in(spasm_account_get_connection(chat->sa), id, nick, 0,
args[1] + 1, time(NULL));
}
g_free(nick);
}
static GHashTable *
spasm_chat_service_init_handlers(void) {
static SpasmChatMessageHandler handlers[] = {
{ "001", 0, NULL }, /* ignore RPL_WELCOME */
{ "002", 0, NULL }, /* ignore RPL_YOURHOST */
{ "003", 0, NULL }, /* ignore RPL_CREATED */
{ "004", 0, NULL }, /* ignore RPL_MYINFO */
{ "353", 0, NULL }, /* ignore RPL_NAMREPLY */
{ "366", 0, NULL }, /* ignore RPL_ENDOFNAMES */
{ "372", 0, NULL }, /* ignore RPL_MOTD */
{ "375", 0, NULL }, /* ignore RPL_MOTDSTART */
{ "376", 0, NULL }, /* ignore RPL_ENDOFMOTD */
{ "JOIN", 1, spasm_chat_service_handle_join },
{ "PRIVMSG", 2, spasm_chat_service_handle_privmsg },
};
GHashTable *ret = NULL;
gint i;
ret = g_hash_table_new(g_str_hash, g_str_equal);
for(i = 0; i < G_N_ELEMENTS(handlers); i++) {
g_hash_table_insert(ret, handlers[i].name, &handlers[i]);
}
return ret;
}
/******************************************************************************
* 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);
purple_debug_info("spasm-chat", "send buffer: %s\n", buffer);
success = g_output_stream_printf(
chat->output_stream,
NULL,
cancellable,
&error,
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);
}
/******************************************************************************
* read loop
*****************************************************************************/
static void
spasm_chat_service_parse(SpasmChatService *chat,
const gchar *buffer)
{
SpasmChatMessageHandler *handler = NULL;
gchar **parts = NULL;
g_return_if_fail(buffer != NULL);
/* special case message parsing */
if(purple_str_has_prefix(buffer, "PING ")) {
purple_debug_misc("spasm-chat", "PING? PONG!\n");
spasm_chat_service_real_send(chat, "PONG %s\r\n", buffer + 5);
return;
}
/* handle basic message parsing */
if(buffer[0] != ':') {
purple_debug_warning("spasm", "failed to parse message \"%s\"\n",
buffer);
return;
}
parts = g_strsplit(buffer, " ", 3);
/* look up the command handler */
handler = g_hash_table_lookup(chat->handlers, parts[1]);
if(handler == NULL) {
purple_debug_misc("spasm-chat", "unknown message type %s\n", parts[1]);
purple_debug_misc("spasm-chat", "from: %s\n", parts[0]);
purple_debug_misc("spasm-chat", "command: %s\n", parts[1]);
purple_debug_misc("spasm-chat", "args: %s\n", parts[2]);
purple_debug_misc("spasm-chat", "-----\n");
} else {
if(handler->callback != NULL) {
gchar **args = g_strsplit(g_strstrip(parts[2]), " ",
handler->n_args);
gchar *from = parts[0];
if(from && *from == ':') {
from++; /* increment past the : */
}
handler->callback(chat, from, args);
g_strfreev(args);
}
}
g_strfreev(parts);
}
static void spasm_chat_read(SpasmChatService *chat);
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;
}
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));
/* now do the login */
spasm_chat_service_real_send(
chat,
"PASS oauth:%s\r\n",
spasm_account_get_access_token(chat->sa)
);
/* now try to use our nick */
spasm_chat_service_real_send(
chat,
"NICK %s\r\n",
spasm_account_get_name(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();
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_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;
SpasmChatService *chat = NULL;
const gchar *channel = NULL;
channel = g_hash_table_lookup(components, "channel");
sa = purple_connection_get_protocol_data(connection);
chat = spasm_account_get_chat_service(sa);
spasm_chat_service_real_send(chat, "JOIN #%s\r\n", channel);
}
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) {
}
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);
spasm_chat_service_real_send(chat, "PRIVMSG #%s :%s\r\n",
purple_conversation_get_name(conversation),
message);
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)
{
}