grim/purple-spasm

First stab at emote support. It kind of works...
draft emotes
2020-06-18, Gary Kramlich
1eb66b376722
Parents 418d8818aa44
Children
First stab at emote support. It kind of works...
--- 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')
subdir('icons')
subdir('src')
--- 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_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
*****************************************************************************/
@@ -59,12 +72,242 @@
* 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>(?:[^ :]+(?: [^ :]+)*)))*"
- "(?<coda> +:(?<trailing>.*)?)?",
+ "(?: +:(?<trailing>.*)?)?",
0, 0, NULL);
g_assert(chat->regex_message != NULL);
@@ -75,6 +318,18 @@
"#(?<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 *
@@ -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;
+ 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, 0, trailing, time(NULL));
+ nick, flags, trailing, time(NULL));
+
+ g_hash_table_unref(tags_table);
}
g_free(target);
@@ -371,9 +635,6 @@
{
if(g_ascii_strcasecmp(middle, "ACK") == 0) {
spasm_chat_service_real_send(chat, "CAP END");
- } else {
- 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,
+ NULL);
+
return chat;
}
@@ -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 @@
"scope=%s&" \
"state=%s"
+#define SPASM_EMOTE_URI_FORMAT "http://static-cdn.jtvnw.net/emoticons/v1/%s/1.0"
+
#endif /* SPASM_CONSTS_H */