Add basic API, only with system endpoints
This commit is contained in:
parent
ab49ad7217
commit
4874879979
156
PluralKit.API/Controllers/SystemController.cs
Normal file
156
PluralKit.API/Controllers/SystemController.cs
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace PluralKit.API.Controllers
|
||||||
|
{
|
||||||
|
public struct SwitchesReturn
|
||||||
|
{
|
||||||
|
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||||
|
[JsonProperty("members")] public IEnumerable<string> Members { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct FrontersReturn
|
||||||
|
{
|
||||||
|
[JsonProperty("timestamp")] public Instant Timestamp { get; set; }
|
||||||
|
[JsonProperty("members")] public IEnumerable<PKMember> Members { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct PostSwitchParams
|
||||||
|
{
|
||||||
|
public ICollection<string> Members { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("s")]
|
||||||
|
public class SystemController : ControllerBase
|
||||||
|
{
|
||||||
|
private SystemStore _systems;
|
||||||
|
private MemberStore _members;
|
||||||
|
private SwitchStore _switches;
|
||||||
|
private IDbConnection _conn;
|
||||||
|
private TokenAuthService _auth;
|
||||||
|
|
||||||
|
public SystemController(SystemStore systems, MemberStore members, SwitchStore switches, IDbConnection conn, TokenAuthService auth)
|
||||||
|
{
|
||||||
|
_systems = systems;
|
||||||
|
_members = members;
|
||||||
|
_switches = switches;
|
||||||
|
_conn = conn;
|
||||||
|
_auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{hid}")]
|
||||||
|
public async Task<ActionResult<PKSystem>> GetSystem(string hid)
|
||||||
|
{
|
||||||
|
var system = await _systems.GetByHid(hid);
|
||||||
|
if (system == null) return NotFound("System not found.");
|
||||||
|
return Ok(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{hid}/members")]
|
||||||
|
public async Task<ActionResult<IEnumerable<PKMember>>> GetMembers(string hid)
|
||||||
|
{
|
||||||
|
var system = await _systems.GetByHid(hid);
|
||||||
|
if (system == null) return NotFound("System not found.");
|
||||||
|
|
||||||
|
var members = await _members.GetBySystem(system);
|
||||||
|
return Ok(members);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{hid}/switches")]
|
||||||
|
public async Task<ActionResult<IEnumerable<SwitchesReturn>>> GetSwitches(string hid, [FromQuery(Name = "before")] Instant? before)
|
||||||
|
{
|
||||||
|
if (before == default(Instant)) before = SystemClock.Instance.GetCurrentInstant();
|
||||||
|
|
||||||
|
var system = await _systems.GetByHid(hid);
|
||||||
|
if (system == null) return NotFound("System not found.");
|
||||||
|
|
||||||
|
var res = await _conn.QueryAsync<SwitchesReturn>(
|
||||||
|
@"select *, array(
|
||||||
|
select members.hid from switch_members, members
|
||||||
|
where switch_members.switch = switches.id and members.id = switch_members.member
|
||||||
|
) as members from switches
|
||||||
|
where switches.system = @System and switches.timestamp < @Before
|
||||||
|
order by switches.timestamp desc
|
||||||
|
limit 100;", new { System = system.Id, Before = before });
|
||||||
|
return Ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{hid}/fronters")]
|
||||||
|
public async Task<ActionResult<FrontersReturn>> GetFronters(string hid)
|
||||||
|
{
|
||||||
|
var system = await _systems.GetByHid(hid);
|
||||||
|
if (system == null) return NotFound("System not found.");
|
||||||
|
|
||||||
|
var sw = await _switches.GetLatestSwitch(system);
|
||||||
|
var members = await _switches.GetSwitchMembers(sw);
|
||||||
|
return Ok(new FrontersReturn
|
||||||
|
{
|
||||||
|
Timestamp = sw.Timestamp,
|
||||||
|
Members = members
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPatch]
|
||||||
|
public async Task<ActionResult<PKSystem>> EditSystem([FromBody] PKSystem newSystem)
|
||||||
|
{
|
||||||
|
if (_auth.CurrentSystem == null) return Unauthorized("No token specified in Authorization header.");
|
||||||
|
var system = _auth.CurrentSystem;
|
||||||
|
|
||||||
|
system.Name = newSystem.Name;
|
||||||
|
system.Description = newSystem.Description;
|
||||||
|
system.Tag = newSystem.Tag;
|
||||||
|
system.AvatarUrl = newSystem.AvatarUrl;
|
||||||
|
system.UiTz = newSystem.UiTz ?? "UTC";
|
||||||
|
|
||||||
|
await _systems.Save(system);
|
||||||
|
return Ok(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("switches")]
|
||||||
|
public async Task<IActionResult> PostSwitch([FromBody] PostSwitchParams param)
|
||||||
|
{
|
||||||
|
if (_auth.CurrentSystem == null) return Unauthorized("No token specified in Authorization header.");
|
||||||
|
|
||||||
|
if (param.Members.Distinct().Count() != param.Members.Count())
|
||||||
|
return BadRequest("Duplicate members in member list.");
|
||||||
|
|
||||||
|
// We get the current switch, if it exists
|
||||||
|
var latestSwitch = await _switches.GetLatestSwitch(_auth.CurrentSystem);
|
||||||
|
var latestSwitchMembers = await _switches.GetSwitchMembers(latestSwitch);
|
||||||
|
|
||||||
|
// Bail if this switch is identical to the latest one
|
||||||
|
if (latestSwitchMembers.Select(m => m.Hid).SequenceEqual(param.Members))
|
||||||
|
return BadRequest("New members identical to existing fronters.");
|
||||||
|
|
||||||
|
// Resolve member objects for all given IDs
|
||||||
|
var membersList = (await _conn.QueryAsync<PKMember>("select * from members where hid = any(@Hids)", new {Hids = param.Members})).ToList();
|
||||||
|
foreach (var member in membersList)
|
||||||
|
if (member.System != _auth.CurrentSystem.Id)
|
||||||
|
return BadRequest($"Cannot switch to member '{member.Hid}' not in system.");
|
||||||
|
|
||||||
|
// membersList is in DB order, and we want it in actual input order
|
||||||
|
// so we go through a dict and map the original input appropriately
|
||||||
|
var membersDict = membersList.ToDictionary(m => m.Hid);
|
||||||
|
|
||||||
|
var membersInOrder = new List<PKMember>();
|
||||||
|
// We do this without .Select() since we want to have the early return bail if it doesn't find the member
|
||||||
|
foreach (var givenMemberId in param.Members)
|
||||||
|
{
|
||||||
|
if (!membersDict.TryGetValue(givenMemberId, out var member)) return BadRequest($"Member '{givenMemberId}' not found.");
|
||||||
|
membersInOrder.Add(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, log the switch (yay!)
|
||||||
|
await _switches.RegisterSwitch(_auth.CurrentSystem, membersInOrder);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
PluralKit.API/PluralKit.API.csproj
Normal file
17
PluralKit.API/PluralKit.API.csproj
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.HttpsPolicy" Version="2.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\PluralKit.Core\PluralKit.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
26
PluralKit.API/Program.cs
Normal file
26
PluralKit.API/Program.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace PluralKit.API
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
InitUtils.Init();
|
||||||
|
CreateWebHostBuilder(args).Build().Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
|
||||||
|
WebHost.CreateDefaultBuilder(args)
|
||||||
|
.UseConfiguration(InitUtils.BuildConfiguration(args).Build())
|
||||||
|
.UseStartup<Startup>();
|
||||||
|
}
|
||||||
|
}
|
30
PluralKit.API/Properties/launchSettings.json
Normal file
30
PluralKit.API/Properties/launchSettings.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:48228",
|
||||||
|
"sslPort": 44372
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "api/values",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PluralKit.API": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "api/values",
|
||||||
|
"applicationUrl": "https://localhost:5001;http://localhost:5000",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
72
PluralKit.API/Startup.cs
Normal file
72
PluralKit.API/Startup.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.HttpsPolicy;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NodaTime;
|
||||||
|
using NodaTime.Serialization.JsonNet;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
namespace PluralKit.API
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public Startup(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
Configuration = configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Configuration { get; }
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddMvc(opts => { })
|
||||||
|
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
|
||||||
|
.AddJsonOptions(opts => { opts.SerializerSettings.BuildSerializerSettings(); });
|
||||||
|
|
||||||
|
services
|
||||||
|
.AddTransient<SystemStore>()
|
||||||
|
.AddTransient<MemberStore>()
|
||||||
|
.AddTransient<SwitchStore>()
|
||||||
|
.AddTransient<MessageStore>()
|
||||||
|
|
||||||
|
.AddScoped<TokenAuthService>()
|
||||||
|
|
||||||
|
.AddTransient(_ => Configuration.GetSection("PluralKit").Get<CoreConfig>() ?? new CoreConfig())
|
||||||
|
.AddScoped<IDbConnection>(svc =>
|
||||||
|
{
|
||||||
|
var conn = new NpgsqlConnection(svc.GetRequiredService<CoreConfig>().Database);
|
||||||
|
conn.Open();
|
||||||
|
return conn;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
|
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||||
|
{
|
||||||
|
if (env.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
|
//app.UseHsts();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseMiddleware<TokenAuthService>();
|
||||||
|
app.UseMvc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
PluralKit.API/TokenAuthService.cs
Normal file
31
PluralKit.API/TokenAuthService.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace PluralKit.API
|
||||||
|
{
|
||||||
|
public class TokenAuthService: IMiddleware
|
||||||
|
{
|
||||||
|
public PKSystem CurrentSystem { get; set; }
|
||||||
|
|
||||||
|
private SystemStore _systems;
|
||||||
|
|
||||||
|
public TokenAuthService(SystemStore systems)
|
||||||
|
{
|
||||||
|
_systems = systems;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||||
|
{
|
||||||
|
var token = context.Request.Headers["Authorization"].FirstOrDefault();
|
||||||
|
if (token != null)
|
||||||
|
{
|
||||||
|
CurrentSystem = await _systems.GetByToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next.Invoke(context);
|
||||||
|
CurrentSystem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
PluralKit.API/app.config
Normal file
6
PluralKit.API/app.config
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<configuration>
|
||||||
|
<runtime>
|
||||||
|
<gcServer enabled="true"/>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
9
PluralKit.API/appsettings.Development.json
Normal file
9
PluralKit.API/appsettings.Development.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"System": "Information",
|
||||||
|
"Microsoft": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
PluralKit.API/appsettings.json
Normal file
8
PluralKit.API/appsettings.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
@ -21,18 +21,13 @@ namespace PluralKit.Bot
|
|||||||
{
|
{
|
||||||
private IConfiguration _config;
|
private IConfiguration _config;
|
||||||
|
|
||||||
static void Main(string[] args) => new Initialize { _config = new ConfigurationBuilder()
|
static void Main(string[] args) => new Initialize { _config = InitUtils.BuildConfiguration(args).Build()}.MainAsync().GetAwaiter().GetResult();
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
|
||||||
.AddJsonFile("pluralkit.conf", true)
|
|
||||||
.AddEnvironmentVariables()
|
|
||||||
.AddCommandLine(args)
|
|
||||||
.Build()}.MainAsync().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
private async Task MainAsync()
|
private async Task MainAsync()
|
||||||
{
|
{
|
||||||
Console.WriteLine("Starting PluralKit...");
|
Console.WriteLine("Starting PluralKit...");
|
||||||
|
|
||||||
DatabaseUtils.Init();
|
InitUtils.Init();
|
||||||
|
|
||||||
using (var services = BuildServiceProvider())
|
using (var services = BuildServiceProvider())
|
||||||
{
|
{
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
using System.Data;
|
|
||||||
using Dapper;
|
|
||||||
using NodaTime;
|
|
||||||
using Npgsql;
|
|
||||||
|
|
||||||
namespace PluralKit
|
|
||||||
{
|
|
||||||
public static class DatabaseUtils
|
|
||||||
{
|
|
||||||
public static void Init()
|
|
||||||
{
|
|
||||||
// Dapper by default tries to pass ulongs to Npgsql, which rejects them since PostgreSQL technically
|
|
||||||
// doesn't support unsigned types on its own.
|
|
||||||
// Instead we add a custom mapper to encode them as signed integers instead, converting them back and forth.
|
|
||||||
SqlMapper.RemoveTypeMap(typeof(ulong));
|
|
||||||
SqlMapper.AddTypeHandler<ulong>(new UlongEncodeAsLongHandler());
|
|
||||||
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
|
||||||
|
|
||||||
// Also, use NodaTime. it's good.
|
|
||||||
NpgsqlConnection.GlobalTypeMapper.UseNodaTime();
|
|
||||||
// With the thing we add above, Npgsql already handles NodaTime integration
|
|
||||||
// This makes Dapper confused since it thinks it has to convert it anyway and doesn't understand the types
|
|
||||||
// So we add a custom type handler that literally just passes the type through to Npgsql
|
|
||||||
SqlMapper.AddTypeHandler(new PassthroughTypeHandler<Instant>());
|
|
||||||
SqlMapper.AddTypeHandler(new PassthroughTypeHandler<LocalDate>());
|
|
||||||
}
|
|
||||||
|
|
||||||
class UlongEncodeAsLongHandler : SqlMapper.TypeHandler<ulong>
|
|
||||||
{
|
|
||||||
public override ulong Parse(object value)
|
|
||||||
{
|
|
||||||
// Cast to long to unbox, then to ulong (???)
|
|
||||||
return (ulong)(long)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void SetValue(IDbDataParameter parameter, ulong value)
|
|
||||||
{
|
|
||||||
parameter.Value = (long)value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PassthroughTypeHandler<T> : SqlMapper.TypeHandler<T>
|
|
||||||
{
|
|
||||||
public override void SetValue(IDbDataParameter parameter, T value)
|
|
||||||
{
|
|
||||||
parameter.Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override T Parse(object value)
|
|
||||||
{
|
|
||||||
return (T) value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
using Dapper.Contrib.Extensions;
|
using Dapper.Contrib.Extensions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using NodaTime.Text;
|
using NodaTime.Text;
|
||||||
|
|
||||||
@ -6,39 +7,38 @@ namespace PluralKit
|
|||||||
{
|
{
|
||||||
public class PKSystem
|
public class PKSystem
|
||||||
{
|
{
|
||||||
[Key]
|
[Key] [JsonIgnore] public int Id { get; set; }
|
||||||
public int Id { get; set; }
|
[JsonProperty("id")] public string Hid { get; set; }
|
||||||
public string Hid { get; set; }
|
[JsonProperty("name")] public string Name { get; set; }
|
||||||
public string Name { get; set; }
|
[JsonProperty("description")] public string Description { get; set; }
|
||||||
public string Description { get; set; }
|
[JsonProperty("tag")] public string Tag { get; set; }
|
||||||
public string Tag { get; set; }
|
[JsonProperty("avatar_url")] public string AvatarUrl { get; set; }
|
||||||
public string AvatarUrl { get; set; }
|
[JsonIgnore] public string Token { get; set; }
|
||||||
public string Token { get; set; }
|
[JsonProperty("created")] public Instant Created { get; set; }
|
||||||
public Instant Created { get; set; }
|
[JsonProperty("tz")] public string UiTz { get; set; }
|
||||||
public string UiTz { get; set; }
|
|
||||||
|
|
||||||
public int MaxMemberNameLength => Tag != null ? 32 - Tag.Length - 1 : 32;
|
[JsonIgnore] public int MaxMemberNameLength => Tag != null ? 32 - Tag.Length - 1 : 32;
|
||||||
|
|
||||||
public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz);
|
[JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PKMember
|
public class PKMember
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
[JsonIgnore] public int Id { get; set; }
|
||||||
public string Hid { get; set; }
|
[JsonProperty("id")] public string Hid { get; set; }
|
||||||
public int System { get; set; }
|
[JsonIgnore] public int System { get; set; }
|
||||||
public string Color { get; set; }
|
[JsonProperty("color")] public string Color { get; set; }
|
||||||
public string AvatarUrl { get; set; }
|
[JsonProperty("avatar_url")] public string AvatarUrl { get; set; }
|
||||||
public string Name { get; set; }
|
[JsonProperty("name")] public string Name { get; set; }
|
||||||
public LocalDate? Birthday { get; set; }
|
[JsonProperty("birthday")] public LocalDate? Birthday { get; set; }
|
||||||
public string Pronouns { get; set; }
|
[JsonProperty("pronouns")] public string Pronouns { get; set; }
|
||||||
public string Description { get; set; }
|
[JsonProperty("description")] public string Description { get; set; }
|
||||||
public string Prefix { get; set; }
|
[JsonProperty("prefix")] public string Prefix { get; set; }
|
||||||
public string Suffix { get; set; }
|
[JsonProperty("suffix")] public string Suffix { get; set; }
|
||||||
public Instant Created { get; set; }
|
[JsonProperty("created")] public Instant Created { get; set; }
|
||||||
|
|
||||||
/// Returns a formatted string representing the member's birthday, taking into account that a year of "0001" is hidden
|
/// Returns a formatted string representing the member's birthday, taking into account that a year of "0001" is hidden
|
||||||
public string BirthdayString
|
[JsonIgnore] public string BirthdayString
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@ -50,8 +50,8 @@ namespace PluralKit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasProxyTags => Prefix != null || Suffix != null;
|
[JsonIgnore] public bool HasProxyTags => Prefix != null || Suffix != null;
|
||||||
public string ProxyString => $"{Prefix ?? ""}text{Suffix ?? ""}";
|
[JsonIgnore] public string ProxyString => $"{Prefix ?? ""}text{Suffix ?? ""}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PKSwitch
|
public class PKSwitch
|
||||||
|
@ -13,8 +13,15 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
||||||
|
<PackageReference Include="NodaTime" Version="3.0.0-alpha01" />
|
||||||
|
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.2.0" />
|
||||||
<PackageReference Include="Npgsql" Version="4.0.6" />
|
<PackageReference Include="Npgsql" Version="4.0.6" />
|
||||||
<PackageReference Include="Npgsql.NodaTime" Version="4.0.6" />
|
<PackageReference Include="Npgsql.NodaTime" Version="4.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="db_schema.sql" />
|
||||||
|
<EmbeddedResource Include="db_schema.sql" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,64 +1,19 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
|
||||||
namespace PluralKit {
|
namespace PluralKit {
|
||||||
public static class Schema {
|
public static class Schema {
|
||||||
public static async Task CreateTables(IDbConnection connection) {
|
public static async Task CreateTables(IDbConnection connection)
|
||||||
await connection.ExecuteAsync(@"create table if not exists systems (
|
{
|
||||||
id serial primary key,
|
// Load the schema from disk (well, embedded resource) and execute the commands in there
|
||||||
hid char(5) unique not null,
|
using (var stream = typeof(Schema).Assembly.GetManifestResourceStream("PluralKit.Core.db_schema.sql"))
|
||||||
name text,
|
using (var reader = new StreamReader(stream))
|
||||||
description text,
|
{
|
||||||
tag text,
|
var result = await reader.ReadToEndAsync();
|
||||||
avatar_url text,
|
await connection.ExecuteAsync(result);
|
||||||
token text,
|
}
|
||||||
created timestamp not null default (current_timestamp at time zone 'utc'),
|
|
||||||
ui_tz text not null default 'UTC'
|
|
||||||
)");
|
|
||||||
await connection.ExecuteAsync(@"create table if not exists members (
|
|
||||||
id serial primary key,
|
|
||||||
hid char(5) unique not null,
|
|
||||||
system serial not null references systems(id) on delete cascade,
|
|
||||||
color char(6),
|
|
||||||
avatar_url text,
|
|
||||||
name text not null,
|
|
||||||
birthday date,
|
|
||||||
pronouns text,
|
|
||||||
description text,
|
|
||||||
prefix text,
|
|
||||||
suffix text,
|
|
||||||
created timestamp not null default (current_timestamp at time zone 'utc')
|
|
||||||
)");
|
|
||||||
await connection.ExecuteAsync(@"create table if not exists accounts (
|
|
||||||
uid bigint primary key,
|
|
||||||
system serial not null references systems(id) on delete cascade
|
|
||||||
)");
|
|
||||||
await connection.ExecuteAsync(@"create table if not exists messages (
|
|
||||||
mid bigint primary key,
|
|
||||||
channel bigint not null,
|
|
||||||
member serial not null references members(id) on delete cascade,
|
|
||||||
sender bigint not null
|
|
||||||
)");
|
|
||||||
await connection.ExecuteAsync(@"create table if not exists switches (
|
|
||||||
id serial primary key,
|
|
||||||
system serial not null references systems(id) on delete cascade,
|
|
||||||
timestamp timestamp not null default (current_timestamp at time zone 'utc')
|
|
||||||
)");
|
|
||||||
await connection.ExecuteAsync(@"create table if not exists switch_members (
|
|
||||||
id serial primary key,
|
|
||||||
switch serial not null references switches(id) on delete cascade,
|
|
||||||
member serial not null references members(id) on delete cascade
|
|
||||||
)");
|
|
||||||
await connection.ExecuteAsync(@"create table if not exists webhooks (
|
|
||||||
channel bigint primary key,
|
|
||||||
webhook bigint not null,
|
|
||||||
token text not null
|
|
||||||
)");
|
|
||||||
await connection.ExecuteAsync(@"create table if not exists servers (
|
|
||||||
id bigint primary key,
|
|
||||||
log_channel bigint
|
|
||||||
)");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
|
using NodaTime.Serialization.JsonNet;
|
||||||
using NodaTime.Text;
|
using NodaTime.Text;
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
|
||||||
namespace PluralKit
|
namespace PluralKit
|
||||||
@ -258,4 +265,70 @@ namespace PluralKit
|
|||||||
public static IPattern<LocalDateTime> LocalDateTimeFormat = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");
|
public static IPattern<LocalDateTime> LocalDateTimeFormat = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");
|
||||||
public static IPattern<ZonedDateTime> ZonedDateTimeFormat = ZonedDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss x", DateTimeZoneProviders.Tzdb);
|
public static IPattern<ZonedDateTime> ZonedDateTimeFormat = ZonedDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss x", DateTimeZoneProviders.Tzdb);
|
||||||
}
|
}
|
||||||
|
public static class InitUtils
|
||||||
|
{
|
||||||
|
public static IConfigurationBuilder BuildConfiguration(string[] args) => new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddJsonFile("pluralkit.conf", true)
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.AddCommandLine(args);
|
||||||
|
|
||||||
|
public static void Init()
|
||||||
|
{
|
||||||
|
InitDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InitDatabase()
|
||||||
|
{
|
||||||
|
// Dapper by default tries to pass ulongs to Npgsql, which rejects them since PostgreSQL technically
|
||||||
|
// doesn't support unsigned types on its own.
|
||||||
|
// Instead we add a custom mapper to encode them as signed integers instead, converting them back and forth.
|
||||||
|
SqlMapper.RemoveTypeMap(typeof(ulong));
|
||||||
|
SqlMapper.AddTypeHandler<ulong>(new UlongEncodeAsLongHandler());
|
||||||
|
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
|
||||||
|
|
||||||
|
// Also, use NodaTime. it's good.
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.UseNodaTime();
|
||||||
|
// With the thing we add above, Npgsql already handles NodaTime integration
|
||||||
|
// This makes Dapper confused since it thinks it has to convert it anyway and doesn't understand the types
|
||||||
|
// So we add a custom type handler that literally just passes the type through to Npgsql
|
||||||
|
SqlMapper.AddTypeHandler(new PassthroughTypeHandler<Instant>());
|
||||||
|
SqlMapper.AddTypeHandler(new PassthroughTypeHandler<LocalDate>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonSerializerSettings BuildSerializerSettings() => new JsonSerializerSettings().BuildSerializerSettings();
|
||||||
|
|
||||||
|
public static JsonSerializerSettings BuildSerializerSettings(this JsonSerializerSettings settings)
|
||||||
|
{
|
||||||
|
settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UlongEncodeAsLongHandler : SqlMapper.TypeHandler<ulong>
|
||||||
|
{
|
||||||
|
public override ulong Parse(object value)
|
||||||
|
{
|
||||||
|
// Cast to long to unbox, then to ulong (???)
|
||||||
|
return (ulong)(long)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetValue(IDbDataParameter parameter, ulong value)
|
||||||
|
{
|
||||||
|
parameter.Value = (long)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PassthroughTypeHandler<T> : SqlMapper.TypeHandler<T>
|
||||||
|
{
|
||||||
|
public override void SetValue(IDbDataParameter parameter, T value)
|
||||||
|
{
|
||||||
|
parameter.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override T Parse(object value)
|
||||||
|
{
|
||||||
|
return (T) value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
69
PluralKit.Core/db_schema.sql
Normal file
69
PluralKit.Core/db_schema.sql
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
create table if not exists systems
|
||||||
|
(
|
||||||
|
id serial primary key,
|
||||||
|
hid char(5) unique not null,
|
||||||
|
name text,
|
||||||
|
description text,
|
||||||
|
tag text,
|
||||||
|
avatar_url text,
|
||||||
|
token text,
|
||||||
|
created timestamp not null default (current_timestamp at time zone 'utc'),
|
||||||
|
ui_tz text not null default 'UTC'
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists members
|
||||||
|
(
|
||||||
|
id serial primary key,
|
||||||
|
hid char(5) unique not null,
|
||||||
|
system serial not null references systems (id) on delete cascade,
|
||||||
|
color char(6),
|
||||||
|
avatar_url text,
|
||||||
|
name text not null,
|
||||||
|
birthday date,
|
||||||
|
pronouns text,
|
||||||
|
description text,
|
||||||
|
prefix text,
|
||||||
|
suffix text,
|
||||||
|
created timestamp not null default (current_timestamp at time zone 'utc')
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists accounts
|
||||||
|
(
|
||||||
|
uid bigint primary key,
|
||||||
|
system serial not null references systems (id) on delete cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists messages
|
||||||
|
(
|
||||||
|
mid bigint primary key,
|
||||||
|
channel bigint not null,
|
||||||
|
member serial not null references members (id) on delete cascade,
|
||||||
|
sender bigint not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists switches
|
||||||
|
(
|
||||||
|
id serial primary key,
|
||||||
|
system serial not null references systems (id) on delete cascade,
|
||||||
|
timestamp timestamp not null default (current_timestamp at time zone 'utc')
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists switch_members
|
||||||
|
(
|
||||||
|
id serial primary key,
|
||||||
|
switch serial not null references switches (id) on delete cascade,
|
||||||
|
member serial not null references members (id) on delete cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists webhooks
|
||||||
|
(
|
||||||
|
channel bigint primary key,
|
||||||
|
webhook bigint not null,
|
||||||
|
token text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists servers
|
||||||
|
(
|
||||||
|
id bigint primary key,
|
||||||
|
log_channel bigint
|
||||||
|
);
|
@ -20,7 +20,7 @@ namespace PluralKit.Web
|
|||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
DatabaseUtils.Init();
|
InitUtils.Init();
|
||||||
|
|
||||||
var config = Configuration.GetSection("PluralKit").Get<CoreConfig>();
|
var config = Configuration.GetSection("PluralKit").Get<CoreConfig>();
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.Core", "PluralKit
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.Web", "PluralKit.Web\PluralKit.Web.csproj", "{975F9DED-78D1-4742-8412-DF70BB381E92}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.Web", "PluralKit.Web\PluralKit.Web.csproj", "{975F9DED-78D1-4742-8412-DF70BB381E92}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.API", "PluralKit.API\PluralKit.API.csproj", "{3420F8A9-125C-4F7F-A444-10DD16945754}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -24,5 +26,9 @@ Global
|
|||||||
{975F9DED-78D1-4742-8412-DF70BB381E92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{975F9DED-78D1-4742-8412-DF70BB381E92}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{975F9DED-78D1-4742-8412-DF70BB381E92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{975F9DED-78D1-4742-8412-DF70BB381E92}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{975F9DED-78D1-4742-8412-DF70BB381E92}.Release|Any CPU.Build.0 = Release|Any CPU
|
{975F9DED-78D1-4742-8412-DF70BB381E92}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3420F8A9-125C-4F7F-A444-10DD16945754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3420F8A9-125C-4F7F-A444-10DD16945754}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3420F8A9-125C-4F7F-A444-10DD16945754}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3420F8A9-125C-4F7F-A444-10DD16945754}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Loading…
Reference in New Issue
Block a user