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>(); | |||||
} | |||||
} | } | ||||
} | } |