Discord: Gifs aren't rendered #331

Closed
opened 2026-10-09 10:46:57 -04:00 by Salastil · 1 comment
Owner

Discord uses external services like klipy.com alongside a few others to handle gifs in its client. Currently when one of these gifs is entered into the chatbuffer it just shows the url since the url does not end in .gif. We need both a gif picker that can browse and enter the stickers into the chat like the official client. The chat buffer also needs to handle embedding these like Discord does. The embedding in the chat buffer should exist on every protocol since these gif sites should be regarded as trustworthy.

Discord uses external services like klipy.com alongside a few others to handle gifs in its client. Currently when one of these gifs is entered into the chatbuffer it just shows the url since the url does not end in .gif. We need both a gif picker that can browse and enter the stickers into the chat like the official client. The chat buffer also needs to handle embedding these like Discord does. The embedding in the chat buffer should exist on every protocol since these gif sites should be regarded as trustworthy.
Salastil added this to the v1.0 milestone 2026-10-09 10:57:36 -04:00
Author
Owner

Finished: 55248eb.

Before, the same three messages (a Tenor, a Giphy and a Klipy link):

before

After:

after

The picker:

the GIF picker

Why a Klipy GIF showed as its address. Discord sends a posted GIF with an embed of type gifv carrying the video. The daemon skipped any embed whose source link was already in the message text (a rule meant for YouTube), so the GIF was dropped and only the link showed. A gifv is now kept as a GIF and played from Discord's own proxied copy, so reading a message does not tell Tenor or Klipy who read it (the arrangement Discord's client makes).

On every service. For everything that is not Discord, the daemon now reads the page itself, the way it does for YouTube cards, through the account's own route (Tor where the account is): Tenor from the page's OpenGraph tags, Giphy from the link's own id with no request at all. A GIF is drawn as a picture - silent, looping, no player controls - using the existing autoplay setting (off, it plays while the pointer is over it), and the page address it came from is not shown as well. Pages of these sites are never probed by the window for content type.

Not possible: bare Klipy page links on non-Discord services. Klipy's pages sit behind a Cloudflare challenge ("Just a moment..."), which a program cannot pass and which I did not try to get round. They stay links. Klipy GIFs that arrive through Discord are drawn (that is what gifv is), and so are direct links to a Klipy file.

The picker (a GIF button in the composer wherever a Discord account is set up) lists what is trending with categories, and searches. It reads Discord's own two GIF requests through any connected Discord account, only when open, only after a pause in typing, cached for a few minutes. Choosing one sends it: to a Discord conversation, the link, as Discord's own client does (so Discord unfurls it); to any other service, the GIF file, which any client shows as a link and moho draws as the GIF.

Checked: unit tests for Discord's embed, the Tenor and Giphy resolution, the picker's parsing and the link rules. npm run test:gif (new, also in the release workflow) drives the real app: a GIF message is drawn as a looping silent video with the address gone, words around the link stay, a Klipy page stays a link, and the picker opens in view, lists, filters, and sends the file to a fake IRC server, read back off the wire.

Not checked against the real thing: I have not seen Discord's live /gifs/trending and /gifs/search answers (the daemon that has a Discord account is yours, and picks this up on restart), so the picker's parsing is written to the shape Discord's client uses and tolerates variations; if the picker comes up empty or says it failed, the daemon log will say what came back and I will fix it. A real Klipy or Tenor GIF arriving through Discord has also not been seen; the gifv shape is covered by a test built from one.

Finished: [55248eb](https://git.salastil.com/Salastil/moho/commit/55248eb5834f4dc3c496703924e7dc931f859545). Before, the same three messages (a Tenor, a Giphy and a Klipy link): ![before](https://git.salastil.com/attachments/890b7cb2-9e7b-4a67-8158-3cea6deadffb) After: ![after](https://git.salastil.com/attachments/ffb0edc0-48e7-4403-8ceb-bd0f4b761b01) The picker: ![the GIF picker](https://git.salastil.com/attachments/a10e38a9-5f45-4623-aaf9-6a064270e003) **Why a Klipy GIF showed as its address.** Discord sends a posted GIF with an embed of type `gifv` carrying the video. The daemon skipped any embed whose source link was already in the message text (a rule meant for YouTube), so the GIF was dropped and only the link showed. A `gifv` is now kept as a GIF and played from Discord's own proxied copy, so reading a message does not tell Tenor or Klipy who read it (the arrangement Discord's client makes). **On every service.** For everything that is not Discord, the daemon now reads the page itself, the way it does for YouTube cards, through the account's own route (Tor where the account is): Tenor from the page's OpenGraph tags, Giphy from the link's own id with no request at all. A GIF is drawn as a picture - silent, looping, no player controls - using the existing autoplay setting (off, it plays while the pointer is over it), and the page address it came from is not shown as well. Pages of these sites are never probed by the window for content type. **Not possible: bare Klipy page links on non-Discord services.** Klipy's pages sit behind a Cloudflare challenge ("Just a moment..."), which a program cannot pass and which I did not try to get round. They stay links. Klipy GIFs that arrive through Discord are drawn (that is what `gifv` is), and so are direct links to a Klipy file. **The picker** (a GIF button in the composer wherever a Discord account is set up) lists what is trending with categories, and searches. It reads Discord's own two GIF requests through any connected Discord account, only when open, only after a pause in typing, cached for a few minutes. Choosing one sends it: to a Discord conversation, the link, as Discord's own client does (so Discord unfurls it); to any other service, the GIF file, which any client shows as a link and moho draws as the GIF. **Checked:** unit tests for Discord's embed, the Tenor and Giphy resolution, the picker's parsing and the link rules. `npm run test:gif` (new, also in the release workflow) drives the real app: a GIF message is drawn as a looping silent video with the address gone, words around the link stay, a Klipy page stays a link, and the picker opens in view, lists, filters, and sends the file to a fake IRC server, read back off the wire. **Not checked against the real thing:** I have not seen Discord's live `/gifs/trending` and `/gifs/search` answers (the daemon that has a Discord account is yours, and picks this up on restart), so the picker's parsing is written to the shape Discord's client uses and tolerates variations; if the picker comes up empty or says it failed, the daemon log will say what came back and I will fix it. A real Klipy or Tenor GIF arriving through Discord has also not been seen; the `gifv` shape is covered by a test built from one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#331