mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-10-23 13:32:53 +00:00
Make sure discord invite links are clickable
This commit is contained in:
1 parent
cc40ead28d
commit
1f68a4b047
4 files changed
+89
-13
No files matched your search
@@ -328,15 +328,26 @@ func validMeshCoreKey(s string) bool {
|
||||
// Signal enforces its username grammar; other text platforms (Discord) just need
|
||||
// a non-empty, space-free handle.
|
||||
func normalizeTextHandle(platform, val string) (string, string) {
|
||||
v := strings.TrimPrefix(strings.TrimSpace(val), "@")
|
||||
val = strings.TrimSpace(val)
|
||||
if platform == store.SignalPlatform {
|
||||
v := strings.TrimPrefix(val, "@")
|
||||
if !validSignalUsername(v) {
|
||||
return "", "Enter a valid Signal username (3–32 characters: letters, digits, . and _)."
|
||||
}
|
||||
return v, ""
|
||||
}
|
||||
// A text handle (Discord) also accepts an invite/profile URL, stored and
|
||||
// rendered as a clickable link; a bare username stays plain text.
|
||||
if store.LooksLikeURL(val) {
|
||||
u := store.NormalizeLinkURL(val)
|
||||
if !store.ValidLinkURL(u) {
|
||||
return "", "Enter a valid username or invite link."
|
||||
}
|
||||
return u, ""
|
||||
}
|
||||
v := strings.TrimPrefix(val, "@")
|
||||
if v == "" || len(v) > 64 || strings.ContainsAny(v, " \t\n\r") {
|
||||
return "", "Enter a valid username (no spaces)."
|
||||
return "", "Enter a valid username (no spaces) or an invite link."
|
||||
}
|
||||
return v, ""
|
||||
}
|
||||
|
||||
+14
-6
@@ -327,13 +327,21 @@ func (s *Handlers) handleSetOrgLinks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// Validate/canonicalise per kind, leaving `u` as the value to persist.
|
||||
switch p.Kind {
|
||||
case store.KindText: // Discord — a handle shown as text.
|
||||
v := strings.TrimPrefix(u, "@")
|
||||
if v == "" || len(v) > 64 || strings.ContainsAny(v, " \t\n\r") {
|
||||
orgErr(w, r, "Enter a valid username (no spaces).")
|
||||
return
|
||||
case store.KindText: // Discord — a username shown as text, or an invite link.
|
||||
if store.LooksLikeURL(u) {
|
||||
u = store.NormalizeLinkURL(u)
|
||||
if !store.ValidLinkURL(u) {
|
||||
orgErr(w, r, "Enter a valid username or invite link.")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
v := strings.TrimPrefix(u, "@")
|
||||
if v == "" || len(v) > 64 || strings.ContainsAny(v, " \t\n\r") {
|
||||
orgErr(w, r, "Enter a valid username (no spaces) or an invite link.")
|
||||
return
|
||||
}
|
||||
u = v
|
||||
}
|
||||
u = v
|
||||
case store.KindHandle:
|
||||
canon, ok := p.CanonicalHandleURL(u)
|
||||
if !ok {
|
||||
|
||||
@@ -108,6 +108,10 @@ func TestHandleFromURLAndDisplay(t *testing.T) {
|
||||
{UserLink{Platform: "github", Label: "My code", URL: "https://github.com/octocat"}, "My code", "https://github.com/octocat"},
|
||||
// Text platform: display the handle text, no link.
|
||||
{UserLink{Platform: "discord", URL: "cooluser"}, "cooluser", ""},
|
||||
// A new-style Discord username may contain dots but is still just text.
|
||||
{UserLink{Platform: "discord", URL: "cool.user"}, "cool.user", ""},
|
||||
// A Discord invite URL is clickable and reads as the platform name.
|
||||
{UserLink{Platform: "discord", URL: "https://discord.gg/abc123"}, "Discord", "https://discord.gg/abc123"},
|
||||
{UserLink{Platform: SignalPlatform, URL: "alice.42"}, "alice.42", ""},
|
||||
// Email: mailto:, and the address itself as the display (not "Email").
|
||||
{UserLink{Platform: EmailPlatform, URL: "a@b.com"}, "a@b.com", "mailto:a@b.com"},
|
||||
@@ -133,3 +137,21 @@ func TestHandleFromURLAndDisplay(t *testing.T) {
|
||||
t.Errorf("org discord Href() = %q, want empty", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksLikeURL(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"cooluser": false, // bare handle
|
||||
"cool.user": false, // dotted handle (new Discord usernames)
|
||||
"@handle": false,
|
||||
"discord.gg/abc": true, // bare host + path
|
||||
"discord.com/invite/abc": true,
|
||||
"https://discord.gg/abc": true,
|
||||
"http://example.com": true,
|
||||
"example.com": false, // dotted host but no path — treated as a handle
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := LooksLikeURL(in); got != want {
|
||||
t.Errorf("LooksLikeURL(%q) = %v, want %v", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ var linkPlatforms = []LinkPlatform{
|
||||
{Key: "telegram", Name: "Telegram", Kind: KindHandle, Icon: "icon-brand-telegram", URLFmt: "https://t.me/%s", Hosts: []string{"t.me", "telegram.me"}, Placeholder: "@username or profile URL"},
|
||||
{Key: mastodonKey, Name: "Mastodon", Kind: KindHandle, Icon: "icon-brand-mastodon", Placeholder: "@user@instance or profile URL"},
|
||||
{Key: "bluesky", Name: "Bluesky", Kind: KindHandle, Icon: "icon-brand-bluesky", URLFmt: "https://bsky.app/profile/%s", Hosts: []string{"bsky.app"}, Placeholder: "@handle or profile URL"},
|
||||
{Key: "discord", Name: "Discord", Kind: KindText, Icon: "icon-brand-discord", Placeholder: "username"},
|
||||
{Key: "discord", Name: "Discord", Kind: KindText, Icon: "icon-brand-discord", Placeholder: "username or invite link"},
|
||||
}
|
||||
|
||||
var linkPlatformByKey = func() map[string]LinkPlatform {
|
||||
@@ -322,8 +322,15 @@ func linkDisplay(p LinkPlatform, key, value string) string {
|
||||
return h
|
||||
}
|
||||
return p.Name
|
||||
case KindText, KindEmail:
|
||||
// Show the handle / email address itself rather than the platform name.
|
||||
case KindText:
|
||||
// A text handle (e.g. a Discord username) shows itself; but the field also
|
||||
// accepts an invite/profile URL, which reads better as the platform name.
|
||||
if isHTTPURL(value) {
|
||||
return p.Name
|
||||
}
|
||||
return value
|
||||
case KindEmail:
|
||||
// Show the email address itself rather than the platform name.
|
||||
return value
|
||||
case "":
|
||||
return key // unknown platform — show the raw key defensively
|
||||
@@ -333,10 +340,17 @@ func linkDisplay(p LinkPlatform, key, value string) string {
|
||||
}
|
||||
|
||||
// linkHref computes the hyperlink target for a link value given its platform
|
||||
// descriptor. Text/key platforms aren't linkable; email becomes mailto:.
|
||||
// descriptor. Key platforms aren't linkable; email becomes mailto:. A text handle
|
||||
// isn't linkable either — except the field accepts a URL (e.g. a Discord invite),
|
||||
// which we do link.
|
||||
func linkHref(p LinkPlatform, value string) string {
|
||||
switch p.Kind {
|
||||
case KindText, KindKey:
|
||||
case KindKey:
|
||||
return ""
|
||||
case KindText:
|
||||
if isHTTPURL(value) {
|
||||
return value
|
||||
}
|
||||
return ""
|
||||
case KindEmail:
|
||||
return "mailto:" + value
|
||||
@@ -345,6 +359,27 @@ func linkHref(p LinkPlatform, value string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// isHTTPURL reports whether value is already a stored http(s) URL. Stored values
|
||||
// are normalized on save, so a scheme prefix is a reliable signal.
|
||||
func isHTTPURL(value string) bool {
|
||||
return strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://")
|
||||
}
|
||||
|
||||
// LooksLikeURL reports whether raw was entered as a web URL (a scheme, or a
|
||||
// dotted host followed by a path like "discord.gg/abc") rather than a bare handle.
|
||||
// A handle may contain dots (new Discord usernames do) but never a slash, so the
|
||||
// path is the reliable discriminator.
|
||||
func LooksLikeURL(raw string) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if strings.Contains(raw, "://") {
|
||||
return true
|
||||
}
|
||||
if i := strings.IndexByte(raw, '/'); i > 0 {
|
||||
return strings.Contains(raw[:i], ".")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ListOrgLinks returns an org's links in display order.
|
||||
func (s *Store) ListOrgLinks(ctx context.Context, orgID int64) ([]OrgLink, error) {
|
||||
rows, err := s.pool.Query(ctx,
|
||||
|
||||
Reference in new issue
Block a user