Frankly, I have no idea if any of this is in the correct place, or follows the "norm" for internal stuff. Feedback would be nice for this implementation, as I believe this implementation uses the least amount of partial objects (I couldn't really avoid them in RestAuditLogEntry, unless I wanted to write entry types for each type of entry) and is also fairly simple.pull/719/head
| @@ -20,6 +20,7 @@ namespace Discord | |||||
| public const int MaxMessagesPerBatch = 100; | public const int MaxMessagesPerBatch = 100; | ||||
| public const int MaxUsersPerBatch = 1000; | public const int MaxUsersPerBatch = 1000; | ||||
| public const int MaxGuildsPerBatch = 100; | public const int MaxGuildsPerBatch = 100; | ||||
| public const int MaxAuditLogEntriesPerBatch = 100; | |||||
| /// <summary> Gets or sets how a request should act in the case of an error, by default. </summary> | /// <summary> Gets or sets how a request should act in the case of an error, by default. </summary> | ||||
| public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; | public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; | ||||
| @@ -0,0 +1,50 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// The action type within a <see cref="IAuditLogEntry"/> | |||||
| /// </summary> | |||||
| public enum ActionType | |||||
| { | |||||
| GuildUpdated = 1, | |||||
| ChannelCreated = 10, | |||||
| ChannelUpdated = 11, | |||||
| ChannelDeleted = 12, | |||||
| OverwriteCreated = 13, | |||||
| OverwriteUpdated = 14, | |||||
| OverwriteDeleted = 15, | |||||
| Kick = 20, | |||||
| Prune = 21, | |||||
| Ban = 22, | |||||
| Unban = 23, | |||||
| MemberUpdated = 24, | |||||
| MemberRoleUpdated = 25, | |||||
| RoleCreated = 30, | |||||
| RoleUpdated = 31, | |||||
| RoleDeleted = 32, | |||||
| InviteCreated = 40, | |||||
| InviteUpdated = 41, | |||||
| InviteDeleted = 42, | |||||
| WebhookCreated = 50, | |||||
| WebhookUpdated = 51, | |||||
| WebhookDeleted = 52, | |||||
| EmojiCreated = 60, | |||||
| EmojiUpdated = 61, | |||||
| EmojiDeleted = 62, | |||||
| MessageDeleted = 72 | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents changes which may occur within a <see cref="IAuditLogEntry"/> | |||||
| /// </summary> | |||||
| public interface IAuditLogChange | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,44 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents an entry in an audit log | |||||
| /// </summary> | |||||
| public interface IAuditLogEntry : IEntity<ulong> | |||||
| { | |||||
| /// <summary> | |||||
| /// The action which occured to create this entry | |||||
| /// </summary> | |||||
| ActionType Action { get; } | |||||
| /// <summary> | |||||
| /// The changes which occured within this entry. May be empty if no changes occured. | |||||
| /// </summary> | |||||
| IReadOnlyCollection<IAuditLogChange> Changes { get; } | |||||
| /// <summary> | |||||
| /// Any options which apply to this entry. If no options were provided, this may be <see cref="null"/>. | |||||
| /// </summary> | |||||
| IAuditLogOptions Options { get; } | |||||
| /// <summary> | |||||
| /// The id which the target applies to | |||||
| /// </summary> | |||||
| ulong TargetId { get; } | |||||
| /// <summary> | |||||
| /// The user responsible for causing the changes | |||||
| /// </summary> | |||||
| IUser User { get; } | |||||
| /// <summary> | |||||
| /// The reason behind the change. May be <see cref="null"/> if no reason was provided. | |||||
| /// </summary> | |||||
| string Reason { get; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,16 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | |||||
| { | |||||
| /// <summary> | |||||
| /// Represents options which may be applied to an <see cref="IAuditLogEntry"/> | |||||
| /// </summary> | |||||
| public interface IAuditLogOptions | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -114,5 +114,8 @@ namespace Discord | |||||
| Task DownloadUsersAsync(); | Task DownloadUsersAsync(); | ||||
| /// <summary> Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. </summary> | /// <summary> Removes all users from this guild if they have not logged on in a provided number of days or, if simulate is true, returns the number of users that would be removed. </summary> | ||||
| Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); | Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null); | ||||
| Task<IReadOnlyCollection<IAuditLogEntry>> GetAuditLogAsync(int limit = DiscordConfig.MaxAuditLogEntriesPerBatch, | |||||
| CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null); | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,17 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | |||||
| { | |||||
| internal class AuditLog | |||||
| { | |||||
| //TODO: figure out how this works | |||||
| //[JsonProperty("webhooks")] | |||||
| //public object Webhooks { get; set; } | |||||
| [JsonProperty("users")] | |||||
| public User[] Users { get; set; } | |||||
| [JsonProperty("audit_log_entries")] | |||||
| public AuditLogEntry[] Entries { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| using Newtonsoft.Json; | |||||
| using Newtonsoft.Json.Linq; | |||||
| namespace Discord.API | |||||
| { | |||||
| internal class AuditLogChange | |||||
| { | |||||
| [JsonProperty("key")] | |||||
| public string ChangedProperty { get; set; } | |||||
| [JsonProperty("new_value")] | |||||
| public JToken NewValue { get; set; } | |||||
| [JsonProperty("old_value")] | |||||
| public JToken OldValue { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | |||||
| { | |||||
| internal class AuditLogEntry | |||||
| { | |||||
| [JsonProperty("target_id")] | |||||
| public ulong TargetId { get; set; } | |||||
| [JsonProperty("user_id")] | |||||
| public ulong UserId { get; set; } | |||||
| [JsonProperty("changes")] | |||||
| public AuditLogChange[] Changes { get; set; } | |||||
| [JsonProperty("options")] | |||||
| public AuditLogOptions Options { get; set; } | |||||
| [JsonProperty("id")] | |||||
| public ulong Id { get; set; } | |||||
| [JsonProperty("action_type")] | |||||
| public ActionType Action { get; set; } | |||||
| [JsonProperty("reason")] | |||||
| public string Reason { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using Newtonsoft.Json; | |||||
| namespace Discord.API | |||||
| { | |||||
| // TODO: Complete this with all possible values for options | |||||
| internal class AuditLogOptions | |||||
| { | |||||
| [JsonProperty("count")] | |||||
| public int Count { get; set; } | |||||
| [JsonProperty("channel_id")] | |||||
| public ulong ChannelId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,8 @@ | |||||
| namespace Discord.API.Rest | |||||
| { | |||||
| class GetAuditLogsParams | |||||
| { | |||||
| public Optional<int> Limit { get; set; } | |||||
| public Optional<ulong> AfterEntryId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -1059,6 +1059,21 @@ namespace Discord.API | |||||
| return await SendJsonAsync<IReadOnlyCollection<Role>>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); | return await SendJsonAsync<IReadOnlyCollection<Role>>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false); | ||||
| } | } | ||||
| //Audit logs | |||||
| public async Task<AuditLog> GetAuditLogsAsync(ulong guildId, GetAuditLogsParams args, RequestOptions options = null) | |||||
| { | |||||
| Preconditions.NotEqual(guildId, 0, nameof(guildId)); | |||||
| Preconditions.NotNull(args, nameof(args)); | |||||
| options = RequestOptions.CreateOrClone(options); | |||||
| int limit = args.Limit.GetValueOrDefault(int.MaxValue); | |||||
| ulong afterEntryId = args.AfterEntryId.GetValueOrDefault(0); | |||||
| var ids = new BucketIds(guildId: guildId); | |||||
| Expression<Func<string>> endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}&after={afterEntryId}"; | |||||
| return await SendAsync<AuditLog>("GET", endpoint, ids, options: options).ConfigureAwait(false); | |||||
| } | |||||
| //Users | //Users | ||||
| public async Task<User> GetUserAsync(ulong userId, RequestOptions options = null) | public async Task<User> GetUserAsync(ulong userId, RequestOptions options = null) | ||||
| { | { | ||||
| @@ -0,0 +1,37 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| using EntryModel = Discord.API.AuditLogEntry; | |||||
| using ChangeModel = Discord.API.AuditLogChange; | |||||
| using OptionModel = Discord.API.AuditLogOptions; | |||||
| namespace Discord.Rest | |||||
| { | |||||
| internal static class AuditLogHelper | |||||
| { | |||||
| public static IAuditLogChange CreateChange(BaseDiscordClient discord, EntryModel entryModel, ChangeModel model) | |||||
| { | |||||
| switch (entryModel.Action) | |||||
| { | |||||
| case ActionType.MemberRoleUpdated: | |||||
| return new MemberRoleAuditLogChange(discord, model); | |||||
| default: | |||||
| throw new NotImplementedException($"{nameof(AuditLogHelper)} does not implement the {entryModel.Action} audit log action."); | |||||
| } | |||||
| } | |||||
| public static IAuditLogOptions CreateOptions(BaseDiscordClient discord, EntryModel entryModel, OptionModel model) | |||||
| { | |||||
| switch (entryModel.Action) | |||||
| { | |||||
| case ActionType.MessageDeleted: | |||||
| return new MessageDeleteAuditLogOptions(discord, model); | |||||
| default: | |||||
| throw new NotImplementedException($"{nameof(AuditLogHelper)} does not implement the {entryModel.Action} audit log action."); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| using Newtonsoft.Json; | |||||
| using Model = Discord.API.AuditLogChange; | |||||
| namespace Discord.Rest | |||||
| { | |||||
| public class MemberRoleAuditLogChange : IAuditLogChange | |||||
| { | |||||
| internal MemberRoleAuditLogChange(BaseDiscordClient discord, Model model) | |||||
| { | |||||
| RoleAdded = model.ChangedProperty == "$add"; | |||||
| RoleId = model.NewValue.Value<ulong>("id"); | |||||
| } | |||||
| public bool RoleAdded { get; set; } | |||||
| //TODO: convert to IRole | |||||
| public ulong RoleId { get; set; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,23 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| using Model = Discord.API.AuditLogOptions; | |||||
| namespace Discord.Rest | |||||
| { | |||||
| public class MessageDeleteAuditLogOptions : IAuditLogOptions | |||||
| { | |||||
| internal MessageDeleteAuditLogOptions(BaseDiscordClient discord, Model model) | |||||
| { | |||||
| MessageCount = model.Count; | |||||
| SourceChannelId = model.ChannelId; | |||||
| } | |||||
| //TODO: turn this into an IChannel | |||||
| public ulong SourceChannelId { get; } | |||||
| public int MessageCount { get; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,47 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Linq; | |||||
| using FullModel = Discord.API.AuditLog; | |||||
| using Model = Discord.API.AuditLogEntry; | |||||
| namespace Discord.Rest | |||||
| { | |||||
| public class RestAuditLogEntry : RestEntity<ulong>, IAuditLogEntry | |||||
| { | |||||
| internal RestAuditLogEntry(BaseDiscordClient discord, Model model, API.User user) | |||||
| : base(discord, model.Id) | |||||
| { | |||||
| Action = model.Action; | |||||
| if (model.Changes != null) | |||||
| Changes = model.Changes | |||||
| .Select(x => AuditLogHelper.CreateChange(discord, model, x)) | |||||
| .ToReadOnlyCollection(() => model.Changes.Length); | |||||
| else | |||||
| Changes = ImmutableArray.Create<IAuditLogChange>(); | |||||
| if (model.Options != null) | |||||
| Options = AuditLogHelper.CreateOptions(discord, model, model.Options); | |||||
| TargetId = model.TargetId; | |||||
| User = RestUser.Create(discord, user); | |||||
| Reason = model.Reason; | |||||
| } | |||||
| internal static RestAuditLogEntry Create(BaseDiscordClient discord, FullModel fullLog, Model model) | |||||
| { | |||||
| var user = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId); | |||||
| return new RestAuditLogEntry(discord, model, user); | |||||
| } | |||||
| public ActionType Action { get; } | |||||
| public IReadOnlyCollection<IAuditLogChange> Changes { get; } | |||||
| public IAuditLogOptions Options { get; } | |||||
| public ulong TargetId { get; } | |||||
| public IUser User { get; } | |||||
| public string Reason { get; } | |||||
| } | |||||
| } | |||||
| @@ -247,5 +247,34 @@ namespace Discord.Rest | |||||
| model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); | model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false); | ||||
| return model.Pruned; | return model.Pruned; | ||||
| } | } | ||||
| public static IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client, | |||||
| ulong? from, int? limit, RequestOptions options) | |||||
| { | |||||
| return new PagedAsyncEnumerable<RestAuditLogEntry>( | |||||
| DiscordConfig.MaxAuditLogEntriesPerBatch, | |||||
| async (info, ct) => | |||||
| { | |||||
| var args = new GetAuditLogsParams | |||||
| { | |||||
| Limit = info.PageSize | |||||
| }; | |||||
| if (info.Position != null) | |||||
| args.AfterEntryId = info.Position.Value; | |||||
| var model = await client.ApiClient.GetAuditLogsAsync(guild.Id, args, options); | |||||
| return model.Entries.Select((x) => RestAuditLogEntry.Create(client, model, x)).ToImmutableArray(); | |||||
| }, | |||||
| nextPage: (info, lastPage) => | |||||
| { | |||||
| if (lastPage.Count != DiscordConfig.MaxAuditLogEntriesPerBatch) | |||||
| return false; | |||||
| info.Position = lastPage.Max(x => x.Id); | |||||
| return true; | |||||
| }, | |||||
| start: from, | |||||
| count: limit | |||||
| ); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -239,6 +239,10 @@ namespace Discord.Rest | |||||
| public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) | public Task<int> PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null) | ||||
| => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); | => GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options); | ||||
| //Audit logs | |||||
| public IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(int limit, RequestOptions options = null) | |||||
| => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options); | |||||
| public override string ToString() => Name; | public override string ToString() => Name; | ||||
| private string DebuggerDisplay => $"{Name} ({Id})"; | private string DebuggerDisplay => $"{Name} ({Id})"; | ||||
| @@ -361,5 +365,13 @@ namespace Discord.Rest | |||||
| return ImmutableArray.Create<IGuildUser>(); | return ImmutableArray.Create<IGuildUser>(); | ||||
| } | } | ||||
| Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } | Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } | ||||
| async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogAsync(int limit, CacheMode cacheMode, RequestOptions options) | |||||
| { | |||||
| if (cacheMode == CacheMode.AllowDownload) | |||||
| return (await GetAuditLogsAsync(limit, options).Flatten().ConfigureAwait(false)).ToImmutableArray(); | |||||
| else | |||||
| return ImmutableArray.Create<IAuditLogEntry>(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -423,6 +423,10 @@ namespace Discord.WebSocket | |||||
| _downloaderPromise.TrySetResultAsync(true); | _downloaderPromise.TrySetResultAsync(true); | ||||
| } | } | ||||
| //Audit logs | |||||
| public IAsyncEnumerable<IReadOnlyCollection<RestAuditLogEntry>> GetAuditLogsAsync(int limit, RequestOptions options = null) | |||||
| => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options); | |||||
| //Voice States | //Voice States | ||||
| internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) | internal async Task<SocketVoiceState> AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model) | ||||
| { | { | ||||
| @@ -659,5 +663,13 @@ namespace Discord.WebSocket | |||||
| Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IGuildUser>(Owner); | => Task.FromResult<IGuildUser>(Owner); | ||||
| Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } | Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); } | ||||
| async Task<IReadOnlyCollection<IAuditLogEntry>> IGuild.GetAuditLogAsync(int limit, CacheMode cacheMode, RequestOptions options) | |||||
| { | |||||
| if (cacheMode == CacheMode.AllowDownload) | |||||
| return (await GetAuditLogsAsync(limit, options).Flatten().ConfigureAwait(false)).ToImmutableArray(); | |||||
| else | |||||
| return ImmutableArray.Create<IAuditLogEntry>(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||