Add system and member privacy support
This commit is contained in:
parent
f0cc5c5961
commit
98613c4287
@ -11,10 +11,12 @@ namespace PluralKit.API.Controllers
|
|||||||
public class AccountController: ControllerBase
|
public class AccountController: ControllerBase
|
||||||
{
|
{
|
||||||
private IDataStore _data;
|
private IDataStore _data;
|
||||||
|
private TokenAuthService _auth;
|
||||||
|
|
||||||
public AccountController(IDataStore data)
|
public AccountController(IDataStore data, TokenAuthService auth)
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
|
_auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{aid}")]
|
[HttpGet("{aid}")]
|
||||||
@ -23,7 +25,7 @@ namespace PluralKit.API.Controllers
|
|||||||
var system = await _data.GetSystemByAccount(aid);
|
var system = await _data.GetSystemByAccount(aid);
|
||||||
if (system == null) return NotFound("Account not found.");
|
if (system == null) return NotFound("Account not found.");
|
||||||
|
|
||||||
return Ok(system.ToJson());
|
return Ok(system.ToJson(_auth.ContextFor(system)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -28,7 +28,7 @@ namespace PluralKit.API.Controllers
|
|||||||
var member = await _data.GetMemberByHid(hid);
|
var member = await _data.GetMemberByHid(hid);
|
||||||
if (member == null) return NotFound("Member not found.");
|
if (member == null) return NotFound("Member not found.");
|
||||||
|
|
||||||
return Ok(member.ToJson());
|
return Ok(member.ToJson(_auth.ContextFor(member)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@ -41,7 +41,7 @@ namespace PluralKit.API.Controllers
|
|||||||
return BadRequest("Member name must be specified.");
|
return BadRequest("Member name must be specified.");
|
||||||
|
|
||||||
// Enforce per-system member limit
|
// Enforce per-system member limit
|
||||||
var memberCount = await _data.GetSystemMemberCount(system);
|
var memberCount = await _data.GetSystemMemberCount(system, true);
|
||||||
if (memberCount >= Limits.MaxMemberCount)
|
if (memberCount >= Limits.MaxMemberCount)
|
||||||
return BadRequest($"Member limit reached ({memberCount} / {Limits.MaxMemberCount}).");
|
return BadRequest($"Member limit reached ({memberCount} / {Limits.MaxMemberCount}).");
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ namespace PluralKit.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _data.SaveMember(member);
|
await _data.SaveMember(member);
|
||||||
return Ok(member.ToJson());
|
return Ok(member.ToJson(_auth.ContextFor(member)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("{hid}")]
|
[HttpPatch("{hid}")]
|
||||||
@ -78,7 +78,7 @@ namespace PluralKit.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _data.SaveMember(member);
|
await _data.SaveMember(member);
|
||||||
return Ok(member.ToJson());
|
return Ok(member.ToJson(_auth.ContextFor(member)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{hid}")]
|
[HttpDelete("{hid}")]
|
||||||
|
@ -25,10 +25,12 @@ namespace PluralKit.API.Controllers
|
|||||||
public class MessageController: ControllerBase
|
public class MessageController: ControllerBase
|
||||||
{
|
{
|
||||||
private IDataStore _data;
|
private IDataStore _data;
|
||||||
|
private TokenAuthService _auth;
|
||||||
|
|
||||||
public MessageController(IDataStore _data)
|
public MessageController(IDataStore _data, TokenAuthService auth)
|
||||||
{
|
{
|
||||||
this._data = _data;
|
this._data = _data;
|
||||||
|
_auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{mid}")]
|
[HttpGet("{mid}")]
|
||||||
@ -43,8 +45,8 @@ namespace PluralKit.API.Controllers
|
|||||||
Id = msg.Message.Mid.ToString(),
|
Id = msg.Message.Mid.ToString(),
|
||||||
Channel = msg.Message.Channel.ToString(),
|
Channel = msg.Message.Channel.ToString(),
|
||||||
Sender = msg.Message.Sender.ToString(),
|
Sender = msg.Message.Sender.ToString(),
|
||||||
Member = msg.Member.ToJson(),
|
Member = msg.Member.ToJson(_auth.ContextFor(msg.System)),
|
||||||
System = msg.System.ToJson(),
|
System = msg.System.ToJson(_auth.ContextFor(msg.System)),
|
||||||
Original = msg.Message.OriginalMid?.ToString()
|
Original = msg.Message.OriginalMid?.ToString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
@ -48,7 +50,7 @@ namespace PluralKit.API.Controllers
|
|||||||
[RequiresSystem]
|
[RequiresSystem]
|
||||||
public Task<ActionResult<JObject>> GetOwnSystem()
|
public Task<ActionResult<JObject>> GetOwnSystem()
|
||||||
{
|
{
|
||||||
return Task.FromResult<ActionResult<JObject>>(Ok(_auth.CurrentSystem.ToJson()));
|
return Task.FromResult<ActionResult<JObject>>(Ok(_auth.CurrentSystem.ToJson(_auth.ContextFor(_auth.CurrentSystem))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{hid}")]
|
[HttpGet("{hid}")]
|
||||||
@ -56,7 +58,7 @@ namespace PluralKit.API.Controllers
|
|||||||
{
|
{
|
||||||
var system = await _data.GetSystemByHid(hid);
|
var system = await _data.GetSystemByHid(hid);
|
||||||
if (system == null) return NotFound("System not found.");
|
if (system == null) return NotFound("System not found.");
|
||||||
return Ok(system.ToJson());
|
return Ok(system.ToJson(_auth.ContextFor(system)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{hid}/members")]
|
[HttpGet("{hid}/members")]
|
||||||
@ -65,8 +67,13 @@ namespace PluralKit.API.Controllers
|
|||||||
var system = await _data.GetSystemByHid(hid);
|
var system = await _data.GetSystemByHid(hid);
|
||||||
if (system == null) return NotFound("System not found.");
|
if (system == null) return NotFound("System not found.");
|
||||||
|
|
||||||
|
if (!system.MemberListPrivacy.CanAccess(_auth.ContextFor(system)))
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view member list.");
|
||||||
|
|
||||||
var members = await _data.GetSystemMembers(system);
|
var members = await _data.GetSystemMembers(system);
|
||||||
return Ok(members.Select(m => m.ToJson()));
|
return Ok(members
|
||||||
|
.Where(m => m.MemberPrivacy.CanAccess(_auth.ContextFor(system)))
|
||||||
|
.Select(m => m.ToJson(_auth.ContextFor(system))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{hid}/switches")]
|
[HttpGet("{hid}/switches")]
|
||||||
@ -77,6 +84,9 @@ namespace PluralKit.API.Controllers
|
|||||||
var system = await _data.GetSystemByHid(hid);
|
var system = await _data.GetSystemByHid(hid);
|
||||||
if (system == null) return NotFound("System not found.");
|
if (system == null) return NotFound("System not found.");
|
||||||
|
|
||||||
|
if (!system.FrontHistoryPrivacy.CanAccess(_auth.ContextFor(system)))
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view front history.");
|
||||||
|
|
||||||
using (var conn = await _conn.Obtain())
|
using (var conn = await _conn.Obtain())
|
||||||
{
|
{
|
||||||
var res = await conn.QueryAsync<SwitchesReturn>(
|
var res = await conn.QueryAsync<SwitchesReturn>(
|
||||||
@ -97,6 +107,9 @@ namespace PluralKit.API.Controllers
|
|||||||
var system = await _data.GetSystemByHid(hid);
|
var system = await _data.GetSystemByHid(hid);
|
||||||
if (system == null) return NotFound("System not found.");
|
if (system == null) return NotFound("System not found.");
|
||||||
|
|
||||||
|
if (!system.FrontPrivacy.CanAccess(_auth.ContextFor(system)))
|
||||||
|
return StatusCode(StatusCodes.Status403Forbidden, "Unauthorized to view fronter.");
|
||||||
|
|
||||||
var sw = await _data.GetLatestSwitch(system);
|
var sw = await _data.GetLatestSwitch(system);
|
||||||
if (sw == null) return NotFound("System has no registered switches.");
|
if (sw == null) return NotFound("System has no registered switches.");
|
||||||
|
|
||||||
@ -104,7 +117,7 @@ namespace PluralKit.API.Controllers
|
|||||||
return Ok(new FrontersReturn
|
return Ok(new FrontersReturn
|
||||||
{
|
{
|
||||||
Timestamp = sw.Timestamp,
|
Timestamp = sw.Timestamp,
|
||||||
Members = members.Select(m => m.ToJson())
|
Members = members.Select(m => m.ToJson(_auth.ContextFor(system)))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +137,7 @@ namespace PluralKit.API.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _data.SaveSystem(system);
|
await _data.SaveSystem(system);
|
||||||
return Ok(system.ToJson());
|
return Ok(system.ToJson(_auth.ContextFor(system)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("switches")]
|
[HttpPost("switches")]
|
||||||
|
@ -26,6 +26,7 @@ namespace PluralKit.API
|
|||||||
|
|
||||||
services
|
services
|
||||||
.AddTransient<IDataStore, PostgresDataStore>()
|
.AddTransient<IDataStore, PostgresDataStore>()
|
||||||
|
.AddSingleton<SchemaService>()
|
||||||
|
|
||||||
.AddSingleton(svc => InitUtils.InitMetrics(svc.GetRequiredService<CoreConfig>(), "API"))
|
.AddSingleton(svc => InitUtils.InitMetrics(svc.GetRequiredService<CoreConfig>(), "API"))
|
||||||
|
|
||||||
@ -41,6 +42,8 @@ namespace PluralKit.API
|
|||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
|
SchemaService.Initialize();
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
@ -26,5 +26,11 @@ namespace PluralKit.API
|
|||||||
await next.Invoke(context);
|
await next.Invoke(context);
|
||||||
CurrentSystem = null;
|
CurrentSystem = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LookupContext ContextFor(PKSystem system) =>
|
||||||
|
system.Id == CurrentSystem?.Id ? LookupContext.ByOwner : LookupContext.API;
|
||||||
|
|
||||||
|
public LookupContext ContextFor(PKMember member) =>
|
||||||
|
member.System == CurrentSystem?.Id ? LookupContext.ByOwner : LookupContext.API;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -48,6 +48,8 @@ namespace PluralKit.Bot
|
|||||||
|
|
||||||
using (var services = BuildServiceProvider())
|
using (var services = BuildServiceProvider())
|
||||||
{
|
{
|
||||||
|
SchemaService.Initialize();
|
||||||
|
|
||||||
var logger = services.GetRequiredService<ILogger>().ForContext<Initialize>();
|
var logger = services.GetRequiredService<ILogger>().ForContext<Initialize>();
|
||||||
var coreConfig = services.GetRequiredService<CoreConfig>();
|
var coreConfig = services.GetRequiredService<CoreConfig>();
|
||||||
var botConfig = services.GetRequiredService<BotConfig>();
|
var botConfig = services.GetRequiredService<BotConfig>();
|
||||||
|
@ -57,13 +57,13 @@ namespace PluralKit.Bot.CommandSystem
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
|
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Match(params string[] potentialMatches)
|
public bool Match(ref string used, params string[] potentialMatches)
|
||||||
{
|
{
|
||||||
foreach (var match in potentialMatches)
|
foreach (var match in potentialMatches)
|
||||||
{
|
{
|
||||||
if (PeekArgument().Equals(match, StringComparison.InvariantCultureIgnoreCase))
|
if (PeekArgument().Equals(match, StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
PopArgument();
|
used = PopArgument();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,6 +71,15 @@ namespace PluralKit.Bot.CommandSystem
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
|
||||||
|
/// </summary>
|
||||||
|
public bool Match(params string[] potentialMatches)
|
||||||
|
{
|
||||||
|
string used = null; // Unused and unreturned, we just yeet it
|
||||||
|
return Match(ref used, potentialMatches);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task Execute<T>(Command commandDef, Func<T, Task> handler)
|
public async Task Execute<T>(Command commandDef, Func<T, Task> handler)
|
||||||
{
|
{
|
||||||
_currentCommand = commandDef;
|
_currentCommand = commandDef;
|
||||||
@ -237,6 +246,15 @@ namespace PluralKit.Bot.CommandSystem
|
|||||||
throw new PKError("This command can not be run in a DM.");
|
throw new PKError("This command can not be run in a DM.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LookupContext LookupContextFor(PKSystem target) =>
|
||||||
|
System?.Id == target.Id ? LookupContext.ByOwner : LookupContext.ByNonOwner;
|
||||||
|
|
||||||
|
public Context CheckSystemPrivacy(PKSystem target, PrivacyLevel level)
|
||||||
|
{
|
||||||
|
if (level.CanAccess(LookupContextFor(target))) return this;
|
||||||
|
throw new PKError("You do not have permission to access this information.");
|
||||||
|
}
|
||||||
|
|
||||||
public ITextChannel MatchChannel()
|
public ITextChannel MatchChannel()
|
||||||
{
|
{
|
||||||
if (!MentionUtils.TryParseChannel(PeekArgument(), out var channel)) return null;
|
if (!MentionUtils.TryParseChannel(PeekArgument(), out var channel)) return null;
|
||||||
|
@ -22,6 +22,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
public static Command SystemFronter = new Command("system fronter", "system [system] fronter", "Shows a system's fronter(s)");
|
public static Command SystemFronter = new Command("system fronter", "system [system] fronter", "Shows a system's fronter(s)");
|
||||||
public static Command SystemFrontHistory = new Command("system fronthistory", "system [system] fronthistory", "Shows a system's front history");
|
public static Command SystemFrontHistory = new Command("system fronthistory", "system [system] fronthistory", "Shows a system's front history");
|
||||||
public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown");
|
public static Command SystemFrontPercent = new Command("system frontpercent", "system [system] frontpercent [timespan]", "Shows a system's front breakdown");
|
||||||
|
public static Command SystemPrivacy = new Command("system privacy", "system [system] privacy <description|members|fronter|fronthistory> <public|private>", "Changes your system's privacy settings");
|
||||||
public static Command MemberInfo = new Command("member", "member <member>", "Looks up information about a member");
|
public static Command MemberInfo = new Command("member", "member <member>", "Looks up information about a member");
|
||||||
public static Command MemberNew = new Command("member new", "member new <name>", "Creates a new member");
|
public static Command MemberNew = new Command("member new", "member new <name>", "Creates a new member");
|
||||||
public static Command MemberRename = new Command("member rename", "member <member> rename <new name>", "Renames a member");
|
public static Command MemberRename = new Command("member rename", "member <member> rename <new name>", "Renames a member");
|
||||||
@ -36,6 +37,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
public static Command MemberServerName = new Command("member servername", "member <member> servername [server name]", "Changes a member's display name in the current server");
|
public static Command MemberServerName = new Command("member servername", "member <member> servername [server name]", "Changes a member's display name in the current server");
|
||||||
public static Command MemberKeepProxy = new Command("member keepproxy", "member <member> keepproxy [on|off]", "Sets whether to include a member's proxy tags when proxying");
|
public static Command MemberKeepProxy = new Command("member keepproxy", "member <member> keepproxy [on|off]", "Sets whether to include a member's proxy tags when proxying");
|
||||||
public static Command MemberRandom = new Command("random", "random", "Gets a random member from your system");
|
public static Command MemberRandom = new Command("random", "random", "Gets a random member from your system");
|
||||||
|
public static Command MemberPrivacy = new Command("member privacy", "member <member> privacy [on|off]", "Sets whether a member is private or public");
|
||||||
public static Command Switch = new Command("switch", "switch <member> [member 2] [member 3...]", "Registers a switch");
|
public static Command Switch = new Command("switch", "switch <member> [member 2] [member 3...]", "Registers a switch");
|
||||||
public static Command SwitchOut = new Command("switch out", "switch out", "Registers a switch with no members");
|
public static Command SwitchOut = new Command("switch out", "switch out", "Registers a switch with no members");
|
||||||
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
|
public static Command SwitchMove = new Command("switch move", "switch move <date/time>", "Moves the latest switch in time");
|
||||||
@ -58,7 +60,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
|
|
||||||
public static Command[] SystemCommands = {
|
public static Command[] SystemCommands = {
|
||||||
SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemDelete, SystemTimezone,
|
SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemDelete, SystemTimezone,
|
||||||
SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent
|
SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, SystemPrivacy
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Command[] MemberCommands = {
|
public static Command[] MemberCommands = {
|
||||||
@ -185,6 +187,8 @@ namespace PluralKit.Bot.Commands
|
|||||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
|
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
|
||||||
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
||||||
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
|
await ctx.Execute<SystemCommands>(SystemFrontPercent, m => m.SystemFrontPercent(ctx, ctx.System));
|
||||||
|
else if (ctx.Match("privacy"))
|
||||||
|
await ctx.Execute<SystemCommands>(SystemPrivacy, m => m.SystemPrivacy(ctx));
|
||||||
else if (ctx.Match("commands", "help"))
|
else if (ctx.Match("commands", "help"))
|
||||||
await PrintCommandList(ctx, "systems", SystemCommands);
|
await PrintCommandList(ctx, "systems", SystemCommands);
|
||||||
else if (!ctx.HasNext()) // Bare command
|
else if (!ctx.HasNext()) // Bare command
|
||||||
@ -272,6 +276,8 @@ namespace PluralKit.Bot.Commands
|
|||||||
await ctx.Execute<MemberCommands>(MemberServerName, m => m.MemberServerName(ctx, target));
|
await ctx.Execute<MemberCommands>(MemberServerName, m => m.MemberServerName(ctx, target));
|
||||||
else if (ctx.Match("keepproxy", "keeptags", "showtags"))
|
else if (ctx.Match("keepproxy", "keeptags", "showtags"))
|
||||||
await ctx.Execute<MemberCommands>(MemberKeepProxy, m => m.MemberKeepProxy(ctx, target));
|
await ctx.Execute<MemberCommands>(MemberKeepProxy, m => m.MemberKeepProxy(ctx, target));
|
||||||
|
else if (ctx.Match("private", "privacy", "hidden"))
|
||||||
|
await ctx.Execute<MemberCommands>(MemberPrivacy, m => m.MemberPrivacy(ctx, target));
|
||||||
else if (!ctx.HasNext()) // Bare command
|
else if (!ctx.HasNext()) // Bare command
|
||||||
await ctx.Execute<MemberCommands>(MemberInfo, m => m.ViewMember(ctx, target));
|
await ctx.Execute<MemberCommands>(MemberInfo, m => m.ViewMember(ctx, target));
|
||||||
else
|
else
|
||||||
|
@ -38,7 +38,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enforce per-system member limit
|
// Enforce per-system member limit
|
||||||
var memberCount = await _data.GetSystemMemberCount(ctx.System);
|
var memberCount = await _data.GetSystemMemberCount(ctx.System, true);
|
||||||
if (memberCount >= Limits.MaxMemberCount)
|
if (memberCount >= Limits.MaxMemberCount)
|
||||||
throw Errors.MemberLimitReachedError;
|
throw Errors.MemberLimitReachedError;
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
if (members == null || !members.Any())
|
if (members == null || !members.Any())
|
||||||
throw Errors.NoMembersError;
|
throw Errors.NoMembersError;
|
||||||
var randInt = randGen.Next(members.Count);
|
var randInt = randGen.Next(members.Count);
|
||||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild));
|
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(ctx.System, members[randInt], ctx.Guild, ctx.LookupContextFor(ctx.System)));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -430,10 +430,30 @@ namespace PluralKit.Bot.Commands
|
|||||||
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
await _proxyCache.InvalidateResultsForSystem(ctx.System);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task MemberPrivacy(Context ctx, PKMember target)
|
||||||
|
{
|
||||||
|
if (ctx.System == null) throw Errors.NoSystemError;
|
||||||
|
if (target.System != ctx.System.Id) throw Errors.NotOwnMemberError;
|
||||||
|
|
||||||
|
bool newValue;
|
||||||
|
if (ctx.Match("private", "hide", "hidden", "on", "enable", "yes")) newValue = true;
|
||||||
|
else if (ctx.Match("public", "show", "shown", "displayed", "off", "disable", "no")) newValue = false;
|
||||||
|
else if (ctx.HasNext()) throw new PKSyntaxError("You must pass either \"private\" or \"public\".");
|
||||||
|
else newValue = target.MemberPrivacy != PrivacyLevel.Private;
|
||||||
|
|
||||||
|
target.MemberPrivacy = newValue ? PrivacyLevel.Private : PrivacyLevel.Public;
|
||||||
|
await _data.SaveMember(target);
|
||||||
|
|
||||||
|
if (newValue)
|
||||||
|
await ctx.Reply($"{Emojis.Success} Member privacy set to **private**. This member will no longer show up in member lists and will return limited information when queried by other accounts.");
|
||||||
|
else
|
||||||
|
await ctx.Reply($"{Emojis.Success} Member privacy set to **public**. This member will now show up in member lists and will return all information when queried by other accounts.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ViewMember(Context ctx, PKMember target)
|
public async Task ViewMember(Context ctx, PKMember target)
|
||||||
{
|
{
|
||||||
var system = await _data.GetSystemById(target.System);
|
var system = await _data.GetSystemById(target.System);
|
||||||
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild));
|
await ctx.Reply(embed: await _embeds.CreateMemberEmbed(system, target, ctx.Guild, ctx.LookupContextFor(system)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,7 +29,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
public async Task Query(Context ctx, PKSystem system) {
|
public async Task Query(Context ctx, PKSystem system) {
|
||||||
if (system == null) throw Errors.NoSystemError;
|
if (system == null) throw Errors.NoSystemError;
|
||||||
|
|
||||||
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(system));
|
await ctx.Reply(embed: await _embeds.CreateSystemEmbed(system, ctx.LookupContextFor(system)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task New(Context ctx)
|
public async Task New(Context ctx)
|
||||||
@ -149,27 +149,57 @@ namespace PluralKit.Bot.Commands
|
|||||||
|
|
||||||
public async Task MemberShortList(Context ctx, PKSystem system) {
|
public async Task MemberShortList(Context ctx, PKSystem system) {
|
||||||
if (system == null) throw Errors.NoSystemError;
|
if (system == null) throw Errors.NoSystemError;
|
||||||
|
ctx.CheckSystemPrivacy(system, system.MemberListPrivacy);
|
||||||
|
|
||||||
var members = await _data.GetSystemMembers(system);
|
var authCtx = ctx.LookupContextFor(system);
|
||||||
|
var shouldShowPrivate = authCtx == LookupContext.ByOwner && ctx.Match("all", "everyone", "private");
|
||||||
|
|
||||||
|
var members = (await _data.GetSystemMembers(system)).ToList();
|
||||||
var embedTitle = system.Name != null ? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
var embedTitle = system.Name != null ? $"Members of {system.Name.SanitizeMentions()} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
||||||
await ctx.Paginate<PKMember>(
|
|
||||||
members.OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase).ToList(),
|
var membersToDisplay = members
|
||||||
|
.Where(m => m.MemberPrivacy == PrivacyLevel.Public || shouldShowPrivate)
|
||||||
|
.OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||||
|
var anyMembersHidden = members.Any(m => m.MemberPrivacy == PrivacyLevel.Private && !shouldShowPrivate);
|
||||||
|
|
||||||
|
await ctx.Paginate(
|
||||||
|
membersToDisplay,
|
||||||
25,
|
25,
|
||||||
embedTitle,
|
embedTitle,
|
||||||
(eb, ms) => eb.Description = string.Join("\n", ms.Select((m) => {
|
(eb, ms) =>
|
||||||
if (m.HasProxyTags) return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({m.ProxyTagsString().SanitizeMentions()})*";
|
{
|
||||||
|
eb.Description = string.Join("\n", ms.Select((m) =>
|
||||||
|
{
|
||||||
|
if (m.HasProxyTags)
|
||||||
|
return
|
||||||
|
$"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({m.ProxyTagsString().SanitizeMentions()})*";
|
||||||
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**";
|
return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}**";
|
||||||
}))
|
}));
|
||||||
);
|
|
||||||
|
var footer = $"{membersToDisplay.Count} total.";
|
||||||
|
if (anyMembersHidden && authCtx == LookupContext.ByOwner)
|
||||||
|
footer += "Private members have been hidden. type \"pk;system list all\" to include them.";
|
||||||
|
eb.WithFooter(footer);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MemberLongList(Context ctx, PKSystem system) {
|
public async Task MemberLongList(Context ctx, PKSystem system) {
|
||||||
if (system == null) throw Errors.NoSystemError;
|
if (system == null) throw Errors.NoSystemError;
|
||||||
|
ctx.CheckSystemPrivacy(system, system.MemberListPrivacy);
|
||||||
|
|
||||||
var members = await _data.GetSystemMembers(system);
|
var authCtx = ctx.LookupContextFor(system);
|
||||||
|
var shouldShowPrivate = authCtx == LookupContext.ByOwner && ctx.Match("all", "everyone", "private");
|
||||||
|
|
||||||
|
var members = (await _data.GetSystemMembers(system)).ToList();
|
||||||
var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
var embedTitle = system.Name != null ? $"Members of {system.Name} (`{system.Hid}`)" : $"Members of `{system.Hid}`";
|
||||||
await ctx.Paginate<PKMember>(
|
|
||||||
members.OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase).ToList(),
|
var membersToDisplay = members
|
||||||
|
.Where(m => m.MemberPrivacy == PrivacyLevel.Public || shouldShowPrivate)
|
||||||
|
.OrderBy(m => m.Name, StringComparer.InvariantCultureIgnoreCase).ToList();
|
||||||
|
var anyMembersHidden = members.Any(m => m.MemberPrivacy == PrivacyLevel.Private && !shouldShowPrivate);
|
||||||
|
|
||||||
|
await ctx.Paginate(
|
||||||
|
membersToDisplay,
|
||||||
5,
|
5,
|
||||||
embedTitle,
|
embedTitle,
|
||||||
(eb, ms) => {
|
(eb, ms) => {
|
||||||
@ -179,8 +209,16 @@ namespace PluralKit.Bot.Commands
|
|||||||
if (m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
|
if (m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
|
||||||
if (m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
|
if (m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
|
||||||
if (m.Description != null) profile += $"\n\n{m.Description}";
|
if (m.Description != null) profile += $"\n\n{m.Description}";
|
||||||
|
if (m.MemberPrivacy == PrivacyLevel.Private)
|
||||||
|
profile += "*(this member is private)*";
|
||||||
|
|
||||||
eb.AddField(m.Name, profile.Truncate(1024));
|
eb.AddField(m.Name, profile.Truncate(1024));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var footer = $"{membersToDisplay.Count} total.";
|
||||||
|
if (anyMembersHidden && authCtx == LookupContext.ByOwner)
|
||||||
|
footer += " Private members have been hidden. type \"pk;system list full all\" to include them.";
|
||||||
|
eb.WithFooter(footer);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -188,6 +226,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
public async Task SystemFronter(Context ctx, PKSystem system)
|
public async Task SystemFronter(Context ctx, PKSystem system)
|
||||||
{
|
{
|
||||||
if (system == null) throw Errors.NoSystemError;
|
if (system == null) throw Errors.NoSystemError;
|
||||||
|
ctx.CheckSystemPrivacy(system, system.FrontPrivacy);
|
||||||
|
|
||||||
var sw = await _data.GetLatestSwitch(system);
|
var sw = await _data.GetLatestSwitch(system);
|
||||||
if (sw == null) throw Errors.NoRegisteredSwitches;
|
if (sw == null) throw Errors.NoRegisteredSwitches;
|
||||||
@ -198,6 +237,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
public async Task SystemFrontHistory(Context ctx, PKSystem system)
|
public async Task SystemFrontHistory(Context ctx, PKSystem system)
|
||||||
{
|
{
|
||||||
if (system == null) throw Errors.NoSystemError;
|
if (system == null) throw Errors.NoSystemError;
|
||||||
|
ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy);
|
||||||
|
|
||||||
var sws = (await _data.GetSwitches(system, 10)).ToList();
|
var sws = (await _data.GetSwitches(system, 10)).ToList();
|
||||||
if (sws.Count == 0) throw Errors.NoRegisteredSwitches;
|
if (sws.Count == 0) throw Errors.NoRegisteredSwitches;
|
||||||
@ -208,6 +248,7 @@ namespace PluralKit.Bot.Commands
|
|||||||
public async Task SystemFrontPercent(Context ctx, PKSystem system)
|
public async Task SystemFrontPercent(Context ctx, PKSystem system)
|
||||||
{
|
{
|
||||||
if (system == null) throw Errors.NoSystemError;
|
if (system == null) throw Errors.NoSystemError;
|
||||||
|
ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy);
|
||||||
|
|
||||||
string durationStr = ctx.RemainderOrNull() ?? "30d";
|
string durationStr = ctx.RemainderOrNull() ?? "30d";
|
||||||
|
|
||||||
@ -267,6 +308,80 @@ namespace PluralKit.Bot.Commands
|
|||||||
await ctx.Reply($"System time zone changed to {zone.Id}.");
|
await ctx.Reply($"System time zone changed to {zone.Id}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SystemPrivacy(Context ctx)
|
||||||
|
{
|
||||||
|
ctx.CheckSystem();
|
||||||
|
|
||||||
|
if (!ctx.HasNext())
|
||||||
|
{
|
||||||
|
string PrivacyLevelString(PrivacyLevel level) => level switch
|
||||||
|
{
|
||||||
|
PrivacyLevel.Private => "**Private** (visible only when queried by you)",
|
||||||
|
PrivacyLevel.Public => "**Public** (visible to everyone)",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
var eb = new EmbedBuilder()
|
||||||
|
.WithTitle("Current privacy settings for your system")
|
||||||
|
.AddField("Description", PrivacyLevelString(ctx.System.DescriptionPrivacy))
|
||||||
|
.AddField("Member list", PrivacyLevelString(ctx.System.MemberListPrivacy))
|
||||||
|
.AddField("Current fronter(s)", PrivacyLevelString(ctx.System.FrontPrivacy))
|
||||||
|
.AddField("Front/switch history", PrivacyLevelString(ctx.System.FrontHistoryPrivacy))
|
||||||
|
.WithDescription("To edit privacy settings, use the command:\n`pk;system privacy <subject> <level>`\n\n- `subject` is one of `description`, `list`, `front` or `fronthistory`\n- `level` is either `public` or `private`.");
|
||||||
|
await ctx.Reply(embed: eb.Build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrivacyLevel PopPrivacyLevel(string subject, out string levelStr, out string levelExplanation)
|
||||||
|
{
|
||||||
|
if (ctx.Match("public", "show", "shown", "visible"))
|
||||||
|
{
|
||||||
|
levelStr = "public";
|
||||||
|
levelExplanation = "be able to query";
|
||||||
|
return PrivacyLevel.Public;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx.Match("private", "hide", "hidden"))
|
||||||
|
{
|
||||||
|
levelStr = "private";
|
||||||
|
levelExplanation = "*not* be able to query";
|
||||||
|
return PrivacyLevel.Private;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.HasNext())
|
||||||
|
throw new PKSyntaxError($"You must pass a privacy level for `{subject}` (`public` or `private`)");
|
||||||
|
throw new PKSyntaxError($"Invalid privacy level `{ctx.PopArgument().SanitizeMentions()}` (must be `public` or `private`).");
|
||||||
|
}
|
||||||
|
|
||||||
|
string levelStr, levelExplanation, subjectStr;
|
||||||
|
var subjectList = "`description`, `members`, `front` or `fronthistory`";
|
||||||
|
if (ctx.Match("description", "desc", "text", "info"))
|
||||||
|
{
|
||||||
|
subjectStr = "description";
|
||||||
|
ctx.System.DescriptionPrivacy = PopPrivacyLevel("description", out levelStr, out levelExplanation);
|
||||||
|
}
|
||||||
|
else if (ctx.Match("members", "memberlist", "list", "mlist"))
|
||||||
|
{
|
||||||
|
subjectStr = "member list";
|
||||||
|
ctx.System.MemberListPrivacy = PopPrivacyLevel("members", out levelStr, out levelExplanation);
|
||||||
|
}
|
||||||
|
else if (ctx.Match("front", "fronter"))
|
||||||
|
{
|
||||||
|
subjectStr = "fronter(s)";
|
||||||
|
ctx.System.FrontPrivacy = PopPrivacyLevel("front", out levelStr, out levelExplanation);
|
||||||
|
}
|
||||||
|
else if (ctx.Match("switch", "switches", "fronthistory", "fh"))
|
||||||
|
{
|
||||||
|
subjectStr = "front history";
|
||||||
|
ctx.System.FrontHistoryPrivacy = PopPrivacyLevel("fronthistory", out levelStr, out levelExplanation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw new PKSyntaxError($"Invalid privacy subject `{ctx.PopArgument().SanitizeMentions()}` (must be {subjectList}).");
|
||||||
|
|
||||||
|
await _data.SaveSystem(ctx.System);
|
||||||
|
await ctx.Reply($"System {subjectStr} privacy has been set to **{levelStr}**. Other accounts will now {levelExplanation} your system {subjectStr}.");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr) {
|
public async Task<DateTimeZone> FindTimeZone(Context ctx, string zoneStr) {
|
||||||
// First, if we're given a flag emoji, we extract the flag emoji code from it.
|
// First, if we're given a flag emoji, we extract the flag emoji code from it.
|
||||||
zoneStr = PluralKit.Utils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
zoneStr = PluralKit.Utils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
||||||
|
@ -20,13 +20,13 @@ namespace PluralKit.Bot {
|
|||||||
_data = data;
|
_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Embed> CreateSystemEmbed(PKSystem system) {
|
public async Task<Embed> CreateSystemEmbed(PKSystem system, LookupContext ctx) {
|
||||||
var accounts = await _data.GetSystemAccounts(system);
|
var accounts = await _data.GetSystemAccounts(system);
|
||||||
|
|
||||||
// Fetch/render info for all accounts simultaneously
|
// Fetch/render info for all accounts simultaneously
|
||||||
var users = await Task.WhenAll(accounts.Select(async uid => (await _client.GetUserAsync(uid))?.NameAndMention() ?? $"(deleted account {uid})"));
|
var users = await Task.WhenAll(accounts.Select(async uid => (await _client.GetUserAsync(uid))?.NameAndMention() ?? $"(deleted account {uid})"));
|
||||||
|
|
||||||
var memberCount = await _data.GetSystemMemberCount(system);
|
var memberCount = await _data.GetSystemMemberCount(system, false);
|
||||||
var eb = new EmbedBuilder()
|
var eb = new EmbedBuilder()
|
||||||
.WithColor(Color.Blue)
|
.WithColor(Color.Blue)
|
||||||
.WithTitle(system.Name ?? null)
|
.WithTitle(system.Name ?? null)
|
||||||
@ -34,7 +34,7 @@ namespace PluralKit.Bot {
|
|||||||
.WithFooter($"System ID: {system.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(system.Created.InZone(system.Zone))}");
|
.WithFooter($"System ID: {system.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(system.Created.InZone(system.Zone))}");
|
||||||
|
|
||||||
var latestSwitch = await _data.GetLatestSwitch(system);
|
var latestSwitch = await _data.GetLatestSwitch(system);
|
||||||
if (latestSwitch != null)
|
if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx))
|
||||||
{
|
{
|
||||||
var switchMembers = (await _data.GetSwitchMembers(latestSwitch)).ToList();
|
var switchMembers = (await _data.GetSwitchMembers(latestSwitch)).ToList();
|
||||||
if (switchMembers.Count > 0)
|
if (switchMembers.Count > 0)
|
||||||
@ -44,9 +44,12 @@ namespace PluralKit.Bot {
|
|||||||
|
|
||||||
if (system.Tag != null) eb.AddField("Tag", system.Tag);
|
if (system.Tag != null) eb.AddField("Tag", system.Tag);
|
||||||
eb.AddField("Linked accounts", string.Join(", ", users).Truncate(1000), true);
|
eb.AddField("Linked accounts", string.Join(", ", users).Truncate(1000), true);
|
||||||
|
|
||||||
|
if (system.MemberListPrivacy.CanAccess(ctx))
|
||||||
eb.AddField($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true);
|
eb.AddField($"Members ({memberCount})", $"(see `pk;system {system.Hid} list` or `pk;system {system.Hid} list full`)", true);
|
||||||
|
|
||||||
if (system.Description != null) eb.AddField("Description", system.Description.Truncate(1024), false);
|
if (system.Description != null && system.DescriptionPrivacy.CanAccess(ctx))
|
||||||
|
eb.AddField("Description", system.Description.Truncate(1024), false);
|
||||||
|
|
||||||
return eb.Build();
|
return eb.Build();
|
||||||
}
|
}
|
||||||
@ -62,7 +65,7 @@ namespace PluralKit.Bot {
|
|||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Embed> CreateMemberEmbed(PKSystem system, PKMember member, IGuild guild)
|
public async Task<Embed> CreateMemberEmbed(PKSystem system, PKMember member, IGuild guild, LookupContext ctx)
|
||||||
{
|
{
|
||||||
var name = member.Name;
|
var name = member.Name;
|
||||||
if (system.Name != null) name = $"{member.Name} ({system.Name})";
|
if (system.Name != null) name = $"{member.Name} ({system.Name})";
|
||||||
@ -91,19 +94,21 @@ namespace PluralKit.Bot {
|
|||||||
var eb = new EmbedBuilder()
|
var eb = new EmbedBuilder()
|
||||||
// TODO: add URL of website when that's up
|
// TODO: add URL of website when that's up
|
||||||
.WithAuthor(name, member.AvatarUrl)
|
.WithAuthor(name, member.AvatarUrl)
|
||||||
.WithColor(color)
|
.WithColor(member.MemberPrivacy.CanAccess(ctx) ? color : Color.Default)
|
||||||
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}");
|
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Created on {Formats.ZonedDateTimeFormat.Format(member.Created.InZone(system.Zone))}");
|
||||||
|
|
||||||
|
if (member.MemberPrivacy == PrivacyLevel.Private) eb.WithDescription("*(this member is private)*");
|
||||||
|
|
||||||
if (member.AvatarUrl != null) eb.WithThumbnailUrl(member.AvatarUrl);
|
if (member.AvatarUrl != null) eb.WithThumbnailUrl(member.AvatarUrl);
|
||||||
|
|
||||||
if (member.DisplayName != null) eb.AddField("Display Name", member.DisplayName.Truncate(1024), true);
|
if (member.DisplayName != null) eb.AddField("Display Name", member.DisplayName.Truncate(1024), true);
|
||||||
if (guild != null && guildDisplayName != null) eb.AddField($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true);
|
if (guild != null && guildDisplayName != null) eb.AddField($"Server Nickname (for {guild.Name})", guildDisplayName.Truncate(1024), true);
|
||||||
if (member.Birthday != null) eb.AddField("Birthdate", member.BirthdayString, true);
|
if (member.Birthday != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Birthdate", member.BirthdayString, true);
|
||||||
if (member.Pronouns != null) eb.AddField("Pronouns", member.Pronouns.Truncate(1024), true);
|
if (member.Pronouns != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Pronouns", member.Pronouns.Truncate(1024), true);
|
||||||
if (messageCount > 0) eb.AddField("Message Count", messageCount, true);
|
if (messageCount > 0 && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Message Count", messageCount, true);
|
||||||
if (member.HasProxyTags) eb.AddField("Proxy Tags", string.Join('\n', proxyTagsStr).Truncate(1024), true);
|
if (member.HasProxyTags) eb.AddField("Proxy Tags", string.Join('\n', proxyTagsStr).Truncate(1024), true);
|
||||||
if (member.Color != null) eb.AddField("Color", $"#{member.Color}", true);
|
if (member.Color != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true);
|
||||||
if (member.Description != null) eb.AddField("Description", member.Description, false);
|
if (member.Description != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Description", member.Description, false);
|
||||||
|
|
||||||
return eb.Build();
|
return eb.Build();
|
||||||
}
|
}
|
||||||
|
13
PluralKit.Core/Migrations/2.sql
Normal file
13
PluralKit.Core/Migrations/2.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
-- We're doing a psuedo-enum here since Dapper is wonky with enums
|
||||||
|
-- Still getting mapped to enums at the CLR level, though.
|
||||||
|
-- https://github.com/StackExchange/Dapper/issues/332 (from 2015, still unsolved!)
|
||||||
|
-- 1 = "public"
|
||||||
|
-- 2 = "private"
|
||||||
|
-- not doing a bool here since I want to open up for the possibliity of other privacy levels (eg. "mutuals only")
|
||||||
|
alter table systems add column description_privacy integer check (description_privacy in (1, 2)) not null default 1;
|
||||||
|
alter table systems add column member_list_privacy integer check (member_list_privacy in (1, 2)) not null default 1;
|
||||||
|
alter table systems add column front_privacy integer check (front_privacy in (1, 2)) not null default 1;
|
||||||
|
alter table systems add column front_history_privacy integer check (front_history_privacy in (1, 2)) not null default 1;
|
||||||
|
alter table members add column member_privacy integer check (member_privacy in (1, 2)) not null default 1;
|
||||||
|
|
||||||
|
update info set schema_version = 2;
|
@ -18,6 +18,25 @@ namespace PluralKit
|
|||||||
public PKParseError(string message): base(message) { }
|
public PKParseError(string message): base(message) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PrivacyLevel
|
||||||
|
{
|
||||||
|
Public = 1,
|
||||||
|
Private = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PrivacyExt
|
||||||
|
{
|
||||||
|
public static bool CanAccess(this PrivacyLevel level, LookupContext ctx) =>
|
||||||
|
level == PrivacyLevel.Public || ctx == LookupContext.ByOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LookupContext
|
||||||
|
{
|
||||||
|
ByOwner,
|
||||||
|
ByNonOwner,
|
||||||
|
API
|
||||||
|
}
|
||||||
|
|
||||||
public struct ProxyTag
|
public struct ProxyTag
|
||||||
{
|
{
|
||||||
public ProxyTag(string prefix, string suffix)
|
public ProxyTag(string prefix, string suffix)
|
||||||
@ -58,14 +77,19 @@ namespace PluralKit
|
|||||||
[JsonIgnore] public string Token { get; set; }
|
[JsonIgnore] public string Token { get; set; }
|
||||||
[JsonProperty("created")] public Instant Created { get; set; }
|
[JsonProperty("created")] public Instant Created { get; set; }
|
||||||
[JsonProperty("tz")] public string UiTz { get; set; }
|
[JsonProperty("tz")] public string UiTz { get; set; }
|
||||||
|
public PrivacyLevel DescriptionPrivacy { get; set; }
|
||||||
|
public PrivacyLevel MemberListPrivacy { get; set; }
|
||||||
|
public PrivacyLevel FrontPrivacy { get; set; }
|
||||||
|
public PrivacyLevel FrontHistoryPrivacy { get; set; }
|
||||||
|
|
||||||
[JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz);
|
[JsonIgnore] public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz);
|
||||||
|
|
||||||
public JObject ToJson()
|
public JObject ToJson(LookupContext ctx)
|
||||||
{
|
{
|
||||||
var o = new JObject();
|
var o = new JObject();
|
||||||
o.Add("id", Hid);
|
o.Add("id", Hid);
|
||||||
o.Add("name", Name);
|
o.Add("name", Name);
|
||||||
o.Add("description", Description);
|
o.Add("description", DescriptionPrivacy.CanAccess(ctx) ? Description : null);
|
||||||
o.Add("tag", Tag);
|
o.Add("tag", Tag);
|
||||||
o.Add("avatar_url", AvatarUrl);
|
o.Add("avatar_url", AvatarUrl);
|
||||||
o.Add("created", Formats.TimestampExportFormat.Format(Created));
|
o.Add("created", Formats.TimestampExportFormat.Format(Created));
|
||||||
@ -100,6 +124,8 @@ namespace PluralKit
|
|||||||
[JsonProperty("keep_proxy")] public bool KeepProxy { get; set; }
|
[JsonProperty("keep_proxy")] public bool KeepProxy { get; set; }
|
||||||
[JsonProperty("created")] public Instant Created { get; set; }
|
[JsonProperty("created")] public Instant Created { get; set; }
|
||||||
|
|
||||||
|
public PrivacyLevel MemberPrivacy { 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
|
||||||
[JsonIgnore] public string BirthdayString
|
[JsonIgnore] public string BirthdayString
|
||||||
{
|
{
|
||||||
@ -120,17 +146,17 @@ namespace PluralKit
|
|||||||
return $"{guildDisplayName ?? DisplayName ?? Name} {systemTag}";
|
return $"{guildDisplayName ?? DisplayName ?? Name} {systemTag}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public JObject ToJson()
|
public JObject ToJson(LookupContext ctx)
|
||||||
{
|
{
|
||||||
var o = new JObject();
|
var o = new JObject();
|
||||||
o.Add("id", Hid);
|
o.Add("id", Hid);
|
||||||
o.Add("name", Name);
|
o.Add("name", Name);
|
||||||
o.Add("color", Color);
|
o.Add("color", MemberPrivacy.CanAccess(ctx) ? Color : null);
|
||||||
o.Add("display_name", DisplayName);
|
o.Add("display_name", DisplayName);
|
||||||
o.Add("birthday", Birthday.HasValue ? Formats.DateExportFormat.Format(Birthday.Value) : null);
|
o.Add("birthday", MemberPrivacy.CanAccess(ctx) && Birthday.HasValue ? Formats.DateExportFormat.Format(Birthday.Value) : null);
|
||||||
o.Add("pronouns", Pronouns);
|
o.Add("pronouns", MemberPrivacy.CanAccess(ctx) ? Pronouns : null);
|
||||||
o.Add("avatar_url", AvatarUrl);
|
o.Add("avatar_url", AvatarUrl);
|
||||||
o.Add("description", Description);
|
o.Add("description", MemberPrivacy.CanAccess(ctx) ? Description : null);
|
||||||
|
|
||||||
var tagArray = new JArray();
|
var tagArray = new JArray();
|
||||||
foreach (var tag in ProxyTags)
|
foreach (var tag in ProxyTags)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Data;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
@ -11,7 +10,7 @@ using Serilog;
|
|||||||
namespace PluralKit {
|
namespace PluralKit {
|
||||||
public class SchemaService
|
public class SchemaService
|
||||||
{
|
{
|
||||||
private const int TargetSchemaVersion = 1;
|
private const int TargetSchemaVersion = 2;
|
||||||
|
|
||||||
private DbConnectionFactory _conn;
|
private DbConnectionFactory _conn;
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
@ -22,6 +21,13 @@ namespace PluralKit {
|
|||||||
_logger = logger.ForContext<SchemaService>();
|
_logger = logger.ForContext<SchemaService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
// Without these it'll still *work* but break at the first launch + probably cause other small issues
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.MapComposite<ProxyTag>("proxy_tag");
|
||||||
|
NpgsqlConnection.GlobalTypeMapper.MapEnum<PrivacyLevel>("privacy_level");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ApplyMigrations()
|
public async Task ApplyMigrations()
|
||||||
{
|
{
|
||||||
for (var version = 0; version <= TargetSchemaVersion; version++)
|
for (var version = 0; version <= TargetSchemaVersion; version++)
|
||||||
|
@ -116,7 +116,8 @@ namespace PluralKit {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the member count of a system.
|
/// Gets the member count of a system.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<int> GetSystemMemberCount(PKSystem system);
|
/// <param name="includePrivate">Whether the returned count should include private members.</param>
|
||||||
|
Task<int> GetSystemMemberCount(PKSystem system, bool includePrivate);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a list of members with proxy tags that conflict with the given tags.
|
/// Gets a list of members with proxy tags that conflict with the given tags.
|
||||||
@ -488,7 +489,7 @@ namespace PluralKit {
|
|||||||
|
|
||||||
public async Task SaveSystem(PKSystem system) {
|
public async Task SaveSystem(PKSystem system) {
|
||||||
using (var conn = await _conn.Obtain())
|
using (var conn = await _conn.Obtain())
|
||||||
await conn.ExecuteAsync("update systems set name = @Name, description = @Description, tag = @Tag, avatar_url = @AvatarUrl, token = @Token, ui_tz = @UiTz where id = @Id", system);
|
await conn.ExecuteAsync("update systems set name = @Name, description = @Description, tag = @Tag, avatar_url = @AvatarUrl, token = @Token, ui_tz = @UiTz, description_privacy = @DescriptionPrivacy, member_list_privacy = @MemberListPrivacy, front_privacy = @FrontPrivacy, front_history_privacy = @FrontHistoryPrivacy where id = @Id", system);
|
||||||
|
|
||||||
_logger.Information("Updated system {@System}", system);
|
_logger.Information("Updated system {@System}", system);
|
||||||
}
|
}
|
||||||
@ -590,7 +591,7 @@ namespace PluralKit {
|
|||||||
|
|
||||||
public async Task SaveMember(PKMember member) {
|
public async Task SaveMember(PKMember member) {
|
||||||
using (var conn = await _conn.Obtain())
|
using (var conn = await _conn.Obtain())
|
||||||
await conn.ExecuteAsync("update members set name = @Name, display_name = @DisplayName, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, proxy_tags = @ProxyTags, keep_proxy = @KeepProxy where id = @Id", member);
|
await conn.ExecuteAsync("update members set name = @Name, display_name = @DisplayName, description = @Description, color = @Color, avatar_url = @AvatarUrl, birthday = @Birthday, pronouns = @Pronouns, proxy_tags = @ProxyTags, keep_proxy = @KeepProxy, member_privacy = @MemberPrivacy where id = @Id", member);
|
||||||
|
|
||||||
_logger.Information("Updated member {@Member}", member);
|
_logger.Information("Updated member {@Member}", member);
|
||||||
}
|
}
|
||||||
@ -637,10 +638,13 @@ namespace PluralKit {
|
|||||||
new { System = system.Id });
|
new { System = system.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> GetSystemMemberCount(PKSystem system)
|
public async Task<int> GetSystemMemberCount(PKSystem system, bool includePrivate)
|
||||||
{
|
{
|
||||||
|
var query = "select count(*) from members where system = @Id";
|
||||||
|
if (includePrivate) query += " and member_privacy = 1"; // 1 = public
|
||||||
|
|
||||||
using (var conn = await _conn.Obtain())
|
using (var conn = await _conn.Obtain())
|
||||||
return await conn.ExecuteScalarAsync<int>("select count(*) from members where system = @Id", system);
|
return await conn.ExecuteScalarAsync<int>(query, system);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ulong> GetTotalMembers()
|
public async Task<ulong> GetTotalMembers()
|
||||||
|
@ -18,6 +18,11 @@ Authentication is done with a simple "system token". You can get your system tok
|
|||||||
Discord bot, either in a channel with the bot or in DMs. Then, pass this token in the `Authorization` HTTP header
|
Discord bot, either in a channel with the bot or in DMs. Then, pass this token in the `Authorization` HTTP header
|
||||||
on requests that require it. Failure to do so on endpoints that require authentication will return a `401 Unauthorized`.
|
on requests that require it. Failure to do so on endpoints that require authentication will return a `401 Unauthorized`.
|
||||||
|
|
||||||
|
Some endpoints show information that a given system may have set to private. If this is a specific field
|
||||||
|
(eg. description), the field will simply contain `null` rather than the true value. If this applies to entire endpoint
|
||||||
|
responses (eg. fronter, switches, member list), the entire request will return `403 Forbidden`. Authenticating with the
|
||||||
|
system's token (as described above) will override these privacy settings and show the full information.
|
||||||
|
|
||||||
## Models
|
## Models
|
||||||
The following three models (usually represented in JSON format) represent the various objects in PluralKit's API. A `?` after the column type indicates an optional (nullable) parameter.
|
The following three models (usually represented in JSON format) represent the various objects in PluralKit's API. A `?` after the column type indicates an optional (nullable) parameter.
|
||||||
|
|
||||||
@ -99,6 +104,7 @@ Returns information about your own system.
|
|||||||
|
|
||||||
### GET /s/\<id>
|
### GET /s/\<id>
|
||||||
Queries a system by its 5-character ID, and returns information about it. If the system doesn't exist, returns `404 Not Found`.
|
Queries a system by its 5-character ID, and returns information about it. If the system doesn't exist, returns `404 Not Found`.
|
||||||
|
Some fields may be set to `null` if unauthenticated and the system has chosen to make those fields private.
|
||||||
|
|
||||||
#### Example request
|
#### Example request
|
||||||
GET https://api.pluralkit.me/v1/s/abcde
|
GET https://api.pluralkit.me/v1/s/abcde
|
||||||
@ -118,6 +124,8 @@ Queries a system by its 5-character ID, and returns information about it. If the
|
|||||||
|
|
||||||
### GET /s/\<id>/members
|
### GET /s/\<id>/members
|
||||||
Queries a system's member list by its 5-character ID. If the system doesn't exist, returns `404 Not Found`.
|
Queries a system's member list by its 5-character ID. If the system doesn't exist, returns `404 Not Found`.
|
||||||
|
If the system has chosen to hide its member list, this will return `403 Forbidden`, unless the request is authenticated with the system's token.
|
||||||
|
If the request is not authenticated with the system's token, members marked as private will *not* be returned.
|
||||||
|
|
||||||
#### Example request
|
#### Example request
|
||||||
GET https://api.pluralkit.me/v1/s/abcde/members
|
GET https://api.pluralkit.me/v1/s/abcde/members
|
||||||
@ -145,6 +153,8 @@ Returns a system's switch history in newest-first chronological order, with a ma
|
|||||||
Optionally takes a `?before=` query parameter with an ISO-8601-formatted timestamp, and will only return switches
|
Optionally takes a `?before=` query parameter with an ISO-8601-formatted timestamp, and will only return switches
|
||||||
that happen before that timestamp.
|
that happen before that timestamp.
|
||||||
|
|
||||||
|
If the system has chosen to hide its switch history, this will return `403 Forbidden`, unless the request is authenticated with the system's token.
|
||||||
|
|
||||||
#### Example request
|
#### Example request
|
||||||
GET https://api.pluralkit.me/v1/s/abcde/switches?before=2019-03-01T14:00:00Z
|
GET https://api.pluralkit.me/v1/s/abcde/switches?before=2019-03-01T14:00:00Z
|
||||||
|
|
||||||
@ -168,6 +178,7 @@ that happen before that timestamp.
|
|||||||
|
|
||||||
### GET /s/\<id>/fronters
|
### GET /s/\<id>/fronters
|
||||||
Returns a system's current fronter(s), with fully hydrated member objects. If the system doesn't exist, *or* the system has no registered switches, returns `404 Not Found`.
|
Returns a system's current fronter(s), with fully hydrated member objects. If the system doesn't exist, *or* the system has no registered switches, returns `404 Not Found`.
|
||||||
|
If the system has chosen to hide its current fronters, this will return `403 Forbidden`, unless the request is authenticated with the system's token. If a returned member is private, and the request isn't properly authenticated, some fields may be null.
|
||||||
|
|
||||||
#### Example request
|
#### Example request
|
||||||
GET https://api.pluralkit.me/v1/s/abcde/fronters
|
GET https://api.pluralkit.me/v1/s/abcde/fronters
|
||||||
@ -243,6 +254,7 @@ Registers a new switch to your own system given a list of member IDs.
|
|||||||
|
|
||||||
### GET /m/\<id>
|
### GET /m/\<id>
|
||||||
Queries a member's information by its 5-character member ID. If the member does not exist, will return `404 Not Found`.
|
Queries a member's information by its 5-character member ID. If the member does not exist, will return `404 Not Found`.
|
||||||
|
If this member is marked private, and the request isn't authenticated with the member's system's token, some fields (currently only `description`) will contain `null` rather than the true value.
|
||||||
|
|
||||||
#### Example request
|
#### Example request
|
||||||
GET https://api.pluralkit.me/v1/m/qwert
|
GET https://api.pluralkit.me/v1/m/qwert
|
||||||
@ -354,6 +366,7 @@ Deletes a member from the database. Be careful as there is no confirmation and t
|
|||||||
|
|
||||||
### GET /a/\<id>
|
### GET /a/\<id>
|
||||||
Queries a system by its linked Discord account ID (17/18-digit numeric snowflake). Returns `404 Not Found` if the account doesn't have a system linked.
|
Queries a system by its linked Discord account ID (17/18-digit numeric snowflake). Returns `404 Not Found` if the account doesn't have a system linked.
|
||||||
|
Some fields may be set to `null` if unauthenticated and the system has chosen to make those fields private.
|
||||||
|
|
||||||
#### Example request
|
#### Example request
|
||||||
GET https://api.pluralkit.me/v1/a/466378653216014359
|
GET https://api.pluralkit.me/v1/a/466378653216014359
|
||||||
@ -375,6 +388,8 @@ Queries a system by its linked Discord account ID (17/18-digit numeric snowflake
|
|||||||
Looks up a proxied message by its message ID. Returns `404 Not Found` if the message ID is invalid or wasn't found (eg. was deleted or not proxied by PK).
|
Looks up a proxied message by its message ID. Returns `404 Not Found` if the message ID is invalid or wasn't found (eg. was deleted or not proxied by PK).
|
||||||
You can also look messages up by their *trigger* message ID (useful for, say, logging bot integration).
|
You can also look messages up by their *trigger* message ID (useful for, say, logging bot integration).
|
||||||
|
|
||||||
|
The returned system and member's privacy settings will be respected, and as such, some fields may be set to null without the proper authentication.
|
||||||
|
|
||||||
#### Example request
|
#### Example request
|
||||||
GET https://api.pluralkit.me/v1/msg/601014599386398700
|
GET https://api.pluralkit.me/v1/msg/601014599386398700
|
||||||
|
|
||||||
@ -411,6 +426,8 @@ You can also look messages up by their *trigger* message ID (useful for, say, lo
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Version history
|
## Version history
|
||||||
|
* 2020-01-08
|
||||||
|
* Added privacy support, meaning some responses will now lack information or return 403s, depending on the specific system and member's privacy settings.
|
||||||
* 2019-12-28
|
* 2019-12-28
|
||||||
* Changed behaviour of missing fields in PATCH responses, will now preserve the old value instead of clearing
|
* Changed behaviour of missing fields in PATCH responses, will now preserve the old value instead of clearing
|
||||||
* This is technically a breaking change, but not *significantly* so, so I won't bump the version number.
|
* This is technically a breaking change, but not *significantly* so, so I won't bump the version number.
|
||||||
|
Loading…
Reference in New Issue
Block a user