diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b961b81..4545f2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,10 +15,6 @@ updates: labels: - "type: change" - "area: build/ci" - # For all packages, ignore all patch updates - ignore: - - dependency-name: "*" - update-types: [ "version-update:semver-patch" ] - package-ecosystem: "nuget" # See documentation for possible values directory: "/" # Location of package manifests @@ -34,8 +30,3 @@ updates: remora: patterns: - "Remora.Discord.*" - # For all packages, ignore all patch updates - ignore: - - dependency-name: "GitInfo" - - dependency-name: "*" - update-types: [ "version-update:semver-patch" ] diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 07d5b90..8002f6f 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -22,13 +22,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - name: ReSharper CLI InspectCode - uses: muno92/resharper_inspectcode@1.13.0 + uses: muno92/resharper_inspectcode@1.11.7 with: solutionPath: ./Octobot.sln ignoreIssueType: InvertIf, ConvertIfStatementToSwitchStatement, ConvertToPrimaryConstructor diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index e7afe8e..a757eb2 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -5,83 +5,60 @@ concurrency: on: push: - branches: [ "master", "deploy-test" ] + branches: [ "master" ] jobs: - upload-image: - name: Upload Octobot Docker image + upload-solution: + name: Upload Octobot to production runs-on: ubuntu-latest permissions: - packages: write + actions: read + contents: read environment: production steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository + uses: actions/checkout@v4 - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - push: true - tags: ghcr.io/${{vars.NAMESPACE}}/${{vars.IMAGE_NAME}}:latest - build-args: | - BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 - PUBLISH_OPTIONS=${{vars.PUBLISH_OPTIONS}} + - name: Publish solution + run: dotnet publish $PUBLISH_FLAGS + env: + PUBLISH_FLAGS: ${{vars.PUBLISH_FLAGS}} - update-production: - name: Update Octobot on production - runs-on: ubuntu-latest - environment: production - needs: upload-image - - steps: - - name: Copy SSH key + - name: Setup SSH key run: | - install -m 600 -D /dev/null ~/.ssh/id_ed25519 - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519 + install -m 600 -D /dev/null ~/.ssh/id_rsa + echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa + ssh-keyscan -H $SSH_HOST > ~/.ssh/known_hosts shell: bash env: SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}} - - - name: Generate SSH known hosts file - run: | - ssh-keyscan -H -p $SSH_PORT $SSH_HOST > ~/.ssh/known_hosts - shell: bash - env: SSH_HOST: ${{secrets.SSH_HOST}} - SSH_PORT: ${{secrets.SSH_PORT}} - + - name: Stop currently running instance run: | - ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $STOP_COMMAND + ssh $SSH_USER@$SSH_HOST $STOP_COMMAND shell: bash env: - SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} STOP_COMMAND: ${{vars.STOP_COMMAND}} - - name: Update Docker image + - name: Upload published solution run: | - ssh -p $SSH_PORT $SSH_USER@$SSH_HOST docker pull ghcr.io/$NAMESPACE/$IMAGE_NAME:latest + scp -r $UPLOAD_FROM $SSH_USER@$SSH_HOST:$UPLOAD_TO shell: bash env: - SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} - NAMESPACE: ${{vars.NAMESPACE}} - IMAGE_NAME: ${{vars.IMAGE_NAME}} + UPLOAD_FROM: ${{vars.UPLOAD_FROM}} + UPLOAD_TO: ${{vars.UPLOAD_TO}} - name: Start new instance run: | - ssh -p $SSH_PORT $SSH_USER@$SSH_HOST $START_COMMAND + ssh $SSH_USER@$SSH_HOST $START_COMMAND shell: bash env: - SSH_PORT: ${{secrets.SSH_PORT}} SSH_USER: ${{secrets.SSH_USER}} SSH_HOST: ${{secrets.SSH_HOST}} START_COMMAND: ${{vars.START_COMMAND}} diff --git a/.gitignore b/.gitignore index fcda727..f97f6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ riderModule.iml /.vs/ GuildData/ Logs/ -compose.yaml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 6cfeac6..0000000 --- a/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0@sha256:7d24e90a392e88eb56093e4eb325ff883ad609382a55d42f17fd557b997022ca AS build-env -WORKDIR /Octobot - -# Copy everything -COPY . ./ -# Load build argument with publish options -ARG PUBLISH_OPTIONS="-c Release" -# Build and publish a release -RUN dotnet publish ./TeamOctolings.Octobot $PUBLISH_OPTIONS -o out - -# Build runtime image -FROM mcr.microsoft.com/dotnet/runtime:9.0@sha256:1e5eb0ed94ca96a34a914456db80e48bd1bb7bc3e3c8eda5e2c3d89c153c3081 -WORKDIR /Octobot -COPY --from=build-env /Octobot/out . -ENTRYPOINT ["./TeamOctolings.Octobot"] diff --git a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj b/Octobot.csproj similarity index 78% rename from TeamOctolings.Octobot/TeamOctolings.Octobot.csproj rename to Octobot.csproj index b67eaf8..bdfb46a 100644 --- a/TeamOctolings.Octobot/TeamOctolings.Octobot.csproj +++ b/Octobot.csproj @@ -1,8 +1,8 @@ - + Exe - net9.0 + net8.0 enable enable 2.0.0 @@ -16,31 +16,31 @@ TeamOctolings en A general-purpose Discord bot for moderation written in C# - ../docs/octobot.ico + docs/octobot.ico false - + - + - - - - - - + + + + + + - + ResXFileCodeGenerator Messages.Designer.cs - + diff --git a/Octobot.sln b/Octobot.sln index b82f7a9..9dd2b89 100644 --- a/Octobot.sln +++ b/Octobot.sln @@ -1,6 +1,6 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TeamOctolings.Octobot", "TeamOctolings.Octobot\TeamOctolings.Octobot.csproj", "{A1679BA2-3A36-4D98-80C0-EEE771398FBD}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Octobot", "Octobot.csproj", "{9CA7A44F-167C-46D4-923D-88CE71044144}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -8,9 +8,9 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A1679BA2-3A36-4D98-80C0-EEE771398FBD}.Release|Any CPU.Build.0 = Release|Any CPU + {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9CA7A44F-167C-46D4-923D-88CE71044144}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9CA7A44F-167C-46D4-923D-88CE71044144}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs b/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs deleted file mode 100644 index 2936392..0000000 --- a/TeamOctolings.Octobot/Commands/ToolsCommandGroup.cs +++ /dev/null @@ -1,272 +0,0 @@ -using System.ComponentModel; -using System.Text; -using JetBrains.Annotations; -using Remora.Commands.Attributes; -using Remora.Commands.Groups; -using Remora.Discord.API.Abstractions.Objects; -using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.Commands.Attributes; -using Remora.Discord.Commands.Contexts; -using Remora.Discord.Commands.Feedback.Services; -using Remora.Discord.Extensions.Embeds; -using Remora.Discord.Extensions.Formatting; -using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Parsers; -using TeamOctolings.Octobot.Services; - -namespace TeamOctolings.Octobot.Commands; - -/// -/// Handles tool commands: /random, /timestamp, /8ball. -/// -[UsedImplicitly] -public sealed class ToolsCommandGroup : CommandGroup -{ - private static readonly TimestampStyle[] AllStyles = - [ - TimestampStyle.ShortDate, - TimestampStyle.LongDate, - TimestampStyle.ShortTime, - TimestampStyle.LongTime, - TimestampStyle.ShortDateTime, - TimestampStyle.LongDateTime, - TimestampStyle.RelativeTime - ]; - - private static readonly string[] AnswerTypes = - [ - "Positive", "Questionable", "Neutral", "Negative" - ]; - - private readonly ICommandContext _context; - private readonly IFeedbackService _feedback; - private readonly GuildDataService _guildData; - private readonly IDiscordRestUserAPI _userApi; - - public ToolsCommandGroup( - ICommandContext context, IFeedbackService feedback, - GuildDataService guildData, IDiscordRestUserAPI userApi) - { - _context = context; - _guildData = guildData; - _feedback = feedback; - _userApi = userApi; - } - - /// - /// A slash command that generates a random number using maximum and minimum numbers. - /// - /// The first number used for randomization. - /// The second number used for randomization. Default value: 0 - /// - /// A feedback sending result which may or may not have succeeded. - /// - [Command("random")] - [DiscordDefaultDMPermission(false)] - [Description("Generates a random number")] - [UsedImplicitly] - public async Task ExecuteRandomAsync( - [Description("First number")] long first, - [Description("Second number (Default: 0)")] - long? second = null) - { - if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) - { - return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); - } - - var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); - if (!executorResult.IsDefined(out var executor)) - { - return ResultExtensions.FromError(executorResult); - } - - var data = await _guildData.GetData(guildId, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(data.Settings); - - return await SendRandomNumberAsync(first, second, executor, CancellationToken); - } - - private Task SendRandomNumberAsync(long first, long? secondNullable, - IUser executor, CancellationToken ct = default) - { - const long secondDefault = 0; - var second = secondNullable ?? secondDefault; - - var min = Math.Min(first, second); - var max = Math.Max(first, second); - - var i = Random.Shared.NextInt64(min, max + 1); - - var description = new StringBuilder().Append("# ").Append(i); - - description.AppendLine().AppendBulletPoint(string.Format( - Messages.RandomMin, Markdown.InlineCode(min.ToString()))); - if (secondNullable is null && first >= secondDefault) - { - description.Append(' ').Append(Messages.Default); - } - - description.AppendLine().AppendBulletPoint(string.Format( - Messages.RandomMax, Markdown.InlineCode(max.ToString()))); - if (secondNullable is null && first < secondDefault) - { - description.Append(' ').Append(Messages.Default); - } - - var embedColor = ColorsList.Blue; - if (secondNullable is not null && min == max) - { - description.AppendLine().Append(Markdown.Italicise(Messages.RandomMinMaxSame)); - embedColor = ColorsList.Red; - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.RandomTitle, executor.GetTag()), executor) - .WithDescription(description.ToString()) - .WithColour(embedColor) - .Build(); - - return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); - } - - /// - /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. - /// - /// The offset for the current timestamp. - /// - /// A feedback sending result which may or may not have succeeded. - /// - [Command("timestamp")] - [DiscordDefaultDMPermission(false)] - [Description("Shows a timestamp in all styles")] - [UsedImplicitly] - public async Task ExecuteTimestampAsync( - [Description("Offset from current time")] [Option("offset")] - string? stringOffset = null) - { - if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) - { - return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); - } - - var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!botResult.IsDefined(out var bot)) - { - return ResultExtensions.FromError(botResult); - } - - var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); - if (!executorResult.IsDefined(out var executor)) - { - return ResultExtensions.FromError(executorResult); - } - - var data = await _guildData.GetData(guildId, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(data.Settings); - - if (stringOffset is null) - { - return await SendTimestampAsync(null, executor, CancellationToken); - } - - var parseResult = TimeSpanParser.TryParse(stringOffset); - if (!parseResult.IsDefined(out var offset)) - { - var failedEmbed = new EmbedBuilder() - .WithSmallTitle(Messages.InvalidTimeSpan, bot) - .WithDescription(Messages.TimeSpanExample) - .WithColour(ColorsList.Red) - .Build(); - - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); - } - - return await SendTimestampAsync(offset, executor, CancellationToken); - } - - private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct = default) - { - var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds(); - - var description = new StringBuilder().Append("# ").AppendLine(timestamp.ToString()); - - if (offset is not null) - { - description.AppendLine(string.Format( - Messages.TimestampOffset, Markdown.InlineCode(offset.ToString() ?? string.Empty))).AppendLine(); - } - - foreach (var markdownTimestamp in AllStyles.Select(style => Markdown.Timestamp(timestamp, style))) - { - description.AppendBulletPoint(Markdown.InlineCode(markdownTimestamp)) - .Append(" → ").AppendLine(markdownTimestamp); - } - - var embed = new EmbedBuilder().WithSmallTitle( - string.Format(Messages.TimestampTitle, executor.GetTag()), executor) - .WithDescription(description.ToString()) - .WithColour(ColorsList.Blue) - .Build(); - - return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); - } - - /// - /// A slash command that shows a random answer from the Magic 8-Ball. - /// - /// Unused input. - /// - /// The 8-Ball answers were taken from Wikipedia. - /// - /// - /// A feedback sending result which may or may not have succeeded. - /// - [Command("8ball")] - [DiscordDefaultDMPermission(false)] - [Description("Ask the Magic 8-Ball a question")] - [UsedImplicitly] - public async Task ExecuteEightBallAsync( - // let the user think he's actually asking the ball a question - [Description("Question to ask")] string question) - { - if (!_context.TryGetContextIDs(out var guildId, out _, out _)) - { - return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); - } - - var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); - if (!botResult.IsDefined(out var bot)) - { - return ResultExtensions.FromError(botResult); - } - - var data = await _guildData.GetData(guildId, CancellationToken); - Messages.Culture = GuildSettings.Language.Get(data.Settings); - - return await AnswerEightBallAsync(bot, CancellationToken); - } - - private Task AnswerEightBallAsync(IUser bot, CancellationToken ct = default) - { - var typeNumber = Random.Shared.Next(0, 4); - var embedColor = typeNumber switch - { - 0 => ColorsList.Blue, - 1 => ColorsList.Green, - 2 => ColorsList.Yellow, - 3 => ColorsList.Red, - _ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber)) - }; - - var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized(); - - var embed = new EmbedBuilder().WithSmallTitle(answer, bot) - .WithColour(embedColor) - .Build(); - - return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); - } -} diff --git a/TeamOctolings.Octobot/Data/Reminder.cs b/TeamOctolings.Octobot/Data/Reminder.cs deleted file mode 100644 index 40f29e1..0000000 --- a/TeamOctolings.Octobot/Data/Reminder.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace TeamOctolings.Octobot.Data; - -public sealed record Reminder -{ - public required DateTimeOffset At { get; init; } - public required string Text { get; init; } - public required ulong ChannelId { get; init; } - public required ulong MessageId { get; init; } -} diff --git a/TeamOctolings.Octobot/Services/GuildDataService.cs b/TeamOctolings.Octobot/Services/GuildDataService.cs deleted file mode 100644 index 88edb5f..0000000 --- a/TeamOctolings.Octobot/Services/GuildDataService.cs +++ /dev/null @@ -1,297 +0,0 @@ -using System.Collections.Concurrent; -using System.Text.Json; -using System.Text.Json.Nodes; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Remora.Rest.Core; -using TeamOctolings.Octobot.Data; - -namespace TeamOctolings.Octobot.Services; - -/// -/// Handles saving, loading, initializing and providing . -/// -public sealed class GuildDataService : BackgroundService -{ - private readonly ConcurrentDictionary _datas = new(); - private readonly ILogger _logger; - - public GuildDataService(ILogger logger) - { - _logger = logger; - } - - public override Task StopAsync(CancellationToken ct) - { - base.StopAsync(ct); - return SaveAsync(ct); - } - - private Task SaveAsync(CancellationToken ct = default) - { - var tasks = new List(); - var datas = _datas.Values.ToArray(); - foreach (var data in datas.Where(data => !data.DataLoadFailed)) - { - tasks.Add(SerializeObjectSafelyAsync(data.Settings, data.SettingsPath, ct)); - tasks.Add(SerializeObjectSafelyAsync(data.ScheduledEvents, data.ScheduledEventsPath, ct)); - - var memberDatas = data.MemberData.Values.ToArray(); - tasks.AddRange(memberDatas.Select(memberData => - SerializeObjectSafelyAsync(memberData, $"{data.MemberDataPath}/{memberData.Id}.json", ct))); - } - - return Task.WhenAll(tasks); - } - - private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct = default) - { - var tempFilePath = path + ".tmp"; - await using (var tempFileStream = File.Create(tempFilePath)) - { - await JsonSerializer.SerializeAsync(tempFileStream, obj, cancellationToken: ct); - } - - File.Copy(tempFilePath, path, true); - File.Delete(tempFilePath); - } - - protected override async Task ExecuteAsync(CancellationToken ct) - { - using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); - - while (await timer.WaitForNextTickAsync(ct)) - { - await SaveAsync(ct); - } - } - - public async Task GetData(Snowflake guildId, CancellationToken ct = default) - { - return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); - } - - private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) - { - var path = $"GuildData/{guildId}"; - var memberDataPath = $"{path}/MemberData"; - - var settingsPath = $"{path}/Settings.json"; - - var scheduledEventsPath = $"{path}/ScheduledEvents.json"; - - MigrateDataDirectory(guildId, path); - - Directory.CreateDirectory(path); - - var dataLoadFailed = false; - - var jsonSettings = await LoadGuildSettings(settingsPath, ct); - if (jsonSettings is not null) - { - FixJsonSettings(jsonSettings); - } - else - { - dataLoadFailed = true; - } - - var events = await LoadScheduledEvents(scheduledEventsPath, ct); - if (events is null) - { - dataLoadFailed = true; - } - - var memberData = new Dictionary(); - foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles() - .Where(dataFileInfo => - !memberData.ContainsKey( - ulong.Parse(dataFileInfo.Name.Replace(".json", "").Replace(".tmp", ""))))) - { - var data = await LoadMemberData(dataFileInfo, memberDataPath, true, ct); - - if (data == null) - { - dataLoadFailed = true; - continue; - } - - memberData.TryAdd(data.Id, data); - } - - var finalData = new GuildData( - jsonSettings ?? new JsonObject(), settingsPath, - events ?? new Dictionary(), scheduledEventsPath, - memberData, memberDataPath, - dataLoadFailed); - - _datas.TryAdd(guildId, finalData); - - return finalData; - } - - private async Task LoadMemberData(FileInfo dataFileInfo, string memberDataPath, bool loadTmp, - CancellationToken ct = default) - { - MemberData? data; - var temporaryPath = $"{dataFileInfo.FullName}.tmp"; - var usedInfo = loadTmp && File.Exists(temporaryPath) ? new FileInfo(temporaryPath) : dataFileInfo; - - var isTmp = usedInfo.Extension is ".tmp"; - try - { - await using var dataStream = usedInfo.OpenRead(); - data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); - if (isTmp) - { - usedInfo.CopyTo(usedInfo.FullName.Replace(".tmp", ""), true); - usedInfo.Delete(); - } - } - catch (Exception e) - { - if (isTmp) - { - _logger.LogWarning(e, - "Unable to load temporary member data file, deleting: {MemberDataPath}/{FileName}", memberDataPath, - usedInfo.Name); - usedInfo.Delete(); - return await LoadMemberData(dataFileInfo, memberDataPath, false, ct); - } - - _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath, - usedInfo.Name); - return null; - } - - return data; - } - - private async Task?> LoadScheduledEvents(string scheduledEventsPath, - CancellationToken ct = default) - { - var tempScheduledEventsPath = $"{scheduledEventsPath}.tmp"; - - if (!File.Exists(scheduledEventsPath) && !File.Exists(tempScheduledEventsPath)) - { - return new Dictionary(); - } - - if (File.Exists(tempScheduledEventsPath)) - { - _logger.LogWarning("Found temporary scheduled events file, will try to parse and copy to main: ${Path}", - tempScheduledEventsPath); - try - { - await using var tempEventsStream = File.OpenRead(tempScheduledEventsPath); - var events = await JsonSerializer.DeserializeAsync>( - tempEventsStream, cancellationToken: ct); - File.Copy(tempScheduledEventsPath, scheduledEventsPath, true); - File.Delete(tempScheduledEventsPath); - - _logger.LogInformation("Successfully loaded temporary scheduled events file: ${Path}", - tempScheduledEventsPath); - return events; - } - catch (Exception e) - { - _logger.LogError(e, "Unable to load temporary scheduled events file: {Path}, deleting", - tempScheduledEventsPath); - File.Delete(tempScheduledEventsPath); - } - } - - try - { - await using var eventsStream = File.OpenRead(scheduledEventsPath); - return await JsonSerializer.DeserializeAsync>( - eventsStream, cancellationToken: ct); - } - catch (Exception e) - { - _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath); - return null; - } - } - - private async Task LoadGuildSettings(string settingsPath, CancellationToken ct = default) - { - var tempSettingsPath = $"{settingsPath}.tmp"; - - if (!File.Exists(settingsPath) && !File.Exists(tempSettingsPath)) - { - return new JsonObject(); - } - - if (File.Exists(tempSettingsPath)) - { - _logger.LogWarning("Found temporary settings file, will try to parse and copy to main: ${Path}", - tempSettingsPath); - try - { - await using var tempSettingsStream = File.OpenRead(tempSettingsPath); - var jsonSettings = await JsonNode.ParseAsync(tempSettingsStream, cancellationToken: ct); - - File.Copy(tempSettingsPath, settingsPath, true); - File.Delete(tempSettingsPath); - - _logger.LogInformation("Successfully loaded temporary settings file: ${Path}", tempSettingsPath); - return jsonSettings; - } - catch (Exception e) - { - _logger.LogError(e, "Unable to load temporary settings file: {Path}, deleting", tempSettingsPath); - File.Delete(tempSettingsPath); - } - } - - try - { - await using var settingsStream = File.OpenRead(settingsPath); - return await JsonNode.ParseAsync(settingsStream, cancellationToken: ct); - } - catch (Exception e) - { - _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath); - return null; - } - } - - private void MigrateDataDirectory(Snowflake guildId, string newPath) - { - var oldPath = $"{guildId}"; - - if (Directory.Exists(oldPath)) - { - Directory.CreateDirectory($"{newPath}/.."); - Directory.Move(oldPath, newPath); - - _logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath, - newPath); - } - } - - private static void FixJsonSettings(JsonNode settings) - { - var language = settings[GuildSettings.Language.Name]?.GetValue(); - if (language is "mctaylors-ru") - { - settings[GuildSettings.Language.Name] = "ru"; - } - } - - public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) - { - return (await GetData(guildId, ct)).Settings; - } - - public ICollection GetGuildIds() - { - return _datas.Keys; - } - - public bool UnloadGuildData(Snowflake id) - { - return _datas.TryRemove(id, out _); - } -} diff --git a/compose.example.yaml b/compose.example.yaml deleted file mode 100644 index 522281f..0000000 --- a/compose.example.yaml +++ /dev/null @@ -1,17 +0,0 @@ -services: - octobot: - container_name: octobot - build: - context: . - args: - - PUBLISH_OPTIONS - environment: - - BOT_TOKEN - volumes: - - guild-data:/Octobot/GuildData - - logs:/Octobot/Logs - restart: unless-stopped - -volumes: - guild-data: - logs: diff --git a/docs/README.md b/docs/README.md index ccc3b83..7056857 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,16 +15,23 @@ Veemo! I'm a general-purpose bot for moderation (formerly known as Boyfriend) wr * Reminding everyone about that new event you made * Renaming those annoying self-hoisting members * Log everything from joining the server to deleting messages -* Listen to Inkantation! +* Listen to music! *...a-a-and more!* ## Building Octobot -Check out the Octobot's Wiki for details. - -| [Windows](https://github.com/TeamOctolings/Octobot/wiki/Installing-Windows) | [Linux/macOS](https://github.com/TeamOctolings/Octobot/wiki/Installing-Unix) | -| --- | --- | +1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) +2. Go to the [Discord Developer Portal](https://discord.com/developers), create a new application and get a bot token. Don't forget to also enable all intents! +3. Clone this repository and open `Octobot` folder. +``` +git clone https://github.com/TeamOctolings/Octobot +cd Octobot +``` +4. Run Octobot using `dotnet` with `BOT_TOKEN` variable. +``` +dotnet run BOT_TOKEN='ENTER_TOKEN_HERE' +``` ## Contributing diff --git a/TeamOctolings.Octobot/Messages.resx b/locale/Messages.resx similarity index 96% rename from TeamOctolings.Octobot/Messages.resx rename to locale/Messages.resx index e4107fb..4a20337 100644 --- a/TeamOctolings.Octobot/Messages.resx +++ b/locale/Messages.resx @@ -138,24 +138,12 @@ ms - - Not specified - - - Not specified - Language - - Prefix - Remove roles on mute - - Send welcome messages - Mute role @@ -198,9 +186,6 @@ Channel for event notifications - - Event start notifications receivers - Event "{0}" started @@ -216,9 +201,6 @@ Nothing changed! `{0}` is already set to {1} - - Not specified - You need to specify a user! @@ -399,8 +381,8 @@ Developers: - - Open Website + + Octobot's source code About {0} @@ -447,12 +429,6 @@ There are {0} total pages - - Next - - - Previous - {0}'s reminders @@ -681,7 +657,4 @@ Moderator role - - The setting value is the same as the input value. - diff --git a/TeamOctolings.Octobot/Messages.ru.resx b/locale/Messages.ru.resx similarity index 96% rename from TeamOctolings.Octobot/Messages.ru.resx rename to locale/Messages.ru.resx index d942cec..f6c0c0a 100644 --- a/TeamOctolings.Octobot/Messages.ru.resx +++ b/locale/Messages.ru.resx @@ -135,24 +135,12 @@ мс - - Не указан - - - Не указана - Язык - - Префикс - Удалять роли при муте - - Отправлять приветствия - Роль мута @@ -195,9 +183,6 @@ Канал для уведомлений о событиях - - Получатели уведомлений о начале событий - Событие "{0}" началось @@ -213,9 +198,6 @@ Ничего не изменилось! Значение настройки `{0}` уже {1} - - Не указано - Надо указать пользователя! @@ -399,8 +381,8 @@ Разработчики: - - Открыть веб-сайт + + Исходный код Octobot О боте {0} @@ -447,12 +429,6 @@ Всего есть {0} страниц(-ы) - - Далее - - - Назад - Напоминания {0} @@ -681,7 +657,4 @@ Роль модератора - - Значение настройки такое же, как и вводное значение. - diff --git a/locale/Messages.tt-ru.resx b/locale/Messages.tt-ru.resx new file mode 100644 index 0000000..b987042 --- /dev/null +++ b/locale/Messages.tt-ru.resx @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + я родился! + + + сообщение {0} вырезано: + + + сообщение {0} переделано: + + + {0}, добро пожаловать на сервер {1} + + + вииимо! + + + вуууми! + + + нгьес! + + + вы были забанены + + + время бана закончиловсь + + + вы были кикнуты + + + мс + + + язык + + + удалять звание при муте + + + звание замученного + + + такого языка нету... + + + да + + + нъет + + + шизик не забанен + + + шизоид не замучен! + + + здравствуйте (типо настройка) + + + {0} забанен + + + получать инфу о старте бота + + + криво настроил прикол, давай по новой + + + ты шо, мутить больше чем на 28 дней таймаут не разрешает, вот настроишь роль мута, тогда поговорим + + + я не могу замутить ботов, сделай что нибудь + + + роль для уведомлений о создании движухи + + + канал для уведомлений о движухах + + + движуха "{0}" начинается + + + движуха "{0}" отменена! + + + движуха "{0}" завершена! + + + вырезано {0} забавных сообщений + + + ты все сломал! значение прикола `{0}` и так {1} + + + укажи самого шизика + + + бан + + + тебе нельзя иметь власть над сообщениями шизоидов + + + кик шизиков нельзя + + + тебе нельзя мутить шизоидов + + + тебе нельзя раззамучивать шизоидов + + + тебе нельзя редактировать дурку + + + я не могу ваще никого банить чел. + + + я не могу исправлять орфографический кринж участников, сделай что нибудь. + + + я не могу ваще никого кикать чел. + + + я не могу контроллировать за всеми ними, сделай что нибудь. + + + я не могу этому серверу хоть че либо нибудь изменить, сделай что нибудь. + + + ээбля френдли фаер огонь по своим + + + бан админу нельзя + + + бан этому шизику нельзя + + + самобан нельзя + + + я не могу его забанить... + + + кик админу нельзя + + + самокик нельзя + + + ээбля френдли фаер огонь по своим + + + я не могу его кикнуть... + + + кик этому шизику нельзя + + + мут админу нельзя + + + самомут нельзя + + + ээбля френдли фаер огонь по своим + + + я не могу его замутить... + + + мут этому шизику нельзя + + + сильно + + + ты замучен. + + + ... + + + тебе нельзя раззамучивать + + + я не могу его раззамутить... + + + движуха "{0}" начнется {1}! + + + заранее пнуть в минутах до начала движухи + + + у нас такого шизоида нету, проверь, валиден ли ID уважаемого (я забываю о шизоидах если они ливнули минимум месяц назад) + + + дефолтное звание + + + канал для секретных уведомлений + + + канал для не секретных уведомлений + + + вернуть звания при переподключении в дурку + + + автоматом стартить движухи + + + ответственный + + + {0} создает новое событие: + + + движуха произойдет {0} в канале {1} + + + движуха будет происходить с {0} до {1} в {2} + + + открыть ивент + + + все это длилось `{0}` + + + движуха происходит в {0} + + + движуха происходит в {0} до {1} + + + этот шизоид уже лежит в бане + + + {0} раззабанен + + + {0} в муте + + + {0} в размуте + + + этого шизоида никто не мутил. + + + у нас такого шизоида нету... + + + {0} вышел с посторонней помощью + + + причина: {0} + + + до: {0} + + + этот шизоид УЖЕ замучился + + + от {0} + + + девелоперы: + + + репа Octobot (тык) + + + немного об {0} + + + скучный девелопер + дизайнер создавший Octobot's Wiki + + + ВАЖНЫЙ соучастник кодинг-стримов @Octol1ttle + + + САМЫЙ ВАЖНЫЙ чел написавший кода больше всех (99.99%) + + + напоминалка для {0} скрафченА + + + напоминалка для {0} + + + ты хотел чтоб я напомнил тебе {0} + + + приколы Octobot + + + прикол редактирован + + + прикол сдох + + + стало + + + переобувать шизоидов пытающихся поднять себя в табе + + + это страница + + + если я был бы html, я бы сказал 404 + + + ну а если быть точнее, тут всего {0} страниц(-ы) + + + напоминалки {0} + + + у тебя нет напоминалки на этом номере! + + + напоминалка уничтожена + + + ты еще не крафтил напоминалки + + + {0} откачен к заводским + + + откатываемся к заводским... + + + чекнуть сообщение: {0} + + + чекнуть канал: {0} + + + номер в списке: {0} + + + время отправки: {0} + + + че там в напоминалке: {0} + + + дисплейнейм + + + деанон {0} + + + замучен + + + юзер Discord со времен + + + забанен + + + приколы полученные по заслугам + + + пермабан + + + вышел из сервера + + + замучен таймаутом + + + замучен ролькой + + + участник сервера со времен + + + сервернейм + + + рольки + + + бустит сервер со времен + + + рандомное число {0}: + + + ну чувак... + + + наибольшее: {0} + + + наименьшее: {0} + + + (дефолт) + + + таймштамп для {0}: + + + офсет: {0} + + + дескрипшон гильдии + + + создался + + + админ гильдии + + + буст гильдии + + + уровень + + + кол-во бустов + + + алло а чё мне удалять-то + + + вырезано {0} забавных сообщений от {1} + + + произошёл тотальный разнос в гилддате. + + + возможно всё съедет с крыши, но знай, что я больше ничё не сохраню. + + + произошёл тотальный разнос в команде, удачи. + + + если ты это читаешь второй раз за сегодня, пиши разрабам + + + зарепортить баг + + + ну, мы потеряли {0} + + + до свидания (типо настройка) + + + ты там правильно напиши таймспан + + + кикнут + + + напоминалка подправлена + + + абсолютли + + + заявлено + + + ваще не сомневайся + + + 100% да + + + будь в этом уверен + + + я считаю что да + + + ну вполне вероятно + + + ну выглядит нормально + + + мне сказали ок + + + мгм + + + ну-ка попробуй снова + + + давай позже + + + щас пока не скажу + + + я не могу сейчас предсказать + + + ну сконцентрируйся и давай еще раз + + + даже не думай + + + мое завление это нет + + + я тут посчитал, короче нет + + + выглядит такое себе + + + чот сомневаюсь + + + правильно пишут так: `1h30m` + + + {0} + + + канал куда говорить здравствуйте + + + вот иди сам и почини что сломал + + + вики Octobot (жмак) + + + звание админа + + diff --git a/TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs b/src/Attributes/StaticCallersOnlyAttribute.cs similarity index 88% rename from TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs rename to src/Attributes/StaticCallersOnlyAttribute.cs index 0256f62..e8787bf 100644 --- a/TeamOctolings.Octobot/Attributes/StaticCallersOnlyAttribute.cs +++ b/src/Attributes/StaticCallersOnlyAttribute.cs @@ -1,4 +1,4 @@ -namespace TeamOctolings.Octobot.Attributes; +namespace Octobot.Attributes; /// /// Any property marked with should only be accessed by static methods. diff --git a/TeamOctolings.Octobot/BuildInfo.cs b/src/BuildInfo.cs similarity index 68% rename from TeamOctolings.Octobot/BuildInfo.cs rename to src/BuildInfo.cs index a91e7f3..2eb6059 100644 --- a/TeamOctolings.Octobot/BuildInfo.cs +++ b/src/BuildInfo.cs @@ -1,10 +1,8 @@ -namespace TeamOctolings.Octobot; +namespace Octobot; public static class BuildInfo { - public const string WebsiteUrl = "https://teamoctolings.github.io/Octobot"; - - private const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; + public const string RepositoryUrl = "https://github.com/TeamOctolings/Octobot"; public const string IssuesUrl = $"{RepositoryUrl}/issues"; diff --git a/TeamOctolings.Octobot/ColorsList.cs b/src/ColorsList.cs similarity index 95% rename from TeamOctolings.Octobot/ColorsList.cs rename to src/ColorsList.cs index 3b66c0a..cd40313 100644 --- a/TeamOctolings.Octobot/ColorsList.cs +++ b/src/ColorsList.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace TeamOctolings.Octobot; +namespace Octobot; /// /// Contains all colors used in embeds. diff --git a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs b/src/Commands/AboutCommandGroup.cs similarity index 85% rename from TeamOctolings.Octobot/Commands/AboutCommandGroup.cs rename to src/Commands/AboutCommandGroup.cs index 28caccf..027e7f8 100644 --- a/TeamOctolings.Octobot/Commands/AboutCommandGroup.cs +++ b/src/Commands/AboutCommandGroup.cs @@ -1,6 +1,9 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -15,17 +18,14 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles the command to show information about this bot: /about. /// [UsedImplicitly] -public sealed class AboutCommandGroup : CommandGroup +public class AboutCommandGroup : CommandGroup { private static readonly (string Username, Snowflake Id)[] Developers = [ @@ -36,9 +36,9 @@ public sealed class AboutCommandGroup : CommandGroup private readonly ICommandContext _context; private readonly IFeedbackService _feedback; - private readonly IDiscordRestGuildAPI _guildApi; private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestGuildAPI _guildApi; public AboutCommandGroup( ICommandContext context, GuildDataService guildData, @@ -100,21 +100,21 @@ public sealed class AboutCommandGroup : CommandGroup .WithSmallTitle(string.Format(Messages.AboutBot, bot.Username), bot) .WithDescription(builder.ToString()) .WithColour(ColorsList.Cyan) - .WithImageUrl("https://raw.githubusercontent.com/TeamOctolings/Octobot/HEAD/docs/octobot-banner.png") + .WithImageUrl("https://i.ibb.co/fS6wZhh/octobot-banner.png") .WithFooter(string.Format(Messages.Version, BuildInfo.Version)) .Build(); var repositoryButton = new ButtonComponent( ButtonComponentStyle.Link, - Messages.ButtonOpenWebsite, - new PartialEmoji(Name: "\ud83c\udf10"), // 'GLOBE WITH MERIDIANS' (U+1F310) - URL: BuildInfo.WebsiteUrl + Messages.ButtonOpenRepository, + new PartialEmoji(Name: "🌐"), + URL: BuildInfo.RepositoryUrl ); var wikiButton = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonOpenWiki, - new PartialEmoji(Name: "\ud83d\udcd6"), // 'OPEN BOOK' (U+1F4D6) + new PartialEmoji(Name: "📖"), URL: BuildInfo.WikiUrl ); @@ -123,7 +123,7 @@ public sealed class AboutCommandGroup : CommandGroup BuildInfo.IsDirty ? Messages.ButtonDirty : Messages.ButtonReportIssue, - new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0) + new PartialEmoji(Name: "⚠️"), URL: BuildInfo.IssuesUrl, IsDisabled: BuildInfo.IsDirty ); @@ -131,7 +131,7 @@ public sealed class AboutCommandGroup : CommandGroup return await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent([repositoryButton, wikiButton, issuesButton]) + new ActionRowComponent(new[] { repositoryButton, wikiButton, issuesButton }) }), ct); } } diff --git a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs b/src/Commands/BanCommandGroup.cs similarity index 97% rename from TeamOctolings.Octobot/Commands/BanCommandGroup.cs rename to src/Commands/BanCommandGroup.cs index 1d6b26c..02a377a 100644 --- a/TeamOctolings.Octobot/Commands/BanCommandGroup.cs +++ b/src/Commands/BanCommandGroup.cs @@ -2,6 +2,11 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Parsers; +using Octobot.Services; +using Octobot.Services.Update; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -14,19 +19,14 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Parsers; -using TeamOctolings.Octobot.Services; -using TeamOctolings.Octobot.Services.Update; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles commands related to ban management: /ban and /unban. /// [UsedImplicitly] -public sealed class BanCommandGroup : CommandGroup +public class BanCommandGroup : CommandGroup { private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; @@ -62,7 +62,7 @@ public sealed class BanCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user - /// was banned and vice versa. + /// was banned and vice-versa. /// /// [Command("ban", "бан")] @@ -219,7 +219,7 @@ public sealed class BanCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the user - /// was unbanned and vice versa. + /// was unbanned and vice-versa. /// /// /// diff --git a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs b/src/Commands/ClearCommandGroup.cs similarity index 95% rename from TeamOctolings.Octobot/Commands/ClearCommandGroup.cs rename to src/Commands/ClearCommandGroup.cs index 7f29581..84b69db 100644 --- a/TeamOctolings.Octobot/Commands/ClearCommandGroup.cs +++ b/src/Commands/ClearCommandGroup.cs @@ -1,6 +1,9 @@ using System.ComponentModel; using System.Text; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -13,17 +16,14 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles the command to clear messages in a channel: /clear. /// [UsedImplicitly] -public sealed class ClearCommandGroup : CommandGroup +public class ClearCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly ICommandContext _context; @@ -51,7 +51,7 @@ public sealed class ClearCommandGroup : CommandGroup /// The user whose messages will be cleared. /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that any messages - /// were cleared and vice versa. + /// were cleared and vice-versa. /// [Command("clear", "очистить")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] @@ -64,7 +64,6 @@ public sealed class ClearCommandGroup : CommandGroup public async Task ExecuteClear( [Description("Number of messages to remove (2-100)")] [MinValue(2)] [MaxValue(100)] int amount, - [Description("Ignore messages except from the specified author")] IUser? author = null) { if (!_context.TryGetContextIDs(out var guildId, out var channelId, out var executorId)) diff --git a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs similarity index 90% rename from TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs rename to src/Commands/Events/ErrorLoggingPostExecutionEvent.cs index ff7339f..5fa2ea8 100644 --- a/TeamOctolings.Octobot/Commands/Events/ErrorLoggingPostExecutionEvent.cs +++ b/src/Commands/Events/ErrorLoggingPostExecutionEvent.cs @@ -1,5 +1,6 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; @@ -10,15 +11,14 @@ using Remora.Discord.Commands.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Results; -using TeamOctolings.Octobot.Extensions; -namespace TeamOctolings.Octobot.Commands.Events; +namespace Octobot.Commands.Events; /// /// Handles error logging for slash command groups. /// [UsedImplicitly] -public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent +public class ErrorLoggingPostExecutionEvent : IPostExecutionEvent { private readonly IFeedbackService _feedback; private readonly ILogger _logger; @@ -73,7 +73,7 @@ public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent BuildInfo.IsDirty ? Messages.ButtonDirty : Messages.ButtonReportIssue, - new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0) + new PartialEmoji(Name: "⚠️"), URL: BuildInfo.IssuesUrl, IsDisabled: BuildInfo.IsDirty ); @@ -81,7 +81,7 @@ public sealed class ErrorLoggingPostExecutionEvent : IPostExecutionEvent return ResultExtensions.FromError(await _feedback.SendContextualEmbedResultAsync(embed, new FeedbackMessageOptions(MessageComponents: new[] { - new ActionRowComponent([issuesButton]) + new ActionRowComponent(new[] { issuesButton }) }), ct) ); } diff --git a/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs b/src/Commands/Events/LoggingPreparationErrorEvent.cs similarity index 88% rename from TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs rename to src/Commands/Events/LoggingPreparationErrorEvent.cs index 9e69a7f..87b4090 100644 --- a/TeamOctolings.Octobot/Commands/Events/LoggingPreparationErrorEvent.cs +++ b/src/Commands/Events/LoggingPreparationErrorEvent.cs @@ -1,17 +1,17 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Octobot.Extensions; using Remora.Discord.Commands.Contexts; using Remora.Discord.Commands.Services; using Remora.Results; -using TeamOctolings.Octobot.Extensions; -namespace TeamOctolings.Octobot.Commands.Events; +namespace Octobot.Commands.Events; /// /// Handles error logging for slash commands that couldn't be successfully prepared. /// [UsedImplicitly] -public sealed class LoggingPreparationErrorEvent : IPreparationErrorEvent +public class LoggingPreparationErrorEvent : IPreparationErrorEvent { private readonly ILogger _logger; diff --git a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs b/src/Commands/KickCommandGroup.cs similarity index 96% rename from TeamOctolings.Octobot/Commands/KickCommandGroup.cs rename to src/Commands/KickCommandGroup.cs index 3011375..87b915a 100644 --- a/TeamOctolings.Octobot/Commands/KickCommandGroup.cs +++ b/src/Commands/KickCommandGroup.cs @@ -1,6 +1,9 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -12,17 +15,14 @@ using Remora.Discord.Commands.Feedback.Services; using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles the command to kick members of a guild: /kick. /// [UsedImplicitly] -public sealed class KickCommandGroup : CommandGroup +public class KickCommandGroup : CommandGroup { private readonly AccessControlService _access; private readonly IDiscordRestChannelAPI _channelApi; @@ -57,7 +57,7 @@ public sealed class KickCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was kicked and vice versa. + /// was kicked and vice-versa. /// [Command("kick", "кик")] [DiscordDefaultMemberPermissions(DiscordPermission.ManageMessages)] diff --git a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs b/src/Commands/MuteCommandGroup.cs similarity index 96% rename from TeamOctolings.Octobot/Commands/MuteCommandGroup.cs rename to src/Commands/MuteCommandGroup.cs index 5dce0b6..ce0a296 100644 --- a/TeamOctolings.Octobot/Commands/MuteCommandGroup.cs +++ b/src/Commands/MuteCommandGroup.cs @@ -2,6 +2,11 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Parsers; +using Octobot.Services; +using Octobot.Services.Update; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -14,19 +19,14 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Parsers; -using TeamOctolings.Octobot.Services; -using TeamOctolings.Octobot.Services.Update; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles commands related to mute management: /mute and /unmute. /// [UsedImplicitly] -public sealed class MuteCommandGroup : CommandGroup +public class MuteCommandGroup : CommandGroup { private readonly AccessControlService _access; private readonly ICommandContext _context; @@ -59,7 +59,7 @@ public sealed class MuteCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was muted and vice versa. + /// was muted and vice-versa. /// /// [Command("mute", "мут")] @@ -170,7 +170,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task SelectMuteMethodAsync( IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, GuildData data, - IUser bot, DateTimeOffset until, CancellationToken ct = default) + IUser bot, DateTimeOffset until, CancellationToken ct) { var muteRole = GuildSettings.MuteRole.Get(data.Settings); @@ -186,7 +186,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task RoleMuteUserAsync( IUser executor, IUser target, string reason, Snowflake guildId, GuildData data, - DateTimeOffset until, Snowflake muteRole, CancellationToken ct = default) + DateTimeOffset until, Snowflake muteRole, CancellationToken ct) { var assignRoles = new List { muteRole }; var memberData = data.GetOrCreateMemberData(target.ID); @@ -208,7 +208,7 @@ public sealed class MuteCommandGroup : CommandGroup private async Task TimeoutUserAsync( IUser executor, IUser target, string reason, TimeSpan duration, Snowflake guildId, - IUser bot, DateTimeOffset until, CancellationToken ct = default) + IUser bot, DateTimeOffset until, CancellationToken ct) { if (duration.TotalDays >= 28) { @@ -235,7 +235,7 @@ public sealed class MuteCommandGroup : CommandGroup /// /// /// A feedback sending result which may or may not have succeeded. A successful result does not mean that the member - /// was unmuted and vice versa. + /// was unmuted and vice-versa. /// /// /// diff --git a/TeamOctolings.Octobot/Commands/PingCommandGroup.cs b/src/Commands/PingCommandGroup.cs similarity index 94% rename from TeamOctolings.Octobot/Commands/PingCommandGroup.cs rename to src/Commands/PingCommandGroup.cs index 01a1ee2..d64c6dd 100644 --- a/TeamOctolings.Octobot/Commands/PingCommandGroup.cs +++ b/src/Commands/PingCommandGroup.cs @@ -1,5 +1,8 @@ using System.ComponentModel; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -12,17 +15,14 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles the command to get the time taken for the gateway to respond to the last heartbeat: /ping /// [UsedImplicitly] -public sealed class PingCommandGroup : CommandGroup +public class PingCommandGroup : CommandGroup { private readonly IDiscordRestChannelAPI _channelApi; private readonly DiscordGatewayClient _client; diff --git a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs b/src/Commands/RemindCommandGroup.cs similarity index 95% rename from TeamOctolings.Octobot/Commands/RemindCommandGroup.cs rename to src/Commands/RemindCommandGroup.cs index 3188d27..aa1ef7e 100644 --- a/TeamOctolings.Octobot/Commands/RemindCommandGroup.cs +++ b/src/Commands/RemindCommandGroup.cs @@ -2,6 +2,9 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Text; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -14,24 +17,21 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Parsers; -using TeamOctolings.Octobot.Services; +using Octobot.Parsers; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles commands to manage reminders: /remind, /listremind, /delremind /// [UsedImplicitly] -public sealed class RemindCommandGroup : CommandGroup +public class RemindCommandGroup : CommandGroup { private readonly IInteractionCommandContext _context; private readonly IFeedbackService _feedback; private readonly GuildDataService _guildData; - private readonly IDiscordRestInteractionAPI _interactionApi; private readonly IDiscordRestUserAPI _userApi; + private readonly IDiscordRestInteractionAPI _interactionApi; public RemindCommandGroup( IInteractionCommandContext context, GuildDataService guildData, IFeedbackService feedback, @@ -78,7 +78,7 @@ public sealed class RemindCommandGroup : CommandGroup return await ListRemindersAsync(data.GetOrCreateMemberData(executorId), guildId, executor, bot, CancellationToken); } - private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct = default) + private Task ListRemindersAsync(MemberData data, Snowflake guildId, IUser executor, IUser bot, CancellationToken ct) { if (data.Reminders.Count == 0) { @@ -94,7 +94,7 @@ public sealed class RemindCommandGroup : CommandGroup { var reminder = data.Reminders[i]; builder.AppendBulletPointLine(string.Format(Messages.ReminderPosition, Markdown.InlineCode((i + 1).ToString()))) - .AppendSubBulletPointLine(string.Format(Messages.ReminderText, reminder.Text)) + .AppendSubBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) .AppendSubBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))) .AppendSubBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); } @@ -182,7 +182,7 @@ public sealed class RemindCommandGroup : CommandGroup }); var builder = new StringBuilder() - .AppendLine(MarkdownExtensions.Quote(text)) + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(text))) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderCreated, executor.GetTag()), executor) @@ -279,7 +279,7 @@ public sealed class RemindCommandGroup : CommandGroup data.Reminders.RemoveAt(index); var builder = new StringBuilder() - .AppendLine(MarkdownExtensions.Quote(oldReminder.Text)) + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(oldReminder.Text))) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(remindAt))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderEdited, executor.GetTag()), executor) @@ -309,7 +309,7 @@ public sealed class RemindCommandGroup : CommandGroup data.Reminders.RemoveAt(index); var builder = new StringBuilder() - .AppendLine(MarkdownExtensions.Quote(value)) + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(value))) .AppendBulletPoint(string.Format(Messages.ReminderTime, Markdown.Timestamp(oldReminder.At))); var embed = new EmbedBuilder().WithSmallTitle( string.Format(Messages.ReminderEdited, executor.GetTag()), executor) @@ -353,7 +353,7 @@ public sealed class RemindCommandGroup : CommandGroup } private Task DeleteReminderAsync(MemberData data, int index, IUser bot, - CancellationToken ct = default) + CancellationToken ct) { if (index >= data.Reminders.Count) { @@ -367,7 +367,7 @@ public sealed class RemindCommandGroup : CommandGroup var reminder = data.Reminders[index]; var description = new StringBuilder() - .AppendLine(MarkdownExtensions.Quote(reminder.Text)) + .AppendBulletPointLine(string.Format(Messages.ReminderText, Markdown.InlineCode(reminder.Text))) .AppendBulletPointLine(string.Format(Messages.ReminderTime, Markdown.Timestamp(reminder.At))); data.Reminders.RemoveAt(index); diff --git a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs b/src/Commands/SettingsCommandGroup.cs similarity index 89% rename from TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs rename to src/Commands/SettingsCommandGroup.cs index 15aa42b..a39e9c7 100644 --- a/TeamOctolings.Octobot/Commands/SettingsCommandGroup.cs +++ b/src/Commands/SettingsCommandGroup.cs @@ -3,6 +3,10 @@ using System.ComponentModel.DataAnnotations; using System.Text; using System.Text.Json.Nodes; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Data.Options; +using Octobot.Extensions; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -15,27 +19,23 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Data.Options; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// /// Handles the commands to list and modify per-guild settings: /settings and /settings list. /// [UsedImplicitly] -public sealed class SettingsCommandGroup : CommandGroup +public class SettingsCommandGroup : CommandGroup { /// - /// Represents all options as an array of objects implementing . + /// Represents all options as an array of objects implementing . /// /// /// WARNING: If you update this array in any way, you must also update and make sure /// that the orders match. /// - private static readonly IGuildOption[] AllOptions = + private static readonly IOption[] AllOptions = [ GuildSettings.Language, GuildSettings.WelcomeMessage, @@ -199,30 +199,9 @@ public sealed class SettingsCommandGroup : CommandGroup } private async Task EditSettingAsync( - IGuildOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot, + IOption option, string value, GuildData data, Snowflake channelId, IUser executor, IUser bot, CancellationToken ct = default) { - var equalsResult = option.ValueEquals(data.Settings, value); - if (!equalsResult.IsSuccess) - { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot) - .WithDescription(equalsResult.Error.Message) - .WithColour(ColorsList.Red) - .Build(); - - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); - } - - if (equalsResult.Entity) - { - var failedEmbed = new EmbedBuilder().WithSmallTitle(Messages.SettingNotChanged, bot) - .WithDescription(Messages.SettingValueEquals) - .WithColour(ColorsList.Red) - .Build(); - - return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: ct); - } - var setResult = option.Set(data.Settings, value); if (!setResult.IsSuccess) { @@ -291,7 +270,7 @@ public sealed class SettingsCommandGroup : CommandGroup } private async Task ResetSingleSettingAsync(JsonNode cfg, IUser bot, - IGuildOption option, CancellationToken ct = default) + IOption option, CancellationToken ct = default) { var resetResult = option.Reset(cfg); if (!resetResult.IsSuccess) diff --git a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs b/src/Commands/ToolsCommandGroup.cs similarity index 56% rename from TeamOctolings.Octobot/Commands/InfoCommandGroup.cs rename to src/Commands/ToolsCommandGroup.cs index f07b210..d4f3f75 100644 --- a/TeamOctolings.Octobot/Commands/InfoCommandGroup.cs +++ b/src/Commands/ToolsCommandGroup.cs @@ -2,6 +2,10 @@ using System.ComponentModel; using System.Drawing; using System.Text; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Parsers; +using Octobot.Services; using Remora.Commands.Attributes; using Remora.Commands.Groups; using Remora.Discord.API.Abstractions.Objects; @@ -13,17 +17,14 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Commands; +namespace Octobot.Commands; /// -/// Handles info commands: /userinfo, /guildinfo. +/// Handles tool commands: /userinfo, /guildinfo, /random, /timestamp, /8ball. /// [UsedImplicitly] -public sealed class InfoCommandGroup : CommandGroup +public class ToolsCommandGroup : CommandGroup { private readonly ICommandContext _context; private readonly IFeedbackService _feedback; @@ -31,7 +32,7 @@ public sealed class InfoCommandGroup : CommandGroup private readonly GuildDataService _guildData; private readonly IDiscordRestUserAPI _userApi; - public InfoCommandGroup( + public ToolsCommandGroup( ICommandContext context, IFeedbackService feedback, GuildDataService guildData, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi) @@ -288,7 +289,7 @@ public sealed class InfoCommandGroup : CommandGroup return await ShowGuildInfoAsync(bot, guild, CancellationToken); } - private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct = default) + private Task ShowGuildInfoAsync(IUser bot, IGuild guild, CancellationToken ct) { var description = new StringBuilder().AppendLine($"## {guild.Name}"); @@ -326,4 +327,235 @@ public sealed class InfoCommandGroup : CommandGroup return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); } + + /// + /// A slash command that generates a random number using maximum and minimum numbers. + /// + /// The first number used for randomization. + /// The second number used for randomization. Default value: 0 + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("random")] + [DiscordDefaultDMPermission(false)] + [Description("Generates a random number")] + [UsedImplicitly] + public async Task ExecuteRandomAsync( + [Description("First number")] long first, + [Description("Second number (Default: 0)")] + long? second = null) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) + { + return ResultExtensions.FromError(executorResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await SendRandomNumberAsync(first, second, executor, CancellationToken); + } + + private Task SendRandomNumberAsync(long first, long? secondNullable, + IUser executor, CancellationToken ct) + { + const long secondDefault = 0; + var second = secondNullable ?? secondDefault; + + var min = Math.Min(first, second); + var max = Math.Max(first, second); + + var i = Random.Shared.NextInt64(min, max + 1); + + var description = new StringBuilder().Append("# ").Append(i); + + description.AppendLine().AppendBulletPoint(string.Format( + Messages.RandomMin, Markdown.InlineCode(min.ToString()))); + if (secondNullable is null && first >= secondDefault) + { + description.Append(' ').Append(Messages.Default); + } + + description.AppendLine().AppendBulletPoint(string.Format( + Messages.RandomMax, Markdown.InlineCode(max.ToString()))); + if (secondNullable is null && first < secondDefault) + { + description.Append(' ').Append(Messages.Default); + } + + var embedColor = ColorsList.Blue; + if (secondNullable is not null && min == max) + { + description.AppendLine().Append(Markdown.Italicise(Messages.RandomMinMaxSame)); + embedColor = ColorsList.Red; + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.RandomTitle, executor.GetTag()), executor) + .WithDescription(description.ToString()) + .WithColour(embedColor) + .Build(); + + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + private static readonly TimestampStyle[] AllStyles = + [ + TimestampStyle.ShortDate, + TimestampStyle.LongDate, + TimestampStyle.ShortTime, + TimestampStyle.LongTime, + TimestampStyle.ShortDateTime, + TimestampStyle.LongDateTime, + TimestampStyle.RelativeTime + ]; + + /// + /// A slash command that shows the current timestamp with an optional offset in all styles supported by Discord. + /// + /// The offset for the current timestamp. + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("timestamp")] + [DiscordDefaultDMPermission(false)] + [Description("Shows a timestamp in all styles")] + [UsedImplicitly] + public async Task ExecuteTimestampAsync( + [Description("Offset from current time")] [Option("offset")] + string? stringOffset = null) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out var executorId)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return ResultExtensions.FromError(botResult); + } + + var executorResult = await _userApi.GetUserAsync(executorId, CancellationToken); + if (!executorResult.IsDefined(out var executor)) + { + return ResultExtensions.FromError(executorResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + if (stringOffset is null) + { + return await SendTimestampAsync(null, executor, CancellationToken); + } + + var parseResult = TimeSpanParser.TryParse(stringOffset); + if (!parseResult.IsDefined(out var offset)) + { + var failedEmbed = new EmbedBuilder() + .WithSmallTitle(Messages.InvalidTimeSpan, bot) + .WithDescription(Messages.TimeSpanExample) + .WithColour(ColorsList.Red) + .Build(); + + return await _feedback.SendContextualEmbedResultAsync(failedEmbed, ct: CancellationToken); + } + + return await SendTimestampAsync(offset, executor, CancellationToken); + } + + private Task SendTimestampAsync(TimeSpan? offset, IUser executor, CancellationToken ct) + { + var timestamp = DateTimeOffset.UtcNow.Add(offset ?? TimeSpan.Zero).ToUnixTimeSeconds(); + + var description = new StringBuilder().Append("# ").AppendLine(timestamp.ToString()); + + if (offset is not null) + { + description.AppendLine(string.Format( + Messages.TimestampOffset, Markdown.InlineCode(offset.ToString() ?? string.Empty))).AppendLine(); + } + + foreach (var markdownTimestamp in AllStyles.Select(style => Markdown.Timestamp(timestamp, style))) + { + description.AppendBulletPoint(Markdown.InlineCode(markdownTimestamp)) + .Append(" → ").AppendLine(markdownTimestamp); + } + + var embed = new EmbedBuilder().WithSmallTitle( + string.Format(Messages.TimestampTitle, executor.GetTag()), executor) + .WithDescription(description.ToString()) + .WithColour(ColorsList.Blue) + .Build(); + + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } + + /// + /// A slash command that shows a random answer from the Magic 8-Ball. + /// + /// Unused input. + /// + /// The 8-Ball answers were taken from Wikipedia. + /// + /// + /// A feedback sending result which may or may not have succeeded. + /// + [Command("8ball")] + [DiscordDefaultDMPermission(false)] + [Description("Ask the Magic 8-Ball a question")] + [UsedImplicitly] + public async Task ExecuteEightBallAsync( + // let the user think he's actually asking the ball a question + [Description("Question to ask")] string question) + { + if (!_context.TryGetContextIDs(out var guildId, out _, out _)) + { + return new ArgumentInvalidError(nameof(_context), "Unable to retrieve necessary IDs from command context"); + } + + var botResult = await _userApi.GetCurrentUserAsync(CancellationToken); + if (!botResult.IsDefined(out var bot)) + { + return ResultExtensions.FromError(botResult); + } + + var data = await _guildData.GetData(guildId, CancellationToken); + Messages.Culture = GuildSettings.Language.Get(data.Settings); + + return await AnswerEightBallAsync(bot, CancellationToken); + } + + private static readonly string[] AnswerTypes = + [ + "Positive", "Questionable", "Neutral", "Negative" + ]; + + private Task AnswerEightBallAsync(IUser bot, CancellationToken ct) + { + var typeNumber = Random.Shared.Next(0, 4); + var embedColor = typeNumber switch + { + 0 => ColorsList.Blue, + 1 => ColorsList.Green, + 2 => ColorsList.Yellow, + 3 => ColorsList.Red, + _ => throw new ArgumentOutOfRangeException(null, nameof(typeNumber)) + }; + + var answer = $"EightBall{AnswerTypes[typeNumber]}{Random.Shared.Next(1, 6)}".Localized(); + + var embed = new EmbedBuilder().WithSmallTitle(answer, bot) + .WithColour(embedColor) + .Build(); + + return _feedback.SendContextualEmbedResultAsync(embed, ct: ct); + } } diff --git a/TeamOctolings.Octobot/Data/GuildData.cs b/src/Data/GuildData.cs similarity index 97% rename from TeamOctolings.Octobot/Data/GuildData.cs rename to src/Data/GuildData.cs index f393323..5a903d6 100644 --- a/TeamOctolings.Octobot/Data/GuildData.cs +++ b/src/Data/GuildData.cs @@ -1,7 +1,7 @@ using System.Text.Json.Nodes; using Remora.Rest.Core; -namespace TeamOctolings.Octobot.Data; +namespace Octobot.Data; /// /// Stores information about a guild. This information is not accessible via the Discord API. diff --git a/TeamOctolings.Octobot/Data/GuildSettings.cs b/src/Data/GuildSettings.cs similarity index 92% rename from TeamOctolings.Octobot/Data/GuildSettings.cs rename to src/Data/GuildSettings.cs index dc59d6f..a1d8d74 100644 --- a/TeamOctolings.Octobot/Data/GuildSettings.cs +++ b/src/Data/GuildSettings.cs @@ -1,8 +1,8 @@ +using Octobot.Data.Options; +using Octobot.Responders; using Remora.Discord.API.Abstractions.Objects; -using TeamOctolings.Octobot.Data.Options; -using TeamOctolings.Octobot.Responders; -namespace TeamOctolings.Octobot.Data; +namespace Octobot.Data; /// /// Contains all per-guild settings that can be set by a member @@ -22,7 +22,7 @@ public static class GuildSettings /// /// /// - public static readonly GuildOption WelcomeMessage = new("WelcomeMessage", "default"); + public static readonly Option WelcomeMessage = new("WelcomeMessage", "default"); /// /// Controls what message should be sent in when a member leaves the guild. @@ -34,7 +34,7 @@ public static class GuildSettings /// /// /// - public static readonly GuildOption LeaveMessage = new("LeaveMessage", "default"); + public static readonly Option LeaveMessage = new("LeaveMessage", "default"); /// /// Controls whether or not the message should be sent diff --git a/TeamOctolings.Octobot/Data/MemberData.cs b/src/Data/MemberData.cs similarity index 93% rename from TeamOctolings.Octobot/Data/MemberData.cs rename to src/Data/MemberData.cs index 984d4af..1b3d0d9 100644 --- a/TeamOctolings.Octobot/Data/MemberData.cs +++ b/src/Data/MemberData.cs @@ -1,4 +1,4 @@ -namespace TeamOctolings.Octobot.Data; +namespace Octobot.Data; /// /// Stores information about a member diff --git a/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs b/src/Data/Options/AllOptionsEnum.cs similarity index 92% rename from TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs rename to src/Data/Options/AllOptionsEnum.cs index 6a4280e..d9e0c13 100644 --- a/TeamOctolings.Octobot/Data/Options/AllOptionsEnum.cs +++ b/src/Data/Options/AllOptionsEnum.cs @@ -1,7 +1,7 @@ using JetBrains.Annotations; -using TeamOctolings.Octobot.Commands; +using Octobot.Commands; -namespace TeamOctolings.Octobot.Data.Options; +namespace Octobot.Data.Options; /// /// Represents all options as enums. diff --git a/TeamOctolings.Octobot/Data/Options/BoolOption.cs b/src/Data/Options/BoolOption.cs similarity index 72% rename from TeamOctolings.Octobot/Data/Options/BoolOption.cs rename to src/Data/Options/BoolOption.cs index 3b81abb..6876164 100644 --- a/TeamOctolings.Octobot/Data/Options/BoolOption.cs +++ b/src/Data/Options/BoolOption.cs @@ -1,9 +1,9 @@ using System.Text.Json.Nodes; using Remora.Results; -namespace TeamOctolings.Octobot.Data.Options; +namespace Octobot.Data.Options; -public sealed class BoolOption : GuildOption +public sealed class BoolOption : Option { public BoolOption(string name, bool defaultValue) : base(name, defaultValue) { } @@ -12,16 +12,6 @@ public sealed class BoolOption : GuildOption return Get(settings) ? Messages.Yes : Messages.No; } - public override Result ValueEquals(JsonNode settings, string value) - { - if (!TryParseBool(value, out var boolean)) - { - return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue); - } - - return Value(settings).Equals(boolean.ToString()); - } - public override Result Set(JsonNode settings, string from) { if (!TryParseBool(from, out var value)) diff --git a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs b/src/Data/Options/IOption.cs similarity index 59% rename from TeamOctolings.Octobot/Data/Options/IGuildOption.cs rename to src/Data/Options/IOption.cs index 9920281..b8ed03c 100644 --- a/TeamOctolings.Octobot/Data/Options/IGuildOption.cs +++ b/src/Data/Options/IOption.cs @@ -1,13 +1,12 @@ using System.Text.Json.Nodes; using Remora.Results; -namespace TeamOctolings.Octobot.Data.Options; +namespace Octobot.Data.Options; -public interface IGuildOption +public interface IOption { string Name { get; } string Display(JsonNode settings); - Result ValueEquals(JsonNode settings, string value); Result Set(JsonNode settings, string from); Result Reset(JsonNode settings); } diff --git a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs b/src/Data/Options/LanguageOption.cs similarity index 71% rename from TeamOctolings.Octobot/Data/Options/LanguageOption.cs rename to src/Data/Options/LanguageOption.cs index f58e011..464c61b 100644 --- a/TeamOctolings.Octobot/Data/Options/LanguageOption.cs +++ b/src/Data/Options/LanguageOption.cs @@ -1,23 +1,25 @@ using System.Globalization; using System.Text.Json.Nodes; +using Remora.Discord.Extensions.Formatting; using Remora.Results; -namespace TeamOctolings.Octobot.Data.Options; +namespace Octobot.Data.Options; /// -public sealed class LanguageOption : GuildOption +public sealed class LanguageOption : Option { private static readonly Dictionary CultureInfoCache = new() { { "en", new CultureInfo("en-US") }, - { "ru", new CultureInfo("ru-RU") } + { "ru", new CultureInfo("ru-RU") }, + { "mctaylors-ru", new CultureInfo("tt-RU") } }; public LanguageOption(string name, string defaultValue) : base(name, CultureInfoCache[defaultValue]) { } - protected override string Value(JsonNode settings) + public override string Display(JsonNode settings) { - return settings[Name]?.GetValue() ?? "en"; + return Markdown.InlineCode(settings[Name]?.GetValue() ?? "en"); } /// diff --git a/TeamOctolings.Octobot/Data/Options/GuildOption.cs b/src/Data/Options/Option.cs similarity index 74% rename from TeamOctolings.Octobot/Data/Options/GuildOption.cs rename to src/Data/Options/Option.cs index ea9c30e..5d703a8 100644 --- a/TeamOctolings.Octobot/Data/Options/GuildOption.cs +++ b/src/Data/Options/Option.cs @@ -2,18 +2,18 @@ using System.Text.Json.Nodes; using Remora.Discord.Extensions.Formatting; using Remora.Results; -namespace TeamOctolings.Octobot.Data.Options; +namespace Octobot.Data.Options; /// -/// Represents a per-guild option. +/// Represents an per-guild option. /// /// The type of the option. -public class GuildOption : IGuildOption +public class Option : IOption where T : notnull { protected readonly T DefaultValue; - public GuildOption(string name, T defaultValue) + public Option(string name, T defaultValue) { Name = name; DefaultValue = defaultValue; @@ -21,19 +21,9 @@ public class GuildOption : IGuildOption public string Name { get; } - protected virtual string Value(JsonNode settings) - { - return Get(settings).ToString() ?? throw new InvalidOperationException(); - } - public virtual string Display(JsonNode settings) { - return Markdown.InlineCode(Value(settings)); - } - - public virtual Result ValueEquals(JsonNode settings, string value) - { - return Value(settings).Equals(value); + return Markdown.InlineCode(Get(settings).ToString() ?? throw new InvalidOperationException()); } /// diff --git a/TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs b/src/Data/Options/SnowflakeOption.cs similarity index 87% rename from TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs rename to src/Data/Options/SnowflakeOption.cs index b7405f2..7118da8 100644 --- a/TeamOctolings.Octobot/Data/Options/SnowflakeOption.cs +++ b/src/Data/Options/SnowflakeOption.cs @@ -1,13 +1,13 @@ using System.Text.Json.Nodes; using System.Text.RegularExpressions; +using Octobot.Extensions; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Extensions; -namespace TeamOctolings.Octobot.Data.Options; +namespace Octobot.Data.Options; -public sealed partial class SnowflakeOption : GuildOption +public sealed partial class SnowflakeOption : Option { public SnowflakeOption(string name) : base(name, 0UL.ToSnowflake()) { } diff --git a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs b/src/Data/Options/TimeSpanOption.cs similarity index 59% rename from TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs rename to src/Data/Options/TimeSpanOption.cs index 7e21343..d237b6e 100644 --- a/TeamOctolings.Octobot/Data/Options/TimeSpanOption.cs +++ b/src/Data/Options/TimeSpanOption.cs @@ -1,23 +1,13 @@ using System.Text.Json.Nodes; +using Octobot.Parsers; using Remora.Results; -using TeamOctolings.Octobot.Parsers; -namespace TeamOctolings.Octobot.Data.Options; +namespace Octobot.Data.Options; -public sealed class TimeSpanOption : GuildOption +public sealed class TimeSpanOption : Option { public TimeSpanOption(string name, TimeSpan defaultValue) : base(name, defaultValue) { } - public override Result ValueEquals(JsonNode settings, string value) - { - if (!TimeSpanParser.TryParse(value).IsDefined(out var span)) - { - return new ArgumentInvalidError(nameof(value), Messages.InvalidSettingValue); - } - - return Value(settings).Equals(span.ToString()); - } - public override TimeSpan Get(JsonNode settings) { var property = settings[Name]; diff --git a/src/Data/Reminder.cs b/src/Data/Reminder.cs new file mode 100644 index 0000000..f21b222 --- /dev/null +++ b/src/Data/Reminder.cs @@ -0,0 +1,9 @@ +namespace Octobot.Data; + +public struct Reminder +{ + public DateTimeOffset At { get; init; } + public string Text { get; init; } + public ulong ChannelId { get; init; } + public ulong MessageId { get; init; } +} diff --git a/TeamOctolings.Octobot/Data/ScheduledEventData.cs b/src/Data/ScheduledEventData.cs similarity index 97% rename from TeamOctolings.Octobot/Data/ScheduledEventData.cs rename to src/Data/ScheduledEventData.cs index 7ba6e92..59efc63 100644 --- a/TeamOctolings.Octobot/Data/ScheduledEventData.cs +++ b/src/Data/ScheduledEventData.cs @@ -1,7 +1,7 @@ using System.Text.Json.Serialization; using Remora.Discord.API.Abstractions.Objects; -namespace TeamOctolings.Octobot.Data; +namespace Octobot.Data; /// /// Stores information about scheduled events. This information is not provided by the Discord API. diff --git a/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs b/src/Extensions/ChannelApiExtensions.cs similarity index 74% rename from TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs rename to src/Extensions/ChannelApiExtensions.cs index 82f8889..99eff67 100644 --- a/TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs +++ b/src/Extensions/ChannelApiExtensions.cs @@ -5,19 +5,18 @@ using Remora.Discord.API.Objects; using Remora.Rest.Core; using Remora.Results; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class ChannelApiExtensions { public static async Task CreateMessageWithEmbedResultAsync(this IDiscordRestChannelAPI channelApi, Snowflake channelId, Optional message = default, Optional nonce = default, Optional isTextToSpeech = default, Optional> embedResult = default, - Optional allowedMentions = default, Optional messageReference = default, + Optional allowedMentions = default, Optional messageRefenence = default, Optional> components = default, Optional> stickerIds = default, Optional>> attachments = default, - Optional flags = default, Optional enforceNonce = default, - Optional poll = default, CancellationToken ct = default) + Optional flags = default, CancellationToken ct = default) { if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed)) { @@ -25,6 +24,6 @@ public static class ChannelApiExtensions } return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed }, - allowedMentions, messageReference, components, stickerIds, attachments, flags, enforceNonce, poll, ct); + allowedMentions, messageRefenence, components, stickerIds, attachments, flags, ct); } } diff --git a/TeamOctolings.Octobot/Extensions/CollectionExtensions.cs b/src/Extensions/CollectionExtensions.cs similarity index 96% rename from TeamOctolings.Octobot/Extensions/CollectionExtensions.cs rename to src/Extensions/CollectionExtensions.cs index 3ea13a8..2369532 100644 --- a/TeamOctolings.Octobot/Extensions/CollectionExtensions.cs +++ b/src/Extensions/CollectionExtensions.cs @@ -1,6 +1,6 @@ using Remora.Results; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class CollectionExtensions { diff --git a/TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs b/src/Extensions/CommandContextExtensions.cs similarity index 92% rename from TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs rename to src/Extensions/CommandContextExtensions.cs index 16b8b56..a0c02f2 100644 --- a/TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs +++ b/src/Extensions/CommandContextExtensions.cs @@ -2,7 +2,7 @@ using Remora.Discord.Commands.Extensions; using Remora.Rest.Core; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class CommandContextExtensions { diff --git a/TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs b/src/Extensions/DiffPaneModelExtensions.cs similarity index 94% rename from TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs rename to src/Extensions/DiffPaneModelExtensions.cs index 3bb707b..1c3a098 100644 --- a/TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs +++ b/src/Extensions/DiffPaneModelExtensions.cs @@ -1,7 +1,7 @@ using System.Text; using DiffPlex.DiffBuilder.Model; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class DiffPaneModelExtensions { diff --git a/TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs b/src/Extensions/EmbedBuilderExtensions.cs similarity index 99% rename from TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs rename to src/Extensions/EmbedBuilderExtensions.cs index dab0265..2d61403 100644 --- a/TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs +++ b/src/Extensions/EmbedBuilderExtensions.cs @@ -4,7 +4,7 @@ using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Rest.Core; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class EmbedBuilderExtensions { diff --git a/TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs b/src/Extensions/FeedbackServiceExtensions.cs similarity index 93% rename from TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs rename to src/Extensions/FeedbackServiceExtensions.cs index c66c946..e6ef376 100644 --- a/TeamOctolings.Octobot/Extensions/FeedbackServiceExtensions.cs +++ b/src/Extensions/FeedbackServiceExtensions.cs @@ -3,7 +3,7 @@ using Remora.Discord.Commands.Feedback.Messages; using Remora.Discord.Commands.Feedback.Services; using Remora.Results; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class FeedbackServiceExtensions { diff --git a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs b/src/Extensions/GuildScheduledEventExtensions.cs similarity index 92% rename from TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs rename to src/Extensions/GuildScheduledEventExtensions.cs index 7822d9b..f1b6985 100644 --- a/TeamOctolings.Octobot/Extensions/GuildScheduledEventExtensions.cs +++ b/src/Extensions/GuildScheduledEventExtensions.cs @@ -2,7 +2,7 @@ using Remora.Rest.Core; using Remora.Results; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class GuildScheduledEventExtensions { @@ -10,7 +10,7 @@ public static class GuildScheduledEventExtensions out string? location) { endTime = default; - location = null; + location = default; if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata)) { return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata)); diff --git a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs b/src/Extensions/LoggerExtensions.cs similarity index 92% rename from TeamOctolings.Octobot/Extensions/LoggerExtensions.cs rename to src/Extensions/LoggerExtensions.cs index fac4dda..fca3702 100644 --- a/TeamOctolings.Octobot/Extensions/LoggerExtensions.cs +++ b/src/Extensions/LoggerExtensions.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Logging; using Remora.Results; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class LoggerExtensions { @@ -25,7 +25,7 @@ public static class LoggerExtensions if (result.Error is ExceptionError exe) { - if (exe.Exception is OperationCanceledException) + if (exe.Exception is TaskCanceledException) { return; } diff --git a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs b/src/Extensions/MarkdownExtensions.cs similarity index 50% rename from TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs rename to src/Extensions/MarkdownExtensions.cs index 30ddff5..7b7f780 100644 --- a/TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs +++ b/src/Extensions/MarkdownExtensions.cs @@ -1,4 +1,4 @@ -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class MarkdownExtensions { @@ -13,16 +13,4 @@ public static class MarkdownExtensions { return $"- {text}"; } - - /// - /// Formats a string to use Markdown Quote formatting. - /// - /// The input text to format. - /// - /// A markdown-formatted quote string. - /// - public static string Quote(string text) - { - return $"> {text}"; - } } diff --git a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs b/src/Extensions/ResultExtensions.cs similarity index 79% rename from TeamOctolings.Octobot/Extensions/ResultExtensions.cs rename to src/Extensions/ResultExtensions.cs index 6872d34..f456dac 100644 --- a/TeamOctolings.Octobot/Extensions/ResultExtensions.cs +++ b/src/Extensions/ResultExtensions.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using Remora.Results; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class ResultExtensions { @@ -21,25 +21,21 @@ public static class ResultExtensions return casted; } + [Conditional("DEBUG")] private static void LogResultStackTrace(Result result) { - if (result.IsSuccess || result.Error is ExceptionError { Exception: OperationCanceledException }) + if (Octobot.StaticLogger is null || result.IsSuccess) { return; } - if (Utility.StaticLogger is null) - { - throw new InvalidOperationException(); - } - - Utility.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}", + Octobot.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}", result.Error.GetType().FullName, result.Error.Message, Environment.NewLine, ConstructStackTrace()); var inner = result.Inner; while (inner is { IsSuccess: false }) { - Utility.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}", + Octobot.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}", inner.Error.GetType().FullName, inner.Error.Message); inner = inner.Inner; diff --git a/TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs b/src/Extensions/SnowflakeExtensions.cs similarity index 96% rename from TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs rename to src/Extensions/SnowflakeExtensions.cs index 70810ef..e60bc44 100644 --- a/TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs +++ b/src/Extensions/SnowflakeExtensions.cs @@ -1,6 +1,6 @@ using Remora.Rest.Core; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class SnowflakeExtensions { diff --git a/TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs b/src/Extensions/StringBuilderExtensions.cs similarity index 98% rename from TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs rename to src/Extensions/StringBuilderExtensions.cs index 25b7b5b..ddd24a3 100644 --- a/TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs +++ b/src/Extensions/StringBuilderExtensions.cs @@ -1,6 +1,6 @@ using System.Text; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class StringBuilderExtensions { diff --git a/TeamOctolings.Octobot/Extensions/StringExtensions.cs b/src/Extensions/StringExtensions.cs similarity index 98% rename from TeamOctolings.Octobot/Extensions/StringExtensions.cs rename to src/Extensions/StringExtensions.cs index bf7f6c8..cb8d606 100644 --- a/TeamOctolings.Octobot/Extensions/StringExtensions.cs +++ b/src/Extensions/StringExtensions.cs @@ -1,7 +1,7 @@ using System.Net; using Remora.Discord.Extensions.Formatting; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class StringExtensions { diff --git a/TeamOctolings.Octobot/Extensions/UInt64Extensions.cs b/src/Extensions/UInt64Extensions.cs similarity index 82% rename from TeamOctolings.Octobot/Extensions/UInt64Extensions.cs rename to src/Extensions/UInt64Extensions.cs index 2b9c0a2..5d1db00 100644 --- a/TeamOctolings.Octobot/Extensions/UInt64Extensions.cs +++ b/src/Extensions/UInt64Extensions.cs @@ -1,7 +1,7 @@ using Remora.Discord.API; using Remora.Rest.Core; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class UInt64Extensions { diff --git a/TeamOctolings.Octobot/Extensions/UserExtensions.cs b/src/Extensions/UserExtensions.cs similarity index 85% rename from TeamOctolings.Octobot/Extensions/UserExtensions.cs rename to src/Extensions/UserExtensions.cs index d9eff33..38fe985 100644 --- a/TeamOctolings.Octobot/Extensions/UserExtensions.cs +++ b/src/Extensions/UserExtensions.cs @@ -1,6 +1,6 @@ using Remora.Discord.API.Abstractions.Objects; -namespace TeamOctolings.Octobot.Extensions; +namespace Octobot.Extensions; public static class UserExtensions { diff --git a/TeamOctolings.Octobot/Messages.Designer.cs b/src/Messages.Designer.cs similarity index 82% rename from TeamOctolings.Octobot/Messages.Designer.cs rename to src/Messages.Designer.cs index 1a81e02..729fd95 100644 --- a/TeamOctolings.Octobot/Messages.Designer.cs +++ b/src/Messages.Designer.cs @@ -7,34 +7,31 @@ // //------------------------------------------------------------------------------ -namespace TeamOctolings.Octobot { - using System; - - +namespace Octobot { [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [System.Diagnostics.DebuggerNonUserCodeAttribute()] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Messages { - + private static System.Resources.ResourceManager resourceMan; - + private static System.Globalization.CultureInfo resourceCulture; - + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Messages() { } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Resources.ResourceManager ResourceManager { get { if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("TeamOctolings.Octobot.Messages", typeof(Messages).Assembly); + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Octobot.locale.Messages", typeof(Messages).Assembly); resourceMan = temp; } return resourceMan; } } - + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] internal static System.Globalization.CultureInfo Culture { get { @@ -44,1163 +41,1102 @@ namespace TeamOctolings.Octobot { resourceCulture = value; } } - + internal static string Ready { get { return ResourceManager.GetString("Ready", resourceCulture); } } - + internal static string CachedMessageDeleted { get { return ResourceManager.GetString("CachedMessageDeleted", resourceCulture); } } - + internal static string CachedMessageEdited { get { return ResourceManager.GetString("CachedMessageEdited", resourceCulture); } } - + internal static string DefaultWelcomeMessage { get { return ResourceManager.GetString("DefaultWelcomeMessage", resourceCulture); } } - + internal static string Generic1 { get { return ResourceManager.GetString("Generic1", resourceCulture); } } - + internal static string Generic2 { get { return ResourceManager.GetString("Generic2", resourceCulture); } } - + internal static string Generic3 { get { return ResourceManager.GetString("Generic3", resourceCulture); } } - + internal static string YouWereBanned { get { return ResourceManager.GetString("YouWereBanned", resourceCulture); } } - + internal static string PunishmentExpired { get { return ResourceManager.GetString("PunishmentExpired", resourceCulture); } } - + internal static string YouWereKicked { get { return ResourceManager.GetString("YouWereKicked", resourceCulture); } } - + internal static string Milliseconds { get { return ResourceManager.GetString("Milliseconds", resourceCulture); } } - - internal static string ChannelNotSpecified { + + internal static string SettingsLang { get { - return ResourceManager.GetString("ChannelNotSpecified", resourceCulture); + return ResourceManager.GetString("SettingsLang", resourceCulture); } } - - internal static string RoleNotSpecified { - get { - return ResourceManager.GetString("RoleNotSpecified", resourceCulture); - } - } - - internal static string SettingsLanguage { - get { - return ResourceManager.GetString("SettingsLanguage", resourceCulture); - } - } - - internal static string SettingsPrefix { - get { - return ResourceManager.GetString("SettingsPrefix", resourceCulture); - } - } - + internal static string SettingsRemoveRolesOnMute { get { return ResourceManager.GetString("SettingsRemoveRolesOnMute", resourceCulture); } } - - internal static string SettingsSendWelcomeMessages { - get { - return ResourceManager.GetString("SettingsSendWelcomeMessages", resourceCulture); - } - } - + internal static string SettingsMuteRole { get { return ResourceManager.GetString("SettingsMuteRole", resourceCulture); } } - + internal static string LanguageNotSupported { get { return ResourceManager.GetString("LanguageNotSupported", resourceCulture); } } - + internal static string Yes { get { return ResourceManager.GetString("Yes", resourceCulture); } } - + internal static string No { get { return ResourceManager.GetString("No", resourceCulture); } } - + internal static string UserNotBanned { get { return ResourceManager.GetString("UserNotBanned", resourceCulture); } } - + internal static string MemberNotMuted { get { return ResourceManager.GetString("MemberNotMuted", resourceCulture); } } - + internal static string SettingsWelcomeMessage { get { return ResourceManager.GetString("SettingsWelcomeMessage", resourceCulture); } } - + internal static string UserBanned { get { return ResourceManager.GetString("UserBanned", resourceCulture); } } - + internal static string SettingsReceiveStartupMessages { get { return ResourceManager.GetString("SettingsReceiveStartupMessages", resourceCulture); } } - + internal static string InvalidSettingValue { get { return ResourceManager.GetString("InvalidSettingValue", resourceCulture); } } - + + internal static string InvalidRole { + get { + return ResourceManager.GetString("InvalidRole", resourceCulture); + } + } + + internal static string InvalidChannel { + get { + return ResourceManager.GetString("InvalidChannel", resourceCulture); + } + } + internal static string DurationRequiredForTimeOuts { get { return ResourceManager.GetString("DurationRequiredForTimeOuts", resourceCulture); } } - + internal static string CannotTimeOutBot { get { return ResourceManager.GetString("CannotTimeOutBot", resourceCulture); } } - + internal static string SettingsEventNotificationRole { get { return ResourceManager.GetString("SettingsEventNotificationRole", resourceCulture); } } - + internal static string SettingsEventNotificationChannel { get { return ResourceManager.GetString("SettingsEventNotificationChannel", resourceCulture); } } - - internal static string SettingsEventStartedReceivers { - get { - return ResourceManager.GetString("SettingsEventStartedReceivers", resourceCulture); - } - } - + internal static string EventStarted { get { return ResourceManager.GetString("EventStarted", resourceCulture); } } - + internal static string EventCancelled { get { return ResourceManager.GetString("EventCancelled", resourceCulture); } } - + internal static string EventCompleted { get { return ResourceManager.GetString("EventCompleted", resourceCulture); } } - + internal static string MessagesCleared { get { return ResourceManager.GetString("MessagesCleared", resourceCulture); } } - + internal static string SettingsNothingChanged { get { return ResourceManager.GetString("SettingsNothingChanged", resourceCulture); } } - - internal static string SettingNotDefined { - get { - return ResourceManager.GetString("SettingNotDefined", resourceCulture); - } - } - - internal static string MissingUser { - get { - return ResourceManager.GetString("MissingUser", resourceCulture); - } - } - - internal static string UserCannotBanMembers { - get { - return ResourceManager.GetString("UserCannotBanMembers", resourceCulture); - } - } - - internal static string UserCannotManageMessages { - get { - return ResourceManager.GetString("UserCannotManageMessages", resourceCulture); - } - } - - internal static string UserCannotKickMembers { - get { - return ResourceManager.GetString("UserCannotKickMembers", resourceCulture); - } - } - - internal static string UserCannotMuteMembers { - get { - return ResourceManager.GetString("UserCannotMuteMembers", resourceCulture); - } - } - - internal static string UserCannotUnmuteMembers { - get { - return ResourceManager.GetString("UserCannotUnmuteMembers", resourceCulture); - } - } - - internal static string UserCannotManageGuild { - get { - return ResourceManager.GetString("UserCannotManageGuild", resourceCulture); - } - } - + internal static string BotCannotBanMembers { get { return ResourceManager.GetString("BotCannotBanMembers", resourceCulture); } } - + internal static string BotCannotManageMessages { get { return ResourceManager.GetString("BotCannotManageMessages", resourceCulture); } } - + internal static string BotCannotKickMembers { get { return ResourceManager.GetString("BotCannotKickMembers", resourceCulture); } } - + internal static string BotCannotModerateMembers { get { return ResourceManager.GetString("BotCannotModerateMembers", resourceCulture); } } - + internal static string BotCannotManageGuild { get { return ResourceManager.GetString("BotCannotManageGuild", resourceCulture); } } - + internal static string UserCannotBanOwner { get { return ResourceManager.GetString("UserCannotBanOwner", resourceCulture); } } - + internal static string UserCannotBanThemselves { get { return ResourceManager.GetString("UserCannotBanThemselves", resourceCulture); } } - + internal static string UserCannotBanBot { get { return ResourceManager.GetString("UserCannotBanBot", resourceCulture); } } - + internal static string BotCannotBanTarget { get { return ResourceManager.GetString("BotCannotBanTarget", resourceCulture); } } - + internal static string UserCannotBanTarget { get { return ResourceManager.GetString("UserCannotBanTarget", resourceCulture); } } - + internal static string UserCannotKickOwner { get { return ResourceManager.GetString("UserCannotKickOwner", resourceCulture); } } - + internal static string UserCannotKickThemselves { get { return ResourceManager.GetString("UserCannotKickThemselves", resourceCulture); } } - + internal static string UserCannotKickBot { get { return ResourceManager.GetString("UserCannotKickBot", resourceCulture); } } - + internal static string BotCannotKickTarget { get { return ResourceManager.GetString("BotCannotKickTarget", resourceCulture); } } - + internal static string UserCannotKickTarget { get { return ResourceManager.GetString("UserCannotKickTarget", resourceCulture); } } - + internal static string UserCannotMuteOwner { get { return ResourceManager.GetString("UserCannotMuteOwner", resourceCulture); } } - + internal static string UserCannotMuteThemselves { get { return ResourceManager.GetString("UserCannotMuteThemselves", resourceCulture); } } - + internal static string UserCannotMuteBot { get { return ResourceManager.GetString("UserCannotMuteBot", resourceCulture); } } - + internal static string BotCannotMuteTarget { get { return ResourceManager.GetString("BotCannotMuteTarget", resourceCulture); } } - + internal static string UserCannotMuteTarget { get { return ResourceManager.GetString("UserCannotMuteTarget", resourceCulture); } } - + internal static string UserCannotUnmuteOwner { get { return ResourceManager.GetString("UserCannotUnmuteOwner", resourceCulture); } } - + internal static string UserCannotUnmuteThemselves { get { return ResourceManager.GetString("UserCannotUnmuteThemselves", resourceCulture); } } - + internal static string UserCannotUnmuteBot { get { return ResourceManager.GetString("UserCannotUnmuteBot", resourceCulture); } } - + internal static string BotCannotUnmuteTarget { get { return ResourceManager.GetString("BotCannotUnmuteTarget", resourceCulture); } } - + internal static string UserCannotUnmuteTarget { get { return ResourceManager.GetString("UserCannotUnmuteTarget", resourceCulture); } } - + internal static string EventEarlyNotification { get { return ResourceManager.GetString("EventEarlyNotification", resourceCulture); } } - + internal static string SettingsEventEarlyNotificationOffset { get { return ResourceManager.GetString("SettingsEventEarlyNotificationOffset", resourceCulture); } } - + internal static string UserNotFound { get { return ResourceManager.GetString("UserNotFound", resourceCulture); } } - + internal static string SettingsDefaultRole { get { return ResourceManager.GetString("SettingsDefaultRole", resourceCulture); } } - + internal static string SettingsPublicFeedbackChannel { get { return ResourceManager.GetString("SettingsPublicFeedbackChannel", resourceCulture); } } - + internal static string SettingsPrivateFeedbackChannel { get { return ResourceManager.GetString("SettingsPrivateFeedbackChannel", resourceCulture); } } - + internal static string SettingsReturnRolesOnRejoin { get { return ResourceManager.GetString("SettingsReturnRolesOnRejoin", resourceCulture); } } - + internal static string SettingsAutoStartEvents { get { return ResourceManager.GetString("SettingsAutoStartEvents", resourceCulture); } } - + internal static string IssuedBy { get { return ResourceManager.GetString("IssuedBy", resourceCulture); } } - + internal static string EventCreatedTitle { get { return ResourceManager.GetString("EventCreatedTitle", resourceCulture); } } - + internal static string DescriptionLocalEventCreated { get { return ResourceManager.GetString("DescriptionLocalEventCreated", resourceCulture); } } - + internal static string DescriptionExternalEventCreated { get { return ResourceManager.GetString("DescriptionExternalEventCreated", resourceCulture); } } - + internal static string ButtonOpenEventInfo { get { return ResourceManager.GetString("ButtonOpenEventInfo", resourceCulture); } } - + internal static string EventDuration { get { return ResourceManager.GetString("EventDuration", resourceCulture); } } - + internal static string DescriptionLocalEventStarted { get { return ResourceManager.GetString("DescriptionLocalEventStarted", resourceCulture); } } - + internal static string DescriptionExternalEventStarted { get { return ResourceManager.GetString("DescriptionExternalEventStarted", resourceCulture); } } - + internal static string UserAlreadyBanned { get { return ResourceManager.GetString("UserAlreadyBanned", resourceCulture); } } - + internal static string UserUnbanned { get { return ResourceManager.GetString("UserUnbanned", resourceCulture); } } - + internal static string UserMuted { get { return ResourceManager.GetString("UserMuted", resourceCulture); } } - + internal static string UserUnmuted { get { return ResourceManager.GetString("UserUnmuted", resourceCulture); } } - + internal static string UserNotMuted { get { return ResourceManager.GetString("UserNotMuted", resourceCulture); } } - + internal static string UserNotFoundShort { get { return ResourceManager.GetString("UserNotFoundShort", resourceCulture); } } - + internal static string UserKicked { get { return ResourceManager.GetString("UserKicked", resourceCulture); } } - + internal static string DescriptionActionReason { get { return ResourceManager.GetString("DescriptionActionReason", resourceCulture); } } - + internal static string DescriptionActionExpiresAt { get { return ResourceManager.GetString("DescriptionActionExpiresAt", resourceCulture); } } - + internal static string UserAlreadyMuted { get { return ResourceManager.GetString("UserAlreadyMuted", resourceCulture); } } - + internal static string MessageFrom { get { return ResourceManager.GetString("MessageFrom", resourceCulture); } } - + internal static string AboutTitleDevelopers { get { return ResourceManager.GetString("AboutTitleDevelopers", resourceCulture); } } - - internal static string ButtonOpenWebsite { + + internal static string ButtonOpenRepository { get { - return ResourceManager.GetString("ButtonOpenWebsite", resourceCulture); + return ResourceManager.GetString("ButtonOpenRepository", resourceCulture); } } - + internal static string AboutBot { get { return ResourceManager.GetString("AboutBot", resourceCulture); } } - + internal static string AboutDeveloper_mctaylors { get { return ResourceManager.GetString("AboutDeveloper@mctaylors", resourceCulture); } } - + internal static string AboutDeveloper_Octol1ttle { get { return ResourceManager.GetString("AboutDeveloper@Octol1ttle", resourceCulture); } } - + internal static string AboutDeveloper_neroduckale { get { return ResourceManager.GetString("AboutDeveloper@neroduckale", resourceCulture); } } - + internal static string ReminderCreated { get { return ResourceManager.GetString("ReminderCreated", resourceCulture); } } - + internal static string Reminder { get { return ResourceManager.GetString("Reminder", resourceCulture); } } - + internal static string DescriptionReminder { get { return ResourceManager.GetString("DescriptionReminder", resourceCulture); } } - + internal static string SettingsListTitle { get { return ResourceManager.GetString("SettingsListTitle", resourceCulture); } } - + internal static string SettingSuccessfullyChanged { get { return ResourceManager.GetString("SettingSuccessfullyChanged", resourceCulture); } } - + internal static string SettingNotChanged { get { return ResourceManager.GetString("SettingNotChanged", resourceCulture); } } - + internal static string SettingIsNow { get { return ResourceManager.GetString("SettingIsNow", resourceCulture); } } - - internal static string SettingsRenameHoistedUsers { - get { - return ResourceManager.GetString("SettingsRenameHoistedUsers", resourceCulture); - } - } - + internal static string Page { get { return ResourceManager.GetString("Page", resourceCulture); } } - + internal static string PageNotFound { get { return ResourceManager.GetString("PageNotFound", resourceCulture); } } - + internal static string PagesAllowed { get { return ResourceManager.GetString("PagesAllowed", resourceCulture); } } - - internal static string Next { - get { - return ResourceManager.GetString("Next", resourceCulture); - } - } - - internal static string Previous { - get { - return ResourceManager.GetString("Previous", resourceCulture); - } - } - + internal static string ReminderList { get { return ResourceManager.GetString("ReminderList", resourceCulture); } } - + internal static string InvalidReminderPosition { get { return ResourceManager.GetString("InvalidReminderPosition", resourceCulture); } } - + internal static string ReminderDeleted { get { return ResourceManager.GetString("ReminderDeleted", resourceCulture); } } - + internal static string NoRemindersFound { get { return ResourceManager.GetString("NoRemindersFound", resourceCulture); } } - + internal static string SingleSettingReset { get { return ResourceManager.GetString("SingleSettingReset", resourceCulture); } } - + internal static string AllSettingsReset { get { return ResourceManager.GetString("AllSettingsReset", resourceCulture); } } - + internal static string DescriptionActionJumpToMessage { get { return ResourceManager.GetString("DescriptionActionJumpToMessage", resourceCulture); } } - + internal static string DescriptionActionJumpToChannel { get { return ResourceManager.GetString("DescriptionActionJumpToChannel", resourceCulture); } } - + internal static string ReminderPosition { get { return ResourceManager.GetString("ReminderPosition", resourceCulture); } } - + internal static string ReminderTime { get { return ResourceManager.GetString("ReminderTime", resourceCulture); } } - + internal static string ReminderText { get { return ResourceManager.GetString("ReminderText", resourceCulture); } } - - internal static string UserInfoDisplayName { - get { - return ResourceManager.GetString("UserInfoDisplayName", resourceCulture); - } - } - + internal static string InformationAbout { get { return ResourceManager.GetString("InformationAbout", resourceCulture); } } - - internal static string UserInfoMuted { + + internal static string UserInfoDisplayName { get { - return ResourceManager.GetString("UserInfoMuted", resourceCulture); + return ResourceManager.GetString("UserInfoDisplayName", resourceCulture); } } - + internal static string UserInfoDiscordUserSince { get { return ResourceManager.GetString("UserInfoDiscordUserSince", resourceCulture); } } - + + internal static string UserInfoMuted { + get { + return ResourceManager.GetString("UserInfoMuted", resourceCulture); + } + } + internal static string UserInfoBanned { get { return ResourceManager.GetString("UserInfoBanned", resourceCulture); } } - + internal static string UserInfoPunishments { get { return ResourceManager.GetString("UserInfoPunishments", resourceCulture); } } - + internal static string UserInfoBannedPermanently { get { return ResourceManager.GetString("UserInfoBannedPermanently", resourceCulture); } } - + internal static string UserInfoNotOnGuild { get { return ResourceManager.GetString("UserInfoNotOnGuild", resourceCulture); } } - + internal static string UserInfoMutedByTimeout { get { return ResourceManager.GetString("UserInfoMutedByTimeout", resourceCulture); } } - + internal static string UserInfoMutedByMuteRole { get { return ResourceManager.GetString("UserInfoMutedByMuteRole", resourceCulture); } } - + internal static string UserInfoGuildMemberSince { get { return ResourceManager.GetString("UserInfoGuildMemberSince", resourceCulture); } } - + internal static string UserInfoGuildNickname { get { return ResourceManager.GetString("UserInfoGuildNickname", resourceCulture); } } - + internal static string UserInfoGuildRoles { get { return ResourceManager.GetString("UserInfoGuildRoles", resourceCulture); } } - + internal static string UserInfoGuildMemberPremiumSince { get { return ResourceManager.GetString("UserInfoGuildMemberPremiumSince", resourceCulture); } } - - internal static string RandomTitle { + + internal static string RandomTitle + { get { return ResourceManager.GetString("RandomTitle", resourceCulture); } } - - internal static string RandomMinMaxSame { + + internal static string RandomMinMaxSame + { get { return ResourceManager.GetString("RandomMinMaxSame", resourceCulture); } } - - internal static string RandomMin { - get { - return ResourceManager.GetString("RandomMin", resourceCulture); - } - } - - internal static string RandomMax { + + internal static string RandomMax + { get { return ResourceManager.GetString("RandomMax", resourceCulture); } } - - internal static string Default { + + internal static string RandomMin + { + get { + return ResourceManager.GetString("RandomMin", resourceCulture); + } + } + + internal static string Default + { get { return ResourceManager.GetString("Default", resourceCulture); } } - - internal static string TimestampTitle { - get { + + internal static string TimestampTitle + { + get + { return ResourceManager.GetString("TimestampTitle", resourceCulture); } } - - internal static string TimestampOffset { - get { + + internal static string TimestampOffset + { + get + { return ResourceManager.GetString("TimestampOffset", resourceCulture); } } - - internal static string GuildInfoDescription { - get { + + internal static string GuildInfoDescription + { + get + { return ResourceManager.GetString("GuildInfoDescription", resourceCulture); } } - - internal static string GuildInfoCreatedAt { - get { + + internal static string GuildInfoCreatedAt + { + get + { return ResourceManager.GetString("GuildInfoCreatedAt", resourceCulture); } } - - internal static string GuildInfoOwner { - get { + + internal static string GuildInfoOwner + { + get + { return ResourceManager.GetString("GuildInfoOwner", resourceCulture); } } - - internal static string GuildInfoServerBoost { - get { + + internal static string GuildInfoServerBoost + { + get + { return ResourceManager.GetString("GuildInfoServerBoost", resourceCulture); } } - - internal static string GuildInfoBoostTier { - get { + + internal static string GuildInfoBoostTier + { + get + { return ResourceManager.GetString("GuildInfoBoostTier", resourceCulture); } } - - internal static string GuildInfoBoostCount { - get { + + internal static string GuildInfoBoostCount + { + get + { return ResourceManager.GetString("GuildInfoBoostCount", resourceCulture); } } - - internal static string NoMessagesToClear { - get { + + internal static string NoMessagesToClear + { + get + { return ResourceManager.GetString("NoMessagesToClear", resourceCulture); } } - - internal static string MessagesClearedFiltered { - get { + + internal static string MessagesClearedFiltered + { + get + { return ResourceManager.GetString("MessagesClearedFiltered", resourceCulture); } } - - internal static string DataLoadFailedTitle { - get { + + internal static string DataLoadFailedTitle + { + get + { return ResourceManager.GetString("DataLoadFailedTitle", resourceCulture); } } - - internal static string DataLoadFailedDescription { - get { + + internal static string DataLoadFailedDescription + { + get + { return ResourceManager.GetString("DataLoadFailedDescription", resourceCulture); } } - - internal static string CommandExecutionFailed { - get { + + internal static string CommandExecutionFailed + { + get + { return ResourceManager.GetString("CommandExecutionFailed", resourceCulture); } } - - internal static string ContactDevelopers { - get { + + internal static string ContactDevelopers + { + get + { return ResourceManager.GetString("ContactDevelopers", resourceCulture); } } - - internal static string ButtonReportIssue { - get { + + internal static string ButtonReportIssue + { + get + { return ResourceManager.GetString("ButtonReportIssue", resourceCulture); } } - + internal static string DefaultLeaveMessage { get { return ResourceManager.GetString("DefaultLeaveMessage", resourceCulture); } } - + internal static string SettingsLeaveMessage { get { return ResourceManager.GetString("SettingsLeaveMessage", resourceCulture); } } - + internal static string InvalidTimeSpan { get { return ResourceManager.GetString("InvalidTimeSpan", resourceCulture); } } - + internal static string UserInfoKicked { get { return ResourceManager.GetString("UserInfoKicked", resourceCulture); } } - + internal static string ReminderEdited { get { return ResourceManager.GetString("ReminderEdited", resourceCulture); } } - + internal static string EightBallPositive1 { get { return ResourceManager.GetString("EightBallPositive1", resourceCulture); } } - + internal static string EightBallPositive2 { get { return ResourceManager.GetString("EightBallPositive2", resourceCulture); } } - + internal static string EightBallPositive3 { get { return ResourceManager.GetString("EightBallPositive3", resourceCulture); } } - + internal static string EightBallPositive4 { get { return ResourceManager.GetString("EightBallPositive4", resourceCulture); } } - + internal static string EightBallPositive5 { get { return ResourceManager.GetString("EightBallPositive5", resourceCulture); } } - + internal static string EightBallQuestionable1 { get { return ResourceManager.GetString("EightBallQuestionable1", resourceCulture); } } - + internal static string EightBallQuestionable2 { get { return ResourceManager.GetString("EightBallQuestionable2", resourceCulture); } } - + internal static string EightBallQuestionable3 { get { return ResourceManager.GetString("EightBallQuestionable3", resourceCulture); } } - + internal static string EightBallQuestionable4 { get { return ResourceManager.GetString("EightBallQuestionable4", resourceCulture); } } - + internal static string EightBallQuestionable5 { get { return ResourceManager.GetString("EightBallQuestionable5", resourceCulture); } } - + internal static string EightBallNeutral1 { get { return ResourceManager.GetString("EightBallNeutral1", resourceCulture); } } - + internal static string EightBallNeutral2 { get { return ResourceManager.GetString("EightBallNeutral2", resourceCulture); } } - + internal static string EightBallNeutral3 { get { return ResourceManager.GetString("EightBallNeutral3", resourceCulture); } } - + internal static string EightBallNeutral4 { get { return ResourceManager.GetString("EightBallNeutral4", resourceCulture); } } - + internal static string EightBallNeutral5 { get { return ResourceManager.GetString("EightBallNeutral5", resourceCulture); } } - + internal static string EightBallNegative1 { get { return ResourceManager.GetString("EightBallNegative1", resourceCulture); } } - + internal static string EightBallNegative2 { get { return ResourceManager.GetString("EightBallNegative2", resourceCulture); } } - + internal static string EightBallNegative3 { get { return ResourceManager.GetString("EightBallNegative3", resourceCulture); } } - + internal static string EightBallNegative4 { get { return ResourceManager.GetString("EightBallNegative4", resourceCulture); } } - + internal static string EightBallNegative5 { get { return ResourceManager.GetString("EightBallNegative5", resourceCulture); } } - + internal static string TimeSpanExample { get { return ResourceManager.GetString("TimeSpanExample", resourceCulture); } } - + internal static string Version { get { return ResourceManager.GetString("Version", resourceCulture); } } - + internal static string SettingsWelcomeMessagesChannel { get { return ResourceManager.GetString("SettingsWelcomeMessagesChannel", resourceCulture); } } - + internal static string ButtonDirty { get { return ResourceManager.GetString("ButtonDirty", resourceCulture); } } - + internal static string ButtonOpenWiki { get { return ResourceManager.GetString("ButtonOpenWiki", resourceCulture); } } - - internal static string SettingsModeratorRole { - get { - return ResourceManager.GetString("SettingsModeratorRole", resourceCulture); - } - } - - internal static string SettingValueEquals { - get { - return ResourceManager.GetString("SettingValueEquals", resourceCulture); - } - } } } diff --git a/TeamOctolings.Octobot/Program.cs b/src/Octobot.cs similarity index 54% rename from TeamOctolings.Octobot/Program.cs rename to src/Octobot.cs index 8cdbdcf..065967e 100644 --- a/TeamOctolings.Octobot/Program.cs +++ b/src/Octobot.cs @@ -2,8 +2,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Attributes; +using Octobot.Commands.Events; +using Octobot.Services; +using Octobot.Services.Update; using Remora.Discord.API.Abstractions.Gateway.Commands; using Remora.Discord.API.Abstractions.Objects; +using Remora.Discord.API.Objects; using Remora.Discord.Caching.Extensions; using Remora.Discord.Caching.Services; using Remora.Discord.Commands.Extensions; @@ -11,20 +16,24 @@ using Remora.Discord.Commands.Services; using Remora.Discord.Extensions.Extensions; using Remora.Discord.Gateway; using Remora.Discord.Hosting.Extensions; +using Remora.Rest.Core; using Serilog.Extensions.Logging; -using TeamOctolings.Octobot.Commands.Events; -using TeamOctolings.Octobot.Services; -using TeamOctolings.Octobot.Services.Update; -namespace TeamOctolings.Octobot; +namespace Octobot; -public sealed class Program +public sealed class Octobot { + public static readonly AllowedMentions NoMentions = new( + Array.Empty(), Array.Empty(), Array.Empty()); + + [StaticCallersOnly] + public static ILogger? StaticLogger { get; private set; } + public static async Task Main(string[] args) { var host = CreateHostBuilder(args).UseConsoleLifetime().Build(); var services = host.Services; - Utility.StaticLogger = services.GetRequiredService>(); + StaticLogger = services.GetRequiredService>(); var slashService = services.GetRequiredService(); // Providing a guild ID to this call will result in command duplicates! @@ -39,7 +48,8 @@ public sealed class Program private static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) - .AddDiscordService(services => + .AddDiscordService( + services => { var configuration = services.GetRequiredService(); @@ -48,29 +58,32 @@ public sealed class Program "No bot token has been provided. Set the " + "BOT_TOKEN environment variable to a valid token."); } - ).ConfigureServices((_, services) => + ).ConfigureServices( + (_, services) => { - services.Configure(options => - { - options.Intents |= GatewayIntents.MessageContents - | GatewayIntents.GuildMembers - | GatewayIntents.GuildPresences - | GatewayIntents.GuildScheduledEvents; - }); - services.Configure(cSettings => - { - cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); - cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); - cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); - cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); - }); + services.Configure( + options => + { + options.Intents |= GatewayIntents.MessageContents + | GatewayIntents.GuildMembers + | GatewayIntents.GuildPresences + | GatewayIntents.GuildScheduledEvents; + }); + services.Configure( + cSettings => + { + cSettings.SetDefaultAbsoluteExpiration(TimeSpan.FromHours(1)); + cSettings.SetDefaultSlidingExpiration(TimeSpan.FromMinutes(30)); + cSettings.SetAbsoluteExpiration(TimeSpan.FromDays(7)); + cSettings.SetSlidingExpiration(TimeSpan.FromDays(7)); + }); services.AddTransient() // Init .AddDiscordCaching() .AddDiscordCommands(true, false) - .AddRespondersFromAssembly(typeof(Program).Assembly) - .AddCommandGroupsFromAssembly(typeof(Program).Assembly) + .AddRespondersFromAssembly(typeof(Octobot).Assembly) + .AddCommandGroupsFromAssembly(typeof(Octobot).Assembly) // Slash command event handlers .AddPreparationErrorEvent() .AddPostExecutionEvent() @@ -83,13 +96,14 @@ public sealed class Program .AddHostedService() .AddHostedService(); } - ).ConfigureLogging(c => c.AddConsole() - .AddFile("Logs/Octobot-{Date}.log", - outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") - .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) - .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + ).ConfigureLogging( + c => c.AddConsole() + .AddFile("Logs/Octobot-{Date}.log", + outputTemplate: "{Timestamp:o} [{Level:u4}] {Message} {NewLine}{Exception}") + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.LogicalHandler", LogLevel.Warning) + .AddFilter("System.Net.Http.HttpClient.*.ClientHandler", LogLevel.Warning) ); } } diff --git a/TeamOctolings.Octobot/Parsers/TimeSpanParser.cs b/src/Parsers/TimeSpanParser.cs similarity index 98% rename from TeamOctolings.Octobot/Parsers/TimeSpanParser.cs rename to src/Parsers/TimeSpanParser.cs index 99a8b90..1f44d46 100644 --- a/TeamOctolings.Octobot/Parsers/TimeSpanParser.cs +++ b/src/Parsers/TimeSpanParser.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using Remora.Commands.Parsers; using Remora.Results; -namespace TeamOctolings.Octobot.Parsers; +namespace Octobot.Parsers; /// /// Parses s. diff --git a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs b/src/Responders/GuildLoadedResponder.cs similarity index 90% rename from TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs rename to src/Responders/GuildLoadedResponder.cs index b24ef0b..b03fd3f 100644 --- a/TeamOctolings.Octobot/Responders/GuildLoadedResponder.cs +++ b/src/Responders/GuildLoadedResponder.cs @@ -1,5 +1,8 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -8,18 +11,15 @@ using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Responders; +namespace Octobot.Responders; /// /// Handles sending a message to a guild that has just initialized if that guild /// has enabled /// [UsedImplicitly] -public sealed class GuildLoadedResponder : IResponder +public class GuildLoadedResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; private readonly GuildDataService _guildData; @@ -94,7 +94,7 @@ public sealed class GuildLoadedResponder : IResponder GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, ct: ct); } - private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct = default) + private async Task SendDataLoadFailed(IGuild guild, GuildData data, IUser bot, CancellationToken ct) { var channelResult = await _utility.GetEmergencyFeedbackChannel(guild, data, ct); if (!channelResult.IsDefined(out var channel)) @@ -114,12 +114,12 @@ public sealed class GuildLoadedResponder : IResponder BuildInfo.IsDirty ? Messages.ButtonDirty : Messages.ButtonReportIssue, - new PartialEmoji(Name: "\u26a0\ufe0f"), // 'WARNING SIGN' (U+26A0) + new PartialEmoji(Name: "⚠️"), URL: BuildInfo.IssuesUrl, IsDisabled: BuildInfo.IsDirty ); return await _channelApi.CreateMessageWithEmbedResultAsync(channel, embedResult: errorEmbed, - components: new[] { new ActionRowComponent([issuesButton]) }, ct: ct); + components: new[] { new ActionRowComponent(new[] { issuesButton }) }, ct: ct); } } diff --git a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs b/src/Responders/GuildMemberJoinedResponder.cs similarity index 91% rename from TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs rename to src/Responders/GuildMemberJoinedResponder.cs index ae9f174..61ef5cc 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberJoinedResponder.cs +++ b/src/Responders/GuildMemberJoinedResponder.cs @@ -1,16 +1,16 @@ using System.Text.Json.Nodes; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Responders; +namespace Octobot.Responders; /// /// Handles sending a guild's if one is set. @@ -18,7 +18,7 @@ namespace TeamOctolings.Octobot.Responders; /// /// [UsedImplicitly] -public sealed class GuildMemberJoinedResponder : IResponder +public class GuildMemberJoinedResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildAPI _guildApi; @@ -77,11 +77,11 @@ public sealed class GuildMemberJoinedResponder : IResponder return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, - allowedMentions: Utility.NoMentions, ct: ct); + allowedMentions: Octobot.NoMentions, ct: ct); } private async Task TryReturnRolesAsync( - JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct = default) + JsonNode cfg, MemberData memberData, Snowflake guildId, Snowflake userId, CancellationToken ct) { if (!GuildSettings.ReturnRolesOnRejoin.Get(cfg)) { diff --git a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs b/src/Responders/GuildMemberLeftResponder.cs similarity index 80% rename from TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs rename to src/Responders/GuildMemberLeftResponder.cs index 957a107..90cc64c 100644 --- a/TeamOctolings.Octobot/Responders/GuildMemberLeftResponder.cs +++ b/src/Responders/GuildMemberLeftResponder.cs @@ -1,21 +1,21 @@ using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Responders; +namespace Octobot.Responders; /// /// Handles sending a guild's if one is set. /// /// [UsedImplicitly] -public sealed class GuildMemberLeftResponder : IResponder +public class GuildMemberLeftResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildAPI _guildApi; @@ -36,9 +36,13 @@ public sealed class GuildMemberLeftResponder : IResponder var cfg = data.Settings; var memberData = data.GetOrCreateMemberData(user.ID); - if (memberData.BannedUntil is not null || memberData.Kicked - || GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() - || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") + if (memberData.BannedUntil is not null || memberData.Kicked) + { + return Result.Success; + } + + if (GuildSettings.WelcomeMessagesChannel.Get(cfg).Empty() + || GuildSettings.LeaveMessage.Get(cfg) is "off" or "disable" or "disabled") { return Result.Success; } @@ -63,6 +67,6 @@ public sealed class GuildMemberLeftResponder : IResponder return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.WelcomeMessagesChannel.Get(cfg), embedResult: embed, - allowedMentions: Utility.NoMentions, ct: ct); + allowedMentions: Octobot.NoMentions, ct: ct); } } diff --git a/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs b/src/Responders/GuildUnloadedResponder.cs similarity index 84% rename from TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs rename to src/Responders/GuildUnloadedResponder.cs index c73c134..b49d136 100644 --- a/TeamOctolings.Octobot/Responders/GuildUnloadedResponder.cs +++ b/src/Responders/GuildUnloadedResponder.cs @@ -1,18 +1,18 @@ using JetBrains.Annotations; using Microsoft.Extensions.Logging; +using Octobot.Data; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.Gateway.Responders; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Responders; +namespace Octobot.Responders; /// /// Handles removing guild ID from if the guild becomes unavailable. /// [UsedImplicitly] -public sealed class GuildUnloadedResponder : IResponder +public class GuildUnloadedResponder : IResponder { private readonly GuildDataService _guildData; private readonly ILogger _logger; diff --git a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs b/src/Responders/MessageDeletedResponder.cs similarity index 89% rename from TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs rename to src/Responders/MessageDeletedResponder.cs index f0e3d22..5a69273 100644 --- a/TeamOctolings.Octobot/Responders/MessageDeletedResponder.cs +++ b/src/Responders/MessageDeletedResponder.cs @@ -1,5 +1,8 @@ using System.Text; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -7,18 +10,15 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Discord.Gateway.Responders; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Responders; +namespace Octobot.Responders; /// /// Handles logging the contents of a deleted message and the user who deleted the message /// to a guild's if one is set. /// [UsedImplicitly] -public sealed class MessageDeletedResponder : IResponder +public class MessageDeletedResponder : IResponder { private readonly IDiscordRestAuditLogAPI _auditLogApi; private readonly IDiscordRestChannelAPI _channelApi; @@ -66,10 +66,10 @@ public sealed class MessageDeletedResponder : IResponder return ResultExtensions.FromError(auditLogResult); } - var deleterResult = Result.FromSuccess(message.Author); + var auditLog = auditLogPage.AuditLogEntries.Single(); - var auditLog = auditLogPage.AuditLogEntries.SingleOrDefault(); - if (auditLog is { UserID: not null } + var deleterResult = Result.FromSuccess(message.Author); + if (auditLog.UserID is not null && auditLog.Options.Value.ChannelID == gatewayEvent.ChannelID && DateTimeOffset.UtcNow.Subtract(auditLog.ID.Timestamp).TotalSeconds <= 2) { @@ -102,6 +102,6 @@ public sealed class MessageDeletedResponder : IResponder return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, - allowedMentions: Utility.NoMentions, ct: ct); + allowedMentions: Octobot.NoMentions, ct: ct); } } diff --git a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs b/src/Responders/MessageEditedResponder.cs similarity index 70% rename from TeamOctolings.Octobot/Responders/MessageEditedResponder.cs rename to src/Responders/MessageEditedResponder.cs index e3d1c58..1143652 100644 --- a/TeamOctolings.Octobot/Responders/MessageEditedResponder.cs +++ b/src/Responders/MessageEditedResponder.cs @@ -1,6 +1,9 @@ using System.Text; using DiffPlex.DiffBuilder; using JetBrains.Annotations; +using Octobot.Data; +using Octobot.Extensions; +using Octobot.Services; using Remora.Discord.API.Abstractions.Gateway.Events; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; @@ -9,18 +12,15 @@ using Remora.Discord.Caching.Services; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Gateway.Responders; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -using TeamOctolings.Octobot.Services; -namespace TeamOctolings.Octobot.Responders; +namespace Octobot.Responders; /// /// Handles logging the difference between an edited message's old and new content /// to a guild's if one is set. /// [UsedImplicitly] -public sealed class MessageEditedResponder : IResponder +public class MessageEditedResponder : IResponder { private readonly CacheService _cacheService; private readonly IDiscordRestChannelAPI _channelApi; @@ -36,29 +36,40 @@ public sealed class MessageEditedResponder : IResponder public async Task RespondAsync(IMessageUpdate gatewayEvent, CancellationToken ct = default) { + if (!gatewayEvent.ID.IsDefined(out var messageId)) + { + return new ArgumentNullError(nameof(gatewayEvent.ID)); + } + + if (!gatewayEvent.ChannelID.IsDefined(out var channelId)) + { + return new ArgumentNullError(nameof(gatewayEvent.ChannelID)); + } + if (!gatewayEvent.GuildID.IsDefined(out var guildId) - || !gatewayEvent.EditedTimestamp.HasValue - || gatewayEvent.Author.IsBot.OrDefault(false)) + || !gatewayEvent.Author.IsDefined(out var author) + || !gatewayEvent.EditedTimestamp.IsDefined(out var timestamp) + || !gatewayEvent.Content.IsDefined(out var newContent)) { return Result.Success; } var cfg = await _guildData.GetSettings(guildId, ct); - if (GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) + if (author.IsBot.OrDefault(false) || GuildSettings.PrivateFeedbackChannel.Get(cfg).Empty()) { return Result.Success; } - var cacheKey = new KeyHelpers.MessageCacheKey(gatewayEvent.ChannelID, gatewayEvent.ID); + var cacheKey = new KeyHelpers.MessageCacheKey(channelId, messageId); var messageResult = await _cacheService.TryGetValueAsync( cacheKey, ct); if (!messageResult.IsDefined(out var message)) { - _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); + _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); return Result.Success; } - if (message.Content == gatewayEvent.Content) + if (message.Content == newContent) { return Result.Success; } @@ -72,27 +83,27 @@ public sealed class MessageEditedResponder : IResponder // We don't need to await this since the result is not needed // NOTE: Because this is not awaited, there may be a race condition depending on how fast clients are able to edit their messages // NOTE: Awaiting this might not even solve this if the same responder is called asynchronously - _ = _channelApi.GetChannelMessageAsync(gatewayEvent.ChannelID, gatewayEvent.ID, ct); + _ = _channelApi.GetChannelMessageAsync(channelId, messageId, ct); - var diff = InlineDiffBuilder.Diff(message.Content, gatewayEvent.Content); + var diff = InlineDiffBuilder.Diff(message.Content, newContent); Messages.Culture = GuildSettings.Language.Get(cfg); var builder = new StringBuilder() .AppendLine(diff.AsMarkdown()) .AppendLine(string.Format(Messages.DescriptionActionJumpToMessage, - $"https://discord.com/channels/{guildId}/{gatewayEvent.ChannelID}/{gatewayEvent.ID}") + $"https://discord.com/channels/{guildId}/{channelId}/{messageId}") ); var embed = new EmbedBuilder() .WithSmallTitle(string.Format(Messages.CachedMessageEdited, message.Author.GetTag()), message.Author) .WithDescription(builder.ToString()) - .WithTimestamp(gatewayEvent.EditedTimestamp.Value) + .WithTimestamp(timestamp.Value) .WithColour(ColorsList.Yellow) .Build(); return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.PrivateFeedbackChannel.Get(cfg), embedResult: embed, - allowedMentions: Utility.NoMentions, ct: ct); + allowedMentions: Octobot.NoMentions, ct: ct); } } diff --git a/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs b/src/Responders/MessageReceivedResponder.cs similarity index 91% rename from TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs rename to src/Responders/MessageReceivedResponder.cs index 24d53a5..4c26d8d 100644 --- a/TeamOctolings.Octobot/Responders/MessageReceivedResponder.cs +++ b/src/Responders/MessageReceivedResponder.cs @@ -5,13 +5,13 @@ using Remora.Discord.Gateway.Responders; using Remora.Rest.Core; using Remora.Results; -namespace TeamOctolings.Octobot.Responders; +namespace Octobot.Responders; /// /// Handles sending replies to easter egg messages. /// [UsedImplicitly] -public sealed class MessageCreateResponder : IResponder +public class MessageCreateResponder : IResponder { private readonly IDiscordRestChannelAPI _channelApi; diff --git a/TeamOctolings.Octobot/Services/AccessControlService.cs b/src/Services/AccessControlService.cs similarity index 58% rename from TeamOctolings.Octobot/Services/AccessControlService.cs rename to src/Services/AccessControlService.cs index d39c9e5..aeb16e4 100644 --- a/TeamOctolings.Octobot/Services/AccessControlService.cs +++ b/src/Services/AccessControlService.cs @@ -1,39 +1,44 @@ -using Remora.Discord.API.Abstractions.Objects; +using Octobot.Data; +using Octobot.Extensions; +using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; +using Remora.Discord.Commands.Conditions; +using Remora.Discord.Commands.Results; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -namespace TeamOctolings.Octobot.Services; +namespace Octobot.Services; public sealed class AccessControlService { private readonly GuildDataService _data; private readonly IDiscordRestGuildAPI _guildApi; + private readonly RequireDiscordPermissionCondition _permission; private readonly IDiscordRestUserAPI _userApi; - public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi) + public AccessControlService(GuildDataService data, IDiscordRestGuildAPI guildApi, IDiscordRestUserAPI userApi, + RequireDiscordPermissionCondition permission) { _data = data; _guildApi = guildApi; _userApi = userApi; + _permission = permission; } - private static bool CheckPermission(IEnumerable roles, GuildData data, MemberData memberData, - DiscordPermission permission) + private async Task> CheckPermissionAsync(GuildData data, Snowflake memberId, IGuildMember member, + DiscordPermission permission, CancellationToken ct = default) { var moderatorRole = GuildSettings.ModeratorRole.Get(data.Settings); - if (!moderatorRole.Empty() && memberData.Roles.Contains(moderatorRole.Value)) + var result = await _permission.CheckAsync(new RequireDiscordPermissionAttribute([permission]), member, ct); + + if (result.Error is not null and not PermissionDeniedError) { - return true; + return Result.FromError(result); } - return roles - .Where(r => memberData.Roles.Contains(r.ID.Value)) - .Any(r => - r.Permissions.HasPermission(permission) - ); + var hasPermission = result.IsSuccess; + return hasPermission || (!moderatorRole.Empty() && + data.GetOrCreateMemberData(memberId).Roles.Contains(moderatorRole.Value)); } /// @@ -62,21 +67,28 @@ public sealed class AccessControlService return Result.FromSuccess($"UserCannot{action}Themselves".Localized()); } + var botResult = await _userApi.GetCurrentUserAsync(ct); + if (!botResult.IsDefined(out var bot)) + { + return Result.FromError(botResult); + } + var guildResult = await _guildApi.GetGuildAsync(guildId, ct: ct); if (!guildResult.IsDefined(out var guild)) { return Result.FromError(guildResult); } - if (interacterId == guild.OwnerID) + var targetMemberResult = await _guildApi.GetGuildMemberAsync(guildId, targetId, ct); + if (!targetMemberResult.IsDefined(out var targetMember)) { return Result.FromSuccess(null); } - var botResult = await _userApi.GetCurrentUserAsync(ct); - if (!botResult.IsDefined(out var bot)) + var botMemberResult = await _guildApi.GetGuildMemberAsync(guildId, bot.ID, ct); + if (!botMemberResult.IsDefined(out var botMember)) { - return Result.FromError(botResult); + return Result.FromError(botMemberResult); } var rolesResult = await _guildApi.GetGuildRolesAsync(guildId, ct); @@ -85,46 +97,63 @@ public sealed class AccessControlService return Result.FromError(rolesResult); } - var data = await _data.GetData(guildId, ct); - var targetData = data.GetOrCreateMemberData(targetId); - var botData = data.GetOrCreateMemberData(bot.ID); - if (interacterId is null) { - return CheckInteractions(action, guild, roles, targetData, botData, botData); + return CheckInteractions(action, guild, roles, targetMember, botMember, botMember); } - var interacterData = data.GetOrCreateMemberData(interacterId.Value); - var hasPermission = CheckPermission(roles, data, interacterData, + var interacterResult = await _guildApi.GetGuildMemberAsync(guildId, interacterId.Value, ct); + if (!interacterResult.IsDefined(out var interacter)) + { + return Result.FromError(interacterResult); + } + + var data = await _data.GetData(guildId, ct); + + var permissionResult = await CheckPermissionAsync(data, interacterId.Value, interacter, action switch { "Ban" => DiscordPermission.BanMembers, "Kick" => DiscordPermission.KickMembers, "Mute" or "Unmute" => DiscordPermission.ModerateMembers, _ => throw new Exception() - }); + }, ct); + if (!permissionResult.IsDefined(out var hasPermission)) + { + return Result.FromError(permissionResult); + } return hasPermission - ? CheckInteractions(action, guild, roles, targetData, botData, interacterData) + ? CheckInteractions(action, guild, roles, targetMember, botMember, interacter) : Result.FromSuccess($"UserCannot{action}Members".Localized()); } private static Result CheckInteractions( - string action, IGuild guild, IReadOnlyList roles, MemberData targetData, MemberData botData, - MemberData interacterData) + string action, IGuild guild, IReadOnlyList roles, IGuildMember targetMember, IGuildMember botMember, + IGuildMember interacter) { - if (botData.Id == targetData.Id) + if (!targetMember.User.IsDefined(out var targetUser)) + { + return new ArgumentNullError(nameof(targetMember.User)); + } + + if (!interacter.User.IsDefined(out var interacterUser)) + { + return new ArgumentNullError(nameof(interacter.User)); + } + + if (botMember.User == targetMember.User) { return Result.FromSuccess($"UserCannot{action}Bot".Localized()); } - if (targetData.Id == guild.OwnerID) + if (targetUser.ID == guild.OwnerID) { return Result.FromSuccess($"UserCannot{action}Owner".Localized()); } - var targetRoles = roles.Where(r => targetData.Roles.Contains(r.ID.Value)).ToList(); - var botRoles = roles.Where(r => botData.Roles.Contains(r.ID.Value)); + var targetRoles = roles.Where(r => targetMember.Roles.Contains(r.ID)).ToList(); + var botRoles = roles.Where(r => botMember.Roles.Contains(r.ID)); var targetBotRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - botRoles.MaxOrDefault(r => r.Position); if (targetBotRoleDiff >= 0) @@ -132,7 +161,12 @@ public sealed class AccessControlService return Result.FromSuccess($"BotCannot{action}Target".Localized()); } - var interacterRoles = roles.Where(r => interacterData.Roles.Contains(r.ID.Value)); + if (interacterUser.ID == guild.OwnerID) + { + return Result.FromSuccess(null); + } + + var interacterRoles = roles.Where(r => interacter.Roles.Contains(r.ID)); var targetInteracterRoleDiff = targetRoles.MaxOrDefault(r => r.Position) - interacterRoles.MaxOrDefault(r => r.Position); return targetInteracterRoleDiff < 0 diff --git a/src/Services/GuildDataService.cs b/src/Services/GuildDataService.cs new file mode 100644 index 0000000..c9458a0 --- /dev/null +++ b/src/Services/GuildDataService.cs @@ -0,0 +1,186 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Octobot.Data; +using Remora.Rest.Core; + +namespace Octobot.Services; + +/// +/// Handles saving, loading, initializing and providing . +/// +public sealed class GuildDataService : BackgroundService +{ + private readonly ConcurrentDictionary _datas = new(); + private readonly ILogger _logger; + + public GuildDataService(ILogger logger) + { + _logger = logger; + } + + public override Task StopAsync(CancellationToken ct) + { + base.StopAsync(ct); + return SaveAsync(ct); + } + + private Task SaveAsync(CancellationToken ct) + { + var tasks = new List(); + var datas = _datas.Values.ToArray(); + foreach (var data in datas.Where(data => !data.DataLoadFailed)) + { + tasks.Add(SerializeObjectSafelyAsync(data.Settings, data.SettingsPath, ct)); + tasks.Add(SerializeObjectSafelyAsync(data.ScheduledEvents, data.ScheduledEventsPath, ct)); + + var memberDatas = data.MemberData.Values.ToArray(); + tasks.AddRange(memberDatas.Select(memberData => + SerializeObjectSafelyAsync(memberData, $"{data.MemberDataPath}/{memberData.Id}.json", ct))); + } + + return Task.WhenAll(tasks); + } + + private static async Task SerializeObjectSafelyAsync(T obj, string path, CancellationToken ct) + { + var tempFilePath = path + ".tmp"; + await using (var tempFileStream = File.Create(tempFilePath)) + { + await JsonSerializer.SerializeAsync(tempFileStream, obj, cancellationToken: ct); + } + + File.Copy(tempFilePath, path, true); + File.Delete(tempFilePath); + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); + + while (await timer.WaitForNextTickAsync(ct)) + { + await SaveAsync(ct); + } + } + + public async Task GetData(Snowflake guildId, CancellationToken ct = default) + { + return _datas.TryGetValue(guildId, out var data) ? data : await InitializeData(guildId, ct); + } + + private async Task InitializeData(Snowflake guildId, CancellationToken ct = default) + { + var path = $"GuildData/{guildId}"; + var memberDataPath = $"{path}/MemberData"; + var settingsPath = $"{path}/Settings.json"; + var scheduledEventsPath = $"{path}/ScheduledEvents.json"; + + MigrateGuildData(guildId, path); + + Directory.CreateDirectory(path); + + if (!File.Exists(settingsPath)) + { + await File.WriteAllTextAsync(settingsPath, "{}", ct); + } + + if (!File.Exists(scheduledEventsPath)) + { + await File.WriteAllTextAsync(scheduledEventsPath, "{}", ct); + } + + var dataLoadFailed = false; + + await using var settingsStream = File.OpenRead(settingsPath); + JsonNode? jsonSettings = null; + try + { + jsonSettings = await JsonNode.ParseAsync(settingsStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Guild settings load failed: {Path}", settingsPath); + dataLoadFailed = true; + } + + await using var eventsStream = File.OpenRead(scheduledEventsPath); + Dictionary? events = null; + try + { + events = await JsonSerializer.DeserializeAsync>( + eventsStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Guild scheduled events load failed: {Path}", scheduledEventsPath); + dataLoadFailed = true; + } + + var memberData = new Dictionary(); + foreach (var dataFileInfo in Directory.CreateDirectory(memberDataPath).GetFiles()) + { + await using var dataStream = dataFileInfo.OpenRead(); + MemberData? data; + try + { + data = await JsonSerializer.DeserializeAsync(dataStream, cancellationToken: ct); + } + catch (Exception e) + { + _logger.LogError(e, "Member data load failed: {MemberDataPath}/{FileName}", memberDataPath, + dataFileInfo.Name); + dataLoadFailed = true; + continue; + } + + if (data is null) + { + continue; + } + + memberData.Add(data.Id, data); + } + + var finalData = new GuildData( + jsonSettings ?? new JsonObject(), settingsPath, + events ?? new Dictionary(), scheduledEventsPath, + memberData, memberDataPath, + dataLoadFailed); + + _datas.TryAdd(guildId, finalData); + + return finalData; + } + + private void MigrateGuildData(Snowflake guildId, string newPath) + { + var oldPath = $"{guildId}"; + + if (Directory.Exists(oldPath)) + { + Directory.CreateDirectory($"{newPath}/.."); + Directory.Move(oldPath, newPath); + + _logger.LogInformation("Moved guild data to separate folder: \"{OldPath}\" -> \"{NewPath}\"", oldPath, + newPath); + } + } + + public async Task GetSettings(Snowflake guildId, CancellationToken ct = default) + { + return (await GetData(guildId, ct)).Settings; + } + + public ICollection GetGuildIds() + { + return _datas.Keys; + } + + public bool UnloadGuildData(Snowflake id) + { + return _datas.TryRemove(id, out _); + } +} diff --git a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs b/src/Services/Update/MemberUpdateService.cs similarity index 95% rename from TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs rename to src/Services/Update/MemberUpdateService.cs index 3170060..e177fca 100644 --- a/TeamOctolings.Octobot/Services/Update/MemberUpdateService.cs +++ b/src/Services/Update/MemberUpdateService.cs @@ -2,16 +2,16 @@ using System.Text; using System.Text.RegularExpressions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Data; +using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -namespace TeamOctolings.Octobot.Services.Update; +namespace Octobot.Services.Update; public sealed partial class MemberUpdateService : BackgroundService { @@ -62,7 +62,7 @@ public sealed partial class MemberUpdateService : BackgroundService } } - private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct = default) + private async Task TickMemberDatasAsync(Snowflake guildId, CancellationToken ct) { var guildData = await _guildData.GetData(guildId, ct); var defaultRole = GuildSettings.DefaultRole.Get(guildData.Settings); @@ -79,7 +79,7 @@ public sealed partial class MemberUpdateService : BackgroundService private async Task TickMemberDataAsync(Snowflake guildId, GuildData guildData, Snowflake defaultRole, MemberData data, - CancellationToken ct = default) + CancellationToken ct) { var failedResults = new List(); var id = data.Id.ToSnowflake(); @@ -144,7 +144,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task TryAutoUnbanAsync( - Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default) + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) { if (data.BannedUntil is null || DateTimeOffset.UtcNow <= data.BannedUntil) { @@ -169,7 +169,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task TryAutoUnmuteAsync( - Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct = default) + Snowflake guildId, Snowflake id, MemberData data, CancellationToken ct) { if (data.MutedUntil is null || DateTimeOffset.UtcNow <= data.MutedUntil) { @@ -188,7 +188,7 @@ public sealed partial class MemberUpdateService : BackgroundService } private async Task FilterNicknameAsync(Snowflake guildId, IUser user, IGuildMember member, - CancellationToken ct = default) + CancellationToken ct) { var currentNickname = member.Nickname.IsDefined(out var nickname) ? nickname @@ -226,7 +226,7 @@ public sealed partial class MemberUpdateService : BackgroundService private static partial Regex IllegalChars(); private async Task TickReminderAsync(Reminder reminder, IUser user, MemberData data, Snowflake guildId, - CancellationToken ct = default) + CancellationToken ct) { if (DateTimeOffset.UtcNow < reminder.At) { @@ -234,7 +234,7 @@ public sealed partial class MemberUpdateService : BackgroundService } var builder = new StringBuilder() - .AppendLine(MarkdownExtensions.Quote(reminder.Text)) + .AppendBulletPointLine(string.Format(Messages.DescriptionReminder, Markdown.InlineCode(reminder.Text))) .AppendBulletPointLine(string.Format(Messages.DescriptionActionJumpToMessage, $"https://discord.com/channels/{guildId.Value}/{reminder.ChannelId}/{reminder.MessageId}")); diff --git a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs b/src/Services/Update/ScheduledEventUpdateService.cs similarity index 97% rename from TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs rename to src/Services/Update/ScheduledEventUpdateService.cs index 389a6a8..8168fc1 100644 --- a/TeamOctolings.Octobot/Services/Update/ScheduledEventUpdateService.cs +++ b/src/Services/Update/ScheduledEventUpdateService.cs @@ -1,6 +1,8 @@ using System.Text.Json.Nodes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Octobot.Data; +using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; using Remora.Discord.API.Objects; @@ -8,10 +10,8 @@ using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -namespace TeamOctolings.Octobot.Services.Update; +namespace Octobot.Services.Update; public sealed class ScheduledEventUpdateService : BackgroundService { @@ -46,7 +46,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } } - private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct = default) + private async Task TickScheduledEventsAsync(Snowflake guildId, CancellationToken ct) { var failedResults = new List(); var data = await _guildData.GetData(guildId, ct); @@ -133,7 +133,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService private async Task TickScheduledEventAsync( Snowflake guildId, GuildData data, IGuildScheduledEvent scheduledEvent, ScheduledEventData eventData, - CancellationToken ct = default) + CancellationToken ct) { if (GuildSettings.AutoStartEvents.Get(data.Settings) && DateTimeOffset.UtcNow >= scheduledEvent.ScheduledStartTime @@ -160,7 +160,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task AutoStartEventAsync( - Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct = default) + Snowflake guildId, IGuildScheduledEvent scheduledEvent, CancellationToken ct) { return (Result)await _eventApi.ModifyGuildScheduledEventAsync( guildId, scheduledEvent.ID, @@ -223,13 +223,13 @@ public sealed class ScheduledEventUpdateService : BackgroundService var button = new ButtonComponent( ButtonComponentStyle.Link, Messages.ButtonOpenEventInfo, - new PartialEmoji(Name: "\ud83d\udccb"), // 'CLIPBOARD' (U+1F4CB) + new PartialEmoji(Name: "📋"), URL: $"https://discord.com/events/{scheduledEvent.GuildID}/{scheduledEvent.ID}" ); return await _channelApi.CreateMessageWithEmbedResultAsync( GuildSettings.EventNotificationChannel.Get(settings), roleMention, embedResult: embed, - components: new[] { new ActionRowComponent([button]) }, ct: ct); + components: new[] { new ActionRowComponent(new[] { button }) }, ct: ct); } private static Result GetExternalScheduledEventCreatedEmbedDescription( @@ -319,7 +319,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendScheduledEventCompletedMessage(ScheduledEventData eventData, GuildData data, - CancellationToken ct = default) + CancellationToken ct) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { @@ -351,7 +351,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendScheduledEventCancelledMessage(ScheduledEventData eventData, GuildData data, - CancellationToken ct = default) + CancellationToken ct) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { @@ -405,7 +405,7 @@ public sealed class ScheduledEventUpdateService : BackgroundService } private async Task SendEarlyEventNotificationAsync( - IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct = default) + IGuildScheduledEvent scheduledEvent, GuildData data, CancellationToken ct) { if (GuildSettings.EventNotificationChannel.Get(data.Settings).Empty()) { diff --git a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs b/src/Services/Update/SongUpdateService.cs similarity index 66% rename from TeamOctolings.Octobot/Services/Update/SongUpdateService.cs rename to src/Services/Update/SongUpdateService.cs index 8eaa4c2..53cc59b 100644 --- a/TeamOctolings.Octobot/Services/Update/SongUpdateService.cs +++ b/src/Services/Update/SongUpdateService.cs @@ -4,7 +4,7 @@ using Remora.Discord.API.Gateway.Commands; using Remora.Discord.API.Objects; using Remora.Discord.Gateway; -namespace TeamOctolings.Octobot.Services.Update; +namespace Octobot.Services.Update; public sealed class SongUpdateService : BackgroundService { @@ -29,19 +29,10 @@ public sealed class SongUpdateService : BackgroundService ("Callie", "Bomb Rush Blush", new TimeSpan(0, 2, 18)), ("Turquoise October", "Octoling Rendezvous", new TimeSpan(0, 1, 57)), ("Damp Socks feat. Off the Hook", "Tentacle to the Metal", new TimeSpan(0, 2, 51)), - ("Off the Hook feat. Dedf1sh", "Spectrum Obligato ~ Ebb & Flow (Out of Order)", new TimeSpan(0, 4, 30)), - ("Dedf1sh feat. Off the Hook", "#47 onward", new TimeSpan(0, 4, 40)), - ("Free Association", "EchΘ Θnslaught", new TimeSpan(0, 2, 52)), - ("Off the Hook", "Short Order", new TimeSpan(0, 3, 36)), - ("Deep Cut", "Fins in the Air", new TimeSpan(0, 3, 1)) + ("Off the Hook", "Fly Octo Fly ~ Ebb & Flow (Octo)", new TimeSpan(0, 3, 5)) ]; - private static readonly (string Author, string Name, TimeSpan Duration)[] SpecialSongList = - [ - ("Squid Sisters", "Maritime Memory", new TimeSpan(0, 2, 47)) - ]; - - private readonly List _activityList = [new("with Remora.Discord", ActivityType.Game)]; + private readonly List _activityList = [new Activity("with Remora.Discord", ActivityType.Game)]; private readonly DiscordGatewayClient _client; private readonly GuildDataService _guildData; @@ -63,33 +54,19 @@ public sealed class SongUpdateService : BackgroundService while (!ct.IsCancellationRequested) { - var nextSong = NextSong(); + var nextSong = SongList[_nextSongIndex]; _activityList[0] = new Activity($"{nextSong.Name} / {nextSong.Author}", ActivityType.Listening); _client.SubmitCommand( new UpdatePresence( UserStatus.Online, false, DateTimeOffset.UtcNow, _activityList)); + _nextSongIndex++; + if (_nextSongIndex >= SongList.Length) + { + _nextSongIndex = 0; + } await Task.Delay(nextSong.Duration, ct); } } - - private (string Author, string Name, TimeSpan Duration) NextSong() - { - var today = DateTime.Today; - // Discontinuation of Online Services for Nintendo Wii U - if (today.Day is 8 or 9 && today.Month is 4) - { - return SpecialSongList[0]; // Maritime Memory / Squid Sisters - } - - var nextSong = SongList[_nextSongIndex]; - _nextSongIndex++; - if (_nextSongIndex >= SongList.Length) - { - _nextSongIndex = 0; - } - - return nextSong; - } } diff --git a/TeamOctolings.Octobot/Utility.cs b/src/Services/Utility.cs similarity index 89% rename from TeamOctolings.Octobot/Utility.cs rename to src/Services/Utility.cs index a2f7aca..3b9ab19 100644 --- a/TeamOctolings.Octobot/Utility.cs +++ b/src/Services/Utility.cs @@ -1,19 +1,16 @@ using System.Drawing; using System.Text; using System.Text.Json.Nodes; -using Microsoft.Extensions.Logging; +using Octobot.Data; +using Octobot.Extensions; using Remora.Discord.API.Abstractions.Objects; using Remora.Discord.API.Abstractions.Rest; -using Remora.Discord.API.Objects; using Remora.Discord.Extensions.Embeds; using Remora.Discord.Extensions.Formatting; using Remora.Rest.Core; using Remora.Results; -using TeamOctolings.Octobot.Attributes; -using TeamOctolings.Octobot.Data; -using TeamOctolings.Octobot.Extensions; -namespace TeamOctolings.Octobot; +namespace Octobot.Services; /// /// Provides utility methods that cannot be transformed to extension methods because they require usage @@ -21,9 +18,6 @@ namespace TeamOctolings.Octobot; /// public sealed class Utility { - public static readonly AllowedMentions NoMentions = new( - Array.Empty(), Array.Empty(), Array.Empty()); - private readonly IDiscordRestChannelAPI _channelApi; private readonly IDiscordRestGuildScheduledEventAPI _eventApi; private readonly IDiscordRestGuildAPI _guildApi; @@ -36,9 +30,6 @@ public sealed class Utility _guildApi = guildApi; } - [StaticCallersOnly] - public static ILogger? StaticLogger { get; set; } - /// /// Gets the string mentioning the and event subscribers related to /// a scheduled @@ -67,8 +58,8 @@ public sealed class Utility builder.Append($"{Mention.Role(role)} "); } - builder = subscribers.Where(subscriber => - !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) + builder = subscribers.Where( + subscriber => !data.GetOrCreateMemberData(subscriber.User.ID).Roles.Contains(role.Value)) .Aggregate(builder, (current, subscriber) => current.Append($"{Mention.User(subscriber.User)} ")); return builder.ToString(); } @@ -125,7 +116,7 @@ public sealed class Utility } } - public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct = default) + public async Task> GetEmergencyFeedbackChannel(IGuild guild, GuildData data, CancellationToken ct) { var privateFeedback = GuildSettings.PrivateFeedbackChannel.Get(data.Settings); if (!privateFeedback.Empty())