openzca 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +595 -268
- package/dist/db-migrations.js +52 -0
- package/dist/db-worker.js +14 -146
- package/dist/migrations/001-initial-schema.sql +138 -0
- package/dist/migrations/002-add-contacts.sql +18 -0
- package/dist/migrations/003-backfill-contacts.sql +158 -0
- package/package.json +2 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/lib/db-migrations.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
var MIGRATIONS = [
|
|
4
|
+
{ version: "001", file: "001-initial-schema.sql" },
|
|
5
|
+
{ version: "002", file: "002-add-contacts.sql" },
|
|
6
|
+
{ version: "003", file: "003-backfill-contacts.sql" }
|
|
7
|
+
];
|
|
8
|
+
var CREATE_MIGRATIONS_TABLE_SQL = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
10
|
+
version TEXT NOT NULL PRIMARY KEY,
|
|
11
|
+
applied_at TEXT NOT NULL
|
|
12
|
+
)
|
|
13
|
+
`;
|
|
14
|
+
function nowIso() {
|
|
15
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
16
|
+
}
|
|
17
|
+
async function readMigrationSql(file) {
|
|
18
|
+
return await fs.readFile(new URL(`./migrations/${file}`, import.meta.url), "utf8");
|
|
19
|
+
}
|
|
20
|
+
function listAppliedVersions(db) {
|
|
21
|
+
const rows = db.prepare("SELECT version FROM schema_migrations").all();
|
|
22
|
+
return new Set(
|
|
23
|
+
rows.map((row) => typeof row.version === "string" ? row.version : "").filter(Boolean)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
async function runMigrations(db) {
|
|
27
|
+
db.exec(CREATE_MIGRATIONS_TABLE_SQL);
|
|
28
|
+
const applied = listAppliedVersions(db);
|
|
29
|
+
for (const migration of MIGRATIONS) {
|
|
30
|
+
if (applied.has(migration.version)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const sql = await readMigrationSql(migration.file);
|
|
34
|
+
db.exec("BEGIN");
|
|
35
|
+
try {
|
|
36
|
+
db.exec(sql);
|
|
37
|
+
db.prepare(
|
|
38
|
+
"INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)"
|
|
39
|
+
).run(migration.version, nowIso());
|
|
40
|
+
db.exec("COMMIT");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
try {
|
|
43
|
+
db.exec("ROLLBACK");
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
runMigrations
|
|
52
|
+
};
|
package/dist/db-worker.js
CHANGED
|
@@ -1,150 +1,5 @@
|
|
|
1
1
|
// src/lib/db-worker.ts
|
|
2
2
|
import { parentPort, workerData } from "worker_threads";
|
|
3
|
-
var INIT_SQL = `
|
|
4
|
-
PRAGMA journal_mode = WAL;
|
|
5
|
-
PRAGMA synchronous = FULL;
|
|
6
|
-
PRAGMA busy_timeout = 5000;
|
|
7
|
-
PRAGMA foreign_keys = ON;
|
|
8
|
-
|
|
9
|
-
CREATE TABLE IF NOT EXISTS threads (
|
|
10
|
-
profile TEXT NOT NULL,
|
|
11
|
-
scope_thread_id TEXT NOT NULL,
|
|
12
|
-
raw_thread_id TEXT NOT NULL,
|
|
13
|
-
thread_type TEXT NOT NULL,
|
|
14
|
-
peer_id TEXT,
|
|
15
|
-
title TEXT,
|
|
16
|
-
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
17
|
-
is_hidden INTEGER NOT NULL DEFAULT 0,
|
|
18
|
-
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
19
|
-
raw_json TEXT,
|
|
20
|
-
created_at TEXT NOT NULL,
|
|
21
|
-
updated_at TEXT NOT NULL,
|
|
22
|
-
PRIMARY KEY (profile, scope_thread_id)
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
CREATE TABLE IF NOT EXISTS thread_members (
|
|
26
|
-
profile TEXT NOT NULL,
|
|
27
|
-
scope_thread_id TEXT NOT NULL,
|
|
28
|
-
user_id TEXT NOT NULL,
|
|
29
|
-
display_name TEXT,
|
|
30
|
-
zalo_name TEXT,
|
|
31
|
-
avatar TEXT,
|
|
32
|
-
account_status INTEGER,
|
|
33
|
-
member_type INTEGER,
|
|
34
|
-
raw_json TEXT,
|
|
35
|
-
snapshot_at_ms INTEGER NOT NULL,
|
|
36
|
-
created_at TEXT NOT NULL,
|
|
37
|
-
updated_at TEXT NOT NULL,
|
|
38
|
-
PRIMARY KEY (profile, scope_thread_id, user_id)
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
CREATE TABLE IF NOT EXISTS friends (
|
|
42
|
-
profile TEXT NOT NULL,
|
|
43
|
-
user_id TEXT NOT NULL,
|
|
44
|
-
display_name TEXT,
|
|
45
|
-
zalo_name TEXT,
|
|
46
|
-
avatar TEXT,
|
|
47
|
-
account_status INTEGER,
|
|
48
|
-
raw_json TEXT,
|
|
49
|
-
created_at TEXT NOT NULL,
|
|
50
|
-
updated_at TEXT NOT NULL,
|
|
51
|
-
PRIMARY KEY (profile, user_id)
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
CREATE TABLE IF NOT EXISTS self_profiles (
|
|
55
|
-
profile TEXT NOT NULL,
|
|
56
|
-
user_id TEXT NOT NULL,
|
|
57
|
-
display_name TEXT,
|
|
58
|
-
info_json TEXT,
|
|
59
|
-
created_at TEXT NOT NULL,
|
|
60
|
-
updated_at TEXT NOT NULL,
|
|
61
|
-
PRIMARY KEY (profile)
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
65
|
-
profile TEXT NOT NULL,
|
|
66
|
-
message_uid TEXT NOT NULL,
|
|
67
|
-
scope_thread_id TEXT NOT NULL,
|
|
68
|
-
raw_thread_id TEXT NOT NULL,
|
|
69
|
-
thread_type TEXT NOT NULL,
|
|
70
|
-
msg_id TEXT,
|
|
71
|
-
cli_msg_id TEXT,
|
|
72
|
-
action_id TEXT,
|
|
73
|
-
sender_id TEXT,
|
|
74
|
-
sender_name TEXT,
|
|
75
|
-
to_id TEXT,
|
|
76
|
-
timestamp_ms INTEGER NOT NULL,
|
|
77
|
-
msg_type TEXT,
|
|
78
|
-
content_text TEXT,
|
|
79
|
-
content_json TEXT,
|
|
80
|
-
quote_msg_id TEXT,
|
|
81
|
-
quote_cli_msg_id TEXT,
|
|
82
|
-
quote_owner_id TEXT,
|
|
83
|
-
quote_text TEXT,
|
|
84
|
-
source TEXT NOT NULL,
|
|
85
|
-
raw_message_json TEXT,
|
|
86
|
-
raw_payload_json TEXT,
|
|
87
|
-
created_at TEXT NOT NULL,
|
|
88
|
-
updated_at TEXT NOT NULL,
|
|
89
|
-
PRIMARY KEY (profile, message_uid)
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
CREATE TABLE IF NOT EXISTS message_media (
|
|
93
|
-
profile TEXT NOT NULL,
|
|
94
|
-
message_uid TEXT NOT NULL,
|
|
95
|
-
item_index INTEGER NOT NULL,
|
|
96
|
-
media_kind TEXT,
|
|
97
|
-
media_url TEXT,
|
|
98
|
-
media_path TEXT,
|
|
99
|
-
media_type TEXT,
|
|
100
|
-
raw_json TEXT,
|
|
101
|
-
created_at TEXT NOT NULL,
|
|
102
|
-
updated_at TEXT NOT NULL,
|
|
103
|
-
PRIMARY KEY (profile, message_uid, item_index)
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
107
|
-
profile TEXT NOT NULL,
|
|
108
|
-
message_uid TEXT NOT NULL,
|
|
109
|
-
item_index INTEGER NOT NULL,
|
|
110
|
-
target_user_id TEXT NOT NULL,
|
|
111
|
-
pos INTEGER,
|
|
112
|
-
len INTEGER,
|
|
113
|
-
mention_type INTEGER,
|
|
114
|
-
raw_json TEXT,
|
|
115
|
-
created_at TEXT NOT NULL,
|
|
116
|
-
updated_at TEXT NOT NULL,
|
|
117
|
-
PRIMARY KEY (profile, message_uid, item_index)
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
CREATE TABLE IF NOT EXISTS sync_state (
|
|
121
|
-
profile TEXT NOT NULL,
|
|
122
|
-
scope TEXT NOT NULL,
|
|
123
|
-
scope_thread_id TEXT NOT NULL,
|
|
124
|
-
thread_type TEXT NOT NULL,
|
|
125
|
-
status TEXT NOT NULL,
|
|
126
|
-
completeness TEXT,
|
|
127
|
-
cursor TEXT,
|
|
128
|
-
last_sync_at TEXT,
|
|
129
|
-
error TEXT,
|
|
130
|
-
created_at TEXT NOT NULL,
|
|
131
|
-
updated_at TEXT NOT NULL,
|
|
132
|
-
PRIMARY KEY (profile, scope)
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
CREATE INDEX IF NOT EXISTS idx_messages_thread_time
|
|
136
|
-
ON messages (profile, scope_thread_id, timestamp_ms DESC);
|
|
137
|
-
CREATE INDEX IF NOT EXISTS idx_messages_msg_id
|
|
138
|
-
ON messages (profile, msg_id);
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_messages_cli_msg_id
|
|
140
|
-
ON messages (profile, cli_msg_id);
|
|
141
|
-
CREATE INDEX IF NOT EXISTS idx_threads_type
|
|
142
|
-
ON threads (profile, thread_type, updated_at DESC);
|
|
143
|
-
CREATE INDEX IF NOT EXISTS idx_members_thread
|
|
144
|
-
ON thread_members (profile, scope_thread_id);
|
|
145
|
-
CREATE INDEX IF NOT EXISTS idx_friends_name
|
|
146
|
-
ON friends (profile, display_name, zalo_name, user_id);
|
|
147
|
-
`;
|
|
148
3
|
if (!parentPort) {
|
|
149
4
|
throw new Error("DB worker requires parentPort");
|
|
150
5
|
}
|
|
@@ -180,6 +35,12 @@ async function loadSqliteModule() {
|
|
|
180
35
|
process.emitWarning = originalEmitWarning;
|
|
181
36
|
}
|
|
182
37
|
}
|
|
38
|
+
async function loadMigrationModule() {
|
|
39
|
+
const currentUrl = new URL(import.meta.url);
|
|
40
|
+
const specifier = currentUrl.pathname.endsWith("/src/lib/db-worker.ts") ? new URL("./db-migrations.ts", currentUrl).href : new URL("./db-migrations.js", currentUrl).href;
|
|
41
|
+
const importDynamic = new Function("specifier", "return import(specifier);");
|
|
42
|
+
return await importDynamic(specifier);
|
|
43
|
+
}
|
|
183
44
|
function setDefensiveMode(db) {
|
|
184
45
|
const maybeDb = db;
|
|
185
46
|
if (typeof maybeDb.enableDefensive === "function") {
|
|
@@ -197,9 +58,16 @@ function allStatement(db, statement) {
|
|
|
197
58
|
}
|
|
198
59
|
async function main() {
|
|
199
60
|
const { DatabaseSync } = await loadSqliteModule();
|
|
61
|
+
const { runMigrations } = await loadMigrationModule();
|
|
200
62
|
const { filename } = workerData;
|
|
201
63
|
const db = new DatabaseSync(filename);
|
|
202
|
-
db.exec(
|
|
64
|
+
db.exec(`
|
|
65
|
+
PRAGMA journal_mode = WAL;
|
|
66
|
+
PRAGMA synchronous = FULL;
|
|
67
|
+
PRAGMA busy_timeout = 5000;
|
|
68
|
+
PRAGMA foreign_keys = ON;
|
|
69
|
+
`);
|
|
70
|
+
await runMigrations(db);
|
|
203
71
|
setDefensiveMode(db);
|
|
204
72
|
port.postMessage({ type: "ready" });
|
|
205
73
|
port.on("message", (message) => {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
2
|
+
profile TEXT NOT NULL,
|
|
3
|
+
scope_thread_id TEXT NOT NULL,
|
|
4
|
+
raw_thread_id TEXT NOT NULL,
|
|
5
|
+
thread_type TEXT NOT NULL,
|
|
6
|
+
peer_id TEXT,
|
|
7
|
+
title TEXT,
|
|
8
|
+
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
is_hidden INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
raw_json TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL,
|
|
14
|
+
PRIMARY KEY (profile, scope_thread_id)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS thread_members (
|
|
18
|
+
profile TEXT NOT NULL,
|
|
19
|
+
scope_thread_id TEXT NOT NULL,
|
|
20
|
+
user_id TEXT NOT NULL,
|
|
21
|
+
display_name TEXT,
|
|
22
|
+
zalo_name TEXT,
|
|
23
|
+
avatar TEXT,
|
|
24
|
+
account_status INTEGER,
|
|
25
|
+
member_type INTEGER,
|
|
26
|
+
raw_json TEXT,
|
|
27
|
+
snapshot_at_ms INTEGER NOT NULL,
|
|
28
|
+
created_at TEXT NOT NULL,
|
|
29
|
+
updated_at TEXT NOT NULL,
|
|
30
|
+
PRIMARY KEY (profile, scope_thread_id, user_id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS friends (
|
|
34
|
+
profile TEXT NOT NULL,
|
|
35
|
+
user_id TEXT NOT NULL,
|
|
36
|
+
display_name TEXT,
|
|
37
|
+
zalo_name TEXT,
|
|
38
|
+
avatar TEXT,
|
|
39
|
+
account_status INTEGER,
|
|
40
|
+
raw_json TEXT,
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
updated_at TEXT NOT NULL,
|
|
43
|
+
PRIMARY KEY (profile, user_id)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS self_profiles (
|
|
47
|
+
profile TEXT NOT NULL,
|
|
48
|
+
user_id TEXT NOT NULL,
|
|
49
|
+
display_name TEXT,
|
|
50
|
+
info_json TEXT,
|
|
51
|
+
created_at TEXT NOT NULL,
|
|
52
|
+
updated_at TEXT NOT NULL,
|
|
53
|
+
PRIMARY KEY (profile)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
57
|
+
profile TEXT NOT NULL,
|
|
58
|
+
message_uid TEXT NOT NULL,
|
|
59
|
+
scope_thread_id TEXT NOT NULL,
|
|
60
|
+
raw_thread_id TEXT NOT NULL,
|
|
61
|
+
thread_type TEXT NOT NULL,
|
|
62
|
+
msg_id TEXT,
|
|
63
|
+
cli_msg_id TEXT,
|
|
64
|
+
action_id TEXT,
|
|
65
|
+
sender_id TEXT,
|
|
66
|
+
sender_name TEXT,
|
|
67
|
+
to_id TEXT,
|
|
68
|
+
timestamp_ms INTEGER NOT NULL,
|
|
69
|
+
msg_type TEXT,
|
|
70
|
+
content_text TEXT,
|
|
71
|
+
content_json TEXT,
|
|
72
|
+
quote_msg_id TEXT,
|
|
73
|
+
quote_cli_msg_id TEXT,
|
|
74
|
+
quote_owner_id TEXT,
|
|
75
|
+
quote_text TEXT,
|
|
76
|
+
source TEXT NOT NULL,
|
|
77
|
+
raw_message_json TEXT,
|
|
78
|
+
raw_payload_json TEXT,
|
|
79
|
+
created_at TEXT NOT NULL,
|
|
80
|
+
updated_at TEXT NOT NULL,
|
|
81
|
+
PRIMARY KEY (profile, message_uid)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS message_media (
|
|
85
|
+
profile TEXT NOT NULL,
|
|
86
|
+
message_uid TEXT NOT NULL,
|
|
87
|
+
item_index INTEGER NOT NULL,
|
|
88
|
+
media_kind TEXT,
|
|
89
|
+
media_url TEXT,
|
|
90
|
+
media_path TEXT,
|
|
91
|
+
media_type TEXT,
|
|
92
|
+
raw_json TEXT,
|
|
93
|
+
created_at TEXT NOT NULL,
|
|
94
|
+
updated_at TEXT NOT NULL,
|
|
95
|
+
PRIMARY KEY (profile, message_uid, item_index)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
99
|
+
profile TEXT NOT NULL,
|
|
100
|
+
message_uid TEXT NOT NULL,
|
|
101
|
+
item_index INTEGER NOT NULL,
|
|
102
|
+
target_user_id TEXT NOT NULL,
|
|
103
|
+
pos INTEGER,
|
|
104
|
+
len INTEGER,
|
|
105
|
+
mention_type INTEGER,
|
|
106
|
+
raw_json TEXT,
|
|
107
|
+
created_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL,
|
|
109
|
+
PRIMARY KEY (profile, message_uid, item_index)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
113
|
+
profile TEXT NOT NULL,
|
|
114
|
+
scope TEXT NOT NULL,
|
|
115
|
+
scope_thread_id TEXT NOT NULL,
|
|
116
|
+
thread_type TEXT NOT NULL,
|
|
117
|
+
status TEXT NOT NULL,
|
|
118
|
+
completeness TEXT,
|
|
119
|
+
cursor TEXT,
|
|
120
|
+
last_sync_at TEXT,
|
|
121
|
+
error TEXT,
|
|
122
|
+
created_at TEXT NOT NULL,
|
|
123
|
+
updated_at TEXT NOT NULL,
|
|
124
|
+
PRIMARY KEY (profile, scope)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread_time
|
|
128
|
+
ON messages (profile, scope_thread_id, timestamp_ms DESC);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_messages_msg_id
|
|
130
|
+
ON messages (profile, msg_id);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_messages_cli_msg_id
|
|
132
|
+
ON messages (profile, cli_msg_id);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_threads_type
|
|
134
|
+
ON threads (profile, thread_type, updated_at DESC);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_members_thread
|
|
136
|
+
ON thread_members (profile, scope_thread_id);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_friends_name
|
|
138
|
+
ON friends (profile, display_name, zalo_name, user_id);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS contacts (
|
|
2
|
+
profile TEXT NOT NULL,
|
|
3
|
+
user_id TEXT NOT NULL,
|
|
4
|
+
display_name TEXT,
|
|
5
|
+
zalo_name TEXT,
|
|
6
|
+
avatar TEXT,
|
|
7
|
+
account_status INTEGER,
|
|
8
|
+
relationship TEXT NOT NULL DEFAULT 'unknown',
|
|
9
|
+
first_seen_at_ms INTEGER,
|
|
10
|
+
last_seen_at_ms INTEGER,
|
|
11
|
+
raw_json TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL,
|
|
14
|
+
PRIMARY KEY (profile, user_id)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_name
|
|
18
|
+
ON contacts (profile, display_name, zalo_name, user_id);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
INSERT INTO contacts (
|
|
2
|
+
profile,
|
|
3
|
+
user_id,
|
|
4
|
+
display_name,
|
|
5
|
+
zalo_name,
|
|
6
|
+
avatar,
|
|
7
|
+
account_status,
|
|
8
|
+
relationship,
|
|
9
|
+
first_seen_at_ms,
|
|
10
|
+
last_seen_at_ms,
|
|
11
|
+
raw_json,
|
|
12
|
+
created_at,
|
|
13
|
+
updated_at
|
|
14
|
+
)
|
|
15
|
+
SELECT
|
|
16
|
+
profile,
|
|
17
|
+
user_id,
|
|
18
|
+
display_name,
|
|
19
|
+
zalo_name,
|
|
20
|
+
avatar,
|
|
21
|
+
account_status,
|
|
22
|
+
'friend',
|
|
23
|
+
NULL,
|
|
24
|
+
NULL,
|
|
25
|
+
raw_json,
|
|
26
|
+
created_at,
|
|
27
|
+
updated_at
|
|
28
|
+
FROM friends
|
|
29
|
+
WHERE 1 = 1
|
|
30
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
31
|
+
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
32
|
+
zalo_name = COALESCE(excluded.zalo_name, contacts.zalo_name),
|
|
33
|
+
avatar = COALESCE(excluded.avatar, contacts.avatar),
|
|
34
|
+
account_status = COALESCE(excluded.account_status, contacts.account_status),
|
|
35
|
+
relationship = CASE
|
|
36
|
+
WHEN contacts.relationship = 'friend' OR excluded.relationship = 'friend' THEN 'friend'
|
|
37
|
+
WHEN contacts.relationship = 'seen_dm' OR excluded.relationship = 'seen_dm' THEN 'seen_dm'
|
|
38
|
+
WHEN contacts.relationship = 'seen_group' OR excluded.relationship = 'seen_group' THEN 'seen_group'
|
|
39
|
+
ELSE COALESCE(excluded.relationship, contacts.relationship, 'unknown')
|
|
40
|
+
END,
|
|
41
|
+
raw_json = COALESCE(excluded.raw_json, contacts.raw_json),
|
|
42
|
+
updated_at = excluded.updated_at;
|
|
43
|
+
|
|
44
|
+
INSERT INTO contacts (
|
|
45
|
+
profile,
|
|
46
|
+
user_id,
|
|
47
|
+
display_name,
|
|
48
|
+
zalo_name,
|
|
49
|
+
avatar,
|
|
50
|
+
account_status,
|
|
51
|
+
relationship,
|
|
52
|
+
first_seen_at_ms,
|
|
53
|
+
last_seen_at_ms,
|
|
54
|
+
raw_json,
|
|
55
|
+
created_at,
|
|
56
|
+
updated_at
|
|
57
|
+
)
|
|
58
|
+
SELECT
|
|
59
|
+
profile,
|
|
60
|
+
user_id,
|
|
61
|
+
NULLIF(MAX(display_name), ''),
|
|
62
|
+
NULLIF(MAX(zalo_name), ''),
|
|
63
|
+
NULLIF(MAX(avatar), ''),
|
|
64
|
+
MAX(account_status),
|
|
65
|
+
'seen_group',
|
|
66
|
+
MIN(snapshot_at_ms),
|
|
67
|
+
MAX(snapshot_at_ms),
|
|
68
|
+
NULLIF(MAX(raw_json), ''),
|
|
69
|
+
MIN(created_at),
|
|
70
|
+
MAX(updated_at)
|
|
71
|
+
FROM thread_members
|
|
72
|
+
WHERE 1 = 1
|
|
73
|
+
GROUP BY profile, user_id
|
|
74
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
75
|
+
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
76
|
+
zalo_name = COALESCE(excluded.zalo_name, contacts.zalo_name),
|
|
77
|
+
avatar = COALESCE(excluded.avatar, contacts.avatar),
|
|
78
|
+
account_status = COALESCE(excluded.account_status, contacts.account_status),
|
|
79
|
+
relationship = CASE
|
|
80
|
+
WHEN contacts.relationship = 'friend' OR excluded.relationship = 'friend' THEN 'friend'
|
|
81
|
+
WHEN contacts.relationship = 'seen_dm' OR excluded.relationship = 'seen_dm' THEN 'seen_dm'
|
|
82
|
+
WHEN contacts.relationship = 'seen_group' OR excluded.relationship = 'seen_group' THEN 'seen_group'
|
|
83
|
+
ELSE COALESCE(excluded.relationship, contacts.relationship, 'unknown')
|
|
84
|
+
END,
|
|
85
|
+
first_seen_at_ms = CASE
|
|
86
|
+
WHEN contacts.first_seen_at_ms IS NULL THEN excluded.first_seen_at_ms
|
|
87
|
+
WHEN excluded.first_seen_at_ms IS NULL THEN contacts.first_seen_at_ms
|
|
88
|
+
ELSE MIN(contacts.first_seen_at_ms, excluded.first_seen_at_ms)
|
|
89
|
+
END,
|
|
90
|
+
last_seen_at_ms = CASE
|
|
91
|
+
WHEN contacts.last_seen_at_ms IS NULL THEN excluded.last_seen_at_ms
|
|
92
|
+
WHEN excluded.last_seen_at_ms IS NULL THEN contacts.last_seen_at_ms
|
|
93
|
+
ELSE MAX(contacts.last_seen_at_ms, excluded.last_seen_at_ms)
|
|
94
|
+
END,
|
|
95
|
+
raw_json = COALESCE(excluded.raw_json, contacts.raw_json),
|
|
96
|
+
updated_at = excluded.updated_at;
|
|
97
|
+
|
|
98
|
+
INSERT INTO contacts (
|
|
99
|
+
profile,
|
|
100
|
+
user_id,
|
|
101
|
+
display_name,
|
|
102
|
+
zalo_name,
|
|
103
|
+
avatar,
|
|
104
|
+
account_status,
|
|
105
|
+
relationship,
|
|
106
|
+
first_seen_at_ms,
|
|
107
|
+
last_seen_at_ms,
|
|
108
|
+
raw_json,
|
|
109
|
+
created_at,
|
|
110
|
+
updated_at
|
|
111
|
+
)
|
|
112
|
+
SELECT
|
|
113
|
+
t.profile,
|
|
114
|
+
COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id) AS user_id,
|
|
115
|
+
COALESCE(
|
|
116
|
+
NULLIF(MAX(CASE
|
|
117
|
+
WHEN m.sender_id = COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id) THEN m.sender_name
|
|
118
|
+
ELSE NULL
|
|
119
|
+
END), ''),
|
|
120
|
+
NULLIF(MAX(t.title), '')
|
|
121
|
+
) AS display_name,
|
|
122
|
+
NULL,
|
|
123
|
+
NULL,
|
|
124
|
+
NULL,
|
|
125
|
+
'seen_dm',
|
|
126
|
+
MIN(m.timestamp_ms),
|
|
127
|
+
MAX(m.timestamp_ms),
|
|
128
|
+
t.raw_json,
|
|
129
|
+
MIN(t.created_at),
|
|
130
|
+
MAX(COALESCE(m.updated_at, t.updated_at))
|
|
131
|
+
FROM threads t
|
|
132
|
+
LEFT JOIN messages m
|
|
133
|
+
ON m.profile = t.profile
|
|
134
|
+
AND m.scope_thread_id = t.scope_thread_id
|
|
135
|
+
AND m.thread_type = 'user'
|
|
136
|
+
WHERE t.thread_type = 'user'
|
|
137
|
+
AND COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id) <> ''
|
|
138
|
+
GROUP BY t.profile, COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id), t.raw_json
|
|
139
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
140
|
+
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
141
|
+
relationship = CASE
|
|
142
|
+
WHEN contacts.relationship = 'friend' OR excluded.relationship = 'friend' THEN 'friend'
|
|
143
|
+
WHEN contacts.relationship = 'seen_dm' OR excluded.relationship = 'seen_dm' THEN 'seen_dm'
|
|
144
|
+
WHEN contacts.relationship = 'seen_group' OR excluded.relationship = 'seen_group' THEN 'seen_group'
|
|
145
|
+
ELSE COALESCE(excluded.relationship, contacts.relationship, 'unknown')
|
|
146
|
+
END,
|
|
147
|
+
first_seen_at_ms = CASE
|
|
148
|
+
WHEN contacts.first_seen_at_ms IS NULL THEN excluded.first_seen_at_ms
|
|
149
|
+
WHEN excluded.first_seen_at_ms IS NULL THEN contacts.first_seen_at_ms
|
|
150
|
+
ELSE MIN(contacts.first_seen_at_ms, excluded.first_seen_at_ms)
|
|
151
|
+
END,
|
|
152
|
+
last_seen_at_ms = CASE
|
|
153
|
+
WHEN contacts.last_seen_at_ms IS NULL THEN excluded.last_seen_at_ms
|
|
154
|
+
WHEN excluded.last_seen_at_ms IS NULL THEN contacts.last_seen_at_ms
|
|
155
|
+
ELSE MAX(contacts.last_seen_at_ms, excluded.last_seen_at_ms)
|
|
156
|
+
END,
|
|
157
|
+
raw_json = COALESCE(excluded.raw_json, contacts.raw_json),
|
|
158
|
+
updated_at = excluded.updated_at;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzca",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
4
4
|
"description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "npm run build:cli && npm run build:worker",
|
|
18
18
|
"build:cli": "tsup src/cli.ts --format esm --target node22 --out-dir dist --clean",
|
|
19
|
-
"build:worker": "tsup src/lib/db-worker.ts --format esm --target node22 --out-dir dist",
|
|
19
|
+
"build:worker": "tsup src/lib/db-worker.ts src/lib/db-migrations.ts --format esm --target node22 --out-dir dist && node scripts/copy-db-migrations.mjs",
|
|
20
20
|
"dev": "tsx src/cli.ts",
|
|
21
21
|
"test": "tsx --test tests/*.test.ts",
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json",
|