Added mostly complete MessageCache
This commit is contained in:
parent
d21c0c1155
commit
69f9da5313
1 changed files with 224 additions and 30 deletions
|
@ -1,4 +1,5 @@
|
||||||
using Discord.WebSocket;
|
using Discord;
|
||||||
|
using Discord.WebSocket;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using NpgsqlTypes;
|
using NpgsqlTypes;
|
||||||
using System;
|
using System;
|
||||||
|
@ -31,28 +32,178 @@ namespace Noikoio.RegexBot.Module.ModLogs
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Event handling
|
#region Event handling
|
||||||
private async Task Client_MessageReceived(SocketMessage arg) => await CacheMessage(arg);
|
private async Task Client_MessageReceived(SocketMessage arg) => await AddOrUpdateCacheItemAsync(arg);
|
||||||
|
|
||||||
private Task Client_MessageUpdated(Discord.Cacheable<Discord.IMessage, ulong> arg1, SocketMessage arg2, ISocketMessageChannel arg3)
|
private async Task Client_MessageUpdated(
|
||||||
|
Discord.Cacheable<Discord.IMessage, ulong> before,
|
||||||
|
SocketMessage after, ISocketMessageChannel channel)
|
||||||
{
|
{
|
||||||
/*
|
if (after is SocketUserMessage afterMsg)
|
||||||
* TODO:
|
{
|
||||||
* Edited messages seem to retain their ID. Need to look into this.
|
// We're not interested in all message updates, only those that leave a timestamp.
|
||||||
* In any case, the new message must be stored in case of future edits.
|
if (!afterMsg.EditedTimestamp.HasValue) return;
|
||||||
* The change must be sent to the reporting channel (if one exists) as if it were
|
}
|
||||||
* a typical log entry (even though it's not).
|
else return; // no after???
|
||||||
*/
|
|
||||||
throw new NotImplementedException();
|
// Once an edited message is cached, the original message contents are discarded.
|
||||||
|
// This is the only time to report it.
|
||||||
|
await ProcessReportMessage(false, before.Id, channel, after.Content);
|
||||||
|
|
||||||
|
await AddOrUpdateCacheItemAsync(after);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task Client_MessageDeleted(Discord.Cacheable<Discord.IMessage, ulong> arg1, ISocketMessageChannel arg2)
|
private async Task Client_MessageDeleted(
|
||||||
|
Discord.Cacheable<Discord.IMessage, ulong> msg, ISocketMessageChannel channel)
|
||||||
{
|
{
|
||||||
// TODO report message deletion, if reporting channel exists and message is in cache.
|
await ProcessReportMessage(true, msg.Id, channel, null);
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Database manipulation
|
#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)
|
||||||
|
{
|
||||||
|
var cht = ch as SocketTextChannel;
|
||||||
|
if (cht == null)
|
||||||
|
{
|
||||||
|
// TODO remove debug print
|
||||||
|
Console.WriteLine("Incoming message not of a text channel");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ulong guildId = cht.Guild.Id;
|
||||||
|
|
||||||
|
// Check if enabled before doing anything else
|
||||||
|
var rptTarget = _outGetConfig(guildId) as ConfigItem.EntityName?;
|
||||||
|
if (!rptTarget.HasValue) return;
|
||||||
|
|
||||||
|
// Regardless of delete or edit, it is necessary to get database information.
|
||||||
|
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.)*";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userId != 0) ucd = await EntityCache.EntityCache.QueryAsync(guildId, userId);
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await _outLog($"SQL error in {nameof(ProcessReportMessage)}: " + ex.Message);
|
||||||
|
cacheMsg = "**Database error. See log.**";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find target channel, prepare and send out message
|
||||||
|
var em = CreateReportEmbed(isDelete, ucd, messageId, ch, (cacheMsg, editMsg));
|
||||||
|
var rptTargetChannel = _dClient.GetGuild(guildId)?.GetTextChannel(rptTarget.Value.Id.Value);
|
||||||
|
if (rptTargetChannel == null)
|
||||||
|
{
|
||||||
|
await _outLog("Target channel not found.");
|
||||||
|
// TODO make a more descriptive error message
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await rptTargetChannel.SendMessageAsync("", embed: em);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int ReportCutoffLength = 750;
|
||||||
|
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,
|
||||||
|
(string, string) content) // tuple: Item1 = cached content. Item2 = after-edit message
|
||||||
|
{
|
||||||
|
string before = content.Item1;
|
||||||
|
string after = content.Item2;
|
||||||
|
if (content.Item1.Length > ReportCutoffLength)
|
||||||
|
{
|
||||||
|
before = string.Format(ReportCutoffNotify, ReportCutoffLength)
|
||||||
|
+ content.Item1.Substring(ReportCutoffLength);
|
||||||
|
}
|
||||||
|
if (isDelete && content.Item2.Length > ReportCutoffLength)
|
||||||
|
{
|
||||||
|
after = string.Format(ReportCutoffNotify, ReportCutoffLength)
|
||||||
|
+ content.Item2.Substring(ReportCutoffLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Value for ucb is null if cached user could not be determined
|
||||||
|
var eb = new EmbedBuilder
|
||||||
|
{
|
||||||
|
Author = new EmbedAuthorBuilder()
|
||||||
|
{
|
||||||
|
IconUrl = ucd?.AvatarUrl
|
||||||
|
},
|
||||||
|
Fields = new System.Collections.Generic.List<EmbedFieldBuilder>(),
|
||||||
|
Footer = new EmbedFooterBuilder()
|
||||||
|
{
|
||||||
|
Text = (ucd == null ? "" : $"UID {ucd.UserId} - ") + $"MID {messageId}",
|
||||||
|
IconUrl = _dClient.CurrentUser.GetAvatarUrl()
|
||||||
|
},
|
||||||
|
Timestamp = DateTimeOffset.Now
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDelete)
|
||||||
|
{
|
||||||
|
eb.Color = new Color(0x9b9b9b);
|
||||||
|
eb.Description = content.Item1;
|
||||||
|
eb.Author.Name = "Message deleted by "
|
||||||
|
+ ucd == null ? "unknown user" : $"{ucd.Username}#{ucd.Discriminator}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
eb.Color = new Color(8615955);
|
||||||
|
eb.Fields.Add(new EmbedFieldBuilder()
|
||||||
|
{
|
||||||
|
Name = "Before",
|
||||||
|
Value = before
|
||||||
|
});
|
||||||
|
eb.Fields.Add(new EmbedFieldBuilder()
|
||||||
|
{
|
||||||
|
Name = "After",
|
||||||
|
Value = after
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ucd != null) eb.Fields.Add(new EmbedFieldBuilder()
|
||||||
|
{
|
||||||
|
Name = "Username",
|
||||||
|
Value = $"<@!{ucd.UserId}>",
|
||||||
|
IsInline = true
|
||||||
|
});
|
||||||
|
eb.Fields.Add(new EmbedFieldBuilder()
|
||||||
|
{
|
||||||
|
Name = "Channel",
|
||||||
|
Value = $"<#{chInfo.Id}>\n#{chInfo.Name}",
|
||||||
|
IsInline = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Database storage/retrieval
|
||||||
const string TableMessage = "cache_messages";
|
const string TableMessage = "cache_messages";
|
||||||
|
|
||||||
private void CreateCacheTables()
|
private void CreateCacheTables()
|
||||||
|
@ -65,20 +216,61 @@ namespace Noikoio.RegexBot.Module.ModLogs
|
||||||
+ "message_id bigint primary key, "
|
+ "message_id bigint primary key, "
|
||||||
+ "author_id bigint not null, "
|
+ "author_id bigint not null, "
|
||||||
+ "guild_id bigint not null, "
|
+ "guild_id bigint not null, "
|
||||||
+ "channel_id bigint not null, " // channel cache later? something to think about...
|
+ "channel_id bigint not null, " // TODO channel cache fk when that gets implemented
|
||||||
+ "created_ts timestamptz not null, "
|
+ "created_ts timestamptz not null, "
|
||||||
+ "edited_ts timestamptz null, "
|
+ "edited_ts timestamptz null, "
|
||||||
+ "message text not null, "
|
+ "message text not null, "
|
||||||
+ $"FOREIGN KEY (author_id, guild_id) references {EntityCache.SqlHelper.TableUser} (user_id, guild_id)"
|
+ $"FOREIGN KEY (author_id, guild_id) references {EntityCache.SqlHelper.TableUser} (user_id, guild_id)"
|
||||||
+ ")";
|
+ ")";
|
||||||
// TODO are more columns needed for edit info?
|
|
||||||
c.ExecuteNonQuery();
|
c.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
private async Task CacheMessage(SocketMessage msg)
|
private async Task AddOrUpdateCacheItemAsync(SocketMessage msg)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync())
|
||||||
|
{
|
||||||
|
// No upsert. Delete, then add.
|
||||||
|
using (var t = db.BeginTransaction())
|
||||||
|
{
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
c.CommandText = "DELETE FROM " + TableMessage + " WHERE message_id = @MessageId";
|
||||||
|
c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = msg.Id;
|
||||||
|
c.Prepare();
|
||||||
|
await c.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
using (var c = db.CreateCommand())
|
||||||
|
{
|
||||||
|
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)";
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NpgsqlException ex)
|
||||||
|
{
|
||||||
|
await _outLog($"SQL error in {nameof(AddOrUpdateCacheItemAsync)}: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetCachedMessageAsync(ulong messageId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -86,24 +278,26 @@ namespace Noikoio.RegexBot.Module.ModLogs
|
||||||
{
|
{
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand())
|
||||||
{
|
{
|
||||||
c.CommandText = "INSERT INTO " + TableMessage
|
c.CommandText = "SELECT message FROM " + TableMessage
|
||||||
+ " (message_id, author_id, guild_id, channel_id, created_ts, message) VALUES "
|
+ " WHERE message_id = @MessageId";
|
||||||
+ "(@MessageId, @UserId, @GuildId, @ChannelId, @Date, @Message)";
|
c.Parameters.Add("@MessageId", NpgsqlDbType.Bigint).Value = messageId;
|
||||||
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;
|
|
||||||
c.Parameters.Add("@Message", NpgsqlDbType.Text).Value = msg.Content;
|
|
||||||
c.Prepare();
|
c.Prepare();
|
||||||
await c.ExecuteNonQueryAsync();
|
using (var r = await c.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
if (await r.ReadAsync())
|
||||||
|
return r.GetString(0);
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (NpgsqlException ex)
|
catch (NpgsqlException ex)
|
||||||
{
|
{
|
||||||
await _outLog($"SQL error in {nameof(CacheMessage)}: " + ex.Message);
|
await _outLog($"SQL error in {nameof(GetCachedMessageAsync)}: " + ex.Message);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue