Files
continuwuity/src/service/migrations.rs
T

1053 lines
30 KiB
Rust

use std::{
cmp,
collections::{BTreeMap, HashMap},
future::ready,
pin::Pin,
sync::Arc,
time::Instant,
};
use conduwuit::{
Err, Error, Event, Pdu, PduCount, Result, debug, debug_info, debug_warn, err, error, info,
result::NotFound,
trace,
utils::{
IterStream, ReadyExt,
stream::{TryExpect, TryIgnore, automatic_width},
to_canonical_object,
},
warn,
};
use database::Json;
use futures::{FutureExt, StreamExt, TryStreamExt};
use itertools::Itertools;
use ruma::{
OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
api::OAuthClientScope,
events::{
AnyStrippedStateEvent, GlobalAccountDataEventType, StateEventType,
push_rules::PushRulesEvent,
room::member::{MembershipState, RoomMemberEventContent},
},
push::Ruleset,
serde::{Raw, from_raw_json_value},
};
use serde_json::{Value, value::to_raw_value};
use crate::{Services, media, rooms, rooms::short::ShortStateHash};
type MigrationFn =
for<'a> fn(&'a Services) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;
enum DatabaseMigrationStep {
Migration(&'static str, MigrationFn),
VersionBump(u64),
}
static MIGRATIONS: &[DatabaseMigrationStep] = const {
use DatabaseMigrationStep::*;
macro_rules! migration {
($name:expr => $function:path) => {
Migration($name, |services| {
Box::pin(async {
let db = &services.db;
if db["global"].get($name).await.is_not_found() {
info!("Starting migration {}", $name);
let cork = db.cork_and_sync();
$function(services)
.await
.map_err(|err| err!("Failed to run migration {}: {err}", $name))?;
drop(cork);
db["global"].insert($name, []);
db.db.sort()?;
}
Ok(())
})
})
};
($name:ident) => {
// cargo fmt really wants this to be left-aligned, I guess?
migration!(stringify!($name) => $name)
};
}
// When you're writing a migration that makes backwards-incompatible
// changes, you need to use `VersionBump` to keep old versions from
// accepting the new database. The `VersionBump` command should come
// _before_ your migration, which ensures that old versions will reject
// the new database even if the server crashes or restarts while your
// migration is running.
// If your migration is backwards-compatible, meaning that its changes can
// be understood by a version of continuwuity that has not run it, it
// doesn't need a version bump. Use your best discretion when deciding what
// "be understood" means. Also, make sure to add a comment that explains
// why the bump was necessary, for the sake of future developers.
// Database versions should increase monotonically (you'll get a const-eval
// panic if they don't) and linearly. I don't know why conduit skipped
// from 13 to 17, don't bother asking.
&[
// Begin pre-fork logic. We don't have insight into why this works
// the way it does, so there are no comments here.
VersionBump(12),
migration!(fix_push_rule_names),
VersionBump(13),
migration!(fix_server_default_push_rules),
VersionBump(17),
migration!("feat_sha256_media" => media::migrations::migrate_sha256_media),
migration!(fix_bad_double_separator_in_state_cache),
migration!(retroactively_fix_bad_data_from_roomuserid_joined),
migration!(fix_referencedevents_missing_sep),
migration!(fix_readreceiptid_readreceipt_duplicates),
// End pre-fork logic.
// v18: `fix_corrupt_msc4133_fields` will change the
// `us.cloke.msc4175.tz` field in a way that old versions don't
// understand.
VersionBump(18),
migration!(fix_corrupt_msc4133_fields),
// v19: `populate_userroomid_leftstate_table` will put data in the
// values of `userroomid_leftstate` that old versions will fail to
// deserialize because they expect the values to be empty.
VersionBump(19),
migration!(populate_userroomid_leftstate_table),
// This is backwards compatible because it fixes some data that clients
// care about but we don't.
migration!(fix_local_invite_state),
// v20: Old versions expect remote users to be tracked in
// `userid_password`. This migration moves them to a different keyspace.
VersionBump(20),
migration!(split_userid_password),
// This is backwards compatible because old versions will recreate the
// keyspace if it's missing.
migration!("drop_roomsynctoken_shortstatehash" => obliterate_roomsynctoken_shortstatehash_with_extreme_prejudice),
// v21: `unembed_unsigned_info` will remove `unsigned` data from PDUs
// that old versions expect to be embedded.
VersionBump(21),
// This migration can take several hours on old databases, so we run it
// in the background to keep the service manager from killing us for
// taking too long to start. Future maintainers: !!! BE VERY CAREFUL !!!
// if you need to write more migrations like this! This one is fine
// because it deletes data that we don't check anymore, but migrations
// which replace existing data that we do check should NOT run in
// the background. If they do, the server will finish starting up and
// might access data that the migration hasn't gotten to yet.
Migration("unembed_unsigned_info", |services| {
Box::pin(async {
if services.db["global"]
.get(b"unembed_unsigned_info")
.await
.is_not_found()
{
let db = services.db.clone();
let metadata = services.rooms.metadata.clone();
let timeline = services.rooms.timeline.clone();
services.server.runtime().spawn(async move {
unembed_unsigned_info(metadata, timeline).await;
db["global"].insert(b"unembed_unsigned_info", []);
});
}
Ok(())
})
}),
// This is backwards compatible because old versions will recreate the
// keyspace if it's missing.
migration!(drop_server_signingkeys),
// v22: Old versions will fail to deserialize the fixed OAuth scope
// names, preventing users from signing in.
VersionBump(22),
migration!(fix_oauth_scope_names),
// Removes any extremities in roomid_pduleaves that we do not know about (and
// consequently should not reference).
migration!(remove_unknown_extremities),
// Re-calculates extremities for any rooms that actually lack any extremities at all.
migration!(consistency_check_extremity_tracker),
]
};
pub(crate) const DATABASE_VERSION: u64 = const {
let mut latest_version = 0_u64;
// TODO: refactor once slice iter is const stable
let mut i = 0;
while i < MIGRATIONS.len() {
if let DatabaseMigrationStep::VersionBump(version) = &MIGRATIONS[i] {
if *version > latest_version {
latest_version = *version;
} else {
panic!("Database version must monotonically increase");
}
}
i += 1;
}
latest_version
};
// This should match the first `VersionBump` in `MIGRATIONS`.
const OLDEST_SUPPORTED_VERSION: u64 = 11;
pub(crate) async fn migrations(services: &Services) -> Result<()> {
let users_count = services.users.count().await;
// Matrix resource ownership is based on the server name; changing it
// requires recreating the database from scratch.
if users_count > 0 {
let server_user = &services.globals.server_user;
if !services.users.status(server_user).await.is_found() {
error!("The {server_user} server user does not exist, and the database is not new.");
return Err!(Database(
"Cannot reuse an existing database after changing the server name, please \
delete the old one first.",
));
}
}
if users_count > 0 {
migrate(services).await
} else {
fresh(services).await
}
}
async fn fresh(services: &Services) -> Result<()> {
info!("Creating new fresh database");
let db = &services.db;
services.globals.db.bump_database_version(DATABASE_VERSION);
for step in MIGRATIONS {
if let DatabaseMigrationStep::Migration(key, _) = step {
db["global"].insert(key, []);
}
}
// Create the admin room and server user on first run
info!("Creating admin room and server user");
crate::admin::create_admin_room(services)
.boxed()
.await
.inspect_err(|e| error!("Failed to create admin room during db init: {e}"))?;
info!("Created new database with version {DATABASE_VERSION}");
Ok(())
}
/// Apply any migrations
async fn migrate(services: &Services) -> Result<()> {
if services.globals.db.database_version().await < OLDEST_SUPPORTED_VERSION {
return Err!(Database(
"Database schema version {} is too old to migrate. Please use an old version of \
conduwuit to migrate to version 11 and then run Continuwuity again.",
services.globals.db.database_version().await
));
}
for step in MIGRATIONS {
match step {
| DatabaseMigrationStep::Migration(_, migration) => {
migration(services).await?;
},
| DatabaseMigrationStep::VersionBump(version) => {
if services.globals.db.database_version().await < *version {
services.globals.db.bump_database_version(*version);
info!("Bumped database version to {version}.");
}
},
}
}
if services.config.media_startup_check {
info!("Starting media startup integrity check.");
let now = Instant::now();
media::migrations::checkup_sha256_media(services)
.await
.map_err(|e| err!("Failed to verify media integrity: {e}"))?;
info!(
"Finished media startup integrity check in {} seconds.",
now.elapsed().as_secs_f32()
);
}
assert_eq!(
services.globals.db.database_version().await,
DATABASE_VERSION,
"Failed asserting local database version {} is equal to known latest continuwuity \
database version {}",
services.globals.db.database_version().await,
DATABASE_VERSION,
);
{
let patterns = services.globals.forbidden_usernames();
if !patterns.is_empty() {
services
.users
.stream_local_users()
.filter_map(async |user_id| {
if services.users.status(&user_id).await.is_found() {
Some(user_id)
} else {
None
}
})
.ready_for_each(|user_id| {
let matches = patterns.matches(user_id.localpart());
if matches.matched_any() {
warn!(
"User {} matches the following forbidden username patterns: {}",
user_id.to_string(),
matches
.into_iter()
.map(|x| &patterns.patterns()[x])
.join(", ")
);
}
})
.await;
}
}
{
let patterns = services.globals.forbidden_alias_names();
if !patterns.is_empty() {
for room_id in services.rooms.metadata.iter_ids().collect::<Vec<_>>().await {
services
.rooms
.alias
.local_aliases_for_room(&room_id)
.ready_for_each(|room_alias| {
let matches = patterns.matches(room_alias.alias());
if matches.matched_any() {
warn!(
"Room with alias {} ({}) matches the following forbidden room \
name patterns: {}",
room_alias,
&room_id,
matches
.into_iter()
.map(|x| &patterns.patterns()[x])
.join(", ")
);
}
})
.await;
}
}
}
info!("Loaded RocksDB database with schema version {DATABASE_VERSION}");
Ok(())
}
async fn fix_push_rule_names(services: &Services) -> Result<()> {
for username in &services
.users
.stream_local_users()
.ready_filter(|user| services.globals.user_is_local(user))
.collect::<Vec<OwnedUserId>>()
.await
{
let user = match UserId::parse_with_server_name(username.as_str(), &services.server.name)
{
| Ok(u) => u,
| Err(e) => {
warn!("Invalid username {username}: {e}");
continue;
},
};
let Some(mut account_data) = services
.account_data
.get_global::<PushRulesEvent>(&user, GlobalAccountDataEventType::PushRules)
.await
.ok()
else {
// Skip users with no push rules
continue;
};
let rules_list = &mut account_data.content.global;
//content rule
{
let content_rule_transformation =
[".m.rules.contains_user_name", ".m.rule.contains_user_name"];
let rule = rules_list.content.get(content_rule_transformation[0]);
if let Some(rule) = rule {
let mut rule = rule.clone();
content_rule_transformation[1].clone_into(&mut rule.rule_id);
rules_list
.content
.shift_remove(content_rule_transformation[0]);
rules_list.content.insert(rule);
}
}
//underride rules
{
let underride_rule_transformation = [
[".m.rules.call", ".m.rule.call"],
[".m.rules.room_one_to_one", ".m.rule.room_one_to_one"],
[".m.rules.encrypted_room_one_to_one", ".m.rule.encrypted_room_one_to_one"],
[".m.rules.message", ".m.rule.message"],
[".m.rules.encrypted", ".m.rule.encrypted"],
];
for transformation in underride_rule_transformation {
let rule = rules_list.underride.get(transformation[0]);
if let Some(rule) = rule {
let mut rule = rule.clone();
transformation[1].clone_into(&mut rule.rule_id);
rules_list.underride.shift_remove(transformation[0]);
rules_list.underride.insert(rule);
}
}
}
services
.account_data
.update(
None,
&user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
}
Ok(())
}
async fn fix_server_default_push_rules(services: &Services) -> Result<()> {
for username in &services
.users
.stream_local_users()
.ready_filter(|user| services.globals.user_is_local(user))
.collect::<Vec<OwnedUserId>>()
.await
{
let user = match UserId::parse_with_server_name(username.as_str(), &services.server.name)
{
| Ok(u) => u,
| Err(e) => {
warn!("Invalid username {username}: {e}");
continue;
},
};
let Some(mut account_data) = services
.account_data
.get_global::<PushRulesEvent>(&user, GlobalAccountDataEventType::PushRules)
.await
.ok()
else {
// Skip users with no push rules
continue;
};
let user_default_rules = Ruleset::server_default(&user);
account_data
.content
.global
.update_with_server_default(user_default_rules);
services
.account_data
.update(
None,
&user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)
.await?;
}
Ok(())
}
async fn fix_bad_double_separator_in_state_cache(services: &Services) -> Result<()> {
info!("Fixing bad double separator in state_cache roomuserid_joined");
let db = &services.db;
let roomuserid_joined = &db["roomuserid_joined"];
let mut iter_count: usize = 0;
roomuserid_joined
.raw_stream()
.ignore_err()
.ready_for_each(|(key, value)| {
let mut key = key.to_vec();
iter_count = iter_count.saturating_add(1);
debug_info!(%iter_count);
let first_sep_index = key
.iter()
.position(|&i| i == 0xFF)
.expect("found 0xFF delim");
if key
.iter()
.get(first_sep_index..=first_sep_index.saturating_add(1))
.copied()
.collect_vec()
== vec![0xFF, 0xFF]
{
debug_warn!("Found bad key: {key:?}");
roomuserid_joined.remove(&key);
key.remove(first_sep_index);
debug_warn!("Fixed key: {key:?}");
roomuserid_joined.insert(&key, value);
}
})
.await;
info!("Finished fixing");
Ok(())
}
async fn retroactively_fix_bad_data_from_roomuserid_joined(services: &Services) -> Result<()> {
info!("Retroactively fixing bad data from broken roomuserid_joined");
let room_ids = services.rooms.metadata.iter_ids().collect::<Vec<_>>().await;
for room_id in &room_ids {
debug_info!("Fixing room {room_id}");
let users_in_room: Vec<OwnedUserId> = services
.rooms
.state_cache
.room_members(room_id)
.collect()
.await;
let joined_members = users_in_room
.iter()
.stream()
.filter(|user_id| {
services
.rooms
.state_accessor
.get_member(room_id, user_id)
.map(|member| {
member.is_ok_and(|member| member.membership == MembershipState::Join)
})
})
.collect::<Vec<_>>()
.await;
let non_joined_members = users_in_room
.iter()
.stream()
.filter(|user_id| {
services
.rooms
.state_accessor
.get_member(room_id, user_id)
.map(|member| {
member.is_ok_and(|member| member.membership == MembershipState::Join)
})
})
.collect::<Vec<_>>()
.await;
for user_id in &joined_members {
debug_info!("User is joined, marking as joined");
services.rooms.state_cache.mark_as_joined(user_id, room_id);
}
for user_id in &non_joined_members {
debug_info!("User is left or banned, marking as left");
services
.rooms
.state_cache
.mark_as_left(user_id, room_id, None)
.await;
}
}
for room_id in &room_ids {
debug_info!(
"Updating joined count for room {room_id} to fix servers in room after correcting \
membership states"
);
services
.rooms
.state_cache
.update_joined_count(room_id)
.await;
}
info!("Finished fixing");
Ok(())
}
async fn fix_referencedevents_missing_sep(services: &Services) -> Result {
info!("Fixing missing record separator between room_id and event_id in referencedevents");
let referencedevents = services.db["referencedevents"].clone();
let totals: (usize, usize) = (0, 0);
let (total, fixed) = referencedevents
.raw_stream()
.expect_ok()
.enumerate()
.ready_fold(totals, |mut a, (i, (key, val))| {
debug_assert!(val.is_empty(), "expected no value");
let has_sep = key.contains(&database::SEP);
if !has_sep {
let key_str = std::str::from_utf8(key).expect("key not utf-8");
let room_id_len = key_str.find('$').expect("missing '$' in key");
let (room_id, event_id) = key_str.split_at(room_id_len);
debug!(?a, "fixing {room_id}, {event_id}");
let new_key = (room_id, event_id);
referencedevents.put_raw(new_key, val);
referencedevents.remove(key);
}
a.0 = cmp::max(i, a.0);
a.1 = a.1.saturating_add((!has_sep).into());
a
})
.await;
info!(?total, ?fixed, "Fixed missing record separators in 'referencedevents'.");
Ok(())
}
async fn fix_readreceiptid_readreceipt_duplicates(services: &Services) -> Result {
type ArrayId = String;
type Key = (OwnedRoomId, u64, OwnedUserId);
info!("Fixing undeleted entries in readreceiptid_readreceipt...");
let db = &services.db;
let readreceiptid_readreceipt = db["readreceiptid_readreceipt"].clone();
let mut cur_room: Option<ArrayId> = None;
let mut cur_user: Option<ArrayId> = None;
let (mut total, mut fixed): (usize, usize) = (0, 0);
readreceiptid_readreceipt
.keys()
.expect_ok()
.ready_for_each(|key: Key| {
let (ref room_id, _, ref user_id) = key;
let last_room = cur_room.replace(room_id.as_str().into());
let last_user = cur_user.replace(user_id.as_str().into());
let is_dup = cur_room == last_room && cur_user == last_user;
if is_dup {
readreceiptid_readreceipt.del(key);
}
fixed = fixed.saturating_add(is_dup.into());
total = total.saturating_add(1);
})
.await;
info!(?total, ?fixed, "Fixed undeleted entries in readreceiptid_readreceipt.");
Ok(())
}
async fn fix_corrupt_msc4133_fields(services: &Services) -> Result {
// Due to an old bug, some conduwuit databases have `us.cloke.msc4175.tz`
// user profile fields with raw strings instead of quoted JSON ones.
// This migration fixes that.
use serde_json::{Value, from_slice};
type KeyVal<'a> = ((OwnedUserId, String), &'a [u8]);
info!("Fixing corrupted `us.cloke.msc4175.tz` fields...");
let db = &services.db;
let useridprofilekey_value = db["useridprofilekey_value"].clone();
let (total, fixed) = useridprofilekey_value
.stream()
.try_fold(
(0_usize, 0_usize),
async |(mut total, mut fixed),
((user, key), value): KeyVal<'_>|
-> Result<(usize, usize)> {
match from_slice::<Value>(value) {
// corrupted timezone field
| Err(_) if key == "us.cloke.msc4175.tz" => {
let new_value = Value::String(String::from_utf8(value.to_vec())?);
useridprofilekey_value.put((user, key), Json(new_value));
fixed = fixed.saturating_add(1);
},
// corrupted value for some other key
| Err(error) => {
warn!(
"deleting MSC4133 key {} for user {} due to deserialization \
failure: {}",
key, user, error
);
useridprofilekey_value.del((user, key));
},
// other key with no issues
| Ok(_) => {
// do nothing
},
}
total = total.saturating_add(1);
Ok((total, fixed))
},
)
.await?;
info!(?total, ?fixed, "Fixed corrupted `us.cloke.msc4175.tz` fields.");
Ok(())
}
async fn populate_userroomid_leftstate_table(services: &Services) -> Result {
type KeyVal = (Key, Raw<Option<Pdu>>);
type Key = (OwnedUserId, OwnedRoomId);
let db = &services.db;
let userroomid_leftstate = db["userroomid_leftstate"].clone();
let (total, fixed, _) = userroomid_leftstate
.stream()
.try_fold(
(0_usize, 0_usize, HashMap::<OwnedRoomId, ShortStateHash>::new()),
async |(mut total, mut fixed, mut shortstatehash_cache): (
usize,
usize,
HashMap<_, _>,
),
((user_id, room_id), state): KeyVal|
-> Result<(usize, usize, HashMap<_, _>)> {
if state.deserialize().is_err() {
let latest_shortstatehash =
if let Some(shortstatehash) = shortstatehash_cache.get(&room_id) {
*shortstatehash
} else if let Ok(shortstatehash) =
services.rooms.state.get_room_shortstatehash(&room_id).await
{
shortstatehash_cache.insert(room_id.clone(), shortstatehash);
shortstatehash
} else {
warn!(%room_id, %user_id, "room has no shortstatehash");
return Ok((total, fixed, shortstatehash_cache));
};
let leave_state_event = services
.rooms
.state_accessor
.state_get(
latest_shortstatehash,
&StateEventType::RoomMember,
user_id.as_str(),
)
.await;
match leave_state_event {
| Ok(leave_state_event) => {
userroomid_leftstate.put((user_id, room_id), Json(leave_state_event));
fixed = fixed.saturating_add(1);
},
| Err(_) => {
warn!(
%room_id,
%user_id,
"room cached as left has no leave event for user, removing \
cache entry"
);
userroomid_leftstate.del((user_id, room_id));
},
}
}
total = total.saturating_add(1);
Ok((total, fixed, shortstatehash_cache))
},
)
.await?;
info!(?total, ?fixed, "Fixed entries in `userroomid_leftstate`.");
Ok(())
}
async fn fix_local_invite_state(services: &Services) -> Result {
// Clean up the effects of !1249 by caching stripped state for invites
type KeyVal = (Key, Raw<Vec<AnyStrippedStateEvent>>);
type Key = (OwnedUserId, OwnedRoomId);
let db = &services.db;
let userroomid_invitestate = db["userroomid_invitestate"].clone();
// for each user invited to a room
let fixed = userroomid_invitestate.stream()
// if they're a local user on this homeserver
.try_filter(|((user_id, _), _): &KeyVal| ready(services.globals.user_is_local(user_id)))
.and_then(async |((user_id, room_id), stripped_state): KeyVal| Ok::<_,
Error>((user_id.clone(), room_id.clone(), stripped_state.deserialize
().unwrap_or_else(|e| {
trace!("Failed to deserialize: {:?}", stripped_state.json());
warn!(
%user_id,
%room_id,
"Failed to deserialize stripped state for invite, removing from db: {e}"
);
userroomid_invitestate.del((&user_id, &room_id));
vec![]
}))))
.try_fold(0_usize, async |mut fixed, (user_id, room_id, stripped_state)| {
// and their invite state is None
if stripped_state.is_empty()
// and they are actually invited to the room
&& let Ok(membership_event) = services.rooms.state_accessor.room_state_get(&room_id, &StateEventType::RoomMember, user_id.as_str()).await
&& membership_event.get_content::<RoomMemberEventContent>().is_ok_and(|content| content.membership == MembershipState::Invite)
// and the invite was sent by a local user
&& services.globals.user_is_local(&membership_event.sender) {
// build and save stripped state for their invite in the database
let stripped_state = services.rooms.state.summary_stripped(&membership_event, &room_id, &user_id, false).await;
userroomid_invitestate.put((&user_id, &room_id), Json(stripped_state));
fixed = fixed.saturating_add(1);
}
Ok(fixed)
})
.await?;
info!(?fixed, "Fixed local invite state cache entries.");
Ok(())
}
async fn split_userid_password(services: &Services) -> Result {
// Split remote and deactivated users out from the `userid_password` table
let db = &services.db;
let userid_password = db["userid_password"].clone();
let remoteuserid_remoteuser = db["remoteuserid_remoteuser"].clone();
let userid_deactivated = db["userid_deactivated"].clone();
let remote_users = userid_password
.stream::<OwnedUserId, String>()
.ignore_err()
.fold(0_usize, async |mut remote_users, (user_id, hash)| {
if !services.globals.user_is_local(&user_id) {
assert!(hash.is_empty(), "non-empty hash {hash} for remote user {user_id}");
remoteuserid_remoteuser.insert(&user_id, "");
userid_password.remove(&user_id);
remote_users = remote_users.saturating_add(1);
} else if hash.is_empty() {
if !(services.appservice.is_user_id(&user_id)
|| user_id == services.globals.server_user)
{
info!("Marking {user_id} as deactivated");
userid_deactivated.insert(&user_id, "");
}
}
remote_users
})
.await;
info!(?remote_users, "Split userid_password.");
Ok(())
}
async fn obliterate_roomsynctoken_shortstatehash_with_extreme_prejudice(
services: &Services,
) -> Result {
services.db.db.drop_column("roomsynctoken_shortstatehash")?;
info!("Cleared roomsynctoken_shortstatehash.");
Ok(())
}
#[tracing::instrument(name = "unembed_unsigned_info", skip_all)]
async fn unembed_unsigned_info(
metadata: Arc<rooms::metadata::Service>,
timeline: Arc<rooms::timeline::Service>,
) {
type Map = BTreeMap<String, Box<serde_json::value::RawValue>>;
info!(
"Starting background migration. This may take a long time, and may cause your server to \
lag. Please avoid restarting until the migration is complete."
);
let start = Instant::now();
let mut total_migrated = 0_usize;
let mut all_rooms = metadata.iter_ids();
while let Some(room_id) = all_rooms.next().await {
info!("Migrating PDUs in {room_id}");
let mut pdus = std::pin::pin!(timeline.all_pdus(&room_id));
let mut migrated = 0_usize;
while let Some((_, pdu)) = pdus.next().await {
let mut pdu = pdu.clone();
let mut unsigned: Map = pdu
.unsigned()
.and_then(|u| from_raw_json_value::<Map, Error>(u).ok())
.unwrap_or_default();
unsigned.remove("age");
unsigned.remove("membership");
unsigned.remove("prev_content");
unsigned.remove("prev_sender");
if let Some(redacted_because) = unsigned
.remove("redacted_because")
.and_then(|r| from_raw_json_value::<Map, Error>(&r).ok())
{
match redacted_because
.get("event_id")
.and_then(|e| from_raw_json_value::<OwnedEventId, Error>(e).ok())
{
| None => {},
| Some(event_id) => {
unsigned.insert(
"org.continuwuity.redacted_by".to_owned(),
to_raw_value(event_id.as_str()).expect("str must be raw value"),
);
},
}
}
pdu.unsigned = Some(to_raw_value(&unsigned).expect("unsigned must be valid JSON"));
let pdu_id = timeline
.get_pdu_id(pdu.event_id())
.await
.expect("PDU must have an id");
timeline
.replace_pdu(&pdu_id, &to_canonical_object(pdu).expect("PDU must be valid JSON"))
.await
.expect("must be able to replace PDU after migrating unsigned data");
migrated = migrated.saturating_add(1);
}
total_migrated = total_migrated.saturating_add(migrated);
info!(elapsed=?start.elapsed(), "Migrated {migrated} PDUs in {room_id}");
}
info!(elapsed=?start.elapsed(), total_migrated_pdus=total_migrated, "Finished migration.");
}
async fn drop_server_signingkeys(services: &Services) -> Result<()> {
services.db.db.drop_column("server_signingkeys")?;
info!("Cleared server_signingkeys.");
Ok(())
}
async fn fix_oauth_scope_names(services: &Services) -> Result {
let db = &services.db;
let userdeviceid_oauthsessioninfo = db["userdeviceid_oauthsessioninfo"].clone();
userdeviceid_oauthsessioninfo
.stream::<(OwnedUserId, OwnedDeviceId), Value>()
.ignore_err()
.for_each(async |((user_id, device_id), mut session_info)| {
let map = session_info.as_object_mut().unwrap();
let scopes = map.get_mut("scopes").unwrap().as_array_mut().unwrap();
if let Some(old_scope_index) = scopes
.iter()
.position(|value| *value == Value::String("ClientApi".to_owned()))
{
scopes[old_scope_index] =
Value::String(OAuthClientScope::ApiFullAccess.to_string());
}
userdeviceid_oauthsessioninfo.put((user_id, device_id), Json(session_info));
})
.await;
info!("Fixed OAuth session scope names");
Ok(())
}
async fn remove_unknown_extremities(services: &Services) -> Result {
services.db["roomid_pduleaves"]
.keys()
.ignore_err()
.for_each_concurrent(
automatic_width(),
|(room_id, event_id): (OwnedRoomId, OwnedEventId)| async move {
if !services.rooms.timeline.pdu_exists(&event_id).await {
debug!("Removing unknown extremity in {room_id}: {event_id}");
services
.rooms
.state
.remove_forward_extremity(&room_id, &event_id);
}
},
)
.await;
Ok(())
}
async fn consistency_check_extremity_tracker(services: &Services) -> Result {
let mut rooms = services.rooms.metadata.iter_ids();
// For every room the server is a member of,
while let Some(room_id) = rooms.next().await {
// If this room has no extremities, re-calculate
if services
.rooms
.state
.get_forward_extremities(&room_id)
.count()
.await
>= 1
{
continue;
}
warn!(
"Room {room_id} is missing extremity information, recalculating. This may take a \
long time if this room is old."
);
services
.rooms
.timeline
.pdus(&room_id, Some(PduCount::Normal(0)))
.ignore_err()
.for_each(|(_, pdu)| {
let room_id = &room_id;
async move {
services
.rooms
.timeline
.progress_extremities(&pdu)
.await
.inspect_err(|e| {
error!("Failed to progress extremities in {room_id}: {e:?}");
})
.ok();
}
})
.await;
}
Ok(())
}