public async Task <IActionResult> DoGroupPatch(string groupRef, [FromBody] JObject data) { var system = await ResolveSystem("@me"); var group = await ResolveGroup(groupRef); if (group == null) { throw Errors.GroupNotFound; } if (group.System != system.Id) { throw Errors.NotOwnGroupError; } var patch = GroupPatch.FromJson(data); patch.AssertIsValid(); if (patch.Errors.Count > 0) { throw new ModelParseError(patch.Errors); } var newGroup = await _repo.UpdateGroup(group.Id, patch); return(Ok(newGroup.ToJson(LookupContext.ByOwner))); }
public static GroupPatch WithAllPrivacy(this GroupPatch member, PrivacyLevel level) { foreach (var subject in Enum.GetValues(typeof(GroupPrivacySubject))) { member.WithPrivacy((GroupPrivacySubject)subject, level); } return(member); }
public async Task GroupDescription(Context ctx, PKGroup target) { if (await ctx.MatchClear("this group's description")) { ctx.CheckOwnGroup(target); var patch = new GroupPatch { Description = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Group description cleared."); } else if (!ctx.HasNext()) { if (target.Description == null) { if (ctx.System?.Id == target.System) { await ctx.Reply($"This group does not have a description set. To set one, type `pk;group {target.Reference()} description <description>`."); } else { await ctx.Reply("This group 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 DiscordEmbedBuilder() .WithTitle("Group description") .WithDescription(target.Description) .AddField("\u200B", $"To print the description with formatting, type `pk;group {target.Reference()} description -raw`." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference()} description -clear`." : "")) .Build()); } } else { ctx.CheckOwnGroup(target); var description = ctx.RemainderOrNull().NormalizeLineEndSpacing(); if (description.IsLongerThan(Limits.MaxDescriptionLength)) { throw Errors.DescriptionTooLongError(description.Length); } var patch = new GroupPatch { Description = Partial <string> .Present(description) }; await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Group description changed."); } }
public async Task <PKGroup> UpdateGroup(GroupId id, GroupPatch patch, IPKConnection?conn = null) { _logger.Information("Updated {GroupId}: {@GroupPatch}", id, patch); var query = patch.Apply(new Query("groups").Where("id", id)); var group = await _db.QueryFirst <PKGroup>(conn, query, "returning *"); if (conn == null) { _ = _dispatch.Dispatch(id, new UpdateDispatchData { Event = DispatchEvent.UPDATE_GROUP, EventData = patch.ToJson() }); } return(group); }
public static GroupPatch WithPrivacy(this GroupPatch group, GroupPrivacySubject subject, PrivacyLevel level) { // what do you mean switch expressions can't be statements >.> _ = subject switch { GroupPrivacySubject.Name => group.NamePrivacy = level, GroupPrivacySubject.Description => group.DescriptionPrivacy = level, GroupPrivacySubject.Icon => group.IconPrivacy = level, GroupPrivacySubject.List => group.ListPrivacy = level, GroupPrivacySubject.Metadata => group.MetadataPrivacy = level, GroupPrivacySubject.Visibility => group.Visibility = level, _ => throw new ArgumentOutOfRangeException($"Unknown privacy subject {subject}") }; return(group); }
public async Task <IActionResult> GroupCreate([FromBody] JObject data) { var system = await ResolveSystem("@me"); var config = await _repo.GetSystemConfig(system.Id); // Check group cap var existingGroupCount = await _repo.GetSystemGroupCount(system.Id); var groupLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount; if (existingGroupCount >= groupLimit) { throw Errors.GroupLimitReached; } var patch = GroupPatch.FromJson(data); patch.AssertIsValid(); if (!patch.Name.IsPresent) { patch.Errors.Add(new ValidationError("name", "Key 'name' is required when creating new group.")); } if (patch.Errors.Count > 0) { throw new ModelParseError(patch.Errors); } using var conn = await _db.Obtain(); using var tx = await conn.BeginTransactionAsync(); var newGroup = await _repo.CreateGroup(system.Id, patch.Name.Value, conn); newGroup = await _repo.UpdateGroup(newGroup.Id, patch, conn); _ = _dispatch.Dispatch(newGroup.Id, new UpdateDispatchData() { Event = DispatchEvent.CREATE_GROUP, EventData = patch.ToJson(), }); await tx.CommitAsync(); return(Ok(newGroup.ToJson(LookupContext.ByOwner))); }
public async Task GroupDisplayName(Context ctx, PKGroup target) { if (await ctx.MatchClear("this group's display name")) { ctx.CheckOwnGroup(target); var patch = new GroupPatch { DisplayName = Partial <string> .Null() }; await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Group display name cleared."); } else if (!ctx.HasNext()) { // No perms check, display name isn't covered by member privacy var eb = new DiscordEmbedBuilder() .AddField("Name", target.Name) .AddField("Display Name", target.DisplayName ?? "*(none)*"); if (ctx.System?.Id == target.System) { eb.WithDescription($"To change display name, type `pk;group {target.Reference()} displayname <display name>`.\nTo clear it, type `pk;group {target.Reference()} displayname -clear`."); } await ctx.Reply(embed : eb.Build()); } else { ctx.CheckOwnGroup(target); var newDisplayName = ctx.RemainderOrNull(); var patch = new GroupPatch { DisplayName = Partial <string> .Present(newDisplayName) }; await _db.Execute(conn => _repo.UpdateGroup(conn, target.Id, patch)); await ctx.Reply($"{Emojis.Success} Group display name changed."); } }
public async Task <IActionResult> BulkGroupPrivacy([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 = GroupPatch.FromJson(data); patch.AssertIsValid(); if (patch.Errors.Count > 0) { throw new ModelParseError(patch.Errors); } await _db.ExecuteQuery(patch.Apply(new Query("groups").Where("system", systemId))); return(NoContent()); }
private async Task ImportGroup(JObject group) { var id = group.Value <string>("id"); var name = group.Value <string>("name"); var(found, isHidExisting) = TryGetExistingGroup(id, name); var isNewGroup = found == null; var referenceName = isHidExisting ? id : name; _logger.Debug( "Importing group with identifier {FileId} to system {System} (is creating new group? {IsCreatingNewGroup})", referenceName, _system.Id, isNewGroup ); var patch = GroupPatch.FromJson(group); patch.AssertIsValid(); if (patch.Errors.Count > 0) { var err = patch.Errors[0]; if (err is FieldTooLongError) { throw new ImportException($"Field {err.Key} in group {name} is too long " + $"({(err as FieldTooLongError).ActualLength} > {(err as FieldTooLongError).MaxLength})."); } if (err.Text != null) { throw new ImportException($"group {name}: {err.Text}"); } throw new ImportException($"Field {err.Key} in group {name} is invalid."); } var groupId = found; if (isNewGroup) { var newGroup = await _repo.CreateGroup(_system.Id, patch.Name.Value, _conn); groupId = newGroup.Id; } _knownGroupIdentifiers[id] = groupId.Value; await _repo.UpdateGroup(groupId.Value, patch, _conn); var groupMembers = group.Value <JArray>("members"); var currentGroupMembers = (await _conn.QueryAsync <MemberId>( "select member_id from group_members where group_id = @groupId", new { groupId = groupId.Value } )).ToList(); await using (var importer = _conn.BeginBinaryImport("copy group_members (group_id, member_id) from stdin (format binary)")) { foreach (var memberIdentifier in groupMembers) { if (!_knownMemberIdentifiers.TryGetValue(memberIdentifier.ToString(), out var memberId)) { throw new Exception( $"Attempted to import group member with member identifier {memberIdentifier} but could not find a recently imported member with this id!"); } if (currentGroupMembers.Contains(memberId)) { continue; } await importer.StartRowAsync(); await importer.WriteAsync(groupId.Value.Value, NpgsqlDbType.Integer); await importer.WriteAsync(memberId.Value, NpgsqlDbType.Integer); } await importer.CompleteAsync(); } }
public async Task CreateGroup(Context ctx) { ctx.CheckSystem(); // Check group name length var groupName = ctx.RemainderOrNull() ?? throw new PKSyntaxError("You must pass a group name."); if (groupName.Length > Limits.MaxGroupNameLength) { throw new PKError($"Group name too long ({groupName.Length}/{Limits.MaxGroupNameLength} characters)."); } // Check group cap var existingGroupCount = await ctx.Repository.GetSystemGroupCount(ctx.System.Id); var groupLimit = ctx.Config.GroupLimitOverride ?? Limits.MaxGroupCount; if (existingGroupCount >= groupLimit) { throw new PKError( $"System has reached the maximum number of groups ({groupLimit}). Please delete unused groups first in order to create new ones."); } // Warn if there's already a group by this name var existingGroup = await ctx.Repository.GetGroupByName(ctx.System.Id, groupName); if (existingGroup != null) { var msg = $"{Emojis.Warn} You already have a group in your system with the name \"{existingGroup.Name}\" (with ID `{existingGroup.Hid}`). Do you want to create another group with the same name?"; if (!await ctx.PromptYesNo(msg, "Create")) { throw new PKError("Group creation cancelled."); } } // todo: this is supposed to be a transaction, but it's not used in any useful way // consider removing it? using var conn = await ctx.Database.Obtain(); var newGroup = await ctx.Repository.CreateGroup(ctx.System.Id, groupName); var dispatchData = new JObject(); dispatchData.Add("name", groupName); if (ctx.Config.GroupDefaultPrivate) { var patch = new GroupPatch().WithAllPrivacy(PrivacyLevel.Private); await ctx.Repository.UpdateGroup(newGroup.Id, patch, conn); dispatchData.Merge(patch.ToJson()); } _ = _dispatch.Dispatch(newGroup.Id, new UpdateDispatchData { Event = DispatchEvent.CREATE_GROUP, EventData = dispatchData }); var reference = newGroup.Reference(ctx); var eb = new EmbedBuilder() .Description( $"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:") .Field(new Embed.Field("View the group card", $"> pk;group **{reference}**")) .Field(new Embed.Field("Add members to the group", $"> pk;group **{reference}** add **MemberName**\n> pk;group **{reference}** add **Member1** **Member2** **Member3** (and so on...)")) .Field(new Embed.Field("Set the description", $"> pk;group **{reference}** description **This is my new group, and here is the description!**")) .Field(new Embed.Field("Set the group icon", $"> pk;group **{reference}** icon\n*(with an image attached)*")); await ctx.Reply($"{Emojis.Success} Group created!", eb.Build()); if (existingGroupCount >= Limits.WarnThreshold(groupLimit)) { await ctx.Reply( $"{Emojis.Warn} You are approaching the per-system group limit ({existingGroupCount} / {groupLimit} groups). Please review your group list for unused or duplicate groups."); } }
public async Task GroupDescription(Context ctx, PKGroup target) { ctx.CheckSystemPrivacy(target.System, target.DescriptionPrivacy); var noDescriptionSetMessage = "This group does not have a description set."; if (ctx.System?.Id == target.System) { noDescriptionSetMessage += $" To set one, type `pk;group {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("Group description") .Description(target.Description) .Field(new Embed.Field("\u200B", $"To print the description with formatting, type `pk;group {target.Reference(ctx)} description -raw`." + (ctx.System?.Id == target.System ? $" To clear it, type `pk;group {target.Reference(ctx)} description -clear`." : ""))) .Build()); } return; } ctx.CheckOwnGroup(target); if (await ctx.MatchClear("this group's description")) { var patch = new GroupPatch { Description = Partial <string> .Null() }; await ctx.Repository.UpdateGroup(target.Id, patch); await ctx.Reply($"{Emojis.Success} Group 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 GroupPatch { Description = Partial <string> .Present(description) }; await ctx.Repository.UpdateGroup(target.Id, patch); await ctx.Reply($"{Emojis.Success} Group description changed."); } }
public async Task GroupDisplayName(Context ctx, PKGroup target) { var noDisplayNameSetMessage = "This group does not have a display name set."; if (ctx.System?.Id == target.System) { noDisplayNameSetMessage += $" To set one, type `pk;group {target.Reference(ctx)} displayname <display name>`."; } // No perms check, display name isn't covered by member privacy if (ctx.MatchRaw()) { if (target.DisplayName == null) { await ctx.Reply(noDisplayNameSetMessage); } else { await ctx.Reply($"```\n{target.DisplayName}\n```"); } return; } if (!ctx.HasNext(false)) { if (target.DisplayName == null) { await ctx.Reply(noDisplayNameSetMessage); } else { var eb = new EmbedBuilder() .Field(new Embed.Field("Name", target.Name)) .Field(new Embed.Field("Display Name", target.DisplayName)); var reference = target.Reference(ctx); if (ctx.System?.Id == target.System) { eb.Description( $"To change display name, type `pk;group {reference} displayname <display name>`." + $"To clear it, type `pk;group {reference} displayname -clear`." + $"To print the raw display name, type `pk;group {reference} displayname -raw`."); } await ctx.Reply(embed : eb.Build()); } return; } ctx.CheckOwnGroup(target); if (await ctx.MatchClear("this group's display name")) { var patch = new GroupPatch { DisplayName = Partial <string> .Null() }; await ctx.Repository.UpdateGroup(target.Id, patch); await ctx.Reply($"{Emojis.Success} Group display name cleared."); if (target.NamePrivacy == PrivacyLevel.Private) { await ctx.Reply($"{Emojis.Warn} Since this group no longer has a display name set, their name privacy **can no longer take effect**."); } } else { var newDisplayName = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); var patch = new GroupPatch { DisplayName = Partial <string> .Present(newDisplayName) }; await ctx.Repository.UpdateGroup(target.Id, patch); await ctx.Reply($"{Emojis.Success} Group display name changed."); } }