# Architecture A deep-dive into how PresenceJam works under the hood. ## Overview PresenceJam is a Tauri 2 desktop application with: - **Frontend:** Svelte 5 + TypeScript (SPA mode via `@sveltejs/adapter-static`) - **Backend:** Rust (Tauri 2 command handlers + polling thread) - **Storage:** `tauri-plugin-store` for tokens, JSON files for config - **Auth:** Spotify PKCE OAuth 2.0 + Microsoft Teams Device Code flow - **Platform:** Windows + macOS (single-instance enforcement, system tray, DPAPI token encryption on Windows, Keychain on macOS) ## System Diagram ```mermaid graph TD subgraph Frontend ["Frontend (Svelte)"] UI["+page.svelte
Dashboard / Onboarding
Settings / LogViewer"] Stores["Stores
app.ts, config.ts
spotify.ts, teams.ts"] end subgraph Backend ["Backend (Rust / Tauri)"] Commands["commands.rs
invoke handlers"] Polling["polling.rs
sync thread"] SpotifyAPI["spotify.rs
Spotify Web API"] TeamsAPI["teams.rs
Microsoft Graph API"] Tray["tray.rs
system tray"] Menu["menu.rs
application menu"] end subgraph Storage ["Storage"] TokenStore["tauri-plugin-store
tokens.json"] Config["config.json
%APPDATA%\\PresenceJam"] end UI <-->|"invoke / events"| Commands Commands -->|"HTTP"| SpotifyAPI Commands -->|"HTTP"| TeamsAPI Polling -->|"HTTP"| SpotifyAPI Polling -->|"HTTP"| TeamsAPI Commands --> Polling SpotifyAPI -->|"tokens"| TokenStore TeamsAPI -->|"tokens"| TokenStore ``` ## CI/CD Pipeline Releases are automated via GitHub Actions. When a version tag is pushed, the pipeline builds and releases for both platforms: ```mermaid flowchart TD subgraph Trigger["πŸ”” Trigger: git tag v* pushed"] tag["git tag v2.2.0 && git push --tags"] end subgraph Build["πŸ”¨ Build Matrix"] direction LR macos_build["macOS Build
aarch64-apple-darwin"] windows_build["Windows Build
x86_64-pc-windows-msvc"] end subgraph Package["πŸ“¦ Package"] direction LR macos_zip["macOS: .app β†’ .zip"] msi["Windows: .msi"] end subgraph Release["πŸš€ GitHub Release"] release["Auto-created Release
Drafted from tag"] assets["Artifacts attached:
PresenceJam-v2.2.0-macos.dmg
PresenceJam-v2.2.0.msi"] end tag --> Build Build --> Package Package --> Release ``` ### Release Process 1. **Tag Push:** Developer runs `git tag v2.2.0 && git push --tags` 2. **Workflow Trigger:** GitHub Actions detects the `v*` tag pattern 3. **Parallel Build:** macOS and Windows builds run concurrently on GitHub's hosted runners 4. **Artifact Upload:** Build outputs are uploaded as workflow artifacts 5. **Release Creation:** `release-action` creates a GitHub Release and attaches all artifacts ### Workflow File The CI/CD workflow is defined in [`.github/workflows/release.yml`](.github/workflows/release.yml). ## Authentication Flows ### Spotify PKCE OAuth ```mermaid sequenceDiagram actor User participant App participant Spotify as Spotify
Developer Portal participant Browser User->>App: Enter Client ID + Secret App->>Spotify: POST /api/token (code_verifier) Spotify-->>Browser: Open auth URL with code_challenge User->>Browser: Login + Authorize Browser-->>App: Redirect to presencejam://callback?code=XXX App->>Spotify: POST /api/token (code, code_verifier) Spotify-->>App: access_token + refresh_token App->>App: Store tokens via tauri-plugin-store ``` 1. App generates a PKCE `code_verifier` (random 64-byte string) 2. App sends `code_challenge` (SHA256 hash of verifier) to Spotify 3. Spotify returns an auth URL β†’ App opens browser 4. User authorizes β†’ Spotify redirects to `presencejam://callback?code=XXX` 5. App extracts the `code`, sends it + `code_verifier` to Spotify 6. Spotify returns `access_token` + `refresh_token` β†’ stored via `tauri-plugin-store` ### Microsoft Teams Device Code Flow ```mermaid sequenceDiagram actor User participant App participant Microsoft as Microsoft
login.microsoftonline.com participant Teams as Microsoft Graph API User->>App: Click "Sign in with Microsoft" App->>Microsoft: POST /devicecode Microsoft-->>App: user_code + verification_url App->>User: Display code + URL User->>Browser: Visit verification_url, enter code User->>Microsoft: Enter code in browser loop Poll every 5s App->>Microsoft: POST /token (device_code) Note over Microsoft: authorization_pending end Microsoft-->>App: access_token + refresh_token App->>Teams: PATCH /me/presence/setStatusMessage Teams-->>App: 204 No Content ``` The app polls Microsoft's token endpoint every 5 seconds while the user completes the browser auth. Once authorized, tokens are stored and the status message is set via Graph API. ## Startup Loading On app launch, PresenceJam loads saved config and tokens from persistent storage into `AppState`: ```mermaid sequenceDiagram participant App as Tauri App participant Config as Config Module participant Polling as Polling Module participant Store as tauri-plugin-store App->>App: app.manage(state) App->>Config: load_config() Config-->>App: AppConfig App->>App: state.config = Some(cfg) App->>Polling: restore_pending_spotify_auth() Polling->>Store: get("spotify_*") Store-->>Polling: pending auth | None Polling-->>App: Some(PendingSpotifyAuth) | None App->>App: state.pending_spotify_auth = Some(auth) Note over App: deep-link callback can now succeed App->>Polling: load_spotify_tokens() Polling->>Store: get("spotify_tokens") Store-->>Polling: SpotifyTokens | None Polling-->>App: Some(tokens) | None App->>App: state.spotify_tokens = Some(tokens) App->>Polling: load_teams_tokens() Polling->>Store: get("teams_tokens") Store-->>Polling: TeamsTokens | None Polling-->>App: Some(tokens) | None App->>App: state.teams_tokens = Some(tokens) ``` This means: - **On first launch**, the app starts fresh and requires onboarding - **On subsequent launches**, the app auto-connects if valid tokens exist - **If app restarts during Spotify OAuth**, pending auth state is restored so the callback succeeds - **Reconnect** clears tokens from both memory and store, forcing re-auth ## Reconnect Flow When a user clicks "Reconnect" in Settings, the app clears auth state and triggers re-authentication: ```mermaid sequenceDiagram actor User participant UI as Settings UI participant Commands as Tauri Commands participant Polling as Polling Module participant Store as tauri-plugin-store User->>UI: Click Spotify reconnect UI->>Commands: invoke("reconnect_spotify") Commands->>Commands: state.spotify_tokens = None Commands->>Commands: state.pending_spotify_auth = None Commands->>Polling: clear_spotify_tokens() Polling->>Store: delete("spotify_tokens") Polling->>Store: delete("spotify_client_id, client_secret, etc.") Commands->>UI: emit("spotify-reconnect-required") UI->>User: Show re-authentication flow ``` ### Commands | Command | Action | |---------|--------| | `reconnect_spotify` | Clears Spotify tokens from state and store, emits `spotify-reconnect-required` event | | `reconnect_teams` | Clears Teams tokens from state and store, emits `teams-reconnect-required` event | ## Polling Loop ```mermaid flowchart TD START[Start Syncing] --> TOKEN_CHECK{Spotify Tokens
Available?} TOKEN_CHECK -->|No| WAIT_30[Sleep 30s] --> TOKEN_CHECK TOKEN_CHECK -->|Yes| SPOTIFY_EXPIRED{Spotify Token
Expired?} SPOTIFY_EXPIRED -->|Yes| REFRESH_SPOTIFY[Refresh Spotify Token] --> TRACK_POLL SPOTIFY_EXPIRED -->|No| TRACK_POLL[Poll Spotify
/me/player/currently-playing] TRACK_POLL --> TRACK_CHANGED{Track
Changed?} TRACK_CHANGED -->|No| SLEEP_SMART[Smart Sleep
remaining - 5s buffer] TRACK_CHANGED -->|Yes| FORMAT_STATUS[Format Status
from template] FORMAT_STATUS --> PROFANITY_CHECK{Profanity
Filter
Enabled?} PROFANITY_CHECK -->|Yes| FILTER_PROFANITY[Filter Status
replace profane
with placeholder] PROFANITY_CHECK -->|No| TEAMS_TOKEN_CHECK FILTER_PROFANITY --> TEAMS_TOKEN_CHECK{Teams Token
Expired?} TEAMS_TOKEN_CHECK -->|Yes| REFRESH_TEAMS[Refresh Teams Token] --> UPDATE_TEAMS TEAMS_TOKEN_CHECK -->|No| UPDATE_TEAMS[Set Teams Status
with final message] UPDATE_TEAMS --> SLEEP_SMART SLEEP_SMART --> TOKEN_CHECK TRACK_POLL --> NO_TRACK{No Track
Playing?} NO_TRACK -->|Yes| CLEAR_CHECK{clear_on_pause?} CLEAR_CHECK -->|Yes| CLEAR_STATUS[Clear Teams Status] CLEAR_CHECK -->|No| PRESERVE_STATUS[Preserve Status] CLEAR_STATUS --> SLEEP_30[Sleep 30s] --> TOKEN_CHECK PRESERVE_STATUS --> SLEEP_30 NO_TRACK -->|No| TRACK_CHANGED ``` ### Profanity Filter The `profanity.rs` module screens the formatted status before it is sent to Teams. If profanity is detected, the status is replaced with a safe placeholder rather than exposing the profane content. **How it works:** 1. `format_status()` produces the status string from the track template 2. If `config.teams.profanity_filter` is enabled, `filter_status()` is called 3. `contains_profanity()` normalizes the text and checks against a 25-word curated list 4. If matched, the placeholder (with `{emoji}` resolved to 🎡 or ⏸️) replaces the status 5. The replacement is logged, but the original profane content is **never written to logs** **Detection features:** - Leetspeak normalization (1β†’i, 3β†’e, $β†’s, @β†’a, 0β†’o, 5β†’s, 7β†’t, !β†’i, |β†’i) - Repeated-character collapse (shiiit β†’ shiit, but not excessive repeats) - Word-boundary checks prevent false positives (class, assassin, cocktail, vacuum) - Special handling: "fucking", "fucked", "fucker" are detected as profane variants - Safe suffix words (tail, head, hand, etc.) allow compound words without false positives **TODO:** Currently filters the formatted status string. A future refactor should filter raw Spotify fields (track, artist, album) before formatting to prevent placeholder injection via custom templates with `{emoji}`. ### Smart Sleep Logic When a track is playing, the app calculates the exact time remaining until the track ends: ```rust let remaining_ms = track.duration_ms - track.progress_ms; let buffer_ms = 5000u64; // 5 second buffer let sleep_secs = (remaining_ms / 1000).saturating_sub(buffer_ms / 1000); sleep_secs.max(MINIMUM_INTERVAL_SECONDS).min(MAX_INTERVAL_SECONDS) ``` This means: - **No API calls** while you're listening to a 4-minute track (~240 seconds of silence) - **Polling resumes immediately** when the track changes - **Minimum 10 seconds** between polls (configurable) ## Event Bus The Rust backend communicates with the Svelte frontend via Tauri events: ```mermaid sequenceDiagram participant Polling participant App as Rust AppHandle participant Frontend as Svelte Polling->>App: emit("spotify-track-changed", trackInfo) App->>Frontend: listen("spotify-track-changed") Polling->>App: emit("presence-updated", status) App->>Frontend: listen("presence-updated") Polling->>App: emit("presence-cleared", {}) App->>Frontend: listen("presence-cleared") Polling->>App: emit("error", errorInfo) App->>Frontend: listen("error") ``` | Event | Payload | Triggered When | |-------|---------|---------------| | `spotify-track-changed` | `TrackInfo` | New track detected or track state changed | | `presence-updated` | `{status, timestamp}` | Teams status successfully updated | | `presence-cleared` | `{timestamp}` | Teams status cleared | | `error` | `{source, message}` | Any API error (Spotify, Teams, or auth) | | `spotify-reconnect-required` | `null` | Spotify token expired or auth failure requiring re-auth | | `teams-reconnect-required` | `null` | Teams token expired or auth failure requiring re-auth | | `reconnect-required` | `null` | Transient failure retry limit exhausted, polling loop exiting | | `polling-thread-panicked` | `null` | Polling thread panicked and was caught by `catch_unwind` | | `tray-click` | β€” | User clicks tray icon | | `toggle-pause` | β€” | User clicks Pause in tray menu | ## Deep Link Routing PresenceJam registers a custom URL scheme to handle OAuth callbacks: | Scheme | Used For | |--------|----------| | `presencejam://callback` | Spotify PKCE OAuth redirect | | `presencejam://teams-callback` | Teams auth (reserved for future use) | The app's `lib.rs` intercepts these URLs via Tauri's `deep-link` plugin and routes them to the appropriate handler. ## Directory Structure ``` PresenceJam-Desktop/ β”œβ”€β”€ src/ # Svelte frontend β”‚ β”œβ”€β”€ lib/ β”‚ β”‚ β”œβ”€β”€ components/ β”‚ β”‚ β”‚ β”œβ”€β”€ Dashboard.svelte # Main sync view β”‚ β”‚ β”‚ β”œβ”€β”€ Onboarding.svelte # 3-step auth wizard β”‚ β”‚ β”‚ β”œβ”€β”€ Settings.svelte # Config editor β”‚ β”‚ β”‚ └── LogViewer.svelte # In-app log viewer β”‚ β”‚ β”œβ”€β”€ stores/ β”‚ β”‚ β”‚ β”œβ”€β”€ app.ts # currentView, isSyncing, appError β”‚ β”‚ β”‚ β”œβ”€β”€ config.ts # configStore, saveConfig β”‚ β”‚ β”‚ β”œβ”€β”€ spotify.ts # Spotify token state β”‚ β”‚ β”‚ └── teams.ts # Teams token state β”‚ β”‚ └── utils/ β”‚ β”‚ └── dev.ts # devLog() conditional logger β”‚ └── routes/ β”‚ └── +page.svelte # SPA entry, routes to views β”œβ”€β”€ src-tauri/ β”‚ β”œβ”€β”€ src/ β”‚ β”‚ β”œβ”€β”€ lib.rs # Tauri entry, command registration β”‚ β”‚ β”œβ”€β”€ commands.rs # All invoke() command handlers β”‚ β”‚ β”œβ”€β”€ config.rs # JSON config load/save, AppConfig struct β”‚ β”‚ β”œβ”€β”€ polling.rs # Polling loop (thread::spawn) β”‚ β”‚ β”œβ”€β”€ profanity.rs # Profanity filter module β”‚ β”‚ β”œβ”€β”€ spotify.rs # Spotify Web API client β”‚ β”‚ β”œβ”€β”€ teams.rs # Microsoft Graph API client β”‚ β”‚ β”œβ”€β”€ tray.rs # System tray setup β”‚ β”‚ └── menu.rs # Application menu bar β”‚ β”œβ”€β”€ Cargo.toml # Rust dependencies β”‚ β”œβ”€β”€ tauri.conf.json # Tauri 2 config (window, deep-link, plugins) β”‚ └── capabilities/ β”‚ └── default.json # Permission grants (store, http, deep-link, etc.) β”œβ”€β”€ package.json # Node dependencies β”œβ”€β”€ svelte.config.js # SvelteKit SPA config (adapter-static) β”œβ”€β”€ vite.config.js # Vite/Tauri dev server config └── tsconfig.json # TypeScript config ``` ## State Management ### Rust State (`AppState`) ```rust pub struct AppState { pub spotify_tokens: RwLock>, pub teams_tokens: RwLock>, pub pending_spotify_auth: RwLock>, pub config: RwLock>, pub current_track: RwLock>, pub is_syncing: RwLock, } ``` Multiple threads access this shared state via `RwLock`: - `polling.rs` thread writes to `spotify_tokens`, `teams_tokens`, `current_track`, `is_syncing` - `commands.rs` handlers read/write via `tauri::State` ### Frontend Stores | Store | Type | Purpose | |-------|------|---------| | `currentView` | `'onboarding' \| 'dashboard' \| 'settings' \| 'logs'` | Active view | | `isSyncing` | `boolean` | Sync running/paused | | `appError` | `string \| null` | Current error message | | `configStore` | `AppConfig` | Full app config | | `currentTrack` (spotify.ts) | `TrackInfo \| null` | Currently playing track (managed as local component state) |