LCOV - code coverage report
Current view: top level - lib/src - user.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 85 97 87.6 %
Date: 2024-09-30 15:57:20 Functions: 0 0 -

          Line data    Source code
       1             : /*
       2             :  *   Famedly Matrix SDK
       3             :  *   Copyright (C) 2019, 2020, 2021 Famedly GmbH
       4             :  *
       5             :  *   This program is free software: you can redistribute it and/or modify
       6             :  *   it under the terms of the GNU Affero General Public License as
       7             :  *   published by the Free Software Foundation, either version 3 of the
       8             :  *   License, or (at your option) any later version.
       9             :  *
      10             :  *   This program is distributed in the hope that it will be useful,
      11             :  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
      12             :  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      13             :  *   GNU Affero General Public License for more details.
      14             :  *
      15             :  *   You should have received a copy of the GNU Affero General Public License
      16             :  *   along with this program.  If not, see <https://www.gnu.org/licenses/>.
      17             :  */
      18             : 
      19             : import 'package:matrix/matrix.dart';
      20             : 
      21             : /// Represents a Matrix User which may be a participant in a Matrix Room.
      22             : class User extends StrippedStateEvent {
      23             :   final Room room;
      24             :   final Map<String, Object?>? prevContent;
      25             : 
      26           9 :   factory User(
      27             :     String id, {
      28             :     String? membership,
      29             :     String? displayName,
      30             :     String? avatarUrl,
      31             :     required Room room,
      32             :   }) {
      33           9 :     return User.fromState(
      34             :       stateKey: id,
      35             :       senderId: id,
      36           9 :       content: {
      37           8 :         if (membership != null) 'membership': membership,
      38           8 :         if (displayName != null) 'displayname': displayName,
      39           4 :         if (avatarUrl != null) 'avatar_url': avatarUrl,
      40             :       },
      41             :       typeKey: EventTypes.RoomMember,
      42             :       room: room,
      43             :     );
      44             :   }
      45             : 
      46          32 :   User.fromState({
      47             :     required String super.stateKey,
      48             :     super.content = const {},
      49             :     required String typeKey,
      50             :     required super.senderId,
      51             :     required this.room,
      52             :     this.prevContent,
      53          32 :   }) : super(
      54             :           type: typeKey,
      55             :         );
      56             : 
      57             :   /// The full qualified Matrix ID in the format @username:server.abc.
      58          64 :   String get id => stateKey ?? '@unknown:unknown';
      59             : 
      60             :   /// The displayname of the user if the user has set one.
      61          10 :   String? get displayName =>
      62          20 :       content.tryGet<String>('displayname') ??
      63          16 :       (membership == Membership.join
      64             :           ? null
      65           2 :           : prevContent?.tryGet<String>('displayname'));
      66             : 
      67             :   /// Returns the power level of this user.
      68          16 :   int get powerLevel => room.getPowerLevelByUserId(id);
      69             : 
      70             :   /// The membership status of the user. One of:
      71             :   /// join
      72             :   /// invite
      73             :   /// leave
      74             :   /// ban
      75          96 :   Membership get membership => Membership.values.firstWhere((e) {
      76          64 :         if (content['membership'] != null) {
      77         160 :           return e.toString() == 'Membership.${content['membership']}';
      78             :         }
      79             :         return false;
      80           8 :       }, orElse: () => Membership.join);
      81             : 
      82             :   /// The avatar if the user has one.
      83           2 :   Uri? get avatarUrl {
      84           4 :     final uri = content.tryGet<String>('avatar_url') ??
      85           0 :         (membership == Membership.join
      86             :             ? null
      87           0 :             : prevContent?.tryGet<String>('avatar_url'));
      88           2 :     return uri == null ? null : Uri.tryParse(uri);
      89             :   }
      90             : 
      91             :   /// Returns the displayname or the local part of the Matrix ID if the user
      92             :   /// has no displayname. If [formatLocalpart] is true, then the localpart will
      93             :   /// be formatted in the way, that all "_" characters are becomming white spaces and
      94             :   /// the first character of each word becomes uppercase.
      95             :   /// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
      96             :   /// if there is no other displayname available. If not then this will return "Unknown user".
      97           6 :   String calcDisplayname(
      98             :       {bool? formatLocalpart,
      99             :       bool? mxidLocalPartFallback,
     100             :       MatrixLocalizations i18n = const MatrixDefaultLocalizations()}) {
     101          18 :     formatLocalpart ??= room.client.formatLocalpart;
     102          18 :     mxidLocalPartFallback ??= room.client.mxidLocalPartFallback;
     103           6 :     final displayName = this.displayName;
     104           5 :     if (displayName != null && displayName.isNotEmpty) {
     105             :       return displayName;
     106             :     }
     107           3 :     final stateKey = this.stateKey;
     108             :     if (stateKey != null && mxidLocalPartFallback) {
     109             :       if (!formatLocalpart) {
     110           2 :         return stateKey.localpart ?? '';
     111             :       }
     112           9 :       final words = stateKey.localpart?.replaceAll('_', ' ').split(' ') ?? [];
     113           9 :       for (var i = 0; i < words.length; i++) {
     114           6 :         if (words[i].isNotEmpty) {
     115          21 :           words[i] = words[i][0].toUpperCase() + words[i].substring(1);
     116             :         }
     117             :       }
     118           6 :       return words.join(' ').trim();
     119             :     }
     120           2 :     return i18n.unknownUser;
     121             :   }
     122             : 
     123             :   /// Call the Matrix API to kick this user from this room.
     124           8 :   Future<void> kick() async => await room.kick(id);
     125             : 
     126             :   /// Call the Matrix API to ban this user from this room.
     127           8 :   Future<void> ban() async => await room.ban(id);
     128             : 
     129             :   /// Call the Matrix API to unban this banned user from this room.
     130           8 :   Future<void> unban() async => await room.unban(id);
     131             : 
     132             :   /// Call the Matrix API to change the power level of this user.
     133           8 :   Future<void> setPower(int power) async => await room.setPower(id, power);
     134             : 
     135             :   /// Returns an existing direct chat ID with this user or creates a new one.
     136             :   /// Returns null on error.
     137           2 :   Future<String> startDirectChat({
     138             :     bool? enableEncryption,
     139             :     List<StateEvent>? initialState,
     140             :     bool waitForSync = true,
     141             :   }) async =>
     142           6 :       room.client.startDirectChat(
     143           2 :         id,
     144             :         enableEncryption: enableEncryption,
     145             :         initialState: initialState,
     146             :         waitForSync: waitForSync,
     147             :       );
     148             : 
     149             :   /// The newest presence of this user if there is any and null if not.
     150           0 :   @Deprecated('Deprecated in favour of currentPresence.')
     151           0 :   Presence? get presence => room.client.presences[id]?.toPresence();
     152             : 
     153           0 :   @Deprecated('Use fetchCurrentPresence() instead')
     154           0 :   Future<CachedPresence> get currentPresence => fetchCurrentPresence();
     155             : 
     156             :   /// The newest presence of this user if there is any. Fetches it from the
     157             :   /// database first and then from the server if necessary or returns offline.
     158           2 :   Future<CachedPresence> fetchCurrentPresence() =>
     159           8 :       room.client.fetchCurrentPresence(id);
     160             : 
     161             :   /// Whether the client is able to ban/unban this user.
     162           6 :   bool get canBan => room.canBan && powerLevel < room.ownPowerLevel;
     163             : 
     164             :   /// Whether the client is able to kick this user.
     165           2 :   bool get canKick =>
     166           6 :       [Membership.join, Membership.invite].contains(membership) &&
     167           4 :       room.canKick &&
     168           0 :       powerLevel < room.ownPowerLevel;
     169             : 
     170           0 :   @Deprecated('Use [canChangeUserPowerLevel] instead.')
     171           0 :   bool get canChangePowerLevel => canChangeUserPowerLevel;
     172             : 
     173             :   /// Whether the client is allowed to change the power level of this user.
     174             :   /// Please be aware that you can only set the power level to at least your own!
     175           2 :   bool get canChangeUserPowerLevel =>
     176           4 :       room.canChangePowerLevel &&
     177          18 :       (powerLevel < room.ownPowerLevel || id == room.client.userID);
     178             : 
     179           1 :   @override
     180           1 :   bool operator ==(Object other) => (other is User &&
     181           3 :       other.id == id &&
     182           3 :       other.room == room &&
     183           3 :       other.membership == membership);
     184             : 
     185           0 :   @override
     186           0 :   int get hashCode => Object.hash(id, room, membership);
     187             : 
     188             :   /// Get the mention text to use in a plain text body to mention this specific user
     189             :   /// in this specific room
     190           2 :   String get mention {
     191             :     // if the displayname has [ or ] or : we can't build our more fancy stuff, so fall back to the id
     192             :     // [] is used for the delimitors
     193             :     // If we allowed : we could get collissions with the mxid fallbacks
     194           2 :     final displayName = this.displayName;
     195             :     if (displayName == null ||
     196           2 :         displayName.isEmpty ||
     197          10 :         {'[', ']', ':'}.any(displayName.contains)) {
     198           2 :       return id;
     199             :     }
     200             : 
     201             :     final identifier =
     202           8 :         '@${RegExp(r'^\w+$').hasMatch(displayName) ? displayName : '[$displayName]'}';
     203             : 
     204             :     // get all the users with the same display name
     205           4 :     final allUsersWithSameDisplayname = room.getParticipants();
     206           4 :     allUsersWithSameDisplayname.removeWhere((user) =>
     207           6 :         user.id == id ||
     208           4 :         (user.displayName?.isEmpty ?? true) ||
     209           4 :         user.displayName != displayName);
     210           2 :     if (allUsersWithSameDisplayname.isEmpty) {
     211             :       return identifier;
     212             :     }
     213             :     // ok, we have multiple users with the same display name....time to calculate a hash
     214           8 :     final hashes = allUsersWithSameDisplayname.map((u) => _hash(u.id));
     215           4 :     final ourHash = _hash(id);
     216             :     // hash collission...just return our own mxid again
     217           2 :     if (hashes.contains(ourHash)) {
     218           0 :       return id;
     219             :     }
     220           2 :     return '$identifier#$ourHash';
     221             :   }
     222             : 
     223             :   /// Get the mention fragments for this user.
     224           4 :   Set<String> get mentionFragments {
     225           4 :     final displayName = this.displayName;
     226             :     if (displayName == null ||
     227           4 :         displayName.isEmpty ||
     228          20 :         {'[', ']', ':'}.any(displayName.contains)) {
     229             :       return {};
     230             :     }
     231             :     final identifier =
     232          16 :         '@${RegExp(r'^\w+$').hasMatch(displayName) ? displayName : '[$displayName]'}';
     233             : 
     234           8 :     final hash = _hash(id);
     235           8 :     return {identifier, '$identifier#$hash'};
     236             :   }
     237             : }
     238             : 
     239             : const _maximumHashLength = 10000;
     240           4 : String _hash(String s) =>
     241          24 :     (s.codeUnits.fold<int>(0, (a, b) => a + b) % _maximumHashLength).toString();
     242             : 
     243             : extension FromStrippedStateEventExtension on StrippedStateEvent {
     244          64 :   User asUser(Room room) => User.fromState(
     245             :         // state key should always be set for member events
     246          32 :         stateKey: stateKey!,
     247          32 :         content: content,
     248          32 :         typeKey: type,
     249          32 :         senderId: senderId,
     250             :         room: room,
     251             :       );
     252             : }

Generated by: LCOV version 1.14