From c5af3c1d631d7f82c0d00369a698d877375c1de0 Mon Sep 17 00:00:00 2001 From: deluan Date: Wed, 4 Mar 2026 12:14:58 -0500 Subject: [PATCH] feat: add truncateText and truncateURL helpers for Discord field limits Discord silently rejects presence updates when text fields exceed 128 characters or URL fields exceed 256 characters. Fixes #16 --- rpc.go | 24 ++++++++++++++++++++++++ rpc_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/rpc.go b/rpc.go index 7b2c573..bc33b60 100644 --- a/rpc.go +++ b/rpc.go @@ -55,6 +55,30 @@ const ( const heartbeatInterval = 41 // Heartbeat interval in seconds +// Discord API field length limits +const ( + maxTextLength = 128 // Max characters for text fields (details, state, name, large_text) + maxURLLength = 256 // Max characters for URL fields (details_url, state_url, etc.) +) + +// truncateText truncates s to maxTextLength runes, appending "…" if truncated. +func truncateText(s string) string { + runes := []rune(s) + if len(runes) <= maxTextLength { + return s + } + return string(runes[:maxTextLength-1]) + "…" +} + +// truncateURL returns s unchanged if within maxURLLength, otherwise returns "" +// (a truncated URL would be broken, so we omit it entirely). +func truncateURL(s string) string { + if len(s) <= maxURLLength { + return s + } + return "" +} + // activity represents a Discord activity sent via Gateway opcode 3. type activity struct { Name string `json:"name"` diff --git a/rpc_test.go b/rpc_test.go index be5dca5..1b3931b 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -448,4 +448,55 @@ var _ = Describe("discordRPC", func() { Expect(err).ToNot(HaveOccurred()) }) }) + + Describe("truncateText", func() { + It("returns short strings unchanged", func() { + Expect(truncateText("hello")).To(Equal("hello")) + }) + + It("returns exactly 128-char strings unchanged", func() { + s := strings.Repeat("a", 128) + Expect(truncateText(s)).To(Equal(s)) + }) + + It("truncates strings over 128 chars to 127 + ellipsis", func() { + s := strings.Repeat("a", 200) + result := truncateText(s) + Expect([]rune(result)).To(HaveLen(128)) + Expect(result).To(HaveSuffix("…")) + }) + + It("handles multi-byte characters correctly", func() { + // 130 Japanese characters — each is one rune but 3 bytes + s := strings.Repeat("あ", 130) + result := truncateText(s) + runes := []rune(result) + Expect(runes).To(HaveLen(128)) + Expect(string(runes[127])).To(Equal("…")) + }) + + It("returns empty string unchanged", func() { + Expect(truncateText("")).To(Equal("")) + }) + }) + + Describe("truncateURL", func() { + It("returns short URLs unchanged", func() { + Expect(truncateURL("https://example.com")).To(Equal("https://example.com")) + }) + + It("returns exactly 256-char URLs unchanged", func() { + u := "https://example.com/" + strings.Repeat("a", 236) + Expect(truncateURL(u)).To(Equal(u)) + }) + + It("returns empty string for URLs over 256 chars", func() { + u := "https://example.com/" + strings.Repeat("a", 237) + Expect(truncateURL(u)).To(Equal("")) + }) + + It("returns empty string unchanged", func() { + Expect(truncateURL("")).To(Equal("")) + }) + }) })