LCOV - code coverage report
Current view: top level - lib/src - user.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 87 99 87.9 %
Date: 2024-11-12 07:37:08 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 1.14