Skip to main content

matrix_sdk_base/room/
members.rs

1// Copyright 2020 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    collections::{BTreeMap, BTreeSet, HashMap},
17    mem,
18    sync::Arc,
19};
20
21use bitflags::bitflags;
22use futures_util::future;
23#[cfg(feature = "unstable-msc4426")]
24use ruma::profile::{Call, CallProfileField, Status, StatusProfileField, UserProfile};
25use ruma::{
26    Int, MxcUri, OwnedUserId, UserId,
27    events::{
28        MessageLikeEventType, StateEventType,
29        ignored_user_list::IgnoredUserListEventContent,
30        presence::PresenceEvent,
31        room::{
32            member::{MembershipState, RoomMemberEventContent},
33            power_levels::{PowerLevelAction, RoomPowerLevels, UserPowerLevel},
34        },
35    },
36};
37use tracing::debug;
38
39use super::Room;
40use crate::{
41    MinimalRoomMemberEvent, StateStore, StoreError,
42    deserialized_responses::{DisplayName, MemberEvent},
43    store::{Result as StoreResult, StateStoreExt, ambiguity_map::is_display_name_ambiguous},
44};
45
46impl Room {
47    /// Check if the room has its members fully synced.
48    ///
49    /// Members might be missing if lazy member loading was enabled for the
50    /// sync.
51    ///
52    /// Returns true if no members are missing, false otherwise.
53    pub fn are_members_synced(&self) -> bool {
54        self.info.read().members_synced
55    }
56
57    /// Mark this Room as holding all member information.
58    ///
59    /// Useful in tests if we want to persuade the Room not to sync when asked
60    /// about its members.
61    #[cfg(feature = "testing")]
62    pub fn mark_members_synced(&self) {
63        self.info.update(|info| {
64            info.members_synced = true;
65        });
66    }
67
68    /// Mark this Room as still missing member information.
69    pub fn mark_members_missing(&self) {
70        self.info.update_if(|info| {
71            // notify observable subscribers only if the previous value was
72            // false
73            mem::replace(&mut info.members_synced, false)
74        })
75    }
76
77    /// Get the `RoomMember`s of this room that are known to the store, with the
78    /// given memberships.
79    pub async fn members(&self, memberships: RoomMemberships) -> StoreResult<Vec<RoomMember>> {
80        let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
81
82        if user_ids.is_empty() {
83            return Ok(Vec::new());
84        }
85
86        let member_events = self
87            .store
88            .get_state_events_for_keys_static::<RoomMemberEventContent, _, _>(
89                self.room_id(),
90                &user_ids,
91            )
92            .await?
93            .into_iter()
94            .map(|raw_event| raw_event.deserialize())
95            .collect::<Result<Vec<_>, _>>()?;
96
97        let mut profiles = self.store.get_profiles(self.room_id(), &user_ids).await?;
98
99        #[cfg(feature = "unstable-msc4426")]
100        let mut global_profiles = self.store.get_global_profiles(&user_ids).await?;
101
102        let mut presences = self
103            .store
104            .get_presence_events(&user_ids)
105            .await?
106            .into_iter()
107            .filter_map(|e| {
108                e.deserialize().ok().map(|presence| (presence.sender.clone(), presence))
109            })
110            .collect::<BTreeMap<_, _>>();
111
112        let display_names = member_events.iter().map(|e| e.display_name()).collect::<Vec<_>>();
113        let room_info = self.member_room_info(&display_names).await?;
114
115        let mut members = Vec::new();
116
117        for event in member_events {
118            let profile = profiles.remove(event.user_id());
119            #[cfg(feature = "unstable-msc4426")]
120            let global_profile = global_profiles.remove(event.user_id());
121            let presence = presences.remove(event.user_id());
122            members.push(RoomMember::from_parts(
123                event,
124                profile,
125                #[cfg(feature = "unstable-msc4426")]
126                global_profile,
127                presence,
128                &room_info,
129            ))
130        }
131
132        Ok(members)
133    }
134
135    /// Get the user IDs of the members with the given memberships, without the
136    /// service members, see [`Self::service_members`].
137    pub async fn human_member_ids(
138        &self,
139        memberships: RoomMemberships,
140    ) -> StoreResult<Vec<OwnedUserId>> {
141        let user_ids = self.store.get_user_ids(self.room_id(), memberships).await?;
142
143        let Some(service_members) = self.service_members() else {
144            return Ok(user_ids);
145        };
146
147        Ok(user_ids.into_iter().filter(|user_id| !service_members.contains(user_id)).collect())
148    }
149
150    /// Returns the number of members who have joined or been invited to the
151    /// room.
152    pub fn active_members_count(&self) -> u64 {
153        self.info.read().active_members_count()
154    }
155
156    /// Returns the number of members who have been invited to the room.
157    pub fn invited_members_count(&self) -> u64 {
158        self.info.read().invited_members_count()
159    }
160
161    /// Returns the number of members who have joined the room.
162    pub fn joined_members_count(&self) -> u64 {
163        self.info.read().joined_members_count()
164    }
165
166    /// Get the `RoomMember` with the given `user_id`.
167    ///
168    /// Returns `None` if the member was never part of this room, otherwise
169    /// return a `RoomMember` that can be in a joined, RoomState::Invited, left,
170    /// banned state.
171    ///
172    /// Async because it can read from storage.
173    pub async fn get_member(&self, user_id: &UserId) -> StoreResult<Option<RoomMember>> {
174        let event = async {
175            let Some(raw_event) = self.store.get_member_event(self.room_id(), user_id).await?
176            else {
177                debug!(%user_id, "Member event not found in state store");
178                return Ok(None);
179            };
180
181            Ok(Some(raw_event.deserialize()?))
182        };
183        let presence = async {
184            let raw_event = self.store.get_presence_event(user_id).await?;
185            Ok::<Option<PresenceEvent>, StoreError>(raw_event.and_then(|e| e.deserialize().ok()))
186        };
187
188        let profile = self.store.get_profile(self.room_id(), user_id);
189
190        #[cfg(feature = "unstable-msc4426")]
191        let (Some(event), presence, profile, global_profile) =
192            future::try_join4(event, presence, profile, async {
193                self.store.get_global_profile(user_id).await
194            })
195            .await?
196        else {
197            return Ok(None);
198        };
199        #[cfg(not(feature = "unstable-msc4426"))]
200        let (Some(event), presence, profile) = future::try_join3(event, presence, profile).await?
201        else {
202            return Ok(None);
203        };
204
205        let display_names = [event.display_name()];
206        let room_info = self.member_room_info(&display_names).await?;
207
208        Ok(Some(RoomMember::from_parts(
209            event,
210            profile,
211            #[cfg(feature = "unstable-msc4426")]
212            global_profile,
213            presence,
214            &room_info,
215        )))
216    }
217
218    /// The current `MemberRoomInfo` for this room.
219    ///
220    /// Async because it can read from storage.
221    async fn member_room_info<'a>(
222        &self,
223        display_names: &'a [DisplayName],
224    ) -> StoreResult<MemberRoomInfo<'a>> {
225        let max_power_level = self.max_power_level();
226        let power_levels = async { Ok(self.power_levels_or_default().await) };
227
228        let users_display_names =
229            self.store.get_users_with_display_names(self.room_id(), display_names);
230
231        let ignored_users = async {
232            Ok(self
233                .store
234                .get_account_data_event_static::<IgnoredUserListEventContent>()
235                .await?
236                .map(|c| c.deserialize())
237                .transpose()?
238                .map(|e| e.content.ignored_users.into_keys().collect()))
239        };
240
241        let (power_levels, users_display_names, ignored_users) =
242            future::try_join3(power_levels, users_display_names, ignored_users).await?;
243
244        Ok(MemberRoomInfo {
245            power_levels: power_levels.into(),
246            max_power_level,
247            users_display_names,
248            ignored_users,
249            service_members: self.service_members(),
250        })
251    }
252}
253
254/// A member of a room.
255#[derive(Clone, Debug)]
256pub struct RoomMember {
257    pub(crate) event: Arc<MemberEvent>,
258    // The latest member event sent by the member themselves. Stored in addition
259    // to the latest member event overall to get displayname and avatar from,
260    // which should be ignored on events sent by others.
261    pub(crate) profile: Arc<Option<MinimalRoomMemberEvent>>,
262    // The user's status, taken from their global profile.
263    #[cfg(feature = "unstable-msc4426")]
264    pub(crate) status: Option<StatusProfileField>,
265    // The user's call indicator, taken from their global profile.
266    #[cfg(feature = "unstable-msc4426")]
267    pub(crate) call: Option<CallProfileField>,
268    #[allow(dead_code)]
269    pub(crate) presence: Arc<Option<PresenceEvent>>,
270    pub(crate) power_levels: Arc<RoomPowerLevels>,
271    pub(crate) max_power_level: i64,
272    pub(crate) display_name_ambiguous: bool,
273    pub(crate) is_ignored: bool,
274    pub(crate) is_service_member: bool,
275}
276
277impl RoomMember {
278    pub(crate) fn from_parts(
279        event: MemberEvent,
280        profile: Option<MinimalRoomMemberEvent>,
281        #[cfg(feature = "unstable-msc4426")] global_profile: Option<UserProfile>,
282        presence: Option<PresenceEvent>,
283        room_info: &MemberRoomInfo<'_>,
284    ) -> Self {
285        let MemberRoomInfo {
286            power_levels,
287            max_power_level,
288            users_display_names,
289            ignored_users,
290            service_members,
291        } = room_info;
292
293        let user_id = event.user_id().to_owned();
294        let display_name = event.display_name();
295        let display_name_ambiguous = users_display_names
296            .get(&display_name)
297            .is_some_and(|s| is_display_name_ambiguous(&display_name, s));
298        let is_ignored = ignored_users.as_ref().is_some_and(|s| s.contains(event.user_id()));
299        let is_service_member = service_members.as_ref().is_some_and(|s| s.contains(&user_id));
300
301        // Extract global profile fields, swallowing any deserialization errors.
302        #[cfg(feature = "unstable-msc4426")]
303        let status = global_profile.as_ref().and_then(|p| p.get_static::<Status>().ok().flatten());
304        #[cfg(feature = "unstable-msc4426")]
305        let call = global_profile.as_ref().and_then(|p| p.get_static::<Call>().ok().flatten());
306
307        Self {
308            event: event.into(),
309            profile: profile.into(),
310            #[cfg(feature = "unstable-msc4426")]
311            status,
312            #[cfg(feature = "unstable-msc4426")]
313            call,
314            presence: presence.into(),
315            power_levels: power_levels.clone(),
316            max_power_level: *max_power_level,
317            display_name_ambiguous,
318            is_ignored,
319            is_service_member,
320        }
321    }
322
323    /// Get the unique user id of this member.
324    pub fn user_id(&self) -> &UserId {
325        self.event.user_id()
326    }
327
328    /// Get the original member event
329    pub fn event(&self) -> &Arc<MemberEvent> {
330        &self.event
331    }
332
333    /// Get the display name of the member if there is one.
334    pub fn display_name(&self) -> Option<&str> {
335        if let Some(p) = self.profile.as_ref() {
336            p.content.displayname.as_deref()
337        } else {
338            self.event.displayname_value()
339        }
340    }
341
342    /// Get the name of the member.
343    ///
344    /// This returns either the display name or the local part of the user id if
345    /// the member didn't set a display name.
346    pub fn name(&self) -> &str {
347        if let Some(d) = self.display_name() { d } else { self.user_id().localpart() }
348    }
349
350    /// Get the avatar url of the member, if there is one.
351    pub fn avatar_url(&self) -> Option<&MxcUri> {
352        if let Some(p) = self.profile.as_ref() {
353            p.content.avatar_url.as_deref()
354        } else {
355            self.event.avatar_url()
356        }
357    }
358
359    /// Get the user's status, if any.
360    ///
361    /// This is the user-set status (emoji + text) taken from the user's global
362    /// profile.
363    ///
364    /// Global profiles are only populated when syncing via sliding sync with
365    /// the profiles extension enabled; this is always `None` under sync v2.
366    #[cfg(feature = "unstable-msc4426")]
367    pub fn status(&self) -> Option<&StatusProfileField> {
368        self.status.as_ref()
369    }
370
371    /// Get the user's call indicator, if any.
372    ///
373    /// This indicates the user is currently in a call (and optionally how long
374    /// they've been in it). It is taken from the user's global profile.
375    ///
376    /// Global profiles are only populated when syncing via sliding sync with
377    /// the profiles extension enabled; this is always `None` under sync v2.
378    #[cfg(feature = "unstable-msc4426")]
379    pub fn call(&self) -> Option<&CallProfileField> {
380        self.call.as_ref()
381    }
382
383    /// Get the normalized power level of this member.
384    ///
385    /// The normalized power level depends on the maximum power level that can
386    /// be found in a certain room, positive values that are not `Infinite` are
387    /// always in the range of 0-100.
388    pub fn normalized_power_level(&self) -> UserPowerLevel {
389        let UserPowerLevel::Int(power_level) = self.power_level() else {
390            return UserPowerLevel::Infinite;
391        };
392
393        let normalized_power_level = if self.max_power_level > 0 {
394            normalize_power_level(power_level, self.max_power_level)
395        } else {
396            power_level
397        };
398
399        UserPowerLevel::Int(normalized_power_level)
400    }
401
402    /// Get the power level of this member.
403    pub fn power_level(&self) -> UserPowerLevel {
404        self.power_levels.for_user(self.user_id())
405    }
406
407    /// Whether this user can ban other users based on the power levels.
408    ///
409    /// Same as `member.can_do(PowerLevelAction::Ban)`.
410    pub fn can_ban(&self) -> bool {
411        self.can_do_impl(|pls| pls.user_can_ban(self.user_id()))
412    }
413
414    /// Whether this user can invite other users based on the power levels.
415    ///
416    /// Same as `member.can_do(PowerLevelAction::Invite)`.
417    pub fn can_invite(&self) -> bool {
418        self.can_do_impl(|pls| pls.user_can_invite(self.user_id()))
419    }
420
421    /// Whether this user can kick other users based on the power levels.
422    ///
423    /// Same as `member.can_do(PowerLevelAction::Kick)`.
424    pub fn can_kick(&self) -> bool {
425        self.can_do_impl(|pls| pls.user_can_kick(self.user_id()))
426    }
427
428    /// Whether this user can redact their own events based on the power levels.
429    ///
430    /// Same as `member.can_do(PowerLevelAction::RedactOwn)`.
431    pub fn can_redact_own(&self) -> bool {
432        self.can_do_impl(|pls| pls.user_can_redact_own_event(self.user_id()))
433    }
434
435    /// Whether this user can redact events of other users based on the power
436    /// levels.
437    ///
438    /// Same as `member.can_do(PowerLevelAction::RedactOther)`.
439    pub fn can_redact_other(&self) -> bool {
440        self.can_do_impl(|pls| pls.user_can_redact_event_of_other(self.user_id()))
441    }
442
443    /// Whether this user can send message events based on the power levels.
444    ///
445    /// Same as `member.can_do(PowerLevelAction::SendMessage(msg_type))`.
446    pub fn can_send_message(&self, msg_type: MessageLikeEventType) -> bool {
447        self.can_do_impl(|pls| pls.user_can_send_message(self.user_id(), msg_type))
448    }
449
450    /// Whether this user can send state events based on the power levels.
451    ///
452    /// Same as `member.can_do(PowerLevelAction::SendState(state_type))`.
453    pub fn can_send_state(&self, state_type: StateEventType) -> bool {
454        self.can_do_impl(|pls| pls.user_can_send_state(self.user_id(), state_type))
455    }
456
457    /// Whether this user can pin or unpin events based on the power levels.
458    pub fn can_pin_or_unpin_event(&self) -> bool {
459        self.can_send_state(StateEventType::RoomPinnedEvents)
460    }
461
462    /// Whether this user can notify everybody in the room by writing `@room` in
463    /// a message.
464    ///
465    /// Same as
466    /// `member. can_do(PowerLevelAction::TriggerNotification(NotificationPowerLevelType::Room))`.
467    pub fn can_trigger_room_notification(&self) -> bool {
468        self.can_do_impl(|pls| pls.user_can_trigger_room_notification(self.user_id()))
469    }
470
471    /// Whether this user can do the given action based on the power levels.
472    pub fn can_do(&self, action: PowerLevelAction) -> bool {
473        self.can_do_impl(|pls| pls.user_can_do(self.user_id(), action))
474    }
475
476    fn can_do_impl(&self, f: impl FnOnce(&RoomPowerLevels) -> bool) -> bool {
477        f(&self.power_levels)
478    }
479
480    /// Is the name that the member uses ambiguous in the room.
481    ///
482    /// A name is considered to be ambiguous if at least one other member shares
483    /// the same name.
484    pub fn name_ambiguous(&self) -> bool {
485        self.display_name_ambiguous
486    }
487
488    /// Get the membership state of this member.
489    pub fn membership(&self) -> &MembershipState {
490        self.event.membership()
491    }
492
493    /// Is the room member ignored by the current account user
494    pub fn is_ignored(&self) -> bool {
495        self.is_ignored
496    }
497
498    /// Is the room member a service member.
499    pub fn is_service_member(&self) -> bool {
500        self.is_service_member
501    }
502}
503
504// Information about the room a member is in.
505pub(crate) struct MemberRoomInfo<'a> {
506    pub(crate) power_levels: Arc<RoomPowerLevels>,
507    pub(crate) max_power_level: i64,
508    pub(crate) users_display_names: HashMap<&'a DisplayName, BTreeSet<OwnedUserId>>,
509    pub(crate) ignored_users: Option<BTreeSet<OwnedUserId>>,
510    pub(crate) service_members: Option<BTreeSet<OwnedUserId>>,
511}
512
513/// The kind of room member updates that just happened.
514#[derive(Debug, Clone)]
515pub enum RoomMembersUpdate {
516    /// The whole list room members was reloaded.
517    FullReload,
518    /// A few members were updated, their user ids are included.
519    Partial(BTreeSet<OwnedUserId>),
520}
521
522bitflags! {
523    /// Room membership filter as a bitset.
524    ///
525    /// Note that [`RoomMemberships::empty()`] doesn't filter the results and
526    /// [`RoomMemberships::all()`] filters out unknown memberships.
527    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
528    pub struct RoomMemberships: u16 {
529        /// The member joined the room.
530        const JOIN    = 0b00000001;
531        /// The member was invited to the room.
532        const INVITE  = 0b00000010;
533        /// The member requested to join the room.
534        const KNOCK   = 0b00000100;
535        /// The member left the room.
536        const LEAVE   = 0b00001000;
537        /// The member was banned.
538        const BAN     = 0b00010000;
539
540        /// The member is active in the room (i.e. joined or invited).
541        const ACTIVE = Self::JOIN.bits() | Self::INVITE.bits();
542    }
543}
544
545impl RoomMemberships {
546    /// Whether the given membership matches this `RoomMemberships`.
547    pub fn matches(&self, membership: &MembershipState) -> bool {
548        if self.is_empty() {
549            return true;
550        }
551
552        let membership = match membership {
553            MembershipState::Ban => Self::BAN,
554            MembershipState::Invite => Self::INVITE,
555            MembershipState::Join => Self::JOIN,
556            MembershipState::Knock => Self::KNOCK,
557            MembershipState::Leave => Self::LEAVE,
558            _ => return false,
559        };
560
561        self.contains(membership)
562    }
563
564    /// Get this `RoomMemberships` as a list of matching [`MembershipState`]s.
565    pub fn as_vec(&self) -> Vec<MembershipState> {
566        let mut memberships = Vec::new();
567
568        if self.contains(Self::JOIN) {
569            memberships.push(MembershipState::Join);
570        }
571        if self.contains(Self::INVITE) {
572            memberships.push(MembershipState::Invite);
573        }
574        if self.contains(Self::KNOCK) {
575            memberships.push(MembershipState::Knock);
576        }
577        if self.contains(Self::LEAVE) {
578            memberships.push(MembershipState::Leave);
579        }
580        if self.contains(Self::BAN) {
581            memberships.push(MembershipState::Ban);
582        }
583
584        memberships
585    }
586}
587
588/// Scale the given `power_level` to a range between 0-100.
589pub fn normalize_power_level(power_level: Int, max_power_level: i64) -> Int {
590    let mut power_level = i64::from(power_level);
591    power_level = (power_level * 100) / max_power_level;
592
593    Int::try_from(power_level.clamp(0, 100))
594        .expect("We clamped the normalized power level so they must fit into the Int")
595}
596
597#[cfg(test)]
598mod tests {
599    #[cfg(feature = "unstable-msc4426")]
600    use matrix_sdk_common::ROOM_VERSION_RULES_FALLBACK;
601    #[cfg(feature = "unstable-msc4426")]
602    use matrix_sdk_test::event_factory::EventFactory;
603    use proptest::prelude::*;
604    #[cfg(feature = "unstable-msc4426")]
605    use ruma::{
606        SecondsSinceUnixEpoch, events::room::power_levels::RoomPowerLevelsSource,
607        profile::ProfileFieldValue, user_id,
608    };
609
610    use super::*;
611
612    prop_compose! {
613        fn arb_int()(id in any::<i64>()) -> Int {
614            id.try_into().unwrap_or_default()
615        }
616    }
617
618    proptest! {
619        #![proptest_config(ProptestConfig::with_cases(10_000))]
620        #[test]
621        fn test_power_level_normalization_with_min_max_level(power_level in arb_int()) {
622            let normalized = normalize_power_level(power_level, 1);
623            let normalized = i64::from(normalized);
624
625            assert!(normalized >= 0);
626            assert!(normalized <= 100);
627        }
628    }
629
630    proptest! {
631        #![proptest_config(ProptestConfig::with_cases(10_000))]
632        #[test]
633        fn test_power_level_normalization(power_level in arb_int(), max_level in 1i64..) {
634            let normalized = normalize_power_level(power_level, max_level);
635            let normalized = i64::from(normalized);
636
637            assert!(normalized >= 0);
638            assert!(normalized <= 100);
639        }
640    }
641
642    #[test]
643    fn test_power_level_normalization_limits() {
644        let level = Int::MIN;
645        let normalized = normalize_power_level(level, 1);
646        let normalized = i64::from(normalized);
647        assert!(normalized >= 0);
648        assert!(normalized <= 100);
649
650        let level = Int::MAX;
651        let normalized = normalize_power_level(level, 1);
652        let normalized = i64::from(normalized);
653        assert!(normalized >= 0);
654        assert!(normalized <= 100);
655    }
656
657    #[cfg(feature = "unstable-msc4426")]
658    #[test]
659    fn test_global_profile_fields() {
660        let user_id = user_id!("@alice:example.org");
661        let event = MemberEvent::Sync(EventFactory::new().sender(user_id).member(user_id).into());
662        let room_info = MemberRoomInfo {
663            power_levels: Arc::new(RoomPowerLevels::new(
664                RoomPowerLevelsSource::None,
665                &ROOM_VERSION_RULES_FALLBACK.authorization,
666                std::iter::empty::<OwnedUserId>(),
667            )),
668            max_power_level: 100,
669            users_display_names: HashMap::new(),
670            ignored_users: None,
671            service_members: None,
672        };
673
674        // Without a global profile, neither field is set.
675        let member = RoomMember::from_parts(event.clone(), None, None, None, &room_info);
676        assert!(member.status().is_none());
677        assert!(member.call().is_none());
678
679        // A global profile carrying both m.status and m.call is extracted into
680        // both fields.
681        let mut call = CallProfileField::new();
682        call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
683        let global_profile = UserProfile::from_iter([
684            ProfileFieldValue::Status(StatusProfileField::new(
685                "Working".to_owned(),
686                "💻".to_owned(),
687            )),
688            ProfileFieldValue::Call(call),
689        ]);
690
691        let member = RoomMember::from_parts(event, None, Some(global_profile), None, &room_info);
692
693        let status = member.status().expect("status is set");
694        assert_eq!(status.text, "Working");
695        assert_eq!(status.emoji, "💻");
696        assert_eq!(
697            member.call().expect("call is set").call_joined_ts,
698            Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
699        );
700    }
701}