LCOV - code coverage report
Current view: top level - lib/src - user.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 87.9 % 99 87
Test Date: 2025-01-14 13:39:53 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           10 :   factory User(
      27              :     String id, {
      28              :     String? membership,
      29              :     String? displayName,
      30              :     String? avatarUrl,
      31              :     required Room room,
      32              :   }) {
      33           10 :     return User.fromState(
      34              :       stateKey: id,
      35              :       senderId: id,
      36           10 :       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           33 :   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           33 :   }) : super(
      54              :           type: typeKey,
      55              :         );
      56              : 
      57              :   /// The full qualified Matrix ID in the format @username:server.abc.
      58           66 :   String get id => stateKey ?? '@unknown:unknown';
      59              : 
      60              :   /// The displayname of the user if the user has set one.
      61           11 :   String? get displayName =>
      62           22 :       content.tryGet<String>('displayname') ??
      63           18 :       (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           66 :   Membership get membership => Membership.values.firstWhere(
      76           33 :         (e) {
      77           66 :           if (content['membership'] != null) {
      78          165 :             return e.toString() == 'Membership.${content['membership']}';
      79              :           }
      80              :           return false;
      81              :         },
      82            9 :         orElse: () => Membership.join,
      83              :       );
      84              : 
      85              :   /// The avatar if the user has one.
      86            2 :   Uri? get avatarUrl {
      87            4 :     final uri = content.tryGet<String>('avatar_url') ??
      88            0 :         (membership == Membership.join
      89              :             ? null
      90            0 :             : prevContent?.tryGet<String>('avatar_url'));
      91            2 :     return uri == null ? null : Uri.tryParse(uri);
      92              :   }
      93              : 
      94              :   /// Returns the displayname or the local part of the Matrix ID if the user
      95              :   /// has no displayname. If [formatLocalpart] is true, then the localpart will
      96              :   /// be formatted in the way, that all "_" characters are becomming white spaces and
      97              :   /// the first character of each word becomes uppercase.
      98              :   /// If [mxidLocalPartFallback] is true, then the local part of the mxid will be shown
      99              :   /// if there is no other displayname available. If not then this will return "Unknown user".
     100            7 :   String calcDisplayname({
     101              :     bool? formatLocalpart,
     102              :     bool? mxidLocalPartFallback,
     103              :     MatrixLocalizations i18n = const MatrixDefaultLocalizations(),
     104              :   }) {
     105           21 :     formatLocalpart ??= room.client.formatLocalpart;
     106           21 :     mxidLocalPartFallback ??= room.client.mxidLocalPartFallback;
     107            7 :     final displayName = this.displayName;
     108            5 :     if (displayName != null && displayName.isNotEmpty) {
     109              :       return displayName;
     110              :     }
     111            4 :     final stateKey = this.stateKey;
     112              :     if (stateKey != null && mxidLocalPartFallback) {
     113              :       if (!formatLocalpart) {
     114            2 :         return stateKey.localpart ?? '';
     115              :       }
     116           12 :       final words = stateKey.localpart?.replaceAll('_', ' ').split(' ') ?? [];
     117           12 :       for (var i = 0; i < words.length; i++) {
     118            8 :         if (words[i].isNotEmpty) {
     119           28 :           words[i] = words[i][0].toUpperCase() + words[i].substring(1);
     120              :         }
     121              :       }
     122            8 :       return words.join(' ').trim();
     123              :     }
     124            2 :     return i18n.unknownUser;
     125              :   }
     126              : 
     127              :   /// Call the Matrix API to kick this user from this room.
     128            8 :   Future<void> kick() async => await room.kick(id);
     129              : 
     130              :   /// Call the Matrix API to ban this user from this room.
     131            8 :   Future<void> ban() async => await room.ban(id);
     132              : 
     133              :   /// Call the Matrix API to unban this banned user from this room.
     134            8 :   Future<void> unban() async => await room.unban(id);
     135              : 
     136              :   /// Call the Matrix API to change the power level of this user.
     137            8 :   Future<void> setPower(int power) async => await room.setPower(id, power);
     138              : 
     139              :   /// Returns an existing direct chat ID with this user or creates a new one.
     140              :   /// Returns null on error.
     141            2 :   Future<String> startDirectChat({
     142              :     bool? enableEncryption,
     143              :     List<StateEvent>? initialState,
     144              :     bool waitForSync = true,
     145              :   }) async =>
     146            6 :       room.client.startDirectChat(
     147            2 :         id,
     148              :         enableEncryption: enableEncryption,
     149              :         initialState: initialState,
     150              :         waitForSync: waitForSync,
     151              :       );
     152              : 
     153              :   /// The newest presence of this user if there is any and null if not.
     154            0 :   @Deprecated('Deprecated in favour of currentPresence.')
     155            0 :   Presence? get presence => room.client.presences[id]?.toPresence();
     156              : 
     157            0 :   @Deprecated('Use fetchCurrentPresence() instead')
     158            0 :   Future<CachedPresence> get currentPresence => fetchCurrentPresence();
     159              : 
     160              :   /// The newest presence of this user if there is any. Fetches it from the
     161              :   /// database first and then from the server if necessary or returns offline.
     162            2 :   Future<CachedPresence> fetchCurrentPresence() =>
     163            8 :       room.client.fetchCurrentPresence(id);
     164              : 
     165              :   /// Whether the client is able to ban/unban this user.
     166            6 :   bool get canBan => room.canBan && powerLevel < room.ownPowerLevel;
     167              : 
     168              :   /// Whether the client is able to kick this user.
     169            2 :   bool get canKick =>
     170            6 :       [Membership.join, Membership.invite].contains(membership) &&
     171            4 :       room.canKick &&
     172            0 :       powerLevel < room.ownPowerLevel;
     173              : 
     174            0 :   @Deprecated('Use [canChangeUserPowerLevel] instead.')
     175            0 :   bool get canChangePowerLevel => canChangeUserPowerLevel;
     176              : 
     177              :   /// Whether the client is allowed to change the power level of this user.
     178              :   /// Please be aware that you can only set the power level to at least your own!
     179            2 :   bool get canChangeUserPowerLevel =>
     180            4 :       room.canChangePowerLevel &&
     181           18 :       (powerLevel < room.ownPowerLevel || id == room.client.userID);
     182              : 
     183            1 :   @override
     184            1 :   bool operator ==(Object other) => (other is User &&
     185            3 :       other.id == id &&
     186            3 :       other.room == room &&
     187            3 :       other.membership == membership);
     188              : 
     189            0 :   @override
     190            0 :   int get hashCode => Object.hash(id, room, membership);
     191              : 
     192              :   /// Get the mention text to use in a plain text body to mention this specific user
     193              :   /// in this specific room
     194            2 :   String get mention {
     195              :     // if the displayname has [ or ] or : we can't build our more fancy stuff, so fall back to the id
     196              :     // [] is used for the delimitors
     197              :     // If we allowed : we could get collissions with the mxid fallbacks
     198            2 :     final displayName = this.displayName;
     199              :     if (displayName == null ||
     200            2 :         displayName.isEmpty ||
     201           10 :         {'[', ']', ':'}.any(displayName.contains)) {
     202            2 :       return id;
     203              :     }
     204              : 
     205              :     final identifier =
     206            8 :         '@${RegExp(r'^\w+$').hasMatch(displayName) ? displayName : '[$displayName]'}';
     207              : 
     208              :     // get all the users with the same display name
     209            4 :     final allUsersWithSameDisplayname = room.getParticipants();
     210            2 :     allUsersWithSameDisplayname.removeWhere(
     211            2 :       (user) =>
     212            6 :           user.id == id ||
     213            4 :           (user.displayName?.isEmpty ?? true) ||
     214            4 :           user.displayName != displayName,
     215              :     );
     216            2 :     if (allUsersWithSameDisplayname.isEmpty) {
     217              :       return identifier;
     218              :     }
     219              :     // ok, we have multiple users with the same display name....time to calculate a hash
     220            8 :     final hashes = allUsersWithSameDisplayname.map((u) => _hash(u.id));
     221            4 :     final ourHash = _hash(id);
     222              :     // hash collission...just return our own mxid again
     223            2 :     if (hashes.contains(ourHash)) {
     224            0 :       return id;
     225              :     }
     226            2 :     return '$identifier#$ourHash';
     227              :   }
     228              : 
     229              :   /// Get the mention fragments for this user.
     230            4 :   Set<String> get mentionFragments {
     231            4 :     final displayName = this.displayName;
     232              :     if (displayName == null ||
     233            4 :         displayName.isEmpty ||
     234           20 :         {'[', ']', ':'}.any(displayName.contains)) {
     235              :       return {};
     236              :     }
     237              :     final identifier =
     238           16 :         '@${RegExp(r'^\w+$').hasMatch(displayName) ? displayName : '[$displayName]'}';
     239              : 
     240            8 :     final hash = _hash(id);
     241            8 :     return {identifier, '$identifier#$hash'};
     242              :   }
     243              : }
     244              : 
     245              : const _maximumHashLength = 10000;
     246            4 : String _hash(String s) =>
     247           24 :     (s.codeUnits.fold<int>(0, (a, b) => a + b) % _maximumHashLength).toString();
     248              : 
     249              : extension FromStrippedStateEventExtension on StrippedStateEvent {
     250           66 :   User asUser(Room room) => User.fromState(
     251              :         // state key should always be set for member events
     252           33 :         stateKey: stateKey!,
     253           33 :         content: content,
     254           33 :         typeKey: type,
     255           33 :         senderId: senderId,
     256              :         room: room,
     257              :       );
     258              : }
        

Generated by: LCOV version 2.0-1