using AppKit; using CoreGraphics; using VoiceCat.Core; using Voicecat.V1; using Channel = Voicecat.V1.Channel; namespace VoiceCat.Mac; internal sealed class AdministrationWindowController : NSWindowController { private readonly VoiceCatClient client; private readonly uint selfId; private readonly NSPopUpButton users = new(new CGRect(25, 335, 310, 28), false); private readonly NSPopUpButton channels = new(new CGRect(25, 175, 310, 28), false); private readonly NSTextField status = NSTextField.CreateLabel(""); internal AdministrationWindowController(VoiceCatClient client, uint selfId) : base(new NSWindow(new CGRect(0, 0, 560, 420), NSWindowStyle.Titled | NSWindowStyle.Closable, NSBackingStore.Buffered, false)) { this.client = client; this.selfId = selfId; Window!.Title = "VoiceCat Administration"; Window.Center(); NSView view = Window.ContentView!; AddLabel(view, "User moderation", 25, 375); view.AddSubview(users); AddButton(view, "Move…", 350, 333, Move); AddButton(view, "Kick…", 440, 333, Kick); AddButton(view, "Ban…", 350, 293, Ban); AddButton(view, "Permissions…", 440, 293, Permissions); AddButton(view, "Server mute", 350, 253, ServerMute); AddButton(view, "Server deafen", 440, 253, ServerDeafen); AddButton(view, "Audio tuning…", 25, 253, Tune); AddLabel(view, "Channel administration", 25, 215); view.AddSubview(channels); AddButton(view, "Create…", 350, 173, CreateChannel); AddButton(view, "Edit…", 440, 173, EditChannel); AddButton(view, "Delete", 350, 133, DeleteChannel); AddButton(view, "Accounts…", 440, 133, Accounts); var refresh = new NSButton(new CGRect(25, 75, 100, 32)) { Title = "Refresh" }; refresh.Activated += (_, _) => Refresh(); view.AddSubview(refresh); status.Frame = new CGRect(25, 25, 510, 35); status.AccessibilityLabel = "Administration result"; view.AddSubview(status); ((INSAccessibility)users).AccessibilityLabel = "User to administer"; ((INSAccessibility)channels).AccessibilityLabel = "Channel to administer"; Refresh(); ApplyPermissions(); } private void Refresh() { uint selectedUser = Selected(users), selectedChannel = Selected(channels); users.RemoveAllItems(); foreach (User user in client.Users.Where(user => user.Id != selfId).OrderBy(user => user.Nickname)) Add(users, user.Nickname, user.Id); channels.RemoveAllItems(); foreach (Channel channel in client.Channels.OrderBy(channel => channel.Order).ThenBy(channel => channel.Name)) Add(channels, channel.Name, channel.Id); Select(users, selectedUser); Select(channels, selectedChannel); } private void ApplyPermissions() { Permissions permissions = client.Permissions; status.StringValue = permissions.IsAdmin ? "Connected as administrator" : "Only actions allowed by your server permissions will succeed."; } private async void Move(object? sender, EventArgs args) { uint userId = Selected(users); if (userId == 0) return; string[] names = client.Channels.Select(channel => channel.Name).ToArray(); int index = PromptChoice("Move user", "Destination channel", names); if (index < 0) return; Show(await client.MoveUserAsync(userId, client.Channels[index].Id)); } private async void Kick(object? sender, EventArgs args) { uint id = Selected(users); string? reason = PromptText("Kick user", "Reason", "Kicked by moderator"); if (id != 0 && reason is not null) Show(await client.KickUserAsync(id, reason)); } private async void Ban(object? sender, EventArgs args) { uint id = Selected(users); if (id == 0) return; string? reason = PromptText("Ban user", "Reason (append @1h, @1d, or @permanent)", "Banned by moderator @1d"); if (reason is null) return; ulong expiry = 0; string lowered = reason.ToLowerInvariant(); if (lowered.EndsWith("@1h")) expiry = checked((ulong)DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeMilliseconds()); else if (lowered.EndsWith("@1d")) expiry = checked((ulong)DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeMilliseconds()); Show(await client.BanUserAsync(id, reason.Replace("@1h", "", StringComparison.OrdinalIgnoreCase).Replace("@1d", "", StringComparison.OrdinalIgnoreCase).Replace("@permanent", "", StringComparison.OrdinalIgnoreCase).Trim(), expiry)); } private async void Permissions(object? sender, EventArgs args) { uint id = Selected(users); if (id == 0) return; Permissions? permissions = PermissionEditor.Run(); if (permissions is not null) Show(await client.SetPermissionsAsync(id, permissions)); } private async void ServerMute(object? sender, EventArgs args) { User? user = User(); if (user is not null) Show(await client.SetServerMuteAsync(user.Id, !user.ServerMuted, user.ServerDeafened)); } private async void ServerDeafen(object? sender, EventArgs args) { User? user = User(); if (user is not null) Show(await client.SetServerMuteAsync(user.Id, user.ServerMuted, !user.ServerDeafened)); } private void Tune(object? sender, EventArgs args) { User? user = User(); if (user is not null) PerUserTuning.Run(client, user); } private async void CreateChannel(object? sender, EventArgs args) { ChannelEdit? edit = ChannelEditor.Run(null, client.Channels); if (edit is not null) { Show(await client.CreateChannelAsync(edit.Channel, edit.Password)); Refresh(); } } private async void EditChannel(object? sender, EventArgs args) { Channel? channel = Channel(); if (channel is null) return; ChannelEdit? edit = ChannelEditor.Run(channel, client.Channels); if (edit is not null) { Show(await client.EditChannelAsync(edit.Channel, edit.Password)); Refresh(); } } private async void DeleteChannel(object? sender, EventArgs args) { Channel? channel = Channel(); if (channel is null || channel.Id == 1) return; if (!Confirm("Delete channel?", channel.Name)) return; Show(await client.DeleteChannelAsync(channel.Id)); Refresh(); } private async void Accounts(object? sender, EventArgs args) => await AccountEditor.RunAsync(client); private User? User() => client.Users.FirstOrDefault(user => user.Id == Selected(users)); private Channel? Channel() => client.Channels.FirstOrDefault(channel => channel.Id == Selected(channels)); private void Show(GenericResult result) { status.StringValue = result.Ok ? result.Message.Length == 0 ? "Operation completed." : result.Message : result.Message; Refresh(); } private static void Add(NSPopUpButton picker, string title, uint id) { picker.AddItem(title); picker.LastItem!.RepresentedObject = new Foundation.NSString(id.ToString()); } private static uint Selected(NSPopUpButton picker) => uint.TryParse(picker.SelectedItem?.RepresentedObject?.ToString(), out uint id) ? id : 0; private static void Select(NSPopUpButton picker, uint id) { for (nint i = 0; i < picker.ItemCount; i++) if (picker.ItemAtIndex(i)?.RepresentedObject?.ToString() == id.ToString()) { picker.SelectItem(i); break; } } private static void AddLabel(NSView view, string value, double x, double y) { NSTextField label = NSTextField.CreateLabel(value); label.Frame = new CGRect(x, y, 300, 22); label.Font = NSFont.BoldSystemFontOfSize(13)!; view.AddSubview(label); } private static void AddButton(NSView view, string title, double x, double y, EventHandler handler) { var button = new NSButton(new CGRect(x, y, 95, 32)) { Title = title }; button.Activated += handler; view.AddSubview(button); } internal static string? PromptText(string title, string prompt, string value = "", bool secure = false) { NSTextField field = secure ? new NSSecureTextField(new CGRect(0, 0, 340, 26)) : new NSTextField(new CGRect(0, 0, 340, 26)); field.StringValue = value; var alert = new NSAlert { MessageText = title, InformativeText = prompt, AccessoryView = field }; alert.AddButton("OK"); alert.AddButton("Cancel"); return alert.RunModal() == 1000 ? field.StringValue : null; } internal static int PromptChoice(string title, string prompt, string[] values) { var picker = new NSPopUpButton(new CGRect(0, 0, 340, 28), false); picker.AddItems(values); var alert = new NSAlert { MessageText = title, InformativeText = prompt, AccessoryView = picker }; alert.AddButton("OK"); alert.AddButton("Cancel"); return alert.RunModal() == 1000 ? checked((int)picker.IndexOfSelectedItem) : -1; } internal static bool Confirm(string title, string detail) { var alert = new NSAlert { MessageText = title, InformativeText = detail, AlertStyle = NSAlertStyle.Warning }; alert.AddButton("Continue"); alert.AddButton("Cancel"); return alert.RunModal() == 1000; } } internal sealed record ChannelEdit(Channel Channel, string Password); internal static class PermissionEditor { internal static Permissions? Run() { string[] labels = ["Create temporary channels", "Kick users", "Ban users", "Move users", "Manage accounts", "Full administrator"]; var view = new NSView(new CGRect(0, 0, 330, 160)); var checks = new List(); for (int i = 0; i < labels.Length; i++) { NSButton check = NSButton.CreateCheckbox(labels[i], () => { }); check.Frame = new CGRect(0, 135 - i * 25, 320, 22); view.AddSubview(check); checks.Add(check); } var alert = new NSAlert { MessageText = "Set permissions", AccessoryView = view }; alert.AddButton("Save"); alert.AddButton("Cancel"); if (alert.RunModal() != 1000) return null; return new() { CanCreateTempChannel = On(0), CanKick = On(1), CanBan = On(2), CanMoveUsers = On(3), CanAdminAccounts = On(4), IsAdmin = On(5) }; bool On(int i) => checks[i].State == NSCellStateValue.On; } } internal static class PerUserTuning { internal static void Run(VoiceCatClient client, User user) { if (user.Streams.Count == 0) { AdministrationWindowController.PromptText("Audio tuning", "This user has no active streams."); return; } var stream = new NSPopUpButton(new CGRect(0, 95, 340, 28), false); stream.AddItems(user.Streams.Select(value => $"{value.Label} ({value.Kind})").ToArray()); var gain = new NSSlider(new CGRect(0, 60, 340, 24)) { MinValue = 0, MaxValue = 400, DoubleValue = 100 }; var muted = NSButton.CreateCheckbox("Mute this stream", () => { }); muted.Frame = new CGRect(0, 30, 160, 24); var noise = NSButton.CreateCheckbox("Receive noise reduction", () => { }); noise.Frame = new CGRect(170, 30, 170, 24); var view = new NSView(new CGRect(0, 0, 340, 130)); view.AddSubview(stream); view.AddSubview(gain); view.AddSubview(muted); view.AddSubview(noise); void LoadSelected() { if (stream.IndexOfSelectedItem < 0) return; StreamInfo selected = user.Streams[checked((int)stream.IndexOfSelectedItem)]; (float Gain, bool Muted, bool NoiseReduction)? state = client.Audio.GetRemotePlayback(user.Id, selected.StreamId); gain.DoubleValue = (state?.Gain ?? 1) * 100; muted.State = state?.Muted == true ? NSCellStateValue.On : NSCellStateValue.Off; noise.State = state?.NoiseReduction == true ? NSCellStateValue.On : NSCellStateValue.Off; } stream.Activated += (_, _) => LoadSelected(); LoadSelected(); var alert = new NSAlert { MessageText = $"Audio — {user.Nickname}", InformativeText = "Stream, volume, mute, and receive noise reduction", AccessoryView = view }; alert.AddButton("Apply"); alert.AddButton("Cancel"); if (alert.RunModal() != 1000) return; StreamInfo selected = user.Streams[checked((int)stream.IndexOfSelectedItem)]; client.Audio.SetRemotePlayback(user.Id, selected.StreamId, (float)gain.DoubleValue / 100, muted.State == NSCellStateValue.On, noise.State == NSCellStateValue.On); } } internal static class AccountEditor { internal static async Task RunAsync(VoiceCatClient client) { IReadOnlyList accounts = await client.ListAccountsAsync(); string[] actions = ["Create account", "Reset password", "Delete account", "Close"]; int action = AdministrationWindowController.PromptChoice("Server accounts", string.Join("\n", accounts.Select(a => $"{a.Username}{(a.IsAdmin ? " (admin)" : "")}")), actions); if (action is < 0 or 3) return; if (action == 0) { string? name = AdministrationWindowController.PromptText("Create account", "Username"); if (string.IsNullOrWhiteSpace(name)) return; string? password = AdministrationWindowController.PromptText("Create account", "Password", secure: true); if (!string.IsNullOrEmpty(password)) await client.CreateAccountAsync(name.Trim(), password); return; } int selected = AdministrationWindowController.PromptChoice(action == 1 ? "Reset password" : "Delete account", "Account", accounts.Select(a => a.Username).ToArray()); if (selected < 0) return; if (action == 1) { string? password = AdministrationWindowController.PromptText("Reset password", "New password", secure: true); if (!string.IsNullOrEmpty(password)) await client.ResetPasswordAsync(accounts[selected].Username, password); } else if (AdministrationWindowController.Confirm("Delete account?", accounts[selected].Username)) await client.DeleteAccountAsync(accounts[selected].Username); } }