From 795978c1e8f5299d2c9a1bca58419a7e844b403e Mon Sep 17 00:00:00 2001 From: Ryan Whytsell Date: Tue, 8 Jul 2025 18:35:29 -0400 Subject: [PATCH] Initial commit, working --- .dockerignore | 25 +++++++++++ .gitignore | 5 +++ Dockerfile | 23 ++++++++++ Handlers/VoiceStatesHandler.cs | 22 ++++++++++ Program.cs | 49 +++++++++++++++++++++ Properties/launchSettings.json | 15 +++++++ Providers/PhraseProvider.cs | 27 ++++++++++++ SquadBot.csproj | 23 ++++++++++ SquadBot.http | 11 +++++ SquadBot.sln | 17 +++++++ StateMangers/VoiceStateManager.cs | 73 +++++++++++++++++++++++++++++++ appsettings.Development.json | 8 ++++ appsettings.json | 12 +++++ global.json | 7 +++ 14 files changed, 317 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Handlers/VoiceStatesHandler.cs create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 Providers/PhraseProvider.cs create mode 100644 SquadBot.csproj create mode 100644 SquadBot.http create mode 100644 SquadBot.sln create mode 100644 StateMangers/VoiceStateManager.cs create mode 100644 appsettings.Development.json create mode 100644 appsettings.json create mode 100644 global.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..38bece4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6e9cb2d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["SquadBot.csproj", "./"] +RUN dotnet restore "SquadBot.csproj" +COPY . . +WORKDIR "/src/" +RUN dotnet build "./SquadBot.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./SquadBot.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "SquadBot.dll"] diff --git a/Handlers/VoiceStatesHandler.cs b/Handlers/VoiceStatesHandler.cs new file mode 100644 index 0000000..a4edec7 --- /dev/null +++ b/Handlers/VoiceStatesHandler.cs @@ -0,0 +1,22 @@ +using NetCord.Gateway; +using NetCord.Hosting.Gateway; +using SquadBot.StateMangers; + +namespace SquadBot.Handlers; + +public class VoiceStatesHandler : IVoiceStateUpdateGatewayHandler +{ + private ILogger _logger; + private VoiceStateManager _voiceStateManager; + public VoiceStatesHandler(ILogger Logger, VoiceStateManager vsManager) + { + _logger = Logger ?? throw new System.ArgumentNullException(nameof(Logger)); + _voiceStateManager = vsManager ?? throw new System.ArgumentNullException(nameof(vsManager)); + } + + public async ValueTask HandleAsync(VoiceState voice_event) + { + if (voice_event.User == null) return; + await _voiceStateManager.HandleVoiceStateChange(voice_event); + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..cd4ff3c --- /dev/null +++ b/Program.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; +using NetCord; +using NetCord.Gateway; +using NetCord.Hosting.Gateway; +using NetCord.Hosting.Rest; +using SquadBot.Handlers; +using SquadBot.StateMangers; + +namespace SquadBot; + +public class Program +{ + + public static async Task Main(string[] args) + { + var token = ""; + var pubkey = ""; + if (args.Length == 2) + { + token = args[0]; + pubkey = args[1]; + } + else + { + Console.Error.WriteLine("Usage: squadbot "); + } + + var builder = Host.CreateApplicationBuilder(args); + builder.Services.AddLogging(); + builder.Services.AddDiscordRest(options => + { + options.PublicKey = pubkey; + options.Token = token; + }); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddDiscordGateway(options => + { + options.Intents = GatewayIntents.GuildMessages + | GatewayIntents.GuildVoiceStates; + options.Token = token; + }) + .AddGatewayHandlers(typeof(Program).Assembly); + + var host = builder.Build() + .UseGatewayHandlers(); + await host.RunAsync(); + } +} \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..ef9ed95 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "todos", + "applicationUrl": "http://localhost:13376", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Providers/PhraseProvider.cs b/Providers/PhraseProvider.cs new file mode 100644 index 0000000..3f54ef5 --- /dev/null +++ b/Providers/PhraseProvider.cs @@ -0,0 +1,27 @@ +using System.Security.Cryptography; + +namespace SquadBot.Providers; + +public static class PhraseProvider +{ + private static Random rng = new Random(); + + private static List phrases = new List() + { + "there is a new yapper in the voice chat.", + "someone is in the lobby, go irritate the shit out of them.", + "voice chat: Because typing is so last century.", + "come join the voice chat! If not we will talk about you.", + "join the voice chat. It's the only way to prove you're not a bot.", + "join the voice chat. We're not saying it'll be good, but it'll be something.", + "who needs friends in real life? Someone is in the voice chat!", + "someone is pretending to be productive in the voice chat.", + "the memes are better dubbed", + "voice chat's active. It's probably a disaster, but come anyway." + }; + + public static string GetYapperPhrase() + { + return phrases[rng.Next(0, phrases.Count)]; + } +} \ No newline at end of file diff --git a/SquadBot.csproj b/SquadBot.csproj new file mode 100644 index 0000000..8b5825e --- /dev/null +++ b/SquadBot.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + true + true + Linux + + + + false + + + + + + + + + + diff --git a/SquadBot.http b/SquadBot.http new file mode 100644 index 0000000..f0ad6f5 --- /dev/null +++ b/SquadBot.http @@ -0,0 +1,11 @@ +@SquadBot_HostAddress = http://localhost:13376 + +GET {{SquadBot_HostAddress}}/todos/ +Accept: application/json + +### + +GET {{SquadBot_HostAddress}}/todos/1 +Accept: application/json + +### diff --git a/SquadBot.sln b/SquadBot.sln new file mode 100644 index 0000000..5fe20bf --- /dev/null +++ b/SquadBot.sln @@ -0,0 +1,17 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SquadBot", "SquadBot.csproj", "{910F9D0B-51BD-4811-A2ED-DBD79BF02CCF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {910F9D0B-51BD-4811-A2ED-DBD79BF02CCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {910F9D0B-51BD-4811-A2ED-DBD79BF02CCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {910F9D0B-51BD-4811-A2ED-DBD79BF02CCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {910F9D0B-51BD-4811-A2ED-DBD79BF02CCF}.Release|Any CPU.Build.0 = Release|Any CPU + {910F9D0B-51BD-4811-A2ED-DBD79BF02CCF}.Release|Any CPU.Deploy.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/StateMangers/VoiceStateManager.cs b/StateMangers/VoiceStateManager.cs new file mode 100644 index 0000000..3f1572e --- /dev/null +++ b/StateMangers/VoiceStateManager.cs @@ -0,0 +1,73 @@ +using NetCord; +using NetCord.Gateway; +using NetCord.Hosting.Gateway; +using NetCord.Rest; +using SquadBot.Providers; + +namespace SquadBot.StateMangers; + +public class VoiceStateManager +{ + // Key: GuildId Value: List of UserId for users in voice channel + // TODO: We probably want to persist this somewhere else (Redis/Valkey) + private Dictionary> _guildVoiceStates; + private Dictionary _yapperNotificationTimeouts; + private RestClient _restClient; + private ILogger _logger; + + public VoiceStateManager(ILogger logger, RestClient restClient) + { + _guildVoiceStates = new Dictionary>(); + _yapperNotificationTimeouts = new Dictionary(); + _restClient = restClient ?? throw new System.ArgumentNullException(nameof(restClient)); + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); + } + + public async Task HandleVoiceStateChange(VoiceState voice_state) + { + if (voice_state.ChannelId is not null) + { + // Joining channel + _logger.LogInformation($"New yapper detected: {voice_state.User.Username}"); + if (!_guildVoiceStates.TryGetValue(voice_state.GuildId, out var yappers) || yappers.Count < 0) + { + var channels = await _restClient.GetGuildChannelsAsync(voice_state.GuildId); + var alertChannelId = channels.FirstOrDefault(channel => channel.Name == "bot-tinkering")?.Id; + var guildRoles = await _restClient.GetGuildRolesAsync(voice_state.GuildId, + new RestRequestProperties() { AuditLogReason = "Role lookup" }); + var yapperRoleId = guildRoles.FirstOrDefault(role => role.Name.ToLower() == "yapper")?.Id; + if (alertChannelId is not null && yapperRoleId is not null && (!_yapperNotificationTimeouts.TryGetValue(voice_state.GuildId, out var timeout) || DateTimeOffset.UtcNow.CompareTo(timeout) > 0)) + { + // Notify that new yapper has arrived + await _restClient.SendMessageAsync( + alertChannelId.GetValueOrDefault(), + new MessageProperties() + { + AllowedMentions = new AllowedMentionsProperties() {AllowedRoles = [yapperRoleId.GetValueOrDefault()]}, + Content = $"<@&{yapperRoleId.GetValueOrDefault()}> " + PhraseProvider.GetYapperPhrase() + }); + } + + yappers = new HashSet() { voice_state.UserId }; + } + else + { + yappers = yappers.Append(voice_state.UserId).ToHashSet(); + } + _guildVoiceStates[voice_state.GuildId] = yappers; + } + else + { + // Leaving channel + _logger.LogInformation($"Yapper leaving: {voice_state.User.Username}"); + if (_guildVoiceStates.TryGetValue(voice_state.GuildId, out var yappers)) + { + yappers.Remove(voice_state.UserId); + _guildVoiceStates[voice_state.GuildId] = yappers; + _yapperNotificationTimeouts[voice_state.GuildId] = DateTimeOffset.UtcNow.AddHours(2); + } + } + + return; + } +} \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..d8ab69d --- /dev/null +++ b/appsettings.json @@ -0,0 +1,12 @@ +{ + "Discord": { + + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/global.json b/global.json new file mode 100644 index 0000000..f4fd385 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "9.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file