Add basic API, only with system endpoints
This commit is contained in:
		
							
								
								
									
										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;
 | 
			
		||||
        
 | 
			
		||||
        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())
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -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 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
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,15 @@
 | 
			
		||||
      <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" />
 | 
			
		||||
      <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
 | 
			
		||||
      <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.NodaTime" Version="4.0.6" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
    <ItemGroup>
 | 
			
		||||
      <None Remove="db_schema.sql" />
 | 
			
		||||
      <EmbeddedResource Include="db_schema.sql" />
 | 
			
		||||
    </ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<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 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.
 | 
			
		||||
        public void ConfigureServices(IServiceCollection services)
 | 
			
		||||
        {
 | 
			
		||||
            DatabaseUtils.Init();
 | 
			
		||||
            InitUtils.Init();
 | 
			
		||||
 | 
			
		||||
            var config = Configuration.GetSection("PluralKit").Get<CoreConfig>();
 | 
			
		||||
            
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user