Add native self-hosted instance connection to fluxer_desktop

Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 +
tools/ci) with a "Connect to a Different Server" menu item and popout
that lets the desktop app switch to any self-hosted Fluxer instance,
plus fixes for well-known discovery on single-domain self-hosted
deployments and a false-positive ERR_ABORTED on same-origin client
redirects during the switch. Defaults to chat.fluxr.chat and uses an
isolated userData directory from the official build.
This commit is contained in:
2026-07-01 18:22:43 -04:00
commit 682afacd30
1763 changed files with 613720 additions and 0 deletions
@@ -0,0 +1,643 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pub fn push_json_string(out: &mut String, value: &str) {
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0C}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
}
pub enum JsonValue {
Str(String),
Raw(String),
}
pub fn json_object(fields: &[(&str, JsonValue)]) -> String {
let mut out = String::from("{");
for (i, (key, value)) in fields.iter().enumerate() {
if i > 0 {
out.push(',');
}
push_json_string(&mut out, key);
out.push(':');
match value {
JsonValue::Str(s) => push_json_string(&mut out, s),
JsonValue::Raw(r) => out.push_str(r),
}
}
out.push('}');
out
}
pub fn json_string_array(items: &[String]) -> String {
let mut out = String::from("[");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
push_json_string(&mut out, item);
}
out.push(']');
out
}
pub fn json_u8_array(items: &[u8]) -> String {
let mut out = String::from("[");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&item.to_string());
}
out.push(']');
out
}
pub fn json_raw_array(items: &[String]) -> String {
let mut out = String::from("[");
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(item);
}
out.push(']');
out
}
pub fn json_string_map(items: &std::collections::HashMap<String, String>) -> String {
let mut entries: Vec<(&String, &String)> = items.iter().collect();
entries.sort_by(|left, right| left.0.cmp(right.0));
let mut out = String::from("{");
for (i, (key, value)) in entries.iter().enumerate() {
if i > 0 {
out.push(',');
}
push_json_string(&mut out, key);
out.push(':');
push_json_string(&mut out, value);
}
out.push('}');
out
}
#[cfg(feature = "publisher")]
mod live {
use super::{
JsonValue, json_object, json_raw_array, json_string_array, json_string_map, json_u8_array,
};
use livekit::participant::{
ConnectionQuality, LocalParticipant, Participant, RemoteParticipant,
};
use livekit::publication::{
LocalTrackPublication, RemoteTrackPublication, SubscriptionStatus, TrackPublication,
};
use livekit::track::{TrackKind, TrackSource};
use livekit::{DataPacketKind, RoomEvent};
pub fn track_kind_str(kind: TrackKind) -> &'static str {
match kind {
TrackKind::Audio => "audio",
TrackKind::Video => "video",
}
}
pub fn track_source_str(source: TrackSource) -> &'static str {
match source {
TrackSource::Unknown => "unknown",
TrackSource::Camera => "camera",
TrackSource::Microphone => "microphone",
TrackSource::Screenshare => "screen_share",
TrackSource::ScreenshareAudio => "screen_share_audio",
}
}
pub fn connection_quality_str(quality: ConnectionQuality) -> &'static str {
match quality {
ConnectionQuality::Excellent => "excellent",
ConnectionQuality::Good => "good",
ConnectionQuality::Poor => "poor",
ConnectionQuality::Lost => "lost",
}
}
fn s(value: impl Into<String>) -> JsonValue {
JsonValue::Str(value.into())
}
fn b(value: bool) -> JsonValue {
JsonValue::Raw(value.to_string())
}
fn subscription_status_str(status: SubscriptionStatus) -> &'static str {
match status {
SubscriptionStatus::Desired => "desired",
SubscriptionStatus::Subscribed => "subscribed",
SubscriptionStatus::Unsubscribed => "unsubscribed",
}
}
fn participant_snapshot(participant: &Participant) -> String {
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
])
}
fn push_remote_participant_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
participant: &RemoteParticipant,
) {
fields.push(("participantSid", s(participant.sid().to_string())));
fields.push(("identity", s(participant.identity().to_string())));
fields.push(("participantName", s(participant.name())));
}
fn push_local_participant_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
participant: &LocalParticipant,
) {
fields.push(("participantSid", s(participant.sid().to_string())));
fields.push(("identity", s(participant.identity().to_string())));
fields.push(("participantName", s(participant.name())));
}
fn push_participant_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
participant: &Participant,
) {
fields.push(("participantSid", s(participant.sid().to_string())));
fields.push(("identity", s(participant.identity().to_string())));
fields.push(("participantName", s(participant.name())));
}
fn push_remote_publication_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
publication: &RemoteTrackPublication,
) {
fields.push(("trackSid", s(publication.sid().to_string())));
fields.push(("trackName", s(publication.name())));
fields.push(("kind", s(track_kind_str(publication.kind()))));
fields.push(("source", s(track_source_str(publication.source()))));
fields.push(("muted", b(publication.is_muted())));
fields.push(("subscribed", b(publication.is_subscribed())));
fields.push((
"subscriptionStatus",
s(subscription_status_str(publication.subscription_status())),
));
}
fn push_local_publication_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
publication: &LocalTrackPublication,
) {
fields.push(("trackSid", s(publication.sid().to_string())));
fields.push(("trackName", s(publication.name())));
fields.push(("kind", s(track_kind_str(publication.kind()))));
fields.push(("source", s(track_source_str(publication.source()))));
fields.push(("muted", b(publication.is_muted())));
}
fn push_publication_fields(
fields: &mut Vec<(&'static str, JsonValue)>,
publication: &TrackPublication,
) {
fields.push(("trackSid", s(publication.sid().to_string())));
fields.push(("trackName", s(publication.name())));
fields.push(("kind", s(track_kind_str(publication.kind()))));
fields.push(("source", s(track_source_str(publication.source()))));
fields.push(("muted", b(publication.is_muted())));
}
fn remote_track_payload(
participant: &RemoteParticipant,
publication: &RemoteTrackPublication,
) -> String {
let mut fields = Vec::new();
push_remote_participant_fields(&mut fields, participant);
push_remote_publication_fields(&mut fields, publication);
json_object(&fields)
}
const CONNECTED_ROSTER_PARTICIPANTS_MAX: usize = 1024;
const CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX: usize = 16;
fn connected_payload(
participants_with_tracks: &[(RemoteParticipant, Vec<RemoteTrackPublication>)],
) -> String {
let participant_count = participants_with_tracks
.len()
.min(CONNECTED_ROSTER_PARTICIPANTS_MAX);
let mut entries = Vec::with_capacity(participant_count);
for (participant, publications) in participants_with_tracks
.iter()
.take(CONNECTED_ROSTER_PARTICIPANTS_MAX)
{
let track_count = publications
.len()
.min(CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX);
let mut tracks = Vec::with_capacity(track_count);
for publication in publications
.iter()
.take(CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX)
{
tracks.push(remote_track_payload(participant, publication));
}
entries.push(json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("tracks", JsonValue::Raw(json_raw_array(&tracks))),
]));
}
assert!(entries.len() <= CONNECTED_ROSTER_PARTICIPANTS_MAX);
json_object(&[("participants", JsonValue::Raw(json_raw_array(&entries)))])
}
fn local_track_payload(
participant: &LocalParticipant,
publication: &LocalTrackPublication,
) -> String {
let mut fields = Vec::new();
push_local_participant_fields(&mut fields, participant);
push_local_publication_fields(&mut fields, publication);
json_object(&fields)
}
pub fn map_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
map_participant_lifecycle_room_event(event)
.or_else(|| map_participant_profile_room_event(event))
.or_else(|| map_track_room_event(event))
.or_else(|| map_local_track_room_event(event))
.or_else(|| map_connection_room_event(event))
}
fn map_participant_lifecycle_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::ParticipantConnected(p) | RoomEvent::ParticipantActive(p) => Some((
"participantJoined",
json_object(&[
("sid", s(p.sid().to_string())),
("identity", s(p.identity().to_string())),
("name", s(p.name())),
]),
)),
RoomEvent::ParticipantDisconnected(p) => Some((
"participantLeft",
json_object(&[
("sid", s(p.sid().to_string())),
("identity", s(p.identity().to_string())),
("name", s(p.name())),
]),
)),
RoomEvent::ParticipantNameChanged {
participant,
old_name,
name,
} => Some((
"participantNameChanged",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("oldName", s(old_name.to_string())),
("name", s(name.to_string())),
]),
)),
_ => None,
}
}
fn map_participant_profile_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::ParticipantMetadataChanged {
participant,
old_metadata,
metadata,
} => Some((
"participantMetadataChanged",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("oldMetadata", s(old_metadata.to_string())),
("metadata", s(metadata.to_string())),
(
"attributes",
JsonValue::Raw(json_string_map(&participant.attributes())),
),
]),
)),
RoomEvent::ParticipantAttributesChanged {
participant,
changed_attributes,
} => Some((
"participantAttributesChanged",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
(
"attributes",
JsonValue::Raw(json_string_map(&participant.attributes())),
),
(
"changedAttributes",
JsonValue::Raw(json_string_map(changed_attributes)),
),
]),
)),
_ => None,
}
}
fn map_track_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::TrackSubscribed {
publication,
participant,
..
} => Some((
"trackSubscribed",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackUnsubscribed {
publication,
participant,
..
} => Some((
"trackUnsubscribed",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackSubscriptionFailed {
participant,
error,
track_sid,
} => {
let mut fields = Vec::new();
push_remote_participant_fields(&mut fields, participant);
if let Some(publication) = participant.get_track_publication(track_sid) {
push_remote_publication_fields(&mut fields, &publication);
} else {
fields.push(("trackSid", s(track_sid.to_string())));
}
fields.push(("error", s(format!("{error}"))));
Some(("trackSubscriptionFailed", json_object(&fields)))
}
RoomEvent::TrackPublished {
publication,
participant,
} => Some((
"trackPublished",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackUnpublished {
publication,
participant,
} => Some((
"trackUnpublished",
remote_track_payload(participant, publication),
)),
RoomEvent::TrackMuted {
participant,
publication,
} => {
let mut fields = Vec::new();
push_participant_fields(&mut fields, participant);
push_publication_fields(&mut fields, publication);
Some(("trackMuted", json_object(&fields)))
}
RoomEvent::TrackUnmuted {
participant,
publication,
} => {
let mut fields = Vec::new();
push_participant_fields(&mut fields, participant);
push_publication_fields(&mut fields, publication);
Some(("trackUnmuted", json_object(&fields)))
}
_ => None,
}
}
fn map_connection_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::ActiveSpeakersChanged { speakers } => {
let sids: Vec<String> = speakers.iter().map(|p| p.sid().to_string()).collect();
let participants: Vec<String> = speakers.iter().map(participant_snapshot).collect();
Some((
"activeSpeakers",
json_object(&[
("sids", JsonValue::Raw(json_string_array(&sids))),
(
"participants",
JsonValue::Raw(json_raw_array(&participants)),
),
]),
))
}
RoomEvent::ConnectionQualityChanged {
quality,
participant,
} => Some((
"connectionQuality",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("quality", s(connection_quality_str(*quality))),
]),
)),
RoomEvent::DataReceived {
payload,
topic,
kind,
participant,
} => Some((
"dataReceived",
data_received_payload(
payload.as_ref().as_slice(),
topic.as_deref(),
kind,
participant.as_ref(),
),
)),
RoomEvent::E2eeStateChanged { participant, state } => Some((
"e2eeState",
json_object(&[
("sid", s(participant.sid().to_string())),
("identity", s(participant.identity().to_string())),
("name", s(participant.name())),
("state", s(format!("{state:?}").to_lowercase())),
]),
)),
RoomEvent::ConnectionStateChanged(state) => Some((
"connectionState",
json_object(&[("state", s(format!("{state:?}").to_lowercase()))]),
)),
RoomEvent::Disconnected { reason } => Some((
"disconnected",
json_object(&[("reason", s(format!("{reason:?}").to_lowercase()))]),
)),
RoomEvent::Connected {
participants_with_tracks,
} => Some(("connected", connected_payload(participants_with_tracks))),
_ => None,
}
}
fn data_received_payload(
payload: &[u8],
topic: Option<&str>,
kind: &DataPacketKind,
participant: Option<&RemoteParticipant>,
) -> String {
let mut fields = vec![
("payloadBytes", JsonValue::Raw(json_u8_array(payload))),
(
"reliable",
JsonValue::Raw(matches!(kind, DataPacketKind::Reliable).to_string()),
),
(
"kind",
s(match kind {
DataPacketKind::Reliable => "reliable",
DataPacketKind::Lossy => "lossy",
}),
),
];
if let Some(topic) = topic {
fields.push(("topic", s(topic.to_string())));
}
if let Ok(payload_text) = std::str::from_utf8(payload) {
fields.push(("payloadText", s(payload_text.to_string())));
}
if let Some(participant) = participant {
push_remote_participant_fields(&mut fields, participant);
}
json_object(&fields)
}
fn map_local_track_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
match event {
RoomEvent::LocalTrackPublished {
publication,
participant,
..
} => Some((
"localTrackPublished",
local_track_payload(participant, publication),
)),
RoomEvent::LocalTrackUnpublished {
publication,
participant,
} => Some((
"localTrackUnpublished",
local_track_payload(participant, publication),
)),
RoomEvent::LocalTrackRepublished {
previous_sid,
publication,
participant,
..
} => {
let mut fields = Vec::new();
push_local_participant_fields(&mut fields, participant);
fields.push(("previousTrackSid", s(previous_sid.to_string())));
push_local_publication_fields(&mut fields, publication);
Some(("localTrackRepublished", json_object(&fields)))
}
_ => None,
}
}
}
#[cfg(feature = "publisher")]
pub use live::{map_room_event, track_kind_str, track_source_str};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_quote_backslash_and_control_chars() {
let mut out = String::new();
push_json_string(&mut out, "a\"b\\c\nd\te");
assert_eq!(out, "\"a\\\"b\\\\c\\nd\\te\"");
let mut ctrl = String::new();
push_json_string(&mut ctrl, "\u{01}");
assert_eq!(ctrl, "\"\\u0001\"");
}
#[test]
fn leaves_plain_ascii_and_unicode_untouched() {
let mut out = String::new();
push_json_string(&mut out, "PA_abc123");
assert_eq!(out, "\"PA_abc123\"");
}
#[test]
fn json_object_preserves_order_and_mixes_str_and_raw() {
let json = json_object(&[
("sid", JsonValue::Str("PA_1".into())),
(
"sids",
JsonValue::Raw(json_string_array(&["PA_1".into(), "PA_2".into()])),
),
]);
assert_eq!(json, "{\"sid\":\"PA_1\",\"sids\":[\"PA_1\",\"PA_2\"]}");
}
#[cfg(feature = "publisher")]
#[test]
fn track_source_strings_match_livekit_js_sources() {
use livekit::track::TrackSource;
assert_eq!(track_source_str(TrackSource::Camera), "camera");
assert_eq!(track_source_str(TrackSource::Microphone), "microphone");
assert_eq!(track_source_str(TrackSource::Screenshare), "screen_share");
assert_eq!(
track_source_str(TrackSource::ScreenshareAudio),
"screen_share_audio"
);
}
#[test]
fn empty_object_and_array() {
assert_eq!(json_object(&[]), "{}");
assert_eq!(json_string_array(&[]), "[]");
assert_eq!(json_u8_array(&[]), "[]");
assert_eq!(json_raw_array(&[]), "[]");
}
#[test]
fn json_u8_array_serializes_bytes_as_numbers() {
assert_eq!(json_u8_array(&[0, 1, 127, 255]), "[0,1,127,255]");
}
#[test]
fn raw_array_preserves_prebuilt_json_objects() {
let items = vec![
json_object(&[("sid", JsonValue::Str("PA_1".into()))]),
json_object(&[("sid", JsonValue::Str("PA_2".into()))]),
];
assert_eq!(
json_raw_array(&items),
"[{\"sid\":\"PA_1\"},{\"sid\":\"PA_2\"}]"
);
}
}