Example #1
0
    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)));
    }
Example #2
0
        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.");
                }
            }
        }
Example #3
0
 public static MemberPatch WithAllPrivacy(this MemberPatch member, PrivacyLevel level)
 {
     foreach (var subject in Enum.GetValues(typeof(MemberPrivacySubject)))
     {
         member.WithPrivacy((MemberPrivacySubject)subject, level);
     }
     return(member);
 }
Example #4
0
        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);
    }
Example #6
0
        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}\".");
            }
        }
Example #7
0
    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)));
    }
Example #8
0
        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.");
            }
        }
Example #9
0
        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.");
            }
        }
Example #10
0
        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.");
            }
        }
Example #11
0
    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 *"));
    }
Example #12
0
    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);
    }
Example #13
0
    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)));
    }
Example #14
0
        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}");
            }
        }
Example #15
0
    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());
    }
Example #17
0
        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.");
            }
        }
Example #18
0
    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);
    }
Example #19
0
        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);
        }
Example #21
0
    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.");
        }
    }
Example #22
0
    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.");
        }
    }
Example #23
0
        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()}.");
            }
        }
Example #24
0
    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.");
        }
    }