docs: unify documentation and streamline code comments
- Translated project documentation (LEARNING-JOURNEY, CONTRIBUTORS, AI_DISCLOSURE) to English for better accessibility. - Standardized internal code documentation by converting XML-doc blocks to standard comment format. - Cleaned up inline comments and removed redundant versioning metadata across the codebase. - Refactored non-functional text elements to improve readability and maintain a consistent style.
This commit is contained in:
+52
-204
@@ -127,7 +127,6 @@ internal class MessageStore : IDisposable
|
||||
private const int MessageQueryLimit = 10_000;
|
||||
|
||||
private string DbPath { get; }
|
||||
|
||||
private SqliteConnection Connection { get; set; }
|
||||
|
||||
internal static readonly MessagePackSerializerOptions MsgPackOptions =
|
||||
@@ -147,10 +146,8 @@ internal class MessageStore : IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Pooling=false (set in Connect) avoids ClearAllPools, which is
|
||||
// provider-wide and would touch other plugins' SQLite connections.
|
||||
// GC.Collect was here as a defensive flush; removed because explicit
|
||||
// Close already releases everything we hold.
|
||||
// Pooling=false avoids ClearAllPools which is provider-wide and
|
||||
// would touch other plugins' SQLite connections.
|
||||
Connection.Close();
|
||||
Connection.Dispose();
|
||||
}
|
||||
@@ -176,7 +173,6 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate()
|
||||
{
|
||||
// Get current user_version.
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText = "PRAGMA user_version;";
|
||||
var userVersion = Convert.ToInt32(cmd.ExecuteScalar());
|
||||
@@ -186,9 +182,7 @@ internal class MessageStore : IDisposable
|
||||
{
|
||||
case <= 0:
|
||||
migrationsToDo.Add(Migrate0);
|
||||
|
||||
// Migration support was only added in version 1. Migrate 0 is
|
||||
// idempotent.
|
||||
// Migration support was only added in version 1. Migrate0 is idempotent.
|
||||
migrationsToDo.Add(Migrate1);
|
||||
migrationsToDo.Add(Migrate2);
|
||||
migrationsToDo.Add(Migrate3);
|
||||
@@ -238,7 +232,6 @@ internal class MessageStore : IDisposable
|
||||
Plugin.Log.Information("Running migration 1: Adding Deleted column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
-- Migration 1: Add Deleted column
|
||||
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
||||
"
|
||||
);
|
||||
@@ -251,7 +244,6 @@ internal class MessageStore : IDisposable
|
||||
Plugin.Log.Information("Running migration 2: Adding Channel generated column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
-- Migration 2: Add Channel generated column
|
||||
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_channel ON messages (Channel);
|
||||
"
|
||||
@@ -262,9 +254,8 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private bool ColumnExists(string table, string column)
|
||||
{
|
||||
// PRAGMA does not accept SQLite parameter bindings. The table name is
|
||||
// a compile-time constant fed in from internal call sites, so the
|
||||
// interpolation cannot be reached from any user-controlled path.
|
||||
// PRAGMA does not accept SQLite parameter bindings. Table name is a
|
||||
// compile-time constant from internal call sites only.
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText = $"PRAGMA table_info({table});";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
@@ -280,9 +271,8 @@ internal class MessageStore : IDisposable
|
||||
{
|
||||
Plugin.Log.Information("Running migration 3: Fix log kinds to fit the new format");
|
||||
|
||||
// Recovery for partially-applied Migrate3: if the schema is already
|
||||
// in its target shape (new columns exist, old Code column gone) but
|
||||
// user_version was never bumped, just record the version and exit.
|
||||
// Recovery for partially-applied Migrate3: schema already in target
|
||||
// shape but user_version was never bumped -- just record and exit.
|
||||
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
|
||||
{
|
||||
Plugin.Log.Information(
|
||||
@@ -294,15 +284,6 @@ internal class MessageStore : IDisposable
|
||||
|
||||
Connection.Execute(
|
||||
@"
|
||||
-- Migration 3: Fix log kinds to fit the new format
|
||||
-- Add new ChatType, SourceKind, TargetKind (byte), SortCodeV2
|
||||
-- Migrate OldChatColumn
|
||||
-- ChatType = OldChatColumn & 0x7f
|
||||
-- SourceKind = log2(1 << ((OldChatColumn >> 11) & 0xF))
|
||||
-- TargetKind = trunc(log2(1 << ((OldChatColumn >> 7) & 0xF)))
|
||||
-- Virtual SortCodeV2 = ChatType << 16 | SourceKind << 8 | TargetKind
|
||||
-- Delete OldChatColumn, Virtual Channel
|
||||
|
||||
ALTER TABLE messages ADD COLUMN ChatType INTEGER;
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_chat_type ON messages (ChatType);
|
||||
ALTER TABLE messages ADD COLUMN SourceKind INTEGER;
|
||||
@@ -328,10 +309,8 @@ internal class MessageStore : IDisposable
|
||||
{
|
||||
Plugin.Log.Information($"Setting version {version}");
|
||||
using var cmd = Connection.CreateCommand();
|
||||
// PRAGMA does not accept SQLite parameter bindings, and there is no
|
||||
// pragma_ function variant that can set the version either. The
|
||||
// version is a compile-time int from the migration sequence, never
|
||||
// user input.
|
||||
// PRAGMA does not accept SQLite parameter bindings; version is a
|
||||
// compile-time int from the migration sequence, never user input.
|
||||
cmd.CommandText = $"PRAGMA user_version = {version};";
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
@@ -342,11 +321,8 @@ internal class MessageStore : IDisposable
|
||||
PerformMaintenance();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a (ChatType, count) snapshot over non-deleted messages.
|
||||
/// Used by the Privacy tab to preview the impact of a retroactive
|
||||
/// cleanup before the user confirms.
|
||||
/// </summary>
|
||||
// Returns a (ChatType, count) snapshot over non-deleted messages.
|
||||
// Used by the Privacy tab to preview retroactive cleanup impact.
|
||||
internal Dictionary<int, long> GetMessageCountsByChatType()
|
||||
{
|
||||
var result = new Dictionary<int, long>();
|
||||
@@ -364,12 +340,9 @@ internal class MessageStore : IDisposable
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes messages older than the per-channel retention window, with a
|
||||
/// global default for channels not listed explicitly. Cutoffs are
|
||||
/// computed from "now" at call time. Runs VACUUM only if anything was
|
||||
/// removed. Returns the number of rows deleted.
|
||||
/// </summary>
|
||||
// Deletes messages older than the per-channel retention window, with a global
|
||||
// default for unmapped channels. Runs VACUUM only if rows were removed.
|
||||
// Returns the number of rows deleted.
|
||||
internal long DeleteByRetentionPolicy(
|
||||
IReadOnlyDictionary<int, int> chatTypeDaysMap,
|
||||
int defaultDays
|
||||
@@ -408,10 +381,7 @@ internal class MessageStore : IDisposable
|
||||
index++;
|
||||
}
|
||||
|
||||
// Catch-all for channels without an explicit override. "0" is
|
||||
// treated as "do not delete by default" — without an explicit
|
||||
// user override, unmapped channels stay forever instead of
|
||||
// getting wiped immediately.
|
||||
// defaultDays=0 means "keep forever" for unmapped channels.
|
||||
if (defaultDays > 0)
|
||||
{
|
||||
var defaultCutoff = nowMs - defaultDays * 86400000L;
|
||||
@@ -439,21 +409,14 @@ internal class MessageStore : IDisposable
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hard-deletes every message whose ChatType is not in the supplied
|
||||
/// allowlist, then VACUUMs the database to reclaim disk space.
|
||||
/// Returns the number of rows deleted.
|
||||
/// </summary>
|
||||
// Hard-deletes every message whose ChatType is not in the allowlist,
|
||||
// then VACUUMs. Returns the number of rows deleted.
|
||||
internal long CleanupRetainOnly(IReadOnlyCollection<int> allowedTypes)
|
||||
{
|
||||
if (allowedTypes.Count == 0)
|
||||
{
|
||||
// Defensive: refuse a "delete everything" disguised as a filter.
|
||||
// Use ClearMessages() if a full wipe is actually intended.
|
||||
throw new InvalidOperationException(
|
||||
"CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe."
|
||||
);
|
||||
}
|
||||
|
||||
long deleted;
|
||||
using (var cmd = Connection.CreateCommand())
|
||||
@@ -493,14 +456,9 @@ internal class MessageStore : IDisposable
|
||||
|
||||
internal void UpsertMessage(Message message)
|
||||
{
|
||||
// Hellion Chat privacy filter — drop disallowed ChatTypes before
|
||||
// they reach the storage layer (single source of truth, also
|
||||
// covers any future write paths e.g. webinterface backfill).
|
||||
// Privacy filter -- drop disallowed ChatTypes before they reach storage.
|
||||
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
|
||||
{
|
||||
// Verbose-only: this fires for every dropped message, which is
|
||||
// the common case for users with a tight privacy whitelist. Keep
|
||||
// it for diagnostics but stay out of the default xllog stream.
|
||||
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
|
||||
return;
|
||||
}
|
||||
@@ -509,33 +467,11 @@ internal class MessageStore : IDisposable
|
||||
cmd.CommandText =
|
||||
@"
|
||||
INSERT INTO messages (
|
||||
Id,
|
||||
Receiver,
|
||||
ContentId,
|
||||
Date,
|
||||
ChatType,
|
||||
SourceKind,
|
||||
TargetKind,
|
||||
Sender,
|
||||
Content,
|
||||
SenderSource,
|
||||
ContentSource,
|
||||
ExtraChatChannel,
|
||||
Deleted
|
||||
Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||
Sender, Content, SenderSource, ContentSource, ExtraChatChannel, Deleted
|
||||
) VALUES (
|
||||
$Id,
|
||||
$Receiver,
|
||||
$ContentId,
|
||||
$Date,
|
||||
$ChatType,
|
||||
$SourceKind,
|
||||
$TargetKind,
|
||||
$Sender,
|
||||
$Content,
|
||||
$SenderSource,
|
||||
$ContentSource,
|
||||
$ExtraChatChannel,
|
||||
false
|
||||
$Id, $Receiver, $ContentId, $Date, $ChatType, $SourceKind, $TargetKind,
|
||||
$Sender, $Content, $SenderSource, $ContentSource, $ExtraChatChannel, false
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
Receiver = excluded.Receiver,
|
||||
@@ -580,13 +516,9 @@ internal class MessageStore : IDisposable
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Streams messages for export. Optional filters:
|
||||
/// - <paramref name="chatTypes"/>: limit to these ChatTypes
|
||||
/// - <paramref name="from"/> / <paramref name="to"/>: inclusive date range
|
||||
/// Result is sorted ascending by Date and excludes soft-deleted rows.
|
||||
/// Caller is responsible for disposing the enumerator.
|
||||
/// </summary>
|
||||
// Streams messages for export, sorted ascending by Date, excluding soft-deleted rows.
|
||||
// Optional filters: chatTypes, from/to inclusive date range.
|
||||
// Caller is responsible for disposing the enumerator.
|
||||
internal MessageEnumerator StreamForExport(
|
||||
IReadOnlyCollection<int>? chatTypes,
|
||||
DateTimeOffset? from,
|
||||
@@ -606,18 +538,8 @@ internal class MessageStore : IDisposable
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
ContentId,
|
||||
Date,
|
||||
ChatType,
|
||||
SourceKind,
|
||||
TargetKind,
|
||||
Sender,
|
||||
Content,
|
||||
SenderSource,
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||
Sender, Content, SenderSource, ContentSource, ExtraChatChannel
|
||||
FROM messages
|
||||
WHERE "
|
||||
+ string.Join(" AND ", clauses)
|
||||
@@ -633,12 +555,10 @@ internal class MessageStore : IDisposable
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the most recent messages.
|
||||
/// </summary>
|
||||
/// <param name="receiver">The receiver content ID to filter by. If null, no filtering is performed.</param>
|
||||
/// <param name="since">Only show messages since this date. If null, no filtering is performed.</param>
|
||||
/// <param name="count">The amount to return. Defaults to 10,000.</param>
|
||||
// Returns the most recent messages, oldest-first.
|
||||
// receiver: filter by receiver ContentId (null = no filter)
|
||||
// since: only include messages after this date (null = no filter)
|
||||
// count: max rows to return, defaults to 10,000
|
||||
internal MessageEnumerator GetMostRecentMessages(
|
||||
ulong? receiver = null,
|
||||
DateTimeOffset? since = null,
|
||||
@@ -654,25 +574,14 @@ internal class MessageStore : IDisposable
|
||||
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
||||
|
||||
var cmd = Connection.CreateCommand();
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
// Select last N by date DESC, then reverse to ascending order.
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
ContentId,
|
||||
Date,
|
||||
ChatType,
|
||||
SourceKind,
|
||||
TargetKind,
|
||||
Sender,
|
||||
Content,
|
||||
SenderSource,
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||
Sender, Content, SenderSource, ContentSource, ExtraChatChannel
|
||||
FROM messages
|
||||
"
|
||||
+ whereClause
|
||||
@@ -682,7 +591,7 @@ internal class MessageStore : IDisposable
|
||||
)
|
||||
ORDER BY Date ASC;
|
||||
";
|
||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
||||
cmd.CommandTimeout = 120;
|
||||
|
||||
if (receiver != null)
|
||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||
@@ -694,21 +603,10 @@ internal class MessageStore : IDisposable
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hellion Chat — Auto-Tell-Tabs history preload.
|
||||
///
|
||||
/// Returns up to <paramref name="limit"/> tells exchanged with the named
|
||||
/// player, oldest-first, ready to be added to a freshly spawned auto
|
||||
/// tell tab. The Sender column is a serialized chunk blob, so SQL on its
|
||||
/// own cannot filter by player identity; we narrow with SQL on Receiver
|
||||
/// + ChatType (cheap, indexed) and let the client do the final
|
||||
/// PlayerPayload comparison on the result set.
|
||||
///
|
||||
/// <paramref name="sqlScanLimit"/> caps how many recent tells we scan
|
||||
/// before giving up. 500 covers around 10 days for an active greeter
|
||||
/// and stays well under the 20 ms budget required to keep the spawn on
|
||||
/// the message-processing worker thread.
|
||||
/// </summary>
|
||||
// Returns up to limit tells exchanged with the named player, oldest-first.
|
||||
// SQL narrows by Receiver + ChatType (indexed); client does the final
|
||||
// PlayerPayload comparison. sqlScanLimit caps the scan to stay within
|
||||
// the message-processing worker thread budget.
|
||||
internal IReadOnlyList<Message> GetTellHistoryWithSender(
|
||||
ulong receiver,
|
||||
string senderName,
|
||||
@@ -718,26 +616,14 @@ internal class MessageStore : IDisposable
|
||||
)
|
||||
{
|
||||
if (limit <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
using var cmd = Connection.CreateCommand();
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
ContentId,
|
||||
Date,
|
||||
ChatType,
|
||||
SourceKind,
|
||||
TargetKind,
|
||||
Sender,
|
||||
Content,
|
||||
SenderSource,
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||
Sender, Content, SenderSource, ContentSource, ExtraChatChannel
|
||||
FROM messages
|
||||
WHERE deleted = false
|
||||
AND Receiver = $Receiver
|
||||
@@ -756,27 +642,19 @@ internal class MessageStore : IDisposable
|
||||
foreach (var message in enumerator)
|
||||
{
|
||||
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
collected.Add(message);
|
||||
if (collected.Count >= limit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// SQL was DESC (newest-first) so we hit the limit on the most
|
||||
// recent matching tells. Reverse to oldest-first for chronological
|
||||
// display in the tab.
|
||||
// SQL was DESC (newest-first); reverse to oldest-first for tab display.
|
||||
collected.Reverse();
|
||||
return collected;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks a message as deleted so it won't get returned in queries.
|
||||
/// </summary>
|
||||
// Soft-deletes a message so it won't appear in queries.
|
||||
internal void DeleteMessage(Guid id)
|
||||
{
|
||||
using var cmd = Connection.CreateCommand();
|
||||
@@ -803,8 +681,6 @@ internal class MessageStore : IDisposable
|
||||
|
||||
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
|
||||
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT COUNT(*)
|
||||
@@ -816,7 +692,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
cmd.Parameters.AddWithValue("$After", ((DateTimeOffset)after).ToUnixTimeMilliseconds());
|
||||
cmd.Parameters.AddWithValue("$Before", ((DateTimeOffset)before).ToUnixTimeMilliseconds());
|
||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
||||
cmd.CommandTimeout = 120;
|
||||
|
||||
return (long)cmd.ExecuteScalar()!;
|
||||
}
|
||||
@@ -839,26 +715,14 @@ internal class MessageStore : IDisposable
|
||||
|
||||
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
||||
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
ContentId,
|
||||
Date,
|
||||
ChatType,
|
||||
SourceKind,
|
||||
TargetKind,
|
||||
Sender,
|
||||
Content,
|
||||
SenderSource,
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||
Sender, Content, SenderSource, ContentSource, ExtraChatChannel
|
||||
FROM messages
|
||||
" + whereClause;
|
||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
||||
cmd.CommandTimeout = 120;
|
||||
|
||||
if (receiver != null)
|
||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||
@@ -888,23 +752,11 @@ internal class MessageStore : IDisposable
|
||||
|
||||
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
|
||||
|
||||
// Select last N messages by date DESC, but reverse the order to get
|
||||
// them in ascending order.
|
||||
cmd.CommandText =
|
||||
@"
|
||||
SELECT
|
||||
Id,
|
||||
Receiver,
|
||||
ContentId,
|
||||
Date,
|
||||
ChatType,
|
||||
SourceKind,
|
||||
TargetKind,
|
||||
Sender,
|
||||
Content,
|
||||
SenderSource,
|
||||
ContentSource,
|
||||
ExtraChatChannel
|
||||
Id, Receiver, ContentId, Date, ChatType, SourceKind, TargetKind,
|
||||
Sender, Content, SenderSource, ContentSource, ExtraChatChannel
|
||||
FROM messages
|
||||
"
|
||||
+ whereClause
|
||||
@@ -912,7 +764,7 @@ internal class MessageStore : IDisposable
|
||||
ORDER BY Date
|
||||
LIMIT $Offset, $OffsetCount;
|
||||
";
|
||||
cmd.CommandTimeout = 120; // this could take a while on slow computers
|
||||
cmd.CommandTimeout = 120;
|
||||
|
||||
if (receiver != null)
|
||||
cmd.Parameters.AddWithValue("$Receiver", receiver);
|
||||
@@ -925,10 +777,8 @@ internal class MessageStore : IDisposable
|
||||
return new MessageEnumerator(cmd.ExecuteReader());
|
||||
}
|
||||
|
||||
// Build "$prefix0,$prefix1,..." placeholder list and bind values to
|
||||
// the command. SQLite has no native array parameter, so we generate
|
||||
// the list at runtime and bind each entry under its own name. Used
|
||||
// for IN-clauses and similar dynamic-arity SQL fragments.
|
||||
// Builds a "$prefix0,$prefix1,..." placeholder list and binds values to the command.
|
||||
// SQLite has no native array parameter, so placeholders are generated per entry.
|
||||
private static string BindIntList(SqliteCommand cmd, string prefix, IEnumerable<int> values)
|
||||
{
|
||||
var names = new List<string>();
|
||||
@@ -951,8 +801,6 @@ internal class MessageEnumerator(DbDataReader reader)
|
||||
{
|
||||
private const int MaxErrorLogs = 10;
|
||||
|
||||
// FailedIds and FailedCount are separate, because messages might fail to
|
||||
// even parse the ID field.
|
||||
private readonly List<Guid> FailedIds = [];
|
||||
private int FailedCount;
|
||||
public bool DidError => FailedCount > 0;
|
||||
|
||||
Reference in New Issue
Block a user