teams-cli 0.2.3__tar.gz → 0.2.5__tar.gz
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.
- {teams_cli-0.2.3 → teams_cli-0.2.5}/CHANGELOG.md +21 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/Cargo.lock +5 -5
- {teams_cli-0.2.3 → teams_cli-0.2.5}/Cargo.toml +1 -1
- {teams_cli-0.2.3 → teams_cli-0.2.5}/PKG-INFO +1 -1
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/browser.rs +4 -2
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/helper.rs +1 -1
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop.js +32 -7
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop.rs +39 -7
- teams_cli-0.2.5/src/desktop_cache.js +48 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop_people.js +1 -1
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/tui.rs +91 -2
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/desktop_scripts.test.cjs +108 -8
- teams_cli-0.2.3/src/desktop_cache.js +0 -17
- {teams_cli-0.2.3 → teams_cli-0.2.5}/.github/workflows/ci.yml +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/.github/workflows/dependencies.yml +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/.github/workflows/release.yml +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/.gitignore +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/.mise.toml +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/LICENSE +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/Makefile +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/README.md +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/build.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/docs/releases.md +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/examples/validate_schema.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/pyproject.toml +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/scripts/smoke-release.py +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/auth/pass.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/auth/process.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/auth/wsl.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/auth/wsl_credentials.ps1 +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/auth.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/backend.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/cli.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/config.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/delivery.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/avatars.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/browser_children.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/browser_core.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/browser_install.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/browser_lifetime.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/browser_setup.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/cdp.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/connection.ps1 +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/connection.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/install.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/macos.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/owner.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/session.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop/windows.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop_core.js +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/desktop_send.js +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/error.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/graph.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/lib.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/main.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/output.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/render.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/schema.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/setup/capabilities.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/setup.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/src/teams_cli_alias.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/browser_lifetime.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/cli.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/connection.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/connection.test.ps1 +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/credentials.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/desktop.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/desktop_scripts.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/fixtures/clispec-v0.3.json +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/js/people.test.mjs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/package_alias.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/pass_credentials.sh +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/release_smoke.rs +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/tests/wsl_credentials.test.ps1 +0 -0
- {teams_cli-0.2.3 → teams_cli-0.2.5}/vership.toml +0 -0
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.5](https://github.com/rvben/teams-cli/compare/v0.2.4...v0.2.5) - 2026-10-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Update TLS certificate verification and bundled root certificates to address RUSTSEC-2026-0382 and RUSTSEC-2026-0383.
|
|
15
|
+
- Desktop chats whose messages Teams keeps in a secondary reply-chain store (`replychains-2`, ...) no longer look empty. All such stores are now read and merged, which also fills in member names for those 1:1 chats.
|
|
16
|
+
- Desktop chat messages missing from the current cache are filled in from caches the same user and tenant left under earlier display languages. Those caches no longer update, so messages edited or deleted since can appear as they were.
|
|
17
|
+
- In the TUI, a chat whose messages are missing now shows its latest message from the chat list and why, instead of "No messages in this chat yet." With the desktop backend it says no messages for the chat were found in the Teams cache on this PC.
|
|
18
|
+
- **deps**: update TLS certificate trust dependencies ([435b11c](https://github.com/rvben/teams-cli/commit/435b11c39113e165e843d73579bc28505ef05620))
|
|
19
|
+
|
|
20
|
+
## [0.2.4](https://github.com/rvben/teams-cli/compare/v0.2.3...v0.2.4) - 2026-10-20
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- When Teams keeps caches for several accounts, guest organizations or display languages, the desktop backend now reads the one for the signed-in user, tenant and display language instead of refusing.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Desktop errors now tell apart two cases: personal and work/school Teams both being open, which `--desktop-account` resolves, and one surface holding cached data for several accounts or organizations, which it does not.
|
|
29
|
+
- Windows desktop helpers use protocol 5. WSL requires the helper from the same CLI release.
|
|
30
|
+
|
|
10
31
|
## [0.2.3](https://github.com/rvben/teams-cli/compare/v0.2.2...v0.2.3) - 2026-10-16
|
|
11
32
|
|
|
12
33
|
### Fixed
|
|
@@ -2815,9 +2815,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
|
|
2815
2815
|
|
|
2816
2816
|
[[package]]
|
|
2817
2817
|
name = "rustls-webpki"
|
|
2818
|
-
version = "0.103.
|
|
2818
|
+
version = "0.103.16"
|
|
2819
2819
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2820
|
-
checksum = "
|
|
2820
|
+
checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281"
|
|
2821
2821
|
dependencies = [
|
|
2822
2822
|
"aws-lc-rs",
|
|
2823
2823
|
"ring",
|
|
@@ -3187,7 +3187,7 @@ dependencies = [
|
|
|
3187
3187
|
|
|
3188
3188
|
[[package]]
|
|
3189
3189
|
name = "teams-cli"
|
|
3190
|
-
version = "0.2.
|
|
3190
|
+
version = "0.2.5"
|
|
3191
3191
|
dependencies = [
|
|
3192
3192
|
"assert_cmd",
|
|
3193
3193
|
"base64 0.23.1",
|
|
@@ -3843,9 +3843,9 @@ dependencies = [
|
|
|
3843
3843
|
|
|
3844
3844
|
[[package]]
|
|
3845
3845
|
name = "webpki-root-certs"
|
|
3846
|
-
version = "1.0.
|
|
3846
|
+
version = "1.0.10"
|
|
3847
3847
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3848
|
-
checksum = "
|
|
3848
|
+
checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb"
|
|
3849
3849
|
dependencies = [
|
|
3850
3850
|
"rustls-pki-types",
|
|
3851
3851
|
]
|
|
@@ -1690,9 +1690,11 @@ mod tests {
|
|
|
1690
1690
|
.unwrap();
|
|
1691
1691
|
std::fs::set_permissions(&browser, std::fs::Permissions::from_mode(0o700)).unwrap();
|
|
1692
1692
|
let started = std::time::Instant::now();
|
|
1693
|
-
|
|
1693
|
+
// Fresh executable inspection on macOS can exceed 100 ms before the
|
|
1694
|
+
// script writes its PID. Allow startup while still bounding the hang.
|
|
1695
|
+
let error = browser_version_with_timeout(&browser, Duration::from_secs(1)).unwrap_err();
|
|
1694
1696
|
assert!(error.to_string().contains("timed out"));
|
|
1695
|
-
assert!(started.elapsed() < Duration::from_secs(
|
|
1697
|
+
assert!(started.elapsed() < Duration::from_secs(3));
|
|
1696
1698
|
let pid: i32 = std::fs::read_to_string(dir.path().join("browser.pid"))
|
|
1697
1699
|
.unwrap()
|
|
1698
1700
|
.parse()
|
|
@@ -10,7 +10,7 @@ use crate::error::AppError;
|
|
|
10
10
|
use serde_json::{Value, json};
|
|
11
11
|
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncWrite, AsyncWriteExt};
|
|
12
12
|
|
|
13
|
-
pub const PROTOCOL: u32 =
|
|
13
|
+
pub const PROTOCOL: u32 = 5;
|
|
14
14
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
15
15
|
/// The commit this binary was built from (see `build.rs`), reported in `hello` for
|
|
16
16
|
/// diagnosis and never compared; empty when unknown.
|
|
@@ -7,13 +7,16 @@ async (p, select) => {
|
|
|
7
7
|
q.onsuccess = () => resolve(q.result);
|
|
8
8
|
q.onerror = () => reject(q.error);
|
|
9
9
|
});
|
|
10
|
-
const
|
|
11
|
-
if (!cache[k]) throw Error('layout');
|
|
10
|
+
const openName = name => {
|
|
12
11
|
// If the cache disappeared after enumeration, abort rather than create a database.
|
|
13
|
-
const q = indexedDB.open(
|
|
12
|
+
const q = indexedDB.open(name);
|
|
14
13
|
q.onupgradeneeded = () => q.transaction.abort();
|
|
15
14
|
return req(q);
|
|
16
15
|
};
|
|
16
|
+
const open = k => {
|
|
17
|
+
if (!cache[k]) throw Error('layout');
|
|
18
|
+
return openName(cache[k]);
|
|
19
|
+
};
|
|
17
20
|
// Missing cache timestamps can be -1. Keep sort keys valid for CLI cursors.
|
|
18
21
|
const time = t => Number.isFinite(+t) && +t > 0 && +t <= 8640000000000000 ? Math.trunc(+t) : 0;
|
|
19
22
|
const iso = t => time(t) ? new Date(time(t)).toISOString() : null;
|
|
@@ -64,8 +67,16 @@ async (p, select) => {
|
|
|
64
67
|
}
|
|
65
68
|
return { names, me };
|
|
66
69
|
};
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// Teams can split reply chains across several stores (replychains, replychains-2, ...).
|
|
71
|
+
// Numeric order keeps the primary store's copy of a message found in more than one.
|
|
72
|
+
const chain = async (db, id) => {
|
|
73
|
+
const rank = n => +(n.split('-')[1] || 1);
|
|
74
|
+
const names = [...db.objectStoreNames].filter(n => /^replychains(-\d+)?$/.test(n)).sort((a, b) => rank(a) - rank(b));
|
|
75
|
+
if (!names.length) throw Error('layout');
|
|
76
|
+
const tx = db.transaction(names);
|
|
77
|
+
const lists = await Promise.all(names.map(n => req(tx.objectStore(n).getAll(IDBKeyRange.bound([id], [id, []])))));
|
|
78
|
+
return lists.flat().flatMap(rc => Object.values((rc && rc.messageMap) || {}));
|
|
79
|
+
};
|
|
69
80
|
|
|
70
81
|
const cdb = await open('conversation');
|
|
71
82
|
let pg;
|
|
@@ -124,10 +135,24 @@ async (p, select) => {
|
|
|
124
135
|
}
|
|
125
136
|
return { me, items: chats.map(x => x.value), next: pg.next };
|
|
126
137
|
}
|
|
138
|
+
// The current cache wins; older caches of the same account only add messages it lacks.
|
|
139
|
+
const messages = await chain(rdb, p.chat);
|
|
140
|
+
for (const name of cache.older || []) {
|
|
141
|
+
let db;
|
|
142
|
+
try {
|
|
143
|
+
db = await openName(name);
|
|
144
|
+
messages.push(...await chain(db, p.chat));
|
|
145
|
+
} catch (_) {
|
|
146
|
+
// Older caches are optional; one that vanished or changed layout is skipped.
|
|
147
|
+
} finally {
|
|
148
|
+
if (db) db.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
127
151
|
const rows = [], seen = new Set();
|
|
128
|
-
for (const m of
|
|
129
|
-
if (
|
|
152
|
+
for (const m of messages) {
|
|
153
|
+
if (seen.has(m.id)) continue;
|
|
130
154
|
seen.add(m.id);
|
|
155
|
+
if (system(m.messageType)) continue;
|
|
131
156
|
rows.push({ t: time(m.originalArrivalTime), id: m.id, value: {
|
|
132
157
|
id: m.id, createdDateTime: iso(m.originalArrivalTime),
|
|
133
158
|
clientMessageId: typeof m.clientMessageId === 'string' ? m.clientMessageId : null,
|
|
@@ -71,7 +71,11 @@ pub fn messaging_unsupported() -> AppError {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
fn account_selection_error() -> AppError {
|
|
74
|
-
AppError::Desktop("
|
|
74
|
+
AppError::Desktop("Both personal and work/school Teams are open; use `init --backend desktop --desktop-account personal` or `--desktop-account work-school` to select one".into())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn cached_accounts_error() -> AppError {
|
|
78
|
+
AppError::Desktop("Teams holds cached data for more than one account, organization or display language, and Teams does not record which one is signed in. For a work/school account, Microsoft Graph works instead: `teams init --backend graph`".into())
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
@@ -676,6 +680,7 @@ fn people_result(reply: &Value, create: bool) -> Result<Value, AppError> {
|
|
|
676
680
|
if let Some(code) = value["error"].as_str() {
|
|
677
681
|
return Err(match code {
|
|
678
682
|
"account_ambiguous" => account_selection_error(),
|
|
683
|
+
"cache_ambiguous" => cached_accounts_error(),
|
|
679
684
|
"permission_denied" => AppError::Permission("Teams policy does not allow a chat with this recipient".into()),
|
|
680
685
|
"search_failed" => AppError::Desktop("Teams could not resolve this person. Check sign-in and try their full email address".into()),
|
|
681
686
|
"layout" => AppError::Unsupported("This Teams version does not expose a supported recipient interface".into()),
|
|
@@ -784,6 +789,7 @@ fn send_error(code: &str) -> AppError {
|
|
|
784
789
|
match code {
|
|
785
790
|
"not_found" => AppError::NotFound("chat not found in the Teams desktop cache".into()),
|
|
786
791
|
"account_ambiguous" => account_selection_error(),
|
|
792
|
+
"cache_ambiguous" => cached_accounts_error(),
|
|
787
793
|
"unconfirmed" => AppError::Unconfirmed(
|
|
788
794
|
"Teams did not confirm the message; it may still be delivered. Check the chat before sending again".into(),
|
|
789
795
|
),
|
|
@@ -894,6 +900,9 @@ fn read_page(reply: &Value) -> Result<Page, AppError> {
|
|
|
894
900
|
if code == "account_ambiguous" {
|
|
895
901
|
return Err(account_selection_error());
|
|
896
902
|
}
|
|
903
|
+
if code == "cache_ambiguous" {
|
|
904
|
+
return Err(cached_accounts_error());
|
|
905
|
+
}
|
|
897
906
|
return Err(AppError::Desktop(match (status, code) {
|
|
898
907
|
(Some(status), _) => status.detail().into(),
|
|
899
908
|
(None, "no_target") => {
|
|
@@ -928,6 +937,9 @@ fn read_page(reply: &Value) -> Result<Page, AppError> {
|
|
|
928
937
|
if value["error"] == "account_ambiguous" {
|
|
929
938
|
return Err(account_selection_error());
|
|
930
939
|
}
|
|
940
|
+
if value["error"] == "cache_ambiguous" {
|
|
941
|
+
return Err(cached_accounts_error());
|
|
942
|
+
}
|
|
931
943
|
let items = value["items"].as_array().ok_or_else(invalid)?.clone();
|
|
932
944
|
let next_cursor = match &value["next"] {
|
|
933
945
|
Value::Null => None,
|
|
@@ -1219,7 +1231,7 @@ mod tests {
|
|
|
1219
1231
|
/// the new one; otherwise just update the expectations.
|
|
1220
1232
|
#[test]
|
|
1221
1233
|
fn the_protocol_is_pinned() {
|
|
1222
|
-
assert_eq!(helper::PROTOCOL,
|
|
1234
|
+
assert_eq!(helper::PROTOCOL, 5);
|
|
1223
1235
|
let cursor = encode_cursor(&json!({"t": 1, "id": "m1"})).unwrap();
|
|
1224
1236
|
assert_eq!(
|
|
1225
1237
|
read_request(9222, Some(NOTES), 20, Some(&cursor)).unwrap(),
|
|
@@ -1258,14 +1270,15 @@ mod tests {
|
|
|
1258
1270
|
.map(|b| format!("{b:02x}"))
|
|
1259
1271
|
.collect::<String>()
|
|
1260
1272
|
});
|
|
1261
|
-
// Protocol
|
|
1273
|
+
// Protocol 5 selects the signed-in cache and reports remaining ambiguity apart from surface selection.
|
|
1274
|
+
// Older same-account caches are optional; a helper without them reads as before.
|
|
1262
1275
|
assert_eq!(
|
|
1263
1276
|
scripts,
|
|
1264
1277
|
[
|
|
1265
|
-
"
|
|
1278
|
+
"9c000776f04a",
|
|
1266
1279
|
"baa0bc29c44b",
|
|
1267
|
-
"
|
|
1268
|
-
"
|
|
1280
|
+
"7d2b70751d6a",
|
|
1281
|
+
"23b1a0e4b293"
|
|
1269
1282
|
]
|
|
1270
1283
|
);
|
|
1271
1284
|
}
|
|
@@ -1300,7 +1313,11 @@ mod tests {
|
|
|
1300
1313
|
));
|
|
1301
1314
|
assert!(matches!(
|
|
1302
1315
|
people_result(&page_reply(json!({"error":"account_ambiguous"})), false),
|
|
1303
|
-
Err(AppError::Desktop(
|
|
1316
|
+
Err(AppError::Desktop(m)) if m.contains("--desktop-account")
|
|
1317
|
+
));
|
|
1318
|
+
assert!(matches!(
|
|
1319
|
+
people_result(&page_reply(json!({"error":"cache_ambiguous"})), false),
|
|
1320
|
+
Err(AppError::Desktop(m)) if !m.contains("--desktop-account")
|
|
1304
1321
|
));
|
|
1305
1322
|
for id in [
|
|
1306
1323
|
"someone@example.invalid",
|
|
@@ -1469,6 +1486,7 @@ mod tests {
|
|
|
1469
1486
|
Err(AppError::NotFound(_))
|
|
1470
1487
|
));
|
|
1471
1488
|
let layout = json!({"result":{"exceptionDetails":{"exception":{"description":"Error: layout\n at x"}}}});
|
|
1489
|
+
// Mixed personal and work/school windows are resolved by choosing a surface.
|
|
1472
1490
|
for ambiguous in [
|
|
1473
1491
|
json!({"error": "account_ambiguous"}),
|
|
1474
1492
|
reply(json!({"error": "account_ambiguous"})),
|
|
@@ -1480,6 +1498,20 @@ mod tests {
|
|
|
1480
1498
|
matches!(sent_message(&ambiguous, CHAT), Err(AppError::Desktop(m)) if m.contains("--desktop-account"))
|
|
1481
1499
|
);
|
|
1482
1500
|
}
|
|
1501
|
+
// Several caches within one surface are not, so no flag is suggested.
|
|
1502
|
+
for ambiguous in [
|
|
1503
|
+
json!({"error": "cache_ambiguous"}),
|
|
1504
|
+
reply(json!({"error": "cache_ambiguous"})),
|
|
1505
|
+
] {
|
|
1506
|
+
for result in [
|
|
1507
|
+
read_page(&ambiguous).map(|_| ()),
|
|
1508
|
+
sent_message(&ambiguous, CHAT).map(|_| ()),
|
|
1509
|
+
] {
|
|
1510
|
+
assert!(
|
|
1511
|
+
matches!(result, Err(AppError::Desktop(m)) if m.contains("cached") && !m.contains("--desktop-account"))
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1483
1515
|
assert!(matches!(read_page(&layout), Err(AppError::Desktop(m)) if m.contains("layout")));
|
|
1484
1516
|
for reply in [
|
|
1485
1517
|
json!({"error": "no_target"}),
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Select one account's cache databases. Several account caches can coexist in a
|
|
2
|
+
// storage context; their records must never be joined or sent through arbitrarily.
|
|
3
|
+
all => {
|
|
4
|
+
const candidates = kind => all.filter(db =>
|
|
5
|
+
typeof db.name === 'string' && db.name.includes(':' + kind + ':'));
|
|
6
|
+
// Teams names caches `<prefix>:<kind>:<client>:<tenant>:<user>:<locale>`. Other
|
|
7
|
+
// accounts, guest organizations and earlier display languages leave their own; the
|
|
8
|
+
// signed-in one is what Teams itself records, and anything less agreed is refused.
|
|
9
|
+
const signedIn = () => {
|
|
10
|
+
try {
|
|
11
|
+
const item = key => JSON.parse(localStorage.getItem(key) ?? 'null');
|
|
12
|
+
const user = item('tmp.auth.v1.GLOBAL.PrimaryUserId.PrimaryUserId')?.item;
|
|
13
|
+
const primary = item('tmp.react-web-client.cachedPrimaryUser');
|
|
14
|
+
if (typeof user !== 'string' || primary?.userId !== user || typeof primary.tenantId !== 'string') return null;
|
|
15
|
+
const locale = localStorage.getItem(`tmp.${primary.tenantId}.${user}.react-web-client.applicationLocaleCode`);
|
|
16
|
+
return { tenant: primary.tenantId, user, locale };
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
let conversations = candidates('conversation-manager');
|
|
22
|
+
if (conversations.length > 1) {
|
|
23
|
+
const active = signedIn();
|
|
24
|
+
const part = (db, index) => db.name.split(':')[index];
|
|
25
|
+
if (active) conversations = conversations.filter(db =>
|
|
26
|
+
part(db, 3) === active.tenant && part(db, 4) === active.user);
|
|
27
|
+
if (active && conversations.length > 1) conversations = conversations.filter(db => part(db, 5) === active.locale);
|
|
28
|
+
if (conversations.length !== 1) return { error: 'cache_ambiguous' };
|
|
29
|
+
}
|
|
30
|
+
if (conversations.length !== 1) return { error: 'layout' };
|
|
31
|
+
const conversation = conversations[0].name;
|
|
32
|
+
const [, , , tenant, account] = conversation.split(':');
|
|
33
|
+
if (!account) return { error: 'layout' };
|
|
34
|
+
const related = kind => {
|
|
35
|
+
const sibling = conversation.replace(':conversation-manager:', ':' + kind + ':');
|
|
36
|
+
if (all.some(db => db.name === sibling)) return sibling;
|
|
37
|
+
const matches = candidates(kind).filter(db => db.name.split(':')[4] === account);
|
|
38
|
+
return matches.length === 1 ? matches[0].name : null;
|
|
39
|
+
};
|
|
40
|
+
const replies = related('replychain-manager');
|
|
41
|
+
// Caches the same user and tenant left under earlier display languages. Teams stopped
|
|
42
|
+
// updating them, so they may only fill in messages the current cache lacks.
|
|
43
|
+
const older = candidates('replychain-manager').map(db => db.name).filter(name => {
|
|
44
|
+
const part = name.split(':');
|
|
45
|
+
return name !== replies && part[3] === tenant && part[4] === account;
|
|
46
|
+
}).sort();
|
|
47
|
+
return { account, conversation, replies, profiles: related('profiles'), older };
|
|
48
|
+
}
|
|
@@ -281,7 +281,7 @@ async (p, select) => {
|
|
|
281
281
|
} finally {
|
|
282
282
|
db?.close();
|
|
283
283
|
}
|
|
284
|
-
if (own.length !== 1) return { error: own.length ? "
|
|
284
|
+
if (own.length !== 1) return { error: own.length ? "cache_ambiguous" : "layout" };
|
|
285
285
|
if (own[0] === p.recipient) return {id:"48:notes",chatType:"oneOnOne",members:[{userId:own[0]}],source:"desktop"};
|
|
286
286
|
const userIds = [...new Set([own[0], p.recipient])];
|
|
287
287
|
const lookup = operationDoc(
|
|
@@ -532,6 +532,16 @@ impl App {
|
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
/// Why a chat shows only its latest message, said for where messages come from.
|
|
536
|
+
fn missing_history_note(&self) -> &'static str {
|
|
537
|
+
match self.backend().as_deref() {
|
|
538
|
+
Some(TeamsBackend::Desktop(_)) => {
|
|
539
|
+
"No messages for this chat were found in the Teams cache on this PC. Latest message:"
|
|
540
|
+
}
|
|
541
|
+
_ => "Only the latest message is available.",
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
535
545
|
fn can_send(&self) -> bool {
|
|
536
546
|
match &self.source {
|
|
537
547
|
Source::Live { can_send, .. } => *can_send,
|
|
@@ -2030,7 +2040,21 @@ impl App {
|
|
|
2030
2040
|
}
|
|
2031
2041
|
Load::Ready => {}
|
|
2032
2042
|
}
|
|
2033
|
-
let entries = render::conversation(&thread.items, &self.view(), self.viewer.as_deref());
|
|
2043
|
+
let mut entries = render::conversation(&thread.items, &self.view(), self.viewer.as_deref());
|
|
2044
|
+
// The chat list can carry a latest message whose history was never read.
|
|
2045
|
+
let mut missing = false;
|
|
2046
|
+
if entries.is_empty()
|
|
2047
|
+
&& thread.cursor.is_none()
|
|
2048
|
+
&& let Some(chat) = self.chats.iter().find(|chat| chat["id"] == id.as_str())
|
|
2049
|
+
&& chat["lastMessagePreview"].is_object()
|
|
2050
|
+
{
|
|
2051
|
+
entries = render::conversation(
|
|
2052
|
+
std::slice::from_ref(&chat["lastMessagePreview"]),
|
|
2053
|
+
&self.view(),
|
|
2054
|
+
self.viewer.as_deref(),
|
|
2055
|
+
);
|
|
2056
|
+
missing = !entries.is_empty();
|
|
2057
|
+
}
|
|
2034
2058
|
let mut lines = Vec::new();
|
|
2035
2059
|
if thread.loading_older {
|
|
2036
2060
|
lines.push(
|
|
@@ -2050,7 +2074,12 @@ impl App {
|
|
|
2050
2074
|
.alignment(Alignment::Center),
|
|
2051
2075
|
);
|
|
2052
2076
|
}
|
|
2053
|
-
if
|
|
2077
|
+
if missing {
|
|
2078
|
+
lines.push(Line::raw(""));
|
|
2079
|
+
for note in render::wrap(self.missing_history_note(), usize::from(body.width)) {
|
|
2080
|
+
lines.push(Line::styled(note, theme.muted()).centered());
|
|
2081
|
+
}
|
|
2082
|
+
} else if entries.is_empty() {
|
|
2054
2083
|
lines.push(Line::raw(""));
|
|
2055
2084
|
lines.push(Line::styled("No messages in this chat yet.", theme.muted()).centered());
|
|
2056
2085
|
}
|
|
@@ -3706,6 +3735,66 @@ mod tests {
|
|
|
3706
3735
|
App::new(source, Theme { color: false }, now)
|
|
3707
3736
|
}
|
|
3708
3737
|
|
|
3738
|
+
#[test]
|
|
3739
|
+
fn a_chat_without_history_shows_its_latest_message_and_why() {
|
|
3740
|
+
let empty = |app: &mut App| {
|
|
3741
|
+
let id = app.selected_row().unwrap().id.clone();
|
|
3742
|
+
app.threads.insert(
|
|
3743
|
+
id,
|
|
3744
|
+
Thread {
|
|
3745
|
+
items: Vec::new(),
|
|
3746
|
+
cursor: None,
|
|
3747
|
+
load: Load::Ready,
|
|
3748
|
+
loading_older: false,
|
|
3749
|
+
fetching: false,
|
|
3750
|
+
},
|
|
3751
|
+
);
|
|
3752
|
+
frame_text(app)
|
|
3753
|
+
};
|
|
3754
|
+
let mut app = demo_app();
|
|
3755
|
+
let text = empty(&mut app);
|
|
3756
|
+
assert!(
|
|
3757
|
+
text.contains("Only the latest message is available."),
|
|
3758
|
+
"{text}"
|
|
3759
|
+
);
|
|
3760
|
+
assert!(text.contains("release candidate is green"), "{text}");
|
|
3761
|
+
assert!(!text.contains("No messages in this chat yet."), "{text}");
|
|
3762
|
+
|
|
3763
|
+
let now = app.now.clone();
|
|
3764
|
+
let mut app = live_app(false, "work");
|
|
3765
|
+
app.source = Source::Live {
|
|
3766
|
+
backend: Arc::new(TeamsBackend::Desktop(Box::new(
|
|
3767
|
+
crate::desktop::DesktopClient::new(19224).unwrap(),
|
|
3768
|
+
))),
|
|
3769
|
+
profile: "work".into(),
|
|
3770
|
+
can_send: false,
|
|
3771
|
+
};
|
|
3772
|
+
app.apply(Outcome::Chats {
|
|
3773
|
+
more: false,
|
|
3774
|
+
result: Ok(demo_chats(&now)),
|
|
3775
|
+
});
|
|
3776
|
+
let text = empty(&mut app);
|
|
3777
|
+
assert!(text.contains("were found in the Teams cache"), "{text}");
|
|
3778
|
+
// The note wraps with the pane rather than being cut off.
|
|
3779
|
+
let mut terminal = Terminal::new(TestBackend::new(60, 24)).unwrap();
|
|
3780
|
+
app.focus = Focus::Thread;
|
|
3781
|
+
terminal.draw(|frame| app.draw(frame)).unwrap();
|
|
3782
|
+
let narrow = buffer_text(terminal.backend().buffer());
|
|
3783
|
+
assert!(narrow.contains("Latest message:"), "{narrow}");
|
|
3784
|
+
assert!(text.contains("release candidate is green"), "{text}");
|
|
3785
|
+
|
|
3786
|
+
// Older pages may still hold messages, so nothing is claimed missing.
|
|
3787
|
+
let id = app.selected_row().unwrap().id.clone();
|
|
3788
|
+
app.threads.get_mut(&id).unwrap().cursor = Some("older".into());
|
|
3789
|
+
let text = frame_text(&mut app);
|
|
3790
|
+
assert!(text.contains("No messages in this chat yet."), "{text}");
|
|
3791
|
+
|
|
3792
|
+
// Without a preview there is nothing to show.
|
|
3793
|
+
app.chats[0]["lastMessagePreview"] = Value::Null;
|
|
3794
|
+
let text = empty(&mut app);
|
|
3795
|
+
assert!(text.contains("No messages in this chat yet."), "{text}");
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3709
3798
|
#[test]
|
|
3710
3799
|
fn the_header_shows_the_profile_send_mode_freshness_and_counts() {
|
|
3711
3800
|
let mut app = demo_app();
|
|
@@ -29,7 +29,7 @@ function cursor(records) {
|
|
|
29
29
|
return q;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function fixture({ ambiguous = false, foreignOnly = false, mutate, timeout = false, core = false } = {}) {
|
|
32
|
+
function fixture({ ambiguous = false, foreignOnly = false, older = [], stores = {}, mutate, timeout = false, core = false } = {}) {
|
|
33
33
|
const databases = new Map([
|
|
34
34
|
[name('conversation-manager', 'account-a'), [conversation]],
|
|
35
35
|
[name('replychain-manager', 'account-b'), [{ chat, messageMap: { b: { ...message, content: 'Wrong account' } } }]],
|
|
@@ -40,6 +40,9 @@ function fixture({ ambiguous = false, foreignOnly = false, mutate, timeout = fal
|
|
|
40
40
|
databases.set(name('profiles', 'account-a'), [{ mri: '8:orgid:sender', displayName: 'Correct Name' }]);
|
|
41
41
|
}
|
|
42
42
|
if (ambiguous) databases.set(name('conversation-manager', 'account-b'), [conversation]);
|
|
43
|
+
for (const [locale, records] of older) databases.set(name('replychain-manager', 'account-a') + ':' + locale, records);
|
|
44
|
+
// Extra object stores of the current account's reply-chain database, beside its default store.
|
|
45
|
+
const extra = new Map([[name('replychain-manager', 'account-a'), stores]]);
|
|
43
46
|
const opened = [], mutations = [];
|
|
44
47
|
let inspections = 0;
|
|
45
48
|
const client = { mutate: options => {
|
|
@@ -55,11 +58,15 @@ function fixture({ ambiguous = false, foreignOnly = false, mutate, timeout = fal
|
|
|
55
58
|
open: database => {
|
|
56
59
|
assert.ok(databases.has(database), `Unknown database: ${database}`);
|
|
57
60
|
opened.push(database);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const named = extra.get(database) || {};
|
|
62
|
+
const records = store => store in named ? named[store] : databases.get(database);
|
|
63
|
+
return request({ name: database, close() {},
|
|
64
|
+
objectStoreNames: database.includes(':replychain-manager:') ? ['replychains', ...Object.keys(named)] : [],
|
|
65
|
+
transaction: () => ({ objectStore: store => ({
|
|
66
|
+
get: id => request(records(store).find(record => (record.mri || record.id) === id)),
|
|
67
|
+
getAll: range => request(records(store).filter(record => !range || record.chat === range.chat)),
|
|
68
|
+
openCursor: () => cursor(records(store)),
|
|
69
|
+
}) }) });
|
|
63
70
|
},
|
|
64
71
|
},
|
|
65
72
|
IDBKeyRange: { bound: a => ({ chat: a[0] }) },
|
|
@@ -225,16 +232,109 @@ for (const query of ['chats', 'messages']) {
|
|
|
225
232
|
});
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
test('older caches of the same account only add messages the current cache lacks', async () => {
|
|
236
|
+
const earlier = { ...message, id: '1690000000000', originalArrivalTime: 1690000000000, content: 'Earlier' };
|
|
237
|
+
const f = fixture({ older: [
|
|
238
|
+
['nl-nl', [{ chat, messageMap: { stale: { ...message, content: 'Stale' }, earlier } }]],
|
|
239
|
+
['de-de', [{ chat: '19:other@thread.v2', messageMap: { other: { ...message, id: '1680000000000', content: 'Other chat' } } }]],
|
|
240
|
+
] });
|
|
241
|
+
const result = await f.read();
|
|
242
|
+
assert.deepEqual([...result.items.map(item => item.body.content)], [message.content, 'Earlier']);
|
|
243
|
+
assert.ok(f.opened.every(database => !database.includes(':account-b')));
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('messages filed only in a later reply-chain store are read', async () => {
|
|
247
|
+
const f = fixture({ stores: {
|
|
248
|
+
'replychains-2': [{ chat, messageMap: { a: message } }],
|
|
249
|
+
'unrelated': [{ chat, messageMap: { x: { ...message, id: '1710000000000', content: 'Unrelated store' } } }],
|
|
250
|
+
} });
|
|
251
|
+
f.databases.set(name('replychain-manager', 'account-a'), []);
|
|
252
|
+
const result = await f.read();
|
|
253
|
+
assert.deepEqual([...result.items.map(item => item.body.content)], [message.content]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('messages split across reply-chain stores are merged newest first without duplicates', async () => {
|
|
257
|
+
const earlier = { ...message, id: '1690000000000', originalArrivalTime: 1690000000000, content: 'Earlier' };
|
|
258
|
+
const later = { ...message, id: '1710000000000', originalArrivalTime: 1710000000000, content: 'Later' };
|
|
259
|
+
const f = fixture({ stores: { 'replychains-2': [{ chat, messageMap: { a: message, earlier, later } }] } });
|
|
260
|
+
const result = await f.read();
|
|
261
|
+
assert.deepEqual([...result.items.map(item => item.body.content)], ['Later', message.content, 'Earlier']);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test('reply-chain stores are read in numeric order, so the primary copy of a message wins', async () => {
|
|
265
|
+
const f = fixture({ stores: {
|
|
266
|
+
'replychains-10': [{ chat, messageMap: { a: { ...message, content: 'Tenth store' } } }],
|
|
267
|
+
'replychains-2': [{ chat, messageMap: { a: { ...message, content: 'Second store' } } }],
|
|
268
|
+
} });
|
|
269
|
+
assert.deepEqual([...(await f.read()).items.map(item => item.body.content)], [message.content]);
|
|
270
|
+
f.databases.set(name('replychain-manager', 'account-a'), []);
|
|
271
|
+
assert.deepEqual([...(await f.read()).items.map(item => item.body.content)], ['Second store']);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('1:1 member names come from messages in a later reply-chain store', async () => {
|
|
275
|
+
const one = '19:account-a_peer@unq.gbl.spaces';
|
|
276
|
+
const f = fixture({ stores: { 'replychains-2': [{ chat: one, messageMap: {
|
|
277
|
+
p: { ...message, creator: '8:orgid:peer', imDisplayName: 'Peer Name' },
|
|
278
|
+
} }] } });
|
|
279
|
+
f.databases.set(name('conversation-manager', 'account-a'), [{ ...conversation, id: one, members: [] }]);
|
|
280
|
+
f.databases.set(name('replychain-manager', 'account-a'), []);
|
|
281
|
+
const result = await f.read({ query: 'chats', chat: '' });
|
|
282
|
+
assert.equal(result.me, 'account-a');
|
|
283
|
+
assert.equal(result.items[0].members.find(m => m.userId === 'peer').displayName, 'Peer Name');
|
|
284
|
+
});
|
|
285
|
+
|
|
228
286
|
test('a missing account database never falls back to another account', async () => {
|
|
229
287
|
const f = fixture({ foreignOnly: true });
|
|
230
288
|
await assert.rejects(f.read(), /layout/);
|
|
231
289
|
assert.ok(f.opened.every(database => database.endsWith(':account-a')));
|
|
232
290
|
});
|
|
233
291
|
|
|
292
|
+
function selectWith(names, storage = {}) {
|
|
293
|
+
const context = vm.createContext({ localStorage: { getItem: key => key in storage ? storage[key] : null } });
|
|
294
|
+
// Compare plain values: objects from another context have their own prototypes.
|
|
295
|
+
return JSON.parse(JSON.stringify(vm.runInContext(source('desktop_cache.js'), context)(names.map(name => ({ name })))));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const teams = (kind, tenant, user, locale = 'en-us') => `Teams:${kind}:react-web-client:${tenant}:${user}:${locale}`;
|
|
299
|
+
const signedIn = (tenant, user, locale) => ({
|
|
300
|
+
'tmp.auth.v1.GLOBAL.PrimaryUserId.PrimaryUserId': JSON.stringify({ item: user }),
|
|
301
|
+
'tmp.react-web-client.cachedPrimaryUser': JSON.stringify({ tenantId: tenant, userId: user }),
|
|
302
|
+
...(locale && { [`tmp.${tenant}.${user}.react-web-client.applicationLocaleCode`]: locale }),
|
|
303
|
+
});
|
|
304
|
+
const caches = (tenant, user, locale) => ['conversation-manager', 'replychain-manager', 'profiles']
|
|
305
|
+
.map(kind => teams(kind, tenant, user, locale));
|
|
306
|
+
|
|
307
|
+
test('the signed-in user, tenant and display language select among coexisting caches', () => {
|
|
308
|
+
const names = [
|
|
309
|
+
...caches('home', 'me', 'en-us'), ...caches('home', 'me', 'nl-nl'),
|
|
310
|
+
...caches('guest', 'me-as-guest'), ...caches('home', 'colleague'),
|
|
311
|
+
];
|
|
312
|
+
for (const [tenant, user, locale, older] of [['home', 'me', 'nl-nl', ['en-us']], ['home', 'me', 'en-us', ['nl-nl']], ['guest', 'me-as-guest', 'en-us', []]]) {
|
|
313
|
+
assert.deepEqual(selectWith(names, signedIn(tenant, user, locale)), {
|
|
314
|
+
account: user, conversation: teams('conversation-manager', tenant, user, locale),
|
|
315
|
+
replies: teams('replychain-manager', tenant, user, locale), profiles: teams('profiles', tenant, user, locale),
|
|
316
|
+
// Only the same user in the same tenant may fill in messages.
|
|
317
|
+
older: older.map(other => teams('replychain-manager', tenant, user, other)),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
// Only one cache for the signed-in account needs no display language.
|
|
321
|
+
assert.equal(selectWith(names, signedIn('home', 'colleague')).conversation, teams('conversation-manager', 'home', 'colleague'));
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('caches are refused when Teams does not identify exactly one of them', () => {
|
|
325
|
+
const names = [...caches('home', 'me', 'en-us'), ...caches('home', 'me', 'nl-nl'), ...caches('home', 'colleague')];
|
|
326
|
+
const inconsistent = { ...signedIn('home', 'me', 'en-us'),
|
|
327
|
+
'tmp.react-web-client.cachedPrimaryUser': JSON.stringify({ tenantId: 'home', userId: 'colleague' }) };
|
|
328
|
+
for (const storage of [{}, signedIn('home', 'me'), signedIn('home', 'me', 'de-de'), signedIn('home', 'stranger', 'en-us'), inconsistent,
|
|
329
|
+
{ 'tmp.auth.v1.GLOBAL.PrimaryUserId.PrimaryUserId': 'not json' }]) {
|
|
330
|
+
assert.deepEqual(selectWith(names, storage), { error: 'cache_ambiguous' });
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
234
334
|
test('ambiguous account caches are refused before reading or sending', async () => {
|
|
235
335
|
const f = fixture({ ambiguous: true });
|
|
236
|
-
assert.equal((await f.read()).error, '
|
|
237
|
-
assert.equal((await f.send()).error, '
|
|
336
|
+
assert.equal((await f.read()).error, 'cache_ambiguous');
|
|
337
|
+
assert.equal((await f.send()).error, 'cache_ambiguous');
|
|
238
338
|
assert.deepEqual(f.opened, []);
|
|
239
339
|
assert.deepEqual(f.mutations, []);
|
|
240
340
|
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
// Select one account's cache databases. Several account caches can coexist in a
|
|
2
|
-
// storage context; their records must never be joined or sent through arbitrarily.
|
|
3
|
-
all => {
|
|
4
|
-
const candidates = kind => all.filter(db =>
|
|
5
|
-
typeof db.name === 'string' && db.name.includes(':' + kind + ':'));
|
|
6
|
-
const conversations = candidates('conversation-manager');
|
|
7
|
-
if (conversations.length > 1) return { error: 'account_ambiguous' };
|
|
8
|
-
if (conversations.length !== 1) return { error: 'layout' };
|
|
9
|
-
const conversation = conversations[0].name;
|
|
10
|
-
const account = conversation.split(':')[4];
|
|
11
|
-
if (!account) return { error: 'layout' };
|
|
12
|
-
const related = kind => {
|
|
13
|
-
const matches = candidates(kind).filter(db => db.name.split(':')[4] === account);
|
|
14
|
-
return matches.length === 1 ? matches[0].name : null;
|
|
15
|
-
};
|
|
16
|
-
return { account, conversation, replies: related('replychain-manager'), profiles: related('profiles') };
|
|
17
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|