using axXez.TS3.Protocol; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; using System.Text.RegularExpressions; using System.Threading; namespace axXez.TS3.Bot.Modules { [Module(Namespace = "axXez", DisplayName = "Team Channels")] internal class TeamChannels : Module { public int DeleteDelay { get; set; } = 180; public bool DeleteUnmatched { get; set; } = false; public List FillSubChannelList { get; set; } = new(); private Timer _updateTimer = null; protected override void OnStart() { foreach (var setting in FillSubChannelList) CheckChannel(setting); _updateTimer = new Timer(_ => { foreach (var setting in FillSubChannelList) CheckChannel(setting); }, null, 10000, 10000); } protected override void OnStop(bool queryConnected) { _updateTimer.Dispose(); } protected override void OnUnload() { //base.OnUnload(); //don't auto save config } private void CheckChannel(AutoFillData data) { if (!Server.FindChannel(data.ChannelName, out var parentChannel)) return; if (data.GeneratedChannels == null) { data.GeneratedChannels = new(); if (Server.FindSubChannels(parentChannel.ID, out var subChannels)) { string evaluatedPattern = Regex.Replace(data.NamePattern, "\\$1", "(?[0-9]+)"); foreach (var subChannel in subChannels) { var match = Regex.Match(subChannel.Name, evaluatedPattern); if (!match.Success) { if (DeleteUnmatched) Query.DeleteChannel(subChannel.ID); continue; } int channelNumber = int.Parse(match.Groups["num"].Value); if (!data.GeneratedChannels.ContainsKey(channelNumber)) data.GeneratedChannels.Add(channelNumber, subChannel.ID); } } } var allInUse = true; var teamChannelNumber = 0; foreach (var e in data.GeneratedChannels.OrderBy(e => e.Key).ToList()) { teamChannelNumber++; if (!Server.FindChannel(e.Value, out Channel channel)) { data.GeneratedChannels.Remove(e.Key); teamChannelNumber--; continue; } if (teamChannelNumber != e.Key) { if (Query.EditChannel(e.Value, new() { Name = data.NamePattern.Replace("$1", teamChannelNumber.ToString()) })) { data.GeneratedChannels.Remove(e.Key); data.GeneratedChannels.Add(teamChannelNumber, e.Value); } else { //rename failed... channel number already exists? rescan? return; } } if (teamChannelNumber == data.GeneratedChannels.Count && !allInUse && channel.SecondsEmpty > DeleteDelay && data.GeneratedChannels.Count > data.MinSubChannels) { DeleteChannel(data, e.Value, teamChannelNumber); return; } if (channel.SecondsEmpty > 0) allInUse = false; } var count = data.GeneratedChannels.Count; if (data.GeneratedChannels.Count < data.MinSubChannels) { for (int i = count; i < data.MinSubChannels; i++) if (!AddChannel(data, parentChannel, i + 1)) return; } else if (allInUse && count < data.MaxSubChannels) { AddChannel(data, parentChannel, count + 1); } } private bool AddChannel(AutoFillData data, Channel parent, int number) { var name = data.NamePattern.Replace("$1", number.ToString()); var id = Query.CreateChannel(new EditChannelProperties() { Name = name, SemiPermanentFlag = true, Description = "auto generated", ParentId = parent.ID }); if (id == -1) return false; data.GeneratedChannels.Add(number, id); Log($"addChannel: [{parent.ID}]{parent.Name} -> [{id}]{name}"); return true; } private void DeleteChannel(AutoFillData data, int channelId, int number) { data.GeneratedChannels.Remove(number); Query.DeleteChannel(channelId); } [Command("tc list", Description = "Lists all configured team channel groups")] public CommandResponse List(CommandContext ctx) { if (FillSubChannelList.Count == 0) return ctx.Ok("No team channel groups configured."); ctx.Reply($"[B]Team Channels[/B] — delete delay: [B]{DeleteDelay}s[/B]"); foreach (var entry in FillSubChannelList) { var active = entry.GeneratedChannels?.Count.ToString() ?? "?"; ctx.Reply($" [B]{entry.ChannelName}[/B] | min: {entry.MinSubChannels} | max: {entry.MaxSubChannels} | pattern: [B]{entry.NamePattern}[/B] | active: {active}"); } return ctx.Ok(); } [Command("tc add", Description = "Adds a team channel group", Role = CommandRole.Admin)] public CommandResponse Add(CommandContext ctx, [CommandParam("channel")] string channelName, int min, int max, [CommandParam("pattern")] string pattern) { if (!pattern.Contains("$1")) return ctx.Error("Pattern must contain [B]$1[/B] as the number placeholder."); if (FillSubChannelList.Any(x => x.ChannelName.Equals(channelName, System.StringComparison.InvariantCultureIgnoreCase))) return ctx.Error($"A group for channel [B]{channelName}[/B] already exists."); var entry = new AutoFillData(channelName, min, max, pattern); FillSubChannelList.Add(entry); Save(); CheckChannel(entry); return ctx.Ok($"Team channel group added for [B]{channelName}[/B] (min: {min}, max: {max}, pattern: [B]{pattern}[/B])."); } [Command("tc remove", Description = "Removes a team channel group", Role = CommandRole.Admin)] public CommandResponse Remove(CommandContext ctx, [CommandParam("channel")] string channelName) { int removed = FillSubChannelList.RemoveAll(x => x.ChannelName.Equals(channelName, System.StringComparison.InvariantCultureIgnoreCase)); if (removed == 0) return ctx.Error($"No group found for channel [B]{channelName}[/B]."); Save(); return ctx.Ok($"Team channel group for [B]{channelName}[/B] removed."); } [Command("tc delay", Description = "Gets or sets the delete delay in seconds", Role = CommandRole.Admin)] public CommandResponse Delay(CommandContext ctx, int seconds = -1, bool save = false) { if (seconds == -1) { return ctx.Ok($"Delete delay: [B]{DeleteDelay}s[/B]"); } DeleteDelay = seconds; if (save) Save(); ctx.Reply($"Delete delay set to [B]{seconds}s[/B]{(save ? " and saved." : ".")}"); return ctx.Ok(); } public class AutoFillData { public string ChannelName { get; set; } public int MinSubChannels { get; set; } public int MaxSubChannels { get; set; } public string NamePattern { get; set; } [JsonIgnore] public Dictionary GeneratedChannels { get; set; } public AutoFillData() { } public AutoFillData(string channelName, int min, int max, string pattern) { ChannelName = channelName; MinSubChannels = min; MaxSubChannels = max; NamePattern = pattern; } } } }