Browse Source

Initial audit logs implementation

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
FiniteReality 8 years ago
parent
commit
04b8049cc7
19 changed files with 406 additions and 0 deletions
  1. +1
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  2. +50
    -0
      src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs
  3. +16
    -0
      src/Discord.Net.Core/Entities/AuditLogs/IAuditLogChange.cs
  4. +44
    -0
      src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs
  5. +16
    -0
      src/Discord.Net.Core/Entities/AuditLogs/IAuditLogOptions.cs
  6. +3
    -0
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  7. +17
    -0
      src/Discord.Net.Rest/API/Common/AuditLog.cs
  8. +17
    -0
      src/Discord.Net.Rest/API/Common/AuditLogChange.cs
  9. +26
    -0
      src/Discord.Net.Rest/API/Common/AuditLogEntry.cs
  10. +14
    -0
      src/Discord.Net.Rest/API/Common/AuditLogOptions.cs
  11. +8
    -0
      src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs
  12. +15
    -0
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  13. +37
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs
  14. +19
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/Changes/MemberRoleAuditLogChange.cs
  15. +23
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/Options/MessageDeleteAuditLogOptions.cs
  16. +47
    -0
      src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs
  17. +29
    -0
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  18. +12
    -0
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  19. +12
    -0
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs

+ 1
- 0
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -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;


+ 50
- 0
src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs View File

@@ -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
}
}

+ 16
- 0
src/Discord.Net.Core/Entities/AuditLogs/IAuditLogChange.cs View File

@@ -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
{

}
}

+ 44
- 0
src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs View File

@@ -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; }
}
}

+ 16
- 0
src/Discord.Net.Core/Entities/AuditLogs/IAuditLogOptions.cs View File

@@ -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
{

}
}

+ 3
- 0
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

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

+ 17
- 0
src/Discord.Net.Rest/API/Common/AuditLog.cs View File

@@ -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; }
}
}

+ 17
- 0
src/Discord.Net.Rest/API/Common/AuditLogChange.cs View File

@@ -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; }
}
}

+ 26
- 0
src/Discord.Net.Rest/API/Common/AuditLogEntry.cs View File

@@ -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; }
}
}

+ 14
- 0
src/Discord.Net.Rest/API/Common/AuditLogOptions.cs View File

@@ -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; }
}
}

+ 8
- 0
src/Discord.Net.Rest/API/Rest/GetAuditLogsParams.cs View File

@@ -0,0 +1,8 @@
namespace Discord.API.Rest
{
class GetAuditLogsParams
{
public Optional<int> Limit { get; set; }
public Optional<ulong> AfterEntryId { get; set; }
}
}

+ 15
- 0
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -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)
{ {


+ 37
- 0
src/Discord.Net.Rest/Entities/AuditLogs/AuditLogHelper.cs View File

@@ -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.");
}
}
}
}

+ 19
- 0
src/Discord.Net.Rest/Entities/AuditLogs/Changes/MemberRoleAuditLogChange.cs View File

@@ -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; }
}
}

+ 23
- 0
src/Discord.Net.Rest/Entities/AuditLogs/Options/MessageDeleteAuditLogOptions.cs View File

@@ -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; }
}
}

+ 47
- 0
src/Discord.Net.Rest/Entities/AuditLogs/RestAuditLogEntry.cs View File

@@ -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; }
}
}

+ 29
- 0
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -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
);

}
} }
} }

+ 12
- 0
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

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

+ 12
- 0
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

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

Loading…
Cancel
Save