Split Geekbot.net into src/Bot, src/Core, and src/Web

This commit is contained in:
runebaas 2020-08-08 22:24:01 +02:00
parent 7b6dd2d2f9
commit fc0af492ad
No known key found for this signature in database
GPG key ID: 2677AF508D0300D6
197 changed files with 542 additions and 498 deletions

View file

@ -0,0 +1,16 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
namespace Geekbot.Core.CommandPreconditions
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class DisableInDirectMessageAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
var result = context.Guild.Id != 0 ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Command unavailable in Direct Messaging");
return Task.FromResult(result);
}
}
}

21
src/Core/Constants.cs Normal file
View file

@ -0,0 +1,21 @@
using System.Reflection;
namespace Geekbot.Core
{
public static class Constants
{
public const string Name = "Geekbot";
public static string BotVersion()
{
return typeof(Constants).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
}
public static string LibraryVersion()
{
return typeof(Discord.WebSocket.DiscordSocketClient).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion;
}
public const double ApiVersion = 1;
}
}

View file

@ -0,0 +1,93 @@
using System.Collections;
using System.Text;
namespace Geekbot.Core.Converters
{
public class EmojiConverter : IEmojiConverter
{
public string NumberToEmoji(int number)
{
if (number == 10)
{
return "🔟";
}
var emojiMap = new[]
{
":zero:",
":one:",
":two:",
":three:",
":four:",
":five:",
":six:",
":seven:",
":eight:",
":nine:"
};
var numbers = number.ToString().ToCharArray();
var returnString = new StringBuilder();
foreach (var n in numbers)
{
returnString.Append(emojiMap[int.Parse(n.ToString())]);
}
return returnString.ToString();
}
public string TextToEmoji(string text)
{
var emojiMap = new Hashtable
{
['A'] = ":regional_indicator_a: ",
['B'] = ":b: ",
['C'] = ":regional_indicator_c: ",
['D'] = ":regional_indicator_d: ",
['E'] = ":regional_indicator_e: ",
['F'] = ":regional_indicator_f: ",
['G'] = ":regional_indicator_g: ",
['H'] = ":regional_indicator_h: ",
['I'] = ":regional_indicator_i: ",
['J'] = ":regional_indicator_j: ",
['K'] = ":regional_indicator_k: ",
['L'] = ":regional_indicator_l: ",
['M'] = ":regional_indicator_m: ",
['N'] = ":regional_indicator_n: ",
['O'] = ":regional_indicator_o: ",
['P'] = ":regional_indicator_p: ",
['Q'] = ":regional_indicator_q: ",
['R'] = ":regional_indicator_r: ",
['S'] = ":regional_indicator_s: ",
['T'] = ":regional_indicator_t: ",
['U'] = ":regional_indicator_u: ",
['V'] = ":regional_indicator_v: ",
['W'] = ":regional_indicator_w: ",
['X'] = ":regional_indicator_x: ",
['Y'] = ":regional_indicator_y: ",
['Z'] = ":regional_indicator_z: ",
['!'] = ":exclamation: ",
['?'] = ":question: ",
['#'] = ":hash: ",
['*'] = ":star2: ",
['+'] = ":heavy_plus_sign: ",
['0'] = ":zero: ",
['1'] = ":one: ",
['2'] = ":two: ",
['3'] = ":three: ",
['4'] = ":four: ",
['5'] = ":five: ",
['6'] = ":six: ",
['7'] = ":seven: ",
['8'] = ":eight: ",
['9'] = ":nine: ",
[' '] = ""
};
var letters = text.ToUpper().ToCharArray();
var returnString = new StringBuilder();
foreach (var n in letters)
{
var emoji = emojiMap[n] ?? n;
returnString.Append(emoji);
}
return returnString.ToString();
}
}
}

View file

@ -0,0 +1,8 @@
namespace Geekbot.Core.Converters
{
public interface IEmojiConverter
{
string NumberToEmoji(int number);
string TextToEmoji(string text);
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Core.Converters
{
public interface IMtgManaConverter
{
string ConvertMana(string mana);
}
}

View file

@ -0,0 +1,81 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Geekbot.Core.Converters
{
public class MtgManaConverter : IMtgManaConverter
{
private readonly Dictionary<string, string> _manaDict;
public MtgManaConverter()
{
// these emotes can be found at https://discord.gg/bz8HyA7
_manaDict = new Dictionary<string, string>
{
{"{0}", "<:mtg_0:415216130043412482>"},
{"{1}", "<:mtg_1:415216130253389835>"},
{"{2}", "<:mtg_2:415216130031091713>"},
{"{3}", "<:mtg_3:415216130467037194>"},
{"{4}", "<:mtg_4:415216130026635295>"},
{"{5}", "<:mtg_5:415216130492203008>"},
{"{6}", "<:mtg_6:415216130458779658>"},
{"{7}", "<:mtg_7:415216130190475265>"},
{"{8}", "<:mtg_8:415216130517630986>"},
{"{9}", "<:mtg_9:415216130500722689>"},
{"{10", "<:mtg_10:415216130450391051>"},
{"{11}", "<:mtg_11:415216130811101185>"},
{"{12}", "<:mtg_12:415216130525888532>"},
{"{13}", "<:mtg_13:415216130517631000>"},
{"{14}", "<:mtg_14:415216130165178370>"},
{"{15}", "<:mtg_15:415216130576089108>"},
{"{16}", "<:mtg_16:415216130358247425>"},
{"{17}", "<:mtg_17:415216130601517056>"},
{"{18}", "<:mtg_18:415216130462842891>"},
{"{19}", "<:mtg_19:415216130614099988>"},
{"{20}", "<:mtg_20:415216130656043038>"},
{"{W}", "<:mtg_white:415216131515744256>"},
{"{U}", "<:mtg_blue:415216130521694209>"},
{"{B}", "<:mtg_black:415216130873884683>"},
{"{R}", "<:mtg_red:415216131322806272>"},
{"{G}", "<:mtg_green:415216131180331009>"},
{"{S}", "<:mtg_s:415216131293446144>"},
{"{T}", "<:mtg_tap:415258392727257088>"},
{"{C}", "<:mtg_colorless:415216130706374666>"},
{"{2/W}", "<:mtg_2w:415216130446065664>"},
{"{2/U}", "<:mtg_2u:415216130429550592>"},
{"{2/B}", "<:mtg_2b:415216130160984065>"},
{"{2/R}", "<:mtg_2r:415216130454716436>"},
{"{2/G}", "<:mtg_2g:415216130420899840>"},
{"{W/U}", "<:mtg_wu:415216130970484736>"},
{"{W/B}", "<:mtg_wb:415216131222011914>"},
{"{U/R}", "<:mtg_ur:415216130962096128>"},
{"{U/B}", "<:mtg_ub:415216130865758218>"},
{"{R/W}", "<:mtg_rw:415216130878210057>"},
{"{G/W}", "<:mtg_gw:415216130567962646>"},
{"{G/U}", "<:mtg_gu:415216130739666945>"},
{"{B/R}", "<:mtg_br:415216130580283394>"},
{"{B/G}", "<:mtg_bg:415216130781609994>"},
{"{U/P}", "<:mtg_up:415216130861432842>"},
{"{R/P}", "<:mtg_rp:415216130597322783>"},
{"{G/P}", "<:mtg_gp:415216130760769546>"},
{"{W/P}", "<:mtg_wp:415216131541041172>"},
{"{B/P}", "<:mtg_bp:415216130664169482>"}
};
}
public string ConvertMana(string mana)
{
var rgx = Regex.Matches(mana, @"(\{(.*?)\})");
foreach (Match manaTypes in rgx)
{
var m = _manaDict.GetValueOrDefault(manaTypes.Value);
if (!string.IsNullOrEmpty(m))
{
mana = mana.Replace(manaTypes.Value, m);
}
}
return mana;
}
}
}

40
src/Core/Core.csproj Normal file
View file

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<VersionSuffix>$(VersionSuffix)</VersionSuffix>
<Version Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)</Version>
<Version Condition=" '$(VersionSuffix)' == '' ">0.0.0-DEV</Version>
<RootNamespace>Geekbot.Core</RootNamespace>
<AssemblyName>Geekbot.Core</AssemblyName>
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Discord.Net" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.0-preview.7.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0-preview.7.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="5.0.0-preview.7.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0-preview.7.*" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0-preview.7.*" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0-preview.7.*" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0-preview.7.*" />
<PackageReference Include="Microsoft.Extensions.Options" Version="5.0.0-preview.7.*" />
<PackageReference Include="MyAnimeListSharp" Version="1.3.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.2" />
<PackageReference Include="NLog.Config" Version="4.7.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="5.0.0-preview6" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="SumoLogic.Logging.NLog" Version="1.0.1.3" />
<PackageReference Include="YamlDotNet" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<Content Include="Localization\Translations.yml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View file

@ -0,0 +1,21 @@
using Geekbot.Core.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Geekbot.Core.Database
{
public class DatabaseContext : DbContext
{
public DbSet<QuoteModel> Quotes { get; set; }
public DbSet<UserModel> Users { get; set; }
public DbSet<GuildSettingsModel> GuildSettings { get; set; }
public DbSet<KarmaModel> Karma { get; set; }
public DbSet<ShipsModel> Ships { get; set; }
public DbSet<RollsModel> Rolls { get; set; }
public DbSet<MessagesModel> Messages { get; set; }
public DbSet<SlapsModel> Slaps { get; set; }
public DbSet<GlobalsModel> Globals { get; set; }
public DbSet<RoleSelfServiceModel> RoleSelfService { get; set; }
public DbSet<CookiesModel> Cookies { get; set; }
public DbSet<ReactionListenerModel> ReactionListeners { get; set; }
}
}

View file

@ -0,0 +1,58 @@
using System;
using Geekbot.Core.Database.LoggingAdapter;
using Geekbot.Core.Logger;
using Npgsql.Logging;
namespace Geekbot.Core.Database
{
public class DatabaseInitializer
{
private readonly RunParameters _runParameters;
private readonly GeekbotLogger _logger;
public DatabaseInitializer(RunParameters runParameters, GeekbotLogger logger)
{
_runParameters = runParameters;
_logger = logger;
NpgsqlLogManager.Provider = new NpgsqlLoggingProviderAdapter(logger, runParameters);
}
public DatabaseContext Initialize()
{
DatabaseContext database = null;
try
{
if (_runParameters.InMemory)
{
database = new InMemoryDatabase("geekbot");
}
else
{
database = new SqlDatabase(new SqlConnectionString
{
Host = _runParameters.DbHost,
Port = _runParameters.DbPort,
Database = _runParameters.DbDatabase,
Username = _runParameters.DbUser,
Password = _runParameters.DbPassword,
RequireSsl = _runParameters.DbSsl,
TrustServerCertificate = _runParameters.DbTrustCert,
RedshiftCompatibility = _runParameters.DbRedshiftCompatibility
});
}
}
catch (Exception e)
{
_logger.Error(LogSource.Geekbot, "Could not Connect to datbase", e);
Environment.Exit(GeekbotExitCode.DatabaseConnectionFailed.GetHashCode());
}
if (_runParameters.DbLogging)
{
_logger.Information(LogSource.Database, $"Connected with {database.Database.ProviderName}");
}
return database;
}
}
}

View file

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
namespace Geekbot.Core.Database
{
public class InMemoryDatabase : DatabaseContext
{
private readonly string _name;
public InMemoryDatabase(string name)
{
_name = name;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseInMemoryDatabase(_name);
}
}

View file

@ -0,0 +1,68 @@
using System;
using Geekbot.Core.Logger;
using Npgsql.Logging;
using LogLevel = NLog.LogLevel;
namespace Geekbot.Core.Database.LoggingAdapter
{
public class NpgsqlLoggingAdapter : NpgsqlLogger
{
private readonly string _name;
private readonly IGeekbotLogger _geekbotLogger;
private readonly RunParameters _runParameters;
public NpgsqlLoggingAdapter(string name, IGeekbotLogger geekbotLogger, RunParameters runParameters)
{
_name = name.Substring(7);
_geekbotLogger = geekbotLogger;
_runParameters = runParameters;
geekbotLogger.Trace(LogSource.Database, $"Loaded Npgsql logging adapter: {name}");
}
public override bool IsEnabled(NpgsqlLogLevel level)
{
return (_runParameters.DbLogging && _geekbotLogger.GetNLogger().IsEnabled(ToGeekbotLogLevel(level)));
}
public override void Log(NpgsqlLogLevel level, int connectorId, string msg, Exception exception = null)
{
var nameAndMessage = $"{_name}: {msg}";
switch (level)
{
case NpgsqlLogLevel.Trace:
_geekbotLogger.Trace(LogSource.Database, nameAndMessage);
break;
case NpgsqlLogLevel.Debug:
_geekbotLogger.Debug(LogSource.Database, nameAndMessage);
break;
case NpgsqlLogLevel.Info:
_geekbotLogger.Information(LogSource.Database, nameAndMessage);
break;
case NpgsqlLogLevel.Warn:
_geekbotLogger.Warning(LogSource.Database, nameAndMessage, exception);
break;
case NpgsqlLogLevel.Error:
case NpgsqlLogLevel.Fatal:
_geekbotLogger.Error(LogSource.Database, nameAndMessage, exception);
break;
default:
_geekbotLogger.Information(LogSource.Database, nameAndMessage);
break;
}
}
private static LogLevel ToGeekbotLogLevel(NpgsqlLogLevel level)
{
return level switch
{
NpgsqlLogLevel.Trace => LogLevel.Trace,
NpgsqlLogLevel.Debug => LogLevel.Debug,
NpgsqlLogLevel.Info => LogLevel.Info,
NpgsqlLogLevel.Warn => LogLevel.Warn,
NpgsqlLogLevel.Error => LogLevel.Error,
NpgsqlLogLevel.Fatal => LogLevel.Fatal,
_ => throw new ArgumentOutOfRangeException(nameof(level))
};
}
}
}

View file

@ -0,0 +1,22 @@
using Geekbot.Core.Logger;
using Npgsql.Logging;
namespace Geekbot.Core.Database.LoggingAdapter
{
public class NpgsqlLoggingProviderAdapter : INpgsqlLoggingProvider
{
private readonly GeekbotLogger _geekbotLogger;
private readonly RunParameters _runParameters;
public NpgsqlLoggingProviderAdapter(GeekbotLogger geekbotLogger, RunParameters runParameters)
{
_geekbotLogger = geekbotLogger;
_runParameters = runParameters;
}
public NpgsqlLogger CreateLogger(string name)
{
return new NpgsqlLoggingAdapter(name, _geekbotLogger, _runParameters);
}
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class CookiesModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
[Required]
public long UserId { get; set; }
public int Cookies { get; set; } = 0;
public DateTimeOffset? LastPayout { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class GlobalsModel
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Value { get; set; }
public string Meta { get; set; }
}
}

View file

@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class GuildSettingsModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
public bool Ping { get; set; } = false;
public bool Hui { get; set; } = false;
public long ModChannel { get; set; } = 0;
public string WelcomeMessage { get; set; }
public long WelcomeChannel { get; set; }
public bool ShowDelete { get; set; } = false;
public bool ShowLeave { get; set; } = false;
public string WikiLang { get; set; } = "en";
public string Language { get; set; } = "EN";
}
}

View file

@ -0,0 +1,21 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class KarmaModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
[Required]
public long UserId { get; set; }
public int Karma { get; set; }
public DateTimeOffset TimeOut { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class MessagesModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
[Required]
public long UserId { get; set; }
public int MessageCount { get; set; }
}
}

View file

@ -0,0 +1,28 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class QuoteModel
{
[Key]
public int Id { get; set; }
[Required]
public int InternalId { get; set; }
[Required]
public long GuildId { get; set; }
[Required]
public long UserId { get; set; }
[Required]
[DataType(DataType.DateTime)]
public DateTime Time { get; set; }
public string Quote { get; set; }
public string Image { get; set; }
}
}

View file

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class ReactionListenerModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
[Required]
public long MessageId { get; set; }
[Required]
public long RoleId { get; set; }
[Required]
public string Reaction { get; set; }
}
}

View file

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class RoleSelfServiceModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
public long RoleId { get; set; }
public string WhiteListName { get; set; }
}
}

View file

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class RollsModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
[Required]
public long UserId { get; set; }
public int Rolls { get; set; }
}
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class ShipsModel
{
[Key]
public int Id { get; set; }
public long FirstUserId { get; set; }
public long SecondUserId { get; set; }
public int Strength { get; set; }
}
}

View file

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class SlapsModel
{
[Key]
public int Id { get; set; }
[Required]
public long GuildId { get; set; }
[Required]
public long UserId { get; set; }
public int Given { get; set; }
public int Recieved { get; set; }
}
}

View file

@ -0,0 +1,27 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Geekbot.Core.Database.Models
{
public class UserModel
{
[Key]
public int Id { get; set; }
[Required]
public long UserId { get; set; }
[Required]
public string Username { get; set; }
[Required]
public string Discriminator { get; set; }
public string AvatarUrl { get; set; }
[Required]
public bool IsBot { get; set; }
public DateTimeOffset Joined { get; set; }
}
}

View file

@ -0,0 +1,39 @@
using System.Text;
namespace Geekbot.Core.Database
{
public class SqlConnectionString
{
public string Host { get; set; }
public string Port { get; set; }
public string Database { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public bool RequireSsl { get; set; }
public bool TrustServerCertificate { get; set; }
public bool RedshiftCompatibility { get; set; }
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("Application Name=Geekbot;");
sb.Append($"Host={Host};");
sb.Append($"Port={Port};");
sb.Append($"Database={Database};");
sb.Append($"Username={Username};");
sb.Append($"Password={Password};");
var sslMode = RequireSsl ? "Require" : "Prefer";
sb.Append($"SSL Mode={sslMode};");
sb.Append($"Trust Server Certificate={TrustServerCertificate.ToString()};");
if (RedshiftCompatibility)
{
sb.Append("Server Compatibility Mode=Redshift");
}
return sb.ToString();
}
}
}

View file

@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
namespace Geekbot.Core.Database
{
public class SqlDatabase : DatabaseContext
{
private readonly SqlConnectionString _connectionString;
public SqlDatabase(SqlConnectionString connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseNpgsql(_connectionString.ToString());
}
}

View file

@ -0,0 +1,13 @@
using System;
namespace Geekbot.Core.DiceParser
{
public class DiceException : Exception
{
public DiceException(string message) : base(message)
{
}
public string DiceName { get; set; }
}
}

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Geekbot.Core.DiceParser
{
public class DiceInput
{
public List<SingleDie> Dice { get; set; } = new List<SingleDie>();
public DiceInputOptions Options { get; set; } = new DiceInputOptions();
public int SkillModifier { get; set; }
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Core.DiceParser
{
public struct DiceInputOptions
{
public bool ShowTotal { get; set; }
}
}

View file

@ -0,0 +1,102 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Geekbot.Core.RandomNumberGenerator;
namespace Geekbot.Core.DiceParser
{
public class DiceParser : IDiceParser
{
private readonly IRandomNumberGenerator _randomNumberGenerator;
private readonly Regex _inputRegex;
private readonly Regex _singleDieRegex;
public DiceParser(IRandomNumberGenerator randomNumberGenerator)
{
_randomNumberGenerator = randomNumberGenerator;
_inputRegex = new Regex(
@"((?<DieAdvantage>\+\d+d\d+)|(?<DieDisadvantage>\-\d+d\d+)|(?<DieNormal>\d+d\d+)|(?<Keywords>(total))|(?<SkillModifer>(\+|\-)\d+))\s",
RegexOptions.Compiled | RegexOptions.IgnoreCase,
new TimeSpan(0, 0, 2));
_singleDieRegex = new Regex(
@"\d+d\d+",
RegexOptions.Compiled | RegexOptions.IgnoreCase,
new TimeSpan(0, 0, 0, 0, 500));
}
public DiceInput Parse(string input)
{
// adding a whitespace at the end, otherwise the parser might pickup on false items
var inputWithExtraWhitespace = $"{input} ";
var matches = _inputRegex.Matches(inputWithExtraWhitespace);
var result = new DiceInput();
var resultOptions = new DiceInputOptions();
foreach (Match match in matches)
{
foreach (Group matchGroup in match.Groups)
{
if (matchGroup.Success)
{
switch (matchGroup.Name)
{
case "DieNormal":
result.Dice.Add(Die(matchGroup.Value, DieAdvantageType.None));
break;
case "DieAdvantage":
result.Dice.Add(Die(matchGroup.Value, DieAdvantageType.Advantage));
break;
case "DieDisadvantage":
result.Dice.Add(Die(matchGroup.Value, DieAdvantageType.Disadvantage));
break;
case "Keywords":
Keywords(matchGroup.Value, ref resultOptions);
break;
case "SkillModifer":
result.SkillModifier = SkillModifer(matchGroup.Value);
break;
}
}
}
}
if (!result.Dice.Any())
{
result.Dice.Add(new SingleDie(_randomNumberGenerator));
}
result.Options = resultOptions;
return result;
}
private SingleDie Die(string match, DieAdvantageType advantageType)
{
var x = _singleDieRegex.Match(match).Value.Split('d');
var die = new SingleDie(_randomNumberGenerator)
{
Amount = int.Parse(x[0]),
Sides = int.Parse(x[1]),
AdvantageType = advantageType
};
die.ValidateDie();
return die;
}
private int SkillModifer(string match)
{
return int.Parse(match);
}
private void Keywords(string match, ref DiceInputOptions options)
{
switch (match)
{
case "total":
options.ShowTotal = true;
break;
}
}
}
}

View file

@ -0,0 +1,9 @@
namespace Geekbot.Core.DiceParser
{
public enum DieAdvantageType
{
Advantage,
Disadvantage,
None
}
}

View file

@ -0,0 +1,30 @@
using System;
namespace Geekbot.Core.DiceParser
{
public class DieResult
{
// public int Result { get; set; }
public int Roll1 { get; set; }
public int Roll2 { get; set; }
public DieAdvantageType AdvantageType { get; set; }
public override string ToString()
{
return AdvantageType switch
{
DieAdvantageType.Advantage => Roll1 > Roll2 ? $"(**{Roll1}**, {Roll2})" : $"({Roll1}, **{Roll2}**)",
DieAdvantageType.Disadvantage => Roll1 < Roll2 ? $"(**{Roll1}**, {Roll2})" : $"({Roll1}, **{Roll2}**)",
_ => Result.ToString()
};
}
public int Result => AdvantageType switch
{
DieAdvantageType.None => Roll1,
DieAdvantageType.Advantage => Math.Max(Roll1, Roll2),
DieAdvantageType.Disadvantage => Math.Min(Roll1, Roll2),
_ => 0
};
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Core.DiceParser
{
public interface IDiceParser
{
DiceInput Parse(string input);
}
}

View file

@ -0,0 +1,72 @@
using System.Collections.Generic;
using Geekbot.Core.Extensions;
using Geekbot.Core.RandomNumberGenerator;
namespace Geekbot.Core.DiceParser
{
public class SingleDie
{
private readonly IRandomNumberGenerator _random;
public SingleDie(IRandomNumberGenerator random)
{
_random = random;
}
public int Sides { get; set; } = 20;
public int Amount { get; set; } = 1;
public DieAdvantageType AdvantageType { get; set; } = DieAdvantageType.None;
public string DiceName => AdvantageType switch
{
DieAdvantageType.Advantage => $"{Amount}d{Sides} (with advantage)",
DieAdvantageType.Disadvantage => $"{Amount}d{Sides} (with disadvantage)",
_ => $"{Amount}d{Sides}"
};
public List<DieResult> Roll()
{
var results = new List<DieResult>();
Amount.Times(() =>
{
var result = new DieResult
{
Roll1 = _random.Next(1, Sides + 1),
AdvantageType = AdvantageType
};
if (AdvantageType == DieAdvantageType.Advantage || AdvantageType == DieAdvantageType.Disadvantage)
{
result.Roll2 = _random.Next(1, Sides);
}
results.Add(result);
});
return results;
}
public void ValidateDie()
{
if (Amount < 1)
{
throw new DiceException("To few dice, must be a minimum of 1");
}
if (Amount > 24)
{
throw new DiceException("To many dice, maximum allowed is 24") { DiceName = DiceName };
}
if (Sides < 2)
{
throw new DiceException("Die must have at least 2 sides") { DiceName = DiceName };
}
if (Sides > 144)
{
throw new DiceException("Die can not have more than 144 sides") { DiceName = DiceName };
}
}
}
}

View file

@ -0,0 +1,106 @@
using System;
using System.Net;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.Net;
using Geekbot.Core.Localization;
using Geekbot.Core.Logger;
using SharpRaven;
using SharpRaven.Data;
using Exception = System.Exception;
namespace Geekbot.Core.ErrorHandling
{
public class ErrorHandler : IErrorHandler
{
private readonly IGeekbotLogger _logger;
private readonly ITranslationHandler _translation;
private readonly IRavenClient _raven;
private readonly bool _errorsInChat;
public ErrorHandler(IGeekbotLogger logger, ITranslationHandler translation, RunParameters runParameters)
{
_logger = logger;
_translation = translation;
_errorsInChat = runParameters.ExposeErrors;
var sentryDsn = runParameters.SentryEndpoint;
if (!string.IsNullOrEmpty(sentryDsn))
{
_raven = new RavenClient(sentryDsn) { Release = Constants.BotVersion(), Environment = "Production" };
_logger.Information(LogSource.Geekbot, $"Command Errors will be logged to Sentry: {sentryDsn}");
}
else
{
_raven = null;
}
}
public async Task HandleCommandException(Exception e, ICommandContext context, string errorMessage = "def")
{
try
{
var errorString = errorMessage == "def" ? await _translation.GetString(context.Guild?.Id ?? 0, "errorHandler", "SomethingWentWrong") : errorMessage;
var errorObj = SimpleConextConverter.ConvertContext(context);
if (e.Message.Contains("50007")) return;
if (e.Message.Contains("50013")) return;
_logger.Error(LogSource.Geekbot, "An error ocured", e, errorObj);
if (!string.IsNullOrEmpty(errorMessage))
{
if (_errorsInChat)
{
var resStackTrace = string.IsNullOrEmpty(e.InnerException?.ToString()) ? e.StackTrace : e.InnerException?.ToString();
if (!string.IsNullOrEmpty(resStackTrace))
{
var maxLen = Math.Min(resStackTrace.Length, 1850);
await context.Channel.SendMessageAsync($"{e.Message}\r\n```\r\n{resStackTrace.Substring(0, maxLen)}\r\n```");
}
else
{
await context.Channel.SendMessageAsync(e.Message);
}
}
else
{
await context.Channel.SendMessageAsync(errorString);
}
}
ReportExternal(e, errorObj);
}
catch (Exception ex)
{
await context.Channel.SendMessageAsync("Something went really really wrong here");
_logger.Error(LogSource.Geekbot, "Errorception", ex);
}
}
public async Task HandleHttpException(HttpException e, ICommandContext context)
{
var errorStrings = await _translation.GetDict(context, "httpErrors");
switch(e.HttpCode)
{
case HttpStatusCode.Forbidden:
await context.Channel.SendMessageAsync(errorStrings["403"]);
break;
}
}
private void ReportExternal(Exception e, MessageDto errorObj)
{
if (_raven == null) return;
var sentryEvent = new SentryEvent(e)
{
Tags =
{
["discord_server"] = errorObj.Guild.Name,
["discord_user"] = errorObj.User.Name
},
Message = errorObj.Message.Content,
Extra = errorObj
};
_raven.Capture(sentryEvent);
}
}
}

View file

@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;
using Discord.Commands;
using Discord.Net;
namespace Geekbot.Core.ErrorHandling
{
public interface IErrorHandler
{
Task HandleCommandException(Exception e, ICommandContext context, string errorMessage = "def");
Task HandleHttpException(HttpException e, ICommandContext context);
}
}

View file

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace Geekbot.Core.Extensions
{
public static class DbSetExtensions
{
public static EntityEntry<T> AddIfNotExists<T>(this DbSet<T> dbSet, T entity, Expression<Func<T, bool>> predicate = null) where T : class, new()
{
var exists = predicate != null ? dbSet.Any(predicate) : dbSet.Any();
return !exists ? dbSet.Add(entity) : null;
}
// https://github.com/dotnet/efcore/issues/18124
public static IAsyncEnumerable<TEntity> AsAsyncEnumerable<TEntity>(this Microsoft.EntityFrameworkCore.DbSet<TEntity> obj) where TEntity : class
{
return Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.AsAsyncEnumerable(obj);
}
public static IQueryable<TEntity> Where<TEntity>(this Microsoft.EntityFrameworkCore.DbSet<TEntity> obj, System.Linq.Expressions.Expression<Func<TEntity, bool>> predicate) where TEntity : class
{
return System.Linq.Queryable.Where(obj, predicate);
}
}
}

View file

@ -0,0 +1,12 @@
using Discord;
namespace Geekbot.Core.Extensions
{
public static class EmbedBuilderExtensions
{
public static EmbedBuilder AddInlineField(this EmbedBuilder builder, string name, object value)
{
return builder.AddField(new EmbedFieldBuilder().WithIsInline(true).WithName(name).WithValue(value));
}
}
}

View file

@ -0,0 +1,15 @@
using System;
namespace Geekbot.Core.Extensions
{
public static class IntExtensions
{
public static void Times(this int count, Action action)
{
for (var i = 0; i < count; i++)
{
action();
}
}
}
}

View file

@ -0,0 +1,12 @@
using System;
namespace Geekbot.Core.Extensions
{
public static class LongExtensions
{
public static ulong AsUlong(this long thing)
{
return Convert.ToUInt64(thing);
}
}
}

View file

@ -0,0 +1,12 @@
using System.Linq;
namespace Geekbot.Core.Extensions
{
public static class StringExtensions
{
public static string CapitalizeFirst(this string source)
{
return source.First().ToString().ToUpper() + source.Substring(1);
}
}
}

View file

@ -0,0 +1,12 @@
using System;
namespace Geekbot.Core.Extensions
{
public static class UlongExtensions
{
public static long AsLong(this ulong thing)
{
return Convert.ToInt64(thing);
}
}
}

View file

@ -0,0 +1,20 @@
namespace Geekbot.Core
{
public enum GeekbotExitCode
{
// General
Clean = 0,
InvalidArguments = 1,
// Geekbot Internals
TranslationsFailed = 201,
// Dependent Services
/* 301 not in use anymore (redis) */
DatabaseConnectionFailed = 302,
// Discord Related
CouldNotLogin = 401
}
}

View file

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Geekbot.Core.Database;
using Geekbot.Core.Database.Models;
namespace Geekbot.Core.GlobalSettings
{
public class GlobalSettings : IGlobalSettings
{
private readonly DatabaseContext _database;
private readonly Dictionary<string, string> _cache;
public GlobalSettings(DatabaseContext database)
{
_database = database;
_cache = new Dictionary<string, string>();
}
public async Task<bool> SetKey(string keyName, string value)
{
try
{
var key = GetKeyFull(keyName);
if (key == null)
{
_database.Globals.Add(new GlobalsModel()
{
Name = keyName,
Value = value
});
await _database.SaveChangesAsync();
return true;
}
key.Value = value;
_database.Globals.Update(key);
_cache[keyName] = value;
await _database.SaveChangesAsync();
return true;
}
catch
{
return false;
}
}
public string GetKey(string keyName)
{
var keyValue = "";
if (string.IsNullOrEmpty(_cache.GetValueOrDefault(keyName)))
{
keyValue = _database.Globals.FirstOrDefault(k => k.Name.Equals(keyName))?.Value ?? string.Empty;
_cache[keyName] = keyValue;
}
else
{
keyValue = _cache[keyName];
}
return keyValue ;
}
public GlobalsModel GetKeyFull(string keyName)
{
var key = _database.Globals.FirstOrDefault(k => k.Name.Equals(keyName));
return key;
}
}
}

View file

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Geekbot.Core.Database.Models;
namespace Geekbot.Core.GlobalSettings
{
public interface IGlobalSettings
{
Task<bool> SetKey(string keyName, string value);
string GetKey(string keyName);
GlobalsModel GetKeyFull(string keyName);
}
}

View file

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Geekbot.Core.Database;
using Geekbot.Core.Database.Models;
using Geekbot.Core.Extensions;
namespace Geekbot.Core.GuildSettingsManager
{
public class GuildSettingsManager : IGuildSettingsManager
{
private readonly DatabaseContext _database;
private readonly Dictionary<ulong, GuildSettingsModel> _settings;
public GuildSettingsManager(DatabaseContext database)
{
_database = database;
_settings = new Dictionary<ulong, GuildSettingsModel>();
}
public GuildSettingsModel GetSettings(ulong guildId, bool createIfNonExist = true)
{
return _settings.ContainsKey(guildId) ? _settings[guildId] : GetFromDatabase(guildId, createIfNonExist);
}
public async Task UpdateSettings(GuildSettingsModel settings)
{
_database.GuildSettings.Update(settings);
if (_settings.ContainsKey(settings.GuildId.AsUlong()))
{
_settings[settings.GuildId.AsUlong()] = settings;
}
else
{
_settings.Add(settings.GuildId.AsUlong(), settings);
}
await _database.SaveChangesAsync();
}
private GuildSettingsModel GetFromDatabase(ulong guildId, bool createIfNonExist)
{
var settings = _database.GuildSettings.FirstOrDefault(guild => guild.GuildId.Equals(guildId.AsLong()));
if (createIfNonExist && settings == null)
{
settings = CreateSettings(guildId);
}
_settings.Add(guildId, settings);
return settings;
}
private GuildSettingsModel CreateSettings(ulong guildId)
{
_database.GuildSettings.Add(new GuildSettingsModel
{
GuildId = guildId.AsLong(),
Hui = false,
Ping = false,
Language = "EN",
ShowDelete = false,
ShowLeave = false,
WikiLang = "en"
});
_database.SaveChanges();
return _database.GuildSettings.FirstOrDefault(g => g.GuildId.Equals(guildId.AsLong()));
}
}
}

View file

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Geekbot.Core.Database.Models;
namespace Geekbot.Core.GuildSettingsManager
{
public interface IGuildSettingsManager
{
GuildSettingsModel GetSettings(ulong guildId, bool createIfNonExist = true);
Task UpdateSettings(GuildSettingsModel settings);
}
}

View file

@ -0,0 +1,13 @@
using System;
namespace Geekbot.Core.Highscores
{
public class HighscoreListEmptyException : Exception
{
public HighscoreListEmptyException() {}
public HighscoreListEmptyException(string message) : base(message) {}
public HighscoreListEmptyException(string message, Exception inner) : base(message, inner) {}
}
}

View file

@ -0,0 +1,105 @@
using System.Collections.Generic;
using System.Linq;
using Geekbot.Core.Database;
using Geekbot.Core.Extensions;
using Geekbot.Core.UserRepository;
namespace Geekbot.Core.Highscores
{
public class HighscoreManager : IHighscoreManager
{
private readonly DatabaseContext _database;
private readonly IUserRepository _userRepository;
public HighscoreManager(DatabaseContext databaseContext, IUserRepository userRepository)
{
_database = databaseContext;
_userRepository = userRepository;
}
public Dictionary<HighscoreUserDto, int> GetHighscoresWithUserData(HighscoreTypes type, ulong guildId, int amount)
{
var list = type switch
{
HighscoreTypes.messages => GetMessageList(guildId, amount),
HighscoreTypes.karma => GetKarmaList(guildId, amount),
HighscoreTypes.rolls => GetRollsList(guildId, amount),
HighscoreTypes.cookies => GetCookiesList(guildId, amount),
_ => new Dictionary<ulong, int>()
};
if (!list.Any())
{
throw new HighscoreListEmptyException($"No {type} found for guild {guildId}");
}
var highscoreUsers = new Dictionary<HighscoreUserDto, int>();
foreach (var user in list)
{
try
{
var guildUser = _userRepository.Get(user.Key);
if (guildUser?.Username != null)
{
highscoreUsers.Add(new HighscoreUserDto
{
Username = guildUser.Username,
Discriminator = guildUser.Discriminator,
Avatar = guildUser.AvatarUrl
}, user.Value);
}
else
{
highscoreUsers.Add(new HighscoreUserDto
{
Id = user.Key.ToString()
}, user.Value);
}
}
catch
{
// ignore
}
}
return highscoreUsers;
}
public Dictionary<ulong, int> GetMessageList(ulong guildId, int amount)
{
return _database.Messages
.Where(k => k.GuildId.Equals(guildId.AsLong()))
.OrderByDescending(o => o.MessageCount)
.Take(amount)
.ToDictionary(key => key.UserId.AsUlong(), key => key.MessageCount);
}
public Dictionary<ulong, int> GetKarmaList(ulong guildId, int amount)
{
return _database.Karma
.Where(k => k.GuildId.Equals(guildId.AsLong()))
.OrderByDescending(o => o.Karma)
.Take(amount)
.ToDictionary(key => key.UserId.AsUlong(), key => key.Karma);
}
public Dictionary<ulong, int> GetRollsList(ulong guildId, int amount)
{
return _database.Rolls
.Where(k => k.GuildId.Equals(guildId.AsLong()))
.OrderByDescending(o => o.Rolls)
.Take(amount)
.ToDictionary(key => key.UserId.AsUlong(), key => key.Rolls);
}
private Dictionary<ulong, int> GetCookiesList(ulong guildId, int amount)
{
return _database.Cookies
.Where(k => k.GuildId.Equals(guildId.AsLong()))
.OrderByDescending(o => o.Cookies)
.Take(amount)
.ToDictionary(key => key.UserId.AsUlong(), key => key.Cookies);
}
}
}

View file

@ -0,0 +1,10 @@
namespace Geekbot.Core.Highscores
{
public enum HighscoreTypes
{
messages,
karma,
rolls,
cookies
}
}

View file

@ -0,0 +1,10 @@
namespace Geekbot.Core.Highscores
{
public class HighscoreUserDto
{
public string Username { get; set; }
public string Avatar { get; set; }
public string Discriminator { get; set; }
public string Id { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Geekbot.Core.Highscores
{
public interface IHighscoreManager
{
Dictionary<HighscoreUserDto, int> GetHighscoresWithUserData(HighscoreTypes type, ulong guildId, int amount);
Dictionary<ulong, int> GetMessageList(ulong guildId, int amount);
Dictionary<ulong, int> GetKarmaList(ulong guildId, int amount);
Dictionary<ulong, int> GetRollsList(ulong guildId, int amount);
}
}

View file

@ -0,0 +1,42 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Geekbot.Core
{
public static class HttpAbstractions
{
public static HttpClient CreateDefaultClient()
{
var client = new HttpClient
{
DefaultRequestHeaders =
{
Accept = {MediaTypeWithQualityHeaderValue.Parse("application/json")},
}
};
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Geekbot/v0.0.0 (+https://geekbot.pizzaandcoffee.rocks/)");
return client;
}
public static async Task<T> Get<T>(Uri location, HttpClient httpClient = null, bool disposeClient = true)
{
httpClient ??= CreateDefaultClient();
httpClient.BaseAddress = location;
var response = await httpClient.GetAsync(location.PathAndQuery);
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
if (disposeClient)
{
httpClient.Dispose();
}
return JsonConvert.DeserializeObject<T>(stringResponse);
}
}
}

View file

@ -0,0 +1,9 @@
namespace Geekbot.Core.KvInMemoryStore
{
public interface IKvInMemoryStore
{
public T Get<T>(string key);
public void Set<T>(string key, T value);
public void Remove(string key);
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace Geekbot.Core.KvInMemoryStore
{
public class KvInInMemoryStore : IKvInMemoryStore
{
private readonly Dictionary<string, object> _storage = new Dictionary<string, object>();
public T Get<T>(string key)
{
try
{
return (T) _storage[key];
}
catch
{
return default;
}
}
public void Set<T>(string key, T value)
{
_storage.Remove(key);
_storage.Add(key, value);
}
public void Remove(string key)
{
_storage.Remove(key);
}
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Core.Levels
{
public interface ILevelCalc
{
int GetLevel(int? experience);
}
}

View file

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Geekbot.Core.Levels
{
public class LevelCalc : ILevelCalc
{
private readonly int[] _levels;
public LevelCalc()
{
var levels = new List<int>();
double total = 0;
for (var i = 1; i < 120; i++)
{
total += Math.Floor(i + 300 * Math.Pow(2, i / 7.0));
levels.Add((int) Math.Floor(total / 16));
}
_levels = levels.ToArray();
}
public int GetLevel(int? messages)
{
return 1 + _levels.TakeWhile(level => !(level > messages)).Count();
}
}
}

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord.Commands;
namespace Geekbot.Core.Localization
{
public interface ITranslationHandler
{
Task<string> GetString(ulong guildId, string command, string stringName);
string GetString(string language, string command, string stringName);
Task<Dictionary<string, string>> GetDict(ICommandContext context, string command);
Task<TranslationGuildContext> GetGuildContext(ICommandContext context);
Task<bool> SetLanguage(ulong guildId, string language);
List<string> SupportedLanguages { get; }
}
}

View file

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace Geekbot.Core.Localization
{
public class TranslationGuildContext
{
public ITranslationHandler TranslationHandler { get; }
public string Language { get; }
public Dictionary<string, string> Dict { get; }
public TranslationGuildContext(ITranslationHandler translationHandler, string language, Dictionary<string, string> dict)
{
TranslationHandler = translationHandler;
Language = language;
Dict = dict;
}
public string GetString(string stringToFormat, params object[] args)
{
return string.Format(Dict[stringToFormat] ?? "", args);
}
public string FormatDateTimeAsRemaining(DateTimeOffset dateTime)
{
var remaining = dateTime - DateTimeOffset.Now;
const string formattable = "{0} {1}";
var sb = new StringBuilder();
if (remaining.Days > 0)
{
var s = GetTimeString(TimeTypes.Days);
sb.AppendFormat(formattable, remaining.Days, GetSingOrPlur(remaining.Days, s));
}
if (remaining.Hours > 0)
{
if (sb.Length > 0) sb.Append(", ");
var s = GetTimeString(TimeTypes.Hours);
sb.AppendFormat(formattable, remaining.Hours, GetSingOrPlur(remaining.Hours, s));
}
if (remaining.Minutes > 0)
{
if (sb.Length > 0) sb.Append(", ");
var s = GetTimeString(TimeTypes.Minutes);
sb.AppendFormat(formattable, remaining.Minutes, GetSingOrPlur(remaining.Minutes, s));
}
if (remaining.Seconds > 0)
{
if (sb.Length > 0)
{
var and = TranslationHandler.GetString(Language, "dateTime", "And");
sb.AppendFormat(" {0} ", and);
}
var s = GetTimeString(TimeTypes.Seconds);
sb.AppendFormat(formattable, remaining.Seconds, GetSingOrPlur(remaining.Seconds, s));
}
return sb.ToString().Trim();
}
public Task<bool> SetLanguage(ulong guildId, string language)
{
return TranslationHandler.SetLanguage(guildId, language);
}
private string GetTimeString(TimeTypes type)
{
return TranslationHandler.GetString(Language, "dateTime", type.ToString());
}
private string GetSingOrPlur(int number, string rawString)
{
var versions = rawString.Split('|');
return number == 1 ? versions[0] : versions[1];
}
private enum TimeTypes
{
Days,
Hours,
Minutes,
Seconds
}
}
}

View file

@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Discord.Commands;
using Geekbot.Core.GuildSettingsManager;
using Geekbot.Core.Logger;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
namespace Geekbot.Core.Localization
{
public class TranslationHandler : ITranslationHandler
{
private readonly IGeekbotLogger _logger;
private readonly IGuildSettingsManager _guildSettingsManager;
private readonly Dictionary<ulong, string> _serverLanguages;
private Dictionary<string, Dictionary<string, Dictionary<string, string>>> _translations;
public TranslationHandler(IGeekbotLogger logger, IGuildSettingsManager guildSettingsManager)
{
_logger = logger;
_guildSettingsManager = guildSettingsManager;
_logger.Information(LogSource.Geekbot, "Loading Translations");
LoadTranslations();
_serverLanguages = new Dictionary<ulong, string>();
}
private void LoadTranslations()
{
try
{
// Read the file
var translationFile = File.ReadAllText(Path.GetFullPath("./Lib/Localization/Translations.yml"));
// Deserialize
var input = new StringReader(translationFile);
var mergingParser = new MergingParser(new Parser(input));
var deserializer = new DeserializerBuilder().Build();
var rawTranslations = deserializer.Deserialize<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>(mergingParser);
// Sort
var sortedPerLanguage = new Dictionary<string, Dictionary<string, Dictionary<string, string>>>();
foreach (var command in rawTranslations)
{
foreach (var str in command.Value)
{
foreach (var lang in str.Value)
{
if (!sortedPerLanguage.ContainsKey(lang.Key))
{
var commandDict = new Dictionary<string, Dictionary<string, string>>();
var strDict = new Dictionary<string, string>
{
{str.Key, lang.Value}
};
commandDict.Add(command.Key, strDict);
sortedPerLanguage.Add(lang.Key, commandDict);
}
if (!sortedPerLanguage[lang.Key].ContainsKey(command.Key))
{
var strDict = new Dictionary<string, string>
{
{str.Key, lang.Value}
};
sortedPerLanguage[lang.Key].Add(command.Key, strDict);
}
if (!sortedPerLanguage[lang.Key][command.Key].ContainsKey(str.Key))
{
sortedPerLanguage[lang.Key][command.Key].Add(str.Key, lang.Value);
}
}
}
}
_translations = sortedPerLanguage;
// Find Languages
SupportedLanguages = new List<string>();
foreach (var lang in sortedPerLanguage)
{
SupportedLanguages.Add(lang.Key);
}
}
catch (Exception e)
{
_logger.Error(LogSource.Geekbot, "Failed to load Translations", e);
Environment.Exit(GeekbotExitCode.TranslationsFailed.GetHashCode());
}
}
private Task<string> GetServerLanguage(ulong guildId)
{
try
{
string lang;
try
{
lang = _serverLanguages[guildId];
if (!string.IsNullOrEmpty(lang))
{
return Task.FromResult(lang);
}
throw new Exception();
}
catch
{
lang = _guildSettingsManager.GetSettings(guildId, false)?.Language ?? "EN";
_serverLanguages[guildId] = lang;
return Task.FromResult(lang);
}
}
catch (Exception e)
{
_logger.Error(LogSource.Geekbot, "Could not get guild language", e);
return Task.FromResult("EN");
}
}
public async Task<string> GetString(ulong guildId, string command, string stringName)
{
var serverLang = await GetServerLanguage(guildId);
return GetString(serverLang, command, stringName);
}
public string GetString(string language, string command, string stringName)
{
var translation = _translations[language][command][stringName];
if (!string.IsNullOrWhiteSpace(translation)) return translation;
translation = _translations[command][stringName]["EN"];
if (string.IsNullOrWhiteSpace(translation))
{
_logger.Warning(LogSource.Geekbot, $"No translation found for {command} - {stringName}");
}
return translation;
}
private async Task<Dictionary<string, string>> GetDict(ICommandContext context)
{
try
{
var command = context.Message.Content.Split(' ').First().TrimStart('!').ToLower();
var serverLanguage = await GetServerLanguage(context.Guild?.Id ?? 0);
return _translations[serverLanguage][command];
}
catch (Exception e)
{
_logger.Error(LogSource.Geekbot, "No translations for command found", e);
return new Dictionary<string, string>();
}
}
public async Task<TranslationGuildContext> GetGuildContext(ICommandContext context)
{
var dict = await GetDict(context);
var language = await GetServerLanguage(context.Guild?.Id ?? 0);
return new TranslationGuildContext(this, language, dict);
}
public async Task<Dictionary<string, string>> GetDict(ICommandContext context, string command)
{
try
{
var serverLanguage = await GetServerLanguage(context.Guild?.Id ?? 0);
return _translations[serverLanguage][command];
}
catch (Exception e)
{
_logger.Error(LogSource.Geekbot, "No translations for command found", e);
return new Dictionary<string, string>();
}
}
public async Task<bool> SetLanguage(ulong guildId, string language)
{
try
{
if (!SupportedLanguages.Contains(language)) return false;
var guild = _guildSettingsManager.GetSettings(guildId);
guild.Language = language;
await _guildSettingsManager.UpdateSettings(guild);
_serverLanguages[guildId] = language;
return true;
}
catch (Exception e)
{
_logger.Error(LogSource.Geekbot, "Error while changing language", e);
return false;
}
}
public List<string> SupportedLanguages { get; private set; }
}
}

View file

@ -0,0 +1,207 @@
---
dateTime:
Days:
EN: "day|days"
CHDE: "tag|täg"
Hours:
EN: "hour|hours"
CHDE: "stund|stunde"
Minutes:
EN: "minute|minutes"
CHDE: "minute|minute"
Seconds:
EN: "second|seconds"
CHDE: "sekunde|sekunde"
And:
EN: "and"
CHDE: "und"
admin:
NewLanguageSet:
EN: "I will reply in english from now on"
CHDE: "I werd ab jetzt uf schwiizerdüütsch antworte, äuuä"
GetLanguage:
EN: "I'm talking english"
CHDE: "I red schwiizerdüütsch"
errorHandler:
SomethingWentWrong:
EN: "Something went wrong :confused:"
CHDE: "Öppis isch schief gange :confused:"
httpErrors:
403:
EN: "Seems like i don't have enough permission to that :confused:"
CHDE: "Gseht danach us das ich nid gnueg recht han zum das mache :confused:"
choose:
Choice:
EN: "I Choose **{0}**"
CHDE: "I nimme **{0}**"
good:
CannotChangeOwn:
EN: "Sorry {0}, but you can't give yourself karma"
CHDE: "Sorry {0}, aber du chasch dr selber kei karma geh"
WaitUntill:
EN: "Sorry {0}, but you have to wait {1} before you can give karma again..."
CHDE: "Sorry {0}, aber du musch no {1} warte bisch d wieder karma chasch geh..."
Increased:
EN: "Karma gained"
CHDE: "Karma becho"
By:
EN: "By"
CHDE: "Vo"
Amount:
EN: "Amount"
CHDE: "Mengi"
Current:
EN: "Current"
CHDE: "Jetzt"
bad:
CannotChangeOwn:
EN: "Sorry {0}, but you can't lower your own karma"
CHDE: "Sorry {0}, aber du chasch dr din eigete karma nid weg neh"
WaitUntill:
EN: "Sorry {0}, but you have to wait {1} before you can lower karma again..."
CHDE: "Sorry {0}, aber du musch no {1} warte bisch d wieder karma chasch senke..."
Decreased:
EN: "Karma lowered"
CHDE: "Karma gsenkt"
By:
EN: "By"
CHDE: "Vo"
Amount:
EN: "Amount"
CHDE: "Mengi"
Current:
EN: "Current"
CHDE: "Jetzt"
roll:
Rolled:
EN: "{0}, you rolled {1}, your guess was {2}"
CHDE: "{0}, du hesch {1} grollt und hesch {2} grate"
Gratz:
EN: "Congratulations {0}, your guess was correct!"
CHDE: "Gratuliere {0}, du hesch richtig grate!"
RolledNoGuess:
EN: "{0}, you rolled {1}"
CHDE: "{0}, du hesch {1} grollt"
NoPrevGuess:
EN: ":red_circle: {0}, you can't guess the same number again, guess another number or wait {1}"
CHDE: ":red_circle: {0}, du chasch nid nomol es gliche rate, rate öppis anders oder warte {1}"
cookies: &cookiesAlias
GetCookies:
EN: "You got {0} cookies, there are now {1} cookies in you cookie jar"
CHDE: "Du häsch {0} guetzli becho, du häsch jetzt {1} guetzli ih dr büchse"
WaitForMoreCookies:
EN: "You already got cookies today, you can have more cookies in {0}"
CHDE: "Du hesch scho guetzli becho hüt, du chasch meh ha in {0}"
InYourJar:
EN: "There are {0} cookies in you cookie jar"
CHDE: "Es hät {0} guetzli ih dineri büchs"
Given:
EN: "You gave {0} cookies to {1}"
CHDE: "Du hesch {1} {0} guetzli geh"
NotEnoughToGive:
EN: "You don't have enough cookies"
CHDE: "Du hesch nid gnueg guetzli"
NotEnoughCookiesToEat:
EN: "Your cookie jar looks almost empty, you should probably not eat a cookie"
CHDE: "Du hesch chuum no guetzli ih dineri büchs, du sötsch warschinli keini esse"
AteCookies:
EN: "You ate {0} cookies, you've only got {1} cookies left"
CHDE: "Du hesch {0} guetzli gesse und hesch jezt no {1} übrig"
cookie:
# because command aliases are to hard to deal with...
<<: *cookiesAlias
role:
NoRolesConfigured:
EN: "There are no roles configured for this server"
CHDE: "Es sind kei rolle für dä server konfiguriert"
ListHeader:
EN: "**Self Service Roles on {0}**"
CHDE: "**Self Service Rollene uf {0}**"
ListInstruction:
EN: "To get a role, use `!role [name]`"
CHDE: "Zum ä rolle becho, schriib `!role [name]`"
RoleNotFound:
EN: "That role doesn't exist or is not on the whitelist"
CHDE: "Die rolle gids nid or isch nid uf dr whitelist"
RemovedUserFromRole:
EN: "Removed you from {0}"
CHDE: "Han di entfernt vo {0}"
AddedUserFromRole:
EN: "Added you to {0}"
CHDE: "Han di hinzue gfüegt zu {0}"
CannotAddManagedRole:
EN: "You can't add a role that is managed by discord"
CHDE: "Du chasch kei rolle hinzuefüge wo verwalted wird vo discord"
CannotAddDangerousRole:
EN: "You cannot add that role to self service because it contains one or more dangerous permissions"
CHDE: "Du chasch die rolle nid hinzuefüge will er ein oder mehreri gföhrlichi berechtigunge het"
AddedRoleToWhitelist:
EN: "Added {0} to the whitelist"
CHDE: "{0} isch zur whitelist hinzuegfüegt"
RemovedRoleFromWhitelist:
EN: "Removed {0} from the whitelist"
CHDE: "{0} isch vo dr whitelist glöscht"
quote:
NoQuotesFound:
EN: "This server doesn't seem to have any quotes yet. You can add a quote with `!quote save @user` or `!quote save <messageId>`"
CHDE: "Dä server het no kei quotes. Du chasch quotes hinzuefüege mit `!quote save @user` oder `!quote save <messageId>`"
CannotSaveOwnQuotes:
EN: "You can't save your own quotes..."
CHDE: "Du chasch kei quotes vo dir selber speichere..."
CannotQuoteBots:
EN: "You can't save quotes by a bot..."
CHDE: "Du chasch kei quotes vomne bot speichere..."
QuoteAdded:
EN: "**Quote Added**"
CHDE: "**Quote hinzugfüegt**"
Removed:
EN: "**Removed #{0}**"
CHDE: "**#{0} glöscht**"
NotFoundWithId:
EN: "I couldn't find a quote with that ID :disappointed:"
CHDE: "Ich chan kei quote finde mit därri ID :disappointed:"
QuoteStats:
EN: "Quote Stats"
CHDE: "Quote statistike"
TotalQuotes:
EN: "Total"
CHDE: "Total"
MostQuotesPerson:
EN: "Most quoted person"
CHDE: "Meist quoteti person"
rank:
InvalidType:
EN: "Valid types are '`messages`' '`karma`', '`rolls`' and '`cookies`'"
CHDE: "Gültigi paramenter sind '`messages`' '`karma`', '`rolls`' und '`cookies`'"
LimitingTo20Warning:
EN: ":warning: Limiting to 20\n"
CHDE: ":warning: Limitiert uf 20\n"
NoTypeFoundForServer:
EN: "No {0} found on this server"
CHDE: "Kei {0} gfunde für dä server"
FailedToResolveAllUsernames:
EN: ":warning: I couldn't find all usernames. Maybe they left the server?\n"
CHDE: ":warning: Ich han nid alli benutzername gfunde. villiicht hend sie de server verlah?\n"
HighscoresFor:
EN: ":bar_chart: **{0} Highscore for {1}**"
CHDE: ":bar_chart: **{0} Highscore für {1}**"
ship:
Matchmaking:
EN: "Matchmaking"
CHDE: "Verkupple"
NotGonnaToHappen:
EN: "Not gonna happen"
CHDE: "Wird nöd klappe"
NotSuchAGoodIdea:
EN: "Not such a good idea"
CHDE: "Nöd so ä gueti idee"
ThereMightBeAChance:
EN: "There might be a chance"
CHDE: "Es gid eventuel ä chance"
CouldWork:
EN: "Almost a match"
CHDE: "Fasch en match"
ItsAMatch:
EN: "It's a match"
CHDE: "Es isch es traumpaar"

View file

@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using Discord;
namespace Geekbot.Core.Logger
{
public class DiscordLogger : IDiscordLogger
{
private readonly GeekbotLogger _logger;
public DiscordLogger(GeekbotLogger logger)
{
_logger = logger;
}
public Task Log(LogMessage message)
{
LogSource source;
try
{
source = Enum.Parse<LogSource>(message.Source);
}
catch
{
source = LogSource.Discord;
_logger.Warning(LogSource.Geekbot, $"Could not parse {message.Source} to a LogSource Enum");
}
var logMessage = $"[{message.Source}] {message.Message}";
switch (message.Severity)
{
case LogSeverity.Verbose:
_logger.Trace(source, message.Message);
break;
case LogSeverity.Debug:
_logger.Debug(source, message.Message);
break;
case LogSeverity.Info:
_logger.Information(source, message.Message);
break;
case LogSeverity.Critical:
case LogSeverity.Error:
case LogSeverity.Warning:
if (logMessage.Contains("VOICE_STATE_UPDATE")) break;
_logger.Error(source, message.Message, message.Exception);
break;
default:
_logger.Information(source, $"{logMessage} --- {message.Severity}");
break;
}
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,84 @@
using System;
using Newtonsoft.Json;
namespace Geekbot.Core.Logger
{
public class GeekbotLogger : IGeekbotLogger
{
private readonly bool _logAsJson;
private readonly NLog.Logger _logger;
private readonly JsonSerializerSettings _serializerSettings;
public GeekbotLogger(RunParameters runParameters)
{
_logAsJson = !string.IsNullOrEmpty(runParameters.SumologicEndpoint) || runParameters.LogJson;
_logger = LoggerFactory.CreateNLog(runParameters);
_serializerSettings = new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Serialize,
NullValueHandling = NullValueHandling.Include
};
Information(LogSource.Geekbot, "Using GeekbotLogger");
}
public void Trace(LogSource source, string message, object extra = null)
{
_logger.Trace(CreateLogString("Trace", source, message, null, extra));
}
public void Debug(LogSource source, string message, object extra = null)
{
if (_logAsJson) _logger.Info(CreateLogString("Debug", source, message, null, extra));
else _logger.Debug(CreateLogString("Debug", source, message, null, extra));
}
public void Information(LogSource source, string message, object extra = null)
{
_logger.Info(CreateLogString("Information", source, message, null, extra));
}
public void Warning(LogSource source, string message, Exception stackTrace = null, object extra = null)
{
if (_logAsJson) _logger.Info(CreateLogString("Warning", source, message, stackTrace, extra));
else _logger.Warn(CreateLogString("Warning", source, message, stackTrace, extra));
}
public void Error(LogSource source, string message, Exception stackTrace, object extra = null)
{
if (_logAsJson) _logger.Info(CreateLogString("Error", source, message, stackTrace, extra));
else _logger.Error(stackTrace, CreateLogString("Error", source, message, stackTrace, extra));
}
public NLog.Logger GetNLogger()
{
return _logger;
}
public bool LogAsJson()
{
return _logAsJson;
}
private string CreateLogString(string type, LogSource source, string message, Exception stackTrace = null, object extra = null)
{
if (_logAsJson)
{
var logObject = new GeekbotLoggerObject
{
Timestamp = DateTime.Now,
Type = type,
Source = source,
Message = message,
StackTrace = stackTrace,
Extra = extra
};
return JsonConvert.SerializeObject(logObject, Formatting.None, _serializerSettings);
}
if (source != LogSource.Message) return $"[{source}] - {message}";
var m = (MessageDto) extra;
return $"[{source}] - [{m?.Guild.Name} - {m?.Channel.Name}] {m?.User.Name}: {m?.Message.Content}";
}
}
}

View file

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Discord;
namespace Geekbot.Core.Logger
{
public interface IDiscordLogger
{
Task Log(LogMessage message);
}
}

View file

@ -0,0 +1,15 @@
using System;
namespace Geekbot.Core.Logger
{
public interface IGeekbotLogger
{
void Trace(LogSource source, string message, object extra = null);
void Debug(LogSource source, string message, object extra = null);
void Information(LogSource source, string message, object extra = null);
void Warning(LogSource source, string message, Exception stackTrace = null, object extra = null);
void Error(LogSource source, string message, Exception stackTrace, object extra = null);
NLog.Logger GetNLogger();
bool LogAsJson();
}
}

14
src/Core/Logger/LogDto.cs Normal file
View file

@ -0,0 +1,14 @@
using System;
namespace Geekbot.Core.Logger
{
public class GeekbotLoggerObject
{
public DateTime Timestamp { get; set; }
public string Type { get; set; }
public LogSource Source { get; set; }
public string Message { get; set; }
public Exception StackTrace { get; set; }
public object Extra { get; set; }
}
}

View file

@ -0,0 +1,22 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Geekbot.Core.Logger
{
[JsonConverter(typeof(StringEnumConverter))]
public enum LogSource
{
Geekbot,
Rest,
Gateway,
Discord,
Database,
Message,
UserRepository,
Command,
Api,
Migration,
HighscoreManager,
Other
}
}

View file

@ -0,0 +1,66 @@
using System;
using System.Text;
using NLog;
using NLog.Config;
using NLog.Targets;
using SumoLogic.Logging.NLog;
namespace Geekbot.Core.Logger
{
public class LoggerFactory
{
public static NLog.Logger CreateNLog(RunParameters runParameters)
{
var config = new LoggingConfiguration();
if (!string.IsNullOrEmpty(runParameters.SumologicEndpoint))
{
Console.WriteLine("Logging Geekbot Logs to Sumologic");
config.LoggingRules.Add(
new LoggingRule("*", LogLevel.Debug, LogLevel.Fatal,
new SumoLogicTarget()
{
Url = runParameters.SumologicEndpoint,
SourceName = "GeekbotLogger",
Layout = "${message}",
UseConsoleLog = false,
OptimizeBufferReuse = true,
Name = "Geekbot"
})
);
}
else
{
var minLevel = runParameters.Verbose ? LogLevel.Trace : LogLevel.Info;
config.LoggingRules.Add(
new LoggingRule("*", minLevel, LogLevel.Fatal,
new ColoredConsoleTarget
{
Name = "Console",
Encoding = Encoding.UTF8,
Layout = "[${longdate} ${level:format=FirstCharacter}] ${message} ${exception:format=toString}"
})
);
config.LoggingRules.Add(
new LoggingRule("*", minLevel, LogLevel.Fatal,
new FileTarget
{
Name = "File",
Layout = "[${longdate} ${level}] ${message}",
Encoding = Encoding.UTF8,
LineEnding = LineEndingMode.Default,
MaxArchiveFiles = 30,
ArchiveNumbering = ArchiveNumberingMode.Date,
ArchiveEvery = FileArchivePeriod.Day,
ArchiveFileName = "./Logs/Archive/{#####}.log",
FileName = "./Logs/Geekbot.log"
})
);
}
var loggerConfig = new LogFactory { Configuration = config };
return loggerConfig.GetCurrentClassLogger();
}
}
}

View file

@ -0,0 +1,26 @@
namespace Geekbot.Core.Logger
{
public class MessageDto
{
public MessageContent Message { get; set; }
public IdAndName User { get; set; }
public IdAndName Guild { get; set; }
public IdAndName Channel { get; set; }
public class MessageContent
{
public string Content { get; set; }
public string Id { get; set; }
public int Attachments { get; set; }
public int ChannelMentions { get; set; }
public int UserMentions { get; set; }
public int RoleMentions { get; set; }
}
public class IdAndName
{
public string Id { get; set; }
public string Name { get; set; }
}
}
}

View file

@ -0,0 +1,70 @@
using Discord.Commands;
using Discord.WebSocket;
namespace Geekbot.Core.Logger
{
public class SimpleConextConverter
{
public static MessageDto ConvertContext(ICommandContext context)
{
return new MessageDto
{
Message = new MessageDto.MessageContent
{
Content = context.Message.Content, // Only when an error occurs, including for diagnostic reason
Id = context.Message.Id.ToString(),
Attachments = context.Message.Attachments.Count,
ChannelMentions = context.Message.MentionedChannelIds.Count,
UserMentions = context.Message.MentionedUserIds.Count,
RoleMentions = context.Message.MentionedRoleIds.Count
},
User = new MessageDto.IdAndName
{
Id = context.User.Id.ToString(),
Name = $"{context.User.Username}#{context.User.Discriminator}"
},
Guild = new MessageDto.IdAndName
{
Id = context.Guild?.Id.ToString(),
Name = context.Guild?.Name
},
Channel = new MessageDto.IdAndName
{
Id = context.Channel?.Id.ToString() ?? context.User.Id.ToString(),
Name = context.Channel?.Name ?? "DM-Channel"
}
};
}
public static MessageDto ConvertSocketMessage(SocketMessage message, bool isPrivate = false)
{
var channel = isPrivate ? null : (SocketGuildChannel) message.Channel;
return new MessageDto
{
Message = new MessageDto.MessageContent
{
Id = message.Id.ToString(),
Attachments = message.Attachments.Count,
ChannelMentions = message.MentionedChannels.Count,
UserMentions = message.MentionedUsers.Count,
RoleMentions = message.MentionedRoles.Count
},
User = new MessageDto.IdAndName
{
Id = message.Author.Id.ToString(),
Name = $"{message.Author.Username}#{message.Author.Discriminator}"
},
Guild = new MessageDto.IdAndName
{
Id = channel?.Guild?.Id.ToString(),
Name = channel?.Guild?.Name
},
Channel = new MessageDto.IdAndName
{
Id = channel?.Id.ToString() ?? message.Author.Id.ToString(),
Name = channel?.Name ?? "DM-Channel"
}
};
}
}
}

View file

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using MyAnimeListSharp.Core;
namespace Geekbot.Core.MalClient
{
public interface IMalClient
{
bool IsLoggedIn();
Task<AnimeEntry> GetAnime(string query);
Task<MangaEntry> GetManga(string query);
}
}

View file

@ -0,0 +1,63 @@
using System.Threading.Tasks;
using Geekbot.Core.GlobalSettings;
using Geekbot.Core.Logger;
using MyAnimeListSharp.Auth;
using MyAnimeListSharp.Core;
using MyAnimeListSharp.Facade.Async;
namespace Geekbot.Core.MalClient
{
public class MalClient : IMalClient
{
private readonly IGlobalSettings _globalSettings;
private readonly IGeekbotLogger _logger;
private ICredentialContext _credentials;
private AnimeSearchMethodsAsync _animeSearch;
private MangaSearchMethodsAsync _mangaSearch;
public MalClient(IGlobalSettings globalSettings, IGeekbotLogger logger)
{
_globalSettings = globalSettings;
_logger = logger;
ReloadClient();
}
private bool ReloadClient()
{
var malCredentials = _globalSettings.GetKey("MalCredentials");
if (!string.IsNullOrEmpty(malCredentials))
{
var credSplit = malCredentials.Split('|');
_credentials = new CredentialContext()
{
UserName = credSplit[0],
Password = credSplit[1]
};
_animeSearch = new AnimeSearchMethodsAsync(_credentials);
_mangaSearch = new MangaSearchMethodsAsync(_credentials);
_logger.Debug(LogSource.Geekbot, "Logged in to MAL");
return true;
}
_logger.Debug(LogSource.Geekbot, "No MAL Credentials Set!");
return false;
}
public bool IsLoggedIn()
{
return _credentials != null;
}
public async Task<AnimeEntry> GetAnime(string query)
{
var response = await _animeSearch.SearchDeserializedAsync(query);
return response.Entries.Count == 0 ? null : response.Entries[0];
}
public async Task<MangaEntry> GetManga(string query)
{
var response = await _mangaSearch.SearchDeserializedAsync(query);
return response.Entries.Count == 0 ? null : response.Entries[0];
}
}
}

View file

@ -0,0 +1,33 @@
using System;
using System.IO;
using Geekbot.Core.Logger;
namespace Geekbot.Core.Media
{
public class FortunesProvider : IFortunesProvider
{
private readonly string[] _fortuneArray;
private readonly int _totalFortunes;
public FortunesProvider(IGeekbotLogger logger)
{
var path = Path.GetFullPath("./Storage/fortunes");
if (File.Exists(path))
{
var rawFortunes = File.ReadAllText(path);
_fortuneArray = rawFortunes.Split("%");
_totalFortunes = _fortuneArray.Length;
logger.Trace(LogSource.Geekbot, $"Loaded {_totalFortunes} Fortunes");
}
else
{
logger.Information(LogSource.Geekbot, $"Fortunes File not found at {path}");
}
}
public string GetRandomFortune()
{
return _fortuneArray[new Random().Next(0, _totalFortunes)];
}
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Core.Media
{
public interface IFortunesProvider
{
string GetRandomFortune();
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Core.Media
{
public interface IMediaProvider
{
string GetMedia(MediaType type);
}
}

View file

@ -0,0 +1,61 @@
using System.IO;
using Geekbot.Core.Logger;
using Geekbot.Core.RandomNumberGenerator;
namespace Geekbot.Core.Media
{
public class MediaProvider : IMediaProvider
{
private readonly IRandomNumberGenerator _random;
private readonly IGeekbotLogger _logger;
private readonly string[] _pandaImages;
private readonly string[] _croissantImages;
private readonly string[] _squirrelImages;
private readonly string[] _pumpkinImages;
private readonly string[] _turtlesImages;
private readonly string[] _penguinImages;
private readonly string[] _foxImages;
private readonly string[] _dabImages;
public MediaProvider(IGeekbotLogger logger, IRandomNumberGenerator random)
{
_random = random;
_logger = logger;
logger.Information(LogSource.Geekbot, "Loading Media Files");
LoadMedia("./Storage/pandas", ref _pandaImages);
LoadMedia("./Storage/croissant", ref _croissantImages);
LoadMedia("./Storage/squirrel", ref _squirrelImages);
LoadMedia("./Storage/pumpkin", ref _pumpkinImages);
LoadMedia("./Storage/turtles", ref _turtlesImages);
LoadMedia("./Storage/penguins", ref _penguinImages);
LoadMedia("./Storage/foxes", ref _foxImages);
LoadMedia("./Storage/dab", ref _dabImages);
}
private void LoadMedia(string path, ref string[] storage)
{
var rawLinks = File.ReadAllText(Path.GetFullPath(path));
storage = rawLinks.Split("\n");
_logger.Trace(LogSource.Geekbot, $"Loaded {storage.Length} Images from ${path}");
}
public string GetMedia(MediaType type)
{
var collection = type switch
{
MediaType.Panda => _pandaImages,
MediaType.Croissant => _croissantImages,
MediaType.Squirrel => _squirrelImages,
MediaType.Pumpkin => _pumpkinImages,
MediaType.Turtle => _turtlesImages,
MediaType.Penguin => _penguinImages,
MediaType.Fox => _foxImages,
MediaType.Dab => _dabImages,
_ => new string[0]
};
return collection[_random.Next(0, collection.Length)];
}
}
}

View file

@ -0,0 +1,14 @@
namespace Geekbot.Core.Media
{
public enum MediaType
{
Panda,
Croissant,
Squirrel,
Pumpkin,
Turtle,
Penguin,
Fox,
Dab
}
}

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Immutable;
using System.Threading.Tasks;
using Discord;
namespace Geekbot.Core.Polyfills
{
public class UserPolyfillDto : IUser
{
public ulong Id { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string Mention { get; set; }
public IActivity Activity { get; }
public UserStatus Status { get; set; }
public IImmutableSet<ClientType> ActiveClients { get; }
public string AvatarId { get; set; }
public string Discriminator { get; set; }
public ushort DiscriminatorValue { get; set; }
public bool IsBot { get; set; }
public bool IsWebhook { get; set; }
public string Username { get; set; }
public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
{
return "https://discordapp.com/assets/6debd47ed13483642cf09e832ed0bc1b.png";
}
public string GetDefaultAvatarUrl()
{
throw new NotImplementedException();
}
public Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
{
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,7 @@
namespace Geekbot.Core.RandomNumberGenerator
{
public interface IRandomNumberGenerator
{
int Next(int minValue, int maxExclusiveValue);
}
}

View file

@ -0,0 +1,46 @@
using System;
using System.Security.Cryptography;
namespace Geekbot.Core.RandomNumberGenerator
{
public class RandomNumberGenerator : IRandomNumberGenerator
{
readonly RNGCryptoServiceProvider csp;
public RandomNumberGenerator()
{
csp = new RNGCryptoServiceProvider();
}
public int Next(int minValue, int maxExclusiveValue)
{
if (minValue >= maxExclusiveValue)
{
throw new ArgumentOutOfRangeException("minValue must be lower than maxExclusiveValue");
}
var diff = (long)maxExclusiveValue - minValue;
var upperBound = uint.MaxValue / diff * diff;
uint ui;
do
{
ui = GetRandomUInt();
} while (ui >= upperBound);
return (int)(minValue + (ui % diff));
}
private uint GetRandomUInt()
{
var randomBytes = GenerateRandomBytes(sizeof(uint));
return BitConverter.ToUInt32(randomBytes, 0);
}
private byte[] GenerateRandomBytes(int bytesNumber)
{
var buffer = new byte[bytesNumber];
csp.GetBytes(buffer);
return buffer;
}
}
}

View file

@ -0,0 +1,15 @@
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
namespace Geekbot.Core.ReactionListener
{
public interface IReactionListener
{
bool IsListener(ulong id);
Task AddRoleToListener(ulong messageId, ulong guildId, string emoji, IRole role);
void RemoveRole(ISocketMessageChannel channel, SocketReaction reaction);
void GiveRole(ISocketMessageChannel message, SocketReaction reaction);
IEmote ConvertStringToEmote(string emoji);
}
}

View file

@ -0,0 +1,88 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using Geekbot.Core.Database;
using Geekbot.Core.Database.Models;
using Geekbot.Core.Extensions;
namespace Geekbot.Core.ReactionListener
{
public class ReactionListener : IReactionListener
{
private readonly DatabaseContext _database;
// <messageId, <reaction, roleId>
private Dictionary<ulong, Dictionary<IEmote, ulong>> _listener;
public ReactionListener(DatabaseContext database)
{
_database = database;
LoadListeners();
}
private void LoadListeners()
{
_listener = new Dictionary<ulong, Dictionary<IEmote, ulong>>();
foreach (var row in _database.ReactionListeners)
{
var messageId = row.MessageId.AsUlong();
if (!_listener.ContainsKey(messageId))
{
_listener.Add(messageId, new Dictionary<IEmote, ulong>());
}
_listener[messageId].Add(ConvertStringToEmote(row.Reaction), row.RoleId.AsUlong());
}
}
public bool IsListener(ulong id)
{
return _listener.ContainsKey(id);
}
public async Task AddRoleToListener(ulong messageId, ulong guildId, string emoji, IRole role)
{
var emote = ConvertStringToEmote(emoji);
await _database.ReactionListeners.AddAsync(new ReactionListenerModel()
{
GuildId = guildId.AsLong(),
MessageId = messageId.AsLong(),
RoleId = role.Id.AsLong(),
Reaction = emoji
});
await _database.SaveChangesAsync();
if (!_listener.ContainsKey(messageId))
{
_listener.Add(messageId, new Dictionary<IEmote, ulong>());
}
_listener[messageId].Add(emote, role.Id);
}
public async void RemoveRole(ISocketMessageChannel channel, SocketReaction reaction)
{
var roleId = _listener[reaction.MessageId][reaction.Emote];
var guild = (SocketGuildChannel) channel;
var role = guild.Guild.GetRole(roleId);
await ((IGuildUser) reaction.User.Value).RemoveRoleAsync(role);
}
public async void GiveRole(ISocketMessageChannel channel, SocketReaction reaction)
{
var roleId = _listener[reaction.MessageId][reaction.Emote];
var guild = (SocketGuildChannel) channel;
var role = guild.Guild.GetRole(roleId);
await ((IGuildUser) reaction.User.Value).AddRoleAsync(role);
}
public IEmote ConvertStringToEmote(string emoji)
{
if (!emoji.StartsWith('<'))
{
return new Emoji(emoji);
}
return Emote.Parse(emoji);
}
}
}

116
src/Core/RunParameters.cs Normal file
View file

@ -0,0 +1,116 @@
using System;
using CommandLine;
namespace Geekbot.Core
{
public class RunParameters
{
/************************************
* General *
************************************/
[Option("token", HelpText = "Set a new bot token. By default it will use your previous bot token which was stored in the database (default: null) (env: TOKEN)")]
public string Token { get; set; } = ParamFallback("TOKEN");
[Option('V', "verbose", HelpText = "Logs everything. (default: false) (env: LOG_VERBOSE)")]
public bool Verbose { get; set; } = ParamFallback("LOG_VERBOSE", false);
[Option('j', "log-json", HelpText = "Logger outputs json (default: false ) (env: LOG_JSON)")]
public bool LogJson { get; set; } = ParamFallback("LOG_JSON", false);
[Option('e', "expose-errors", HelpText = "Shows internal errors in the chat (default: false) (env: EXPOSE_ERRORS)")]
public bool ExposeErrors { get; set; } = ParamFallback("EXPOSE_ERRORS", false);
/************************************
* Database *
************************************/
[Option("in-memory", HelpText = "Uses the in-memory database instead of postgresql (default: false) (env: DB_INMEMORY)")]
public bool InMemory { get; set; } = ParamFallback("DB_INMEMORY", false);
// Postresql connection
[Option("database", HelpText = "Select a postgresql database (default: geekbot) (env: DB_DATABASE)")]
public string DbDatabase { get; set; } = ParamFallback("DB_DATABASE", "geekbot");
[Option("db-host", HelpText = "Set a postgresql host (default: localhost) (env: DB_HOST)")]
public string DbHost { get; set; } = ParamFallback("DB_HOST", "localhost");
[Option("db-port", HelpText = "Set a postgresql host (default: 5432) (env: DB_PORT)")]
public string DbPort { get; set; } = ParamFallback("DB_PORT", "5432");
[Option("db-user", HelpText = "Set a postgresql user (default: geekbot) (env: DB_USER)")]
public string DbUser { get; set; } = ParamFallback("DB_USER", "geekbot");
[Option("db-password", HelpText = "Set a posgresql password (default: empty) (env: DB_PASSWORD)")]
public string DbPassword { get; set; } = ParamFallback("DB_PASSWORD", "");
[Option("db-require-ssl", HelpText = "Require SSL to connect to the database (default: false) (env: DB_REQUIRE_SSL)")]
public bool DbSsl { get; set; } = ParamFallback("DB_REQUIRE_SSL", false);
[Option("db-trust-cert", HelpText = "Trust the database certificate, regardless if it is valid (default: false) (env: DB_TRUST_CERT)")]
public bool DbTrustCert { get; set; } = ParamFallback("DB_TRUST_CERT", false);
[Option("db-redshift-compat", HelpText = "Enable compatibility for AWS Redshift and DigitalOcean Managed Database (default: false) (env: DB_REDSHIFT_COMPAT)")]
public bool DbRedshiftCompatibility { get; set; } = ParamFallback("DB_REDSHIFT_COMPAT", false);
// Logging
[Option("db-logging", HelpText = "Enable database logging (default: false) (env: DB_LOGGING)")]
public bool DbLogging { get; set; } = ParamFallback("DB_LOGGING", false);
/************************************
* WebApi *
************************************/
[Option('a', "disable-api", HelpText = "Disables the WebApi (default: false) (env: API_DISABLE)")]
public bool DisableApi { get; set; } = ParamFallback("API_DISABLE", false);
[Option("api-host", HelpText = "Host on which the WebApi listens (default: localhost) (env: API_HOST)")]
public string ApiHost { get; set; } = ParamFallback("API_HOST", "localhost");
[Option("api-port", HelpText = "Port on which the WebApi listens (default: 12995) (env: API_PORT)")]
public string ApiPort { get; set; } = ParamFallback("API_PORT", "12995");
/************************************
* Intergrations *
************************************/
[Option("sumologic", HelpText = "Sumologic endpoint for logging (default: null) (env: SUMOLOGIC)")]
public string SumologicEndpoint { get; set; } = ParamFallback("SUMOLOGIC");
[Option("sentry", HelpText = "Sentry endpoint for error reporting (default: null) (env: SENTRY)")]
public string SentryEndpoint { get; set; } = ParamFallback("SENTRY");
/************************************
* Helper Functions *
************************************/
private static string ParamFallback(string key, string defaultValue = null)
{
var envVar = GetEnvironmentVariable(key);
return !string.IsNullOrEmpty(envVar) ? envVar : defaultValue;
}
private static bool ParamFallback(string key, bool defaultValue)
{
var envVar = GetEnvironmentVariable(key);
if (!string.IsNullOrEmpty(envVar))
{
return envVar.ToLower() switch
{
"true" => true,
"1" => true,
"false" => false,
"0" => false,
_ => defaultValue
};
}
return defaultValue;
}
private static string GetEnvironmentVariable(string name)
{
return Environment.GetEnvironmentVariable($"GEEKBOT_{name}");
}
}
}

View file

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Discord.WebSocket;
using Geekbot.Core.Database.Models;
namespace Geekbot.Core.UserRepository
{
public interface IUserRepository
{
Task<bool> Update(SocketUser user);
UserModel Get(ulong userId);
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Discord.WebSocket;
using Geekbot.Core.Database;
using Geekbot.Core.Database.Models;
using Geekbot.Core.Extensions;
using Geekbot.Core.Logger;
namespace Geekbot.Core.UserRepository
{
public class UserRepository : IUserRepository
{
private readonly DatabaseContext _database;
private readonly IGeekbotLogger _logger;
public UserRepository(DatabaseContext database, IGeekbotLogger logger)
{
_database = database;
_logger = logger;
}
public async Task<bool> Update(SocketUser user)
{
try
{
var savedUser = Get(user.Id);
var isNew = false;
if (savedUser == null)
{
savedUser = new UserModel();
isNew = true;
}
savedUser.UserId = user.Id.AsLong();
savedUser.Username = user.Username;
savedUser.Discriminator = user.Discriminator;
savedUser.AvatarUrl = user.GetAvatarUrl() ?? "";
savedUser.IsBot = user.IsBot;
savedUser.Joined = user.CreatedAt;
if (isNew)
{
await _database.Users.AddAsync(savedUser);
}
else
{
_database.Users.Update(savedUser);
}
await _database.SaveChangesAsync();
_logger.Information(LogSource.UserRepository, "Updated User", savedUser);
await Task.Delay(500);
return true;
}
catch (Exception e)
{
_logger.Warning(LogSource.UserRepository, $"Failed to update user: {user.Username}#{user.Discriminator} ({user.Id})", e);
return false;
}
}
public UserModel Get(ulong userId)
{
try
{
return _database.Users.FirstOrDefault(u => u.UserId.Equals(userId.AsLong()));
}
catch (Exception e)
{
_logger.Warning(LogSource.UserRepository, $"Failed to get {userId} from repository", e);
return null;
}
}
}
}

View file

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Geekbot.Core.WikipediaClient.Page;
namespace Geekbot.Core.WikipediaClient
{
public interface IWikipediaClient
{
Task<PagePreview> GetPreview(string pageName, string language = "en");
}
}

View file

@ -0,0 +1,14 @@
using System;
namespace Geekbot.Core.WikipediaClient.Page
{
public class PageApiUrls
{
public Uri Summary { get; set; }
public Uri Metadata { get; set; }
public Uri References { get; set; }
public Uri Media { get; set; }
public Uri EditHtml { get; set; }
public Uri TalkPageHtml { get; set; }
}
}

View file

@ -0,0 +1,8 @@
namespace Geekbot.Core.WikipediaClient.Page
{
public class PageContentUrlCollection
{
public PageContentUrls Desktop { get; set; }
public PageContentUrls Mobile { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using System;
namespace Geekbot.Core.WikipediaClient.Page
{
public class PageContentUrls
{
public Uri Page { get; set; }
public Uri Revisions { get; set; }
public Uri Edit { get; set; }
public Uri Talk { get; set; }
}
}

View file

@ -0,0 +1,8 @@
namespace Geekbot.Core.WikipediaClient.Page
{
public class PageCoordinates
{
public float Lat { get; set; }
public float Lon { get; set; }
}
}

View file

@ -0,0 +1,12 @@
using System;
namespace Geekbot.Core.WikipediaClient.Page
{
public class PageImage
{
public Uri Source { get; set; }
public int Width { get; set; }
public int Height { get; set; }
}
}

View file

@ -0,0 +1,8 @@
namespace Geekbot.Core.WikipediaClient.Page
{
public class PageNamespace
{
public ulong Id { get; set; }
public string Text { get; set; }
}
}

View file

@ -0,0 +1,67 @@
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace Geekbot.Core.WikipediaClient.Page
{
public class PagePreview
{
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public PageTypes Type { get; set; }
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("displaytitle")]
public string Displaytitle { get; set; }
[JsonProperty("namespace")]
public PageNamespace Namespace { get; set; }
[JsonProperty("titles")]
public PageTitles Titles { get; set; }
[JsonProperty("pageid")]
public ulong Pageid { get; set; }
[JsonProperty("thumbnail")]
public PageImage Thumbnail { get; set; }
[JsonProperty("originalimage")]
public PageImage Originalimage { get; set; }
[JsonProperty("lang")]
public string Lang { get; set; }
[JsonProperty("dir")]
public string Dir { get; set; }
[JsonProperty("revision")]
public ulong Revision { get; set; }
[JsonProperty("tid")]
public string Tid { get; set; }
[JsonProperty("timestamp")]
public DateTimeOffset Timestamp { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("coordinates")]
public PageCoordinates Coordinates { get; set; }
[JsonProperty("content_urls")]
public PageContentUrlCollection ContentUrls { get; set; }
[JsonProperty("api_urls")]
public PageApiUrls ApiUrls { get; set; }
[JsonProperty("extract")]
public string Extract { get; set; }
[JsonProperty("extract_html")]
public string ExtractHtml { get; set; }
}
}

View file

@ -0,0 +1,10 @@
namespace Geekbot.Core.WikipediaClient.Page
{
public class PageTitles
{
public string Canonical { get; set; }
public string Normalized { get; set; }
public string Display { get; set; }
}
}

View file

@ -0,0 +1,19 @@
using System.Runtime.Serialization;
namespace Geekbot.Core.WikipediaClient.Page
{
public enum PageTypes
{
[EnumMember(Value = "standard")]
Standard,
[EnumMember(Value = "disambiguation")]
Disambiguation,
[EnumMember(Value = "mainpage")]
MainPage,
[EnumMember(Value = "no-extract")]
NoExtract
}
}

View file

@ -0,0 +1,25 @@
using System.Net.Http;
using System.Threading.Tasks;
using Geekbot.Core.WikipediaClient.Page;
using Newtonsoft.Json;
namespace Geekbot.Core.WikipediaClient
{
public class WikipediaClient : IWikipediaClient
{
private readonly HttpClient _httpClient;
public WikipediaClient()
{
_httpClient = new HttpClient();
}
public async Task<PagePreview> GetPreview(string pageName, string language = "en")
{
var response = await _httpClient.GetAsync($"https://{language}.wikipedia.org/api/rest_v1/page/summary/{pageName}");
response.EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<PagePreview>(stringResponse);
}
}
}