grim/purple-spasm

Move the parsing to regex and make real_send add the trailing \r\n
/*
* 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 */
GRegex *regex_message;
GRegex *regex_target;
};
typedef void (*SpasmChatMessageHandler)(SpasmChatService *sa,
const gchar *prefix,
const gchar *middle,
const gchar *trailing);
/******************************************************************************
* Helpers
*****************************************************************************/
static void
spasm_chat_service_regex_init(SpasmChatService *chat) {
chat->regex_message = g_regex_new("(?::(?<prefix>[^ ]+) +)?"
"(?<command>[^ :]+)"
"(?:(?: +(?<middle>[^ :]+)))*"
"(?<coda> +:(?<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);
}
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);
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 *prefix,
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 *prefix,
const gchar *middle, const gchar *trailing)
{
GMatchInfo *info = NULL;
if(g_regex_match(chat->regex_target, middle, 0, &info)) {
gchar *target = g_match_info_fetch_named(info, "target");
if(target != NULL) {
PurpleConnection *connection = NULL;
connection = spasm_account_get_connection(chat->sa);
serv_got_joined_chat(connection, chat->id++, target);
}
g_free(target);
}
g_match_info_unref(info);
}
static void
spasm_chat_service_handle_privmsg(SpasmChatService *chat, const gchar *prefix,
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;
gchar *target = NULL, *nick = 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(conversation != NULL) {
gint id = purple_conv_chat_get_id(PURPLE_CONV_CHAT(conversation));
serv_got_chat_in(spasm_account_get_connection(chat->sa), id,
nick, 0, trailing, time(NULL));
}
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 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", NULL); /* Ignore RPL_NAMREPLY */
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, "JOIN", spasm_chat_service_handle_join);
g_hash_table_insert(handlers, "PING", spasm_chat_service_handle_ping);
g_hash_table_insert(handlers, "PRIVMSG",
spasm_chat_service_handle_privmsg);
return handlers;
}
/******************************************************************************
* Read Loop
*****************************************************************************/
static void
spasm_chat_service_parse(SpasmChatService *chat,
const gchar *buffer)
{
GMatchInfo *info = NULL;
gchar *prefix = NULL, *command = NULL, *middle = NULL, *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;
}
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, prefix, middle, trailing);
}
} else {
purple_debug_misc("spasm-chat", "no handler found for \"%s\"\n",
buffer);
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(prefix);
g_free(command);
g_free(middle);
g_free(trailing);
}
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",
spasm_account_get_access_token(chat->sa)
);
/* now try to use our nick */
spasm_chat_service_real_send(
chat,
"NICK %s",
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();
spasm_chat_service_regex_init(chat);
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_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", 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",
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)
{
}