Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 55 additions & 31 deletions lib/src/room.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ const String messageSendingStatusKey =
const String fileSendingStatusKey =
'com.famedly.famedlysdk.file_sending_status';

// Matches full MXIDs (@user:server) and short forms (@room, @Name, @[Display Name]).
final RegExp _mentionCandidateRegex = RegExp(
r'(@[^\s:@]+:(?:[^\s]+\.\w+|[\d\.]+|\[[a-fA-F0-9:]+\])(?::\d+)?|@(?:\[[^\]:]+\]|\w+)(?:#\w+)?)',
);

/// Represents a Matrix room.
class Room {
/// The full qualified Matrix ID for the room in the format '!localid:server.abc'.
Expand Down Expand Up @@ -710,6 +715,43 @@ class Room {
.firstWhereOrNull((u) => u.mentionFragments.contains(mention))
?.id;

bool _hasMentionLeftBoundary(String message, int start) =>
start == 0 || message[start - 1].trim().isEmpty;

Map<String, dynamic> _mentionsMetadataForMessage(
String message, {
Event? inReplyTo,
}) {
final potentialMentions = <String>[];
var hasRoomMention = false;

for (final match in _mentionCandidateRegex.allMatches(message)) {
if (!_hasMentionLeftBoundary(message, match.start)) continue;

final mention = match[1]!;
if (mention == '@room') {
hasRoomMention = true;
continue;
}

final userId = mention.isValidMatrixId ? mention : getMention(mention);
if (userId != null) {
potentialMentions.add(userId);
}
}

// https://spec.matrix.org/v1.7/client-server-api/#mentioning-the-replied-to-user
if (inReplyTo != null) potentialMentions.add(inReplyTo.senderId);

final userIds = potentialMentions.toSet().toList()
..remove(client.userID); // We should never mention ourself.

return {
if (hasRoomMention) 'room': true,
if (userIds.isNotEmpty) 'user_ids': userIds,
};
}

/// Sends a normal text message to this room. Returns the event ID generated
/// by the server for this message.
Future<String?> sendTextEvent(
Expand Down Expand Up @@ -740,6 +782,7 @@ class Room {
txid: txid,
threadRootEventId: threadRootEventId,
threadLastEventId: threadLastEventId,
addMentions: addMentions,
stdout: commandStdout,
);
}
Expand All @@ -749,37 +792,8 @@ class Room {
};

if (addMentions) {
var potentialMentions = message
.split('@')
.map(
(text) => text.startsWith('[')
? '@${text.split(']').first}]'
: '@${text.split(RegExp(r'\s+')).first}',
)
.toList()
..removeAt(0);

final hasRoomMention = potentialMentions.remove('@room');

potentialMentions = potentialMentions
.map(
(mention) =>
mention.isValidMatrixId ? mention : getMention(mention),
)
.nonNulls
.toSet() // Deduplicate
.toList()
..remove(client.userID); // We should never mention ourself.

// https://spec.matrix.org/v1.7/client-server-api/#mentioning-the-replied-to-user
if (inReplyTo != null) potentialMentions.add(inReplyTo.senderId);

if (hasRoomMention || potentialMentions.isNotEmpty) {
event['m.mentions'] = {
if (hasRoomMention) 'room': true,
if (potentialMentions.isNotEmpty) 'user_ids': potentialMentions,
};
}
event['m.mentions'] =
_mentionsMetadataForMessage(message, inReplyTo: inReplyTo);
}

if (parseMarkdown) {
Expand Down Expand Up @@ -1151,6 +1165,14 @@ class Room {
}
}

void _ensureMentionsMetadata(Map<String, dynamic> content, String type) {
if (type != EventTypes.Message || content['body'] is! String) {
return;
}
// Include empty metadata to opt out of legacy body-scanning mentions.
content['m.mentions'] ??= <String, dynamic>{};
}

/// Sends an event to this room with this json as a content. Returns the
/// event ID generated from the server.
/// It uses list of completer to make sure events are sending in a row.
Expand Down Expand Up @@ -1230,6 +1252,8 @@ class Room {
};
}

_ensureMentionsMetadata(content, type);

if (editEventId != null) {
final newContent = content.copy();
content['m.new_content'] = newContent;
Expand Down
8 changes: 8 additions & 0 deletions lib/src/utils/commands_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ extension CommandsClientExtension on Client {
/// - `txid`: an optional transaction ID
/// - `threadRootEventId`: an optional root event ID of a thread the command is supposed to run on
/// - `threadLastEventId`: an optional most recent event ID of a thread the command is supposed to run on
/// - `addMentions`: whether text commands should resolve mention metadata
/// - `stdout`: an optional [StringBuffer] the command can write output to. This is meant as tiny implementation of https://en.wikipedia.org/wiki/Standard_streams in order to process advanced command output to the matrix client. See [DefaultCommandOutput] for a rough idea.
Future<String?> parseAndRunCommand(
Room? room,
Expand All @@ -53,6 +54,7 @@ extension CommandsClientExtension on Client {
String? txid,
String? threadRootEventId,
String? threadLastEventId,
bool addMentions = true,
StringBuffer? stdout,
}) async {
final args = CommandArgs(
Expand All @@ -64,6 +66,7 @@ extension CommandsClientExtension on Client {
txid: txid,
threadRootEventId: threadRootEventId,
threadLastEventId: threadLastEventId,
addMentions: addMentions,
);
if (!msg.startsWith('/')) {
final sendCommand = commands['send'];
Expand Down Expand Up @@ -118,6 +121,7 @@ extension CommandsClientExtension on Client {
txid: args.txid,
threadRootEventId: args.threadRootEventId,
threadLastEventId: args.threadLastEventId,
addMentions: args.addMentions,
);
});
addCommand('me', (args, stdout) async {
Expand All @@ -134,6 +138,7 @@ extension CommandsClientExtension on Client {
txid: args.txid,
threadRootEventId: args.threadRootEventId,
threadLastEventId: args.threadLastEventId,
addMentions: args.addMentions,
);
});
addCommand('dm', (args, stdout) async {
Expand Down Expand Up @@ -182,6 +187,7 @@ extension CommandsClientExtension on Client {
txid: args.txid,
threadRootEventId: args.threadRootEventId,
threadLastEventId: args.threadLastEventId,
addMentions: args.addMentions,
);
});
addCommand('html', (args, stdout) async {
Expand Down Expand Up @@ -490,6 +496,7 @@ class CommandArgs {
String? txid;
String? threadRootEventId;
String? threadLastEventId;
bool addMentions;

CommandArgs({
required this.msg,
Expand All @@ -500,6 +507,7 @@ class CommandArgs {
this.txid,
this.threadRootEventId,
this.threadLastEventId,
this.addMentions = true,
});
}

Expand Down
21 changes: 20 additions & 1 deletion lib/src/utils/pushrule_evaluator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ enum PushRuleConditions {
}
}

// Legacy default mention rules from pre-v1.17 specs. If m.mentions is present,
// they should not fire, but custom user content rules are unaffected.
const Set<String> _legacyMentionRuleIds = {
'.m.rule.contains_user_name',
'.m.rule.contains_display_name',
'.m.rule.roomnotif',
};

class EvaluatedPushRuleAction {
// if this message should be highlighted.
bool highlight = false;
Expand Down Expand Up @@ -220,14 +228,18 @@ class _MemberCountCondition {
}

class _OptimizedRules {
final String ruleId;
final bool defaultRule;
List<_PatternCondition> patterns = [];
List<_EventPropertyCondition> eventProperties = [];
List<_MemberCountCondition> memberCounts = [];
List<String> notificationPermissions = [];
bool matchDisplayname = false;
EvaluatedPushRuleAction actions = EvaluatedPushRuleAction();

_OptimizedRules.fromRule(PushRule rule) {
_OptimizedRules.fromRule(PushRule rule)
: ruleId = rule.ruleId,
defaultRule = rule.default$ {
if (!rule.enabled) return;

for (final condition in rule.conditions ?? <PushCondition>[]) {
Expand Down Expand Up @@ -260,6 +272,9 @@ class _OptimizedRules {
actions = EvaluatedPushRuleAction.fromActions(rule.actions);
}

bool get isLegacyMentionRule =>
defaultRule && _legacyMentionRuleIds.contains(ruleId);

EvaluatedPushRuleAction? match(
Map<String, Object?> flattenedEventJson,
String? displayName,
Expand Down Expand Up @@ -388,11 +403,13 @@ class PushruleEvaluator {
final displayName = event.room
.unsafeGetUserFromMemoryOrFallback(event.room.client.userID!)
.displayName;
final hasMentionsMetadata = event.content.containsKey('m.mentions');
final flattenedEventJson = _flattenJson(event.toJson(), {}, '');
// ensure roomid is present
flattenedEventJson['room_id'] = event.room.id;

for (final o in _override) {
if (hasMentionsMetadata && o.isLegacyMentionRule) continue;
final actions =
o.match(flattenedEventJson, displayName, memberCount, event.room);
if (actions != null) {
Expand All @@ -411,6 +428,7 @@ class PushruleEvaluator {
}

for (final o in _content_rules) {
if (hasMentionsMetadata && o.isLegacyMentionRule) continue;
final actions =
o.match(flattenedEventJson, displayName, memberCount, event.room);
if (actions != null) {
Expand All @@ -419,6 +437,7 @@ class PushruleEvaluator {
}

for (final o in _underride) {
if (hasMentionsMetadata && o.isLegacyMentionRule) continue;
final actions =
o.match(flattenedEventJson, displayName, memberCount, event.room);
if (actions != null) {
Expand Down
Loading
Loading