talkers (#8158)
Release Notes: - Added an "Unmute" action for guests in calls. This lets them use the mic, but not edit projects.
This commit is contained in:
parent
fa2c92d190
commit
c6826a61a0
14 changed files with 225 additions and 67 deletions
|
@ -100,8 +100,12 @@ pub enum ChannelRole {
|
|||
#[sea_orm(string_value = "member")]
|
||||
#[default]
|
||||
Member,
|
||||
/// Talker can read, but not write.
|
||||
/// They can use microphones and the channel chat
|
||||
#[sea_orm(string_value = "talker")]
|
||||
Talker,
|
||||
/// Guest can read, but not write.
|
||||
/// (thought they can use the channel chat)
|
||||
/// They can not use microphones but can use the chat.
|
||||
#[sea_orm(string_value = "guest")]
|
||||
Guest,
|
||||
/// Banned may not read.
|
||||
|
@ -114,8 +118,9 @@ impl ChannelRole {
|
|||
pub fn should_override(&self, other: Self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin => matches!(other, Member | Banned | Guest),
|
||||
Member => matches!(other, Banned | Guest),
|
||||
Admin => matches!(other, Member | Banned | Talker | Guest),
|
||||
Member => matches!(other, Banned | Talker | Guest),
|
||||
Talker => matches!(other, Guest),
|
||||
Banned => matches!(other, Guest),
|
||||
Guest => false,
|
||||
}
|
||||
|
@ -134,7 +139,7 @@ impl ChannelRole {
|
|||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest => visibility == ChannelVisibility::Public,
|
||||
Guest | Talker => visibility == ChannelVisibility::Public,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
|
@ -144,7 +149,7 @@ impl ChannelRole {
|
|||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Guest | Talker | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,16 +157,16 @@ impl ChannelRole {
|
|||
pub fn can_only_see_public_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Guest => true,
|
||||
Guest | Talker => true,
|
||||
Admin | Member | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the role can share screen/microphone/projects into rooms.
|
||||
pub fn can_publish_to_rooms(&self) -> bool {
|
||||
pub fn can_use_microphone(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Admin | Member | Talker => true,
|
||||
Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +176,7 @@ impl ChannelRole {
|
|||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest | Banned => false,
|
||||
Talker | Guest | Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +184,7 @@ impl ChannelRole {
|
|||
pub fn can_read_projects(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member | Guest => true,
|
||||
Admin | Member | Guest | Talker => true,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
|
@ -188,7 +193,7 @@ impl ChannelRole {
|
|||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Banned | Guest => false,
|
||||
Banned | Guest | Talker => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,6 +203,7 @@ impl From<proto::ChannelRole> for ChannelRole {
|
|||
match value {
|
||||
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||
proto::ChannelRole::Member => ChannelRole::Member,
|
||||
proto::ChannelRole::Talker => ChannelRole::Talker,
|
||||
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||
}
|
||||
|
@ -209,6 +215,7 @@ impl Into<proto::ChannelRole> for ChannelRole {
|
|||
match self {
|
||||
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||
ChannelRole::Member => proto::ChannelRole::Member,
|
||||
ChannelRole::Talker => proto::ChannelRole::Talker,
|
||||
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||
}
|
||||
|
|
|
@ -795,6 +795,7 @@ impl Database {
|
|||
match role {
|
||||
Some(ChannelRole::Admin) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Talker)
|
||||
| Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| None => Err(anyhow!(
|
||||
|
@ -813,7 +814,10 @@ impl Database {
|
|||
let channel_role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match channel_role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) => Ok(channel_role.unwrap()),
|
||||
Some(ChannelRole::Banned) | Some(ChannelRole::Guest) | None => Err(anyhow!(
|
||||
Some(ChannelRole::Banned)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker)
|
||||
| None => Err(anyhow!(
|
||||
"user is not a channel member or channel does not exist"
|
||||
))?,
|
||||
}
|
||||
|
@ -828,9 +832,10 @@ impl Database {
|
|||
) -> Result<ChannelRole> {
|
||||
let role = self.channel_role_for_user(channel, user_id, tx).await?;
|
||||
match role {
|
||||
Some(ChannelRole::Admin) | Some(ChannelRole::Member) | Some(ChannelRole::Guest) => {
|
||||
Ok(role.unwrap())
|
||||
}
|
||||
Some(ChannelRole::Admin)
|
||||
| Some(ChannelRole::Member)
|
||||
| Some(ChannelRole::Guest)
|
||||
| Some(ChannelRole::Talker) => Ok(role.unwrap()),
|
||||
Some(ChannelRole::Banned) | None => Err(anyhow!(
|
||||
"user is not a channel participant or channel does not exist"
|
||||
))?,
|
||||
|
|
|
@ -51,7 +51,7 @@ impl Database {
|
|||
if !participant
|
||||
.role
|
||||
.unwrap_or(ChannelRole::Member)
|
||||
.can_publish_to_rooms()
|
||||
.can_edit_projects()
|
||||
{
|
||||
return Err(anyhow!("guests cannot share projects"))?;
|
||||
}
|
||||
|
|
|
@ -169,7 +169,7 @@ impl Database {
|
|||
|
||||
let called_user_role = match caller.role.unwrap_or(ChannelRole::Member) {
|
||||
ChannelRole::Admin | ChannelRole::Member => ChannelRole::Member,
|
||||
ChannelRole::Guest => ChannelRole::Guest,
|
||||
ChannelRole::Guest | ChannelRole::Talker => ChannelRole::Guest,
|
||||
ChannelRole::Banned => return Err(anyhow!("banned users cannot invite").into()),
|
||||
};
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ use axum::{
|
|||
Extension, Router, TypedHeader,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use connection_pool::ConnectionPool;
|
||||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::{self, BoxFuture},
|
||||
|
@ -558,6 +558,7 @@ impl Server {
|
|||
connection: Connection,
|
||||
address: String,
|
||||
user: User,
|
||||
zed_version: ZedVersion,
|
||||
impersonator: Option<User>,
|
||||
mut send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
executor: Executor,
|
||||
|
@ -599,7 +600,7 @@ impl Server {
|
|||
|
||||
{
|
||||
let mut pool = this.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user_id, user.admin);
|
||||
pool.add_connection(connection_id, user_id, user.admin, zed_version);
|
||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||
this.peer.send(connection_id, build_update_user_channels(&channels_for_user))?;
|
||||
this.peer.send(connection_id, build_channels_update(
|
||||
|
@ -879,17 +880,20 @@ pub async fn handle_websocket_request(
|
|||
.into_response();
|
||||
}
|
||||
|
||||
// the first version of zed that sent this header was 0.121.x
|
||||
if let Some(version) = app_version_header.map(|header| header.0 .0) {
|
||||
// 0.123.0 was a nightly version with incompatible collab changes
|
||||
// that were reverted.
|
||||
if version == "0.123.0".parse().unwrap() {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"client must be upgraded".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let Some(version) = app_version_header.map(|header| ZedVersion(header.0 .0)) else {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"no version header found".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
};
|
||||
|
||||
if !version.is_supported() {
|
||||
return (
|
||||
StatusCode::UPGRADE_REQUIRED,
|
||||
"client must be upgraded".to_string(),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let socket_address = socket_address.to_string();
|
||||
|
@ -906,6 +910,7 @@ pub async fn handle_websocket_request(
|
|||
connection,
|
||||
socket_address,
|
||||
user,
|
||||
version,
|
||||
impersonator.0,
|
||||
None,
|
||||
Executor::Production,
|
||||
|
@ -1311,6 +1316,22 @@ async fn set_room_participant_role(
|
|||
response: Response<proto::SetRoomParticipantRole>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let user_id = UserId::from_proto(request.user_id);
|
||||
let role = ChannelRole::from(request.role());
|
||||
|
||||
if role == ChannelRole::Talker {
|
||||
let pool = session.connection_pool().await;
|
||||
|
||||
for connection in pool.user_connections(user_id) {
|
||||
if !connection.zed_version.supports_talker_role() {
|
||||
Err(anyhow!(
|
||||
"This user is on zed {} which does not support unmute",
|
||||
connection.zed_version
|
||||
))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (live_kit_room, can_publish) = {
|
||||
let room = session
|
||||
.db()
|
||||
|
@ -1318,13 +1339,13 @@ async fn set_room_participant_role(
|
|||
.set_room_participant_role(
|
||||
session.user_id,
|
||||
RoomId::from_proto(request.room_id),
|
||||
UserId::from_proto(request.user_id),
|
||||
ChannelRole::from(request.role()),
|
||||
user_id,
|
||||
role,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_room = room.live_kit_room.clone();
|
||||
let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms();
|
||||
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
|
||||
room_updated(&room, &session.peer);
|
||||
(live_kit_room, can_publish)
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ use collections::{BTreeMap, HashSet};
|
|||
use rpc::ConnectionId;
|
||||
use serde::Serialize;
|
||||
use tracing::instrument;
|
||||
use util::SemanticVersion;
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct ConnectionPool {
|
||||
|
@ -16,10 +17,30 @@ struct ConnectedUser {
|
|||
connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ZedVersion(pub SemanticVersion);
|
||||
use std::fmt;
|
||||
|
||||
impl fmt::Display for ZedVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ZedVersion {
|
||||
pub fn is_supported(&self) -> bool {
|
||||
self.0 != SemanticVersion::new(0, 123, 0)
|
||||
}
|
||||
pub fn supports_talker_role(&self) -> bool {
|
||||
self.0 >= SemanticVersion::new(0, 125, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Connection {
|
||||
pub user_id: UserId,
|
||||
pub admin: bool,
|
||||
pub zed_version: ZedVersion,
|
||||
}
|
||||
|
||||
impl ConnectionPool {
|
||||
|
@ -29,9 +50,21 @@ impl ConnectionPool {
|
|||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub fn add_connection(&mut self, connection_id: ConnectionId, user_id: UserId, admin: bool) {
|
||||
self.connections
|
||||
.insert(connection_id, Connection { user_id, admin });
|
||||
pub fn add_connection(
|
||||
&mut self,
|
||||
connection_id: ConnectionId,
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
zed_version: ZedVersion,
|
||||
) {
|
||||
self.connections.insert(
|
||||
connection_id,
|
||||
Connection {
|
||||
user_id,
|
||||
admin,
|
||||
zed_version,
|
||||
},
|
||||
);
|
||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||
connected_user.connection_ids.insert(connection_id);
|
||||
}
|
||||
|
@ -57,6 +90,19 @@ impl ConnectionPool {
|
|||
self.connections.values()
|
||||
}
|
||||
|
||||
pub fn user_connections(&self, user_id: UserId) -> impl Iterator<Item = &Connection> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
.into_iter()
|
||||
.map(|state| {
|
||||
state
|
||||
.connection_ids
|
||||
.iter()
|
||||
.flat_map(|cid| self.connections.get(cid))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn user_connection_ids(&self, user_id: UserId) -> impl Iterator<Item = ConnectionId> + '_ {
|
||||
self.connected_users
|
||||
.get(&user_id)
|
||||
|
|
|
@ -104,7 +104,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
|||
});
|
||||
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
|
||||
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
assert!(room_b
|
||||
.update(cx_b, |room, cx| room.share_microphone(cx))
|
||||
.await
|
||||
|
@ -130,7 +130,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
|
|||
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
|
||||
|
||||
// B sees themselves as muted, and can unmute.
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
|
||||
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
|
||||
cx_a.run_until_parked();
|
||||
|
@ -223,7 +223,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||
let room_b = cx_b
|
||||
.read(ActiveCall::global)
|
||||
.update(cx_b, |call, _| call.room().unwrap().clone());
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
|
@ -240,7 +240,26 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||
.await
|
||||
.unwrap_err();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
|
||||
|
||||
// A tries to grant write access to B, but cannot because B has not
|
||||
// yet signed the zed CLA.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| {
|
||||
call.room().unwrap().update(cx, |room, cx| {
|
||||
room.set_participant_role(
|
||||
client_b.user_id().unwrap(),
|
||||
proto::ChannelRole::Talker,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
|
||||
// User B signs the zed CLA.
|
||||
server
|
||||
|
@ -264,5 +283,6 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
|||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
assert!(room_b.read_with(cx_b, |room, _| !room.read_only()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
|
||||
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
db::{tests::TestDb, NewUserParams, UserId},
|
||||
executor::Executor,
|
||||
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
rpc::{Server, ZedVersion, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
AppState, Config,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
|
@ -37,7 +37,7 @@ use std::{
|
|||
Arc,
|
||||
},
|
||||
};
|
||||
use util::http::FakeHttpClient;
|
||||
use util::{http::FakeHttpClient, SemanticVersion};
|
||||
use workspace::{Workspace, WorkspaceStore};
|
||||
|
||||
pub struct TestServer {
|
||||
|
@ -231,6 +231,7 @@ impl TestServer {
|
|||
server_conn,
|
||||
client_name,
|
||||
user,
|
||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue