upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/main/relay_selector.c
diff options
context:
space:
mode:
Diffstat (limited to 'main/relay_selector.c')
-rw-r--r--main/relay_selector.c270
1 files changed, 270 insertions, 0 deletions
diff --git a/main/relay_selector.c b/main/relay_selector.c
new file mode 100644
index 0000000..7c443fe
--- /dev/null
+++ b/main/relay_selector.c
@@ -0,0 +1,270 @@
1#include "relay_selector.h"
2#include "config.h"
3#include "esp_log.h"
4#include "esp_http_client.h"
5#include "esp_tls.h"
6#include "esp_crt_bundle.h"
7#include "esp_timer.h"
8#include "cJSON.h"
9#include "freertos/FreeRTOS.h"
10#include "freertos/semphr.h"
11#include <string.h>
12#include <stdlib.h>
13
14static const char *TAG = "relay_sel";
15static const int MAX_REDIRECTS = 3;
16static const int PROBE_TIMEOUT_MS = 5000;
17static const int MAX_FAILURES = 3;
18
19static int compare_relays(const void *a, const void *b)
20{
21 const relay_info_t *ra = (const relay_info_t *)a;
22 const relay_info_t *rb = (const relay_info_t *)b;
23
24 if (ra->alive && !rb->alive) return -1;
25 if (!ra->alive && rb->alive) return 1;
26
27 int score_a = (ra->supports_nip77 ? 1000 : 0) - ra->consecutive_failures * 100;
28 int score_b = (rb->supports_nip77 ? 1000 : 0) - rb->consecutive_failures * 100;
29 if (score_a != score_b) return score_b - score_a;
30
31 return (int)ra->latency_ms - (int)rb->latency_ms;
32}
33
34esp_err_t relay_selector_init(relay_selector_t *sel)
35{
36 memset(sel, 0, sizeof(relay_selector_t));
37 sel->primary_idx = -1;
38 sel->fallback_idx = -1;
39 sel->lock = xSemaphoreCreateMutex();
40 if (!sel->lock) return ESP_ERR_NO_MEM;
41 return ESP_OK;
42}
43
44void relay_selector_destroy(relay_selector_t *sel)
45{
46 if (sel->lock) { vSemaphoreDelete(sel->lock); sel->lock = NULL; }
47}
48
49static esp_err_t probe_nip11(const char *wss_url, relay_info_t *info)
50{
51 char http_url[192];
52 const char *host_start = wss_url;
53 if (strncmp(wss_url, "wss://", 6) == 0) host_start = wss_url + 6;
54 else if (strncmp(wss_url, "ws://", 5) == 0) host_start = wss_url + 5;
55
56 snprintf(http_url, sizeof(http_url), "https://%s/", host_start);
57
58 char response[4096];
59 int total_len = 0;
60
61 esp_http_client_config_t http_cfg = {
62 .url = http_url,
63 .method = HTTP_METHOD_GET,
64 .timeout_ms = PROBE_TIMEOUT_MS,
65 .crt_bundle_attach = esp_crt_bundle_attach,
66 .max_redirection_count = MAX_REDIRECTS,
67 .disable_auto_redirect = false,
68 };
69
70 esp_http_client_handle_t client = esp_http_client_init(&http_cfg);
71 if (!client) return ESP_FAIL;
72
73 esp_http_client_set_header(client, "Accept", "application/nostr+json");
74
75 int64_t start_time = esp_timer_get_time();
76 esp_err_t err = esp_http_client_open(client, 0);
77 if (err != ESP_OK) {
78 esp_http_client_cleanup(client);
79 info->alive = false;
80 return err;
81 }
82
83 int content_length = esp_http_client_fetch_headers(client);
84 int status = esp_http_client_get_status_code(client);
85
86 if (status != 200) {
87 esp_http_client_close(client);
88 esp_http_client_cleanup(client);
89 info->alive = (status > 0);
90 return ESP_FAIL;
91 }
92
93 int max_read = content_length > 0 ? content_length : (int)sizeof(response) - 1;
94 if (max_read > (int)sizeof(response) - 1) max_read = (int)sizeof(response) - 1;
95
96 while (total_len < max_read) {
97 int read_len = esp_http_client_read(client, response + total_len,
98 max_read - total_len);
99 if (read_len <= 0) break;
100 total_len += read_len;
101 }
102 response[total_len] = '\0';
103
104 int64_t end_time = esp_timer_get_time();
105 info->latency_ms = (uint32_t)((end_time - start_time) / 1000);
106
107 esp_http_client_close(client);
108 esp_http_client_cleanup(client);
109
110 info->alive = true;
111 info->consecutive_failures = 0;
112
113 cJSON *root = cJSON_Parse(response);
114 if (!root) return ESP_OK;
115
116 cJSON *name = cJSON_GetObjectItem(root, "name");
117 if (name && cJSON_IsString(name))
118 strncpy(info->name, name->valuestring, sizeof(info->name) - 1);
119
120 cJSON *nips = cJSON_GetObjectItem(root, "supported_nips");
121 if (nips && cJSON_IsArray(nips)) {
122 info->nips_count = cJSON_GetArraySize(nips);
123 if (info->nips_count > 32) info->nips_count = 32;
124 info->supports_nip77 = false;
125 for (size_t i = 0; i < info->nips_count; i++) {
126 cJSON *nip = cJSON_GetArrayItem(nips, i);
127 if (nip) {
128 info->supported_nips[i] = (uint8_t)nip->valueint;
129 if (nip->valueint == 77) info->supports_nip77 = true;
130 }
131 }
132 }
133
134 cJSON_Delete(root);
135 return ESP_OK;
136}
137
138static void select_primary_fallback(relay_selector_t *sel)
139{
140 relay_info_t sorted[RELAY_SELECTOR_MAX_RELAYS];
141 size_t sorted_count = 0;
142
143 for (size_t i = 0; i < sel->count; i++) {
144 if (sel->relays[i].alive) {
145 sorted[sorted_count++] = sel->relays[i];
146 }
147 }
148
149 if (sorted_count == 0) {
150 sel->primary_idx = -1;
151 sel->fallback_idx = -1;
152 return;
153 }
154
155 qsort(sorted, sorted_count, sizeof(relay_info_t), compare_relays);
156
157 for (size_t i = 0; i < sel->count; i++) {
158 if (strcmp(sel->relays[i].url, sorted[0].url) == 0) {
159 sel->primary_idx = (int)i;
160 break;
161 }
162 }
163
164 if (sorted_count > 1) {
165 for (size_t i = 0; i < sel->count; i++) {
166 if (strcmp(sel->relays[i].url, sorted[1].url) == 0) {
167 sel->fallback_idx = (int)i;
168 break;
169 }
170 }
171 } else {
172 sel->fallback_idx = -1;
173 }
174
175 ESP_LOGI(TAG, "Primary: %s (latency=%lums, NIP-77=%s)",
176 sel->primary_idx >= 0 ? sel->relays[sel->primary_idx].url : "none",
177 sel->primary_idx >= 0 ? (unsigned long)sel->relays[sel->primary_idx].latency_ms : 0,
178 sel->primary_idx >= 0 && sel->relays[sel->primary_idx].supports_nip77 ? "yes" : "no");
179}
180
181esp_err_t relay_selector_probe_all(relay_selector_t *sel)
182{
183 xSemaphoreTake(sel->lock, portMAX_DELAY);
184
185 ESP_LOGI(TAG, "Probing %zu relays via NIP-11...", sel->count);
186
187 for (size_t i = 0; i < sel->count; i++) {
188 ESP_LOGI(TAG, "Probing %s...", sel->relays[i].url);
189 esp_err_t err = probe_nip11(sel->relays[i].url, &sel->relays[i]);
190 if (err != ESP_OK) {
191 sel->relays[i].consecutive_failures++;
192 ESP_LOGW(TAG, "Probe failed for %s (failures=%d)",
193 sel->relays[i].url, sel->relays[i].consecutive_failures);
194 if (sel->relays[i].consecutive_failures >= MAX_FAILURES) {
195 sel->relays[i].alive = false;
196 }
197 }
198 vTaskDelay(pdMS_TO_TICKS(100));
199 }
200
201 select_primary_fallback(sel);
202
203 int64_t now = (int64_t)(xTaskGetTickCount() / configTICK_RATE_HZ);
204 sel->last_full_probe = (uint32_t)now;
205
206 xSemaphoreGive(sel->lock);
207 return ESP_OK;
208}
209
210const relay_info_t *relay_selector_get_primary(relay_selector_t *sel)
211{
212 if (sel->primary_idx < 0 || sel->primary_idx >= (int)sel->count) return NULL;
213 return &sel->relays[sel->primary_idx];
214}
215
216const relay_info_t *relay_selector_get_fallback(relay_selector_t *sel, int idx)
217{
218 if (idx == 0) {
219 if (sel->fallback_idx < 0) return NULL;
220 return &sel->relays[sel->fallback_idx];
221 }
222 for (size_t i = 0; i < sel->count; i++) {
223 if ((int)i != sel->primary_idx && (int)i != sel->fallback_idx) {
224 if (sel->relays[i].alive) {
225 if (idx <= 0) return &sel->relays[i];
226 idx--;
227 }
228 }
229 }
230 return NULL;
231}
232
233void relay_selector_report_disconnect(relay_selector_t *sel, const char *url)
234{
235 xSemaphoreTake(sel->lock, portMAX_DELAY);
236 for (size_t i = 0; i < sel->count; i++) {
237 if (strcmp(sel->relays[i].url, url) == 0) {
238 sel->relays[i].consecutive_failures++;
239 ESP_LOGW(TAG, "Disconnect reported for %s (failures=%d)",
240 url, sel->relays[i].consecutive_failures);
241 if (sel->relays[i].consecutive_failures >= MAX_FAILURES) {
242 sel->relays[i].alive = false;
243 ESP_LOGW(TAG, "Relay %s marked dead, triggering re-probe", url);
244 select_primary_fallback(sel);
245 }
246 break;
247 }
248 }
249 xSemaphoreGive(sel->lock);
250}
251
252esp_err_t relay_selector_seed_from_config(relay_selector_t *sel)
253{
254 const tollgate_config_t *cfg = tollgate_config_get();
255 xSemaphoreTake(sel->lock, portMAX_DELAY);
256
257 sel->count = 0;
258 for (int i = 0; i < cfg->nostr_seed_relay_count && sel->count < RELAY_SELECTOR_MAX_RELAYS; i++) {
259 if (cfg->nostr_seed_relays[i][0] != '\0') {
260 strncpy(sel->relays[sel->count].url, cfg->nostr_seed_relays[i],
261 RELAY_SELECTOR_URL_LEN - 1);
262 sel->relays[sel->count].alive = true;
263 sel->count++;
264 }
265 }
266
267 xSemaphoreGive(sel->lock);
268 ESP_LOGI(TAG, "Seeded %zu relays from config", sel->count);
269 return ESP_OK;
270}