public async Task <IActionResult> DoMemberPatch(string memberRef, [FromBody] JObject data) { var system = await ResolveSystem("@me"); var member = await ResolveMember(memberRef); if (member == null) { throw Errors.MemberNotFound; } if (member.System != system.Id) { throw Errors.NotOwnMemberError; } var patch = MemberPatch.FromJSON(data); patch.AssertIsValid(); if (patch.Errors.Count > 0) { throw new ModelParseError(patch.Errors); } var newMember = await _repo.UpdateMember(member.Id, patch); return(Ok(newMember.ToJson(LookupContext.ByOwner))); }
public async Task Name(Context ctx, PKMember target) { // TODO: this method is pretty much a 1:1 copy/paste of the above creation method, find a way to clean? if (ctx.System == null) { throw Errors.NoSystemError; } if (target.System != ctx.System.Id) { throw Errors.NotOwnMemberError; } var newName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a new name for the member."); // Hard name length cap if (newName.Length > Limits.MaxMemberNameLength) { throw Errors.MemberNameTooLongError(newName.Length); } // Warn if there's already a member by this name var existingMember = await _db.Execute(conn => _repo.GetMemberByName(conn, ctx.System.Id, newName)); if (existingMember != null && existingMember.Id != target.Id) { var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (`{existingMember.Hid}`). Do you want to rename this member to that name too?"; if (!await ctx.PromptYesNo(msg)) { throw new PKError("Member renaming cancelled."); } } // Rename the member var patch = new MemberPatch { Name = Partial <string> .Present(newName) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member renamed."); if (newName.Contains(" ")) { await ctx.Reply($"{Emojis.Note} Note that this member's name now contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it."); } if (target.DisplayName != null) { await ctx.Reply($"{Emojis.Note} Note that this member has a display name set ({target.DisplayName}), and will be proxied using that name instead."); } if (ctx.Guild != null) { var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); if (memberGuildConfig.DisplayName != null) { await ctx.Reply($"{Emojis.Note} Note that this member has a server name set ({memberGuildConfig.DisplayName}) in this server ({ctx.Guild.Name}), and will be proxied using that name here."); } } }
public static MemberPatch WithAllPrivacy(this MemberPatch member, PrivacyLevel level) { foreach (var subject in Enum.GetValues(typeof(MemberPrivacySubject))) { member.WithPrivacy((MemberPrivacySubject)subject, level); } return(member); }
public async Task Pronouns(Context ctx, PKMember target) { if (await ctx.MatchClear("this member's pronouns")) { ctx.CheckOwnMember(target); var patch = new MemberPatch { Pronouns = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member pronouns cleared."); } else if (!ctx.HasNext()) { if (!target.PronounPrivacy.CanAccess(ctx.LookupContextFor(target.System))) { throw Errors.LookupNotAllowed; } if (target.Pronouns == null) { if (ctx.System?.Id == target.System) { await ctx.Reply($"This member does not have pronouns set. To set some, type `pk;member {target.Reference()} pronouns <pronouns>`."); } else { await ctx.Reply("This member does not have pronouns set."); } } else if (ctx.MatchFlag("r", "raw")) { await ctx.Reply($"```\n{target.Pronouns}\n```"); } else { await ctx.Reply($"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference()} pronouns -raw`." + (ctx.System?.Id == target.System ? $" To clear them, type `pk;member {target.Reference()} pronouns -clear`." : "")); } } else { ctx.CheckOwnMember(target); var pronouns = ctx.RemainderOrNull().NormalizeLineEndSpacing(); if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) { throw Errors.MemberPronounsTooLongError(pronouns.Length); } var patch = new MemberPatch { Pronouns = Partial <string> .Present(pronouns) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member pronouns changed."); } }
private async Task ImportMember(JObject member) { var id = member.Value <string>("id"); var name = member.Value <string>("name"); var(found, isHidExisting) = TryGetExistingMember(id, name); var isNewMember = found == null; var referenceName = isHidExisting ? id : name; if (isNewMember) { _result.Added++; } else { _result.Modified++; } _logger.Debug( "Importing member with identifier {FileId} to system {System} (is creating new member? {IsCreatingNewMember})", referenceName, _system.Id, isNewMember ); var patch = MemberPatch.FromJSON(member, isImport: true); patch.AssertIsValid(); if (patch.Errors.Count > 0) { var err = patch.Errors[0]; if (err is FieldTooLongError) { throw new ImportException($"Field {err.Key} in member {name} is too long " + $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength})."); } if (err.Text != null) { throw new ImportException($"member {name}: {err.Text}"); } throw new ImportException($"Field {err.Key} in member {name} is invalid."); } var memberId = found; if (isNewMember) { patch.MessageCount = member.Value <int>("message_count"); var newMember = await _repo.CreateMember(_system.Id, patch.Name.Value, _conn); memberId = newMember.Id; } _knownMemberIdentifiers[id] = memberId.Value; await _repo.UpdateMember(memberId.Value, patch, _conn); }
public async Task DisplayName(Context ctx, PKMember target) { async Task PrintSuccess(string text) { var successStr = text; if (ctx.Guild != null) { var memberGuildConfig = await _db.Execute(c => _repo.GetMemberGuild(c, ctx.Guild.Id, target.Id)); if (memberGuildConfig.DisplayName != null) { successStr += $" However, this member has a server name set in this server ({ctx.Guild.Name}), and will be proxied using that name, \"{memberGuildConfig.DisplayName}\", here."; } } await ctx.Reply(successStr); } if (await ctx.MatchClear("this member's display name")) { ctx.CheckOwnMember(target); var patch = new MemberPatch { DisplayName = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await PrintSuccess($"{Emojis.Success} Member display name cleared. This member will now be proxied using their member name \"{target.NameFor(ctx)}\"."); } else if (!ctx.HasNext()) { // No perms check, display name isn't covered by member privacy var eb = await CreateMemberNameInfoEmbed(ctx, target); if (ctx.System?.Id == target.System) { eb.Description($"To change display name, type `pk;member {target.Reference()} displayname <display name>`.\nTo clear it, type `pk;member {target.Reference()} displayname -clear`."); } await ctx.Reply(embed : eb.Build()); } else { ctx.CheckOwnMember(target); var newDisplayName = ctx.RemainderOrNull(); var patch = new MemberPatch { DisplayName = Partial <string> .Present(newDisplayName) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await PrintSuccess($"{Emojis.Success} Member display name changed. This member will now be proxied using the name \"{newDisplayName}\"."); } }
public async Task <ActionResult <JObject> > PostMember([FromBody] JObject properties) { if (!properties.ContainsKey("name")) { return(BadRequest("Member name must be specified.")); } var systemId = User.CurrentSystem(); var config = await _repo.GetSystemConfig(systemId); await using var conn = await _db.Obtain(); // Enforce per-system member limit var memberCount = await conn.QuerySingleAsync <int>("select count(*) from members where system = @System", new { System = systemId }); var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; if (memberCount >= memberLimit) { return(BadRequest($"Member limit reached ({memberCount} / {memberLimit}).")); } await using var tx = await conn.BeginTransactionAsync(); var member = await _repo.CreateMember(systemId, properties.Value <string>("name"), conn); var patch = MemberPatch.FromJSON(properties); patch.AssertIsValid(); if (patch.Errors.Count > 0) { await tx.RollbackAsync(); var err = patch.Errors[0]; if (err is FieldTooLongError) { return(BadRequest($"Field {err.Key} is too long " + $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).")); } if (err.Text != null) { return(BadRequest(err.Text)); } return(BadRequest($"Field {err.Key} is invalid.")); } member = await _repo.UpdateMember(member.Id, patch, conn); await tx.CommitAsync(); return(Ok(member.ToJson(User.ContextFor(member), true))); }
public async Task KeepProxy(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("on", "enabled", "true", "yes")) { newValue = true; } else if (ctx.Match("off", "disabled", "false", "no")) { newValue = false; } else if (ctx.HasNext()) { throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); } else { if (target.KeepProxy) { await ctx.Reply("This member has keepproxy **enabled**, which means proxy tags will be **included** in the resulting message when proxying."); } else { await ctx.Reply("This member has keepproxy **disabled**, which means proxy tags will **not** be included in the resulting message when proxying."); } return; }; var patch = new MemberPatch { KeepProxy = Partial <bool> .Present(newValue) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); if (newValue) { await ctx.Reply($"{Emojis.Success} Member proxy tags will now be included in the resulting message when proxying."); } else { await ctx.Reply($"{Emojis.Success} Member proxy tags will now not be included in the resulting message when proxying."); } }
public async Task MemberAutoproxy(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("on", "enabled", "true", "yes") || ctx.MatchFlag("on", "enabled", "true", "yes")) { newValue = true; } else if (ctx.Match("off", "disabled", "false", "no") || ctx.MatchFlag("off", "disabled", "false", "no")) { newValue = false; } else if (ctx.HasNext()) { throw new PKSyntaxError("You must pass either \"on\" or \"off\"."); } else { if (target.AllowAutoproxy) { await ctx.Reply("Latch/front autoproxy are **enabled** for this member. This member will be automatically proxied when autoproxy is set to latch or front mode."); } else { await ctx.Reply("Latch/front autoproxy are **disabled** for this member. This member will not be automatically proxied when autoproxy is set to latch or front mode."); } return; }; var patch = new MemberPatch { AllowAutoproxy = Partial <bool> .Present(newValue) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); if (newValue) { await ctx.Reply($"{Emojis.Success} Latch / front autoproxy have been **enabled** for this member."); } else { await ctx.Reply($"{Emojis.Success} Latch / front autoproxy have been **disabled** for this member."); } }
public async Task Birthday(Context ctx, PKMember target) { if (await ctx.MatchClear("this member's birthday")) { ctx.CheckOwnMember(target); var patch = new MemberPatch { Birthday = Partial <LocalDate?> .Null() }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member birthdate cleared."); } else if (!ctx.HasNext()) { if (!target.BirthdayPrivacy.CanAccess(ctx.LookupContextFor(target.System))) { throw Errors.LookupNotAllowed; } if (target.Birthday == null) { await ctx.Reply("This member does not have a birthdate set." + (ctx.System?.Id == target.System ? $" To set one, type `pk;member {target.Reference()} birthdate <birthdate>`." : "")); } else { await ctx.Reply($"This member's birthdate is **{target.BirthdayString}**." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} birthdate -clear`." : "")); } } else { ctx.CheckOwnMember(target); var birthdayStr = ctx.RemainderOrNull(); var birthday = DateUtils.ParseDate(birthdayStr, true); if (birthday == null) { throw Errors.BirthdayParseError(birthdayStr); } var patch = new MemberPatch { Birthday = Partial <LocalDate?> .Present(birthday) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member birthdate changed."); } }
public Task <PKMember> UpdateMember(MemberId id, MemberPatch patch, IPKConnection?conn = null) { _logger.Information("Updated {MemberId}: {@MemberPatch}", id, patch); var query = patch.Apply(new Query("members").Where("id", id)); if (conn == null) { _ = _dispatch.Dispatch(id, new UpdateDispatchData { Event = DispatchEvent.UPDATE_MEMBER, EventData = patch.ToJson() }); } return(_db.QueryFirst <PKMember>(conn, query, "returning *")); }
public static MemberPatch WithPrivacy(this MemberPatch member, MemberPrivacySubject subject, PrivacyLevel level) { // what do you mean switch expressions can't be statements >.> _ = subject switch { MemberPrivacySubject.Name => member.NamePrivacy = level, MemberPrivacySubject.Description => member.DescriptionPrivacy = level, MemberPrivacySubject.Avatar => member.AvatarPrivacy = level, MemberPrivacySubject.Pronouns => member.PronounPrivacy = level, MemberPrivacySubject.Birthday => member.BirthdayPrivacy = level, MemberPrivacySubject.Metadata => member.MetadataPrivacy = level, MemberPrivacySubject.Visibility => member.Visibility = level, _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") }; return(member); }
public async Task <IActionResult> MemberCreate([FromBody] JObject data) { var system = await ResolveSystem("@me"); var config = await _repo.GetSystemConfig(system.Id); var memberCount = await _repo.GetSystemMemberCount(system.Id); var memberLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; if (memberCount >= memberLimit) { throw Errors.MemberLimitReached; } var patch = MemberPatch.FromJSON(data); patch.AssertIsValid(); if (!patch.Name.IsPresent) { patch.Errors.Add(new ValidationError("name", "Key 'name' is required when creating new member.")); } if (patch.Errors.Count > 0) { throw new ModelParseError(patch.Errors); } using var conn = await _db.Obtain(); using var tx = await conn.BeginTransactionAsync(); var newMember = await _repo.CreateMember(system.Id, patch.Name.Value, conn); newMember = await _repo.UpdateMember(newMember.Id, patch, conn); _ = _dispatch.Dispatch(newMember.Id, new() { Event = DispatchEvent.CREATE_MEMBER, EventData = patch.ToJson(), }); await tx.CommitAsync(); return(Ok(newMember.ToJson(LookupContext.ByOwner))); }
private Task UpdateAvatar(AvatarLocation location, Context ctx, PKMember target, string?url) { switch (location) { case AvatarLocation.Server: var serverPatch = new MemberGuildPatch { AvatarUrl = url }; return(_db.Execute(c => c.UpsertMemberGuild(target.Id, ctx.Guild.Id, serverPatch))); case AvatarLocation.Member: var memberPatch = new MemberPatch { AvatarUrl = url }; return(_db.Execute(c => c.UpdateMember(target.Id, memberPatch))); default: throw new ArgumentOutOfRangeException($"Unknown avatar location {location}"); } }
public async Task <ActionResult <JObject> > PatchMember(string hid, [FromBody] JObject changes) { var member = await _repo.GetMemberByHid(hid); if (member == null) { return(NotFound("Member not found.")); } var res = await _auth.AuthorizeAsync(User, member, "EditMember"); if (!res.Succeeded) { return(StatusCode(StatusCodes.Status403Forbidden, $"Member '{hid}' is not part of your system.")); } var patch = MemberPatch.FromJSON(changes); patch.AssertIsValid(); if (patch.Errors.Count > 0) { var err = patch.Errors[0]; if (err is FieldTooLongError) { return(BadRequest($"Field {err.Key} is too long " + $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength}).")); } if (err.Text != null) { return(BadRequest(err.Text)); } return(BadRequest($"Field {err.Key} is invalid.")); } var newMember = await _repo.UpdateMember(member.Id, patch); return(Ok(newMember.ToJson(User.ContextFor(newMember), true))); }
public async Task <IActionResult> BulkMemberPrivacy([FromBody] JObject inner) { HttpContext.Items.TryGetValue("SystemId", out var systemId); if (systemId == null) { throw Errors.GenericAuthError; } var data = new JObject(); data.Add("privacy", inner); var patch = MemberPatch.FromJSON(data); patch.AssertIsValid(); if (patch.Errors.Count > 0) { throw new ModelParseError(patch.Errors); } await _db.ExecuteQuery(patch.Apply(new Query("members").Where("system", systemId))); return(NoContent()); }
public async Task Description(Context ctx, PKMember target) { if (await ctx.MatchClear("this member's description")) { ctx.CheckOwnMember(target); var patch = new MemberPatch { Description = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member description cleared."); } else if (!ctx.HasNext()) { if (!target.DescriptionPrivacy.CanAccess(ctx.LookupContextFor(target.System))) { throw Errors.LookupNotAllowed; } if (target.Description == null) { if (ctx.System?.Id == target.System) { await ctx.Reply($"This member does not have a description set. To set one, type `pk;member {target.Reference()} description <description>`."); } else { await ctx.Reply("This member does not have a description set."); } } else if (ctx.MatchFlag("r", "raw")) { await ctx.Reply($"```\n{target.Description}\n```"); } else { await ctx.Reply(embed : new EmbedBuilder() .Title("Member description") .Description(target.Description) .Field(new("\u200B", $"To print the description with formatting, type `pk;member {target.Reference()} description -raw`." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} description -clear`." : ""))) .Build()); } } else { ctx.CheckOwnMember(target); var description = ctx.RemainderOrNull().NormalizeLineEndSpacing(); if (description.IsLongerThan(Limits.MaxDescriptionLength)) { throw Errors.DescriptionTooLongError(description.Length); } var patch = new MemberPatch { Description = Partial <string> .Present(description) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member description changed."); } }
private async Task <(string lastSetTag, bool multipleTags, bool hasGroup)> ImportTupper( JObject tupper, string lastSetTag) { if (!tupper.ContainsKey("name") || tupper["name"].Type == JTokenType.Null) { throw new ImportException("Field 'name' cannot be null."); } var hasGroup = tupper.ContainsKey("group_id") && tupper["group_id"].Type != JTokenType.Null; var multipleTags = false; var name = tupper.Value <string>("name"); var isNewMember = false; if (!_existingMemberNames.TryGetValue(name, out var memberId)) { var newMember = await _repo.CreateMember(_system.Id, name, _conn); memberId = newMember.Id; isNewMember = true; _result.Added++; } else { _result.Modified++; } var patch = new MemberPatch(); patch.Name = name; if (tupper.ContainsKey("avatar_url") && tupper["avatar_url"].Type != JTokenType.Null) { patch.AvatarUrl = tupper.Value <string>("avatar_url").NullIfEmpty(); } if (tupper.ContainsKey("brackets")) { var brackets = tupper.Value <JArray>("brackets"); if (brackets.Count % 2 != 0) { throw new ImportException($"Field 'brackets' in tupper {name} is invalid."); } var tags = new List <ProxyTag>(); for (var i = 0; i < brackets.Count / 2; i++) { tags.Add(new ProxyTag((string)brackets[i * 2], (string)brackets[i * 2 + 1])); } patch.ProxyTags = tags.ToArray(); } if (tupper.ContainsKey("posts") && isNewMember) { patch.MessageCount = tupper.Value <int>("posts"); } if (tupper.ContainsKey("show_brackets")) { patch.KeepProxy = tupper.Value <bool>("show_brackets"); } if (tupper.ContainsKey("birthday") && tupper["birthday"].Type != JTokenType.Null) { var parsed = DateTimeFormats.TimestampExportFormat.Parse(tupper.Value <string>("birthday")); if (!parsed.Success) { throw new ImportException($"Field 'birthday' in tupper {name} is invalid."); } patch.Birthday = LocalDate.FromDateTime(parsed.Value.ToDateTimeUtc()); } if (tupper.ContainsKey("description")) { patch.Description = tupper.Value <string>("description"); } if (tupper.ContainsKey("nick")) { patch.DisplayName = tupper.Value <string>("nick"); } if (tupper.ContainsKey("tag") && tupper["tag"].Type != JTokenType.Null) { var tag = tupper.Value <string>("tag"); if (tag != lastSetTag) { lastSetTag = tag; multipleTags = true; } patch.DisplayName = $"{name} {tag}"; } patch.AssertIsValid(); if (patch.Errors.Count > 0) { var err = patch.Errors[0]; if (err is FieldTooLongError) { throw new ImportException($"Field {err.Key} in tupper {name} is too long " + $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength})."); } if (err.Text != null) { throw new ImportException($"tupper {name}: {err.Text}"); } throw new ImportException($"Field {err.Key} in tupper {name} is invalid."); } _logger.Debug( "Importing member with identifier {FileId} to system {System} (is creating new member? {IsCreatingNewMember})", name, _system.Id, isNewMember); await _repo.UpdateMember(memberId, patch, _conn); return(lastSetTag, multipleTags, hasGroup); }
public async Task Color(Context ctx, PKMember target) { var color = ctx.RemainderOrNull(); if (await ctx.MatchClear()) { ctx.CheckOwnMember(target); var patch = new MemberPatch { Color = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member color cleared."); } else if (!ctx.HasNext()) { // if (!target.ColorPrivacy.CanAccess(ctx.LookupContextFor(target.System))) // throw Errors.LookupNotAllowed; if (target.Color == null) { if (ctx.System?.Id == target.System) { await ctx.Reply( $"This member does not have a color set. To set one, type `pk;member {target.Reference()} color <color>`."); } else { await ctx.Reply("This member does not have a color set."); } } else { await ctx.Reply(embed : new EmbedBuilder() .Title("Member color") .Color(target.Color.ToDiscordColor()) .Thumbnail(new($"https://fakeimg.pl/256x256/{target.Color}/?text=%20")) .Description($"This member's color is **#{target.Color}**." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference()} color -clear`." : "")) .Build()); } } else { ctx.CheckOwnMember(target); if (color.StartsWith("#")) { color = color.Substring(1); } if (!Regex.IsMatch(color, "^[0-9a-fA-F]{6}$")) { throw Errors.InvalidColorError(color); } var patch = new MemberPatch { Color = Partial <string> .Present(color.ToLowerInvariant()) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply(embed : new EmbedBuilder() .Title($"{Emojis.Success} Member color changed.") .Color(color.ToDiscordColor()) .Thumbnail(new($"https://fakeimg.pl/256x256/{color}/?text=%20")) .Build()); } }
public static MemberPatch ToMemberPatch(JObject o) { var patch = new MemberPatch(); if (o.ContainsKey("name") && o["name"].Type == JTokenType.Null) { throw new JsonModelParseError("Member name can not be set to null."); } if (o.ContainsKey("name")) { patch.Name = o.Value <string>("name").BoundsCheckField(Limits.MaxMemberNameLength, "Member name"); } if (o.ContainsKey("color")) { patch.Color = o.Value <string>("color").NullIfEmpty()?.ToLower(); } if (o.ContainsKey("display_name")) { patch.DisplayName = o.Value <string>("display_name").NullIfEmpty().BoundsCheckField(Limits.MaxMemberNameLength, "Member display name"); } if (o.ContainsKey("avatar_url")) { patch.AvatarUrl = o.Value <string>("avatar_url").NullIfEmpty().BoundsCheckField(Limits.MaxUriLength, "Member avatar URL"); } if (o.ContainsKey("birthday")) { var str = o.Value <string>("birthday").NullIfEmpty(); var res = DateTimeFormats.DateExportFormat.Parse(str); if (res.Success) { patch.Birthday = res.Value; } else if (str == null) { patch.Birthday = null; } else { throw new JsonModelParseError("Could not parse member birthday."); } } if (o.ContainsKey("pronouns")) { patch.Pronouns = o.Value <string>("pronouns").NullIfEmpty().BoundsCheckField(Limits.MaxPronounsLength, "Member pronouns"); } if (o.ContainsKey("description")) { patch.Description = o.Value <string>("description").NullIfEmpty().BoundsCheckField(Limits.MaxDescriptionLength, "Member descriptoin"); } if (o.ContainsKey("keep_proxy")) { patch.KeepProxy = o.Value <bool>("keep_proxy"); } if (o.ContainsKey("prefix") || o.ContainsKey("suffix") && !o.ContainsKey("proxy_tags")) { patch.ProxyTags = new[] { new ProxyTag(o.Value <string>("prefix"), o.Value <string>("suffix")) } } ; else if (o.ContainsKey("proxy_tags")) { patch.ProxyTags = o.Value <JArray>("proxy_tags") .OfType <JObject>().Select(o => new ProxyTag(o.Value <string>("prefix"), o.Value <string>("suffix"))) .ToArray(); } if (o.ContainsKey("privacy")) //TODO: Deprecate this completely in api v2 { var plevel = o.Value <string>("privacy").ParsePrivacy("member"); patch.Visibility = plevel; patch.NamePrivacy = plevel; patch.AvatarPrivacy = plevel; patch.DescriptionPrivacy = plevel; patch.BirthdayPrivacy = plevel; patch.PronounPrivacy = plevel; // member.ColorPrivacy = plevel; patch.MetadataPrivacy = plevel; } else { if (o.ContainsKey("visibility")) { patch.Visibility = o.Value <string>("visibility").ParsePrivacy("member"); } if (o.ContainsKey("name_privacy")) { patch.NamePrivacy = o.Value <string>("name_privacy").ParsePrivacy("member"); } if (o.ContainsKey("description_privacy")) { patch.DescriptionPrivacy = o.Value <string>("description_privacy").ParsePrivacy("member"); } if (o.ContainsKey("avatar_privacy")) { patch.AvatarPrivacy = o.Value <string>("avatar_privacy").ParsePrivacy("member"); } if (o.ContainsKey("birthday_privacy")) { patch.BirthdayPrivacy = o.Value <string>("birthday_privacy").ParsePrivacy("member"); } if (o.ContainsKey("pronoun_privacy")) { patch.PronounPrivacy = o.Value <string>("pronoun_privacy").ParsePrivacy("member"); } // if (o.ContainsKey("color_privacy")) member.ColorPrivacy = o.Value<string>("color_privacy").ParsePrivacy("member"); if (o.ContainsKey("metadata_privacy")) { patch.MetadataPrivacy = o.Value <string>("metadata_privacy").ParsePrivacy("member"); } } return(patch); }
public async Task Description(Context ctx, PKMember target) { ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy); var noDescriptionSetMessage = "This member does not have a description set."; if (ctx.System?.Id == target.System) { noDescriptionSetMessage += $" To set one, type `pk;member {target.Reference(ctx)} description <description>`."; } if (ctx.MatchRaw()) { if (target.Description == null) { await ctx.Reply(noDescriptionSetMessage); } else { await ctx.Reply($"```\n{target.Description}\n```"); } return; } if (!ctx.HasNext(false)) { if (target.Description == null) { await ctx.Reply(noDescriptionSetMessage); } else { await ctx.Reply(embed : new EmbedBuilder() .Title("Member description") .Description(target.Description) .Field(new Embed.Field("\u200B", $"To print the description with formatting, type `pk;member {target.Reference(ctx)} description -raw`." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;member {target.Reference(ctx)} description -clear`." : ""))) .Build()); } return; } ctx.CheckOwnMember(target); if (await ctx.MatchClear("this member's description")) { var patch = new MemberPatch { Description = Partial <string> .Null() }; await ctx.Repository.UpdateMember(target.Id, patch); await ctx.Reply($"{Emojis.Success} Member description cleared."); } else { var description = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); if (description.IsLongerThan(Limits.MaxDescriptionLength)) { throw Errors.StringTooLongError("Description", description.Length, Limits.MaxDescriptionLength); } var patch = new MemberPatch { Description = Partial <string> .Present(description) }; await ctx.Repository.UpdateMember(target.Id, patch); await ctx.Reply($"{Emojis.Success} Member description changed."); } }
public async Task Pronouns(Context ctx, PKMember target) { var noPronounsSetMessage = "This member does not have pronouns set."; if (ctx.System?.Id == target.System) { noPronounsSetMessage += $"To set some, type `pk;member {target.Reference(ctx)} pronouns <pronouns>`."; } ctx.CheckSystemPrivacy(target.System, target.PronounPrivacy); if (ctx.MatchRaw()) { if (target.Pronouns == null) { await ctx.Reply(noPronounsSetMessage); } else { await ctx.Reply($"```\n{target.Pronouns}\n```"); } return; } if (!ctx.HasNext(false)) { if (target.Pronouns == null) { await ctx.Reply(noPronounsSetMessage); } else { await ctx.Reply( $"**{target.NameFor(ctx)}**'s pronouns are **{target.Pronouns}**.\nTo print the pronouns with formatting, type `pk;member {target.Reference(ctx)} pronouns -raw`." + (ctx.System?.Id == target.System ? $" To clear them, type `pk;member {target.Reference(ctx)} pronouns -clear`." : "")); } return; } ctx.CheckOwnMember(target); if (await ctx.MatchClear("this member's pronouns")) { var patch = new MemberPatch { Pronouns = Partial <string> .Null() }; await ctx.Repository.UpdateMember(target.Id, patch); await ctx.Reply($"{Emojis.Success} Member pronouns cleared."); } else { var pronouns = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); if (pronouns.IsLongerThan(Limits.MaxPronounsLength)) { throw Errors.StringTooLongError("Pronouns", pronouns.Length, Limits.MaxPronounsLength); } var patch = new MemberPatch { Pronouns = Partial <string> .Present(pronouns) }; await ctx.Repository.UpdateMember(target.Id, patch); await ctx.Reply($"{Emojis.Success} Member pronouns changed."); } }
public async Task Proxy(Context ctx, PKMember target) { ctx.CheckSystem().CheckOwnMember(target); ProxyTag ParseProxyTags(string exampleProxy) { // // Make sure there's one and only one instance of "text" in the example proxy given var prefixAndSuffix = exampleProxy.Split("text"); if (prefixAndSuffix.Length == 1) { prefixAndSuffix = prefixAndSuffix[0].Split("TEXT"); } if (prefixAndSuffix.Length < 2) { throw Errors.ProxyMustHaveText; } if (prefixAndSuffix.Length > 2) { throw Errors.ProxyMultipleText; } return(new ProxyTag(prefixAndSuffix[0], prefixAndSuffix[1])); } async Task <bool> WarnOnConflict(ProxyTag newTag) { var query = "select * from (select *, (unnest(proxy_tags)).prefix as prefix, (unnest(proxy_tags)).suffix as suffix from members where system = @System) as _ where prefix is not distinct from @Prefix and suffix is not distinct from @Suffix and id != @Existing"; var conflicts = (await _db.Execute(conn => conn.QueryAsync <PKMember>(query, new { Prefix = newTag.Prefix, Suffix = newTag.Suffix, Existing = target.Id, system = target.System }))).ToList(); if (conflicts.Count <= 0) { return(true); } var conflictList = conflicts.Select(m => $"- **{m.NameFor(ctx)}**"); var msg = $"{Emojis.Warn} The following members have conflicting proxy tags:\n{string.Join('\n', conflictList)}\nDo you want to proceed anyway?"; return(await ctx.PromptYesNo(msg, "Proceed")); } // "Sub"command: clear flag if (await ctx.MatchClear()) { // If we already have multiple tags, this would clear everything, so prompt that if (target.ProxyTags.Count > 1) { var msg = $"{Emojis.Warn} You already have multiple proxy tags set: {target.ProxyTagsString()}\nDo you want to clear them all?"; if (!await ctx.PromptYesNo(msg, "Clear")) { throw Errors.GenericCancelled(); } } var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(new ProxyTag[0]) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Proxy tags cleared."); } // "Sub"command: no arguments; will print proxy tags else if (!ctx.HasNext(skipFlags: false)) { if (target.ProxyTags.Count == 0) { await ctx.Reply("This member does not have any proxy tags."); } else { await ctx.Reply($"This member's proxy tags are:\n{target.ProxyTagsString("\n")}"); } } // Subcommand: "add" else if (ctx.Match("add", "append")) { if (!ctx.HasNext(skipFlags: false)) { throw new PKSyntaxError("You must pass an example proxy to add (eg. `[text]` or `J:text`)."); } var tagToAdd = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false)); if (tagToAdd.IsEmpty) { throw Errors.EmptyProxyTags(target); } if (target.ProxyTags.Contains(tagToAdd)) { throw Errors.ProxyTagAlreadyExists(tagToAdd, target); } if (!await WarnOnConflict(tagToAdd)) { throw Errors.GenericCancelled(); } var newTags = target.ProxyTags.ToList(); newTags.Add(tagToAdd); var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(newTags.ToArray()) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Added proxy tags {tagToAdd.ProxyString.AsCode()}."); } // Subcommand: "remove" else if (ctx.Match("remove", "delete")) { if (!ctx.HasNext(skipFlags: false)) { throw new PKSyntaxError("You must pass a proxy tag to remove (eg. `[text]` or `J:text`)."); } var tagToRemove = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false)); if (tagToRemove.IsEmpty) { throw Errors.EmptyProxyTags(target); } if (!target.ProxyTags.Contains(tagToRemove)) { throw Errors.ProxyTagDoesNotExist(tagToRemove, target); } var newTags = target.ProxyTags.ToList(); newTags.Remove(tagToRemove); var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(newTags.ToArray()) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Removed proxy tags {tagToRemove.ProxyString.AsCode()}."); } // Subcommand: bare proxy tag given else { var requestedTag = ParseProxyTags(ctx.RemainderOrNull(skipFlags: false)); if (requestedTag.IsEmpty) { throw Errors.EmptyProxyTags(target); } // This is mostly a legacy command, so it's gonna warn if there's // already more than one proxy tag. if (target.ProxyTags.Count > 1) { var msg = $"This member already has more than one proxy tag set: {target.ProxyTagsString()}\nDo you want to replace them?"; if (!await ctx.PromptYesNo(msg, "Replace")) { throw Errors.GenericCancelled(); } } if (!await WarnOnConflict(requestedTag)) { throw Errors.GenericCancelled(); } var newTags = new[] { requestedTag }; var patch = new MemberPatch { ProxyTags = Partial <ProxyTag[]> .Present(newTags) }; await _db.Execute(conn => _repo.UpdateMember(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Member proxy tags set to {requestedTag.ProxyString.AsCode()}."); } }
public async Task NewMember(Context ctx) { if (ctx.System == null) { throw Errors.NoSystemError; } var memberName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a member name."); // Hard name length cap if (memberName.Length > Limits.MaxMemberNameLength) { throw Errors.StringTooLongError("Member name", memberName.Length, Limits.MaxMemberNameLength); } // Warn if there's already a member by this name var existingMember = await ctx.Repository.GetMemberByName(ctx.System.Id, memberName); if (existingMember != null) { var msg = $"{Emojis.Warn} You already have a member in your system with the name \"{existingMember.NameFor(ctx)}\" (with ID `{existingMember.Hid}`). Do you want to create another member with the same name?"; if (!await ctx.PromptYesNo(msg, "Create")) { throw new PKError("Member creation cancelled."); } } await using var conn = await ctx.Database.Obtain(); // Enforce per-system member limit var memberCount = await ctx.Repository.GetSystemMemberCount(ctx.System.Id); var memberLimit = ctx.Config.MemberLimitOverride ?? Limits.MaxMemberCount; if (memberCount >= memberLimit) { throw Errors.MemberLimitReachedError(memberLimit); } // Create the member var member = await ctx.Repository.CreateMember(ctx.System.Id, memberName, conn); memberCount++; JObject dispatchData = new JObject(); dispatchData.Add("name", memberName); if (ctx.Config.MemberDefaultPrivate) { var patch = new MemberPatch().WithAllPrivacy(PrivacyLevel.Private); await ctx.Repository.UpdateMember(member.Id, patch, conn); dispatchData.Merge(patch.ToJson()); } // Try to match an image attached to the message var avatarArg = ctx.Message.Attachments.FirstOrDefault(); Exception imageMatchError = null; if (avatarArg != null) { try { await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Url); await ctx.Repository.UpdateMember(member.Id, new MemberPatch { AvatarUrl = avatarArg.Url }, conn); dispatchData.Add("avatar_url", avatarArg.Url); } catch (Exception e) { imageMatchError = e; } } _ = _dispatch.Dispatch(member.Id, new UpdateDispatchData { Event = DispatchEvent.CREATE_MEMBER, EventData = dispatchData, }); // Send confirmation and space hint await ctx.Reply( $"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member"); // todo: move this to ModelRepository if (await ctx.Database.Execute(conn => conn.QuerySingleAsync <bool>("select has_private_members(@System)", new { System = ctx.System.Id })) && !ctx.Config.MemberDefaultPrivate) //if has private members { await ctx.Reply( $"{Emojis.Warn} This member is currently **public**. To change this, use `pk;member {member.Hid} private`."); } if (avatarArg != null) { if (imageMatchError == null) { await ctx.Reply( $"{Emojis.Success} Member avatar set to attached image.\n{Emojis.Warn} If you delete the message containing the attachment, the avatar will stop working."); } else { await ctx.Reply($"{Emojis.Error} Couldn't set avatar: {imageMatchError.Message}"); } } if (memberName.Contains(" ")) { await ctx.Reply( $"{Emojis.Note} Note that this member's name contains spaces. You will need to surround it with \"double quotes\" when using commands referring to it, or just use the member's 5-character ID (which is `{member.Hid}`)."); } if (memberCount >= memberLimit) { await ctx.Reply( $"{Emojis.Warn} You have reached the per-system member limit ({memberLimit}). You will be unable to create additional members until existing members are deleted."); } else if (memberCount >= Limits.WarnThreshold(memberLimit)) { await ctx.Reply( $"{Emojis.Warn} You are approaching the per-system member limit ({memberCount} / {memberLimit} members). Please review your member list for unused or duplicate members."); } }