grim/purple-spasm

First stab at emote support. It kind of works...
draft default tip emotes
12 months ago, Gary Kramlich
6394c25b78bd
Parents 52ab65385767
Children
First stab at emote support. It kind of works...
--- a/meson.build Thu May 14 03:54:05 2020 -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 Thu May 14 03:54:05 2020 -0500
+++ b/src/spasm-chat.c Thu Jun 18 04:18:16 2020 -0500
@@ -43,13 +43,26 @@
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 *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 *
@@ -110,8 +365,6 @@
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,
@@ -328,8 +581,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);
@@ -338,9 +589,20 @@
target, account);
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);
@@ -360,9 +622,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);
}
}
@@ -577,6 +836,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;
}
@@ -589,6 +851,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 Thu May 14 03:54:05 2020 -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 */