From ba620e66aa95ba7617268033f853ffbca046ef45 Mon Sep 17 00:00:00 2001 From: Patrick Cleavelin Date: Tue, 23 Apr 2024 22:35:25 -0500 Subject: [PATCH] cache slack team users + lazy loading of channel messages --- shaders/text_atlas_vertex.glsl | 3 +- src/gfx.h | 15 +++- src/ht.h | 28 ++++--- src/main.c | 78 +++++++++++++++--- src/slack_api.h | 144 ++++++++++++++++++++++++++++++--- src/ui.h | 12 +-- 6 files changed, 239 insertions(+), 41 deletions(-) diff --git a/shaders/text_atlas_vertex.glsl b/shaders/text_atlas_vertex.glsl index c404622..f062f29 100644 --- a/shaders/text_atlas_vertex.glsl +++ b/shaders/text_atlas_vertex.glsl @@ -28,6 +28,7 @@ layout(std430, binding = 1) readonly buffer GlyphBlock { layout(std430, binding = 2) readonly buffer ParamsBlock { vec2 screen_size; + vec2 font_size; }; vec4 to_device_position(vec2 position, vec2 size) { @@ -41,7 +42,7 @@ void main() { vec2 scaled_size = ((vertices[gl_VertexID].position + 1.0) / 2.0) * (glyph.size/2.0); vec2 scaled_size_2 = ((vertices[gl_VertexID].position + 1.0) / 2.0) * (glyph.size); - vec2 glyph_pos = scaled_size + glyph.target_position + vec2(0, glyph.y_offset/2.0+24); + vec2 glyph_pos = scaled_size + glyph.target_position + vec2(0, glyph.y_offset/2.0+font_size.y); vec4 device_position = to_device_position(glyph_pos, screen_size); vec2 atlas_position = (scaled_size_2 + glyph.atlas_position) / 512.0; diff --git a/src/gfx.h b/src/gfx.h index df2938a..5f392c8 100644 --- a/src/gfx.h +++ b/src/gfx.h @@ -52,6 +52,7 @@ arrayTemplate(GpuGlyph); typedef struct { float screen_size[2]; + float font_size[2]; } GpuUniformParams; #if defined(__APPLE__) @@ -495,6 +496,11 @@ static void _metal_gfx_present(_metal_gfx_context *cx) { (float)_gfx_context.frame_width, (float)_gfx_context.frame_height, }, + .font_size = + { + (float)_FONT_WIDTH, + (float)_FONT_HEIGHT, + }, }; gfx_update_buffer(&_gfx_context, 3, &gpu_uniform_params, @@ -641,12 +647,12 @@ static void _metal_gfx_update_buffer(_metal_gfx_context *cx, static void _wayland_pointer_enter(void *data, struct wl_pointer *pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y) { - fprintf(stderr, "pointer enter: %d, %d\n", x, y); + // fprintf(stderr, "pointer enter: %d, %d\n", x, y); } static void _wayland_pointer_leave(void *data, struct wl_pointer *pointer, uint32_t serial, struct wl_surface *surface) { - fprintf(stderr, "pointer leave\n"); + // fprintf(stderr, "pointer leave\n"); } static void _wayland_pointer_button(void *data, struct wl_pointer *pointer, uint32_t serial, uint32_t time, @@ -1046,6 +1052,11 @@ static void _opengl_gfx_present_wayland(_opengl_gfx_context_wayland *cx) { (float)_gfx_context.frame_width, (float)_gfx_context.frame_height, }, + .font_size = + { + (float)_FONT_WIDTH, + (float)_FONT_HEIGHT, + }, }; gfx_update_buffer(&_gfx_context, 3, &gpu_uniform_params, diff --git a/src/ht.h b/src/ht.h index 70efa4a..fd7d20a 100644 --- a/src/ht.h +++ b/src/ht.h @@ -3,10 +3,10 @@ #ifndef ED_HT_INCLUDED #define ED_HT_INCLUDED -#include -#include #include +#include #include +#include #include #include "string.h" @@ -33,7 +33,7 @@ typedef uint64_t ht_hash_t; static ht_hash_t ht_hash(string key, uint64_t mod) { ht_hash_t hash = FNV_OFFSET; - for (size_t i=0; icapacity); - for (size_t i=hash; icapacity; ++i) { - if (ht->key_slots[i].key.data == NULL || string_eq(ht->key_slots[i].key, key)) { + for (size_t i = hash; i < ht->capacity; ++i) { + if (ht->key_slots[i].key.data == NULL || + string_eq(ht->key_slots[i].key, key)) { if (ht->key_slots[i].key.data != NULL) { free(ht->key_slots[i].key.data); } ht->key_slots[i].key = string_copy(key); - memcpy(ht->value_slots+i*ht->value_size, value, ht->value_size); + memcpy(ht->value_slots + i * ht->value_size, value, ht->value_size); return true; } } @@ -75,7 +77,7 @@ bool ht_set(ed_ht *ht, string key, void *value) { } void *ht_get_slot(ed_ht *ht, size_t slot) { - void *value = ht->value_slots+slot*ht->value_size; + void *value = ht->value_slots + slot * ht->value_size; if (slot >= ht->capacity || !value) return NULL; @@ -86,7 +88,7 @@ void *ht_get_slot(ed_ht *ht, size_t slot) { void *ht_get(ed_ht *ht, string key) { ht_hash_t hash = ht_hash(key, ht->capacity); - for (size_t i=hash; icapacity; ++i) { + for (size_t i = hash; i < ht->capacity; ++i) { if (ht->key_slots[i].key.data == NULL) { return NULL; } @@ -102,13 +104,15 @@ void *ht_get(ed_ht *ht, string key) { void ht_remove(ed_ht *ht, string key) { ht_hash_t hash = ht_hash(key, ht->capacity); - for (size_t i=hash; icapacity; ++i) { + for (size_t i = hash; i < ht->capacity; ++i) { if (ht->key_slots[i].key.data == NULL) { return; } if (string_eq(ht->key_slots[i].key, key)) { - ht->key_slots[i] = (ed_ht_slot) { 0 }; + free(ht->key_slots[i].key.data); + ht->key_slots[i] = (ed_ht_slot){0}; + return; } } } diff --git a/src/main.c b/src/main.c index 5ffe243..e7d0b9e 100644 --- a/src/main.c +++ b/src/main.c @@ -15,13 +15,14 @@ #define ED_BUFFER_IMPLEMENTATION #define ED_FILE_IO_IMPLEMENTATION #define CHAT_SLACK_IMPLEMENTATION +#include "ui.h" + #include "ed_array.h" #include "file_io.h" #include "gfx.h" #include "ht.h" #include "slack_api.h" #include "string.h" -#include "ui.h" // static Arena default_arena = {0}; // static Arena temporary_arena = {0}; @@ -45,14 +46,15 @@ static struct { slack_user_info slack_user_info; array(slack_channel) slack_channels; - // TODO: store these for all channels - array(slack_message) slack_messages; + ed_ht slack_messages; + ed_ht slack_users; - size_t selected_channel_idx; + // strictly a weak reference to a channel id + string selected_channel_idx; } state; void init(_gfx_frame_func frame_func) { - state.gfx_cx = gfx_init_context(frame_func, 640, 480); + state.gfx_cx = gfx_init_context(frame_func, 1280, 720); state.ui_cx = ui_init_context(); // TODO: grab default font from the system @@ -200,7 +202,19 @@ void ed_frame(int mouse_x, int mouse_y, bool mouse_left_down, for (int i = 0; i < state.slack_channels.size; ++i) { if (ui_button(&state.ui_cx, state.slack_channels.data[i].name) .clicked) { - state.selected_channel_idx = i; + state.selected_channel_idx = + state.slack_channels.data[i].id; + + // FIXME: DO NOT DO THIS ON THE UI THREAD! + if (!ht_get(&state.slack_messages, + state.selected_channel_idx)) { + + array(slack_message) messages = slack_api_message_list( + &state.slack_client, state.selected_channel_idx); + + ht_set(&state.slack_messages, + state.selected_channel_idx, &messages); + } } } @@ -245,9 +259,41 @@ void ed_frame(int mouse_x, int mouse_y, bool mouse_left_down, } ui_pop_parent(&state.ui_cx); - for (int i = 0; i < state.slack_messages.size; ++i) { - ui_button(&state.ui_cx, state.slack_messages.data[i].normal.text); + ui_element(&state.ui_cx, _String("channel contents"), UI_AXIS_VERTICAL, + ui_make_size(ui_fill, ui_fill), UI_FLAG_DRAW_BACKGROUND); + ui_push_parent(&state.ui_cx); + { + if (state.selected_channel_idx.data != NULL) { + array(slack_message) *messages = + ht_get(&state.slack_messages, state.selected_channel_idx); + + if (messages) { + uint8_t text_buf[1024] = {0}; + for (int i = messages->size - 1; i >= 0; --i) { + string message_text = messages->data[i].normal.text; + slack_user *user = ht_get( + &state.slack_users, messages->data[i].normal.user); + + if (user) { + snprintf(text_buf, 1024, "%.*s: %.*s", + (int)user->real_name.len, user->name.data, + (int)message_text.len, message_text.data); + } else { + snprintf(text_buf, 1024, "Unknown: %.*s", + (int)message_text.len, message_text.data); + } + + ui_button(&state.ui_cx, _CString_To_String(text_buf)); + } + } + } else { + ui_element(&state.ui_cx, _String("no channel selected"), + UI_AXIS_VERTICAL, ui_make_size(ui_fill, ui_fit_text), + UI_FLAG_DRAW_BACKGROUND | UI_FLAG_DRAW_TEXT | + UI_FLAG_CENTERED_TEXT); + } } + ui_pop_parent(&state.ui_cx); } ui_pop_parent(&state.ui_cx); @@ -283,9 +329,19 @@ int main(int argc, char *argv[]) { state.slack_user_info = slack_api_auth_test(&state.slack_client); state.slack_channels = slack_api_channel_list(&state.slack_client, state.slack_user_info); - state.slack_messages = slack_api_message_list( - &state.slack_client, state.slack_channels.data[0].id); - state.selected_channel_idx = 0; + state.selected_channel_idx = (string){0}; + + // state.slack_messages = slack_api_message_list( + // &state.slack_client, state.slack_channels.data[0].id); + + state.slack_messages = ht_create(1000, sizeof(array(slack_message))); + state.slack_users = ht_create(1000, sizeof(slack_user)); + // array(slack_user) users = + // slack_api_user_list(&state.slack_client, + // state.slack_user_info.team_id); + // for (int i = 0; i < users.size; ++i) { + // ht_set(&state.slack_users, users.data[i].id, &users.data[i]); + // } init(ed_frame); diff --git a/src/slack_api.h b/src/slack_api.h index c016a70..44c5555 100644 --- a/src/slack_api.h +++ b/src/slack_api.h @@ -52,11 +52,20 @@ typedef struct { } slack_message; arrayTemplate(slack_message); +// TODO: merge with `slack_user_info` +typedef struct { + string id; + string name; + string real_name; +} slack_user; +arrayTemplate(slack_user); + slack_user_info slack_api_auth_test(slack_client *client); array(slack_channel) slack_api_channel_list(slack_client *client, slack_user_info user); array(slack_message) slack_api_message_list(slack_client *client, string channel_id); +array(slack_user) slack_api_user_list(slack_client *client, string team_id); slack_client slack_init_client(); @@ -208,9 +217,10 @@ array(slack_channel) if (json == NULL) { const char *error_ptr = cJSON_GetErrorPtr(); if (error_ptr != NULL) { - fprintf(stderr, - "SLACK CLIENT: Failed to parse /auth.test: %s\n", - error_ptr); + fprintf( + stderr, + "SLACK CLIENT: Failed to parse /conversations.list: %s\n", + error_ptr); // FIXME: don't panic here exit(1); @@ -253,11 +263,13 @@ array(slack_channel) const cJSON *error = cJSON_GetObjectItemCaseSensitive(json, "error"); if (cJSON_IsString(error)) { - fprintf(stderr, "SLACK CLIENT: /auth.test failed: %s\n", + fprintf(stderr, + "SLACK CLIENT: /conversations.list failed: %s\n", error->valuestring); } else { - fprintf(stderr, "SLACK CLIENT: /auth.test failed: failed " - "to parse 'error' field\n"); + fprintf(stderr, + "SLACK CLIENT: /conversations.list failed: failed " + "to parse 'error' field\n"); } } } @@ -321,7 +333,8 @@ array(slack_message) const char *error_ptr = cJSON_GetErrorPtr(); if (error_ptr != NULL) { fprintf(stderr, - "SLACK CLIENT: Failed to parse /auth.test: %s\n", + "SLACK CLIENT: Failed to parse /conversations.history: " + "%s\n", error_ptr); // FIXME: don't panic here @@ -373,11 +386,14 @@ array(slack_message) const cJSON *error = cJSON_GetObjectItemCaseSensitive(json, "error"); if (cJSON_IsString(error)) { - fprintf(stderr, "SLACK CLIENT: /auth.test failed: %s\n", + fprintf(stderr, + "SLACK CLIENT: /conversations.history failed: %s\n", error->valuestring); } else { - fprintf(stderr, "SLACK CLIENT: /auth.test failed: failed " - "to parse 'error' field\n"); + fprintf( + stderr, + "SLACK CLIENT: /conversations.history failed: failed " + "to parse 'error' field\n"); } } } @@ -390,6 +406,114 @@ array(slack_message) return (array(slack_message)){0}; } +array(slack_user) slack_api_user_list(slack_client *client, string team_id) { + if (client->curl) { + struct curl_slist *headers = NULL; + headers = curl_slist_append( + headers, "Content-Type: application/x-www-form-urlencoded"); + headers = curl_slist_append(headers, client->cookie.data); + + curl_easy_setopt(client->curl, CURLOPT_URL, + "https://slack.com/api/users.list"); + + curl_easy_setopt(client->curl, CURLOPT_HTTPAUTH, (long)CURLAUTH_BEARER); + curl_easy_setopt(client->curl, CURLOPT_XOAUTH2_BEARER, + client->token.data); + curl_easy_setopt(client->curl, CURLOPT_USERAGENT, + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; " + "rv:76.0) Gecko/20100101 Firefox/76.0"); + curl_easy_setopt(client->curl, CURLOPT_HTTPHEADER, headers); + + curl_easy_setopt(client->curl, CURLOPT_POST, 1L); + + char *fields_buffer[2048]; + snprintf(fields_buffer, 2048, "team_id=%.*s", (int)team_id.len, + team_id.data); + fprintf(stderr, "fields_buffer: %s\n", fields_buffer); + curl_easy_setopt(client->curl, CURLOPT_POSTFIELDS, fields_buffer); + + curl_easy_setopt(client->curl, CURLOPT_FOLLOWLOCATION, 1L); + + // TODO: don't allocate this on every request + array(uint8_t) chunk = newArray(uint8_t, 4096); + + curl_easy_setopt(client->curl, CURLOPT_WRITEFUNCTION, + write_memory_callback); + curl_easy_setopt(client->curl, CURLOPT_WRITEDATA, (void *)&chunk); + + CURLcode res = curl_easy_perform(client->curl); + + if (res != CURLE_OK) { + fprintf(stderr, "curl_easy_perform() failed: %s\n", + curl_easy_strerror(res)); + } else { + fprintf(stderr, "Got data\n%.*s\n", (int)chunk.size, chunk.data); + } + + // parse json + cJSON *json = cJSON_ParseWithLength(chunk.data, chunk.size); + if (json == NULL) { + const char *error_ptr = cJSON_GetErrorPtr(); + if (error_ptr != NULL) { + fprintf(stderr, + "SLACK CLIENT: Failed to parse /users.list: %s\n", + error_ptr); + + // FIXME: don't panic here + exit(1); + } + } + { + const cJSON *ok = cJSON_GetObjectItemCaseSensitive(json, "ok"); + if (cJSON_IsBool(ok) && ok->valueint == 1) { + array(slack_user) users = newArray(slack_user, 10); + + fprintf(stderr, "SLACK CLIENT: /users.list succeeded!\n"); + + const cJSON *json_users = + cJSON_GetObjectItemCaseSensitive(json, "members"); + + const cJSON *json_user; + cJSON_ArrayForEach(json_user, json_users) { + const cJSON *user_id = + cJSON_GetObjectItemCaseSensitive(json_user, "id"); + const cJSON *name = + cJSON_GetObjectItemCaseSensitive(json_user, "name"); + const cJSON *real_name = cJSON_GetObjectItemCaseSensitive( + json_user, "real_name"); + + slack_user user = { + .id = string_copy_cstring(user_id->valuestring), + .name = string_copy_cstring(name->valuestring), + .real_name = + string_copy_cstring(real_name->valuestring), + }; + + pushArray(slack_user, &users, user); + } + + return users; + } else { + const cJSON *error = + cJSON_GetObjectItemCaseSensitive(json, "error"); + if (cJSON_IsString(error)) { + fprintf(stderr, "SLACK CLIENT: /users.list failed: %s\n", + error->valuestring); + } else { + fprintf(stderr, "SLACK CLIENT: /users.list failed: failed " + "to parse 'error' field\n"); + } + } + } + cJSON_Delete(json); + + free(chunk.data); + } + + // TODO: create some slack api error type + return (array(slack_user)){0}; +} + slack_client slack_init_client(string token, string cookie) { CURL *curl = curl_easy_init(); diff --git a/src/ui.h b/src/ui.h index 0f4b37f..3a89fcb 100644 --- a/src/ui.h +++ b/src/ui.h @@ -3,11 +3,12 @@ #ifndef ED_UI_INCLUDED #define ED_UI_INCLUDED +#include "string.h" #define MAX_UI_ELEMENTS 8192 // TODO: replace this with functions -#define _FONT_WIDTH 12 #define _FONT_HEIGHT 24 +#define _FONT_WIDTH _FONT_HEIGHT / 2 #define _elm(index) (cx->frame_elements.data + index) #define _flags(index, flgs) ((_elm(index)->flags & (flgs)) == (flgs)) @@ -195,10 +196,10 @@ size_t ui_element(ui_context *cx, string label, ui_axis axis, ui_semantic_size size[2], ui_flags flags) { ui_element_frame_data frame_data = (ui_element_frame_data){ .index = cx->frame_elements.size, - // TODO: don't just set this to label, because then elements + // TODO: don't just set `key` to label, because then elements // with the same label can't be created together - .key = label, - .label = label, + .key = string_copy(label), + .label = string_copy(label), .first = -1, .last = -1, .next = -1, @@ -223,7 +224,7 @@ size_t ui_element(ui_context *cx, string label, ui_axis axis, } else { bool did_insert = ht_set(&cx->cached_elements, label, &(ui_element_cache_data){ - .label = label, + .label = string_copy(label), .size = {0}, .last_instantiated_index = cx->frame_index, }); @@ -560,6 +561,7 @@ void ui_prune(ui_context *cx) { // %zu, frame index: %zu\n", (int)key.len, key.data, // cached->last_instantiated_index, cx->frame_index); + free(cached->label.data); ht_remove(&cx->cached_elements, key); } }