feat: add msglib display entity extraction
This commit is contained in:
@@ -8,7 +8,7 @@ namespace MsgLibReadSidecar
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private const string Version = "0.1.0";
|
||||
private const string Version = "0.2.0";
|
||||
private const string DefaultPassword = "123";
|
||||
|
||||
private static readonly HashSet<string> DefaultTargetTables = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -46,6 +46,9 @@ namespace MsgLibReadSidecar
|
||||
case "display_sources":
|
||||
response = SidecarProtocol.Success(requestId, op, SchemaSummary(opArgs, true));
|
||||
break;
|
||||
case "display_entities":
|
||||
response = SidecarProtocol.Success(requestId, op, DisplayEntities(opArgs));
|
||||
break;
|
||||
default:
|
||||
response = SidecarProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op);
|
||||
break;
|
||||
@@ -71,6 +74,8 @@ namespace MsgLibReadSidecar
|
||||
{ "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", false }
|
||||
};
|
||||
@@ -118,6 +123,52 @@ namespace MsgLibReadSidecar
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static Dictionary<string, object> DisplayEntities(Dictionary<string, object> 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<string, object>
|
||||
{
|
||||
{ "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<Dictionary<string, object>> tables = ReadMasterTables(connection, 256);
|
||||
HashSet<string> entityTables = BuildDisplayEntityTableSet(entityType);
|
||||
List<Dictionary<string, object>> columns = ReadColumns(connection, tables, entityTables);
|
||||
Dictionary<string, HashSet<string>> columnMap = BuildColumnMap(columns);
|
||||
List<Dictionary<string, object>> 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))
|
||||
@@ -269,6 +320,133 @@ namespace MsgLibReadSidecar
|
||||
return sources;
|
||||
}
|
||||
|
||||
|
||||
private sealed class DisplayEntitySpec
|
||||
{
|
||||
public string EntityType;
|
||||
public string SourceTable;
|
||||
public string JidColumn;
|
||||
public string NameColumn;
|
||||
public double Confidence;
|
||||
}
|
||||
|
||||
private static HashSet<string> BuildDisplayEntityTableSet(string entityType)
|
||||
{
|
||||
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (DisplayEntitySpec spec in BuildDisplayEntitySpecs(entityType))
|
||||
{
|
||||
tables.Add(spec.SourceTable);
|
||||
}
|
||||
return tables;
|
||||
}
|
||||
|
||||
private static Dictionary<string, HashSet<string>> BuildColumnMap(List<Dictionary<string, object>> columns)
|
||||
{
|
||||
var columnMap = new Dictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (Dictionary<string, object> column in columns)
|
||||
{
|
||||
string table = Convert.ToString(column["table"]);
|
||||
string name = Convert.ToString(column["name"]);
|
||||
if (!columnMap.ContainsKey(table)) columnMap[table] = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
columnMap[table].Add(name);
|
||||
}
|
||||
return columnMap;
|
||||
}
|
||||
|
||||
private static List<DisplayEntitySpec> BuildDisplayEntitySpecs(string entityType)
|
||||
{
|
||||
var specs = new List<DisplayEntitySpec>();
|
||||
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<DisplayEntitySpec> 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<Dictionary<string, object>> ReadDisplayEntities(DbConnection connection, string entityType, string query, int limit, Dictionary<string, HashSet<string>> columnMap)
|
||||
{
|
||||
var rows = new List<Dictionary<string, object>>();
|
||||
var seen = new HashSet<string>(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<Dictionary<string, object>> rows, HashSet<string> 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<string, object>
|
||||
{
|
||||
{ "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<Dictionary<string, object>> sources, HashSet<string> tableSet, Dictionary<string, HashSet<string>> columnMap, string sourceName, string table, string[] columns)
|
||||
{
|
||||
var presentColumns = new List<string>();
|
||||
|
||||
Reference in New Issue
Block a user