feat(discord-rpc): lazy Flatpak socket bridge keeps the integration opt-in
Discord-Flatpak writes its IPC socket to \$XDG_RUNTIME_DIR/app/com.discordapp.Discord/discord-ipc-N rather than the \$XDG_RUNTIME_DIR/discord-ipc-N path the discord-rich-presence crate searches by default. The naive way to bridge that gap — a startup wrapper that unconditionally symlinks every iteration before the binary launches — works, but it pulls the integration off-script: every other optional integration in QBZ (Last.fm, ListenBrainz, MusicBrainz, Plex) is opt-in, with zero telemetry and zero filesystem reach until the user flips the toggle. A startup wrapper would silently turn Discord RPC into opt-out at the sandbox layer; the symlinks would exist for users who never touched the integrations panel. Move the bridge into the lazy connect path instead. The first time the user actually connects — i.e. the first track plays after they enabled the toggle — prepare_flatpak_discord_socket_links runs: * No-op outside Flatpak (no FLATPAK_ID env var) * No-op when \$XDG_RUNTIME_DIR/discord-ipc-N already exists (native Discord, or a previous lazy run in this Flatpak instance) * No-op when the Discord-Flatpak source path doesn't exist (Discord not running) * Otherwise, symlinks 0..9 from \$XDG_RUNTIME_DIR/discord-ipc-N to \$XDG_RUNTIME_DIR/app/com.discordapp.Discord/discord-ipc-N Symlinks die with the Flatpak instance — \$XDG_RUNTIME_DIR is per- instance, so there's no cross-session pollution. Users who never enable Discord RPC keep the same sandbox surface as before. Manifest: keep the static xdg-run/discord-ipc-0 permission for native Discord, add xdg-run/app/com.discordapp.Discord:create so the lazy symlink path is allowed when the user opts in. The two coexist; the crate's default search path covers Discord-native, the symlinks cover Discord-Flatpak. cc
This commit is contained in:
2 files changed
+73
-1
No files matched your search
@@ -13,6 +13,64 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use discord_rich_presence::{activity, DiscordIpc, DiscordIpcClient};
|
||||
use tauri::State;
|
||||
|
||||
/// Bridge our Flatpak sandbox to the Discord-Flatpak IPC socket.
|
||||
///
|
||||
/// The `discord-rich-presence` crate searches `$XDG_RUNTIME_DIR/discord-ipc-N`
|
||||
/// for `N=0..9`. Native Discord (deb / rpm / AUR) writes its socket directly
|
||||
/// to that path, so the search succeeds out of the box. Discord installed via
|
||||
/// Flathub writes to `$XDG_RUNTIME_DIR/app/com.discordapp.Discord/discord-ipc-N`
|
||||
/// because each Flatpak app gets its own runtime subdirectory; the crate's
|
||||
/// default search misses it, and Rich Presence appears broken even though
|
||||
/// Discord is running.
|
||||
///
|
||||
/// Other Flathub apps work around this with a startup wrapper script that
|
||||
/// unconditionally symlinks every iteration into `$XDG_RUNTIME_DIR` for every
|
||||
/// user. That conflicts with QBZ's external-service-integration posture: all
|
||||
/// optional integrations are opt-in (Last.fm, ListenBrainz, MusicBrainz, Plex,
|
||||
/// Discord here). Running the wrapper for every Flatpak instance — including
|
||||
/// users who never enabled Discord RPC — silently turns the integration into
|
||||
/// opt-out at the sandbox layer.
|
||||
///
|
||||
/// We make the symlinks here instead, lazily, the first time the user
|
||||
/// actually connects to Discord. If the user never toggles Discord RPC on,
|
||||
/// these links are never created and the sandbox stays untouched. The links
|
||||
/// live for the lifetime of the Flatpak instance (`$XDG_RUNTIME_DIR` is
|
||||
/// scoped per-instance) so there's no cross-session pollution either.
|
||||
///
|
||||
/// Returns silently on every failure path — missing env var, non-Flatpak
|
||||
/// process, ENOENT on the source dir, EEXIST on the link target, etc. Any
|
||||
/// caller mistake fails over to the crate's default behavior, which is the
|
||||
/// same fallback the non-Flatpak code path already accepts.
|
||||
fn prepare_flatpak_discord_socket_links() {
|
||||
if std::env::var_os("FLATPAK_ID").is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR") else {
|
||||
return;
|
||||
};
|
||||
let runtime_dir = std::path::PathBuf::from(runtime_dir);
|
||||
|
||||
// discord-rich-presence iterates 0..=9; only bother creating a link when
|
||||
// the slot in $XDG_RUNTIME_DIR is currently empty AND the corresponding
|
||||
// path inside the Discord-Flatpak app dir exists. Either condition
|
||||
// failing is normal (Discord not running, slot already taken by native
|
||||
// Discord, etc.) and silently skipped.
|
||||
for i in 0..=9 {
|
||||
let link_target = runtime_dir.join(format!("discord-ipc-{}", i));
|
||||
if link_target.exists() || link_target.symlink_metadata().is_ok() {
|
||||
continue;
|
||||
}
|
||||
let source = runtime_dir
|
||||
.join("app")
|
||||
.join("com.discordapp.Discord")
|
||||
.join(format!("discord-ipc-{}", i));
|
||||
if !source.exists() {
|
||||
continue;
|
||||
}
|
||||
let _ = std::os::unix::fs::symlink(&source, &link_target);
|
||||
}
|
||||
}
|
||||
|
||||
/// Public Discord Application ID for QBZ. Public identifier, not a secret.
|
||||
const DISCORD_APP_ID: &str = "1501835855587708988";
|
||||
const QBZ_HOMEPAGE: &str = "https://qbz.lol";
|
||||
@@ -69,6 +127,9 @@ pub fn v2_discord_rpc_update(
|
||||
let mut guard = state.client.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
if guard.is_none() {
|
||||
// Bridge the Flatpak sandbox to Discord's IPC socket if we're
|
||||
// running under Flatpak. No-op outside that context.
|
||||
prepare_flatpak_discord_socket_links();
|
||||
if let Ok(mut client) = DiscordIpcClient::new(DISCORD_APP_ID) {
|
||||
if client.connect().is_ok() {
|
||||
*guard = Some(client);
|
||||
|
||||
Reference in new issue
Block a user