2018-03-27 22:15:13 +00:00
|
|
|
|
using System;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Data.Common;
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace Noikoio.RegexBot.Module.ModLogs
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Represents a log entry in the database.
|
|
|
|
|
/// </summary>
|
2018-03-27 22:15:13 +00:00
|
|
|
|
class LogEntry
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
readonly int _logId;
|
2018-04-05 00:25:13 +00:00
|
|
|
|
readonly DateTimeOffset _ts;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
readonly ulong _guildId;
|
2018-04-05 00:25:13 +00:00
|
|
|
|
readonly ulong _invokeId;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
readonly ulong _targetId;
|
|
|
|
|
readonly ulong? _channelId;
|
2018-03-27 22:15:13 +00:00
|
|
|
|
readonly LogType _type;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
readonly string _message;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the ID value of this log entry.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int Id => _logId;
|
|
|
|
|
/// <summary>
|
2018-04-05 00:25:13 +00:00
|
|
|
|
/// Gets the UTC timestamp of the entry.
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// </summary>
|
2018-04-05 00:25:13 +00:00
|
|
|
|
public DateTimeOffset Timestamp => _ts;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the ID of the guild to which this log entry corresponds.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ulong Guild => _guildId;
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the ID of the user to which this log entry corresponds.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ulong Target => _targetId;
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the ID of the invoking user.
|
2018-04-05 00:25:13 +00:00
|
|
|
|
/// This value differs from <see cref="Target"/> if this entry was created through
|
|
|
|
|
/// action of another user, such as the issuer of notes and warnings.
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// </summary>
|
2018-04-05 00:25:13 +00:00
|
|
|
|
public ulong Invoker => _invokeId;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the guild channel ID to which this log entry corresponds, if any.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ulong? TargetChannel => _channelId;
|
|
|
|
|
/// <summary>
|
2018-03-27 22:15:13 +00:00
|
|
|
|
/// Gets this log entry's type.
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// </summary>
|
2018-03-27 22:15:13 +00:00
|
|
|
|
public LogType Type => _type;
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the content of this log entry.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Message => _message;
|
|
|
|
|
|
2018-03-27 22:15:13 +00:00
|
|
|
|
public LogEntry(DbDataReader r)
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
// Double-check ordinals if making changes to QueryColumns
|
|
|
|
|
|
|
|
|
|
_logId = r.GetInt32(0);
|
|
|
|
|
_ts = r.GetDateTime(1).ToUniversalTime();
|
|
|
|
|
unchecked
|
|
|
|
|
{
|
|
|
|
|
_guildId = (ulong)r.GetInt64(2);
|
|
|
|
|
_targetId = (ulong)r.GetInt64(3);
|
2018-04-05 00:25:13 +00:00
|
|
|
|
_invokeId = (ulong)r.GetInt64(4);
|
2018-02-11 03:34:13 +00:00
|
|
|
|
if (r.IsDBNull(5)) _channelId = null;
|
|
|
|
|
else _channelId = (ulong)r.GetInt64(5);
|
|
|
|
|
}
|
2018-03-27 22:15:13 +00:00
|
|
|
|
_type = (LogType)r.GetInt32(6);
|
2018-02-11 03:34:13 +00:00
|
|
|
|
_message = r.GetString(7);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO lazy loading of channel, user, etc from caches
|
|
|
|
|
// TODO methods for updating this log entry(?)
|
|
|
|
|
|
|
|
|
|
// TODO figure out some helper methods to retrieve data of other entities by ID, if it becomes necessary
|
|
|
|
|
|
2018-03-27 22:15:13 +00:00
|
|
|
|
#region Log entry types
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Enumeration of all possible event flags. Names will show themselves to users
|
|
|
|
|
/// and associated values will be saved to the databaase.
|
|
|
|
|
/// Once they're included in a release build, they should never be changed again.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[Flags]
|
|
|
|
|
public enum LogType
|
|
|
|
|
{
|
|
|
|
|
/// <summary>Should only be useful in GuildState and ignored elsewhere.</summary>
|
|
|
|
|
None = 0x0,
|
|
|
|
|
Note = 0x1,
|
|
|
|
|
Warn = 0x2,
|
|
|
|
|
Kick = 0x4,
|
|
|
|
|
Ban = 0x8,
|
|
|
|
|
/// <summary>Record of a user joining a guild.</summary>
|
|
|
|
|
JoinGuild = 0x10,
|
|
|
|
|
/// <summary>Record of a user leaving a guild, voluntarily or by force (kick, ban).</summary>
|
|
|
|
|
LeaveGuild = 0x20,
|
|
|
|
|
NameChange = 0x40,
|
|
|
|
|
/// <summary>Not a database entry, but exists for MessageCache configuration.</summary>
|
|
|
|
|
MsgEdit = 0x80,
|
|
|
|
|
/// <summary>Not a database entry, but exists for MessageCache configuration.</summary>
|
|
|
|
|
MsgDelete = 0x100
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static LogType GetLogTypeFromString(string input)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(input))
|
|
|
|
|
throw new ArgumentException("Types are not properly defined.");
|
|
|
|
|
|
|
|
|
|
var strTypes = input.Split(
|
|
|
|
|
new char[] { ' ', ',', '/', '+' }, // and more?
|
|
|
|
|
StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
|
|
|
|
LogType endResult = LogType.None;
|
|
|
|
|
foreach (var item in strTypes)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = Enum.Parse<LogType>(item, true);
|
|
|
|
|
endResult |= result;
|
|
|
|
|
}
|
|
|
|
|
catch (ArgumentException)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentException($"Unable to determine the given event type \"{item}\".");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return endResult;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region SQL setup and querying
|
|
|
|
|
public const string TblEntry = "modlogs_entries";
|
|
|
|
|
public const string TblEntryIncr = TblEntry + "_id";
|
|
|
|
|
|
|
|
|
|
internal static void CreateTables()
|
|
|
|
|
{
|
|
|
|
|
using (var db = RegexBot.Config.GetOpenDatabaseConnectionAsync().GetAwaiter().GetResult())
|
|
|
|
|
{
|
|
|
|
|
using (var c = db.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
c.CommandText = "CREATE TABLE IF NOT EXISTS " + TblEntry + " ("
|
|
|
|
|
+ "id int primary key, "
|
|
|
|
|
+ "entry_ts timestamptz not null, "
|
|
|
|
|
+ "guild_id bigint not null, "
|
2018-04-05 00:25:13 +00:00
|
|
|
|
+ "target_id bigint not null, " // No foreign constraint: some targets may not be cached
|
|
|
|
|
+ "invoker_id bigint not null, "
|
|
|
|
|
+ "target_channel_id bigint null, "
|
2018-03-27 22:15:13 +00:00
|
|
|
|
+ "entry_type integer not null, "
|
|
|
|
|
+ "message text not null, "
|
2018-04-05 00:25:13 +00:00
|
|
|
|
+ $"FOREIGN KEY (invoker_id, guild_id) REFERENCES {EntityCache.SqlHelper.TableUser} (user_id, guild_id), "
|
|
|
|
|
+ $"FOREIGN KEY (target_channel_id, guild_id) REFERENCES {EntityCache.SqlHelper.TableTextChannel} (channel_id, guild_id)"
|
|
|
|
|
+ ")";
|
2018-03-27 22:15:13 +00:00
|
|
|
|
c.ExecuteNonQuery();
|
|
|
|
|
}
|
|
|
|
|
using (var c = db.CreateCommand())
|
|
|
|
|
{
|
|
|
|
|
c.CommandText = $"CREATE SEQUENCE IF NOT EXISTS {TblEntryIncr} "
|
|
|
|
|
+ $"START 1000 MAXVALUE {int.MaxValue}";
|
|
|
|
|
c.ExecuteNonQuery();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-11 03:34:13 +00:00
|
|
|
|
// Double-check constructor if making changes to this constant
|
2018-04-05 00:25:13 +00:00
|
|
|
|
const string QueryColumns = "id, entry_ts, guild_id, target_id, invoker_id, target_channel_id, entry_type, message";
|
2018-02-11 03:34:13 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
2018-03-27 22:15:13 +00:00
|
|
|
|
/// Attempts to look up a log entry by its ID.
|
2018-02-11 03:34:13 +00:00
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>Null if no result.</returns>
|
2018-03-27 22:15:13 +00:00
|
|
|
|
public static async Task<LogEntry> QueryIdAsync(ulong guild, int id)
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
2018-02-20 08:58:55 +00:00
|
|
|
|
using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync())
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
using (var c = db.CreateCommand())
|
|
|
|
|
{
|
2018-03-27 22:15:13 +00:00
|
|
|
|
c.CommandText = $"SELECT {QueryColumns} FROM {TblEntry} "
|
2018-02-11 03:34:13 +00:00
|
|
|
|
+ "WHERE guild_id = @Guild and id = @Id";
|
|
|
|
|
c.Parameters.Add("@Guild", NpgsqlTypes.NpgsqlDbType.Bigint).Value = guild;
|
|
|
|
|
c.Parameters.Add("@Id", NpgsqlTypes.NpgsqlDbType.Numeric).Value = id;
|
|
|
|
|
c.Prepare();
|
|
|
|
|
using (var r = await c.ExecuteReaderAsync())
|
|
|
|
|
{
|
2018-03-27 22:15:13 +00:00
|
|
|
|
if (r.Read()) return new LogEntry(r);
|
2018-02-11 03:34:13 +00:00
|
|
|
|
else return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-27 22:15:13 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Attempts to look up a log entry by a number of parameters.
|
|
|
|
|
/// At least "target" or "invoker" are required when calling this method.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
public static async Task<IEnumerable<LogEntry>> QueryLogAsync
|
2018-02-11 03:34:13 +00:00
|
|
|
|
(ulong guild,
|
|
|
|
|
ulong? target = null,
|
|
|
|
|
ulong? invoker = null,
|
|
|
|
|
ulong? channel = null,
|
2018-03-27 22:15:13 +00:00
|
|
|
|
LogType? category = null)
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
// Enforce some limits - can't search too broadly here. Requires this at a minimum:
|
|
|
|
|
if (target.HasValue == false && invoker.HasValue == false)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException("Query requires at minimum searching of a target or invoker.");
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-27 22:15:13 +00:00
|
|
|
|
var result = new List<LogEntry>();
|
2018-02-20 08:58:55 +00:00
|
|
|
|
using (var db = await RegexBot.Config.GetOpenDatabaseConnectionAsync())
|
2018-02-11 03:34:13 +00:00
|
|
|
|
{
|
|
|
|
|
using (var c = db.CreateCommand())
|
|
|
|
|
{
|
2018-03-27 22:15:13 +00:00
|
|
|
|
c.CommandText = $"SELECT {QueryColumns} FROM {TblEntry} WHERE";
|
2018-02-11 03:34:13 +00:00
|
|
|
|
|
|
|
|
|
bool and = false;
|
|
|
|
|
if (target.HasValue)
|
|
|
|
|
{
|
|
|
|
|
if (and) c.CommandText += " AND";
|
|
|
|
|
else and = true;
|
|
|
|
|
c.CommandText += " target_id = @TargetId";
|
|
|
|
|
c.Parameters.Add("@TargetId", NpgsqlTypes.NpgsqlDbType.Bigint).Value = target.Value;
|
|
|
|
|
}
|
|
|
|
|
if (invoker.HasValue)
|
|
|
|
|
{
|
|
|
|
|
if (and) c.CommandText += " AND";
|
|
|
|
|
else and = true;
|
|
|
|
|
c.CommandText += " invoke_id = @InvokeId";
|
|
|
|
|
c.Parameters.Add("@InvokeId", NpgsqlTypes.NpgsqlDbType.Bigint).Value = invoker.Value;
|
|
|
|
|
}
|
|
|
|
|
if (channel.HasValue)
|
|
|
|
|
{
|
|
|
|
|
if (and) c.CommandText += " AND";
|
|
|
|
|
else and = true;
|
|
|
|
|
c.CommandText += " target_channel_id = @ChannelId";
|
|
|
|
|
c.Parameters.Add("@ChannelId", NpgsqlTypes.NpgsqlDbType.Bigint).Value = channel.Value;
|
|
|
|
|
}
|
2018-03-27 22:15:13 +00:00
|
|
|
|
if (category.HasValue)
|
|
|
|
|
{
|
|
|
|
|
if (and) c.CommandText += " AND";
|
|
|
|
|
else and = true;
|
|
|
|
|
c.CommandText += " entry_type = @Category";
|
|
|
|
|
c.Parameters.Add("@Category", NpgsqlTypes.NpgsqlDbType.Integer).Value = (int)category;
|
|
|
|
|
}
|
2018-02-11 03:34:13 +00:00
|
|
|
|
c.Prepare();
|
|
|
|
|
|
|
|
|
|
using (var r = await c.ExecuteReaderAsync())
|
|
|
|
|
{
|
|
|
|
|
while (r.Read())
|
|
|
|
|
{
|
2018-03-27 22:15:13 +00:00
|
|
|
|
result.Add(new LogEntry(r));
|
2018-02-11 03:34:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|