Direct media access (v2)

New in version 0.7.0

To avoid spamming your homeserver's media repository with all files from Discord, the bridge has an option to generate fake mxc:// URIs that contain the Discord message ID and some other info. To make the URIs work, the bridge effectively turns into a media-only homeserver.

For example, if your main Matrix server is example.com, you could set up discord-media.example.com as the bridge's server name. The way to do that is to either proxy the subdomain to the bridge entirely, or create a .well-known/matrix/server file (just like normal homeserver setups) pointing somewhere else where the bridge is listening.

When proxying the subdomain entirely, port 443 is enough, the bridge will automatically serve a .well-known to redirect from port 8448 to 443.

Endpoints that the bridge can handle with direct media access:

  • /_matrix/federation/v1/media/download/*
  • /_matrix/client/v1/media/*
  • /_matrix/media/* (legacy, pre-MSC3916)
  • /_matrix/key/*
  • /_matrix/federation/v1/version
  • /.well-known/matrix/server

To enable direct media access:

  • Set bridge -> direct_media -> enabled to true.
  • Change the server_name field to a (sub)domain where the aforementioned endpoints are proxied to the bridge.
  • Ensure server_key is set.
    • The bridge generates one automatically on startup and writes it back to the config. If you prevented the bridge from writing the config, you'll have to set it yourself so it wouldn't generate a new one on every restart.

Legacy direct media access

In the past, the bridge supported specifying templates that a simple reverse proxy could parse to redirect the request to Discord's CDN. However, that method no longer works since Discord started requiring signed URLs that expire.

Old docs

New in version 0.4.0

To avoid spamming your homeserver's media repository with all files from Discord, the bridge has an option to generate fake mxc:// URIs that contain the Discord media ID. The media repo or your reverse proxy can then handle those URIs specially to fetch content directly from the Discord CDN.

To enable this mode, set bridge -> media_patterns -> enabled to true in the bridge config. You can then configure each of the patterns or leave the defaults.

Ways to use patterns

Default pattern and MSC3860-compatible media repo

If your media repo supports MSC3860, you can use the default patterns out of the box with no modifications. Your media repo will act like discord-media.mau.dev is a federated Matrix server, so when your client requests Discord media, your media repo will ask discord-media.mau.dev, which redirects to cdn.discordapp.com. Your media repo then downloads the media and caches it as remote media (like it does for all federated media).

MSC3860 is supported as of Synapse v1.98.0 and matrix-media-repo v1.4.0. Additionally, while Conduit doesn't opt into redirects, it does follow them, so it should work with the default config.

The software on discord-media.mau.dev is just a Caddy instance with the first example config below, plus a static .well-known file to redirect federation to 443. You can find the raw config at mau.dev/maunium/caddy.

Redirect in reverse proxy

You can also configure your reverse proxy to redirect mxc://discord-media.mau.dev/* downloads directly to cdn.discordapp.com. This method doesn't involve your media repo at all, so it already works with most clients. However, it won't work with servers that don't support MSC3860, as they'd still try to connect to discord-media.mau.dev, which may be a problem if you want to use your bridge in federated rooms. Additionally, you may encounter some CORS issues with this method as cdn.discordapp.com doesn't provide CORS headers for all files (like webp images and non-inline documents)

Caddy config example
matrix.example.com {
	handle /_matrix/media/*/download/discord-media.mau.dev/* {
		# The redirect must have CORS headers to let web clients follow it.
		header Access-Control-Allow-Origin *
		# Need to use a route directive to make the uri mutations apply before redir
		route {
			# Remove path prefix
			uri path_regexp ^/_matrix/media/.+/download/discord-media\.mau\.dev/ /
			# The mxc patterns use | instead of /, so replace it first turning the path into attachments/1234/5678/filename.png
			uri replace "%7C" /
			# Then redirect to cdn.discordapp.com/attachments/1234/5678/filename.png with HTTP 307
			redir https://cdn.discordapp.com{uri} 307
		}
	}
	# Special-case stickers because they don't have CORS headers on cdn.discordapp.com for some reason
	handle /_matrix/media/*/download/discord-media.mau.dev/stickers|* {
		header Access-Control-Allow-Origin *
		route {
			uri path_regexp ^/_matrix/media/.+/download/discord-media\.mau\.dev/ /
			uri replace "%7C" /
			redir https://media.discordapp.net{uri} 307
		}
	}
	# Do the same for thumbnails, but redirect to media.discordapp.net (which is Discord's thumbnailing server, and happens to use similar width/height params as Matrix)
	# Alternatively, you can point this at cdn.discordapp.com too. Clients shouldn't mind even if they get a bigger image than they asked for.
	handle /_matrix/media/*/thumbnail/discord-media.mau.dev/* {
		header Access-Control-Allow-Origin *
		route {
			uri path_regexp ^/_matrix/media/.+/thumbnail/discord-media\.mau\.dev/ /
			uri replace "%7C" /
			redir https://media.discordapp.net{uri} 307
		}
	}
	# The usual proxying to your homeserver
	handle /_matrix/* {
		reverse_proxy http://localhost:8008
	}
}
Nginx config example
server {
	listen 443;
	server_name matrix.example.com;
	# ... usual /_matrix location block and other stuff ...
	# N.B. If you use a regex pattern for the /_matrix block, it must be below these locations

	location ~ ^/_matrix/media/(?:v3|r0)/download/discord-media.mau.dev/attachments\|([0-9]+)\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		return 307 https://cdn.discordapp.com/attachments/$1/$2/$3;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/download/discord-media.mau.dev/emojis\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		return 307 https://cdn.discordapp.com/emojis/$1;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/download/discord-media.mau.dev/stickers\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		# Stickers don't have CORS headers on cdn.discordapp.com for some reason, so always use media.
		return 307 https://media.discordapp.net/stickers/$1;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/download/discord-media.mau.dev/avatars\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		return 307 https://cdn.discordapp.com/avatars/$1/$2;
	}

	# Thumbnails (optional-ish)
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/discord-media.mau.dev/attachments\|([0-9]+)\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		return 307 https://media.discordapp.net/attachments/$1/$2/$3?$args;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/discord-media.mau.dev/emojis\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		return 307 https://media.discordapp.net/emojis/$1?$args;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/discord-media.mau.dev/stickers\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		return 307 https://media.discordapp.net/stickers/$1?$args;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/discord-media.mau.dev/avatars\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		return 307 https://media.discordapp.net/avatars/$1/$2?$args;
	}
}

Proxy in reverse proxy

If you want bridged media to work over federation without MSC3860, you can change discord-media.mau.dev to your own server name, and have your reverse proxy actually proxy the downloads instead of just redirecting to cdn.discordapp.com. That way it'll work with all existing servers and clients. The downside of this method is the higher bandwidth use compared to redirecting, and theoretical abuse vectors for spamming the Discord CDN through your server.

When using this example, change discord-media.mau.dev/ in the patterns to example.com/discord_ (replacing example.com with your own domain). The discord_ prefix is there so that other media on your domain will still work normally.

Caddy config example
matrix.example.com {
	handle /_matrix/media/*/download/example.com/discord_* {
		header Access-Control-Allow-Origin *
		# Remove path prefix
		uri path_regexp ^/_matrix/media/.+/download/example\.com/discord_ /
		# The mxc patterns use | instead of /, so replace it first turning it into attachments/1234/5678/filename.png
		uri replace "%7C" /
		reverse_proxy {
			# reverse_proxy automatically includes the uri, so no {uri} at the end
			to https://cdn.discordapp.com
			# Caddy doesn't set the Host header automatically when reverse proxying
			# (because usually reverse proxies are local and don't care about Host headers)
			header_up Host cdn.discordapp.com
		}
	}
	# Do the same for thumbnails, but redirect to media.discordapp.net (which is Discord's thumbnailing server, and happens to use similar width/height params as Matrix)
	# Alternatively, you can point this at cdn.discordapp.com too. Clients shouldn't mind even if they get a bigger image than they asked for.
	handle /_matrix/media/*/thumbnail/example.com/discord_* {
		header Access-Control-Allow-Origin *
		uri path_regexp ^/_matrix/media/.+/thumbnail/example\.com/discord_ /
		uri replace "%7C" /
		reverse_proxy {
			to https://media.discordapp.net
			header_up Host media.discordapp.net
		}
	}
	handle /_matrix/* {
		reverse_proxy http://localhost:8008
	}
}
Nginx config example
server {
	listen 443;
	server_name matrix.example.com;
	# ... usual /_matrix location block and other stuff ...
	# N.B. If you use a regex pattern for the /_matrix block, it must be below these locations

	# You may need to configure a resolver for nginx to be able to resolve cdn.discordapp.com
	#resolver 8.8.8.8;

	location ~ ^/_matrix/media/(?:v3|r0)/download/example.com/discord_attachments\|([0-9]+)\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host cdn.discordapp.com;
		proxy_pass https://cdn.discordapp.com/attachments/$1/$2/$3;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/download/example.com/discord_emojis\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host cdn.discordapp.com;
		proxy_pass https://cdn.discordapp.com/emojis/$1;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/download/example.com/discord_stickers\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host cdn.discordapp.com;
		proxy_pass https://cdn.discordapp.com/stickers/$1;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/download/example.com/discord_avatars\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host cdn.discordapp.com;
		proxy_pass https://cdn.discordapp.com/avatars/$1/$2;
	}

	# Thumbnails (optional-ish)
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/example.com/discord_attachments\|([0-9]+)\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host media.discordapp.net;
		proxy_pass https://media.discordapp.net/attachments/$1/$2/$3?$args;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/example.com/discord_emojis\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host media.discordapp.net;
		proxy_pass https://media.discordapp.net/emojis/$1?$args;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/example.com/discord_stickers\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host media.discordapp.net;
		proxy_pass https://media.discordapp.net/stickers/$1?$args;
	}
	location ~ ^/_matrix/media/(?:v3|r0)/thumbnail/example.com/discord_avatars\|([0-9]+)\|(.+)$ {
		add_header Access-Control-Allow-Origin *;
		proxy_set_header Host media.discordapp.net;
		proxy_pass https://media.discordapp.net/avatars/$1/$2?$args;
	}
}