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
|
||||
{
|
||||
private IDataStore _data;
|
||||
private TokenAuthService _auth;
|
||||
|
||||
public AccountController(IDataStore data)
|
||||
public AccountController(IDataStore data, TokenAuthService auth)
|
||||
{
|
||||
_data = data;
|
||||
_auth = auth;
|
||||
}
|
||||
|
||||
[HttpGet("{aid}")]
|
||||
@ -23,7 +25,7 @@ namespace PluralKit.API.Controllers
|
||||
var system = await _data.GetSystemByAccount(aid);
|
||||
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);
|
||||
if (member == null) return NotFound("Member not found.");
|
||||
|
||||
return Ok(member.ToJson());
|
||||
return Ok(member.ToJson(_auth.ContextFor(member)));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -41,7 +41,7 @@ namespace PluralKit.API.Controllers
|
||||
return BadRequest("Member name must be specified.");
|
||||
|
||||
// Enforce per-system member limit
|
||||
var memberCount = await _data.GetSystemMemberCount(system);
|
||||
var memberCount = await _data.GetSystemMemberCount(system, true);
|
||||
if (memberCount >= Limits.MaxMemberCount)
|
||||
return BadRequest($"Member limit reached ({memberCount} / {Limits.MaxMemberCount}).");
|
||||
|
||||
@ -56,7 +56,7 @@ namespace PluralKit.API.Controllers
|
||||
}
|
||||
|
||||
await _data.SaveMember(member);
|
||||
return Ok(member.ToJson());
|
||||
return Ok(member.ToJson(_auth.ContextFor(member)));
|
||||
}
|
||||
|
||||
[HttpPatch("{hid}")]
|
||||
@ -78,7 +78,7 @@ namespace PluralKit.API.Controllers
|
||||
}
|
||||
|
||||
await _data.SaveMember(member);
|
||||
return Ok(member.ToJson());
|
||||
return Ok(member.ToJson(_auth.ContextFor(member)));
|
||||
}
|
||||
|
||||
[HttpDelete("{hid}")]
|
||||
|
@ -25,10 +25,12 @@ namespace PluralKit.API.Controllers
|
||||
public class MessageController: ControllerBase
|
||||
{
|
||||
private IDataStore _data;
|
||||
private TokenAuthService _auth;
|
||||
|
||||
public MessageController(IDataStore _data)
|
||||
public MessageController(IDataStore _data, TokenAuthService auth)
|
||||
{
|
||||
this._data = _data;
|
||||
_auth = auth;
|
||||
}
|
||||
|
||||
[HttpGet("{mid}")]
|
||||
@ -43,8 +45,8 @@ namespace PluralKit.API.Controllers
|
||||
Id = msg.Message.Mid.ToString(),
|
||||
Channel = msg.Message.Channel.ToString(),
|
||||
Sender = msg.Message.Sender.ToString(),
|
||||
Member = msg.Member.ToJson(),
|
||||
System = msg.System.ToJson(),
|
||||
Member = msg.Member.ToJson(_auth.ContextFor(msg.System)),
|
||||
System = msg.System.ToJson(_auth.ContextFor(msg.System)),
|
||||
Original = msg.Message.OriginalMid?.ToString()
|
||||
};
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -48,7 +50,7 @@ namespace PluralKit.API.Controllers
|
||||
[RequiresSystem]
|
||||
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}")]
|
||||
@ -56,7 +58,7 @@ namespace PluralKit.API.Controllers
|
||||
{
|
||||
var system = await _data.GetSystemByHid(hid);
|
||||
if (system == null) return NotFound("System not found.");
|
||||
return Ok(system.ToJson());
|
||||
return Ok(system.ToJson(_auth.ContextFor(system)));
|
||||
}
|
||||
|
||||
[HttpGet("{hid}/members")]
|
||||
@ -65,8 +67,13 @@ namespace PluralKit.API.Controllers
|
||||
var system = await _data.GetSystemByHid(hid);
|
||||
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);
|
||||
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")]
|
||||
@ -77,6 +84,9 @@ namespace PluralKit.API.Controllers
|
||||
var system = await _data.GetSystemByHid(hid);
|
||||
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())
|
||||
{
|
||||
var res = await conn.QueryAsync<SwitchesReturn>(
|
||||
@ -97,6 +107,9 @@ namespace PluralKit.API.Controllers
|
||||
var system = await _data.GetSystemByHid(hid);
|
||||
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);
|
||||
if (sw == null) return NotFound("System has no registered switches.");
|
||||
|
||||
@ -104,7 +117,7 @@ namespace PluralKit.API.Controllers
|
||||
return Ok(new FrontersReturn
|
||||
{
|
||||
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);
|
||||
return Ok(system.ToJson());
|
||||
return Ok(system.ToJson(_auth.ContextFor(system)));
|
||||
}
|
||||
|
||||
[HttpPost("switches")]
|
||||
|
@ -26,6 +26,7 @@ namespace PluralKit.API
|
||||
|
||||
services
|
||||
.AddTransient<IDataStore, PostgresDataStore>()
|
||||
.AddSingleton<SchemaService>()
|
||||
|
||||
.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.
|
||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||
{
|
||||
SchemaService.Initialize();
|
||||
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
@ -26,5 +26,11 @@ namespace PluralKit.API
|
||||
await next.Invoke(context);
|
||||
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())
|
||||
{
|
||||
SchemaService.Initialize();
|
||||
|
||||
var logger = services.GetRequiredService<ILogger>().ForContext<Initialize>();
|
||||
var coreConfig = services.GetRequiredService<CoreConfig>();
|
||||
var botConfig = services.GetRequiredService<BotConfig>();
|
||||
|
@ -57,13 +57,13 @@ namespace PluralKit.Bot.CommandSystem
|
||||
/// <summary>
|
||||
/// Checks if the next parameter is equal to one of the given keywords. Case-insensitive.
|
||||
/// </summary>
|
||||
public bool Match(params string[] potentialMatches)
|
||||
public bool Match(ref string used, params string[] potentialMatches)
|
||||
{
|
||||
foreach (var match in potentialMatches)
|
||||
{
|
||||
if (PeekArgument().Equals(match, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
PopArgument();
|
||||
used = PopArgument();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -71,6 +71,15 @@ namespace PluralKit.Bot.CommandSystem
|
||||
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)
|
||||
{
|
||||
_currentCommand = commandDef;
|
||||
@ -237,6 +246,15 @@ namespace PluralKit.Bot.CommandSystem
|
||||
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()
|
||||
{
|
||||
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 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 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 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");
|
||||
@ -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 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 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 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");
|
||||
@ -58,7 +60,7 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
public static Command[] SystemCommands = {
|
||||
SystemInfo, SystemNew, SystemRename, SystemTag, SystemDesc, SystemAvatar, SystemDelete, SystemTimezone,
|
||||
SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent
|
||||
SystemList, SystemFronter, SystemFrontHistory, SystemFrontPercent, SystemPrivacy
|
||||
};
|
||||
|
||||
public static Command[] MemberCommands = {
|
||||
@ -185,6 +187,8 @@ namespace PluralKit.Bot.Commands
|
||||
await ctx.Execute<SystemCommands>(SystemFrontHistory, m => m.SystemFrontHistory(ctx, ctx.System));
|
||||
else if (ctx.Match("fp", "frontpercent", "front%", "frontbreakdown"))
|
||||
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"))
|
||||
await PrintCommandList(ctx, "systems", SystemCommands);
|
||||
else if (!ctx.HasNext()) // Bare command
|
||||
@ -272,6 +276,8 @@ namespace PluralKit.Bot.Commands
|
||||
await ctx.Execute<MemberCommands>(MemberServerName, m => m.MemberServerName(ctx, target));
|
||||
else if (ctx.Match("keepproxy", "keeptags", "showtags"))
|
||||
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
|
||||
await ctx.Execute<MemberCommands>(MemberInfo, m => m.ViewMember(ctx, target));
|
||||
else
|
||||
|
@ -38,7 +38,7 @@ namespace PluralKit.Bot.Commands
|
||||
}
|
||||
|
||||
// Enforce per-system member limit
|
||||
var memberCount = await _data.GetSystemMemberCount(ctx.System);
|
||||
var memberCount = await _data.GetSystemMemberCount(ctx.System, true);
|
||||
if (memberCount >= Limits.MaxMemberCount)
|
||||
throw Errors.MemberLimitReachedError;
|
||||
|
||||
@ -69,7 +69,7 @@ namespace PluralKit.Bot.Commands
|
||||
if (members == null || !members.Any())
|
||||
throw Errors.NoMembersError;
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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) {
|
||||
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)
|
||||
@ -149,27 +149,57 @@ namespace PluralKit.Bot.Commands
|
||||
|
||||
public async Task MemberShortList(Context ctx, PKSystem system) {
|
||||
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}`";
|
||||
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,
|
||||
embedTitle,
|
||||
(eb, ms) => eb.Description = string.Join("\n", ms.Select((m) => {
|
||||
if (m.HasProxyTags) return $"[`{m.Hid}`] **{m.Name.SanitizeMentions()}** *({m.ProxyTagsString().SanitizeMentions()})*";
|
||||
(eb, ms) =>
|
||||
{
|
||||
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()}**";
|
||||
}))
|
||||
);
|
||||
}));
|
||||
|
||||
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) {
|
||||
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}`";
|
||||
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,
|
||||
embedTitle,
|
||||
(eb, ms) => {
|
||||
@ -179,8 +209,16 @@ namespace PluralKit.Bot.Commands
|
||||
if (m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
|
||||
if (m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
|
||||
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));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
ctx.CheckSystemPrivacy(system, system.FrontPrivacy);
|
||||
|
||||
var sw = await _data.GetLatestSwitch(system);
|
||||
if (sw == null) throw Errors.NoRegisteredSwitches;
|
||||
@ -198,6 +237,7 @@ namespace PluralKit.Bot.Commands
|
||||
public async Task SystemFrontHistory(Context ctx, PKSystem system)
|
||||
{
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy);
|
||||
|
||||
var sws = (await _data.GetSwitches(system, 10)).ToList();
|
||||
if (sws.Count == 0) throw Errors.NoRegisteredSwitches;
|
||||
@ -208,6 +248,7 @@ namespace PluralKit.Bot.Commands
|
||||
public async Task SystemFrontPercent(Context ctx, PKSystem system)
|
||||
{
|
||||
if (system == null) throw Errors.NoSystemError;
|
||||
ctx.CheckSystemPrivacy(system, system.FrontHistoryPrivacy);
|
||||
|
||||
string durationStr = ctx.RemainderOrNull() ?? "30d";
|
||||
|
||||
@ -267,6 +308,80 @@ namespace PluralKit.Bot.Commands
|
||||
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) {
|
||||
// First, if we're given a flag emoji, we extract the flag emoji code from it.
|
||||
zoneStr = PluralKit.Utils.ExtractCountryFlag(zoneStr) ?? zoneStr;
|
||||
|
@ -20,13 +20,13 @@ namespace PluralKit.Bot {
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public async Task<Embed> CreateSystemEmbed(PKSystem system) {
|
||||
public async Task<Embed> CreateSystemEmbed(PKSystem system, LookupContext ctx) {
|
||||
var accounts = await _data.GetSystemAccounts(system);
|
||||
|
||||
// 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 memberCount = await _data.GetSystemMemberCount(system);
|
||||
var memberCount = await _data.GetSystemMemberCount(system, false);
|
||||
var eb = new EmbedBuilder()
|
||||
.WithColor(Color.Blue)
|
||||
.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))}");
|
||||
|
||||
var latestSwitch = await _data.GetLatestSwitch(system);
|
||||
if (latestSwitch != null)
|
||||
if (latestSwitch != null && system.FrontPrivacy.CanAccess(ctx))
|
||||
{
|
||||
var switchMembers = (await _data.GetSwitchMembers(latestSwitch)).ToList();
|
||||
if (switchMembers.Count > 0)
|
||||
@ -44,9 +44,12 @@ namespace PluralKit.Bot {
|
||||
|
||||
if (system.Tag != null) eb.AddField("Tag", system.Tag);
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
@ -62,7 +65,7 @@ namespace PluralKit.Bot {
|
||||
.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;
|
||||
if (system.Name != null) name = $"{member.Name} ({system.Name})";
|
||||
@ -91,19 +94,21 @@ namespace PluralKit.Bot {
|
||||
var eb = new EmbedBuilder()
|
||||
// TODO: add URL of website when that's up
|
||||
.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))}");
|
||||
|
||||
if (member.MemberPrivacy == PrivacyLevel.Private) eb.WithDescription("*(this member is private)*");
|
||||
|
||||
if (member.AvatarUrl != null) eb.WithThumbnailUrl(member.AvatarUrl);
|
||||
|
||||
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 (member.Birthday != null) eb.AddField("Birthdate", member.BirthdayString, true);
|
||||
if (member.Pronouns != null) eb.AddField("Pronouns", member.Pronouns.Truncate(1024), true);
|
||||
if (messageCount > 0) eb.AddField("Message Count", messageCount, true);
|
||||
if (member.Birthday != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Birthdate", member.BirthdayString, true);
|
||||
if (member.Pronouns != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Pronouns", member.Pronouns.Truncate(1024), 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.Color != null) eb.AddField("Color", $"#{member.Color}", true);
|
||||
if (member.Description != null) eb.AddField("Description", member.Description, false);
|
||||
if (member.Color != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Color", $"#{member.Color}", true);
|
||||
if (member.Description != null && member.MemberPrivacy.CanAccess(ctx)) eb.AddField("Description", member.Description, false);
|
||||
|
||||
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 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 ProxyTag(string prefix, string suffix)
|
||||
@ -58,14 +77,19 @@ namespace PluralKit
|
||||
[JsonIgnore] public string Token { get; set; }
|
||||
[JsonProperty("created")] public Instant Created { 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);
|
||||
|
||||
public JObject ToJson()
|
||||
public JObject ToJson(LookupContext ctx)
|
||||
{
|
||||
var o = new JObject();
|
||||
o.Add("id", Hid);
|
||||
o.Add("name", Name);
|
||||
o.Add("description", Description);
|
||||
o.Add("description", DescriptionPrivacy.CanAccess(ctx) ? Description : null);
|
||||
o.Add("tag", Tag);
|
||||
o.Add("avatar_url", AvatarUrl);
|
||||
o.Add("created", Formats.TimestampExportFormat.Format(Created));
|
||||
@ -100,6 +124,8 @@ namespace PluralKit
|
||||
[JsonProperty("keep_proxy")] public bool KeepProxy { 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
|
||||
[JsonIgnore] public string BirthdayString
|
||||
{
|
||||
@ -120,17 +146,17 @@ namespace PluralKit
|
||||
return $"{guildDisplayName ?? DisplayName ?? Name} {systemTag}";
|
||||
}
|
||||
|
||||
public JObject ToJson()
|
||||
public JObject ToJson(LookupContext ctx)
|
||||
{
|
||||
var o = new JObject();
|
||||
o.Add("id", Hid);
|
||||
o.Add("name", Name);
|
||||
o.Add("color", Color);
|
||||
o.Add("color", MemberPrivacy.CanAccess(ctx) ? Color : null);
|
||||
o.Add("display_name", DisplayName);
|
||||
o.Add("birthday", Birthday.HasValue ? Formats.DateExportFormat.Format(Birthday.Value) : null);
|
||||
o.Add("pronouns", Pronouns);
|
||||
o.Add("birthday", MemberPrivacy.CanAccess(ctx) && Birthday.HasValue ? Formats.DateExportFormat.Format(Birthday.Value) : null);
|
||||
o.Add("pronouns", MemberPrivacy.CanAccess(ctx) ? Pronouns : null);
|
||||
o.Add("avatar_url", AvatarUrl);
|
||||
o.Add("description", Description);
|
||||
o.Add("description", MemberPrivacy.CanAccess(ctx) ? Description : null);
|
||||
|
||||
var tagArray = new JArray();
|
||||
foreach (var tag in ProxyTags)
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
@ -11,7 +10,7 @@ using Serilog;
|
||||
namespace PluralKit {
|
||||
public class SchemaService
|
||||
{
|
||||
private const int TargetSchemaVersion = 1;
|
||||
private const int TargetSchemaVersion = 2;
|
||||
|
||||
private DbConnectionFactory _conn;
|
||||
private ILogger _logger;
|
||||
@ -22,6 +21,13 @@ namespace PluralKit {
|
||||
_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()
|
||||
{
|
||||
for (var version = 0; version <= TargetSchemaVersion; version++)
|
||||
|
@ -116,7 +116,8 @@ namespace PluralKit {
|
||||
/// <summary>
|
||||
/// Gets the member count of a system.
|
||||
/// </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>
|
||||
/// 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) {
|
||||
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);
|
||||
}
|
||||
@ -590,7 +591,7 @@ namespace PluralKit {
|
||||
|
||||
public async Task SaveMember(PKMember member) {
|
||||
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);
|
||||
}
|
||||
@ -637,10 +638,13 @@ namespace PluralKit {
|
||||
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())
|
||||
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()
|
||||
|
@ -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
|
||||
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
|
||||
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>
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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>
|
||||
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
|
||||
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>
|
||||
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
|
||||
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).
|
||||
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
|
||||
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
|
||||
* 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
|
||||
* 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.
|
||||
|
Loading…
Reference in New Issue
Block a user