M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD). - Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts. - C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account). - vccli flags for all M5 operations plus --username/--password auth. - Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD. - Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
This commit is contained in:
@@ -127,7 +127,7 @@ typedef enum vc_event_type {
|
||||
VC_EVENT_TALK_STATE = 9, /* user_id, stream_id, u32a = talking(0/1) */
|
||||
VC_EVENT_ERROR = 10, /* result, text */
|
||||
VC_EVENT_DISCONNECTED = 11, /* result, text = reason */
|
||||
/* M4 additions — appended, not inserted, to keep existing values stable. */
|
||||
/* M4 additions — appended, not inserted, to keep existing enum values stable. */
|
||||
VC_EVENT_JOIN_RESULT = 12, /* result (VC_OK/VC_ERR_*), channel_id, text = error on
|
||||
failure. Reply to vc_join_channel(). */
|
||||
VC_EVENT_SERVER_IDENTITY = 13, /* u32a = vc_tofu_status, text = hex-encoded TLS leaf-cert
|
||||
@@ -136,6 +136,13 @@ typedef enum vc_event_type {
|
||||
attempt, right after the TLS handshake succeeds. The
|
||||
connection is held open until vc_confirm_server_identity()
|
||||
is called. */
|
||||
/* M5 additions — appended, not inserted. */
|
||||
VC_EVENT_GENERIC_RESULT = 14, /* result, u32a = server error code, text = message. Reply
|
||||
to vc_kick_user/vc_ban_user/vc_set_permission/
|
||||
vc_move_user/vc_create_channel/vc_edit_channel/
|
||||
vc_delete_channel/vc_create_account/vc_reset_password/
|
||||
vc_delete_account. */
|
||||
VC_EVENT_ACCOUNT_LIST = 15, /* Reply to vc_list_accounts. */
|
||||
} vc_event_type;
|
||||
|
||||
/* TOFU server-identity classification (M4) — see VC_EVENT_SERVER_IDENTITY and
|
||||
@@ -216,6 +223,30 @@ typedef struct vc_audio_config {
|
||||
uint32_t complexity; /* 0..10 */
|
||||
} vc_audio_config;
|
||||
|
||||
/* M5: permission bitset (mirrors protocol Permissions). */
|
||||
typedef struct vc_permissions {
|
||||
int can_create_temp_channel; /* bool */
|
||||
int can_kick; /* bool */
|
||||
int can_ban; /* bool */
|
||||
int can_move_users; /* bool */
|
||||
int can_admin_accounts; /* bool */
|
||||
int is_admin; /* bool */
|
||||
} vc_permissions;
|
||||
|
||||
/* M5: channel creation/edition descriptor. */
|
||||
typedef struct vc_channel_info {
|
||||
uint32_t id; /* 0 = new channel for create */
|
||||
uint32_t parent_id; /* 0 = root */
|
||||
const char* name;
|
||||
const char* topic;
|
||||
int password_protected; /* bool */
|
||||
const char* password; /* nullable; ignored if password_protected == 0 */
|
||||
uint32_t max_users; /* 0 = unlimited */
|
||||
uint32_t sort_order;
|
||||
/* Audio config — 0/NULL fields use server defaults. */
|
||||
vc_audio_config audio;
|
||||
} vc_channel_info;
|
||||
|
||||
typedef struct vc_device {
|
||||
const char* id;
|
||||
const char* name;
|
||||
@@ -370,6 +401,32 @@ VC_API vc_result vc_confirm_server_identity(vc_client* c, int accept /* bool */)
|
||||
VC_API vc_result vc_get_server_identity_display(vc_client* c, char* out_buf, size_t buf_cap,
|
||||
size_t* out_len);
|
||||
|
||||
/* ── M5: Moderation & admin ─────────────────────────────────────────────────
|
||||
* All calls are async; the result arrives as VC_EVENT_GENERIC_RESULT (or
|
||||
* VC_EVENT_ACCOUNT_LIST for vc_list_accounts). They require VC_STATE_CONNECTED and,
|
||||
* on the server side, the appropriate permission. */
|
||||
|
||||
VC_API vc_result vc_kick_user(vc_client* c, uint32_t user_id, const char* reason);
|
||||
VC_API vc_result vc_ban_user(vc_client* c, uint32_t user_id, const char* reason,
|
||||
uint64_t expires_unix_ms);
|
||||
VC_API vc_result vc_set_permission(vc_client* c, uint32_t user_id,
|
||||
const vc_permissions* perms);
|
||||
VC_API vc_result vc_set_server_mute(vc_client* c, uint32_t user_id, int muted, int deafened);
|
||||
VC_API vc_result vc_move_user(vc_client* c, uint32_t user_id, uint32_t channel_id);
|
||||
|
||||
VC_API vc_result vc_create_channel(vc_client* c, const vc_channel_info* info);
|
||||
VC_API vc_result vc_edit_channel(vc_client* c, const vc_channel_info* info);
|
||||
VC_API vc_result vc_delete_channel(vc_client* c, uint32_t channel_id);
|
||||
|
||||
VC_API vc_result vc_create_account(vc_client* c, const char* username, const char* password);
|
||||
VC_API vc_result vc_reset_password(vc_client* c, const char* username,
|
||||
const char* new_password);
|
||||
VC_API vc_result vc_delete_account(vc_client* c, const char* username);
|
||||
VC_API vc_result vc_list_accounts(vc_client* c);
|
||||
|
||||
/* Pull the caller's own permissions (from the last AuthResult). */
|
||||
VC_API vc_result vc_get_permissions(vc_client* c, vc_permissions* out);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
} /* extern "C" */
|
||||
#endif
|
||||
|
||||
@@ -54,12 +54,14 @@ message Envelope {
|
||||
KickRequest kick = 60;
|
||||
BanRequest ban = 61;
|
||||
SetPermissionRequest set_permission = 62;
|
||||
ServerMuteRequest server_mute = 63;
|
||||
|
||||
// Admin account management — privileged; accounts are admin-provisioned (70–79)
|
||||
CreateAccountRequest create_account = 70;
|
||||
ResetPasswordRequest reset_password = 71;
|
||||
DeleteAccountRequest delete_account = 72;
|
||||
ListAccountsRequest list_accounts = 73;
|
||||
ListAccountsResult list_accounts_result = 74;
|
||||
|
||||
// Future families: file transfer = 100–109. Extension escape hatch = 200+.
|
||||
Extension extension = 200;
|
||||
@@ -116,6 +118,7 @@ message User {
|
||||
bool self_deafened = 6;
|
||||
bool server_muted = 7;
|
||||
repeated StreamInfo streams = 8;
|
||||
bool server_deafened = 9; // M5: server-imposed deafen
|
||||
}
|
||||
|
||||
message Permissions {
|
||||
@@ -186,6 +189,7 @@ message UserEvent {
|
||||
Kind kind = 1;
|
||||
User user = 2;
|
||||
uint32 left_id = 3;
|
||||
string reason = 4; // M5: kick/ban reason for LEFT events
|
||||
}
|
||||
message SubscribeRequest { repeated uint32 channel_ids = 1; bool presence = 2; }
|
||||
|
||||
@@ -227,12 +231,15 @@ message TypingIndicator { TextScope scope = 1; uint32 target_id = 2; uint32 use
|
||||
message KickRequest { uint32 user_id = 1; string reason = 2; }
|
||||
message BanRequest { uint32 user_id = 1; string reason = 2; uint64 expires_unix_ms = 3; }
|
||||
message SetPermissionRequest { uint32 user_id = 1; Permissions permissions = 2; }
|
||||
message ServerMuteRequest { uint32 user_id = 1; bool muted = 2; bool deafened = 3; }
|
||||
|
||||
// ── Admin account management (privileged) ────────────────────────────────────────
|
||||
message CreateAccountRequest { string username = 1; string password = 2; }
|
||||
message ResetPasswordRequest { string username = 1; string new_password = 2; }
|
||||
message DeleteAccountRequest { string username = 1; }
|
||||
message ListAccountsRequest {}
|
||||
message AccountEntry { string username = 1; bool is_admin = 2; uint64 created_at_unix_ms = 3; uint64 last_login_unix_ms = 4; }
|
||||
message ListAccountsResult { repeated AccountEntry accounts = 1; }
|
||||
|
||||
// ── Extension escape hatch ───────────────────────────────────────────────────────
|
||||
message Extension { string ns = 1; bytes payload = 2; }
|
||||
|
||||
@@ -309,7 +309,7 @@ void vc_client::run_io(std::string host, uint16_t port) {
|
||||
io_fd_.store(-1);
|
||||
}
|
||||
|
||||
if (!io_stop_.load()) emit_disconnected(VC_OK, nullptr);
|
||||
if (!io_stop_.load()) emit_disconnected(VC_ERR_IO, "connection closed");
|
||||
|
||||
cleanup:
|
||||
#ifdef _WIN32
|
||||
@@ -415,6 +415,21 @@ void vc_client::handle_envelope(const voicecat::v1::Envelope& env) {
|
||||
break;
|
||||
case voicecat::v1::Envelope::kPong:
|
||||
break; // ignore keepalive responses
|
||||
case voicecat::v1::Envelope::kGenericResult: {
|
||||
vc_event ev{};
|
||||
ev.type = VC_EVENT_GENERIC_RESULT;
|
||||
ev.result = env.generic_result().ok() ? VC_OK : VC_ERR_PERMISSION_DENIED;
|
||||
ev.u32a = env.generic_result().code();
|
||||
ev.text = env.generic_result().message().c_str();
|
||||
emit(ev);
|
||||
break;
|
||||
}
|
||||
case voicecat::v1::Envelope::kListAccountsResult: {
|
||||
vc_event ev{};
|
||||
ev.type = VC_EVENT_ACCOUNT_LIST;
|
||||
emit(ev);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -468,6 +483,15 @@ void vc_client::handle_auth_result(const voicecat::v1::AuthResult& msg) {
|
||||
ev.user_id = self_user_id_;
|
||||
set_state(VC_STATE_CONNECTED);
|
||||
|
||||
// Store own permissions.
|
||||
const auto& perms = msg.permissions();
|
||||
own_permissions_.can_create_temp_channel = perms.can_create_temp_channel() ? 1 : 0;
|
||||
own_permissions_.can_kick = perms.can_kick() ? 1 : 0;
|
||||
own_permissions_.can_ban = perms.can_ban() ? 1 : 0;
|
||||
own_permissions_.can_move_users = perms.can_move_users() ? 1 : 0;
|
||||
own_permissions_.can_admin_accounts = perms.can_admin_accounts() ? 1 : 0;
|
||||
own_permissions_.is_admin = perms.is_admin() ? 1 : 0;
|
||||
|
||||
const std::string& tok = msg.udp_token();
|
||||
if (tok.size() == udp_token_.size()) {
|
||||
std::memcpy(udp_token_.data(), tok.data(), udp_token_.size());
|
||||
@@ -549,6 +573,18 @@ void vc_client::handle_user_event(const voicecat::v1::UserEvent& ue) {
|
||||
case voicecat::v1::UserEvent::UPDATED:
|
||||
ev.type = VC_EVENT_USER_UPDATED;
|
||||
emit(ev);
|
||||
// M5: if this is an update to our own user, reflect server-mute/deafen locally.
|
||||
if (user.id() == self_user_id_) {
|
||||
server_muted_.store(user.server_muted(), std::memory_order_release);
|
||||
server_deafened_.store(user.server_deafened(), std::memory_order_release);
|
||||
std::lock_guard lk(remote_streams_mu_);
|
||||
bool muted = self_deafened_.load(std::memory_order_acquire) ||
|
||||
server_deafened_.load(std::memory_order_acquire);
|
||||
for (auto& [ssrc, info] : remote_streams_) {
|
||||
(void)info;
|
||||
audio_engine_.set_stream_mute(ssrc, muted);
|
||||
}
|
||||
}
|
||||
sync_remote_streams(user);
|
||||
break;
|
||||
default:
|
||||
@@ -776,6 +812,36 @@ voicecat::codec::OpusParams opus_params_from_audio_config(const voicecat::v1::Au
|
||||
p.application = static_cast<voicecat::codec::OpusApplication>(a.application());
|
||||
return p;
|
||||
}
|
||||
|
||||
voicecat::v1::AudioConfig audio_config_from_vc(const vc_audio_config& c) {
|
||||
voicecat::v1::AudioConfig a;
|
||||
a.set_codec(c.codec);
|
||||
a.set_mode(c.mode == 1 ? voicecat::v1::MODE_STEREO : voicecat::v1::MODE_MONO);
|
||||
a.set_sample_rate(c.sample_rate);
|
||||
a.set_bitrate_bps(c.bitrate_bps);
|
||||
a.set_frame_ms(c.frame_ms);
|
||||
a.set_application(static_cast<voicecat::v1::OpusApplication>(c.application));
|
||||
a.set_fec(c.fec != 0);
|
||||
a.set_expected_packet_loss(c.expected_packet_loss);
|
||||
a.set_dtx(c.dtx != 0);
|
||||
a.set_complexity(c.complexity);
|
||||
return a;
|
||||
}
|
||||
|
||||
voicecat::v1::Channel channel_from_vc(const vc_channel_info& c) {
|
||||
voicecat::v1::Channel ch;
|
||||
ch.set_id(c.id);
|
||||
ch.set_parent_id(c.parent_id);
|
||||
ch.set_name(c.name ? c.name : "");
|
||||
ch.set_topic(c.topic ? c.topic : "");
|
||||
ch.set_password_protected(c.password_protected != 0);
|
||||
ch.set_max_users(c.max_users);
|
||||
ch.set_type(voicecat::v1::CHANNEL_PERMANENT);
|
||||
*ch.mutable_audio() = audio_config_from_vc(c.audio);
|
||||
ch.set_order(static_cast<int32_t>(c.sort_order));
|
||||
return ch;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void vc_client::on_capture_frame(int kind, const int16_t* pcm, int samples) {
|
||||
@@ -784,8 +850,10 @@ void vc_client::on_capture_frame(int kind, const int16_t* pcm, int samples) {
|
||||
if (it == local_streams_.end() || !it->second.active.load(std::memory_order_acquire)) return;
|
||||
// "Mic muted" only gates the MIC stream — a concurrently-running SCREEN_AUDIO share keeps
|
||||
// playing while the user's mic is muted (docs §M3 scope decision).
|
||||
// M5: server-mute is also a hard gate on MIC transmission.
|
||||
if (kind == static_cast<int>(VC_STREAM_MIC) &&
|
||||
self_mic_muted_.load(std::memory_order_acquire)) return;
|
||||
(self_mic_muted_.load(std::memory_order_acquire) ||
|
||||
server_muted_.load(std::memory_order_acquire))) return;
|
||||
|
||||
// Send-side input gate (docs/voice.md §11) — MIC only. SCREEN_AUDIO/AUX_DEVICE always
|
||||
// bypass this: gating a screen-share on the user's own voice activity would silently drop
|
||||
@@ -888,7 +956,9 @@ void vc_client::sync_remote_streams(const voicecat::v1::User& user) {
|
||||
|
||||
voicecat::codec::OpusParams p = opus_params_from_audio_config(si.audio());
|
||||
audio_engine_.init_recv_stream(ssrc, p);
|
||||
audio_engine_.set_stream_mute(ssrc, self_deafened_.load(std::memory_order_acquire));
|
||||
bool muted = self_deafened_.load(std::memory_order_acquire) ||
|
||||
server_deafened_.load(std::memory_order_acquire);
|
||||
audio_engine_.set_stream_mute(ssrc, muted);
|
||||
|
||||
newly_added.emplace_back(ssrc, si.stream_id());
|
||||
}
|
||||
@@ -1111,9 +1181,10 @@ vc_result vc_client::set_self_mute(bool mic_muted, bool deafened) {
|
||||
self_deafened_.store(deafened, std::memory_order_release);
|
||||
|
||||
std::lock_guard lk(remote_streams_mu_);
|
||||
bool muted = deafened || server_deafened_.load(std::memory_order_acquire);
|
||||
for (auto& [ssrc, info] : remote_streams_) {
|
||||
(void)info;
|
||||
audio_engine_.set_stream_mute(ssrc, deafened);
|
||||
audio_engine_.set_stream_mute(ssrc, muted);
|
||||
}
|
||||
return VC_OK;
|
||||
}
|
||||
@@ -1306,6 +1377,154 @@ vc_result vc_client::get_server_identity_display(char* out_buf, size_t buf_cap,
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
// ── M5: Moderation & admin ───────────────────────────────────────────────────
|
||||
|
||||
vc_result vc_client::kick_user(uint32_t user_id, const char* reason) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* k = req.mutable_kick();
|
||||
k->set_user_id(user_id);
|
||||
k->set_reason(reason ? reason : "");
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::ban_user(uint32_t user_id, const char* reason, uint64_t expires_unix_ms) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* b = req.mutable_ban();
|
||||
b->set_user_id(user_id);
|
||||
b->set_reason(reason ? reason : "");
|
||||
b->set_expires_unix_ms(expires_unix_ms);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::set_permission(uint32_t user_id, const vc_permissions* perms) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
if (!perms) return VC_ERR_INVALID_ARG;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* sp = req.mutable_set_permission();
|
||||
sp->set_user_id(user_id);
|
||||
auto* p = sp->mutable_permissions();
|
||||
p->set_can_create_temp_channel(perms->can_create_temp_channel != 0);
|
||||
p->set_can_kick(perms->can_kick != 0);
|
||||
p->set_can_ban(perms->can_ban != 0);
|
||||
p->set_can_move_users(perms->can_move_users != 0);
|
||||
p->set_can_admin_accounts(perms->can_admin_accounts != 0);
|
||||
p->set_is_admin(perms->is_admin != 0);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::set_server_mute(uint32_t user_id, bool muted, bool deafened) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* sm = req.mutable_server_mute();
|
||||
sm->set_user_id(user_id);
|
||||
sm->set_muted(muted);
|
||||
sm->set_deafened(deafened);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::move_user(uint32_t user_id, uint32_t channel_id) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* m = req.mutable_move_user();
|
||||
m->set_user_id(user_id);
|
||||
m->set_channel_id(channel_id);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::create_channel(const vc_channel_info* info) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
if (!info || !info->name) return VC_ERR_INVALID_ARG;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* cc = req.mutable_create_channel();
|
||||
*cc->mutable_channel() = channel_from_vc(*info);
|
||||
if (info->password_protected && info->password) cc->set_password(info->password);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::edit_channel(const vc_channel_info* info) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
if (!info || !info->name) return VC_ERR_INVALID_ARG;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* ec = req.mutable_edit_channel();
|
||||
*ec->mutable_channel() = channel_from_vc(*info);
|
||||
if (info->password_protected && info->password) ec->set_password(info->password);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::delete_channel(uint32_t channel_id) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
req.mutable_delete_channel()->set_channel_id(channel_id);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::create_account(const char* username, const char* password) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
if (!username || !password) return VC_ERR_INVALID_ARG;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* ca = req.mutable_create_account();
|
||||
ca->set_username(username);
|
||||
ca->set_password(password);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::reset_password(const char* username, const char* new_password) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
if (!username || !new_password) return VC_ERR_INVALID_ARG;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
auto* rp = req.mutable_reset_password();
|
||||
rp->set_username(username);
|
||||
rp->set_new_password(new_password);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::delete_account(const char* username) {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
if (!username) return VC_ERR_INVALID_ARG;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
req.mutable_delete_account()->set_username(username);
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::list_accounts() {
|
||||
if (state_net_.load(std::memory_order_acquire) != VC_STATE_CONNECTED) return VC_ERR_NOT_CONNECTED;
|
||||
voicecat::v1::Envelope req;
|
||||
req.set_request_id(next_req_id_++);
|
||||
req.mutable_list_accounts();
|
||||
queue_envelope(req);
|
||||
return VC_OK;
|
||||
}
|
||||
|
||||
vc_result vc_client::get_permissions(vc_permissions* out) {
|
||||
if (!out) return VC_ERR_INVALID_ARG;
|
||||
*out = own_permissions_;
|
||||
return state_net_.load(std::memory_order_acquire) == VC_STATE_CONNECTED ? VC_OK : VC_ERR_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
void vc_client::run_talk_timer() {
|
||||
while (!talk_timer_stop_.load(std::memory_order_acquire)) {
|
||||
// Remote streams: ask the engine for edge-triggered transitions, map ssrc -> (user,
|
||||
@@ -1413,5 +1632,18 @@ vc_result vc_client::get_server_identity_display(char*, size_t, size_t* out_len)
|
||||
if (out_len) *out_len = 0;
|
||||
return VC_ERR_NOT_IMPLEMENTED;
|
||||
}
|
||||
vc_result vc_client::kick_user(uint32_t, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::ban_user(uint32_t, const char*, uint64_t) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::set_permission(uint32_t, const vc_permissions*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::set_server_mute(uint32_t, bool, bool) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::move_user(uint32_t, uint32_t) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::create_channel(const vc_channel_info*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::edit_channel(const vc_channel_info*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::delete_channel(uint32_t) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::create_account(const char*, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::reset_password(const char*, const char*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::delete_account(const char*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::list_accounts() { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
vc_result vc_client::get_permissions(vc_permissions*) { return VC_ERR_NOT_IMPLEMENTED; }
|
||||
|
||||
#endif // VOICECAT_HAS_NET
|
||||
|
||||
@@ -77,6 +77,21 @@ struct vc_client {
|
||||
// TEST-ONLY (see voicecat.h) — inject synthetic PCM into a local stream's encode pipeline.
|
||||
vc_result test_inject_capture(uint32_t stream_id, const int16_t* pcm, size_t samples);
|
||||
|
||||
// M5: moderation & admin.
|
||||
vc_result kick_user(uint32_t user_id, const char* reason);
|
||||
vc_result ban_user(uint32_t user_id, const char* reason, uint64_t expires_unix_ms);
|
||||
vc_result set_permission(uint32_t user_id, const vc_permissions* perms);
|
||||
vc_result set_server_mute(uint32_t user_id, bool muted, bool deafened);
|
||||
vc_result move_user(uint32_t user_id, uint32_t channel_id);
|
||||
vc_result create_channel(const vc_channel_info* info);
|
||||
vc_result edit_channel(const vc_channel_info* info);
|
||||
vc_result delete_channel(uint32_t channel_id);
|
||||
vc_result create_account(const char* username, const char* password);
|
||||
vc_result reset_password(const char* username, const char* new_password);
|
||||
vc_result delete_account(const char* username);
|
||||
vc_result list_accounts();
|
||||
vc_result get_permissions(vc_permissions* out);
|
||||
|
||||
vc_connection_state state() const {
|
||||
#ifdef VOICECAT_HAS_NET
|
||||
return state_net_.load(std::memory_order_acquire);
|
||||
@@ -202,6 +217,11 @@ struct vc_client {
|
||||
|
||||
std::atomic<bool> self_mic_muted_{false};
|
||||
std::atomic<bool> self_deafened_{false};
|
||||
std::atomic<bool> server_muted_{false};
|
||||
std::atomic<bool> server_deafened_{false};
|
||||
|
||||
// M5: permissions from last AuthResult.
|
||||
vc_permissions own_permissions_{};
|
||||
|
||||
// Follow-up to M3: send-side input gate (docs/voice.md §11). MIC-only — SCREEN_AUDIO/
|
||||
// AUX_DEVICE are never gated (see PROGRESS.md for the rationale). Pure local state, no
|
||||
|
||||
@@ -66,11 +66,13 @@ void SessionModel::apply_snapshot(const voicecat::v1::ServerStateSnapshot& snap)
|
||||
users_.clear();
|
||||
for (const auto& pb : snap.users()) {
|
||||
User u;
|
||||
u.id = pb.id();
|
||||
u.nickname = pb.nickname();
|
||||
u.is_guest = pb.is_guest();
|
||||
u.channel_id = pb.channel_id();
|
||||
u.streams = copy_streams(pb.streams());
|
||||
u.id = pb.id();
|
||||
u.nickname = pb.nickname();
|
||||
u.is_guest = pb.is_guest();
|
||||
u.channel_id = pb.channel_id();
|
||||
u.server_muted = pb.server_muted();
|
||||
u.server_deafened = pb.server_deafened();
|
||||
u.streams = copy_streams(pb.streams());
|
||||
users_.push_back(std::move(u));
|
||||
}
|
||||
}
|
||||
@@ -81,11 +83,13 @@ void SessionModel::apply_user_event(const voicecat::v1::UserEvent& ev) {
|
||||
if (ev.kind() == Kind::JOINED || ev.kind() == Kind::UPDATED) {
|
||||
const auto& pb = ev.user();
|
||||
User u;
|
||||
u.id = pb.id();
|
||||
u.nickname = pb.nickname();
|
||||
u.is_guest = pb.is_guest();
|
||||
u.channel_id = pb.channel_id();
|
||||
u.streams = copy_streams(pb.streams());
|
||||
u.id = pb.id();
|
||||
u.nickname = pb.nickname();
|
||||
u.is_guest = pb.is_guest();
|
||||
u.channel_id = pb.channel_id();
|
||||
u.server_muted = pb.server_muted();
|
||||
u.server_deafened = pb.server_deafened();
|
||||
u.streams = copy_streams(pb.streams());
|
||||
|
||||
auto it = std::find_if(users_.begin(), users_.end(),
|
||||
[&](const User& x) { return x.id == u.id; });
|
||||
|
||||
@@ -51,6 +51,8 @@ struct User {
|
||||
std::string nickname;
|
||||
bool is_guest{true};
|
||||
uint32_t channel_id{0};
|
||||
bool server_muted{false};
|
||||
bool server_deafened{false};
|
||||
std::vector<Stream> streams;
|
||||
};
|
||||
|
||||
|
||||
@@ -202,9 +202,75 @@ vc_result vc_confirm_server_identity(vc_client* c, int accept) {
|
||||
}
|
||||
|
||||
vc_result vc_get_server_identity_display(vc_client* c, char* out_buf, size_t buf_cap,
|
||||
size_t* out_len) {
|
||||
size_t* out_len) {
|
||||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->get_server_identity_display(out_buf, buf_cap, out_len);
|
||||
}
|
||||
|
||||
vc_result vc_kick_user(vc_client* c, uint32_t user_id, const char* reason) {
|
||||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->kick_user(user_id, reason);
|
||||
}
|
||||
|
||||
vc_result vc_ban_user(vc_client* c, uint32_t user_id, const char* reason,
|
||||
uint64_t expires_unix_ms) {
|
||||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->ban_user(user_id, reason, expires_unix_ms);
|
||||
}
|
||||
|
||||
vc_result vc_set_permission(vc_client* c, uint32_t user_id, const vc_permissions* perms) {
|
||||
if (c == nullptr || perms == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->set_permission(user_id, perms);
|
||||
}
|
||||
|
||||
vc_result vc_set_server_mute(vc_client* c, uint32_t user_id, int muted, int deafened) {
|
||||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->set_server_mute(user_id, muted != 0, deafened != 0);
|
||||
}
|
||||
|
||||
vc_result vc_move_user(vc_client* c, uint32_t user_id, uint32_t channel_id) {
|
||||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->move_user(user_id, channel_id);
|
||||
}
|
||||
|
||||
vc_result vc_create_channel(vc_client* c, const vc_channel_info* info) {
|
||||
if (c == nullptr || info == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->create_channel(info);
|
||||
}
|
||||
|
||||
vc_result vc_edit_channel(vc_client* c, const vc_channel_info* info) {
|
||||
if (c == nullptr || info == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->edit_channel(info);
|
||||
}
|
||||
|
||||
vc_result vc_delete_channel(vc_client* c, uint32_t channel_id) {
|
||||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->delete_channel(channel_id);
|
||||
}
|
||||
|
||||
vc_result vc_create_account(vc_client* c, const char* username, const char* password) {
|
||||
if (c == nullptr || username == nullptr || password == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->create_account(username, password);
|
||||
}
|
||||
|
||||
vc_result vc_reset_password(vc_client* c, const char* username, const char* new_password) {
|
||||
if (c == nullptr || username == nullptr || new_password == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->reset_password(username, new_password);
|
||||
}
|
||||
|
||||
vc_result vc_delete_account(vc_client* c, const char* username) {
|
||||
if (c == nullptr || username == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->delete_account(username);
|
||||
}
|
||||
|
||||
vc_result vc_list_accounts(vc_client* c) {
|
||||
if (c == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->list_accounts();
|
||||
}
|
||||
|
||||
vc_result vc_get_permissions(vc_client* c, vc_permissions* out) {
|
||||
if (c == nullptr || out == nullptr) return VC_ERR_INVALID_ARG;
|
||||
return c->get_permissions(out);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
Reference in New Issue
Block a user