From 4874879979b7455d8557164dd309007d1d5a63e5 Mon Sep 17 00:00:00 2001 From: Ske Date: Tue, 9 Jul 2019 20:39:29 +0200 Subject: [PATCH] Add basic API, only with system endpoints --- PluralKit.API/Controllers/SystemController.cs | 156 ++++++++++++++++++ PluralKit.API/PluralKit.API.csproj | 17 ++ PluralKit.API/Program.cs | 26 +++ PluralKit.API/Properties/launchSettings.json | 30 ++++ PluralKit.API/Startup.cs | 72 ++++++++ PluralKit.API/TokenAuthService.cs | 31 ++++ PluralKit.API/app.config | 6 + PluralKit.API/appsettings.Development.json | 9 + PluralKit.API/appsettings.json | 8 + PluralKit.Bot/Bot.cs | 9 +- PluralKit.Core/DatabaseUtils.cs | 55 ------ PluralKit.Core/Models.cs | 54 +++--- PluralKit.Core/PluralKit.Core.csproj | 7 + PluralKit.Core/Schema.cs | 65 ++------ PluralKit.Core/Utils.cs | 73 ++++++++ PluralKit.Core/db_schema.sql | 69 ++++++++ PluralKit.Web/Startup.cs | 2 +- PluralKit.sln | 6 + 18 files changed, 550 insertions(+), 145 deletions(-) create mode 100644 PluralKit.API/Controllers/SystemController.cs create mode 100644 PluralKit.API/PluralKit.API.csproj create mode 100644 PluralKit.API/Program.cs create mode 100644 PluralKit.API/Properties/launchSettings.json create mode 100644 PluralKit.API/Startup.cs create mode 100644 PluralKit.API/TokenAuthService.cs create mode 100644 PluralKit.API/app.config create mode 100644 PluralKit.API/appsettings.Development.json create mode 100644 PluralKit.API/appsettings.json delete mode 100644 PluralKit.Core/DatabaseUtils.cs create mode 100644 PluralKit.Core/db_schema.sql diff --git a/PluralKit.API/Controllers/SystemController.cs b/PluralKit.API/Controllers/SystemController.cs new file mode 100644 index 00000000..73d1fb14 --- /dev/null +++ b/PluralKit.API/Controllers/SystemController.cs @@ -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 Members { get; set; } + } + + public struct FrontersReturn + { + [JsonProperty("timestamp")] public Instant Timestamp { get; set; } + [JsonProperty("members")] public IEnumerable Members { get; set; } + } + + public struct PostSwitchParams + { + public ICollection 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> 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>> 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>> 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( + @"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> 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> 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 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("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(); + // 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(); + } + } +} \ No newline at end of file diff --git a/PluralKit.API/PluralKit.API.csproj b/PluralKit.API/PluralKit.API.csproj new file mode 100644 index 00000000..382fd144 --- /dev/null +++ b/PluralKit.API/PluralKit.API.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp2.2 + + + + + + + + + + + + + diff --git a/PluralKit.API/Program.cs b/PluralKit.API/Program.cs new file mode 100644 index 00000000..880fba30 --- /dev/null +++ b/PluralKit.API/Program.cs @@ -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(); + } +} \ No newline at end of file diff --git a/PluralKit.API/Properties/launchSettings.json b/PluralKit.API/Properties/launchSettings.json new file mode 100644 index 00000000..ac364798 --- /dev/null +++ b/PluralKit.API/Properties/launchSettings.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/PluralKit.API/Startup.cs b/PluralKit.API/Startup.cs new file mode 100644 index 00000000..077bde80 --- /dev/null +++ b/PluralKit.API/Startup.cs @@ -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() + .AddTransient() + .AddTransient() + .AddTransient() + + .AddScoped() + + .AddTransient(_ => Configuration.GetSection("PluralKit").Get() ?? new CoreConfig()) + .AddScoped(svc => + { + var conn = new NpgsqlConnection(svc.GetRequiredService().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(); + app.UseMvc(); + } + } +} \ No newline at end of file diff --git a/PluralKit.API/TokenAuthService.cs b/PluralKit.API/TokenAuthService.cs new file mode 100644 index 00000000..7bfda4ce --- /dev/null +++ b/PluralKit.API/TokenAuthService.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/PluralKit.API/app.config b/PluralKit.API/app.config new file mode 100644 index 00000000..a6ad8283 --- /dev/null +++ b/PluralKit.API/app.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/PluralKit.API/appsettings.Development.json b/PluralKit.API/appsettings.Development.json new file mode 100644 index 00000000..e203e940 --- /dev/null +++ b/PluralKit.API/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/PluralKit.API/appsettings.json b/PluralKit.API/appsettings.json new file mode 100644 index 00000000..def9159a --- /dev/null +++ b/PluralKit.API/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/PluralKit.Bot/Bot.cs b/PluralKit.Bot/Bot.cs index 7c25970a..eb3cfcb3 100644 --- a/PluralKit.Bot/Bot.cs +++ b/PluralKit.Bot/Bot.cs @@ -21,18 +21,13 @@ namespace PluralKit.Bot { private IConfiguration _config; - static void Main(string[] args) => new Initialize { _config = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("pluralkit.conf", true) - .AddEnvironmentVariables() - .AddCommandLine(args) - .Build()}.MainAsync().GetAwaiter().GetResult(); + static void Main(string[] args) => new Initialize { _config = InitUtils.BuildConfiguration(args).Build()}.MainAsync().GetAwaiter().GetResult(); private async Task MainAsync() { Console.WriteLine("Starting PluralKit..."); - DatabaseUtils.Init(); + InitUtils.Init(); using (var services = BuildServiceProvider()) { diff --git a/PluralKit.Core/DatabaseUtils.cs b/PluralKit.Core/DatabaseUtils.cs deleted file mode 100644 index 06bb76a9..00000000 --- a/PluralKit.Core/DatabaseUtils.cs +++ /dev/null @@ -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(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()); - SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); - } - - class UlongEncodeAsLongHandler : SqlMapper.TypeHandler - { - 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 : SqlMapper.TypeHandler - { - public override void SetValue(IDbDataParameter parameter, T value) - { - parameter.Value = value; - } - - public override T Parse(object value) - { - return (T) value; - } - } - } -} \ No newline at end of file diff --git a/PluralKit.Core/Models.cs b/PluralKit.Core/Models.cs index b947d1ce..347878df 100644 --- a/PluralKit.Core/Models.cs +++ b/PluralKit.Core/Models.cs @@ -1,4 +1,5 @@ using Dapper.Contrib.Extensions; +using Newtonsoft.Json; using NodaTime; using NodaTime.Text; @@ -6,39 +7,38 @@ namespace PluralKit { public class PKSystem { - [Key] - public int Id { get; set; } - public string Hid { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public string Tag { get; set; } - public string AvatarUrl { get; set; } - public string Token { get; set; } - public Instant Created { get; set; } - public string UiTz { get; set; } + [Key] [JsonIgnore] public int Id { get; set; } + [JsonProperty("id")] public string Hid { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("tag")] public string Tag { get; set; } + [JsonProperty("avatar_url")] public string AvatarUrl { get; set; } + [JsonIgnore] public string Token { get; set; } + [JsonProperty("created")] public Instant Created { get; set; } + [JsonProperty("tz")] 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 int Id { get; set; } - public string Hid { get; set; } - public int System { get; set; } - public string Color { get; set; } - public string AvatarUrl { get; set; } - public string Name { get; set; } - public LocalDate? Birthday { get; set; } - public string Pronouns { get; set; } - public string Description { get; set; } - public string Prefix { get; set; } - public string Suffix { get; set; } - public Instant Created { get; set; } + [JsonIgnore] public int Id { get; set; } + [JsonProperty("id")] public string Hid { get; set; } + [JsonIgnore] public int System { get; set; } + [JsonProperty("color")] public string Color { get; set; } + [JsonProperty("avatar_url")] public string AvatarUrl { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("birthday")] public LocalDate? Birthday { get; set; } + [JsonProperty("pronouns")] public string Pronouns { get; set; } + [JsonProperty("description")] public string Description { get; set; } + [JsonProperty("prefix")] public string Prefix { get; set; } + [JsonProperty("suffix")] public string Suffix { 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 - public string BirthdayString + [JsonIgnore] public string BirthdayString { get { @@ -50,8 +50,8 @@ namespace PluralKit } } - public bool HasProxyTags => Prefix != null || Suffix != null; - public string ProxyString => $"{Prefix ?? ""}text{Suffix ?? ""}"; + [JsonIgnore] public bool HasProxyTags => Prefix != null || Suffix != null; + [JsonIgnore] public string ProxyString => $"{Prefix ?? ""}text{Suffix ?? ""}"; } public class PKSwitch diff --git a/PluralKit.Core/PluralKit.Core.csproj b/PluralKit.Core/PluralKit.Core.csproj index 88e25534..0fd4f277 100644 --- a/PluralKit.Core/PluralKit.Core.csproj +++ b/PluralKit.Core/PluralKit.Core.csproj @@ -13,8 +13,15 @@ + + + + + + + diff --git a/PluralKit.Core/Schema.cs b/PluralKit.Core/Schema.cs index 7dbc2f01..10c1f299 100644 --- a/PluralKit.Core/Schema.cs +++ b/PluralKit.Core/Schema.cs @@ -1,64 +1,19 @@ using System.Data; +using System.IO; using System.Threading.Tasks; using Dapper; namespace PluralKit { public static class Schema { - public static async Task CreateTables(IDbConnection connection) { - await connection.ExecuteAsync(@"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' - )"); - 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 - )"); + public static async Task CreateTables(IDbConnection connection) + { + // Load the schema from disk (well, embedded resource) and execute the commands in there + using (var stream = typeof(Schema).Assembly.GetManifestResourceStream("PluralKit.Core.db_schema.sql")) + using (var reader = new StreamReader(stream)) + { + var result = await reader.ReadToEndAsync(); + await connection.ExecuteAsync(result); + } } } } \ No newline at end of file diff --git a/PluralKit.Core/Utils.cs b/PluralKit.Core/Utils.cs index 51ea87af..b73192dd 100644 --- a/PluralKit.Core/Utils.cs +++ b/PluralKit.Core/Utils.cs @@ -1,10 +1,17 @@ using System; using System.Collections.Generic; +using System.Data; +using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text.RegularExpressions; +using Dapper; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; using NodaTime; +using NodaTime.Serialization.JsonNet; using NodaTime.Text; +using Npgsql; namespace PluralKit @@ -258,4 +265,70 @@ namespace PluralKit public static IPattern LocalDateTimeFormat = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss"); public static IPattern 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(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()); + SqlMapper.AddTypeHandler(new PassthroughTypeHandler()); + } + + 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 + { + 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 : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, T value) + { + parameter.Value = value; + } + + public override T Parse(object value) + { + return (T) value; + } + } } diff --git a/PluralKit.Core/db_schema.sql b/PluralKit.Core/db_schema.sql new file mode 100644 index 00000000..d755a12c --- /dev/null +++ b/PluralKit.Core/db_schema.sql @@ -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 +); \ No newline at end of file diff --git a/PluralKit.Web/Startup.cs b/PluralKit.Web/Startup.cs index df339377..050e1635 100644 --- a/PluralKit.Web/Startup.cs +++ b/PluralKit.Web/Startup.cs @@ -20,7 +20,7 @@ namespace PluralKit.Web // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - DatabaseUtils.Init(); + InitUtils.Init(); var config = Configuration.GetSection("PluralKit").Get(); diff --git a/PluralKit.sln b/PluralKit.sln index 18256e5c..1a9d4b73 100644 --- a/PluralKit.sln +++ b/PluralKit.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.Core", "PluralKit EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.Web", "PluralKit.Web\PluralKit.Web.csproj", "{975F9DED-78D1-4742-8412-DF70BB381E92}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralKit.API", "PluralKit.API\PluralKit.API.csproj", "{3420F8A9-125C-4F7F-A444-10DD16945754}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 EndGlobal