2018-02-20 08:48:11 +00:00
|
|
|
|
using Discord;
|
|
|
|
|
using Discord.WebSocket;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
using Npgsql;
|
|
|
|
|
using NpgsqlTypes;
|
2018-02-17 08:45:40 +00:00
|
|
|
|
using System;
|
2018-02-20 20:26:10 +00:00
|
|
|
|
using System.Text;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
2018-02-17 08:45:40 +00:00
|
|
|
|
namespace Noikoio.RegexBot.Module.ModLogs
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
2018-03-02 03:25:08 +00:00
|
|
|
|
/// Helper class for <see cref="ModLogs"/>. Keeps a database-backed cache of recent messages for use
|
2018-02-17 08:45:40 +00:00
|
|
|
|
/// in reporting message changes and deletions, if configured to do so.
|
2018-03-02 03:25:08 +00:00
|
|
|
|
/// Despite its place, it does not manipulate moderation logs. It simply pulls from the same configuration.
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// </summary>
|
2018-02-17 08:45:40 +00:00
|
|
|
|
class MessageCache
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
2018-02-17 08:45:40 +00:00
|
|
|
|
private readonly DiscordSocketClient _dClient;
|
|
|
|
|
private readonly AsyncLogger _outLog;
|
2018-03-27 22:15:13 +00:00
|
|
|
|
private readonly Func<ulong, GuildState> _outGetConfig;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
|
2018-02-23 02:04:27 +00:00
|
|
|
|
// TODO: How to clear the cache after a time? Can't hold on to this forever.
|
2018-04-05 19:14:40 +00:00
|
|
|
|
// TODO Do not store messages at all if features is disabled.
|
2018-02-23 02:04:27 +00:00
|
|
|
|
|
2018-03-27 22:15:13 +00:00
|
|
|
|
public MessageCache(DiscordSocketClient client, AsyncLogger logger, Func<ulong, GuildState> getConfFunc)
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
2018-02-17 08:45:40 +00:00
|
|
|
|
_dClient = client;
|
|
|
|
|
_outLog = logger;
|
|
|
|
|
_outGetConfig = getConfFunc;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
|
2018-02-17 08:45:40 +00:00
|
|
|
|
CreateCacheTables();
|
2018-02-11 03:34:13 +00:00
|
|
|
|
|
2018-02-17 08:45:40 +00:00
|
|
|
|
client.MessageReceived += Client_MessageReceived;
|
|
|
|
|
client.MessageUpdated += Client_MessageUpdated;
|
|
|
|
|
client.MessageDeleted += Client_MessageDeleted;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Event handling
|
2018-02-20 20:26:10 +00:00
|
|
|
|
private async Task Client_MessageReceived(SocketMessage arg)
|
|
|
|
|
{
|
2018-04-05 22:46:15 +00:00
|
|
|
|
if (arg.Author.IsWebhook) return;
|
|
|
|
|
if (arg.Channel is IDMChannel) return; // No DMs
|
2018-02-20 20:26:10 +00:00
|
|
|
|
|
|
|
|
|
await AddOrUpdateCacheItemAsync(arg);
|
|
|
|
|
}
|
2018-02-17 08:45:40 +00:00
|
|
|
|
|
2018-02-20 08:48:11 +00:00
|
|
|
|
private async Task Client_MessageUpdated(
|
2018-02-20 20:26:10 +00:00
|
|
|
|
Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel)
|
2018-02-17 08:45:40 +00:00
|
|
|
|
{
|
2018-04-05 22:46:15 +00:00
|
|
|
|
if (after.Author.IsWebhook) return;
|
2018-02-20 20:26:10 +00:00
|
|
|
|
|
|
|
|
|
// We only want channel messages
|
|
|
|
|
if (after is SocketUserMessage afterMsg && !(afterMsg is IDMChannel))
|
2018-02-20 08:48:11 +00:00
|
|
|
|
{
|
|
|
|
|
// We're not interested in all message updates, only those that leave a timestamp.
|
|
|
|
|
if (!afterMsg.EditedTimestamp.HasValue) return;
|
|
|
|
|
}
|
2018-02-20 20:26:10 +00:00
|
|
|
|
else return; // probably unnecessary?
|
2018-02-20 08:48:11 +00:00
|
|
|
|
|
2018-02-23 02:04:27 +00:00
|
|
|
|
// Once an edited message is cached, the original message contents are lost.
|
|
|
|
|
// This is the only time available to report it.
|
2018-02-20 08:48:11 +00:00
|
|
|
|
await ProcessReportMessage(false, before.Id, channel, after.Content);
|
|
|
|
|
|
|
|
|
|
await AddOrUpdateCacheItemAsync(after);
|
2018-02-17 08:45:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-20 20:26:10 +00:00
|
|
|
|
private async Task Client_MessageDeleted(Cacheable<Discord.IMessage, ulong> msg, ISocketMessageChannel channel)
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
2018-03-02 03:25:08 +00:00
|
|
|
|
if (channel is IDMChannel) return; // No DMs
|
2018-02-20 08:48:11 +00:00
|
|
|
|
await ProcessReportMessage(true, msg.Id, channel, null);
|
2018-02-11 03:34:13 +00:00
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
2018-02-20 08:48:11 +00:00
|
|
|
|
#region Reporting
|
|
|
|
|
// Reports an edited or deleted message as if it were a log entry (even though it's not).
|
|
|
|
|
private async Task ProcessReportMessage(
|
|
|
|
|
bool isDelete, ulong messageId, ISocketMessageChannel ch, string editMsg)
|
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
ulong guildId;
|
|
|
|
|
if (ch is SocketTextChannel sch)
|
2018-02-20 08:48:11 +00:00
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
if (sch is IDMChannel) return;
|
|
|
|
|
guildId = sch.Guild.Id;
|
2018-02-20 08:48:11 +00:00
|
|
|
|
}
|
2018-02-20 20:26:10 +00:00
|
|
|
|
else return;
|
2018-02-20 08:48:11 +00:00
|
|
|
|
|
2018-02-23 02:04:27 +00:00
|
|
|
|
// Check if this feature is enabled before doing anything else.
|
2018-03-22 07:30:22 +00:00
|
|
|
|
var cfg = _outGetConfig(guildId);
|
2018-03-02 03:25:08 +00:00
|
|
|
|
if (cfg == null) return;
|
2018-04-05 22:46:15 +00:00
|
|
|
|
if (cfg.RptIgnore != 0 && ch.Id == cfg.RptIgnore) return; // ignored channel
|
|
|
|
|
if (isDelete && (cfg.RptTypes & LogEntry.LogType.MsgDelete) == 0) return; // not reporting deletions
|
|
|
|
|
if (!isDelete && (cfg.RptTypes & LogEntry.LogType.MsgEdit) == 0) return; // not reporting edits
|
2018-02-23 02:04:27 +00:00
|
|
|
|
|
|
|
|
|
// Regardless of delete or edit, it is necessary to get the equivalent database information.
|
2018-02-20 08:48:11 +00:00
|
|
|
|
EntityCache.CacheUser ucd = null;
|
|
|
|
|
ulong userId;
|
|
|
|
|
string cacheMsg;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync())
|
|
|
|
|
{
|
|
|
|
|
using (var c = db.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
c.CommandText = "SELECT author_id, message FROM " + TableMessage
|
|
|
|
|
+ " WHERE message_id = @MessageId";
|
|
|
|
|
c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = messageId;
|
|
|
|
|
c.Prepare();
|
|
|
|
|
using (var r = await c.ExecuteReaderAsync())
|
|
|
|
|
{
|
|
|
|
|
if (await r.ReadAsync())
|
|
|
|
|
{
|
|
|
|
|
userId = unchecked((ulong)r.GetInt64(0));
|
|
|
|
|
cacheMsg = r.GetString(1);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
userId = 0;
|
|
|
|
|
cacheMsg = "*(Message not in cache.)*";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-27 22:15:13 +00:00
|
|
|
|
if (userId != 0) ucd = await EntityCache.EntityCache.QueryUserAsync(guildId, userId);
|
2018-02-20 08:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
catch (NpgsqlException ex)
|
|
|
|
|
{
|
|
|
|
|
await _outLog($"SQL error in {nameof(ProcessReportMessage)}: " + ex.Message);
|
|
|
|
|
cacheMsg = "**Database error. See log.**";
|
|
|
|
|
}
|
|
|
|
|
|
2018-04-05 22:46:15 +00:00
|
|
|
|
// Prepare and send out message
|
2018-02-23 02:04:27 +00:00
|
|
|
|
var em = CreateReportEmbed(isDelete, ucd, messageId, ch, (cacheMsg, editMsg));
|
2018-04-05 22:46:15 +00:00
|
|
|
|
await cfg.RptTarget.SendMessageAsync("", embeds: new Embed[] { em });
|
2018-02-20 08:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-20 20:26:10 +00:00
|
|
|
|
const int ReportCutoffLength = 500;
|
2018-02-20 08:48:11 +00:00
|
|
|
|
const string ReportCutoffNotify = "**Message length too long; showing first {0} characters.**\n\n";
|
|
|
|
|
private EmbedBuilder CreateReportEmbed(
|
|
|
|
|
bool isDelete,
|
|
|
|
|
EntityCache.CacheUser ucd, ulong messageId, ISocketMessageChannel chInfo,
|
2018-02-23 02:04:27 +00:00
|
|
|
|
(string, string) content) // Item1 = cached content. Item2 = after-edit message (null if isDelete)
|
2018-02-20 08:48:11 +00:00
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
string msgCached = content.Item1;
|
|
|
|
|
string msgPostEdit = content.Item2;
|
2018-02-20 08:48:11 +00:00
|
|
|
|
if (content.Item1.Length > ReportCutoffLength)
|
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
msgCached = string.Format(ReportCutoffNotify, ReportCutoffLength)
|
|
|
|
|
+ content.Item1.Substring(0, ReportCutoffLength);
|
2018-02-20 08:48:11 +00:00
|
|
|
|
}
|
2018-02-20 20:26:10 +00:00
|
|
|
|
if (!isDelete && content.Item2.Length > ReportCutoffLength)
|
2018-02-20 08:48:11 +00:00
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
msgPostEdit = string.Format(ReportCutoffNotify, ReportCutoffLength)
|
|
|
|
|
+ content.Item2.Substring(0, ReportCutoffLength);
|
2018-02-20 08:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-23 02:04:27 +00:00
|
|
|
|
// Note: Value for ucb can be null if cached user could not be determined.
|
2018-02-20 08:48:11 +00:00
|
|
|
|
var eb = new EmbedBuilder
|
|
|
|
|
{
|
|
|
|
|
Author = new EmbedAuthorBuilder()
|
|
|
|
|
{
|
|
|
|
|
IconUrl = ucd?.AvatarUrl
|
|
|
|
|
},
|
|
|
|
|
Fields = new System.Collections.Generic.List<EmbedFieldBuilder>(),
|
|
|
|
|
Footer = new EmbedFooterBuilder()
|
|
|
|
|
{
|
2018-04-05 22:46:15 +00:00
|
|
|
|
Text = "User ID: " + ucd?.UserId.ToString() ?? "Unknown"
|
2018-02-20 08:48:11 +00:00
|
|
|
|
},
|
2018-02-20 20:26:10 +00:00
|
|
|
|
Timestamp = DateTimeOffset.UtcNow
|
2018-02-20 08:48:11 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isDelete)
|
|
|
|
|
{
|
2018-02-23 02:04:27 +00:00
|
|
|
|
eb.Author.Name = "Deleted message by ";
|
|
|
|
|
eb.Color = new Color(0xff7373);
|
2018-02-20 20:26:10 +00:00
|
|
|
|
eb.Description = msgCached;
|
2018-02-20 08:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2018-02-23 02:04:27 +00:00
|
|
|
|
eb.Author.Name = "Edited message by ";
|
|
|
|
|
eb.Color = new Color(0xffcc40);
|
2018-02-20 08:48:11 +00:00
|
|
|
|
eb.Fields.Add(new EmbedFieldBuilder()
|
|
|
|
|
{
|
|
|
|
|
Name = "Before",
|
2018-02-20 20:26:10 +00:00
|
|
|
|
Value = msgCached
|
2018-02-20 08:48:11 +00:00
|
|
|
|
});
|
|
|
|
|
eb.Fields.Add(new EmbedFieldBuilder()
|
|
|
|
|
{
|
|
|
|
|
Name = "After",
|
2018-02-20 20:26:10 +00:00
|
|
|
|
Value = msgPostEdit
|
2018-02-20 08:48:11 +00:00
|
|
|
|
});
|
|
|
|
|
}
|
2018-02-20 20:26:10 +00:00
|
|
|
|
|
|
|
|
|
eb.Author.Name += ucd == null ? "unknown user" : $"{ucd.Username}#{ucd.Discriminator}";
|
|
|
|
|
|
|
|
|
|
var context = new StringBuilder();
|
|
|
|
|
if (ucd != null) context.AppendLine($"Username: <@!{ucd.UserId}>");
|
|
|
|
|
context.AppendLine($"Channel: <#{chInfo.Id}> #{chInfo.Name}");
|
|
|
|
|
context.Append($"Message ID: {messageId}");
|
2018-02-20 08:48:11 +00:00
|
|
|
|
eb.Fields.Add(new EmbedFieldBuilder()
|
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
Name = "Context",
|
|
|
|
|
Value = context.ToString()
|
2018-02-20 08:48:11 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return eb;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Database storage/retrieval
|
2018-02-17 08:45:40 +00:00
|
|
|
|
const string TableMessage = "cache_messages";
|
|
|
|
|
|
2018-02-11 03:34:13 +00:00
|
|
|
|
private void CreateCacheTables()
|
|
|
|
|
{
|
2018-02-17 08:45:40 +00:00
|
|
|
|
using (var db = RegexBot.Config.GetOpenDatabaseConnectionAsync().GetAwaiter().GetResult())
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
using (var c = db.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
c.CommandText = "CREATE TABLE IF NOT EXISTS " + TableMessage + " ("
|
|
|
|
|
+ "message_id bigint primary key, "
|
|
|
|
|
+ "author_id bigint not null, "
|
|
|
|
|
+ "guild_id bigint not null, "
|
2018-02-20 08:48:11 +00:00
|
|
|
|
+ "channel_id bigint not null, " // TODO channel cache fk when that gets implemented
|
2018-02-11 03:34:13 +00:00
|
|
|
|
+ "created_ts timestamptz not null, "
|
|
|
|
|
+ "edited_ts timestamptz null, "
|
|
|
|
|
+ "message text not null, "
|
2018-02-17 08:45:40 +00:00
|
|
|
|
+ $"FOREIGN KEY (author_id, guild_id) references {EntityCache.SqlHelper.TableUser} (user_id, guild_id)"
|
2018-02-11 03:34:13 +00:00
|
|
|
|
+ ")";
|
|
|
|
|
c.ExecuteNonQuery();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-20 08:48:11 +00:00
|
|
|
|
private async Task AddOrUpdateCacheItemAsync(SocketMessage msg)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync())
|
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
using (var c = db.CreateCommand())
|
2018-02-20 08:48:11 +00:00
|
|
|
|
{
|
2018-02-20 20:26:10 +00:00
|
|
|
|
c.CommandText = "INSERT INTO " + TableMessage
|
|
|
|
|
+ " (message_id, author_id, guild_id, channel_id, created_ts, edited_ts, message) VALUES"
|
|
|
|
|
+ " (@MessageId, @UserId, @GuildId, @ChannelId, @Date, @Edit, @Message)"
|
|
|
|
|
+ " ON CONFLICT (message_id) DO UPDATE"
|
|
|
|
|
+ " SET message = EXCLUDED.message, edited_ts = EXCLUDED.edited_ts";
|
|
|
|
|
c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = msg.Id;
|
|
|
|
|
c.Parameters.Add("@UserId", NpgsqlDbType.Bigint).Value = msg.Author.Id;
|
|
|
|
|
c.Parameters.Add("@GuildId", NpgsqlDbType.Bigint).Value = ((SocketGuildUser)msg.Author).Guild.Id;
|
|
|
|
|
c.Parameters.Add("@ChannelId", NpgsqlDbType.Bigint).Value = msg.Channel.Id;
|
|
|
|
|
c.Parameters.Add("@Date", NpgsqlDbType.TimestampTZ).Value = msg.Timestamp;
|
|
|
|
|
if (msg.EditedTimestamp.HasValue)
|
|
|
|
|
c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = msg.EditedTimestamp.Value;
|
|
|
|
|
else
|
|
|
|
|
c.Parameters.Add("@Edit", NpgsqlDbType.TimestampTZ).Value = DBNull.Value;
|
|
|
|
|
c.Parameters.Add("@Message", NpgsqlDbType.Text).Value = msg.Content;
|
|
|
|
|
c.Prepare();
|
|
|
|
|
await c.ExecuteNonQueryAsync();
|
2018-02-20 08:48:11 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (NpgsqlException ex)
|
|
|
|
|
{
|
|
|
|
|
await _outLog($"SQL error in {nameof(AddOrUpdateCacheItemAsync)}: " + ex.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task<string> GetCachedMessageAsync(ulong messageId)
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2018-02-17 08:45:40 +00:00
|
|
|
|
using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync())
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
using (var c = db.CreateCommand())
|
|
|
|
|
{
|
2018-02-20 08:48:11 +00:00
|
|
|
|
c.CommandText = "SELECT message FROM " + TableMessage
|
|
|
|
|
+ " WHERE message_id = @MessageId";
|
|
|
|
|
c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = messageId;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
c.Prepare();
|
2018-02-20 08:48:11 +00:00
|
|
|
|
using (var r = await c.ExecuteReaderAsync())
|
|
|
|
|
{
|
|
|
|
|
if (await r.ReadAsync())
|
|
|
|
|
return r.GetString(0);
|
|
|
|
|
else
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2018-02-11 03:34:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (NpgsqlException ex)
|
|
|
|
|
{
|
2018-02-20 08:48:11 +00:00
|
|
|
|
await _outLog($"SQL error in {nameof(GetCachedMessageAsync)}: " + ex.Message);
|
|
|
|
|
return null;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-02-20 08:48:11 +00:00
|
|
|
|
#endregion
|
2018-02-11 03:34:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|