Refactor !rank, add HighscoreManager and add /v1/highscore to the api

This commit is contained in:
runebaas 2018-09-05 22:55:45 +02:00
parent a5c70859a4
commit 99245b9ead
No known key found for this signature in database
GPG key ID: 2677AF508D0300D6
15 changed files with 261 additions and 109 deletions

View file

@ -5,31 +5,30 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using Geekbot.net.Database; using Geekbot.net.Database;
using Geekbot.net.Lib.AlmostRedis;
using Geekbot.net.Lib.Converters; using Geekbot.net.Lib.Converters;
using Geekbot.net.Lib.ErrorHandling; using Geekbot.net.Lib.ErrorHandling;
using Geekbot.net.Lib.Extensions; using Geekbot.net.Lib.Extensions;
using Geekbot.net.Lib.Highscores;
using Geekbot.net.Lib.UserRepository; using Geekbot.net.Lib.UserRepository;
using StackExchange.Redis;
namespace Geekbot.net.Commands.User.Ranking namespace Geekbot.net.Commands.User.Ranking
{ {
public class Rank : ModuleBase public class Rank : ModuleBase
{ {
private readonly IEmojiConverter _emojiConverter; private readonly IEmojiConverter _emojiConverter;
private readonly IHighscoreManager _highscoreManager;
private readonly IErrorHandler _errorHandler; private readonly IErrorHandler _errorHandler;
private readonly DatabaseContext _database; private readonly DatabaseContext _database;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IAlmostRedis _redis;
public Rank(DatabaseContext database, IErrorHandler errorHandler, IUserRepository userRepository, public Rank(DatabaseContext database, IErrorHandler errorHandler, IUserRepository userRepository,
IEmojiConverter emojiConverter, IAlmostRedis redis) IEmojiConverter emojiConverter, IHighscoreManager highscoreManager)
{ {
_database = database; _database = database;
_errorHandler = errorHandler; _errorHandler = errorHandler;
_userRepository = userRepository; _userRepository = userRepository;
_emojiConverter = emojiConverter; _emojiConverter = emojiConverter;
_redis = redis; _highscoreManager = highscoreManager;
} }
[Command("rank", RunMode = RunMode.Async)] [Command("rank", RunMode = RunMode.Async)]
@ -38,10 +37,10 @@ namespace Geekbot.net.Commands.User.Ranking
{ {
try try
{ {
RankType type; HighscoreTypes type;
try try
{ {
type = Enum.Parse<RankType>(typeUnformated.ToLower()); type = Enum.Parse<HighscoreTypes>(typeUnformated.ToLower());
} }
catch catch
{ {
@ -49,7 +48,6 @@ namespace Geekbot.net.Commands.User.Ranking
return; return;
} }
var replyBuilder = new StringBuilder(); var replyBuilder = new StringBuilder();
if (amount > 20) if (amount > 20)
{ {
@ -57,69 +55,28 @@ namespace Geekbot.net.Commands.User.Ranking
amount = 20; amount = 20;
} }
Dictionary<ulong, int> list; var guildId = Context.Guild.Id;
Dictionary<HighscoreUserDto, int> highscoreUsers;
switch (type) try
{ {
case RankType.messages: highscoreUsers = _highscoreManager.GetHighscoresWithUserData(type, guildId, amount);
list = GetMessageList(amount);
break;
case RankType.karma:
list = GetKarmaList(amount);
break;
case RankType.rolls:
list = GetRollsList(amount);
break;
default:
await ReplyAsync("Valid types are '`messages`' '`karma`', '`rolls`'");
return;
} }
catch (HighscoreListEmptyException)
if (!list.Any())
{ {
await ReplyAsync($"No {type} found on this server"); await ReplyAsync($"No {type} found on this server");
return; return;
} }
int guildMessages = 0; int guildMessages = 0;
if (type == RankType.messages) if (type == HighscoreTypes.messages)
{ {
// guildMessages = _database.Messages guildMessages = _database.Messages
// .Where(e => e.GuildId.Equals(Context.Guild.Id.AsLong())) .Where(e => e.GuildId.Equals(Context.Guild.Id.AsLong()))
// .Select(e => e.MessageCount) .Select(e => e.MessageCount)
// .Sum(); .Sum();
guildMessages = (int) _redis.Db.HashGet($"{Context.Guild.Id}:Messages", 0.ToString());
} }
var highscoreUsers = new Dictionary<RankUserDto, int>(); var failedToRetrieveUser = highscoreUsers.Any(e => string.IsNullOrEmpty(e.Key.Username));
var failedToRetrieveUser = false;
foreach (var user in list)
{
try
{
var guildUser = _userRepository.Get(user.Key);
if (guildUser?.Username != null)
{
highscoreUsers.Add(new RankUserDto
{
Username = guildUser.Username,
Discriminator = guildUser.Discriminator
}, user.Value);
}
else
{
highscoreUsers.Add(new RankUserDto
{
Id = user.Key.ToString()
}, user.Value);
failedToRetrieveUser = true;
}
}
catch
{
// ignore
}
}
if (failedToRetrieveUser) replyBuilder.AppendLine(":warning: I couldn't find all usernames. Maybe they left the server?\n"); if (failedToRetrieveUser) replyBuilder.AppendLine(":warning: I couldn't find all usernames. Maybe they left the server?\n");
replyBuilder.AppendLine($":bar_chart: **{type.ToString().CapitalizeFirst()} Highscore for {Context.Guild.Name}**"); replyBuilder.AppendLine($":bar_chart: **{type.ToString().CapitalizeFirst()} Highscore for {Context.Guild.Name}**");
@ -134,7 +91,7 @@ namespace Geekbot.net.Commands.User.Ranking
? $"**{user.Key.Username}#{user.Key.Discriminator}**" ? $"**{user.Key.Username}#{user.Key.Discriminator}**"
: $"**{user.Key.Id}**"); : $"**{user.Key.Id}**");
replyBuilder.Append(type == RankType.messages replyBuilder.Append(type == HighscoreTypes.messages
? $" - {user.Value} {type} - {Math.Round((double) (100 * user.Value) / guildMessages, digits: 2)}%\n" ? $" - {user.Value} {type} - {Math.Round((double) (100 * user.Value) / guildMessages, digits: 2)}%\n"
: $" - {user.Value} {type}\n"); : $" - {user.Value} {type}\n");
@ -148,38 +105,5 @@ namespace Geekbot.net.Commands.User.Ranking
await _errorHandler.HandleCommandException(e, Context); await _errorHandler.HandleCommandException(e, Context);
} }
} }
private Dictionary<ulong, int> GetMessageList(int amount)
{
return _database.Messages
.Where(k => k.GuildId.Equals(Context.Guild.Id.AsLong()))
.OrderByDescending(o => o.MessageCount)
.Take(amount)
.ToDictionary(key => key.UserId.AsUlong(), key => key.MessageCount);
// return _redis.Db
// .HashGetAll($"{Context.Guild.Id}:Messages")
// .Where(user => !user.Name.Equals(0))
// .OrderByDescending(s => s.Value)
// .Take(amount)
// .ToDictionary(user => ulong.Parse(user.Name), user => int.Parse(user.Value));
}
private Dictionary<ulong, int> GetKarmaList(int amount)
{
return _database.Karma
.Where(k => k.GuildId.Equals(Context.Guild.Id.AsLong()))
.OrderByDescending(o => o.Karma)
.Take(amount)
.ToDictionary(key => key.UserId.AsUlong(), key => key.Karma);
}
private Dictionary<ulong, int> GetRollsList(int amount)
{
return _database.Rolls
.Where(k => k.GuildId.Equals(Context.Guild.Id.AsLong()))
.OrderByDescending(o => o.Rolls)
.Take(amount)
.ToDictionary(key => key.UserId.AsUlong(), key => key.Rolls);
}
} }
} }

View file

@ -1,9 +0,0 @@
namespace Geekbot.net.Commands.User.Ranking
{
public enum RankType
{
messages,
karma,
rolls
}
}

View file

@ -43,7 +43,7 @@ namespace Geekbot.net.Database
Console.WriteLine(g.Name); Console.WriteLine(g.Name);
allGuilds.Add(g); allGuilds.Add(g);
} }
catch (Exception e) catch (Exception)
{ {
// ignore // ignore
} }

View file

@ -0,0 +1,13 @@
using System;
namespace Geekbot.net.Lib.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,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Geekbot.net.Database;
using Geekbot.net.Lib.Extensions;
using Geekbot.net.Lib.Logger;
using Geekbot.net.Lib.UserRepository;
namespace Geekbot.net.Lib.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)
{
Dictionary<ulong, int> list;
switch (type)
{
case HighscoreTypes.messages:
list = GetMessageList(guildId, amount);
break;
case HighscoreTypes.karma:
list = GetKarmaList(guildId, amount);
break;
case HighscoreTypes.rolls:
list = GetRollsList(guildId, amount);
break;
default:
list = new Dictionary<ulong, int>();
break;
}
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);
}
}
}

View file

@ -0,0 +1,9 @@
namespace Geekbot.net.Lib.Highscores
{
public enum HighscoreTypes
{
messages,
karma,
rolls
}
}

View file

@ -1,8 +1,9 @@
namespace Geekbot.net.Commands.User.Ranking namespace Geekbot.net.Lib.Highscores
{ {
internal class RankUserDto public class HighscoreUserDto
{ {
public string Username { get; set; } public string Username { get; set; }
public string Avatar { get; set; }
public string Discriminator { get; set; } public string Discriminator { get; set; }
public string Id { get; set; } public string Id { get; set; }
} }

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Geekbot.net.Lib.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

@ -17,6 +17,7 @@ namespace Geekbot.net.Lib.Logger
Command, Command,
Api, Api,
Migration, Migration,
HighscoreManager,
Other Other
} }
} }

View file

@ -7,7 +7,6 @@ using Discord;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Geekbot.net.Database; using Geekbot.net.Database;
using Geekbot.net.Database.LoggingAdapter;
using Geekbot.net.Lib; using Geekbot.net.Lib;
using Geekbot.net.Lib.AlmostRedis; using Geekbot.net.Lib.AlmostRedis;
using Geekbot.net.Lib.Audio; using Geekbot.net.Lib.Audio;
@ -15,6 +14,7 @@ using Geekbot.net.Lib.Clients;
using Geekbot.net.Lib.Converters; using Geekbot.net.Lib.Converters;
using Geekbot.net.Lib.ErrorHandling; using Geekbot.net.Lib.ErrorHandling;
using Geekbot.net.Lib.GlobalSettings; using Geekbot.net.Lib.GlobalSettings;
using Geekbot.net.Lib.Highscores;
using Geekbot.net.Lib.Levels; using Geekbot.net.Lib.Levels;
using Geekbot.net.Lib.Localization; using Geekbot.net.Lib.Localization;
using Geekbot.net.Lib.Logger; using Geekbot.net.Lib.Logger;
@ -40,6 +40,7 @@ namespace Geekbot.net
private IUserRepository _userRepository; private IUserRepository _userRepository;
private RunParameters _runParameters; private RunParameters _runParameters;
private IAlmostRedis _redis; private IAlmostRedis _redis;
private IHighscoreManager _highscoreManager;
private static void Main(string[] args) private static void Main(string[] args)
{ {
@ -123,6 +124,7 @@ namespace Geekbot.net
var mtgManaConverter = new MtgManaConverter(); var mtgManaConverter = new MtgManaConverter();
var wikipediaClient = new WikipediaClient(); var wikipediaClient = new WikipediaClient();
var audioUtils = new AudioUtils(); var audioUtils = new AudioUtils();
_highscoreManager = new HighscoreManager(_databaseInitializer.Initialize(), _userRepository);
_services.AddSingleton<IAlmostRedis>(_redis); _services.AddSingleton<IAlmostRedis>(_redis);
_services.AddSingleton<IUserRepository>(_userRepository); _services.AddSingleton<IUserRepository>(_userRepository);
@ -135,6 +137,7 @@ namespace Geekbot.net
_services.AddSingleton<IMtgManaConverter>(mtgManaConverter); _services.AddSingleton<IMtgManaConverter>(mtgManaConverter);
_services.AddSingleton<IWikipediaClient>(wikipediaClient); _services.AddSingleton<IWikipediaClient>(wikipediaClient);
_services.AddSingleton<IAudioUtils>(audioUtils); _services.AddSingleton<IAudioUtils>(audioUtils);
_services.AddSingleton<IHighscoreManager>(_highscoreManager);
_services.AddSingleton<IGlobalSettings>(_globalSettings); _services.AddSingleton<IGlobalSettings>(_globalSettings);
_services.AddTransient<DatabaseContext>((e) => _databaseInitializer.Initialize()); _services.AddTransient<DatabaseContext>((e) => _databaseInitializer.Initialize());
@ -203,7 +206,7 @@ namespace Geekbot.net
private Task StartWebApi() private Task StartWebApi()
{ {
_logger.Information(LogSource.Api, "Starting Webserver"); _logger.Information(LogSource.Api, "Starting Webserver");
WebApi.WebApiStartup.StartWebApi(_logger, _runParameters, _commands, _databaseInitializer.Initialize(), _client, _globalSettings); WebApi.WebApiStartup.StartWebApi(_logger, _runParameters, _commands, _databaseInitializer.Initialize(), _client, _globalSettings, _highscoreManager);
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View file

@ -0,0 +1,7 @@
namespace Geekbot.net.WebApi
{
public class ApiError
{
public string Message { get; set; }
}
}

View file

@ -0,0 +1,56 @@
using System.Collections.Generic;
using Geekbot.net.Lib.Highscores;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace Geekbot.net.WebApi.Controllers.Highscores
{
[EnableCors("AllowSpecificOrigin")]
public class HighscoreController : Controller
{
private readonly IHighscoreManager _highscoreManager;
public HighscoreController(IHighscoreManager highscoreManager)
{
_highscoreManager = highscoreManager;
}
[HttpPost]
[Route("/v1/highscore")]
public IActionResult GetHighscores([FromBody] HighscoreControllerPostBodyDto body)
{
if (!ModelState.IsValid || body == null)
{
var error = new SerializableError(ModelState);
return BadRequest(error);
}
Dictionary<HighscoreUserDto, int> list;
try
{
list = _highscoreManager.GetHighscoresWithUserData(body.Type, body.GuildId, body.Amount);
}
catch (HighscoreListEmptyException)
{
return NotFound(new ApiError
{
Message = $"No {body.Type} found on this server"
});
}
var response = new List<HighscoreControllerReponseBody>();
var counter = 1;
foreach (var item in list)
{
response.Add(new HighscoreControllerReponseBody
{
count = item.Value,
rank = counter,
user = item.Key
});
counter++;
}
return Ok(response);
}
}
}

View file

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Geekbot.net.Lib.Highscores;
namespace Geekbot.net.WebApi.Controllers.Highscores
{
public class HighscoreControllerPostBodyDto
{
[Required]
public ulong GuildId { get; set; }
public HighscoreTypes Type { get; set; } = HighscoreTypes.messages;
[Range(1, 150)]
public int Amount { get; set; } = 50;
}
}

View file

@ -0,0 +1,11 @@
using Geekbot.net.Lib.Highscores;
namespace Geekbot.net.WebApi.Controllers.Highscores
{
public class HighscoreControllerReponseBody
{
public int rank { get; set; }
public HighscoreUserDto user { get; set; }
public int count { get; set; }
}
}

View file

@ -5,6 +5,7 @@ using Discord.WebSocket;
using Geekbot.net.Database; using Geekbot.net.Database;
using Geekbot.net.Lib; using Geekbot.net.Lib;
using Geekbot.net.Lib.GlobalSettings; using Geekbot.net.Lib.GlobalSettings;
using Geekbot.net.Lib.Highscores;
using Geekbot.net.Lib.Logger; using Geekbot.net.Lib.Logger;
using Geekbot.net.WebApi.Logging; using Geekbot.net.WebApi.Logging;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
@ -18,7 +19,7 @@ namespace Geekbot.net.WebApi
public static class WebApiStartup public static class WebApiStartup
{ {
public static void StartWebApi(IGeekbotLogger logger, RunParameters runParameters, CommandService commandService, public static void StartWebApi(IGeekbotLogger logger, RunParameters runParameters, CommandService commandService,
DatabaseContext databaseContext, DiscordSocketClient client, IGlobalSettings globalSettings) DatabaseContext databaseContext, DiscordSocketClient client, IGlobalSettings globalSettings, IHighscoreManager highscoreManager)
{ {
WebHost.CreateDefaultBuilder() WebHost.CreateDefaultBuilder()
.UseKestrel(options => .UseKestrel(options =>
@ -32,6 +33,7 @@ namespace Geekbot.net.WebApi
services.AddSingleton<DatabaseContext>(databaseContext); services.AddSingleton<DatabaseContext>(databaseContext);
services.AddSingleton<DiscordSocketClient>(client); services.AddSingleton<DiscordSocketClient>(client);
services.AddSingleton<IGlobalSettings>(globalSettings); services.AddSingleton<IGlobalSettings>(globalSettings);
services.AddSingleton<IHighscoreManager>(highscoreManager);
services.AddCors(options => services.AddCors(options =>
{ {
options.AddPolicy("AllowSpecificOrigin", options.AddPolicy("AllowSpecificOrigin",