Skip to main content

tuwunel_service/migrations/
migrate_profile_keys.rs

1use ruma::{UserId, profile::ProfileFieldName};
2use serde::de::IgnoredAny;
3use tuwunel_core::{
4	Result, info,
5	utils::{ReadyExt, stream::TryExpect},
6	warn,
7};
8use tuwunel_database::Json;
9
10use crate::Services;
11
12/// Relocates the per-user displayname and avatar_url out of their dedicated
13/// columns into the unified useridprofilekey_value store keyed by MSC4133 field
14/// name, where the profile service now reads them.
15///
16/// The dedicated columns are left intact, so an older binary opening the same
17/// database still resolves.
18pub(super) async fn migrate_profile_keys(services: &Services) -> Result {
19	let db = &services.db;
20	let cork = db.cork_and_sync();
21
22	let userid_displayname = db["userid_displayname"].clone();
23	let userid_avatarurl = db["userid_avatarurl"].clone();
24	let userid_blurhash = db["userid_blurhash"].clone();
25	let useridprofilekey_value = db["useridprofilekey_value"].clone();
26
27	warn!(
28		"Relocating displaynames, avatar_urls and blurhashes into the unified profile-key store"
29	);
30
31	let displaynames = userid_displayname
32		.stream()
33		.expect_ok()
34		.ready_fold(0_usize, |count, (user_id, displayname): (&UserId, &str)| {
35			let key = (user_id, ProfileFieldName::DisplayName.as_str());
36			let value = displayname.to_owned();
37
38			useridprofilekey_value.put(key, Json(value));
39
40			count.saturating_add(1)
41		})
42		.await;
43
44	let avatar_urls = userid_avatarurl
45		.stream()
46		.expect_ok()
47		.ready_fold(0_usize, |count, (user_id, avatar_url): (&UserId, &str)| {
48			let key = (user_id, ProfileFieldName::AvatarUrl.as_str());
49			let value = avatar_url.to_owned();
50
51			useridprofilekey_value.put(key, Json(value));
52
53			count.saturating_add(1)
54		})
55		.await;
56
57	let blurhashes = userid_blurhash
58		.stream()
59		.expect_ok()
60		.ready_fold(0_usize, |count, (user_id, blurhash): (&UserId, &str)| {
61			let key = (user_id, "xyz.amorgan.blurhash");
62			let value = blurhash.to_owned();
63
64			useridprofilekey_value.put(key, Json(value));
65
66			count.saturating_add(1)
67		})
68		.await;
69
70	let fixed_strings = useridprofilekey_value
71		.raw_stream()
72		.expect_ok()
73		.ready_fold(0_usize, |count, (key, value)| {
74			if serde_json::from_slice::<IgnoredAny>(value).is_err() {
75				let Ok(string) = str::from_utf8(value) else {
76					warn!("Non-UTF8 data in profile value: {key:?} => {value:?}");
77					useridprofilekey_value.remove(key);
78					return count;
79				};
80				useridprofilekey_value.raw_put(key, Json(string));
81				return count.saturating_add(1);
82			}
83
84			count
85		})
86		.await;
87
88	drop(cork);
89	info!(%displaynames, %avatar_urls, %blurhashes, %fixed_strings, "Relocated profile keys into useridprofilekey_value");
90
91	db["global"].insert(b"migrate_profile_keys_to_useridprofilekey", []);
92	useridprofilekey_value.sort()
93}