using System; using System.Collections.Generic; using System.Data.Common; using System.IO; using System.Reflection; namespace MsgLibReadSidecar { internal static class Program { private const string Version = "0.4.0"; private const string DefaultPassword = "123"; private static readonly HashSet DefaultTargetTables = new HashSet(StringComparer.OrdinalIgnoreCase) { "TD_Roster", "TD_CustomEffigy", "tblRecent", "tblChatLevel", "tblPersonMsg", "tblMsgGroupPersonMsg", "TD_WorkGroupAuth", "TD_ReceiveFileRecord", "TD_SendFileRecord", "TD_SystemMessageRecord", "TD_MsgReadBurn" }; [STAThread] private static int Main(string[] args) { string requestText = Console.In.ReadToEnd(); Dictionary request; string requestId = ""; string op = ""; if (!SidecarProtocol.TryParseRequest(requestText, out request, out requestId, out op)) { Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "INVALID_REQUEST", "request must be a valid isphere.msglib.v1 JSON object"))); return 0; } try { Dictionary opArgs = SidecarProtocol.GetObject(request, "args"); Dictionary response; switch (op) { case "self_check": response = SidecarProtocol.Success(requestId, op, SelfCheckData()); break; case "schema_summary": response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, false)); break; case "display_sources": response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, true)); break; case "message_sources": response = SidecarProtocol.Success(requestId, op, MessageSources(opArgs)); break; case "list_messages": response = SidecarProtocol.Success(requestId, op, ListMessages(opArgs)); break; case "display_entities": response = SidecarProtocol.Success(requestId, op, DisplayEntities(opArgs)); break; default: response = SidecarProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op); break; } Console.WriteLine(SidecarProtocol.ToJson(response)); return 0; } catch (Exception ex) { Console.Error.WriteLine(ex.ToString()); Console.WriteLine(SidecarProtocol.ToJson(SidecarProtocol.Failure(requestId, op, "SIDECAR_FAILED", ex.Message))); return 0; } } private static Dictionary SelfCheckData() { return new Dictionary { { "sidecar_name", "MsgLibReadSidecar" }, { "sidecar_version", Version }, { "protocol", SidecarProtocol.Protocol }, { "runtime", ".NET Framework " + Environment.Version }, { "process_bits", IntPtr.Size * 8 }, { "requires_process_bits", 32 }, { "read_only", true }, { "mutates_db", false }, { "db_mutation_allowed", false }, { "message_body_reads_allowed", true } }; } private static Dictionary SchemaSummary(Dictionary args, bool displayOnly) { string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", ""); string dbPath = SidecarProtocol.GetString(args, "db_path", ""); string password = SidecarProtocol.GetString(args, "password", DefaultPassword); bool includeCounts = SidecarProtocol.GetBool(args, "include_counts", true); int maxTables = Clamp(SidecarProtocol.GetInt(args, "max_tables", 128), 1, 256); List requestedTargets = SidecarProtocol.GetStringList(args, "target_tables"); HashSet targetTables = requestedTargets.Count == 0 ? DefaultTargetTables : new HashSet(requestedTargets, StringComparer.OrdinalIgnoreCase); ValidateInputs(sqliteDllPath, dbPath); var result = new Dictionary { { "metadata_only", true }, { "read_only", true }, { "message_body_values_returned", false }, { "sqlite_dll_path", sqliteDllPath }, { "db_path", dbPath }, { "provider", "System.Data.SQLite" }, { "process_bits", IntPtr.Size * 8 } }; using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password)) { connection.Open(); TryExecuteNonQuery(connection, "PRAGMA query_only=ON"); result["sqlite_version"] = ExecuteScalar(connection, "select sqlite_version()"); List> tables = ReadMasterTables(connection, maxTables); List> columns = ReadColumns(connection, tables, displayOnly ? targetTables : null); result["tables"] = tables; result["columns"] = columns; result["target_row_counts"] = includeCounts ? ReadTargetCounts(connection, tables, targetTables) : new List>(); if (displayOnly) { result["display_sources"] = BuildDisplaySources(tables, columns); } } return result; } private static Dictionary MessageSources(Dictionary args) { string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", ""); string dbPath = SidecarProtocol.GetString(args, "db_path", ""); string password = SidecarProtocol.GetString(args, "password", DefaultPassword); ValidateInputs(sqliteDllPath, dbPath); var result = new Dictionary { { "metadata_only", true }, { "read_only", true }, { "message_body_values_returned", false }, { "raw_rows_returned", false }, { "file_paths_returned", false }, { "provider", "System.Data.SQLite" }, { "process_bits", IntPtr.Size * 8 } }; using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password)) { connection.Open(); TryExecuteNonQuery(connection, "PRAGMA query_only=ON"); List> tables = ReadMasterTables(connection, 256); HashSet messageTables = BuildMessageSourceTableSet(); List> columns = ReadColumns(connection, tables, messageTables); Dictionary> columnMap = BuildColumnMap(columns); Dictionary rowCounts = ReadTargetCountMap(connection, tables, messageTables); result["message_sources"] = BuildMessageSources(tables, columnMap, rowCounts); } return result; } private static Dictionary ListMessages(Dictionary args) { string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", ""); string dbPath = SidecarProtocol.GetString(args, "db_path", ""); string password = SidecarProtocol.GetString(args, "password", DefaultPassword); string conversationId = SidecarProtocol.GetString(args, "conversation_id", "").Trim(); string conversationType = SidecarProtocol.GetString(args, "conversation_type", "auto").Trim().ToLowerInvariant(); string query = SidecarProtocol.GetString(args, "query", "").Trim(); string since = SidecarProtocol.GetString(args, "since", "").Trim(); string cursor = SidecarProtocol.GetString(args, "cursor", "").Trim(); int limit = Clamp(SidecarProtocol.GetInt(args, "limit", 20), 1, 50); bool includeBody = SidecarProtocol.GetBool(args, "include_body", false); bool includeAttachmentMetadata = SidecarProtocol.GetBool(args, "include_attachment_metadata", false); if (!string.IsNullOrWhiteSpace(cursor)) { throw new ArgumentException("list_messages cursor pagination is not available yet"); } if (string.IsNullOrWhiteSpace(conversationType)) conversationType = "auto"; if (!string.Equals(conversationType, "auto", StringComparison.OrdinalIgnoreCase) && !string.Equals(conversationType, "direct", StringComparison.OrdinalIgnoreCase) && !string.Equals(conversationType, "group", StringComparison.OrdinalIgnoreCase) && !string.Equals(conversationType, "system", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("conversation_type must be auto, direct, group, or system"); } ValidateInputs(sqliteDllPath, dbPath); var result = new Dictionary { { "metadata_only", !includeBody }, { "read_only", true }, { "message_body_values_returned", includeBody }, { "raw_rows_returned", false }, { "file_paths_returned", false }, { "limit", limit }, { "provider", "System.Data.SQLite" }, { "process_bits", IntPtr.Size * 8 }, { "next_cursor", null } }; using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password)) { connection.Open(); TryExecuteNonQuery(connection, "PRAGMA query_only=ON"); List> tables = ReadMasterTables(connection, 256); HashSet listTables = BuildListMessagesTableSet(); List> columns = ReadColumns(connection, tables, listTables); Dictionary> columnMap = BuildColumnMap(columns); var sourceTables = new HashSet(StringComparer.OrdinalIgnoreCase); var messages = new List>(); if (ShouldReadMessageRole(conversationType, "direct")) { ReadDirectMessages(connection, columnMap, conversationId, query, since, limit, includeBody, includeAttachmentMetadata, messages, sourceTables); } if (ShouldReadMessageRole(conversationType, "group")) { ReadGroupMessages(connection, columnMap, conversationId, query, since, limit, includeBody, includeAttachmentMetadata, messages, sourceTables); } if (ShouldReadMessageRole(conversationType, "system")) { ReadSystemMessages(connection, columnMap, conversationId, query, since, limit, includeBody, includeAttachmentMetadata, messages, sourceTables); } messages.Sort(CompareMessagesDescending); while (messages.Count > limit) { messages.RemoveAt(messages.Count - 1); } foreach (Dictionary message in messages) { message.Remove("sort_key"); } result["source_tables"] = ToSortedArray(sourceTables); result["messages"] = messages; } return result; } private static Dictionary DisplayEntities(Dictionary args) { string sqliteDllPath = SidecarProtocol.GetString(args, "sqlite_dll_path", ""); string dbPath = SidecarProtocol.GetString(args, "db_path", ""); string password = SidecarProtocol.GetString(args, "password", DefaultPassword); string entityType = SidecarProtocol.GetString(args, "entity_type", "").Trim().ToLowerInvariant(); string query = SidecarProtocol.GetString(args, "query", "").Trim(); int limit = Clamp(SidecarProtocol.GetInt(args, "limit", 25), 1, 100); if (!string.Equals(entityType, "contacts", StringComparison.OrdinalIgnoreCase) && !string.Equals(entityType, "groups", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("entity_type must be contacts or groups"); } ValidateInputs(sqliteDllPath, dbPath); var result = new Dictionary { { "metadata_only", true }, { "read_only", true }, { "message_body_values_returned", false }, { "raw_rows_returned", false }, { "entity_type", entityType }, { "limit", limit }, { "provider", "System.Data.SQLite" }, { "process_bits", IntPtr.Size * 8 } }; using (DbConnection connection = OpenSqliteConnection(sqliteDllPath, dbPath, password)) { connection.Open(); TryExecuteNonQuery(connection, "PRAGMA query_only=ON"); List> tables = ReadMasterTables(connection, 256); HashSet entityTables = BuildDisplayEntityTableSet(entityType); List> columns = ReadColumns(connection, tables, entityTables); Dictionary> columnMap = BuildColumnMap(columns); List> entities = ReadDisplayEntities(connection, entityType, query, limit, columnMap); result["entities"] = entities; result["result_count"] = entities.Count; } return result; } private static void ValidateInputs(string sqliteDllPath, string dbPath) { if (string.IsNullOrWhiteSpace(sqliteDllPath) || !File.Exists(sqliteDllPath)) { throw new ArgumentException("sqlite_dll_path must point to an existing System.Data.SQLite.dll"); } if (string.IsNullOrWhiteSpace(dbPath) || !File.Exists(dbPath)) { throw new ArgumentException("db_path must point to an existing copied MsgLib.db"); } if (!string.Equals(Path.GetFileName(dbPath), "MsgLib.db", StringComparison.OrdinalIgnoreCase)) { throw new ArgumentException("db_path must point to a copied MsgLib.db file"); } } private static DbConnection OpenSqliteConnection(string sqliteDllPath, string dbPath, string password) { Assembly assembly = Assembly.LoadFrom(sqliteDllPath); Type builderType = assembly.GetType("System.Data.SQLite.SQLiteConnectionStringBuilder", true); object builder = Activator.CreateInstance(builderType); SetPropertyIfExists(builderType, builder, "DataSource", dbPath); SetPropertyIfExists(builderType, builder, "Password", password); SetPropertyIfExists(builderType, builder, "ReadOnly", true); SetPropertyIfExists(builderType, builder, "Pooling", false); SetPropertyIfExists(builderType, builder, "FailIfMissing", true); string connectionString = Convert.ToString(builderType.GetProperty("ConnectionString").GetValue(builder, null)); Type connectionType = assembly.GetType("System.Data.SQLite.SQLiteConnection", true); object connection = Activator.CreateInstance(connectionType, new object[] { connectionString }); DbConnection dbConnection = connection as DbConnection; if (dbConnection == null) { throw new InvalidOperationException("SQLiteConnection is not a DbConnection"); } return dbConnection; } private static void SetPropertyIfExists(Type type, object target, string name, object value) { PropertyInfo property = type.GetProperty(name); if (property != null && property.CanWrite) { property.SetValue(target, value, null); } } private static List> ReadMasterTables(DbConnection connection, int maxTables) { var rows = new List>(); using (DbCommand command = connection.CreateCommand()) { command.CommandText = "select type, name, tbl_name from sqlite_master where type in ('table','view') order by type, name"; using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read() && rows.Count < maxTables) { rows.Add(new Dictionary { { "type", Convert.ToString(reader["type"]) }, { "name", Convert.ToString(reader["name"]) }, { "tbl_name", Convert.ToString(reader["tbl_name"]) } }); } } } return rows; } private static List> ReadColumns(DbConnection connection, List> tables, HashSet onlyTables) { var rows = new List>(); foreach (Dictionary table in tables) { string tableName = Convert.ToString(table["name"]); string tableType = Convert.ToString(table["type"]); if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue; if (onlyTables != null && !onlyTables.Contains(tableName)) continue; using (DbCommand command = connection.CreateCommand()) { command.CommandText = "PRAGMA table_info(" + QuoteIdent(tableName) + ")"; using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read()) { rows.Add(new Dictionary { { "table", tableName }, { "cid", Convert.ToString(reader["cid"]) }, { "name", Convert.ToString(reader["name"]) }, { "type", Convert.ToString(reader["type"]) }, { "notnull", Convert.ToString(reader["notnull"]) }, { "pk", Convert.ToString(reader["pk"]) } }); } } } } return rows; } private static List> ReadTargetCounts(DbConnection connection, List> tables, HashSet targetTables) { var rows = new List>(); foreach (Dictionary table in tables) { string tableName = Convert.ToString(table["name"]); string tableType = Convert.ToString(table["type"]); if (!string.Equals(tableType, "table", StringComparison.OrdinalIgnoreCase)) continue; if (!targetTables.Contains(tableName)) continue; rows.Add(new Dictionary { { "table", tableName }, { "row_count", ExecuteScalar(connection, "select count(*) from " + QuoteIdent(tableName)) } }); } return rows; } private static Dictionary ReadTargetCountMap(DbConnection connection, List> tables, HashSet targetTables) { var rowCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (Dictionary item in ReadTargetCounts(connection, tables, targetTables)) { rowCounts[Convert.ToString(item["table"])] = Convert.ToString(item["row_count"]); } return rowCounts; } private static List> BuildDisplaySources(List> tables, List> columns) { var tableSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (Dictionary table in tables) { if (string.Equals(Convert.ToString(table["type"]), "table", StringComparison.OrdinalIgnoreCase)) { tableSet.Add(Convert.ToString(table["name"])); } } var columnMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (Dictionary column in columns) { string table = Convert.ToString(column["table"]); string name = Convert.ToString(column["name"]); if (!columnMap.ContainsKey(table)) columnMap[table] = new HashSet(StringComparer.OrdinalIgnoreCase); columnMap[table].Add(name); } var sources = new List>(); AddSource(sources, tableSet, columnMap, "contacts_roster", "TD_Roster", new string[] { "JId", "Nick", "GroupName" }); AddSource(sources, tableSet, columnMap, "contacts_effigy", "TD_CustomEffigy", new string[] { "PersonJid", "PersonName" }); AddSource(sources, tableSet, columnMap, "recent_display", "tblRecent", new string[] { "PersonId", "PersonName", "FType" }); AddSource(sources, tableSet, columnMap, "person_message_names", "tblPersonMsg", new string[] { "SndPerson", "SndPersonId", "RecvPerson", "RecvPersonId", "ObjPerson", "ObjPersonId" }); AddSource(sources, tableSet, columnMap, "group_message_names", "tblMsgGroupPersonMsg", new string[] { "MsgGroupId", "MsgGroupName", "SessionPersonId", "SessionPersonName", "GroupType" }); AddSource(sources, tableSet, columnMap, "work_group_auth", "TD_WorkGroupAuth", new string[] { "GroupJID", "GroupName", "ChildGroupJid" }); AddSource(sources, tableSet, columnMap, "received_file_metadata", "TD_ReceiveFileRecord", new string[] { "FileName", "SendPersonJid", "SendPersonName" }); AddSource(sources, tableSet, columnMap, "sent_file_metadata", "TD_SendFileRecord", new string[] { "FileName", "ReceivePersonJid", "ReceivePersonName" }); return sources; } private sealed class MessageSourceSpec { public string Source; public string Table; public string Role; public string[] RequiredColumns; } private static HashSet BuildMessageSourceTableSet() { var tables = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (MessageSourceSpec spec in BuildMessageSourceSpecs()) { tables.Add(spec.Table); } return tables; } private static List BuildMessageSourceSpecs() { return new List { new MessageSourceSpec { Source = "direct_messages", Table = "tblPersonMsg", Role = "direct", RequiredColumns = new string[] { "Id", "SndPersonId", "RecvPersonId", "ObjPersonId", "ActionTime", "MsgText", "MessageBody" } }, new MessageSourceSpec { Source = "group_messages", Table = "tblMsgGroupPersonMsg", Role = "group", RequiredColumns = new string[] { "Id", "SndPersonId", "MsgGroupId", "MsgGroupName", "MsgTime", "MsgText", "MessageBody" } }, new MessageSourceSpec { Source = "system_messages", Table = "TD_SystemMessageRecord", Role = "system", RequiredColumns = new string[] { "MsgGuid", "MsgType", "MsgTitle", "MsgText", "SendPersonJid", "MsgTime", "MsgReadState" } }, new MessageSourceSpec { Source = "group_receipts", Table = "TD_GroupReceiptMessage", Role = "receipt_metadata", RequiredColumns = new string[] { "MsgGuid", "PersonID", "ReceiptTime" } }, new MessageSourceSpec { Source = "received_file_metadata", Table = "TD_ReceiveFileRecord", Role = "attachment_metadata", RequiredColumns = new string[] { "ReceiveMsgGuid", "FileName", "FileSize", "SourceID", "SourceName", "FileType", "SendPersonJid", "SendPersonName", "SendTime" } }, new MessageSourceSpec { Source = "recent_conversation_index", Table = "tblRecent", Role = "conversation_index", RequiredColumns = new string[] { "Id", "PersonId", "PersonName", "ActionTime", "FType" } } }; } private static List> BuildMessageSources(List> tables, Dictionary> columnMap, Dictionary rowCounts) { var tableSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (Dictionary table in tables) { if (string.Equals(Convert.ToString(table["type"]), "table", StringComparison.OrdinalIgnoreCase)) { tableSet.Add(Convert.ToString(table["name"])); } } var sources = new List>(); foreach (MessageSourceSpec spec in BuildMessageSourceSpecs()) { AddMessageSource(sources, tableSet, columnMap, rowCounts, spec); } return sources; } private static void AddMessageSource(List> sources, HashSet tableSet, Dictionary> columnMap, Dictionary rowCounts, MessageSourceSpec spec) { var presentColumns = new List(); if (columnMap.ContainsKey(spec.Table)) { foreach (string column in spec.RequiredColumns) { if (columnMap[spec.Table].Contains(column)) presentColumns.Add(column); } } string rowCount = rowCounts.ContainsKey(spec.Table) ? rowCounts[spec.Table] : "0"; sources.Add(new Dictionary { { "source", spec.Source }, { "table", spec.Table }, { "role", spec.Role }, { "available", tableSet.Contains(spec.Table) && presentColumns.Count == spec.RequiredColumns.Length }, { "required_columns", spec.RequiredColumns }, { "present_columns", presentColumns.ToArray() }, { "row_count", rowCount } }); } private static HashSet BuildListMessagesTableSet() { return new HashSet(StringComparer.OrdinalIgnoreCase) { "tblPersonMsg", "tblMsgGroupPersonMsg", "TD_SystemMessageRecord", "TD_ReceiveFileRecord" }; } private static bool ShouldReadMessageRole(string requested, string role) { return string.Equals(requested, "auto", StringComparison.OrdinalIgnoreCase) || string.Equals(requested, role, StringComparison.OrdinalIgnoreCase); } private static void ReadDirectMessages( DbConnection connection, Dictionary> columnMap, string conversationId, string query, string since, int limit, bool includeBody, bool includeAttachmentMetadata, List> messages, HashSet sourceTables) { const string table = "tblPersonMsg"; if (!HasColumns(columnMap, table, new string[] { "Id" })) return; string senderIdColumn = FirstExistingColumn(columnMap, table, new string[] { "SndPersonId", "SenderId" }); string senderNameColumn = FirstExistingColumn(columnMap, table, new string[] { "SndPerson", "SenderName" }); string receiverIdColumn = FirstExistingColumn(columnMap, table, new string[] { "RecvPersonId", "ReceiverId" }); string receiverNameColumn = FirstExistingColumn(columnMap, table, new string[] { "RecvPerson", "ReceiverName" }); string objectIdColumn = FirstExistingColumn(columnMap, table, new string[] { "ObjPersonId", "PersonId" }); string objectNameColumn = FirstExistingColumn(columnMap, table, new string[] { "ObjPerson", "PersonName" }); string timeColumn = FirstExistingColumn(columnMap, table, new string[] { "ActionTime", "MsgTime", "CreateTime", "SendTime" }); string readColumn = FirstExistingColumn(columnMap, table, new string[] { "IsRead", "ReadState", "MsgReadState" }); using (DbCommand command = connection.CreateCommand()) { var where = new List(); where.Add("coalesce(" + QuoteIdent("Id") + ", '') <> ''"); AddListMessageWhere(command, where, table, columnMap, conversationId, query, since, timeColumn, new string[] { senderIdColumn, receiverIdColumn, objectIdColumn }, new string[] { "Id", senderIdColumn, senderNameColumn, receiverIdColumn, receiverNameColumn, objectIdColumn, objectNameColumn }, includeBody); command.CommandText = "select " + QuoteIdent("Id") + " as message_id, " + SelectColumn(senderIdColumn, "sender_id") + ", " + SelectColumn(FirstNonEmpty(senderNameColumn, objectNameColumn), "sender_name") + ", " + SelectColumn(receiverIdColumn, "receiver_id") + ", " + SelectColumn(FirstNonEmpty(objectIdColumn, receiverIdColumn, senderIdColumn), "conversation_id") + ", " + SelectColumn(timeColumn, "timestamp") + ", " + SelectColumn(readColumn, "read_state") + ", " + BodySelect(columnMap, table, includeBody) + " from " + QuoteIdent(table) + " where " + string.Join(" and ", where.ToArray()) + " order by " + OrderByExpression(timeColumn) + " desc limit " + Convert.ToString(limit); using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read() && messages.Count < limit * 3) { string messageId = ReaderString(reader, "message_id"); string timestamp = ReaderString(reader, "timestamp"); string contentText = includeBody ? ReaderString(reader, "content_text") : ""; List> attachments = includeAttachmentMetadata ? ReadAttachmentsForMessage(connection, columnMap, messageId) : new List>(); messages.Add(new Dictionary { { "message_id", messageId }, { "id", messageId }, { "conversation_id", ReaderString(reader, "conversation_id") }, { "conversation_type", "direct" }, { "sender_id", ReaderString(reader, "sender_id") }, { "sender_name", ReaderString(reader, "sender_name") }, { "receiver_id", ReaderString(reader, "receiver_id") }, { "text", contentText }, { "content_text", contentText }, { "content_type", ContentType(contentText, attachments) }, { "timestamp", timestamp }, { "created_at", timestamp }, { "subject", "" }, { "read", ReaderBool(reader, "read_state") }, { "source", "local_readonly" }, { "raw_ref", "msglib:" + table }, { "attachments", attachments }, { "sort_key", timestamp + "|" + messageId } }); } } } sourceTables.Add(table); } private static void ReadGroupMessages( DbConnection connection, Dictionary> columnMap, string conversationId, string query, string since, int limit, bool includeBody, bool includeAttachmentMetadata, List> messages, HashSet sourceTables) { const string table = "tblMsgGroupPersonMsg"; if (!HasColumns(columnMap, table, new string[] { "Id" })) return; string senderIdColumn = FirstExistingColumn(columnMap, table, new string[] { "SndPersonId", "SessionPersonId", "PersonId" }); string senderNameColumn = FirstExistingColumn(columnMap, table, new string[] { "SndPerson", "SessionPersonName", "PersonName" }); string groupIdColumn = FirstExistingColumn(columnMap, table, new string[] { "MsgGroupId", "GroupJID", "GroupId" }); string groupNameColumn = FirstExistingColumn(columnMap, table, new string[] { "MsgGroupName", "GroupName", "SessionPersonName" }); string timeColumn = FirstExistingColumn(columnMap, table, new string[] { "MsgTime", "ActionTime", "CreateTime", "SendTime" }); string readColumn = FirstExistingColumn(columnMap, table, new string[] { "IsRead", "ReadState", "MsgReadState" }); using (DbCommand command = connection.CreateCommand()) { var where = new List(); where.Add("coalesce(" + QuoteIdent("Id") + ", '') <> ''"); AddListMessageWhere(command, where, table, columnMap, conversationId, query, since, timeColumn, new string[] { groupIdColumn, senderIdColumn }, new string[] { "Id", senderIdColumn, senderNameColumn, groupIdColumn, groupNameColumn }, includeBody); command.CommandText = "select " + QuoteIdent("Id") + " as message_id, " + SelectColumn(senderIdColumn, "sender_id") + ", " + SelectColumn(senderNameColumn, "sender_name") + ", " + SelectColumn(groupIdColumn, "conversation_id") + ", " + SelectColumn(timeColumn, "timestamp") + ", " + SelectColumn(readColumn, "read_state") + ", " + BodySelect(columnMap, table, includeBody) + " from " + QuoteIdent(table) + " where " + string.Join(" and ", where.ToArray()) + " order by " + OrderByExpression(timeColumn) + " desc limit " + Convert.ToString(limit); using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read() && messages.Count < limit * 3) { string messageId = ReaderString(reader, "message_id"); string timestamp = ReaderString(reader, "timestamp"); string contentText = includeBody ? ReaderString(reader, "content_text") : ""; List> attachments = includeAttachmentMetadata ? ReadAttachmentsForMessage(connection, columnMap, messageId) : new List>(); messages.Add(new Dictionary { { "message_id", messageId }, { "id", messageId }, { "conversation_id", ReaderString(reader, "conversation_id") }, { "conversation_type", "group" }, { "sender_id", ReaderString(reader, "sender_id") }, { "sender_name", ReaderString(reader, "sender_name") }, { "receiver_id", "" }, { "text", contentText }, { "content_text", contentText }, { "content_type", ContentType(contentText, attachments) }, { "timestamp", timestamp }, { "created_at", timestamp }, { "subject", "" }, { "read", ReaderBool(reader, "read_state") }, { "source", "local_readonly" }, { "raw_ref", "msglib:" + table }, { "attachments", attachments }, { "sort_key", timestamp + "|" + messageId } }); } } } sourceTables.Add(table); } private static void ReadSystemMessages( DbConnection connection, Dictionary> columnMap, string conversationId, string query, string since, int limit, bool includeBody, bool includeAttachmentMetadata, List> messages, HashSet sourceTables) { const string table = "TD_SystemMessageRecord"; string idColumn = FirstExistingColumn(columnMap, table, new string[] { "MsgGuid", "Id" }); if (string.IsNullOrWhiteSpace(idColumn)) return; string senderIdColumn = FirstExistingColumn(columnMap, table, new string[] { "SendPersonJid", "PersonID", "SourceID" }); string senderNameColumn = FirstExistingColumn(columnMap, table, new string[] { "SendPersonName", "PersonName", "SourceName" }); string titleColumn = FirstExistingColumn(columnMap, table, new string[] { "MsgTitle", "Title" }); string timeColumn = FirstExistingColumn(columnMap, table, new string[] { "MsgTime", "ActionTime", "CreateTime", "SendTime" }); string readColumn = FirstExistingColumn(columnMap, table, new string[] { "MsgReadState", "IsRead", "ReadState" }); using (DbCommand command = connection.CreateCommand()) { var where = new List(); where.Add("coalesce(" + QuoteIdent(idColumn) + ", '') <> ''"); AddListMessageWhere(command, where, table, columnMap, conversationId, query, since, timeColumn, new string[] { senderIdColumn }, new string[] { idColumn, senderIdColumn, senderNameColumn, titleColumn }, includeBody); command.CommandText = "select " + QuoteIdent(idColumn) + " as message_id, " + SelectColumn(senderIdColumn, "sender_id") + ", " + SelectColumn(senderNameColumn, "sender_name") + ", " + SelectColumn(titleColumn, "subject") + ", " + SelectColumn(timeColumn, "timestamp") + ", " + SelectColumn(readColumn, "read_state") + ", " + BodySelect(columnMap, table, includeBody) + " from " + QuoteIdent(table) + " where " + string.Join(" and ", where.ToArray()) + " order by " + OrderByExpression(timeColumn) + " desc limit " + Convert.ToString(limit); using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read() && messages.Count < limit * 3) { string messageId = ReaderString(reader, "message_id"); string timestamp = ReaderString(reader, "timestamp"); string contentText = includeBody ? ReaderString(reader, "content_text") : ""; List> attachments = includeAttachmentMetadata ? ReadAttachmentsForMessage(connection, columnMap, messageId) : new List>(); messages.Add(new Dictionary { { "message_id", messageId }, { "id", messageId }, { "conversation_id", ReaderString(reader, "sender_id") }, { "conversation_type", "system" }, { "sender_id", ReaderString(reader, "sender_id") }, { "sender_name", ReaderString(reader, "sender_name") }, { "receiver_id", "" }, { "text", contentText }, { "content_text", contentText }, { "content_type", ContentType(contentText, attachments) }, { "timestamp", timestamp }, { "created_at", timestamp }, { "subject", ReaderString(reader, "subject") }, { "read", ReaderBool(reader, "read_state") }, { "source", "local_readonly" }, { "raw_ref", "msglib:" + table }, { "attachments", attachments }, { "sort_key", timestamp + "|" + messageId } }); } } } sourceTables.Add(table); } private static void AddListMessageWhere( DbCommand command, List where, string table, Dictionary> columnMap, string conversationId, string query, string since, string timeColumn, string[] conversationColumns, string[] queryColumns, bool includeBody) { if (!string.IsNullOrWhiteSpace(conversationId)) { var parts = new List(); foreach (string column in conversationColumns) { if (!string.IsNullOrWhiteSpace(column) && HasColumns(columnMap, table, new string[] { column })) { parts.Add(QuoteIdent(column) + " = @conversation_id"); } } if (parts.Count > 0) { where.Add("(" + string.Join(" or ", parts.ToArray()) + ")"); AddParameter(command, "@conversation_id", conversationId); } } if (!string.IsNullOrWhiteSpace(query)) { var parts = new List(); foreach (string column in queryColumns) { if (!string.IsNullOrWhiteSpace(column) && HasColumns(columnMap, table, new string[] { column })) { parts.Add(QuoteIdent(column) + " like @query"); } } if (includeBody) { if (HasColumns(columnMap, table, new string[] { "MsgText" })) parts.Add(QuoteIdent("MsgText") + " like @query"); if (HasColumns(columnMap, table, new string[] { "MessageBody" })) parts.Add(QuoteIdent("MessageBody") + " like @query"); } if (parts.Count > 0) { where.Add("(" + string.Join(" or ", parts.ToArray()) + ")"); AddParameter(command, "@query", "%" + query + "%"); } } if (!string.IsNullOrWhiteSpace(since) && !string.IsNullOrWhiteSpace(timeColumn) && HasColumns(columnMap, table, new string[] { timeColumn })) { where.Add(QuoteIdent(timeColumn) + " >= @since"); AddParameter(command, "@since", since); } } private static string BodySelect(Dictionary> columnMap, string table, bool includeBody) { if (!includeBody) { return "'' as content_text"; } var expressions = new List(); if (HasColumns(columnMap, table, new string[] { "MsgText" })) expressions.Add(QuoteIdent("MsgText")); if (HasColumns(columnMap, table, new string[] { "MessageBody" })) expressions.Add(QuoteIdent("MessageBody")); if (expressions.Count == 0) { return "'' as content_text"; } expressions.Add("''"); return "coalesce(" + string.Join(", ", expressions.ToArray()) + ") as content_text"; } private static List> ReadAttachmentsForMessage(DbConnection connection, Dictionary> columnMap, string messageId) { var attachments = new List>(); const string table = "TD_ReceiveFileRecord"; if (string.IsNullOrWhiteSpace(messageId)) return attachments; if (!HasColumns(columnMap, table, new string[] { "ReceiveMsgGuid", "FileName" })) return attachments; string sizeColumn = FirstExistingColumn(columnMap, table, new string[] { "FileSize", "Size" }); string sourceColumn = FirstExistingColumn(columnMap, table, new string[] { "SourceID", "FileGuid", "FileId" }); using (DbCommand command = connection.CreateCommand()) { command.CommandText = "select " + SelectColumn(sourceColumn, "source_id") + ", " + SelectColumn("FileName", "file_name") + ", " + SelectColumn(sizeColumn, "size_bytes") + " from " + QuoteIdent(table) + " where " + QuoteIdent("ReceiveMsgGuid") + " = @message_id limit 20"; AddParameter(command, "@message_id", messageId); using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read()) { string fileName = SafeFileName(ReaderString(reader, "file_name")); if (string.IsNullOrWhiteSpace(fileName)) continue; string sourceId = ReaderString(reader, "source_id"); attachments.Add(new Dictionary { { "file_id", FirstNonEmpty(sourceId, messageId + ":" + fileName) }, { "file_name", fileName }, { "size_bytes", ReaderInt64(reader, "size_bytes") }, { "download_ref", "" } }); } } } return attachments; } private static bool HasColumns(Dictionary> columnMap, string table, string[] columns) { if (!columnMap.ContainsKey(table)) return false; foreach (string column in columns) { if (string.IsNullOrWhiteSpace(column)) return false; if (!columnMap[table].Contains(column)) return false; } return true; } private static string FirstExistingColumn(Dictionary> columnMap, string table, string[] columns) { if (!columnMap.ContainsKey(table)) return ""; foreach (string column in columns) { if (!string.IsNullOrWhiteSpace(column) && columnMap[table].Contains(column)) { return column; } } return ""; } private static string SelectColumn(string column, string alias) { if (string.IsNullOrWhiteSpace(column)) { return "'' as " + QuoteIdent(alias); } return QuoteIdent(column) + " as " + QuoteIdent(alias); } private static string OrderByExpression(string column) { if (string.IsNullOrWhiteSpace(column)) { return "1"; } return QuoteIdent(column); } private static string ReaderString(DbDataReader reader, string name) { try { object value = reader[name]; if (value == null || value == DBNull.Value) return ""; return Convert.ToString(value); } catch { return ""; } } private static bool ReaderBool(DbDataReader reader, string name) { string value = ReaderString(reader, name).Trim(); if (string.IsNullOrWhiteSpace(value)) return false; return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "read", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase); } private static long ReaderInt64(DbDataReader reader, string name) { string value = ReaderString(reader, name).Trim(); long parsed; if (long.TryParse(value, out parsed)) return parsed; return 0; } private static string FirstNonEmpty(params string[] values) { foreach (string value in values) { if (!string.IsNullOrWhiteSpace(value)) return value; } return ""; } private static string ContentType(string contentText, List> attachments) { if (attachments != null && attachments.Count > 0) return "file"; if (string.IsNullOrWhiteSpace(contentText)) return "unknown"; return "text"; } private static int CompareMessagesDescending(Dictionary left, Dictionary right) { string leftKey = left.ContainsKey("sort_key") ? Convert.ToString(left["sort_key"]) : ""; string rightKey = right.ContainsKey("sort_key") ? Convert.ToString(right["sort_key"]) : ""; return string.Compare(rightKey, leftKey, StringComparison.Ordinal); } private static string[] ToSortedArray(HashSet values) { var list = new List(); foreach (string value in values) { list.Add(value); } list.Sort(StringComparer.OrdinalIgnoreCase); return list.ToArray(); } private static void AddParameter(DbCommand command, string name, object value) { DbParameter parameter = command.CreateParameter(); parameter.ParameterName = name; parameter.Value = value ?? DBNull.Value; command.Parameters.Add(parameter); } private static string SafeFileName(string value) { if (string.IsNullOrWhiteSpace(value)) return ""; try { return Path.GetFileName(value); } catch { int slash = Math.Max(value.LastIndexOf('/'), value.LastIndexOf('\\')); if (slash >= 0 && slash + 1 < value.Length) return value.Substring(slash + 1); return value; } } private sealed class DisplayEntitySpec { public string EntityType; public string SourceTable; public string JidColumn; public string NameColumn; public double Confidence; } private static HashSet BuildDisplayEntityTableSet(string entityType) { var tables = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (DisplayEntitySpec spec in BuildDisplayEntitySpecs(entityType)) { tables.Add(spec.SourceTable); } return tables; } private static Dictionary> BuildColumnMap(List> columns) { var columnMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (Dictionary column in columns) { string table = Convert.ToString(column["table"]); string name = Convert.ToString(column["name"]); if (!columnMap.ContainsKey(table)) columnMap[table] = new HashSet(StringComparer.OrdinalIgnoreCase); columnMap[table].Add(name); } return columnMap; } private static List BuildDisplayEntitySpecs(string entityType) { var specs = new List(); if (string.Equals(entityType, "contacts", StringComparison.OrdinalIgnoreCase)) { AddEntitySpec(specs, entityType, "TD_Roster", "JId", "Nick", 0.95); AddEntitySpec(specs, entityType, "TD_CustomEffigy", "PersonJid", "PersonName", 0.90); AddEntitySpec(specs, entityType, "tblRecent", "PersonId", "PersonName", 0.70); AddEntitySpec(specs, entityType, "tblChatLevel", "PersonId", "PersonName", 0.70); AddEntitySpec(specs, entityType, "tblPersonMsg", "SndPersonId", "SndPerson", 0.55); AddEntitySpec(specs, entityType, "tblPersonMsg", "RecvPersonId", "RecvPerson", 0.55); AddEntitySpec(specs, entityType, "tblPersonMsg", "ObjPersonId", "ObjPerson", 0.55); } else if (string.Equals(entityType, "groups", StringComparison.OrdinalIgnoreCase)) { AddEntitySpec(specs, entityType, "TD_WorkGroupAuth", "GroupJID", "GroupName", 0.90); AddEntitySpec(specs, entityType, "TD_WorkGroupAuth", "ChildGroupJid", "GroupName", 0.80); AddEntitySpec(specs, entityType, "tblMsgGroupPersonMsg", "MsgGroupId", "MsgGroupName", 0.70); AddEntitySpec(specs, entityType, "tblMsgGroupPersonMsg", "SessionPersonId", "SessionPersonName", 0.60); } return specs; } private static void AddEntitySpec(List specs, string entityType, string table, string jidColumn, string nameColumn, double confidence) { specs.Add(new DisplayEntitySpec { EntityType = entityType, SourceTable = table, JidColumn = jidColumn, NameColumn = nameColumn, Confidence = confidence }); } private static List> ReadDisplayEntities(DbConnection connection, string entityType, string query, int limit, Dictionary> columnMap) { var rows = new List>(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (DisplayEntitySpec spec in BuildDisplayEntitySpecs(entityType)) { if (rows.Count >= limit) break; if (!columnMap.ContainsKey(spec.SourceTable)) continue; if (!columnMap[spec.SourceTable].Contains(spec.JidColumn)) continue; if (!columnMap[spec.SourceTable].Contains(spec.NameColumn)) continue; ReadDisplayEntitiesForSpec(connection, spec, query, limit - rows.Count, rows, seen); } return rows; } private static void ReadDisplayEntitiesForSpec(DbConnection connection, DisplayEntitySpec spec, string query, int limit, List> rows, HashSet seen) { if (limit <= 0) return; using (DbCommand command = connection.CreateCommand()) { string jid = QuoteIdent(spec.JidColumn); string name = QuoteIdent(spec.NameColumn); command.CommandText = "select " + jid + " as jid, " + name + " as display_name from " + QuoteIdent(spec.SourceTable) + " where (coalesce(" + jid + ", '') <> '' or coalesce(" + name + ", '') <> '')"; if (!string.IsNullOrWhiteSpace(query)) { command.CommandText += " and (" + jid + " like @query or " + name + " like @query)"; DbParameter queryParam = command.CreateParameter(); queryParam.ParameterName = "@query"; queryParam.Value = "%" + query + "%"; command.Parameters.Add(queryParam); } command.CommandText += " limit " + Convert.ToString(limit); using (DbDataReader reader = command.ExecuteReader()) { while (reader.Read() && rows.Count < limit) { string entityJid = Convert.ToString(reader["jid"]); string displayName = Convert.ToString(reader["display_name"]); if (string.IsNullOrWhiteSpace(entityJid) && string.IsNullOrWhiteSpace(displayName)) continue; string key = spec.EntityType + "|" + entityJid + "|" + displayName + "|" + spec.SourceTable; if (seen.Contains(key)) continue; seen.Add(key); rows.Add(new Dictionary { { "entity_type", spec.EntityType }, { "source_table", spec.SourceTable }, { "jid", entityJid }, { "display_name", displayName }, { "confidence", spec.Confidence }, { "matched_columns", new string[] { spec.JidColumn, spec.NameColumn } } }); } } } } private static void AddSource(List> sources, HashSet tableSet, Dictionary> columnMap, string sourceName, string table, string[] columns) { var presentColumns = new List(); if (columnMap.ContainsKey(table)) { foreach (string column in columns) { if (columnMap[table].Contains(column)) presentColumns.Add(column); } } sources.Add(new Dictionary { { "source", sourceName }, { "table", table }, { "available", tableSet.Contains(table) && presentColumns.Count > 0 }, { "required_columns", columns }, { "present_columns", presentColumns.ToArray() } }); } private static string ExecuteScalar(DbConnection connection, string sql) { using (DbCommand command = connection.CreateCommand()) { command.CommandText = sql; object value = command.ExecuteScalar(); return Convert.ToString(value); } } private static void TryExecuteNonQuery(DbConnection connection, string sql) { try { using (DbCommand command = connection.CreateCommand()) { command.CommandText = sql; command.ExecuteNonQuery(); } } catch { } } private static string QuoteIdent(string value) { return "\"" + value.Replace("\"", "\"\"") + "\""; } private static int Clamp(int value, int min, int max) { if (value < min) return min; if (value > max) return max; return value; } } }