Go Fix Social Links

While I don't really use social media much these days, my friends and co-workers are constantly sharing links from Twitter, Instagram, Tiktok, and more via Discord. The challenge for me, and all of us really is that Discord doesn't handle the oembed from social media sites well in the app. More likely the cause is that they don't provide accurate omebed data. After all if you watch an embedded short, or view a spicy meme without going to these sites, how can they collect your data, or possible serve any adds?

There is a workaround to this. There are wonderful tools like vxTwitter, ddInstagram, and more. These tools allow you to add a prefix to a URL before sharing it to allow Discord to use its video player, instead of trying to embed Twitter's and being forced to open the app or go to their site and login.

Take for example, sharing of the following Twitter link in Discord (I still refuse to cal it X.com)

1https://x.com/barstoolsports/status/1742676131147354116

twitter-embed.png

Despite being a tweet to a video, the video will not embed in Discord. Discord instead gets a static image of a single frame of the video.

1https://fixvx.com/barstoolsports/status/1742676131147354116

twitter-embed.png

This works great, but requires me to manually add a prefix to social links AND to remember what the prefix should be. I can't be bothered to do something that requires 10 seconds of my attention a couple of times per day. However, I can be bothered to spend a weekend programming a Discord bot that has a command to automatically "fix"

Hacking on a Discord Bot

This bot makes use of bwmarrin/Discordgo and the existing hosted services for "fixing" embeds shared within Discord. To create a new client instance of the bot and create a new session via websocket to connected servers.

 1   token := os.Getenv("Discord_TOKEN")
 2
 3	dSession, err := Discordgo.New("Bot " + token)
 4	if err != nil {
 5		log.Fatal("Failed to create Discord session")
 6		return
 7	}
 8
 9    // Open websocket connection to Discord and begin listening.
10    err = dSession.Open()
11    if err != nil {
12        log.Fatal("Failed to open Discord websocket connection")
13    return
14    }

Next create the slash commands, and register them to the session.

 1command, err := dSession.ApplicationCommandCreate(dSession.State.User.ID, "", &Discordgo.ApplicationCommand{
 2    Name:        "fix-social",
 3    Description: "Attempts to fix a social media link embed",
 4    Options: []*Discordgo.ApplicationCommandOption{
 5        {
 6            Type:        Discordgo.ApplicationCommandOptionString,
 7            Name:        "url",
 8            Description: "URL to fix",
 9            Required:    true,
10		},
11    },
12})
13
14dSession.AddHandler(func(s *Discordgo.Session, i *Discordgo.InteractionCreate) {
15    if i.ApplicationCommandData().Name == "fix-social" {
16		// Link fixing magic.
17    }
18	
19}

Take note that when creating commands you can pass a GUID of a specific server, which is useful in testing, failure to do so will register the slash command globally. While this makes it more widely available the tradeoff comes at the amount of time it now takes to bust the cache for changes to this command to propagate across servers.

With all the wiring of Discord established, all that is left is to do is basic string manipulation and send a response.

 1// Link fixing magic
 2input := i.ApplicationCommandData().Options[0].StringValue()
 3var response string
 4if IsUrl(input) {
 5    url, _ := url.Parse(input)
 6
 7    fixableLink := link.Link{
 8        URL: url,
 9    }
10
11    if fixableLink.IsFixableUrl() {
12        s.InteractionRespond(i.Interaction, &Discordgo.InteractionResponse{
13            Type: Discordgo.InteractionResponseChannelMessageWithSource,
14            Data: &Discordgo.InteractionResponseData{
15                Content: fixableLink.Fix(),
16            },
17        })
18        response = fixableLink.Fix()
19    } 
20}
21
22type Link struct {
23    URL      *url.URL
24    Hostname string
25}
26
27func IsUrl(str string) bool {
28    url, err := url.Parse(str)
29    if err != nil {
30        return false
31    }
32    if url.Scheme == "" || url.Host == "" {
33        return false
34    }
35    return true
36}
37
38func getHostname(hostname string) string {
39    // Check if the hostname has www. in it and if so remove it
40    substring := strings.Split(hostname, ".")
41    
42    if substring[0] == "www" {
43        hostname = strings.Join(substring[1:], ".")
44    }
45    
46    return hostname
47}
48
49func (l *Link) IsFixableUrl() bool {
50    l.Hostname = getHostname(l.URL.Hostname())
51    switch l.Hostname {
52    case "instagram.com", "twitter.com", "x.com", "reddit.com", "tiktok.com":
53        return true
54    }
55    return false
56}
57
58func fixURL(link *Link) string {
59    switch link.Hostname {
60    case "instagram.com":
61        return fixInstagram(link)
62    case "twitter.com":
63        return fixTwitter(link)
64    case "x.com":
65        return fixX(link)
66    case "reddit.com":
67        return fixReddit(link)
68    case "tiktok.com":
69        return fixTikTok(link)
70    }
71    return ""
72}
73
74func fixTwitter(link *Link) string {
75    newURL := link.URL.Scheme + "://fx" + link.Hostname + link.URL.Path
76    return newURL
77}

With everything working, all that's left is to slap upload to a $5 Linode tier or add it to a Raspberry Pi. And with that another successful weekend was invested in preventing the mildest annoyance and saving a few minutes of my time annually.

comments powered by Disqus