M5: Windows client moderation UI; add C ABI getters for account list, user mute/deafen, channel topic

This commit is contained in:
2026-06-17 16:31:29 +02:00
parent 3990f63f0f
commit 9b321d0d4f
24 changed files with 1723 additions and 30 deletions

View File

@@ -88,6 +88,10 @@ public enum VcEventType
JoinResult = 12,
/// <summary>M4: the TOFU server-identity gate — see VcTofuStatus.</summary>
ServerIdentity = 13,
/// <summary>M5: async result for moderation/admin/channel operations.</summary>
GenericResult = 14,
/// <summary>M5: reply to VoiceCatClient.RequestAccountList — call ListAccounts() to read.</summary>
AccountList = 15,
}
/// <summary>

View File

@@ -36,6 +36,7 @@ internal static class Marshaling
raw.Id,
raw.ParentId,
Marshal.PtrToStringUTF8(raw.Name) ?? string.Empty,
Marshal.PtrToStringUTF8(raw.Topic) ?? string.Empty,
raw.PasswordProtected != 0,
raw.MaxUsers));
}
@@ -54,7 +55,11 @@ internal static class Marshaling
raw.Id,
Marshal.PtrToStringUTF8(raw.Nickname) ?? string.Empty,
raw.IsGuest != 0,
raw.ChannelId));
raw.ChannelId,
raw.SelfMicMuted != 0,
raw.SelfDeafened != 0,
raw.ServerMuted != 0,
raw.ServerDeafened != 0));
}
NativeMethods.vc_free_user_list(ref native);
return result;
@@ -87,4 +92,29 @@ internal static class Marshaling
native.ExpectedPacketLoss,
native.Dtx != 0,
native.Complexity);
public static PermissionsInfo ToManaged(in VcPermissionsNative native) => new(
native.CanCreateTempChannel != 0,
native.CanKick != 0,
native.CanBan != 0,
native.CanMoveUsers != 0,
native.CanAdminAccounts != 0,
native.IsAdmin != 0);
public static List<AccountInfo> ToManaged(ref VcAccountListNative native)
{
var result = new List<AccountInfo>((int)native.Count);
int size = Marshal.SizeOf<VcAccountNative>();
for (nuint i = 0; i < native.Count; i++)
{
var raw = Marshal.PtrToStructure<VcAccountNative>(native.Items + (int)i * size);
result.Add(new AccountInfo(
Marshal.PtrToStringUTF8(raw.Username) ?? string.Empty,
raw.IsAdmin != 0,
raw.CreatedAtUnixMs,
raw.LastLoginUnixMs));
}
NativeMethods.vc_free_account_list(ref native);
return result;
}
}

View File

@@ -7,14 +7,44 @@ public sealed record ChannelInfo(
uint Id,
uint ParentId,
string Name,
string Topic,
bool PasswordProtected,
uint MaxUsers);
public sealed record ChannelEditInfo(
uint Id,
uint ParentId,
string Name,
string Topic,
bool PasswordProtected,
string? Password,
uint MaxUsers,
uint SortOrder,
AudioConfigInfo Audio);
public sealed record UserInfo(
uint Id,
string Nickname,
bool IsGuest,
uint ChannelId);
uint ChannelId,
bool SelfMicMuted,
bool SelfDeafened,
bool ServerMuted,
bool ServerDeafened);
public sealed record PermissionsInfo(
bool CanCreateTempChannel,
bool CanKick,
bool CanBan,
bool CanMoveUsers,
bool CanAdminAccounts,
bool IsAdmin);
public sealed record AccountInfo(
string Username,
bool IsAdmin,
ulong CreatedAtUnixMs,
ulong LastLoginUnixMs);
public sealed record StreamSummary(
uint StreamId,

View File

@@ -131,4 +131,54 @@ internal static partial class NativeMethods
[LibraryImport(LibName)]
internal static partial VcResult vc_get_server_identity_display(nint c, nint outBuf,
nuint bufCap, out nuint outLen);
// ── M5: Moderation & admin ─────────────────────────────────────────────────────────────
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_kick_user(nint c, uint userId, string? reason);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_ban_user(nint c, uint userId, string? reason,
ulong expiresUnixMs);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_permission(nint c, uint userId,
in VcPermissionsNative perms);
[LibraryImport(LibName)]
internal static partial VcResult vc_set_server_mute(nint c, uint userId, int muted,
int deafened);
[LibraryImport(LibName)]
internal static partial VcResult vc_move_user(nint c, uint userId, uint channelId);
[LibraryImport(LibName)]
internal static partial VcResult vc_create_channel(nint c, in VcChannelInfoNative info);
[LibraryImport(LibName)]
internal static partial VcResult vc_edit_channel(nint c, in VcChannelInfoNative info);
[LibraryImport(LibName)]
internal static partial VcResult vc_delete_channel(nint c, uint channelId);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_create_account(nint c, string username, string password);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_reset_password(nint c, string username,
string newPassword);
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
internal static partial VcResult vc_delete_account(nint c, string username);
[LibraryImport(LibName)]
internal static partial VcResult vc_list_accounts(nint c);
[LibraryImport(LibName)]
internal static partial VcResult vc_get_account_list(nint c, out VcAccountListNative outList);
[LibraryImport(LibName)]
internal static partial void vc_free_account_list(ref VcAccountListNative list);
[LibraryImport(LibName)]
internal static partial VcResult vc_get_permissions(nint c, out VcPermissionsNative outPerms);
}

View File

@@ -63,6 +63,31 @@ internal struct VcAudioConfigNative
public uint Complexity;
}
[StructLayout(LayoutKind.Sequential)]
internal struct VcPermissionsNative
{
public int CanCreateTempChannel;
public int CanKick;
public int CanBan;
public int CanMoveUsers;
public int CanAdminAccounts;
public int IsAdmin;
}
[StructLayout(LayoutKind.Sequential)]
internal struct VcChannelInfoNative
{
public uint Id;
public uint ParentId;
public IntPtr Name;
public IntPtr Topic;
public int PasswordProtected;
public IntPtr Password;
public uint MaxUsers;
public uint SortOrder;
public VcAudioConfigNative Audio;
}
[StructLayout(LayoutKind.Sequential)]
internal struct VcDeviceNative
{
@@ -84,6 +109,7 @@ internal struct VcChannelNative
public uint Id;
public uint ParentId;
public IntPtr Name;
public IntPtr Topic;
public int PasswordProtected;
public uint MaxUsers;
}
@@ -102,6 +128,10 @@ internal struct VcUserNative
public IntPtr Nickname;
public int IsGuest;
public uint ChannelId;
public int SelfMicMuted;
public int SelfDeafened;
public int ServerMuted;
public int ServerDeafened;
}
[StructLayout(LayoutKind.Sequential)]
@@ -125,3 +155,19 @@ internal struct VcStreamSummaryListNative
public IntPtr Items;
public nuint Count;
}
[StructLayout(LayoutKind.Sequential)]
internal struct VcAccountNative
{
public IntPtr Username;
public int IsAdmin;
public ulong CreatedAtUnixMs;
public ulong LastLoginUnixMs;
}
[StructLayout(LayoutKind.Sequential)]
internal struct VcAccountListNative
{
public IntPtr Items;
public nuint Count;
}

View File

@@ -215,6 +215,124 @@ public sealed class VoiceCatClient : IDisposable
return (r, r == VcResult.Ok ? Marshaling.ToManaged(in native) : null);
}
// ── M5: Moderation & admin ───────────────────────────────────────────────────────────
public VcResult KickUser(uint userId, string? reason = null) =>
NativeMethods.vc_kick_user(_handle.DangerousGetHandle(), userId, reason);
public VcResult BanUser(uint userId, string? reason = null, ulong expiresUnixMs = 0) =>
NativeMethods.vc_ban_user(_handle.DangerousGetHandle(), userId, reason, expiresUnixMs);
public VcResult SetPermission(uint userId, PermissionsInfo perms)
{
var native = new VcPermissionsNative
{
CanCreateTempChannel = perms.CanCreateTempChannel ? 1 : 0,
CanKick = perms.CanKick ? 1 : 0,
CanBan = perms.CanBan ? 1 : 0,
CanMoveUsers = perms.CanMoveUsers ? 1 : 0,
CanAdminAccounts = perms.CanAdminAccounts ? 1 : 0,
IsAdmin = perms.IsAdmin ? 1 : 0,
};
return NativeMethods.vc_set_permission(_handle.DangerousGetHandle(), userId, in native);
}
public VcResult SetServerMute(uint userId, bool muted, bool deafened) =>
NativeMethods.vc_set_server_mute(_handle.DangerousGetHandle(), userId,
muted ? 1 : 0, deafened ? 1 : 0);
public VcResult MoveUser(uint userId, uint channelId) =>
NativeMethods.vc_move_user(_handle.DangerousGetHandle(), userId, channelId);
public VcResult CreateChannel(ChannelEditInfo info)
{
var native = ToNativeChannelInfo(info);
try
{
return NativeMethods.vc_create_channel(_handle.DangerousGetHandle(), in native);
}
finally
{
FreeChannelInfoStrings(native);
}
}
public VcResult EditChannel(ChannelEditInfo info)
{
var native = ToNativeChannelInfo(info);
try
{
return NativeMethods.vc_edit_channel(_handle.DangerousGetHandle(), in native);
}
finally
{
FreeChannelInfoStrings(native);
}
}
public VcResult DeleteChannel(uint channelId) =>
NativeMethods.vc_delete_channel(_handle.DangerousGetHandle(), channelId);
public VcResult CreateAccount(string username, string password) =>
NativeMethods.vc_create_account(_handle.DangerousGetHandle(), username, password);
public VcResult ResetPassword(string username, string newPassword) =>
NativeMethods.vc_reset_password(_handle.DangerousGetHandle(), username, newPassword);
public VcResult DeleteAccount(string username) =>
NativeMethods.vc_delete_account(_handle.DangerousGetHandle(), username);
public VcResult RequestAccountList() =>
NativeMethods.vc_list_accounts(_handle.DangerousGetHandle());
public List<AccountInfo> ListAccounts()
{
NativeMethods.vc_get_account_list(_handle.DangerousGetHandle(), out var native);
return Marshaling.ToManaged(ref native);
}
public PermissionsInfo GetPermissions()
{
NativeMethods.vc_get_permissions(_handle.DangerousGetHandle(), out var native);
return Marshaling.ToManaged(in native);
}
private static VcChannelInfoNative ToNativeChannelInfo(ChannelEditInfo info)
{
return new VcChannelInfoNative
{
Id = info.Id,
ParentId = info.ParentId,
Name = Marshal.StringToCoTaskMemUTF8(info.Name),
Topic = Marshal.StringToCoTaskMemUTF8(info.Topic),
PasswordProtected = info.PasswordProtected ? 1 : 0,
Password = string.IsNullOrEmpty(info.Password)
? 0
: Marshal.StringToCoTaskMemUTF8(info.Password),
MaxUsers = info.MaxUsers,
SortOrder = info.SortOrder,
Audio = new VcAudioConfigNative
{
Codec = info.Audio.Codec,
Mode = info.Audio.Stereo ? 1u : 0u,
SampleRate = info.Audio.SampleRate,
BitrateBps = info.Audio.BitrateBps,
FrameMs = info.Audio.FrameMs,
Application = info.Audio.Application,
Fec = info.Audio.Fec ? 1 : 0,
ExpectedPacketLoss = info.Audio.ExpectedPacketLoss,
Dtx = info.Audio.Dtx ? 1 : 0,
Complexity = info.Audio.Complexity,
}
};
}
private static void FreeChannelInfoStrings(VcChannelInfoNative native)
{
if (native.Name != 0) Marshal.FreeCoTaskMem(native.Name);
if (native.Topic != 0) Marshal.FreeCoTaskMem(native.Topic);
if (native.Password != 0) Marshal.FreeCoTaskMem(native.Password);
}
// ── Text ─────────────────────────────────────────────────────────────────────────────
public VcResult SendText(VcTextScope scope, uint targetId, string utf8) =>
NativeMethods.vc_send_text(_handle.DangerousGetHandle(), scope, targetId, utf8);