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:
2026-06-17 15:08:05 +02:00
parent a2f159e971
commit 3990f63f0f
23 changed files with 3281 additions and 97 deletions

View File

@@ -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

View File

@@ -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 (7079)
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 = 100109. 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; }

View File

@@ -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

View File

@@ -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

View File

@@ -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; });

View File

@@ -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;
};

View File

@@ -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"