diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs
index fd2fe92e8..31f187ffb 100644
--- a/src/Discord.Net.Core/DiscordConfig.cs
+++ b/src/Discord.Net.Core/DiscordConfig.cs
@@ -20,6 +20,7 @@ namespace Discord
public const int MaxMessagesPerBatch = 100;
public const int MaxUsersPerBatch = 1000;
public const int MaxGuildsPerBatch = 100;
+ public const int MaxAuditLogEntriesPerBatch = 100;
/// Gets or sets how a request should act in the case of an error, by default.
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;
diff --git a/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs
new file mode 100644
index 000000000..e5a4ff30a
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// The action type within a
+ ///
+ 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
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogChange.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogChange.cs
new file mode 100644
index 000000000..0c36b53b2
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogChange.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents changes which may occur within a
+ ///
+ public interface IAuditLogChange
+ {
+
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs
new file mode 100644
index 000000000..0569f5fff
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents an entry in an audit log
+ ///
+ public interface IAuditLogEntry : IEntity
+ {
+ ///
+ /// The action which occured to create this entry
+ ///
+ ActionType Action { get; }
+
+ ///
+ /// The changes which occured within this entry. May be empty if no changes occured.
+ ///
+ IReadOnlyCollection Changes { get; }
+
+ ///
+ /// Any options which apply to this entry. If no options were provided, this may be .
+ ///
+ IAuditLogOptions Options { get; }
+
+ ///
+ /// The id which the target applies to
+ ///
+ ulong TargetId { get; }
+
+ ///
+ /// The user responsible for causing the changes
+ ///
+ IUser User { get; }
+
+ ///
+ /// The reason behind the change. May be if no reason was provided.
+ ///
+ string Reason { get; }
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogOptions.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogOptions.cs
new file mode 100644
index 000000000..1ca12255e
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogOptions.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents options which may be applied to an
+ ///
+ public interface IAuditLogOptions
+ {
+
+ }
+}
diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
index 506cbd3e4..4c7ade43b 100644
--- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
+++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
@@ -114,5 +114,8 @@ namespace Discord
Task DownloadUsersAsync();
/// 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.
Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null);
+
+ Task> GetAuditLogAsync(int limit = DiscordConfig.MaxAuditLogEntriesPerBatch,
+ CacheMode cacheMode = CacheMode.AllowDownload, RequestOptions options = null);
}
}
\ No newline at end of file
diff --git a/src/Discord.Net.Rest/API/Common/AuditLog.cs b/src/Discord.Net.Rest/API/Common/AuditLog.cs
new file mode 100644
index 000000000..f2147cb0e
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/AuditLog.cs
@@ -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; }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/AuditLogChange.cs b/src/Discord.Net.Rest/API/Common/AuditLogChange.cs
new file mode 100644
index 000000000..44e585021
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/AuditLogChange.cs
@@ -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; }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs
new file mode 100644
index 000000000..ecdb12650
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/AuditLogEntry.cs
@@ -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; }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs
new file mode 100644
index 000000000..bbda95405
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs
@@ -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; }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs b/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs
new file mode 100644
index 000000000..fc3e0d9f3
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs
@@ -0,0 +1,8 @@
+namespace Discord.API.Rest
+{
+ class GetAuditLogsParams
+ {
+ public Optional Limit { get; set; }
+ public Optional AfterEntryId { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs
index a632e5d42..c7b1b23c5 100644
--- a/src/Discord.Net.Rest/DiscordRestApiClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs
@@ -1059,6 +1059,21 @@ namespace Discord.API
return await SendJsonAsync>("PATCH", () => $"guilds/{guildId}/roles", args, ids, options: options).ConfigureAwait(false);
}
+ //Audit logs
+ public async Task 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> endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}&after={afterEntryId}";
+ return await SendAsync("GET", endpoint, ids, options: options).ConfigureAwait(false);
+ }
+
//Users
public async Task GetUserAsync(ulong userId, RequestOptions options = null)
{
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs
new file mode 100644
index 000000000..7e349e110
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs
@@ -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.");
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/Changes/MemberRoleAuditLogChange.cs b/src/Discord.Net.Rest/Entities/AuditLogs/Changes/MemberRoleAuditLogChange.cs
new file mode 100644
index 000000000..c5f167d6f
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/Changes/MemberRoleAuditLogChange.cs
@@ -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("id");
+ }
+
+ public bool RoleAdded { get; set; }
+ //TODO: convert to IRole
+ public ulong RoleId { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/Options/MessageDeleteAuditLogOptions.cs b/src/Discord.Net.Rest/Entities/AuditLogs/Options/MessageDeleteAuditLogOptions.cs
new file mode 100644
index 000000000..3d53ed790
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/Options/MessageDeleteAuditLogOptions.cs
@@ -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; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs
new file mode 100644
index 000000000..6efe4e88e
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs
@@ -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, 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();
+
+ 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 Changes { get; }
+ public IAuditLogOptions Options { get; }
+ public ulong TargetId { get; }
+ public IUser User { get; }
+ public string Reason { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
index 98303cea6..e568bf402 100644
--- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
@@ -247,5 +247,34 @@ namespace Discord.Rest
model = await client.ApiClient.BeginGuildPruneAsync(guild.Id, args, options).ConfigureAwait(false);
return model.Pruned;
}
+
+ public static IAsyncEnumerable> GetAuditLogsAsync(IGuild guild, BaseDiscordClient client,
+ ulong? from, int? limit, RequestOptions options)
+ {
+ return new PagedAsyncEnumerable(
+ 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
+ );
+
+ }
}
}
diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
index 8b5598ffe..23f6353bf 100644
--- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
+++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
@@ -239,6 +239,10 @@ namespace Discord.Rest
public Task PruneUsersAsync(int days = 30, bool simulate = false, RequestOptions options = null)
=> GuildHelper.PruneUsersAsync(this, Discord, days, simulate, options);
+ //Audit logs
+ public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null)
+ => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options);
+
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})";
@@ -361,5 +365,13 @@ namespace Discord.Rest
return ImmutableArray.Create();
}
Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); }
+
+ async Task> 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();
+ }
}
}
diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
index 5358605c8..72beac217 100644
--- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
+++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
@@ -423,6 +423,10 @@ namespace Discord.WebSocket
_downloaderPromise.TrySetResultAsync(true);
}
+ //Audit logs
+ public IAsyncEnumerable> GetAuditLogsAsync(int limit, RequestOptions options = null)
+ => GuildHelper.GetAuditLogsAsync(this, Discord, null, limit, options);
+
//Voice States
internal async Task AddOrUpdateVoiceStateAsync(ClientState state, VoiceStateModel model)
{
@@ -659,5 +663,13 @@ namespace Discord.WebSocket
Task IGuild.GetOwnerAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult(Owner);
Task IGuild.DownloadUsersAsync() { throw new NotSupportedException(); }
+
+ async Task> 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();
+ }
}
}