From df6ceeda803438fa9b7c7af6f9ee9d81bf491077 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Tue, 18 Aug 2026 14:11:55 +0200 Subject: [PATCH 1/3] ci: add Dependabot auto-merge workflow (#6) --- .github/workflows/dependabot-auto-merge.yml | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/dependabot-auto-merge.yml diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..c9ce6f3 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,24 @@ +name: Dependabot auto-merge + +on: + pull_request_target: + branches: + - master + - main + types: + - opened + +permissions: + pull-requests: write + contents: write + +jobs: + enableAutoMerge: + runs-on: ubuntu-latest + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Enable auto-merge for Dependabot PRs + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From fbb90d5d2a3c0e2d74f7899e5a227fb9f7e85e77 Mon Sep 17 00:00:00 2001 From: Rushaway Date: Tue, 18 Aug 2026 14:36:26 +0200 Subject: [PATCH 2/3] ci: add Dependabot config for github-actions updates (#7) --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From bccd50931eed8788d3bc18759498664d318f459b Mon Sep 17 00:00:00 2001 From: Rushaway Date: Sat, 12 Sep 2026 13:48:23 +0200 Subject: [PATCH 3/3] fix: correct webhook query string, bounds checks and handle leaks (#8) Include (discordWebhookAPI.inc): - Execute(): the thread_id branch produced "&?wait=true", so the "wait" query parameter was named "?wait" and Discord never returned the created message body (200 -> 204), which also broke a subsequent Webhook.Edit(). - Execute(): skip the thread_id query parameter when thread_name is set to avoid Discord error 220002 (a forum webhook cannot carry both). - Embed.GetField() / Webhook.GetEmbed(): the bounds check was inverted (array.Length < index), so every valid index returned null and out-of-range indices read past the array. Use "index >= 0 && index < Length". - AddField(), AddEmbed(), GetField(), GetEmbed(): free the JSONArray handle that was leaked on every call. - GetFooter/GetImage/GetThumbnail/GetVideo/GetProvider/GetAuthor/GetFields/ GetEmbeds: return null instead of raising a native error when the key is not set. - SetTimeStampNow(): drop the malformed "%FT\%T.000%z" format string. - DEBUG path: this.toString -> this.ToString (did not compile) and pass the JSON through a "%s" format instead of as the format string itself. - Size the Execute()/Edit() URL buffers from WEBHOOK_URL_MAX_SIZE. - SetThreadName(): take const char[]; fix the doc (Discord limit is 100). - Add Webhook.GetThreadName(); bump version to 1.1.0. example.sp: - PrintToServer used %s with an int client argument -> use %d. - Free the DataPack in both HTTP callbacks. - PrintToServer(messageId) -> PrintToServer("%s", messageId). README.md: - Success check is HTTPStatus_OK (Execute() forces wait=true), delete the webhook, drop the trailing slash, use discord.com. --- CHANGELOG.md | 35 ++++++++- README.md | 8 +- example.sp | 9 ++- include/discordWebhookAPI.inc | 143 ++++++++++++++++++++++++---------- 4 files changed, 145 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c66b60..2abcf47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,38 @@ -## Release Notes +## Release Notes + +## [1.1.0] + +### Fixed + +- `Webhook.Execute()` built an invalid query string (`&?wait=true`) when a `threadID` was + passed, so Discord never returned the created message body and `Webhook.Edit()` could not + be used afterwards. +- `Embed.GetField()` / `Webhook.GetEmbed()` used an inverted bounds check + (`array.Length < index`), returning `null` for every valid index and reading out of bounds + for invalid ones. +- Memory leaks: the `JSONArray` handles obtained in `AddField()`, `AddEmbed()`, `GetField()` + and `GetEmbed()` were never freed. +- Sub-object / array getters (`GetFooter`, `GetImage`, `GetThumbnail`, `GetVideo`, + `GetProvider`, `GetAuthor`, `GetFields`, `GetEmbeds`) raised a native error instead of + returning `null` when the key was not set. +- `Embed.SetTimeStampNow()` used a malformed format string (`"%FT\%T.000%z"`). +- `DEBUG` build path did not compile (`this.toString` instead of `this.ToString`) and passed + unescaped JSON as a format string to `PrintToServer`. +- URL buffers in `Execute()` / `Edit()` could truncate a near-maximum-length webhook URL. + +### Added + +- `Webhook.GetThreadName()`. +- `Webhook.Execute()` now skips the `thread_id` query parameter when `thread_name` is set, + avoiding Discord error `220002` (a forum webhook cannot use both). + +### Changed + +- `Webhook.SetThreadName()` now takes a `const char[]` and its documentation reflects the + real Discord limit of 100 characters. ## [1.0.0] ### Added -- Initial release. \ No newline at end of file +- Initial release. diff --git a/README.md b/README.md index 2b63e19..b565611 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,15 @@ Contrary to other includes, discordWebhookAPI: public Action SendDiscordWebhook(int client, int args) { Webhook webhook = new Webhook("This is the content of the webhook."); - webhook.Execute("https://discordapp.com/api/webhooks/6758765876/769876789009/", OnWebHookExecuted); + webhook.Execute("https://discord.com/api/webhooks/6758765876/769876789009", OnWebHookExecuted); + delete webhook; return Plugin_Continue; } -public void OnWebHookExecuted(HTTPResponse response, DataPack pack) +public void OnWebHookExecuted(HTTPResponse response, any data) { - if (response.Status == HTTPStatus_NoContent) + // Execute() always appends ?wait=true, so Discord answers 200 OK with the message body. + if (response.Status == HTTPStatus_OK) { PrintToServer("Webhook sent successfully!"); } diff --git a/example.sp b/example.sp index 7acd9bd..6db704a 100644 --- a/example.sp +++ b/example.sp @@ -157,8 +157,9 @@ Action SendDiscordWebhook(int client, int args) public void OnWebHookExecuted(HTTPResponse response, DataPack pack) { int client = pack.ReadCell(); + delete pack; - PrintToServer("Processed client n°%s's webhook, status %d", client, response.Status); + PrintToServer("Processed client n°%d's webhook, status %d", client, response.Status); if (response.Status != HTTPStatus_OK) { PrintToServer("An error has occured while sending the webhook."); @@ -167,10 +168,11 @@ public void OnWebHookExecuted(HTTPResponse response, DataPack pack) PrintToServer("The webhook has been sent successfuly."); // Retrieve the message's id. + // Note: response.Data is owned by the extension, do not delete it. JSONObject resData = view_as(response.Data); char messageId[64]; resData.GetString("id", messageId, sizeof messageId); - PrintToServer(messageId); + PrintToServer("%s", messageId); editWebhook(messageId, client); } @@ -205,8 +207,9 @@ void editWebhook(const char[] messageId, int client) void OnWebHookEdited(HTTPResponse response, DataPack pack) { int client = pack.ReadCell(); + delete pack; - PrintToServer("Edited client n°%s's webhook, status %d", client, response.Status); + PrintToServer("Edited client n°%d's webhook, status %d", client, response.Status); if (response.Status != HTTPStatus_OK) { PrintToServer("An error has occured while editing the webhook."); diff --git a/include/discordWebhookAPI.inc b/include/discordWebhookAPI.inc index 47819d2..cc79bd8 100644 --- a/include/discordWebhookAPI.inc +++ b/include/discordWebhookAPI.inc @@ -3,7 +3,7 @@ #endif #define _discordWebhookAPI_included_ -#define DiscordWebhookAPI_VERSION "1.0.1" +#define DiscordWebhookAPI_VERSION "1.1.0" #define WEBHOOK_MSG_MAX_SIZE 2000 #define WEBHOOK_URL_MAX_SIZE 1000 #define WEBHOOK_THREAD_NAME_MAX_SIZE 100 @@ -617,8 +617,8 @@ methodmap Embed < JSONObject */ public bool SetTimeStampNow() { - char timeNow[256]; - FormatTime(timeNow, sizeof timeNow, "%FT\%T.000%z", GetTime()); + char timeNow[64]; + FormatTime(timeNow, sizeof timeNow, "%Y-%m-%dT%H:%M:%S%z", GetTime()); return this.SetString("timestamp", timeNow); } @@ -650,6 +650,10 @@ methodmap Embed < JSONObject */ public EmbedFooter GetFooter() { + if (!this.HasKey("footer")) + { + return null; + } return view_as(this.Get("footer")); } @@ -671,6 +675,10 @@ methodmap Embed < JSONObject */ public EmbedImage GetImage() { + if (!this.HasKey("image")) + { + return null; + } return view_as(this.Get("image")); } @@ -692,6 +700,10 @@ methodmap Embed < JSONObject */ public EmbedThumbnail GetThumbnail() { + if (!this.HasKey("thumbnail")) + { + return null; + } return view_as(this.Get("thumbnail")); } @@ -713,6 +725,10 @@ methodmap Embed < JSONObject */ public EmbedVideo GetVideo() { + if (!this.HasKey("video")) + { + return null; + } return view_as(this.Get("video")); } @@ -734,6 +750,10 @@ methodmap Embed < JSONObject */ public EmbedProvider GetProvider() { + if (!this.HasKey("provider")) + { + return null; + } return view_as(this.Get("provider")); } @@ -755,6 +775,10 @@ methodmap Embed < JSONObject */ public EmbedAuthor GetAuthor() { + if (!this.HasKey("author")) + { + return null; + } return view_as(this.Get("author")); } @@ -776,22 +800,33 @@ methodmap Embed < JSONObject */ public JSONArray GetFields() { + if (!this.HasKey("fields")) + { + return null; + } return view_as(this.Get("fields")); } /** * Retrieve a field of the embed. - * - * @return Field corresponding to the input index. null if an error occurs. + * + * @param index Index of the field to retrieve. + * @return Field corresponding to the input index. null if the index is out of bounds. */ public EmbedField GetField(int index) { + if (!this.HasKey("fields")) + { + return null; + } JSONArray fields = view_as(this.Get("fields")); - if(fields != null && fields.Length < index) + EmbedField field = null; + if (fields != null && index >= 0 && index < fields.Length) { - return view_as(fields.Get(index)); + field = view_as(fields.Get(index)); } - return null; + delete fields; + return field; } /** @@ -811,15 +846,14 @@ methodmap Embed < JSONObject { fields = new JSONArray(); } - if(fields.Push(view_as(field))) + int count = -1; + if(fields.Push(view_as(field)) && this.Set("fields", fields)) { - if(this.Set("fields", fields)) - { - delete field; - return fields.Length; - } + count = fields.Length; } - return -1; + delete field; + delete fields; + return count; } } @@ -827,12 +861,12 @@ methodmap Embed < JSONObject methodmap Webhook < JSONObject { /** - * Constructor for the Embed methodmap. - * + * Constructor for the Webhook methodmap. + * * @param content Content of the webhook. - * @return Returns the Embed. + * @return Returns the Webhook. */ - public Webhook(const char[] content="") + public Webhook(const char[] content="") { JSONObject jsonObject = new JSONObject(); jsonObject.SetString("content", content); @@ -887,15 +921,29 @@ methodmap Webhook < JSONObject /** * Set the Thread Name of the webhook. - * - * @param threadName Thread Name of the webhook. (char max is 1000) + * When set, executing the webhook on a forum channel creates a new thread with this name. + * Note: Discord limits thread names to WEBHOOK_THREAD_NAME_MAX_SIZE (100) characters. + * + * @param threadName Thread Name of the webhook. * @return True on success. False otherwise. */ - public bool SetThreadName(char[] threadName) + public bool SetThreadName(const char[] threadName) { return this.SetString("thread_name", threadName); } + /** + * Retrieve the thread name of the webhook. + * + * @param buffer String buffer to store value. + * @param maxlength Maximum length of the string buffer. + * @return True on success. False otherwise. + */ + public bool GetThreadName(char[] buffer, int maxlength) + { + return this.GetString("thread_name", buffer, maxlength); + } + /** * Retrieve the avatar_url of the webhook. * @@ -947,27 +995,38 @@ methodmap Webhook < JSONObject */ public JSONArray GetEmbeds() { + if (!this.HasKey("embeds")) + { + return null; + } return view_as(this.Get("embeds")); } /** * Retrieve an embed of the webook from its index. - * - * @return Embed corresponding to the input index. null if an error occurs. + * + * @param index Index of the embed to retrieve. + * @return Embed corresponding to the input index. null if the index is out of bounds. */ public Embed GetEmbed(int index) { + if (!this.HasKey("embeds")) + { + return null; + } JSONArray embeds = view_as(this.Get("embeds")); - if(embeds != null && embeds.Length < index) + Embed embed = null; + if(embeds != null && index >= 0 && index < embeds.Length) { - return view_as(embeds.Get(index)); + embed = view_as(embeds.Get(index)); } - return null; + delete embeds; + return embed; } /** * Add an embed to the webhook. This will delete the handle to the embed. - * + * * @param embed Embed to add to the webhook. * @return The number of embeds after the new one was added. -1 otherwise. */ @@ -982,15 +1041,14 @@ methodmap Webhook < JSONObject { embeds = new JSONArray(); } - if(embeds.Push(view_as(embed))) + int count = -1; + if(embeds.Push(view_as(embed)) && this.Set("embeds", embeds)) { - if(this.Set("embeds", embeds)) - { - delete embed; - return embeds.Length; - } + count = embeds.Length; } - return -1; + delete embed; + delete embeds; + return count; } /** @@ -1003,17 +1061,18 @@ methodmap Webhook < JSONObject */ public void Execute(const char[] webhook, HTTPRequestCallback callback, any data = 0, const char[] threadID = "") { - char webhook_query[1024]; - if (!threadID[0]) + char webhook_query[WEBHOOK_URL_MAX_SIZE + 64]; + // Discord rejects a request that carries both thread_name (body) and thread_id (query). + if (!threadID[0] || this.HasKey("thread_name")) Format(webhook_query, sizeof webhook_query, "%s?wait=true", webhook); else - Format(webhook_query, sizeof webhook_query, "%s?thread_id=%s&?wait=true", webhook, threadID); + Format(webhook_query, sizeof webhook_query, "%s?thread_id=%s&wait=true", webhook, threadID); HTTPRequest httpRequest = new HTTPRequest(webhook_query); #if defined DEBUG - char debug[9999]; - this.toString(debug, sizeof debug); - PrintToServer(debug); + char debug[4096]; + this.ToString(debug, sizeof debug); + PrintToServer("%s", debug); #endif httpRequest.Post(view_as(this), callback, data); } @@ -1028,7 +1087,7 @@ methodmap Webhook < JSONObject */ public void Edit(const char[] webhook, const char[] messageId, HTTPRequestCallback callback, any data = 0) { - char webhook_patch[1024]; + char webhook_patch[WEBHOOK_URL_MAX_SIZE + 64]; Format(webhook_patch, sizeof webhook_patch, "%s/messages/%s", webhook, messageId); HTTPRequest httpRequest = new HTTPRequest(webhook_patch); httpRequest.Patch(view_as(this), callback, data);