@@ -79,6 +79,8 @@ static const uint8_t TEST_GROUP_SECRET[16] = {
7979#if defined(ESP32)
8080 #define DISCORD_WEBHOOK_MIN_INTERVAL_MS 200
8181 #define DISCORD_WEBHOOK_RETRY_DELAY_MS 2000
82+ #define DISCORD_WEBHOOK_MAX_RETRIES 2
83+ #define DISCORD_WEBHOOK_DEDUPE_TTL_MS 30000UL
8284
8385static size_t jsonEscape (const char * src, char * dst, size_t dst_len) {
8486 if (dst_len == 0 ) return 0 ;
@@ -826,6 +828,29 @@ void MyMesh::initTestChannel() {
826828}
827829
828830#if defined(ESP32)
831+ bool MyMesh::shouldQueueDiscordWebhook (const char * sender, const char * body) {
832+ uint32_t msg_hash = 0 ;
833+ mesh::Utils::sha256 ((uint8_t *)&msg_hash, sizeof (msg_hash),
834+ (const uint8_t *)sender, strlen (sender),
835+ (const uint8_t *)body, strlen (body));
836+
837+ int slot = -1 ;
838+ for (uint8_t i = 0 ; i < DISCORD_WEBHOOK_DEDUPE_SIZE ; i++) {
839+ if (webhook_dedupe[i].expires_at == 0 || millisHasNowPassed (webhook_dedupe[i].expires_at )) {
840+ if (slot < 0 ) slot = i;
841+ continue ;
842+ }
843+ if (webhook_dedupe[i].hash == msg_hash) {
844+ return false ;
845+ }
846+ }
847+
848+ if (slot < 0 ) slot = 0 ;
849+ webhook_dedupe[slot].hash = msg_hash;
850+ webhook_dedupe[slot].expires_at = futureMillis (DISCORD_WEBHOOK_DEDUPE_TTL_MS );
851+ return true ;
852+ }
853+
829854void MyMesh::initWifiClient () {
830855 if (_prefs.wifi_ssid [0 ] == 0 || _prefs.wifi_pwd [0 ] == 0 ) return ;
831856 WiFi.mode (WIFI_STA );
@@ -837,6 +862,10 @@ void MyMesh::initWifiClient() {
837862
838863void MyMesh::queueDiscordWebhook (const char * sender, const char * body) {
839864 if (_prefs.discord_webhook_url [0 ] == 0 ) return ;
865+ if (!shouldQueueDiscordWebhook (sender, body)) {
866+ MESH_DEBUG_PRINTLN (" Discord webhook dedupe hit, skipping duplicate" );
867+ return ;
868+ }
840869 if (webhook_count >= DISCORD_WEBHOOK_QUEUE_SIZE ) {
841870 MESH_DEBUG_PRINTLN (" Discord webhook queue full, dropping oldest" );
842871 webhook_head = (uint8_t )((webhook_head + 1 ) % DISCORD_WEBHOOK_QUEUE_SIZE );
@@ -845,6 +874,7 @@ void MyMesh::queueDiscordWebhook(const char* sender, const char* body) {
845874 WebhookItem* item = &webhook_queue[webhook_tail];
846875 StrHelper::strncpy (item->sender , sender, sizeof (item->sender ));
847876 StrHelper::strncpy (item->body , body, sizeof (item->body ));
877+ item->retry_count = 0 ;
848878 webhook_tail = (uint8_t )((webhook_tail + 1 ) % DISCORD_WEBHOOK_QUEUE_SIZE );
849879 webhook_count++;
850880 next_webhook_attempt_at = 0 ;
@@ -866,6 +896,25 @@ void MyMesh::pumpDiscordWebhook() {
866896 if (next_webhook_attempt_at && !millisHasNowPassed (next_webhook_attempt_at)) return ;
867897
868898 WebhookItem* item = &webhook_queue[webhook_head];
899+ auto dropWebhookItem = [&]() {
900+ webhook_head = (uint8_t )((webhook_head + 1 ) % DISCORD_WEBHOOK_QUEUE_SIZE );
901+ webhook_count--;
902+ };
903+ auto scheduleWebhookRetry = [&](const char * reason) {
904+ if (item->retry_count >= DISCORD_WEBHOOK_MAX_RETRIES ) {
905+ MESH_DEBUG_PRINTLN (" Discord webhook dropped after %u retries: %s" ,
906+ (unsigned )item->retry_count , reason);
907+ dropWebhookItem ();
908+ next_webhook_attempt_at = futureMillis (DISCORD_WEBHOOK_MIN_INTERVAL_MS );
909+ return ;
910+ }
911+ item->retry_count ++;
912+ MESH_DEBUG_PRINTLN (" Discord webhook retry %u/%u: %s" ,
913+ (unsigned )item->retry_count ,
914+ (unsigned )DISCORD_WEBHOOK_MAX_RETRIES ,
915+ reason);
916+ next_webhook_attempt_at = futureMillis (DISCORD_WEBHOOK_RETRY_DELAY_MS );
917+ };
869918
870919 char esc_sender[80 ];
871920 char esc_body[256 ];
@@ -907,7 +956,6 @@ void MyMesh::pumpDiscordWebhook() {
907956 } else if (starts_with_ci (p, http_scheme, 7 )) {
908957 is_http = true ;
909958 } else {
910- // Search for a scheme inside the string in case of hidden prefix chars.
911959 const char * s = p;
912960 while (*s) {
913961 if (starts_with_ci (s, https_scheme, 8 )) { p = s; is_https = true ; break ; }
@@ -921,12 +969,12 @@ void MyMesh::pumpDiscordWebhook() {
921969 }
922970 char lead_hex[24 ];
923971 int lh = 0 ;
924- for (int i = 0 ; i < 8 && url [i]; i++) {
925- lh += snprintf (lead_hex + lh, sizeof (lead_hex) - lh, " %02X" , (unsigned char )url [i]);
972+ for (int i = 0 ; i < 8 && p [i]; i++) {
973+ lh += snprintf (lead_hex + lh, sizeof (lead_hex) - lh, " %02X" , (unsigned char )p [i]);
926974 }
927975 MESH_DEBUG_PRINTLN (" Discord webhook scheme: %s lead=%s" , is_https ? " https" : " http" , lead_hex);
928976
929- const char * host = url ;
977+ const char * host = p ;
930978 if (is_https) host += 8 ;
931979 else if (is_http) host += 7 ;
932980
@@ -947,9 +995,10 @@ void MyMesh::pumpDiscordWebhook() {
947995 if (is_https) {
948996 WiFiClientSecure client;
949997 client.setInsecure ();
998+ client.setTimeout (8000 );
950999 if (!client.connect (host_buf, 443 )) {
9511000 MESH_DEBUG_PRINTLN (" Discord webhook TLS connect failed" );
952- next_webhook_attempt_at = futureMillis ( DISCORD_WEBHOOK_RETRY_DELAY_MS );
1001+ scheduleWebhookRetry ( " TLS connect failed " );
9531002 return ;
9541003 }
9551004 client.printf (" POST %s HTTP/1.1\r\n " , path);
@@ -960,16 +1009,17 @@ void MyMesh::pumpDiscordWebhook() {
9601009 client.printf (" Connection: close\r\n\r\n " );
9611010 client.write ((const uint8_t *)payload, strlen (payload));
9621011
963- // Read status line
1012+ unsigned long wait_until = millis () + 8000 ;
1013+ while (client.connected () && !client.available () && (long )(millis () - wait_until) < 0 ) {
1014+ delay (10 );
1015+ }
1016+
9641017 String status = client.readStringUntil (' \n ' );
9651018 status.trim ();
9661019 if (status.startsWith (" HTTP/" )) {
9671020 int sp = status.indexOf (' ' );
968- if (sp > 0 ) {
969- code = status.substring (sp + 1 ).toInt ();
970- }
1021+ if (sp > 0 ) code = status.substring (sp + 1 ).toInt ();
9711022 }
972- // Read headers, then body
9731023 while (client.connected ()) {
9741024 String line = client.readStringUntil (' \n ' );
9751025 if (line == " \r " || line.length () == 0 ) break ;
@@ -979,16 +1029,23 @@ void MyMesh::pumpDiscordWebhook() {
9791029 } else {
9801030 HTTPClient http;
9811031 WiFiClient client;
982- http.begin (client, host_buf, 80 , path, false );
1032+ client.setTimeout (8000 );
1033+ if (!http.begin (client, host_buf, 80 , path, false )) {
1034+ MESH_DEBUG_PRINTLN (" Discord webhook HTTP begin failed" );
1035+ scheduleWebhookRetry (" HTTP begin failed" );
1036+ return ;
1037+ }
1038+ http.setConnectTimeout (8000 );
1039+ http.setTimeout (8000 );
9831040 http.addHeader (" Content-Type" , " application/json" );
1041+ http.addHeader (" User-Agent" , " MeshCore" );
9841042 code = http.POST ((uint8_t *)payload, strlen (payload));
9851043 resp = http.getString ();
9861044 http.end ();
9871045 }
9881046
9891047 if (code >= 200 && code < 300 ) {
990- webhook_head = (uint8_t )((webhook_head + 1 ) % DISCORD_WEBHOOK_QUEUE_SIZE );
991- webhook_count--;
1048+ dropWebhookItem ();
9921049 MESH_DEBUG_PRINTLN (" Discord webhook OK: HTTP %d" , code);
9931050 next_webhook_attempt_at = futureMillis (DISCORD_WEBHOOK_MIN_INTERVAL_MS );
9941051 } else {
@@ -1000,7 +1057,7 @@ void MyMesh::pumpDiscordWebhook() {
10001057 resp_buf[n] = 0 ;
10011058 }
10021059 MESH_DEBUG_PRINTLN (" Discord webhook failed: HTTP %d body=%s" , code, resp_buf[0 ] ? resp_buf : " <empty>" );
1003- next_webhook_attempt_at = futureMillis ( DISCORD_WEBHOOK_RETRY_DELAY_MS );
1060+ scheduleWebhookRetry (resp_buf[ 0 ] ? resp_buf : " <empty> " );
10041061 }
10051062}
10061063#endif
@@ -1318,10 +1375,10 @@ void MyMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mesh::Gro
13181375
13191376 StrHelper::strncpy (pending_ping_sender, sender_name, sizeof (pending_ping_sender));
13201377 StrHelper::strncpy (pending_ping_path, path_str, sizeof (pending_ping_path));
1321- pending_ping_is_5count = is_5count ;
1378+ pending_ping_is_5count = false ;
13221379 pending_ping_channel = channel;
13231380 pending_ping_channel_ready = true ;
1324- pending_ping_total = is_5count ? 5 : PING_REPLY_ATTEMPTS ;
1381+ pending_ping_total = ( uint8_t ) min ( 3 , ( int )max_replies) ;
13251382 pending_ping_retries = pending_ping_total;
13261383 pending_ping_include_prefix = true ;
13271384 next_ping_send_at = 0 ;
@@ -1538,6 +1595,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
15381595 webhook_head = 0 ;
15391596 webhook_tail = 0 ;
15401597 webhook_count = 0 ;
1598+ memset (webhook_dedupe, 0 , sizeof (webhook_dedupe));
15411599#endif
15421600
15431601#if MAX_NEIGHBOURS
0 commit comments