diff --git a/src/api/client/mod.rs b/src/api/client/mod.rs
index 6a6f16a75226039a76706250d6b46b59545e34d5..1461be94536ddabd4004e5050ff9f8cf585ed701 100644
--- a/src/api/client/mod.rs
+++ b/src/api/client/mod.rs
@@ -51,6 +51,7 @@
 pub(super) use message::*;
 pub(super) use presence::*;
 pub(super) use profile::*;
+pub use profile::{update_all_rooms, update_avatar_url, update_displayname};
 pub(super) use push::*;
 pub(super) use read_marker::*;
 pub(super) use redact::*;
diff --git a/src/api/client/profile.rs b/src/api/client/profile.rs
index 96cbe7fddc8f57c693ae3fd21cd85f796943e9ca..8a53b335b0bc7236e551c9e74ae98a92fe76cc88 100644
--- a/src/api/client/profile.rs
+++ b/src/api/client/profile.rs
@@ -10,6 +10,7 @@
 	},
 	events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
 	presence::PresenceState,
+	OwnedMxcUri, OwnedRoomId, OwnedUserId,
 };
 use serde_json::value::to_raw_value;
 use tracing::warn;
@@ -28,70 +29,14 @@ pub(crate) async fn set_displayname_route(
 	body: Ruma<set_display_name::v3::Request>,
 ) -> Result<set_display_name::v3::Response> {
 	let sender_user = body.sender_user.as_ref().expect("user is authenticated");
-
-	services()
-		.users
-		.set_displayname(sender_user, body.displayname.clone())
-		.await?;
-
-	// Send a new membership event and presence update into all joined rooms
-	let all_rooms_joined: Vec<_> = services()
+	let all_joined_rooms: Vec<OwnedRoomId> = services()
 		.rooms
 		.state_cache
 		.rooms_joined(sender_user)
 		.filter_map(Result::ok)
-		.map(|room_id| {
-			Ok::<_, Error>((
-				PduBuilder {
-					event_type: TimelineEventType::RoomMember,
-					content: to_raw_value(&RoomMemberEventContent {
-						displayname: body.displayname.clone(),
-						join_authorized_via_users_server: None,
-						..serde_json::from_str(
-							services()
-								.rooms
-								.state_accessor
-								.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
-								.ok_or_else(|| {
-									Error::bad_database("Tried to send displayname update for user not in the room.")
-								})?
-								.content
-								.get(),
-						)
-						.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
-					})
-					.expect("event is valid, we just created it"),
-					unsigned: None,
-					state_key: Some(sender_user.to_string()),
-					redacts: None,
-				},
-				room_id,
-			))
-		})
-		.filter_map(Result::ok)
 		.collect();
 
-	for (pdu_builder, room_id) in all_rooms_joined {
-		let mutex_state = Arc::clone(
-			services()
-				.globals
-				.roomid_mutex_state
-				.write()
-				.await
-				.entry(room_id.clone())
-				.or_default(),
-		);
-		let state_lock = mutex_state.lock().await;
-
-		if let Err(e) = services()
-			.rooms
-			.timeline
-			.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
-			.await
-		{
-			warn!(%e, "Failed to update/send new display name in room");
-		}
-	}
+	update_displayname(sender_user.clone(), body.displayname.clone(), all_joined_rooms).await?;
 
 	if services().globals.allow_local_presence() {
 		// Presence update
@@ -168,75 +113,20 @@ pub(crate) async fn set_avatar_url_route(
 	body: Ruma<set_avatar_url::v3::Request>,
 ) -> Result<set_avatar_url::v3::Response> {
 	let sender_user = body.sender_user.as_ref().expect("user is authenticated");
-
-	services()
-		.users
-		.set_avatar_url(sender_user, body.avatar_url.clone())
-		.await?;
-
-	services()
-		.users
-		.set_blurhash(sender_user, body.blurhash.clone())
-		.await?;
-
-	// Send a new membership event and presence update into all joined rooms
-	let all_joined_rooms: Vec<_> = services()
+	let all_joined_rooms: Vec<OwnedRoomId> = services()
 		.rooms
 		.state_cache
 		.rooms_joined(sender_user)
 		.filter_map(Result::ok)
-		.map(|room_id| {
-			Ok::<_, Error>((
-				PduBuilder {
-					event_type: TimelineEventType::RoomMember,
-					content: to_raw_value(&RoomMemberEventContent {
-						avatar_url: body.avatar_url.clone(),
-						join_authorized_via_users_server: None,
-						..serde_json::from_str(
-							services()
-								.rooms
-								.state_accessor
-								.room_state_get(&room_id, &StateEventType::RoomMember, sender_user.as_str())?
-								.ok_or_else(|| {
-									Error::bad_database("Tried to send displayname update for user not in the room.")
-								})?
-								.content
-								.get(),
-						)
-						.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
-					})
-					.expect("event is valid, we just created it"),
-					unsigned: None,
-					state_key: Some(sender_user.to_string()),
-					redacts: None,
-				},
-				room_id,
-			))
-		})
-		.filter_map(Result::ok)
 		.collect();
 
-	for (pdu_builder, room_id) in all_joined_rooms {
-		let mutex_state = Arc::clone(
-			services()
-				.globals
-				.roomid_mutex_state
-				.write()
-				.await
-				.entry(room_id.clone())
-				.or_default(),
-		);
-		let state_lock = mutex_state.lock().await;
-
-		if let Err(e) = services()
-			.rooms
-			.timeline
-			.build_and_append_pdu(pdu_builder, sender_user, &room_id, &state_lock)
-			.await
-		{
-			warn!(%e, "Failed to set/update room with new avatar URL / pfp");
-		}
-	}
+	update_avatar_url(
+		sender_user.clone(),
+		body.avatar_url.clone(),
+		body.blurhash.clone(),
+		all_joined_rooms,
+	)
+	.await?;
 
 	if services().globals.allow_local_presence() {
 		// Presence update
@@ -363,3 +253,126 @@ pub(crate) async fn get_profile_route(body: Ruma<get_profile::v3::Request>) -> R
 		displayname: services().users.displayname(&body.user_id)?,
 	})
 }
+
+pub async fn update_displayname(
+	user_id: OwnedUserId, displayname: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
+) -> Result<()> {
+	services()
+		.users
+		.set_displayname(&user_id, displayname.clone())
+		.await?;
+
+	// Send a new join membership event into all joined rooms
+	let all_joined_rooms: Vec<_> = all_joined_rooms
+		.iter()
+		.map(|room_id| {
+			Ok::<_, Error>((
+				PduBuilder {
+					event_type: TimelineEventType::RoomMember,
+					content: to_raw_value(&RoomMemberEventContent {
+						displayname: displayname.clone(),
+						join_authorized_via_users_server: None,
+						..serde_json::from_str(
+							services()
+								.rooms
+								.state_accessor
+								.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
+								.ok_or_else(|| {
+									Error::bad_database("Tried to send display name update for user not in the room.")
+								})?
+								.content
+								.get(),
+						)
+						.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
+					})
+					.expect("event is valid, we just created it"),
+					unsigned: None,
+					state_key: Some(user_id.to_string()),
+					redacts: None,
+				},
+				room_id,
+			))
+		})
+		.filter_map(Result::ok)
+		.collect();
+
+	update_all_rooms(all_joined_rooms, user_id).await;
+
+	Ok(())
+}
+
+pub async fn update_avatar_url(
+	user_id: OwnedUserId, avatar_url: Option<OwnedMxcUri>, blurhash: Option<String>, all_joined_rooms: Vec<OwnedRoomId>,
+) -> Result<()> {
+	services()
+		.users
+		.set_avatar_url(&user_id, avatar_url.clone())
+		.await?;
+	services()
+		.users
+		.set_blurhash(&user_id, blurhash.clone())
+		.await?;
+
+	// Send a new join membership event into all joined rooms
+	let all_joined_rooms: Vec<_> = all_joined_rooms
+		.iter()
+		.map(|room_id| {
+			Ok::<_, Error>((
+				PduBuilder {
+					event_type: TimelineEventType::RoomMember,
+					content: to_raw_value(&RoomMemberEventContent {
+						avatar_url: avatar_url.clone(),
+						blurhash: blurhash.clone(),
+						join_authorized_via_users_server: None,
+						..serde_json::from_str(
+							services()
+								.rooms
+								.state_accessor
+								.room_state_get(room_id, &StateEventType::RoomMember, user_id.as_str())?
+								.ok_or_else(|| {
+									Error::bad_database("Tried to send avatar URL update for user not in the room.")
+								})?
+								.content
+								.get(),
+						)
+						.map_err(|_| Error::bad_database("Database contains invalid PDU."))?
+					})
+					.expect("event is valid, we just created it"),
+					unsigned: None,
+					state_key: Some(user_id.to_string()),
+					redacts: None,
+				},
+				room_id,
+			))
+		})
+		.filter_map(Result::ok)
+		.collect();
+
+	update_all_rooms(all_joined_rooms, user_id).await;
+
+	Ok(())
+}
+
+pub async fn update_all_rooms(all_joined_rooms: Vec<(PduBuilder, &OwnedRoomId)>, user_id: OwnedUserId) {
+	for (pdu_builder, room_id) in all_joined_rooms {
+		let mutex_state = Arc::clone(
+			services()
+				.globals
+				.roomid_mutex_state
+				.write()
+				.await
+				.entry(room_id.clone())
+				.or_default(),
+		);
+		let state_lock = mutex_state.lock().await;
+
+		if let Err(e) = services()
+			.rooms
+			.timeline
+			.build_and_append_pdu(pdu_builder, &user_id, room_id, &state_lock)
+			.await
+		{
+			warn!(%user_id, %room_id, %e, "Failed to update/send new profile join membership update in room");
+		}
+	}
+}
diff --git a/src/service/users/data.rs b/src/service/users/data.rs
index f23557550f48e4a7d9e50e6f0069555ffa7697bf..5d3eadd8627c2d523529abfe2a32059ee78d424e 100644
--- a/src/service/users/data.rs
+++ b/src/service/users/data.rs
@@ -307,7 +307,7 @@ fn blurhash(&self, user_id: &UserId) -> Result<Option<String>> {
 			.transpose()
 	}
 
-	/// Sets a new avatar_url or removes it if avatar_url is None.
+	/// Sets a new blurhash or removes it if blurhash is None.
 	fn set_blurhash(&self, user_id: &UserId, blurhash: Option<String>) -> Result<()> {
 		if let Some(blurhash) = blurhash {
 			self.userid_blurhash
diff --git a/src/service/users/mod.rs b/src/service/users/mod.rs
index ec17e796a62965831be984f0c4fcf010efb23145..b326078b2fa81205338e7fd2887eae39698d653c 100644
--- a/src/service/users/mod.rs
+++ b/src/service/users/mod.rs
@@ -299,7 +299,7 @@ pub async fn set_avatar_url(&self, user_id: &UserId, avatar_url: Option<OwnedMxc
 	/// Get the blurhash of a user.
 	pub fn blurhash(&self, user_id: &UserId) -> Result<Option<String>> { self.db.blurhash(user_id) }
 
-	/// Sets a new avatar_url or removes it if avatar_url is None.
+	/// Sets a new blurhash or removes it if blurhash is None.
 	pub async fn set_blurhash(&self, user_id: &UserId, blurhash: Option<String>) -> Result<()> {
 		self.db.set_blurhash(user_id, blurhash)
 	}