Implement get_voice_channels procedure server-side #29

Merged
puregarlic merged 11 commits from issue/21 into main 2026-02-16 16:02:24 -08:00
Owner

Some things of note:

  • Starting with this change, you'll need to have a SQLite file created before you can compile the server. This is because we're using the cached-query macros in sqlx, which check SQL queries against the DB at compile time. Ergo--no DB to check against, no successful compilation. You can solve this conundrum by using sqlx's CLI to execute migrations. Check the README for details.
  • I removed the logic for checking for room creation before issuing an access token, because access tokens can be encoded with create permissions. I figure it's better to have a "join-and-create" workflow.
  • The origin for the LIVEKIT_URL needs to be https:// instead of wss://. This should only affect @tepichord. @seb get in touch with me to get some working credentials you can try.
Some things of note: - Starting with this change, you'll need to have a SQLite file created before you can compile the server. This is because we're using the cached-query macros in `sqlx`, which check SQL queries against the DB at compile time. Ergo--no DB to check against, no successful compilation. You can solve this conundrum by using `sqlx`'s CLI to execute migrations. Check the README for details. - I removed the logic for checking for room creation before issuing an access token, because access tokens can be encoded with `create` permissions. I figure it's better to have a "join-and-create" workflow. - The origin for the `LIVEKIT_URL` needs to be `https://` instead of `wss://`. This should only affect @tepichord. @seb get in touch with me to get some working credentials you can try.
LiveKit tokens support creating rooms automatically on join, so there's no need to do it automatically.
SQLite will streamline this function, so I'm stubbing it and I'll get back to it after adding Diesel
puregarlic changed title from WIP: Implement get_voice_channels procedure server-side to Implement get_voice_channels procedure server-side 2026-02-15 14:50:49 -08:00
seb approved these changes 2026-02-15 18:25:26 -08:00
Dismissed
seb left a comment
Collaborator

Good PR. I wrote some Rustisms below if you feel like implementing any of them. Whatever you decide on those, this is good to merge.

Good PR. I wrote some Rustisms below if you feel like implementing any of them. Whatever you decide on those, this is good to merge.
@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS channels (
id VARCHAR(21) PRIMARY KEY NOT NULL,
Collaborator

Regarding the id field,

Per the SQLite datatypes documentation:

Most SQL database engines (every SQL database engine other than SQLite, as far as we know) uses static, rigid typing. With static typing, the datatype of a value is determined by its container - the particular column in which the value is stored.

SQLite uses a more general dynamic type system. In SQLite, the datatype of a value is associated with the value itself, not with its container. The dynamic type system of SQLite is backwards compatible with the more common static type systems of other database engines in the sense that SQL statements that work on statically typed databases work the same way in SQLite. However, the dynamic typing in SQLite allows it to do things which are not possible in traditional rigidly typed databases. Flexible typing is a feature of SQLite, not a bug.

...

The following table shows how many common datatype names from more traditional SQL implementations are converted into affinities... This table shows only a small subset of the datatype names that SQLite will accept. Note that numeric arguments in parentheses that following the type name (ex: "VARCHAR(255)") are ignored by SQLite - SQLite does not impose any length restrictions (other than the large global SQLITE_MAX_LENGTH limit) on the length of strings, BLOBs or numeric values.

image

Given that VARCHAR(21) seemingly doesn't actually enforce a length constraint, I see two options:

  1. To hell with it (change the type it to TEXT)
  2. Eat the runtime cost (add a CHECK(LENGTH(id) <= 21))

There's also STRICT tables. I see no indication that the variable length constraint situation improves if you make this a STRICT table, but I am in favor of doing it anyways because jesus christ.

Regarding the `id` field, Per the [SQLite datatypes documentation](https://www.sqlite.org/datatype3.html): > Most SQL database engines (every SQL database engine other than SQLite, as far as we know) uses static, rigid typing. With static typing, the datatype of a value is determined by its container - the particular column in which the value is stored. > > SQLite uses a more general dynamic type system. In SQLite, the datatype of a value is associated with the value itself, not with its container. The dynamic type system of SQLite is backwards compatible with the more common static type systems of other database engines in the sense that SQL statements that work on statically typed databases work the same way in SQLite. However, the dynamic typing in SQLite allows it to do things which are not possible in traditional rigidly typed databases. Flexible typing is a feature of SQLite, not a bug. > >... > >The following table shows how many common datatype names from more traditional SQL implementations are converted into affinities... This table shows only a small subset of the datatype names that SQLite will accept. **Note that numeric arguments in parentheses that following the type name (ex: "VARCHAR(255)") are ignored by SQLite - SQLite does not impose any length restrictions (other than the large global SQLITE_MAX_LENGTH limit) on the length of strings, BLOBs or numeric values.** ![image](/attachments/62ad2c7f-8627-4d91-aad8-9c7290ee625e) Given that VARCHAR(21) seemingly doesn't actually enforce a length constraint, I see two options: 1. To hell with it (change the type it to TEXT) 2. Eat the runtime cost (add a `CHECK(LENGTH(id) <= 21)`) There's also [STRICT tables](https://www.sqlite.org/stricttables.html). I see no indication that the variable length constraint situation improves if you make this a STRICT table, but I am in favor of doing it anyways because jesus christ.
Author
Owner

I changed the column type to TEXT and also enabled STRICT for the table.

I changed the column type to `TEXT` and also enabled STRICT for the table.
puregarlic marked this conversation as resolved
@ -12,0 +13,4 @@
simple_logger::init()?;
let database_url =
std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:data.db?mode=rwc".to_string());
Collaborator

std::env::var

image

std::env::var ![image](/attachments/396ea192-6bd6-4123-99f2-bc623f2e830b)
155 KiB
puregarlic marked this conversation as resolved
@ -87,0 +117,4 @@
let mut rows = sqlx::query_as!(ChannelRow, "SELECT * FROM channels").fetch(&self.db);
let mut channels: Vec<Channel> = Vec::new();
loop {
Collaborator

In my opinion, iterators are usually a cleaner way than iteration statements to build vectors.

sqlx::query::Query::fetch returns a Stream, which you can use like an Iterator. Something like the following with the map_ok crate?

let channels: Vec<Channel> = rows
    .map_ok(|row| Channel {
        id: row.id,
        name: row.name,
        participants: Vec::new(),
    })
    .try_collect()
    .await
    .map_err(|err| {
        error!("failed to query channels: {err}");
        Status::internal("failed to retrieve channels")
    })?;

I didn't try to compile this, but that's my initial idea. Even if the actual implementation you'd need here ends up more verbose, I've found that this iterator-oriented/functional programming pattern tends to have much less nesting than iterative code with match statements. I find the former style easier to read, but ymmv

In my opinion, iterators are usually a cleaner way than iteration statements to build vectors. [sqlx::query::Query::fetch](https://docs.rs/sqlx/latest/sqlx/query/struct.Query.html#method.fetch) returns a Stream, which you can use like an Iterator. Something like the following with the [map_ok](https://docs.rs/map-ok/latest/map_ok/) crate? ``` let channels: Vec<Channel> = rows .map_ok(|row| Channel { id: row.id, name: row.name, participants: Vec::new(), }) .try_collect() .await .map_err(|err| { error!("failed to query channels: {err}"); Status::internal("failed to retrieve channels") })?; ``` I didn't try to compile this, but that's my initial idea. Even if the actual implementation you'd need here ends up more verbose, I've found that this iterator-oriented/functional programming pattern tends to have much less nesting than iterative code with match statements. I find the former style easier to read, but ymmv
puregarlic marked this conversation as resolved
@ -87,0 +155,4 @@
})
}
}
Err(err) => {
Collaborator

You can reduce the number of nested blocks here by simplifying the error handling with map_err and throwing with ? prior to looping.

Something like:

let rooms = room_list.map_err(|err| {
    error!("failed to list rooms: {err}");
    Status::internal("failed to retrieve channels")
})?;

eliminates the outer match statement and some of the inner error handling if you do it outside the for loop. I'm also favorable to using iteration/chaining inside the loop instead of doing nested for loops. If we do that, and we use std::iter::Extend then the whole room list iteration and the remaining match statement could simplify to something like:

let room_names = channels.iter().map(|c| c.id.clone()).collect();
let room_list = self.room_client.list_rooms(room_names).await;

let rooms = room_list.map_err(|err| {
    error!("failed to list rooms: {err}");
    Status::internal("failed to retrieve channels")
})?;

for room in &rooms {
    if let Some(channel) = channels.iter_mut().find(|c| c.id == room.name) {
        let participants = self
            .room_client
            .list_participants(&room.name)
            .await
            .map_err(|err| {
                error!("failed to query room participants: {err}");
                Status::internal("failed to retrieve channels")
            })?;

        channel.participants.extend(
            participants.iter().map(|p| Participant {
                id: p.identity.clone(),
            }),
        );
    }
}

Ok(Response::new(GetVoiceChannelsResponse { channels }))

Personally, I find this easier to read. I didn't try to compile it though lol

You can reduce the number of nested blocks here by simplifying the error handling with [map_err](https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err) and throwing with `?` prior to looping. Something like: ``` let rooms = room_list.map_err(|err| { error!("failed to list rooms: {err}"); Status::internal("failed to retrieve channels") })?; ``` eliminates the outer match statement and some of the inner error handling if you do it outside the for loop. I'm also favorable to using iteration/chaining inside the loop instead of doing nested for loops. If we do that, and we use [std::iter::Extend](https://doc.rust-lang.org/std/iter/trait.Extend.html) then the whole room list iteration and the remaining match statement could simplify to something like: ``` let room_names = channels.iter().map(|c| c.id.clone()).collect(); let room_list = self.room_client.list_rooms(room_names).await; let rooms = room_list.map_err(|err| { error!("failed to list rooms: {err}"); Status::internal("failed to retrieve channels") })?; for room in &rooms { if let Some(channel) = channels.iter_mut().find(|c| c.id == room.name) { let participants = self .room_client .list_participants(&room.name) .await .map_err(|err| { error!("failed to query room participants: {err}"); Status::internal("failed to retrieve channels") })?; channel.participants.extend( participants.iter().map(|p| Participant { id: p.identity.clone(), }), ); } } Ok(Response::new(GetVoiceChannelsResponse { channels })) ``` Personally, I find this easier to read. I didn't try to compile it though lol
Collaborator

One more thing -- channels.iter_mut().find(|c| c.id == room.name) might be nicer to express as a HashMap indexed by room id. Incidentally, it also speeds up the operation considerably, as currently looking for a channel given a room id is O(N²) complexity (channels x rooms). Practically, we will probably never have enough channels or rooms for that to matter. I hope.

One more thing -- `channels.iter_mut().find(|c| c.id == room.name)` might be nicer to express as a HashMap indexed by room id. Incidentally, it also speeds up the operation considerably, as currently looking for a channel given a room id is O(N²) complexity (channels x rooms). Practically, we will probably never have enough channels or rooms for that to matter. I hope.
puregarlic marked this conversation as resolved
Weirdness with Cargo Workspaces means your DB file has to be in the root,
meaning you have to tell `sqlx` where to find your migrations.
dotenv is not maintained and has an open vulnerability
According to PR feedback
puregarlic requested review from seb 2026-02-16 15:21:55 -08:00
seb approved these changes 2026-02-16 15:57:41 -08:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
puregarlic/microclimate!29
No description provided.