Replace the entire functionality of the APIController to be dynamic #minor
This commit is contained in:
commit
da7ff701ef
162
ChaosBot/WebServer/App/ApiControllers/BaseApiController.cs
Normal file
162
ChaosBot/WebServer/App/ApiControllers/BaseApiController.cs
Normal file
@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using ChaosBot.Models;
|
||||
using ChaosBot.WebServer.Services;
|
||||
using FlexLabs.EntityFrameworkCore.Upsert;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ChaosBot.WebServer.App.ApiControllers
|
||||
{
|
||||
public abstract class BaseApiController<T, TDeleteParameter> : Controller where T : class, new()
|
||||
{
|
||||
protected readonly AccessTokenCache AccessTokenCache;
|
||||
protected readonly ValidationService ValidationService;
|
||||
|
||||
protected BaseApiController(
|
||||
AccessTokenCache accessTokenCache,
|
||||
ValidationService validationService
|
||||
)
|
||||
{
|
||||
AccessTokenCache = accessTokenCache;
|
||||
ValidationService = validationService;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index(ulong guildId)
|
||||
{
|
||||
if (!CheckPermissions.GetResult(AccessTokenCache, Request, guildId, out IActionResult result))
|
||||
return result;
|
||||
|
||||
await using ChaosbotContext dbContext = new ChaosbotContext();
|
||||
|
||||
IQueryable<T> query = GetBasicQuery(dbContext);
|
||||
List<T> list = ApplyFilterForCurrentGuild(query, guildId).ToList();
|
||||
|
||||
LoggingFacade.Info("Received request");
|
||||
List<dynamic> response = list.Select(element =>
|
||||
{
|
||||
Dictionary<string, object> responseDict = new Dictionary<string, object>();
|
||||
|
||||
foreach (string field in GetIndexFields())
|
||||
{
|
||||
responseDict.Add(field, element.GetType().GetProperty(field)?.GetValue(element, null));
|
||||
}
|
||||
|
||||
dynamic dynamicResponseElement = new DynamicResponse(responseDict);
|
||||
return dynamicResponseElement;
|
||||
}).ToList();
|
||||
|
||||
return Content(JsonConvert.SerializeObject(response), new MediaTypeHeaderValue("application/json"));
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Upsert(ulong guildId, JsonElement requestBody)
|
||||
{
|
||||
if (!CheckPermissions.GetResult(AccessTokenCache, Request, guildId, out IActionResult result))
|
||||
return result;
|
||||
|
||||
if (!ValidationService.Validate(requestBody, GetValidationRules(), out string errors))
|
||||
return BadRequest(errors);
|
||||
|
||||
await using ChaosbotContext dbContext = new ChaosbotContext();
|
||||
|
||||
T databaseObject = SetDefaultFieldsForUpsert(new T(), guildId);
|
||||
|
||||
foreach (string key in GetValidationRules().Keys)
|
||||
{
|
||||
Type type = databaseObject?.GetType()?.GetProperty(key)?.PropertyType;
|
||||
if (type == null) continue;
|
||||
|
||||
dynamic value;
|
||||
if (type.IsEnum)
|
||||
value = Convert.ChangeType(Enum.ToObject(type, JsonElementHelper.GetValueFromRequest(requestBody, key)), type);
|
||||
else
|
||||
value = Convert.ChangeType(JsonElementHelper.GetValueFromRequest(requestBody, key), type);
|
||||
|
||||
databaseObject
|
||||
.GetType()
|
||||
.GetProperty(key)
|
||||
?.SetValue(databaseObject, value);
|
||||
}
|
||||
|
||||
await ApplyFilterForUpsert(GetBasicQuery(dbContext).Upsert(databaseObject)).RunAsync();
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Delete(ulong guildId, TDeleteParameter deleteParameter)
|
||||
{
|
||||
if (!CheckPermissions.GetResult(AccessTokenCache, Request, guildId, out IActionResult result))
|
||||
return result;
|
||||
|
||||
await using ChaosbotContext dbContext = new ChaosbotContext();
|
||||
|
||||
List<T> toDelete = FilterQueryMultipleForDeletion(GetBasicQuery(dbContext), guildId, deleteParameter);
|
||||
toDelete.Add(FilterQueryForDeletion(GetBasicQuery(dbContext), guildId, deleteParameter));
|
||||
|
||||
if (toDelete.Count == 0)
|
||||
return NotFound();
|
||||
|
||||
foreach (T obj in toDelete)
|
||||
{
|
||||
GetBasicQuery(dbContext).Remove(obj);
|
||||
}
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
protected abstract DbSet<T> GetBasicQuery(ChaosbotContext context);
|
||||
protected abstract IQueryable<T> ApplyFilterForCurrentGuild(IQueryable<T> query, ulong guildId);
|
||||
protected abstract List<string> GetIndexFields();
|
||||
protected abstract Dictionary<string, List<string>> GetValidationRules();
|
||||
protected abstract UpsertCommandBuilder<T> ApplyFilterForUpsert(UpsertCommandBuilder<T> builder);
|
||||
protected abstract T FilterQueryForDeletion(IQueryable<T> query, ulong guildId, TDeleteParameter deleteParameter);
|
||||
protected abstract List<T> FilterQueryMultipleForDeletion(IQueryable<T> query, ulong guildId, TDeleteParameter deleteParameter);
|
||||
protected abstract T SetDefaultFieldsForUpsert(T obj, ulong guildId);
|
||||
|
||||
private class DynamicResponse : DynamicObject
|
||||
{
|
||||
private readonly Dictionary<string, object> _properties;
|
||||
|
||||
public DynamicResponse(Dictionary<string, object> properties)
|
||||
{
|
||||
_properties = properties;
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetDynamicMemberNames()
|
||||
{
|
||||
return _properties.Keys;
|
||||
}
|
||||
|
||||
public override bool TryGetMember(GetMemberBinder binder, out object result)
|
||||
{
|
||||
if (_properties.ContainsKey(binder.Name))
|
||||
{
|
||||
result = _properties[binder.Name];
|
||||
return true;
|
||||
}
|
||||
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool TrySetMember(SetMemberBinder binder, object value)
|
||||
{
|
||||
if (_properties.ContainsKey(binder.Name))
|
||||
{
|
||||
_properties[binder.Name] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
ChaosBot/WebServer/App/ApiControllers/CustomCommandController.cs
Normal file
103
ChaosBot/WebServer/App/ApiControllers/CustomCommandController.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using ChaosBot.Models;
|
||||
using ChaosBot.WebServer.Models;
|
||||
using ChaosBot.WebServer.Services;
|
||||
using FlexLabs.EntityFrameworkCore.Upsert;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ChaosBot.WebServer.App.ApiControllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("/api/custom-commands")]
|
||||
public class CustomCommandController : BaseApiController<CustomCommand, string>
|
||||
{
|
||||
public CustomCommandController(
|
||||
AccessTokenCache accessTokenCache,
|
||||
ValidationService validationService
|
||||
) : base(
|
||||
accessTokenCache,
|
||||
validationService
|
||||
) {}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{guildId}")]
|
||||
public async Task<IActionResult> IndexAction(ulong guildId)
|
||||
{
|
||||
return await Index(guildId);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{guildId}")]
|
||||
public async Task<IActionResult> UpsertAction(
|
||||
[FromRoute] ulong guildId,
|
||||
[FromBody] JsonElement requestBody)
|
||||
{
|
||||
return await Upsert(guildId, requestBody);
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("{guildId}/{command}")]
|
||||
public async Task<IActionResult> DeleteAction([FromRoute]ulong guildId, [FromRoute]string command)
|
||||
{
|
||||
return await Delete(guildId, command);
|
||||
}
|
||||
|
||||
protected override DbSet<CustomCommand> GetBasicQuery(ChaosbotContext context)
|
||||
{
|
||||
return context.CustomCommands;
|
||||
}
|
||||
|
||||
protected override IQueryable<CustomCommand> ApplyFilterForCurrentGuild(IQueryable<CustomCommand> query, ulong guildId)
|
||||
{
|
||||
return query.Where(e => e.DiscordGuildId == guildId);
|
||||
}
|
||||
|
||||
protected override List<string> GetIndexFields() {
|
||||
return new List<string>
|
||||
{
|
||||
"Command",
|
||||
"Type",
|
||||
"Content"
|
||||
};
|
||||
}
|
||||
|
||||
protected override Dictionary<string, List<string>> GetValidationRules()
|
||||
{
|
||||
return new Dictionary<string, List<string>>
|
||||
{
|
||||
{"Command", new List<string>{"required", "type:string", "min:1", "max:128"}},
|
||||
{"Type", new List<string>{"required", "type:integer", "in:CustomCommandType"}},
|
||||
{"Content", new List<string>{"required", "type:string"}},
|
||||
};
|
||||
}
|
||||
|
||||
protected override UpsertCommandBuilder<CustomCommand> ApplyFilterForUpsert(
|
||||
UpsertCommandBuilder<CustomCommand> builder)
|
||||
{
|
||||
return builder.On(cc => new {cc.DiscordGuildId, cc.Command});
|
||||
}
|
||||
|
||||
protected override CustomCommand FilterQueryForDeletion(IQueryable<CustomCommand> query, ulong guildId, string deleteParameter)
|
||||
{
|
||||
return query
|
||||
.Where(cc => cc.DiscordGuildId == guildId)
|
||||
.First(cc => cc.Command == deleteParameter)
|
||||
;
|
||||
}
|
||||
|
||||
protected override List<CustomCommand> FilterQueryMultipleForDeletion(IQueryable<CustomCommand> query, ulong guildId, string deleteParameter)
|
||||
{
|
||||
return new List<CustomCommand>();
|
||||
}
|
||||
|
||||
protected override CustomCommand SetDefaultFieldsForUpsert(CustomCommand obj, ulong guildId)
|
||||
{
|
||||
obj.DiscordGuildId = guildId;
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using ChaosBot.Discord;
|
||||
using ChaosBot.Models;
|
||||
using ChaosBot.WebServer.Models;
|
||||
using ChaosBot.WebServer.Services;
|
||||
using Discord.WebSocket;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ChaosBot.WebServer.App
|
||||
{
|
||||
[ApiController]
|
||||
[Route("/api/custom-commands")]
|
||||
public class CustomCommandController : Controller
|
||||
{
|
||||
private readonly AccessTokenCache _accessTokenCache;
|
||||
|
||||
public CustomCommandController(
|
||||
AccessTokenCache accessTokenCache
|
||||
)
|
||||
{
|
||||
_accessTokenCache = accessTokenCache;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{guildId}")]
|
||||
public async Task<IActionResult> GetCustomCommands([FromRoute]ulong guildId)
|
||||
{
|
||||
if (!CheckPermissions.GetResult(_accessTokenCache, Request, guildId, out IActionResult result))
|
||||
return result;
|
||||
|
||||
await using ChaosbotContext dbContext = new ChaosbotContext();
|
||||
|
||||
IQueryable<CustomCommand> customCommandsQuery = dbContext.CustomCommands;
|
||||
List<CustomCommand> customCommands = customCommandsQuery
|
||||
.Where(cc => cc.DiscordGuildId == guildId)
|
||||
.ToList();
|
||||
|
||||
List<CustomCommandResponse> response = customCommands.Select(e => new CustomCommandResponse(e)).ToList();
|
||||
|
||||
return Json(response);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{guildId}")]
|
||||
public async Task<IActionResult> UpsertCustomCommands([FromRoute]ulong guildId, [FromBody]CustomCommandRequest customCommandRequest)
|
||||
{
|
||||
if (!CheckPermissions.GetResult(_accessTokenCache, Request, guildId, out IActionResult result))
|
||||
return result;
|
||||
|
||||
await using ChaosbotContext dbContext = new ChaosbotContext();
|
||||
|
||||
CustomCommand customCommand = new CustomCommand
|
||||
{
|
||||
DiscordGuildId = guildId,
|
||||
Command = customCommandRequest.Command,
|
||||
Type = customCommandRequest.Type,
|
||||
Content = customCommandRequest.Content
|
||||
};
|
||||
await dbContext.CustomCommands.Upsert(customCommand)
|
||||
.On(cc => new {cc.DiscordGuildId, cc.Command}).RunAsync();
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[Route("{guildId}/{command}")]
|
||||
public async Task<IActionResult> DeleteCustomCommands([FromRoute]ulong guildId, [FromRoute]string command)
|
||||
{
|
||||
if (!CheckPermissions.GetResult(_accessTokenCache, Request, guildId, out IActionResult result))
|
||||
return result;
|
||||
|
||||
await using ChaosbotContext dbContext = new ChaosbotContext();
|
||||
|
||||
IQueryable<CustomCommand> customCommandQuery = dbContext.CustomCommands;
|
||||
CustomCommand customCommand = customCommandQuery
|
||||
.Where(cc => cc.DiscordGuildId == guildId)
|
||||
.First(cc => cc.Command == command);
|
||||
|
||||
if (customCommand == null)
|
||||
return NotFound();
|
||||
|
||||
dbContext.CustomCommands.Remove(customCommand);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
ChaosBot/WebServer/Services/JsonElementHelper.cs
Normal file
30
ChaosBot/WebServer/Services/JsonElementHelper.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ChaosBot.WebServer.Services
|
||||
{
|
||||
public static class JsonElementHelper
|
||||
{
|
||||
public static dynamic GetValueFromRequest(JsonElement requestBody, string key)
|
||||
{
|
||||
JsonElement prop;
|
||||
try
|
||||
{
|
||||
prop = requestBody.GetProperty(key);
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return prop.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => prop.GetString(),
|
||||
JsonValueKind.Number => prop.GetInt64(),
|
||||
JsonValueKind.True => prop.GetBoolean(),
|
||||
JsonValueKind.False => prop.GetBoolean(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
176
ChaosBot/WebServer/Services/ValidationService.cs
Normal file
176
ChaosBot/WebServer/Services/ValidationService.cs
Normal file
@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ChaosBot.WebServer.Services
|
||||
{
|
||||
public class ValidationService
|
||||
{
|
||||
public bool Validate(JsonElement requestBody, Dictionary<string,List<string>> getValidationRules, out string error)
|
||||
{
|
||||
StringBuilder errorBuilder = new StringBuilder();
|
||||
|
||||
foreach (KeyValuePair<string,List<string>> validationRule in getValidationRules)
|
||||
{
|
||||
string key = validationRule.Key;
|
||||
dynamic value = JsonElementHelper.GetValueFromRequest(requestBody, key);
|
||||
|
||||
List<string> rules = validationRule.Value;
|
||||
|
||||
foreach (string rule in rules)
|
||||
{
|
||||
string[] ruleParts = rule.Split(':');
|
||||
string ruleType = ruleParts.First();
|
||||
|
||||
if (ruleType == null)
|
||||
continue;
|
||||
|
||||
ValidationResult result = ruleType switch
|
||||
{
|
||||
"required" => CheckRequired(key, value),
|
||||
"type" => CheckType(key, value, ruleParts),
|
||||
"min" => CheckMin(key, value, ruleParts),
|
||||
"max" => CheckMax(key, value, ruleParts),
|
||||
_ => new ValidationResult.Unknown()
|
||||
};
|
||||
|
||||
if (result.GetError() != null)
|
||||
{
|
||||
errorBuilder.AppendLine($"[{result.GetKey()}] {result.GetError()}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
error = errorBuilder.ToString();
|
||||
return error.Length == 0;
|
||||
}
|
||||
|
||||
private ValidationResult CheckRequired(string key, dynamic value)
|
||||
{
|
||||
if (value != null)
|
||||
return new ValidationResult.Success();
|
||||
return new ValidationResult.Failure(key, $"{key} is required");
|
||||
}
|
||||
|
||||
private ValidationResult CheckType(string key, dynamic value, string[] ruleParts)
|
||||
{
|
||||
if (ruleParts.Length != 2) return new ValidationResult.Unknown();
|
||||
|
||||
Type type = ruleParts[1] switch
|
||||
{
|
||||
"string" => typeof(string),
|
||||
"boolean" => typeof(bool),
|
||||
"short" => typeof(short),
|
||||
"ushort" => typeof(ushort),
|
||||
"int" => typeof(int),
|
||||
"uint" => typeof(uint),
|
||||
"long" => typeof(long),
|
||||
"ulong" => typeof(ulong),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (type == null)
|
||||
return new ValidationResult.Unknown();
|
||||
|
||||
dynamic val = Convert.ChangeType(value, type);
|
||||
if (val != null)
|
||||
return new ValidationResult.Success();
|
||||
return new ValidationResult.Failure(key, $"{key} could not be interpreted as {ruleParts[1]}");
|
||||
}
|
||||
|
||||
private ValidationResult CheckMin(string key, dynamic value, string[] ruleParts)
|
||||
{
|
||||
if (ruleParts.Length != 2) return new ValidationResult.Unknown();
|
||||
|
||||
if (value is string stringValue)
|
||||
{
|
||||
int minLength = Convert.ToInt32(ruleParts[1]);
|
||||
if (stringValue.Length < minLength)
|
||||
return new ValidationResult.Failure(key, $"{key} cannot be shorter than {minLength} characters");
|
||||
} else if (value is ulong intValue)
|
||||
{
|
||||
ulong minSize = Convert.ToUInt64(ruleParts[1]);
|
||||
if (intValue < minSize)
|
||||
return new ValidationResult.Failure(key, $"{key} must be greater than or equal to {minSize}");
|
||||
} else if (value is double floatValue)
|
||||
{
|
||||
double minSize = Convert.ToDouble(ruleParts[1]);
|
||||
if (floatValue < minSize)
|
||||
return new ValidationResult.Failure(key, $"{key} must be greater than or equal to {minSize}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValidationResult.Unknown();
|
||||
}
|
||||
|
||||
return new ValidationResult.Success();
|
||||
}
|
||||
|
||||
private ValidationResult CheckMax(string key, dynamic value, string[] ruleParts)
|
||||
{
|
||||
if (ruleParts.Length != 2) return new ValidationResult.Unknown();
|
||||
|
||||
if (value is string stringValue)
|
||||
{
|
||||
int maxLength = Convert.ToInt32(ruleParts[1]);
|
||||
if (stringValue.Length > maxLength)
|
||||
return new ValidationResult.Failure(key, $"{key} cannot be longer than {maxLength} characters");
|
||||
} else if (value is ulong intValue)
|
||||
{
|
||||
ulong maxSize = Convert.ToUInt64(ruleParts[1]);
|
||||
if (intValue > maxSize)
|
||||
return new ValidationResult.Failure(key, $"{key} must be less than or equal to {maxSize}");
|
||||
} else if (value is double floatValue)
|
||||
{
|
||||
double maxSize = Convert.ToDouble(ruleParts[1]);
|
||||
if (floatValue > maxSize)
|
||||
return new ValidationResult.Failure(key, $"{key} must be less than or equal to {maxSize}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return new ValidationResult.Unknown();
|
||||
}
|
||||
|
||||
return new ValidationResult.Success();
|
||||
}
|
||||
}
|
||||
|
||||
internal class ValidationResult
|
||||
{
|
||||
private readonly string _errorMessage;
|
||||
private readonly string _key;
|
||||
|
||||
private ValidationResult()
|
||||
{
|
||||
_errorMessage = null;
|
||||
_key = null;
|
||||
}
|
||||
|
||||
private ValidationResult(string key, string errorMessage)
|
||||
{
|
||||
_errorMessage = errorMessage;
|
||||
_key = key;
|
||||
}
|
||||
|
||||
internal class Success : ValidationResult {}
|
||||
|
||||
internal class Failure : ValidationResult
|
||||
{
|
||||
public Failure(string key, string message) : base(key, message) {}
|
||||
}
|
||||
internal class Unknown : ValidationResult {}
|
||||
|
||||
public string GetError()
|
||||
{
|
||||
return this._errorMessage;
|
||||
}
|
||||
|
||||
public string GetKey()
|
||||
{
|
||||
return this._key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -33,6 +33,7 @@ namespace ChaosBot.WebServer
|
||||
.AddSingleton(sp => new AccessTokenCache())
|
||||
.AddSingleton(sp => new DiscordInviteGenerator())
|
||||
.AddSingleton(sp => new HttpClient())
|
||||
.AddSingleton(sp => new ValidationService())
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit ae3d8a2ced41905d0b8fc6ca02df1db62a90906f
|
||||
Subproject commit 154e88ae2ef4733b97efbe3f6193973723f4d571
|
||||
Loading…
Reference in New Issue
Block a user