Skip to content

Commit

Permalink
Merge pull request #41 from bobbahbrown/1.3.10
Browse files Browse the repository at this point in the history
Version 1.3.10 - Add Discord Bot
  • Loading branch information
bobbah authored Aug 20, 2021
2 parents 39a2199 + 0585dad commit 7e5bdb0
Show file tree
Hide file tree
Showing 51 changed files with 27,623 additions and 25,306 deletions.
2 changes: 1 addition & 1 deletion CentCom.API/CentCom.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Version>1.3.9</Version>
<Version>1.3.10</Version>
<UserSecretsId>1f5f48fa-862f-4472-ba34-2c5a26035e88</UserSecretsId>
</PropertyGroup>

Expand Down
45 changes: 45 additions & 0 deletions CentCom.Bot/CentCom.Bot.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<Version>1.3.10</Version>
<TargetFramework>net5.0</TargetFramework>
<UserSecretsId>c8af1449-8cdf-4707-a66d-51e896551bfb</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="5.0.0" />
<PackageReference Include="Quartz" Version="3.3.3" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.3.3" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.3.3" />
<PackageReference Include="Remora.Discord" Version="3.0.52" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Filters.Expressions" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CentCom.Common\CentCom.Common.csproj" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.Extensions.Hosting, Version=5.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
<HintPath>..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\5.0.7\Microsoft.Extensions.Hosting.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
13 changes: 13 additions & 0 deletions CentCom.Bot/Configuration/DiscordConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Remora.Discord.Core;

namespace CentCom.Bot.Configuration
{
public class DiscordConfiguration
{
public string Token { get; set; }

public ulong? FailureChannel { get; set; }

public ulong? FailureMention { get; set; }
}
}
83 changes: 83 additions & 0 deletions CentCom.Bot/Jobs/FailedParseJob.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CentCom.Bot.Configuration;
using CentCom.Common.Data;
using CentCom.Common.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Quartz;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.Core;

namespace CentCom.Bot.Jobs
{
[DisallowConcurrentExecution]
public class FailedParseJob : IJob
{
private readonly DatabaseContext _dbContext;
private readonly IDiscordRestChannelAPI _channelAPI;
private readonly IOptions<DiscordConfiguration> _config;

public FailedParseJob(DatabaseContext dbContext, IOptions<DiscordConfiguration> config,
IDiscordRestChannelAPI channelAPI)
{
_dbContext = dbContext;
_channelAPI = channelAPI;
_config = config;
}

public async Task Execute(IJobExecutionContext context)
{
var failures = await _dbContext.CheckHistory
.Include(x => x.Notification)
.Where(x => !x.Success && x.Notification == null)
.ToListAsync();
if (failures.Count == 0)
return;

if (_config.Value == null)
throw new Exception("Missing or invalid Discord configuration, cannot dispatch failure notifications");

// Don't bother if we haven't configured a channel to use
if (_config.Value.FailureChannel == null)
return;

// Get channel, check it exists
var channelRequest = await _channelAPI.GetChannelAsync(new Snowflake(_config.Value.FailureChannel.Value));
if (!channelRequest.IsSuccess || channelRequest.Entity == null)
throw new Exception("Failed to get Discord channel to dispatch parse failure notifications into.");

var notified = new List<NotifiedFailure>();
var channel = channelRequest.Entity;
foreach (var failure in failures)
{
var message = new StringBuilder();
if (_config.Value.FailureMention.HasValue)
message.Append($"<@{_config.Value.FailureMention}> ");
message.Append(
$"Failed to parse bans for {failure.Parser} at <t:{failure.Failed.Value.ToUnixTimeSeconds()}>, exception is as follows... ```");

// Ensure that our length fits
var currLength = message.Length + failure.Exception.Length + 3;
message.Append(currLength > 2000
? $"{failure.Exception[0..^(currLength - 2000 + 4)]}...```"
: $"{failure.Exception}```");

// Try to send, only mark completed if successful
var result = await _channelAPI.CreateMessageAsync(channel.ID, message.ToString());
if (result.IsSuccess)
notified.Add(new NotifiedFailure()
{
CheckHistory = failure,
Timestamp = DateTimeOffset.UtcNow
});
}

_dbContext.NotifiedFailures.AddRange(notified);
await _dbContext.SaveChangesAsync();
}
}
}
116 changes: 116 additions & 0 deletions CentCom.Bot/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Threading.Tasks;
using CentCom.Bot.Configuration;
using CentCom.Bot.Jobs;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Hosting.Extensions;
using CentCom.Common.Configuration;
using CentCom.Common.Data;
using Quartz;
using Serilog;
using Serilog.Filters;

namespace CentCom.Bot
{
public class Program
{
public static Task Main(string[] args)
{
// Setup Serilog
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.WriteTo.Logger(lc =>
{
lc.Filter.ByExcluding(
"Contains(SourceContext, 'Quartz') and (@Level = 'Information')");
lc.WriteTo.Console(
outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}");
})
.WriteTo.Logger(lc =>
{
lc.WriteTo.File(path: "centcom-discord-bot.txt",
outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] ({SourceContext}) {Message:lj}{NewLine}{Exception}");
})
.CreateLogger();

return CreateHostBuilder(args).RunConsoleAsync();
}

private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args)
.AddDiscordService(services =>
{
var configuration = services.GetRequiredService<IConfiguration>();
return configuration.GetValue<string>("discord:token") ??
throw new InvalidOperationException
(
"Failed to read Discord configuration, bot token not found in appsettings.json."
);
})
.ConfigureServices((_, services) =>
{
// Add configuration
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.AddCommandLine(args)
.AddUserSecrets<Program>()
.Build();
services.AddSingleton<IConfiguration>(config);

// Add Discord config
services.AddOptions<DiscordConfiguration>()
.Bind(config.GetSection("discord"))
.Validate(x => x.Token != null);

// Get DB configuration
var dbConfig = new DbConfig();
config.Bind("dbConfig", dbConfig);

// Add appropriate DB context
if (dbConfig == null)
{
throw new Exception(
"Failed to read DB configuration, please ensure you provide one in appsettings.json");
}

switch (dbConfig.DbType)
{
case DbType.Postgres:
services.AddDbContext<DatabaseContext, NpgsqlDbContext>();
break;
case DbType.MariaDB:
services.AddDbContext<DatabaseContext, MariaDbContext>();
break;
case DbType.MySql:
services.AddDbContext<DatabaseContext, MySqlDbContext>();
break;
default:
throw new ArgumentOutOfRangeException();
}

// Add Quartz
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();

q.ScheduleJob<FailedParseJob>(trigger =>
trigger
.StartNow()
.WithSimpleSchedule(x => x.WithIntervalInSeconds(10).RepeatForever()),
job => job.WithIdentity("failed-parse"));
});
services.AddQuartzHostedService();

// Add Quartz jobs
services.AddTransient<FailedParseJob>();

// Add Discord commands
services.AddDiscordCommands();
})
.UseSerilog();
}
}
18 changes: 18 additions & 0 deletions CentCom.Bot/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"dbConfig": {
"connectionString": "connection_string_goes_here",
"dbType": "db_type_goes_here"
},
"discord": {
"token": null,
"failureChannel": 0,
"failureMention": 0
}
}
2 changes: 1 addition & 1 deletion CentCom.Common/CentCom.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Version>1.3.9</Version>
<Version>1.3.10</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
29 changes: 20 additions & 9 deletions CentCom.Common/Data/DatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public abstract class DatabaseContext : DbContext
public DbSet<JobBan> JobBans { get; set; }
public DbSet<FlatBansVersion> FlatBansVersion { get; set; }
public DbSet<CheckHistory> CheckHistory { get; set; }
public DbSet<NotifiedFailure> NotifiedFailures { get; set; }

public DatabaseContext(IConfiguration configuration)
{
Expand All @@ -32,8 +33,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.Id).UseIdentityAlwaysColumn();
entity.Property(e => e.CKey).IsRequired().HasMaxLength(32);
entity.Property(e => e.Source).IsRequired();
entity.Property(e => e.BannedOn).IsRequired().HasConversion(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
entity.Property(e => e.Expires).HasConversion(v => v, v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : (DateTime?)null);
entity.Property(e => e.BannedOn).IsRequired()
.HasConversion(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
entity.Property(e => e.Expires).HasConversion(v => v,
v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : (DateTime?)null);
entity.Property(e => e.BannedBy).IsRequired().HasMaxLength(32);
entity.Property(e => e.UnbannedBy).HasMaxLength(32);
entity.Property(e => e.BanType).IsRequired();
Expand All @@ -54,17 +57,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity<JobBan>(entity =>
{
entity.HasKey(e => new { e.BanId, e.Job });
});
modelBuilder.Entity<JobBan>(entity => { entity.HasKey(e => new { e.BanId, e.Job }); });

modelBuilder.Entity<FlatBansVersion>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).UseIdentityAlwaysColumn();
entity.Property(e => e.Name).IsRequired();
entity.Property(e => e.PerformedAt).IsRequired().HasConversion(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
entity.Property(e => e.PerformedAt).IsRequired()
.HasConversion(v => v, v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
entity.Property(e => e.Version).IsRequired();
entity.HasIndex(e => new { e.Name, e.Version }).IsUnique();
});
Expand All @@ -74,7 +75,16 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).UseIdentityAlwaysColumn();
entity.Property(e => e.Parser).IsRequired();
entity.HasIndex(e => new {e.Parser, e.Started});
entity.HasIndex(e => new { e.Parser, e.Started });
});

modelBuilder.Entity<NotifiedFailure>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).UseIdentityAlwaysColumn();
entity.HasOne(e => e.CheckHistory)
.WithOne(e => e.Notification)
.HasForeignKey<NotifiedFailure>(e => e.CheckHistoryId);
});
}

Expand All @@ -86,7 +96,8 @@ public async Task<bool> Migrate(CancellationToken cancellationToken)
{
await Database.MigrateAsync(cancellationToken);
}

return wasEmpty;
}
}
}
}
Loading

0 comments on commit 7e5bdb0

Please sign in to comment.