--- a/meson.build Sun Mar 13 23:27:48 2022 -0500
+++ b/meson.build Thu Jun 18 04:18:16 2020 -0500
@@ -18,7 +18,7 @@
JSON_GLIB = dependency('json-glib-1.0')
SOUP = dependency('libsoup-2.4')
-PURPLE = dependency('purple', version: '>=2.13.0')
+PURPLE = dependency('purple', version: '>=2.14.0') --- a/src/spasm-chat.c Sun Mar 13 23:27:48 2022 -0500
+++ b/src/spasm-chat.c Thu Jun 18 04:18:16 2020 -0500
@@ -42,14 +42,27 @@
GRegex *regex_names_reply;
+ GRegex *regex_emote_ranges; + /* This table keeps track of which emotes we are currently requesting. */ + GHashTable *emote_lookups; + SpasmChatService *chat; + PurpleConversation *conversation; +} SpasmChatEmoteFetchData; typedef void (*SpasmChatMessageHandler)(SpasmChatService *sa,
/******************************************************************************
*****************************************************************************/
@@ -59,12 +72,242 @@
*****************************************************************************/
+spasm_chat_fetch_emote_cb(GObject *source, GAsyncResult *result, gpointer d) { + SpasmChatEmoteFetchData *data = (SpasmChatEmoteFetchData *)d; + 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); + GFileIOStream *iostream = NULL; + GOutputStream *ostream = NULL; + gboolean added = FALSE; + file = g_file_new_tmp("spasm-emote-XXXXXX.png", &iostream, &error); + purple_debug_misc("spasm-chat", "emote_fetch_cb: %s", + (error) ? error->message : "unknown error"); + 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, + path = g_file_get_path(file); + smiley = purple_smiley_new_from_file(data->text, 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, + gconstpointer smiley_data = NULL; + smiley_data = purple_smiley_get_data(smiley, &smiley_len); + purple_conv_custom_smiley_write(data->conversation, + data->text, smiley_data, + purple_conv_custom_smiley_close(data->conversation, + g_object_unref(G_OBJECT(istream)); + purple_debug_misc("spasm-chat", "failed to fetch emote %s: %s\n", + error ? error->message : "unknown error"); + g_hash_table_remove(data->chat->emote_lookups, data->text); +spasm_chat_service_extract_emote(SpasmChatService *chat, + PurpleConversation *conversation, + const gchar *msg, const gchar *id, + 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; + 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", + g_match_info_unref(info); + s_start = g_match_info_fetch_named(info, "start"); + s_end = g_match_info_fetch_named(info, "end"); + g_match_info_unref(info); + 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); + text = g_utf8_substring(msg, start, end); + smiley = purple_smileys_find_by_shortcut(text); + /* Check if we already have the smiley. */ + /* 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; + 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); + /* the ready function will free data and text */ + data = g_new(SpasmChatEmoteFetchData, 1); + data->conversation = conversation; + data->text = g_strdup(text); + g_file_read_async(remote_file, G_PRIORITY_DEFAULT, NULL, + spasm_chat_fetch_emote_cb, data); +spasm_chat_service_extract_emotes(SpasmChatService *chat, + PurpleConversation *conversation, + const gchar *msg, const gchar *emotes) + GMatchInfo *info = NULL; + if(!g_regex_match(chat->regex_emotes, emotes, 0, &info)) { + g_match_info_unref(info); + while(g_match_info_matches(info)) { + 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_match_info_next(info, &error); + purple_debug_misc("spasm-chat", "failed to parse emotes: %s\n", + error ? error->message: "unknown error"); + g_match_info_unref(info); +spasm_chat_service_parse_tags(SpasmChatService *chat, const gchar *tags) { + 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_match_info_unref(info); + 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); + purple_debug_misc("spasm-chat", "tag regex failed to match: %s\n", + error ? error->message : "unknown error"); + g_match_info_unref(info); spasm_chat_service_regex_init(SpasmChatService *chat) {
chat->regex_message = g_regex_new("(?:@(?<tags>[^ ]+) )?"
"(?::(?<prefix>[^ ]+) +)?"
"(?: (?<middle>(?:[^ :]+(?: [^ :]+)*)))*"
- "(?<coda> +:(?<trailing>.*)?)?",
+ "(?: +:(?<trailing>.*)?)?", g_assert(chat->regex_message != NULL);
@@ -75,6 +318,18 @@
g_assert(chat->regex_names_reply != NULL);
+ chat->regex_tags = g_regex_new("(?:(?<key>[^=]+)=(?<value>[^;]+)?);?", + g_assert(chat->regex_tags != NULL); + chat->regex_emotes = g_regex_new("(?:(?<id>[^:/]+)):(?<ranges>[^/]+)/?", + g_assert(chat->regex_emotes != NULL); + chat->regex_emote_ranges = g_regex_new("(?<start>[^-,]+)-(?<end>[^,]+),?", + g_assert(chat->regex_emote_ranges != NULL); @@ -335,8 +590,6 @@
target = g_match_info_fetch_named(info, "target");
- purple_debug_misc("spasm-chat", "privmsg tags: '%s'\n", tags);
nick = spasm_chat_service_nick_from_mask(prefix);
account = spasm_account_get_account(chat->sa);
@@ -349,9 +602,20 @@
if(conversation != NULL) {
+ GHashTable *tags_table = spasm_chat_service_parse_tags(chat, tags); + PurpleMessageFlags flags = PURPLE_MESSAGE_RECV; + emotes = g_hash_table_lookup(tags_table, "emotes"); + spasm_chat_service_extract_emotes(chat, conversation, trailing, 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));
+ nick, flags, trailing, time(NULL)); + g_hash_table_unref(tags_table); @@ -371,9 +635,6 @@
if(g_ascii_strcasecmp(middle, "ACK") == 0) {
spasm_chat_service_real_send(chat, "CAP END");
- purple_debug_misc("spasm-chat", "cap-middle: '%s'\n", middle);
- purple_debug_misc("spasm-chat", "cap-trailing: '%s'\n", trailing);
@@ -596,6 +857,9 @@
spasm_chat_service_regex_init(chat);
+ chat->emote_lookups = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, @@ -608,6 +872,12 @@
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);
--- a/src/spasm-const.h Sun Mar 13 23:27:48 2022 -0500
+++ b/src/spasm-const.h Thu Jun 18 04:18:16 2020 -0500
@@ -39,4 +39,6 @@
+#define SPASM_EMOTE_URI_FORMAT "http://static-cdn.jtvnw.net/emoticons/v1/%s/1.0" #endif /* SPASM_CONSTS_H */