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/IAuditLogData.cs b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs
new file mode 100644
index 000000000..47aaffb26
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord
+{
+ ///
+ /// Represents data applied to an
+ ///
+ public interface IAuditLogData
+ { }
+}
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..b85730a1d
--- /dev/null
+++ b/src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs
@@ -0,0 +1,34 @@
+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 data for this entry. May be if no data was available.
+ ///
+ IAuditLogData Data { 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/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
index 3ded9e038..07f01a06b 100644
--- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
+++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs
@@ -117,5 +117,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..964399d92
--- /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 AuditLogWebhook[] 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..80d9a9e97
--- /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..c5b229337
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/AuditLogOptions.cs
@@ -0,0 +1,28 @@
+using Newtonsoft.Json;
+
+namespace Discord.API
+{
+ //TODO: Complete this with all possible values for options
+ internal class AuditLogOptions
+ {
+ //Message delete
+ [JsonProperty("count")]
+ public int? MessageDeleteCount { get; set; } //TODO: what type of int? (returned as string)
+ [JsonProperty("channel_id")]
+ public ulong? MessageDeleteChannelId { get; set; }
+
+ //Prune
+ [JsonProperty("delete_member_days")]
+ public int? PruneDeleteMemberDays { get; set; } //TODO: what type of int? (returned as string)
+ [JsonProperty("members_removed")]
+ public int? PruneMembersRemoved { get; set; } //TODO: what type of int? (returned as string)
+
+ //Overwrite Update
+ [JsonProperty("role_name")]
+ public string OverwriteRoleName { get; set; }
+ [JsonProperty("type")]
+ public string OverwriteType { get; set; }
+ [JsonProperty("id")]
+ public ulong? OverwriteTargetId { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/API/Common/AuditLogWebhook.cs b/src/Discord.Net.Rest/API/Common/AuditLogWebhook.cs
new file mode 100644
index 000000000..12d8bfba7
--- /dev/null
+++ b/src/Discord.Net.Rest/API/Common/AuditLogWebhook.cs
@@ -0,0 +1,16 @@
+using Newtonsoft.Json;
+
+namespace Discord.API
+{
+ internal class AuditLogWebhook : User
+ {
+ [JsonProperty("channel_id")]
+ public ulong ChannelId { get; set; }
+
+ [JsonProperty("guild_id")]
+ public ulong GuildId { get; set; }
+
+ [JsonProperty("token")]
+ public string Token { 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..ceabccbc8
--- /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 BeforeEntryId { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs
index 6d551aa95..3fb41107e 100644
--- a/src/Discord.Net.Rest/DiscordRestApiClient.cs
+++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs
@@ -1066,6 +1066,26 @@ 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);
+
+ var ids = new BucketIds(guildId: guildId);
+ Expression> endpoint;
+
+ if (args.BeforeEntryId.IsSpecified)
+ endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}&before={args.BeforeEntryId.Value}";
+ else
+ endpoint = () => $"guilds/{guildId}/audit-logs?limit={limit}";
+
+ 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..541f008b0
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs
@@ -0,0 +1,79 @@
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ internal static class AuditLogHelper
+ {
+ public static IAuditLogData CreateData(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ switch (entry.Action)
+ {
+ case ActionType.GuildUpdated: //1
+ return GuildUpdateAuditLogData.Create(discord, log, entry);
+
+ case ActionType.ChannelCreated: //10
+ return ChannelCreateAuditLogData.Create(discord, log, entry);
+ case ActionType.ChannelUpdated:
+ return ChannelUpdateAuditLogData.Create(discord, log, entry);
+ case ActionType.ChannelDeleted:
+ return ChannelDeleteAuditLogData.Create(discord, log, entry);
+ case ActionType.OverwriteCreated:
+ return OverwriteCreateAuditLogData.Create(discord, log, entry);
+ case ActionType.OverwriteUpdated:
+ return OverwriteUpdateAuditLogData.Create(discord, log, entry);
+ case ActionType.OverwriteDeleted:
+ return OverwriteDeleteAuditLogData.Create(discord, log, entry);
+
+ case ActionType.Kick: //20
+ return KickAuditLogData.Create(discord, log, entry);
+ case ActionType.Prune:
+ return PruneAuditLogData.Create(discord, log, entry);
+ case ActionType.Ban:
+ return BanAuditLogData.Create(discord, log, entry);
+ case ActionType.Unban:
+ return UnbanAuditLogData.Create(discord, log, entry);
+ case ActionType.MemberUpdated:
+ return MemberUpdateAuditLogData.Create(discord, log, entry);
+ case ActionType.MemberRoleUpdated:
+ return MemberRoleAuditLogData.Create(discord, log, entry);
+
+ case ActionType.RoleCreated: //30
+ return RoleCreateAuditLogData.Create(discord, log, entry);
+ case ActionType.RoleUpdated:
+ return RoleUpdateAuditLogData.Create(discord, log, entry);
+ case ActionType.RoleDeleted:
+ return RoleDeleteAuditLogData.Create(discord, log, entry);
+
+ case ActionType.InviteCreated: //40
+ return InviteCreateAuditLogData.Create(discord, log, entry);
+ case ActionType.InviteUpdated:
+ break;
+ case ActionType.InviteDeleted:
+ return InviteDeleteAuditLogData.Create(discord, log, entry);
+
+ case ActionType.WebhookCreated: //50
+ return WebhookCreateAuditLogData.Create(discord, log, entry);
+ case ActionType.WebhookUpdated:
+ return WebhookUpdateAuditLogData.Create(discord, log, entry);
+ case ActionType.WebhookDeleted:
+ return WebhookDeleteAuditLogData.Create(discord, log, entry);
+
+ case ActionType.EmojiCreated: //60
+ return EmoteCreateAuditLogData.Create(discord, log, entry);
+ case ActionType.EmojiUpdated:
+ return EmoteUpdateAuditLogData.Create(discord, log, entry);
+ case ActionType.EmojiDeleted:
+ return EmoteDeleteAuditLogData.Create(discord, log, entry);
+
+ case ActionType.MessageDeleted: //72
+ return MessageDeleteAuditLogData.Create(discord, log, entry);
+
+ default: //Unknown
+ return null;
+ }
+ return null;
+ //throw new NotImplementedException($"{nameof(AuditLogHelper)} does not implement the {entry.Action} audit log event.");
+ }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs
new file mode 100644
index 000000000..4b9d5875f
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/BanAuditLogData.cs
@@ -0,0 +1,23 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class BanAuditLogData : IAuditLogData
+ {
+ private BanAuditLogData(IUser user)
+ {
+ Target = user;
+ }
+
+ internal static BanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
+ return new BanAuditLogData(RestUser.Create(discord, userInfo));
+ }
+
+ public IUser Target { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs
new file mode 100644
index 000000000..e0448a502
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs
@@ -0,0 +1,56 @@
+using Newtonsoft.Json.Linq;
+using System.Collections.Generic;
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class ChannelCreateAuditLogData : IAuditLogData
+ {
+ private ChannelCreateAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites)
+ {
+ ChannelId = id;
+ ChannelName = name;
+ ChannelType = type;
+ Overwrites = overwrites;
+ }
+
+ internal static ChannelCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+ var overwrites = new List();
+
+ var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites");
+ var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
+ var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
+
+ var type = typeModel.NewValue.ToObject();
+ var name = nameModel.NewValue.ToObject();
+
+ foreach (var overwrite in overwritesModel.NewValue)
+ {
+ var deny = overwrite.Value("deny");
+ var _type = overwrite.Value("type");
+ var id = overwrite.Value("id");
+ var allow = overwrite.Value("allow");
+
+ PermissionTarget permType;
+ if (_type == "member")
+ permType = PermissionTarget.User;
+ else
+ permType = PermissionTarget.Role;
+
+ overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny)));
+ }
+
+ return new ChannelCreateAuditLogData(entry.TargetId.Value, name, type, overwrites.ToReadOnlyCollection());
+ }
+
+ public ulong ChannelId { get; } //TODO: IChannel
+ public string ChannelName { get; }
+ public ChannelType ChannelType { get; }
+ public IReadOnlyCollection Overwrites { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs
new file mode 100644
index 000000000..b29205714
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelDeleteAuditLogData.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class ChannelDeleteAuditLogData : IAuditLogData
+ {
+ private ChannelDeleteAuditLogData(ulong id, string name, ChannelType type, IReadOnlyCollection overwrites)
+ {
+ ChannelId = id;
+ ChannelName = name;
+ ChannelType = type;
+ Overwrites = overwrites;
+ }
+
+ internal static ChannelDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var overwritesModel = changes.FirstOrDefault(x => x.ChangedProperty == "permission_overwrites");
+ var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
+ var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
+
+ var overwrites = overwritesModel.OldValue.ToObject()
+ .Select(x => new Overwrite(x.TargetId, x.TargetType, new OverwritePermissions(x.Allow, x.Deny)))
+ .ToList();
+ var type = typeModel.OldValue.ToObject();
+ var name = nameModel.OldValue.ToObject();
+ var id = entry.TargetId.Value; //NOTE: assuming this is the channel id here
+
+ return new ChannelDeleteAuditLogData(id, name, type, overwrites.ToReadOnlyCollection());
+ }
+
+ public ulong ChannelId { get; } //TODO: IChannel
+ public string ChannelName { get; }
+ public ChannelType ChannelType { get; }
+ public IReadOnlyCollection Overwrites { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs
new file mode 100644
index 000000000..ca7a9ce58
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelUpdateAuditLogData.cs
@@ -0,0 +1,67 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class ChannelUpdateAuditLogData : IAuditLogData
+ {
+ private ChannelUpdateAuditLogData(GuildChannelProperties before, GuildChannelProperties after)
+ {
+ Before = before;
+ After = after;
+ }
+
+ internal static ChannelUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
+ var topicModel = changes.FirstOrDefault(x => x.ChangedProperty == "topic");
+ var bitrateModel = changes.FirstOrDefault(x => x.ChangedProperty == "bitrate");
+ var userLimitModel = changes.FirstOrDefault(x => x.ChangedProperty == "user_limit");
+
+ if (topicModel != null) //If topic is supplied, we must be a text channel
+ {
+ var before = new TextChannelProperties
+ {
+ Name = nameModel?.OldValue?.ToObject(),
+ Topic = topicModel.OldValue?.ToObject()
+ };
+ var after = new TextChannelProperties
+ {
+ Name = nameModel?.NewValue?.ToObject(),
+ Topic = topicModel.NewValue?.ToObject()
+ };
+
+ return new ChannelUpdateAuditLogData(before, after);
+ }
+ else //By process of elimination, we must be a voice channel
+ {
+ var beforeBitrate = bitrateModel?.OldValue?.ToObject();
+ var afterBitrate = bitrateModel?.NewValue?.ToObject();
+ var beforeUserLimit = userLimitModel?.OldValue?.ToObject();
+ var afterUserLimit = userLimitModel?.NewValue?.ToObject();
+
+ var before = new VoiceChannelProperties
+ {
+ Name = nameModel?.OldValue?.ToObject(),
+ Bitrate = beforeBitrate ?? Optional.Unspecified,
+ UserLimit = beforeUserLimit
+ };
+ var after = new VoiceChannelProperties
+ {
+ Name = nameModel?.NewValue?.ToObject(),
+ Bitrate = afterBitrate ?? Optional.Unspecified,
+ UserLimit = afterUserLimit
+ };
+
+ return new ChannelUpdateAuditLogData(before, after);
+ }
+ }
+
+ public GuildChannelProperties Before { get; set; }
+ public GuildChannelProperties After { get; set; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs
new file mode 100644
index 000000000..30b85d4e4
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteCreateAuditLogData.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class EmoteCreateAuditLogData : IAuditLogData
+ {
+ private EmoteCreateAuditLogData(Emote emote)
+ {
+ Emote = emote;
+ }
+
+ internal static EmoteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); //TODO: only change?
+
+ var emoteName = change.NewValue?.ToObject();
+ var emote = new Emote(entry.TargetId.Value, emoteName);
+
+ return new EmoteCreateAuditLogData(emote);
+ }
+
+ public Emote Emote { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs
new file mode 100644
index 000000000..21f7b4076
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteDeleteAuditLogData.cs
@@ -0,0 +1,27 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class EmoteDeleteAuditLogData : IAuditLogData
+ {
+ private EmoteDeleteAuditLogData(Emote emote)
+ {
+ Emote = emote;
+ }
+
+ internal static EmoteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); //TODO: only change?
+
+ var emoteName = change.OldValue?.ToObject();
+ var emote = new Emote(entry.TargetId.Value, emoteName);
+
+ return new EmoteDeleteAuditLogData(emote);
+ }
+
+ public Emote Emote { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs
new file mode 100644
index 000000000..980c02195
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/EmoteUpdateAuditLogData.cs
@@ -0,0 +1,31 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class EmoteUpdateAuditLogData : IAuditLogData
+ {
+ private EmoteUpdateAuditLogData(Emote emote, string oldName)
+ {
+ Emote = emote;
+ PreviousName = oldName;
+ }
+
+ internal static EmoteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var change = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "name"); //TODO: only change?
+
+ var emoteName = change.NewValue?.ToObject();
+ var emote = new Emote(entry.TargetId.Value, emoteName);
+
+ var oldName = change.OldValue?.ToObject();
+
+ return new EmoteUpdateAuditLogData(emote, oldName);
+ }
+
+ public Emote Emote { get; }
+ public string PreviousName { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs
new file mode 100644
index 000000000..5d034d9d9
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/GuildUpdateAuditLogData.cs
@@ -0,0 +1,128 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class GuildUpdateAuditLogData : IAuditLogData
+ {
+ private GuildUpdateAuditLogData(GuildInfo before, GuildInfo after)
+ {
+ Before = before;
+ After = after;
+ }
+
+ internal static GuildUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ int? oldAfkTimeout = null,
+ newAfkTimeout = null;
+ DefaultMessageNotifications? oldDefaultMessageNotifications = null,
+ newDefaultMessageNotifications = null;
+ ulong? oldAfkChannelId = null,
+ newAfkChannelId = null;
+ string oldName = null,
+ newName = null;
+ string oldRegionId = null,
+ newRegionId = null;
+ string oldIconHash = null,
+ newIconHash = null;
+ VerificationLevel? oldVerificationLevel = null,
+ newVerificationLevel = null;
+ ulong? oldOwnerId = null,
+ newOwnerId = null;
+
+ foreach (var change in changes)
+ {
+ switch (change.ChangedProperty)
+ {
+ case "afk_timeout":
+ oldAfkTimeout = change.OldValue?.ToObject();
+ newAfkTimeout = change.NewValue?.ToObject();
+ break;
+ case "default_message_notifications":
+ oldDefaultMessageNotifications = change.OldValue?.ToObject();
+ newDefaultMessageNotifications = change.OldValue?.ToObject();
+ break;
+ case "afk_channel_id":
+ oldAfkChannelId = change.OldValue?.ToObject();
+ newAfkChannelId = change.NewValue?.ToObject();
+ break;
+ case "name":
+ oldName = change.OldValue?.ToObject();
+ newName = change.NewValue?.ToObject();
+ break;
+ case "region":
+ oldRegionId = change.OldValue?.ToObject();
+ newRegionId = change.NewValue?.ToObject();
+ break;
+ case "icon_hash":
+ oldIconHash = change.OldValue?.ToObject();
+ newIconHash = change.NewValue?.ToObject();
+ break;
+ case "verification_level":
+ oldVerificationLevel = change.OldValue?.ToObject();
+ newVerificationLevel = change.NewValue?.ToObject();
+ break;
+ case "owner":
+ oldOwnerId = change.OldValue?.ToObject();
+ newOwnerId = change.NewValue?.ToObject();
+ break;
+ //TODO: 2fa auth, explicit content filter
+ }
+ }
+
+ IUser oldOwner = null;
+ if (oldOwnerId != null)
+ {
+ var oldOwnerInfo = log.Users.FirstOrDefault(x => x.Id == oldOwnerId.Value);
+ oldOwner = RestUser.Create(discord, oldOwnerInfo);
+ }
+
+ IUser newOwner = null;
+ if (newOwnerId != null)
+ {
+ var newOwnerInfo = log.Users.FirstOrDefault(x => x.Id == newOwnerId.Value);
+ newOwner = RestUser.Create(discord, newOwnerInfo);
+ }
+
+ var before = new GuildInfo(oldAfkTimeout, oldDefaultMessageNotifications,
+ oldAfkChannelId, oldName, oldRegionId, oldIconHash, oldVerificationLevel, oldOwner);
+ var after = new GuildInfo(newAfkTimeout, newDefaultMessageNotifications,
+ newAfkChannelId, newName, newRegionId, newIconHash, newVerificationLevel, newOwner);
+
+ return new GuildUpdateAuditLogData(before, after);
+ }
+
+ public GuildInfo Before { get; }
+ public GuildInfo After { get; }
+
+ public struct GuildInfo
+ {
+ internal GuildInfo(int? afkTimeout, DefaultMessageNotifications? defaultNotifs,
+ ulong? afkChannel, string name, string region, string icon,
+ VerificationLevel? verification, IUser owner)
+ {
+ AfkTimeout = afkTimeout;
+ DefaultMessageNotifications = defaultNotifs;
+ AfkChannelId = afkChannel;
+ Name = name;
+ RegionId = region;
+ IconHash = icon;
+ VerificationLevel = verification;
+ Owner = owner;
+ }
+
+ public int? AfkTimeout { get; }
+ public DefaultMessageNotifications? DefaultMessageNotifications { get; }
+ public ulong? AfkChannelId { get; }
+ public string Name { get; }
+ public string RegionId { get; }
+ public string IconHash { get; }
+ public VerificationLevel? VerificationLevel { get; }
+ public IUser Owner { get; }
+ }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
new file mode 100644
index 000000000..85e2b0b9b
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteCreateAuditLogData.cs
@@ -0,0 +1,55 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class InviteCreateAuditLogData : IAuditLogData
+ {
+ private InviteCreateAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses)
+ {
+ MaxAge = maxAge;
+ Code = code;
+ Temporary = temporary;
+ Creator = inviter;
+ ChannelId = channelId;
+ Uses = uses;
+ MaxUses = maxUses;
+ }
+
+ internal static InviteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age");
+ var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code");
+ var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary");
+ var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id");
+ var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
+ var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses");
+ var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses");
+
+ var maxAge = maxAgeModel.NewValue.ToObject();
+ var code = codeModel.NewValue.ToObject();
+ var temporary = temporaryModel.NewValue.ToObject();
+ var inviterId = inviterIdModel.NewValue.ToObject();
+ var channelId = channelIdModel.NewValue.ToObject();
+ var uses = usesModel.NewValue.ToObject();
+ var maxUses = maxUsesModel.NewValue.ToObject();
+
+ var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
+ var inviter = RestUser.Create(discord, inviterInfo);
+
+ return new InviteCreateAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
+ }
+
+ public int MaxAge { get; }
+ public string Code { get; }
+ public bool Temporary { get; }
+ public IUser Creator { get; }
+ public ulong ChannelId { get; } //TODO: IChannel-ify
+ public int Uses { get; }
+ public int MaxUses { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
new file mode 100644
index 000000000..97c7a9b2c
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/InviteDeleteAuditLogData.cs
@@ -0,0 +1,55 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class InviteDeleteAuditLogData : IAuditLogData
+ {
+ private InviteDeleteAuditLogData(int maxAge, string code, bool temporary, IUser inviter, ulong channelId, int uses, int maxUses)
+ {
+ MaxAge = maxAge;
+ Code = code;
+ Temporary = temporary;
+ Creator = inviter;
+ ChannelId = channelId;
+ Uses = uses;
+ MaxUses = maxUses;
+ }
+
+ internal static InviteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var maxAgeModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_age");
+ var codeModel = changes.FirstOrDefault(x => x.ChangedProperty == "code");
+ var temporaryModel = changes.FirstOrDefault(x => x.ChangedProperty == "temporary");
+ var inviterIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "inviter_id");
+ var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
+ var usesModel = changes.FirstOrDefault(x => x.ChangedProperty == "uses");
+ var maxUsesModel = changes.FirstOrDefault(x => x.ChangedProperty == "max_uses");
+
+ var maxAge = maxAgeModel.OldValue.ToObject();
+ var code = codeModel.OldValue.ToObject();
+ var temporary = temporaryModel.OldValue.ToObject();
+ var inviterId = inviterIdModel.OldValue.ToObject();
+ var channelId = channelIdModel.OldValue.ToObject();
+ var uses = usesModel.OldValue.ToObject();
+ var maxUses = maxUsesModel.OldValue.ToObject();
+
+ var inviterInfo = log.Users.FirstOrDefault(x => x.Id == inviterId);
+ var inviter = RestUser.Create(discord, inviterInfo);
+
+ return new InviteDeleteAuditLogData(maxAge, code, temporary, inviter, channelId, uses, maxUses);
+ }
+
+ public int MaxAge { get; }
+ public string Code { get; }
+ public bool Temporary { get; }
+ public IUser Creator { get; }
+ public ulong ChannelId { get; } //TODO: IChannel-ify
+ public int Uses { get; }
+ public int MaxUses { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs
new file mode 100644
index 000000000..41b5526b8
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/KickAuditLogData.cs
@@ -0,0 +1,23 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class KickAuditLogData : IAuditLogData
+ {
+ private KickAuditLogData(RestUser user)
+ {
+ Target = user;
+ }
+
+ internal static KickAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
+ return new KickAuditLogData(RestUser.Create(discord, userInfo));
+ }
+
+ public IUser Target { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs
new file mode 100644
index 000000000..43d97ecf3
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberRoleAuditLogData.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class MemberRoleAuditLogData : IAuditLogData
+ {
+ private MemberRoleAuditLogData(IReadOnlyCollection roles, IUser target)
+ {
+ Roles = roles;
+ TargetUser = target;
+ }
+
+ internal static MemberRoleAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var roleInfos = changes.SelectMany(x => x.NewValue.ToObject(),
+ (model, role) => new { model.ChangedProperty, Role = role })
+ .Select(x => new RoleInfo(x.Role.Name, x.Role.Id, x.ChangedProperty == "$add"))
+ .ToList();
+
+ var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
+ var user = RestUser.Create(discord, userInfo);
+
+ return new MemberRoleAuditLogData(roleInfos.ToReadOnlyCollection(), user);
+ }
+
+ public IReadOnlyCollection Roles { get; }
+ public IUser TargetUser { get; }
+
+ public struct RoleInfo
+ {
+ internal RoleInfo(string name, ulong roleId, bool added)
+ {
+ Name = name;
+ RoleId = roleId;
+ Added = added;
+ }
+
+ string Name { get; }
+ ulong RoleId { get; }
+ bool Added { get; }
+ }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs
new file mode 100644
index 000000000..7bbcdc69a
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MemberUpdateAuditLogData.cs
@@ -0,0 +1,35 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+using ChangeModel = Discord.API.AuditLogChange;
+
+namespace Discord.Rest
+{
+ public class MemberUpdateAuditLogData : IAuditLogData
+ {
+ private MemberUpdateAuditLogData(IUser user, string newNick, string oldNick)
+ {
+ User = user;
+ NewNick = newNick;
+ OldNick = oldNick;
+ }
+
+ internal static MemberUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes.FirstOrDefault(x => x.ChangedProperty == "nick"); //TODO: only change?
+
+ var newNick = changes.NewValue?.ToObject();
+ var oldNick = changes.OldValue?.ToObject();
+
+ var targetInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
+ var user = RestUser.Create(discord, targetInfo);
+
+ return new MemberUpdateAuditLogData(user, newNick, oldNick);
+ }
+
+ public IUser User { get; }
+ public string NewNick { get; }
+ public string OldNick { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs
new file mode 100644
index 000000000..73af24209
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/MessageDeleteAuditLogData.cs
@@ -0,0 +1,22 @@
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class MessageDeleteAuditLogData : IAuditLogData
+ {
+ private MessageDeleteAuditLogData(ulong channelId, int count)
+ {
+ ChannelId = channelId;
+ MessageCount = count;
+ }
+
+ internal static MessageDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ return new MessageDeleteAuditLogData(entry.Options.MessageDeleteChannelId.Value, entry.Options.MessageDeleteCount.Value);
+ }
+
+ public int MessageCount { get; }
+ public ulong ChannelId { get; } //TODO: IChannel
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs
new file mode 100644
index 000000000..d58488136
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteCreateAuditLogData.cs
@@ -0,0 +1,37 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class OverwriteCreateAuditLogData : IAuditLogData
+ {
+ private OverwriteCreateAuditLogData(Overwrite overwrite)
+ {
+ Overwrite = overwrite;
+ }
+
+ internal static OverwriteCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
+ var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");
+
+ var deny = denyModel.NewValue.ToObject();
+ var allow = allowModel.NewValue.ToObject();
+
+ var permissions = new OverwritePermissions(allow, deny);
+
+ var id = entry.Options.OverwriteTargetId.Value;
+ var type = entry.Options.OverwriteType;
+
+ PermissionTarget target = type == "member" ? PermissionTarget.User : PermissionTarget.Role;
+
+ return new OverwriteCreateAuditLogData(new Overwrite(id, target, permissions));
+ }
+
+ public Overwrite Overwrite { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs
new file mode 100644
index 000000000..9c635d577
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteDeleteAuditLogData.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+using ChangeModel = Discord.API.AuditLogChange;
+using OptionModel = Discord.API.AuditLogOptions;
+
+namespace Discord.Rest
+{
+ public class OverwriteDeleteAuditLogData : IAuditLogData
+ {
+ private OverwriteDeleteAuditLogData(Overwrite deletedOverwrite)
+ {
+ Overwrite = deletedOverwrite;
+ }
+
+ internal static OverwriteDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
+ var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
+ var idModel = changes.FirstOrDefault(x => x.ChangedProperty == "id");
+ var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");
+
+ var deny = denyModel.OldValue.ToObject();
+ var type = typeModel.OldValue.ToObject(); //'role' or 'member', can't use PermissionsTarget :(
+ var id = idModel.OldValue.ToObject();
+ var allow = allowModel.OldValue.ToObject();
+
+ PermissionTarget target = type == "member" ? PermissionTarget.User : PermissionTarget.Role;
+
+ return new OverwriteDeleteAuditLogData(new Overwrite(id, target, new OverwritePermissions(allow, deny)));
+ }
+
+ public Overwrite Overwrite { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs
new file mode 100644
index 000000000..8ed9d1415
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/OverwriteUpdateAuditLogData.cs
@@ -0,0 +1,46 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class OverwriteUpdateAuditLogData : IAuditLogData
+ {
+ private OverwriteUpdateAuditLogData(OverwritePermissions before, OverwritePermissions after, ulong targetId, PermissionTarget targetType)
+ {
+ Before = before;
+ After = after;
+ OverwriteTargetId = targetId;
+ OverwriteType = targetType;
+ }
+
+ internal static OverwriteUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var denyModel = changes.FirstOrDefault(x => x.ChangedProperty == "deny");
+ var allowModel = changes.FirstOrDefault(x => x.ChangedProperty == "allow");
+
+ var beforeAllow = allowModel?.OldValue?.ToObject();
+ var afterAllow = allowModel?.NewValue?.ToObject();
+ var beforeDeny = denyModel?.OldValue?.ToObject();
+ var afterDeny = denyModel?.OldValue?.ToObject();
+
+ var beforePermissions = new OverwritePermissions(beforeAllow ?? 0, beforeDeny ?? 0);
+ var afterPermissions = new OverwritePermissions(afterAllow ?? 0, afterDeny ?? 0);
+
+ PermissionTarget target = entry.Options.OverwriteType == "member" ? PermissionTarget.User : PermissionTarget.Role;
+
+ return new OverwriteUpdateAuditLogData(beforePermissions, afterPermissions, entry.Options.OverwriteTargetId.Value, target);
+ }
+
+ //TODO: this is kind of janky. Should I leave it, create a custom type, or what?
+ public OverwritePermissions Before { get; }
+ public OverwritePermissions After { get; }
+
+ public ulong OverwriteTargetId { get; }
+ public PermissionTarget OverwriteType { get; }
+ //TODO: should we also include the role name if it is given?
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs
new file mode 100644
index 000000000..0005e304d
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/PruneAuditLogData.cs
@@ -0,0 +1,22 @@
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class PruneAuditLogData : IAuditLogData
+ {
+ private PruneAuditLogData(int pruneDays, int membersRemoved)
+ {
+ PruneDays = pruneDays;
+ MembersRemoved = membersRemoved;
+ }
+
+ internal static PruneAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ return new PruneAuditLogData(entry.Options.PruneDeleteMemberDays.Value, entry.Options.PruneMembersRemoved.Value);
+ }
+
+ public int PruneDays { get; }
+ public int MembersRemoved { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs
new file mode 100644
index 000000000..30457f38c
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleCreateAuditLogData.cs
@@ -0,0 +1,52 @@
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class RoleCreateAuditLogData : IAuditLogData
+ {
+ private RoleCreateAuditLogData(RoleProperties newProps)
+ {
+ Properties = newProps;
+ }
+
+ internal static RoleCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var newProps = new RoleProperties();
+
+ foreach (var model in changes)
+ {
+ switch (model.ChangedProperty)
+ {
+ case "color":
+ if (model.NewValue != null)
+ newProps.Color = new Color(model.NewValue.ToObject());
+ break;
+ case "mentionable":
+ if (model.NewValue != null)
+ newProps.Mentionable = model.NewValue.ToObject();
+ break;
+ case "hoist":
+ if (model.NewValue != null)
+ newProps.Hoist = model.NewValue.ToObject();
+ break;
+ case "name":
+ if (model.NewValue != null)
+ newProps.Name = model.NewValue.ToObject();
+ break;
+ case "permissions":
+ if (model.NewValue != null)
+ newProps.Permissions = new GuildPermissions(model.NewValue.ToObject());
+ break;
+ }
+ }
+
+ return new RoleCreateAuditLogData(newProps);
+ }
+
+ //TODO: replace this with something read-only
+ public RoleProperties Properties { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs
new file mode 100644
index 000000000..baeb27cc2
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleDeleteAuditLogData.cs
@@ -0,0 +1,52 @@
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class RoleDeleteAuditLogData : IAuditLogData
+ {
+ private RoleDeleteAuditLogData(RoleProperties properties)
+ {
+ Properties = properties;
+ }
+
+ internal static RoleDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var oldProps = new RoleProperties();
+
+ foreach (var model in changes)
+ {
+ switch (model.ChangedProperty)
+ {
+ case "color":
+ if (model.OldValue != null)
+ oldProps.Color = new Color(model.OldValue.ToObject());
+ break;
+ case "mentionable":
+ if (model.OldValue != null)
+ oldProps.Mentionable = model.OldValue.ToObject();
+ break;
+ case "hoist":
+ if (model.OldValue != null)
+ oldProps.Hoist = model.OldValue.ToObject();
+ break;
+ case "name":
+ if (model.OldValue != null)
+ oldProps.Name = model.OldValue.ToObject();
+ break;
+ case "permissions":
+ if (model.OldValue != null)
+ oldProps.Permissions = new GuildPermissions(model.OldValue.ToObject());
+ break;
+ }
+ }
+
+ return new RoleDeleteAuditLogData(oldProps);
+ }
+
+ //TODO: replace this with something read-only
+ public RoleProperties Properties { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs
new file mode 100644
index 000000000..4c8d2cf9b
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/RoleUpdateAuditLogData.cs
@@ -0,0 +1,65 @@
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class RoleUpdateAuditLogData : IAuditLogData
+ {
+ private RoleUpdateAuditLogData(RoleProperties oldProps, RoleProperties newProps)
+ {
+ Before = oldProps;
+ After = newProps;
+ }
+
+ internal static RoleUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var newProps = new RoleProperties();
+ var oldProps = new RoleProperties();
+
+ foreach (var model in changes)
+ {
+ switch (model.ChangedProperty)
+ {
+ case "color":
+ if (model.NewValue != null)
+ newProps.Color = new Color(model.NewValue.ToObject());
+ if (model.OldValue != null)
+ oldProps.Color = new Color(model.OldValue.ToObject());
+ break;
+ case "mentionable":
+ if (model.NewValue != null)
+ newProps.Mentionable = model.NewValue.ToObject();
+ if (model.OldValue != null)
+ oldProps.Mentionable = model.OldValue.ToObject();
+ break;
+ case "hoist":
+ if (model.NewValue != null)
+ newProps.Hoist = model.NewValue.ToObject();
+ if (model.OldValue != null)
+ oldProps.Hoist = model.OldValue.ToObject();
+ break;
+ case "name":
+ if (model.NewValue != null)
+ newProps.Name = model.NewValue.ToObject();
+ if (model.OldValue != null)
+ oldProps.Name = model.OldValue.ToObject();
+ break;
+ case "permissions":
+ if (model.NewValue != null)
+ newProps.Permissions = new GuildPermissions(model.NewValue.ToObject());
+ if (model.OldValue != null)
+ oldProps.Permissions = new GuildPermissions(model.OldValue.ToObject());
+ break;
+ }
+ }
+
+ return new RoleUpdateAuditLogData(oldProps, newProps);
+ }
+
+ //TODO: replace these with something read-only
+ public RoleProperties Before { get; }
+ public RoleProperties After { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs
new file mode 100644
index 000000000..c94f18271
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/UnbanAuditLogData.cs
@@ -0,0 +1,23 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class UnbanAuditLogData : IAuditLogData
+ {
+ private UnbanAuditLogData(IUser user)
+ {
+ Target = user;
+ }
+
+ internal static UnbanAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var userInfo = log.Users.FirstOrDefault(x => x.Id == entry.TargetId);
+ return new UnbanAuditLogData(RestUser.Create(discord, userInfo));
+ }
+
+ public IUser Target { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs
new file mode 100644
index 000000000..158ee4d4e
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookCreateAuditLogData.cs
@@ -0,0 +1,44 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class WebhookCreateAuditLogData : IAuditLogData
+ {
+ private WebhookCreateAuditLogData(IWebhookUser user, string token, string name, ulong channelId)
+ {
+ Webhook = user;
+ Token = token;
+ Name = name;
+ ChannelId = channelId;
+ }
+
+ internal static WebhookCreateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
+ var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
+ var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
+
+ var channelId = channelIdModel.NewValue.ToObject();
+ var type = typeModel.NewValue.ToObject(); //TODO: what on *earth* is this for
+ var name = nameModel.NewValue.ToObject();
+
+ var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId);
+ var userInfo = RestWebhookUser.Create(discord, null, webhookInfo, entry.TargetId.Value);
+
+ return new WebhookCreateAuditLogData(userInfo, webhookInfo.Token, name, channelId);
+ }
+
+ //Corresponds to the *current* data
+ public IWebhookUser Webhook { get; }
+ public string Token { get; }
+
+ //Corresponds to the *audit log* data
+ public string Name { get; }
+ public ulong ChannelId { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs
new file mode 100644
index 000000000..0121f48ad
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookDeleteAuditLogData.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class WebhookDeleteAuditLogData : IAuditLogData
+ {
+ private WebhookDeleteAuditLogData(ulong id, ulong channel, string name, string avatar)
+ {
+ WebhookId = id;
+ ChannelId = channel;
+ Name = name;
+ Avatar = avatar;
+ }
+
+ internal static WebhookDeleteAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
+ var typeModel = changes.FirstOrDefault(x => x.ChangedProperty == "type");
+ var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
+ var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash");
+
+ var channelId = channelIdModel.OldValue.ToObject();
+ var type = typeModel.OldValue.ToObject();
+ var name = nameModel.OldValue.ToObject();
+ var avatarHash = avatarHashModel?.OldValue?.ToObject();
+
+ return new WebhookDeleteAuditLogData(entry.TargetId.Value, channelId, name, avatarHash);
+ }
+
+ public ulong WebhookId { get; }
+ public ulong ChannelId { get; }
+ public string Name { get; }
+ public string Avatar { get; }
+ }
+}
diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs
new file mode 100644
index 000000000..3241832ba
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/WebhookUpdateAuditLogData.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class WebhookUpdateAuditLogData : IAuditLogData
+ {
+ private WebhookUpdateAuditLogData(IWebhookUser user, string token, WebhookInfo before, WebhookInfo after)
+ {
+ Webhook = user;
+ Token = token;
+ Before = before;
+ After = after;
+ }
+
+ internal static WebhookUpdateAuditLogData Create(BaseDiscordClient discord, Model log, EntryModel entry)
+ {
+ var changes = entry.Changes;
+
+ var nameModel = changes.FirstOrDefault(x => x.ChangedProperty == "name");
+ var channelIdModel = changes.FirstOrDefault(x => x.ChangedProperty == "channel_id");
+ var avatarHashModel = changes.FirstOrDefault(x => x.ChangedProperty == "avatar_hash");
+
+ var oldName = nameModel?.OldValue?.ToObject();
+ var oldChannelId = channelIdModel?.OldValue?.ToObject();
+ var oldAvatar = avatarHashModel?.OldValue?.ToObject();
+ var before = new WebhookInfo(oldName, oldChannelId, oldAvatar);
+
+ var newName = nameModel?.NewValue?.ToObject();
+ var newChannelId = channelIdModel?.NewValue?.ToObject();
+ var newAvatar = avatarHashModel?.NewValue?.ToObject();
+ var after = new WebhookInfo(newName, newChannelId, newAvatar);
+
+ var webhookInfo = log.Webhooks?.FirstOrDefault(x => x.Id == entry.TargetId);
+ var userInfo = RestWebhookUser.Create(discord, null, webhookInfo, entry.TargetId.Value);
+
+ return new WebhookUpdateAuditLogData(userInfo, webhookInfo.Token, before, after);
+ }
+
+ //Again, the *current* data
+ public IWebhookUser Webhook { get; }
+ public string Token { get; }
+
+ //And the *audit log* data
+ public WebhookInfo Before { get; }
+ public WebhookInfo After { get; }
+
+ public struct WebhookInfo
+ {
+ internal WebhookInfo(string name, ulong? channelId, string avatar)
+ {
+ Name = name;
+ ChannelId = channelId;
+ Avatar = avatar;
+ }
+
+ public string Name { get; }
+ public ulong? ChannelId { get; }
+ public string Avatar { 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..9e30a5014
--- /dev/null
+++ b/src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs
@@ -0,0 +1,38 @@
+using System.Linq;
+
+using Model = Discord.API.AuditLog;
+using EntryModel = Discord.API.AuditLogEntry;
+
+namespace Discord.Rest
+{
+ public class RestAuditLogEntry : RestEntity, IAuditLogEntry
+ {
+ private RestAuditLogEntry(BaseDiscordClient discord, Model fullLog, EntryModel model, IUser user)
+ : base(discord, model.Id)
+ {
+ Action = model.Action;
+ Data = AuditLogHelper.CreateData(discord, fullLog, model);
+ User = user;
+ Reason = model.Reason;
+ }
+
+ internal static RestAuditLogEntry Create(BaseDiscordClient discord, Model fullLog, EntryModel model)
+ {
+ var userInfo = fullLog.Users.FirstOrDefault(x => x.Id == model.UserId);
+ IUser user = null;
+ if (userInfo != null)
+ user = RestUser.Create(discord, userInfo);
+
+ return new RestAuditLogEntry(discord, fullLog, model, user);
+ }
+
+ ///
+ public ActionType Action { get; }
+ ///
+ public IAuditLogData Data { 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 2fa29928c..a1871db5f 100644
--- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
+++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
@@ -253,5 +253,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.BeforeEntryId = 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.Min(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 aee305951..bff893d03 100644
--- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
+++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
@@ -257,6 +257,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})";
@@ -386,5 +390,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 6001e4799..3e8d7d1e1 100644
--- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
+++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
@@ -433,6 +433,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)
{
@@ -672,5 +676,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();
+ }
}
}