Initial commit, working

This commit is contained in:
Ryan Whytsell 2025-07-08 18:35:29 -04:00
commit 795978c1e8
Signed by: Epithium
GPG Key ID: 940AC18C08E925EA
14 changed files with 317 additions and 0 deletions

25
.dockerignore Normal file
View File

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

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/

23
Dockerfile Normal file
View File

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

View File

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

49
Program.cs Normal file
View File

@ -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 <token> <pubkey>");
}
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddLogging();
builder.Services.AddDiscordRest(options =>
{
options.PublicKey = pubkey;
options.Token = token;
});
builder.Services.AddSingleton<VoiceStateManager>();
builder.Services.AddSingleton<VoiceStatesHandler>();
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();
}
}

View File

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

View File

@ -0,0 +1,27 @@
using System.Security.Cryptography;
namespace SquadBot.Providers;
public static class PhraseProvider
{
private static Random rng = new Random();
private static List<string> phrases = new List<string>()
{
"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)];
}
}

23
SquadBot.csproj Normal file
View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<PublishAot>true</PublishAot>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NetCord" Version="1.0.0-alpha.391" />
<PackageReference Include="NetCord.Hosting" Version="1.0.0-alpha.391" />
<PackageReference Include="NetCord.Hosting.AspNetCore" Version="1.0.0-alpha.391" />
<PackageReference Include="NetCord.Hosting.Services" Version="1.0.0-alpha.391" />
</ItemGroup>
</Project>

11
SquadBot.http Normal file
View File

@ -0,0 +1,11 @@
@SquadBot_HostAddress = http://localhost:13376
GET {{SquadBot_HostAddress}}/todos/
Accept: application/json
###
GET {{SquadBot_HostAddress}}/todos/1
Accept: application/json
###

17
SquadBot.sln Normal file
View File

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

View File

@ -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<ulong, ISet<ulong>> _guildVoiceStates;
private Dictionary<ulong, DateTimeOffset> _yapperNotificationTimeouts;
private RestClient _restClient;
private ILogger<VoiceStateManager> _logger;
public VoiceStateManager(ILogger<VoiceStateManager> logger, RestClient restClient)
{
_guildVoiceStates = new Dictionary<ulong, ISet<ulong>>();
_yapperNotificationTimeouts = new Dictionary<ulong, DateTimeOffset>();
_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<ulong>() { 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;
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

12
appsettings.json Normal file
View File

@ -0,0 +1,12 @@
{
"Discord": {
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

7
global.json Normal file
View File

@ -0,0 +1,7 @@
{
"sdk": {
"version": "9.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
}