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:
2026-05-11 00:52:15 +02:00
parent a37882893e
commit c4c85cf4b8
33 changed files with 666 additions and 1364 deletions
+52 -204
View File
@@ -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;