Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 87 additions & 33 deletions electron/main/core/discord.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Client } from 'discord-rpc'
import { productName } from '../../../package.json'
import { execSync } from 'child_process'

const ActivityType = {
Game: 0,
Playing: 0,
Streaming: 1,
Listening: 2,
Watching: 3,
Expand Down Expand Up @@ -35,71 +36,124 @@ export type PayloadType = {
export const DEFAULT_LARGE_IMAGE = 'icon'
export const DEFAULT_SMALL_IMAGE = 'song_icon'

const defaultPayload: PayloadType = {
pid: process.pid,
activity: {
timestamps: {
start: Date.now(),
},
details: productName,
assets: {
large_image: DEFAULT_LARGE_IMAGE,
small_image: DEFAULT_SMALL_IMAGE,
},
instance: true,
type: ActivityType.Listening,
},
}

let discord: Client | null = null
let lastPayload: PayloadType | null = null
let lastActivity: IActivity | null = null
let spoofPid: number | null = null
let refreshInterval: NodeJS.Timeout | null = null

function getSpoofPid(): number {
if (spoofPid) return spoofPid
try {
const stdout = execSync('pgrep -f vesktop || pgrep -f Vesktop || pgrep -f discord || pgrep -f Discord').toString()
const pids = stdout.split('\n').map(p => parseInt(p.trim())).filter(p => !isNaN(p))
if (pids.length > 0) {
spoofPid = pids[0]
return spoofPid
}
} catch (e) {}
return process.pid
}

function init() {
if (discord) return

console.log('Initializing Discord RPC...')
discord = new Client({ transport: 'ipc' })

discord.on('ready', async () => {
// @ts-expect-error raw request
discord.request('SET_ACTIVITY', lastPayload ?? defaultPayload)
console.log('Discord RPC ready. User:', discord?.user?.username)
if (lastActivity) {
updateStatus()
}

if (refreshInterval) clearInterval(refreshInterval)
refreshInterval = setInterval(() => {
if (lastActivity) updateStatus()
}, 15000)
})

discord.on('disconnected', () => {
setTimeout(() => loginRPC(), 1000).unref()
console.log('Discord RPC disconnected.')
if (refreshInterval) clearInterval(refreshInterval)
refreshInterval = null
discord = null
setTimeout(() => init(), 1000).unref()
})
loginRPC()
}

function loginRPC() {
if (!discord) return

const DISCORD_CLIENT_ID = import.meta.env.MAIN_VITE_DISCORD_CLIENT_ID

if (!DISCORD_CLIENT_ID) {
console.log('Discord Client ID not found.')
console.warn('Discord Client ID not found in environment variables.')
return
}

discord.login({ clientId: DISCORD_CLIENT_ID }).catch(() => {
console.log('Logging in to Discord RPC with Client ID:', DISCORD_CLIENT_ID)
discord.login({ clientId: DISCORD_CLIENT_ID }).catch((err) => {
console.error('Discord RPC login failed:', err.message)
setTimeout(() => loginRPC(), 5000).unref()
})
}

function set(data: IActivity | null) {
if (!discord || !discord.user) return
function updateStatus() {
if (!discord || !discord.user || !lastActivity) return

const activity: any = {
details: lastActivity.details,
state: lastActivity.state,
instance: true,
type: 2, // Listening
}

// Convert milliseconds to seconds for Discord
if (lastActivity.timestamps) {
activity.timestamps = {}
if (lastActivity.timestamps.start) {
activity.timestamps.start = lastActivity.timestamps.start > 1000000000000
? Math.floor(lastActivity.timestamps.start / 1000)
: lastActivity.timestamps.start
}
if (lastActivity.timestamps.end) {
activity.timestamps.end = lastActivity.timestamps.end > 1000000000000
? Math.floor(lastActivity.timestamps.end / 1000)
: lastActivity.timestamps.end
}
}

if (data) {
data.instance = true
data.type = ActivityType.Listening
// Assets (Images) - Using snake_case for raw wire protocol
activity.assets = {
large_image: lastActivity.assets?.large_image || DEFAULT_LARGE_IMAGE,
large_text: lastActivity.assets?.large_text || productName,
small_image: DEFAULT_SMALL_IMAGE,
small_text: lastActivity.assets?.small_text || 'Playing'
}

const payload = {
pid: process.pid,
activity: data,
pid: getSpoofPid(),
activity,
}
lastPayload = payload

console.log('Discord RPC Payload:', JSON.stringify(payload, null, 2))

// @ts-expect-error raw request
discord.request('SET_ACTIVITY', payload)
discord.request('SET_ACTIVITY', payload).catch((err) => {
console.error('Error in SET_ACTIVITY:', err)
})
}

function set(data: IActivity | null) {
lastActivity = data

if (!discord || !discord.user) {
if (!discord) init()
return
}

updateStatus()
}

export const RPC = {
Expand Down
13 changes: 10 additions & 3 deletions electron/main/core/discordRpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ export type RpcPayload = {
startTime: number
endTime: number
duration: number
imageUrl?: string
}

function truncate(str: string, max = 128) {
return str.length > max ? `${str.slice(0, max - 3)}...` : str
}

export async function setDiscordRpcActivity(payload: RpcPayload) {
try {
RPC.init()
RPC.set({
details: payload.trackName,
state: `${payload.artist} • ${payload.albumName}`,
details: truncate(payload.trackName),
state: truncate(`${payload.artist} • ${payload.albumName}`),
timestamps: {
start: payload.startTime,
end: payload.endTime,
},
assets: {
large_image: DEFAULT_LARGE_IMAGE,
large_image: payload.imageUrl || DEFAULT_LARGE_IMAGE,
large_text: truncate(payload.albumName),
small_image: DEFAULT_SMALL_IMAGE,
small_text: truncate(payload.trackName),
},
})
} catch {}
Expand Down
61 changes: 61 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

112 changes: 112 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
description = "Aonsoku development environment";

inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
utils.url = "github:numtide/flake-utils";
};

outputs = { self, nixpkgs, utils }:
utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };

deps = with pkgs; [
# System tools
nodejs_20
pnpm
electron
patchelf

# Electron dependencies
alsa-lib
at-spi2-core
atk
cairo
cups
dbus
expat
fontconfig
freetype
gdk-pixbuf
glib
gtk3
libGL
libglvnd
libgbm # Specifically adding libgbm
libappindicator-gtk3
libdrm
libnotify
libpulseaudio
libuuid
libva
libx11
libxcb
libxcomposite
libxcursor
libxdamage
libxext
libxfixes
libxi
libxkbcommon
libxrandr
libxrender
libxshmfence
libxtst
mesa
nspr
nss
pango
pipewire
systemd
vulkan-loader
wayland
];
in
{
devShells.default = pkgs.mkShell {
buildInputs = deps;

shellHook = ''
# Create the library path
export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath deps}:$LD_LIBRARY_PATH

# Default Discord Client ID for Aonsoku
export MAIN_VITE_DISCORD_CLIENT_ID="1215413348007612476"

# Force electron to use nix-provided one
export ELECTRON_OVERRIDE_DIST_PATH="${pkgs.electron}/bin"
if [ ! -f "$ELECTRON_OVERRIDE_DIST_PATH/electron" ]; then
export ELECTRON_OVERRIDE_DIST_PATH="${pkgs.electron}/libexec/electron"
fi

# Patch node_modules binary as a fallback
ELECTRON_BIN="./node_modules/electron/dist/electron"
if [ -f "$ELECTRON_BIN" ]; then
echo "Applying NixOS patch to: $ELECTRON_BIN"
chmod +w "$ELECTRON_BIN"

# Get the dynamic linker path
DYNAMIC_LINKER="$(cat $NIX_CC/nix-support/dynamic-linker)"

# Patch it!
patchelf --set-interpreter "$DYNAMIC_LINKER" \
--set-rpath "$LD_LIBRARY_PATH:\$ORIGIN" \
"$ELECTRON_BIN"

# Also patch the helper binaries if they exist
for helper in "./node_modules/electron/dist/chrome-sandbox" "./node_modules/electron/dist/electron_helper"; do
if [ -f "$helper" ]; then
chmod +w "$helper"
patchelf --set-interpreter "$DYNAMIC_LINKER" --set-rpath "$LD_LIBRARY_PATH" "$helper" 2>/dev/null || true
fi
done
echo "Patching complete."
fi

echo "Aonsoku dev shell loaded."
echo "System Electron: $(which electron)"
'';
};
});
}
Loading