feat(chat): disallow links in messages
Local-first chat between people whose web of trust is still forming is a natural phishing/scam vector, so messages may not contain URLs. A conservative `containsUrl` rule (explicit scheme, www., or a common-TLD domain — but not "3.5kg" or "12.30") gates it: the composer warns and keeps the text for editing, and MessagesCubit.send drops any URL as a backstop. Incoming text was already non-tappable (plain selectable text).
This commit is contained in:
parent
d481e62ff7
commit
a461106dbf
14 changed files with 107 additions and 34 deletions
|
|
@ -4,16 +4,13 @@ import 'package:commons_core/commons_core.dart';
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../domain/message_rules.dart';
|
||||
import '../services/message_store.dart';
|
||||
|
||||
/// A 1:1 conversation with one peer. Holds the running message list plus send
|
||||
/// status. Transport-agnostic — depends on [MessageTransport], not the relay.
|
||||
class ChatState extends Equatable {
|
||||
const ChatState({
|
||||
this.messages = const [],
|
||||
this.sending = false,
|
||||
this.error,
|
||||
});
|
||||
const ChatState({this.messages = const [], this.sending = false, this.error});
|
||||
|
||||
/// Messages in this conversation, oldest first. Incoming ones arrive from the
|
||||
/// inbox; our own are appended optimistically on send (NIP-17 wraps don't come
|
||||
|
|
@ -26,12 +23,11 @@ class ChatState extends Equatable {
|
|||
List<PrivateMessage>? messages,
|
||||
bool? sending,
|
||||
String? Function()? error,
|
||||
}) =>
|
||||
ChatState(
|
||||
messages: messages ?? this.messages,
|
||||
sending: sending ?? this.sending,
|
||||
error: error != null ? error() : this.error,
|
||||
);
|
||||
}) => ChatState(
|
||||
messages: messages ?? this.messages,
|
||||
sending: sending ?? this.sending,
|
||||
error: error != null ? error() : this.error,
|
||||
);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [messages, sending, error];
|
||||
|
|
@ -47,9 +43,9 @@ class MessagesCubit extends Cubit<ChatState> {
|
|||
required this.selfPubkey,
|
||||
MessageStore? store,
|
||||
Future<void> Function()? onDispose,
|
||||
}) : _store = store,
|
||||
_onDispose = onDispose,
|
||||
super(const ChatState());
|
||||
}) : _store = store,
|
||||
_onDispose = onDispose,
|
||||
super(const ChatState());
|
||||
|
||||
final MessageTransport? _transport;
|
||||
final MessageStore? _store;
|
||||
|
|
@ -77,15 +73,12 @@ class MessagesCubit extends Cubit<ChatState> {
|
|||
Future<void> start() async {
|
||||
final transport = _transport;
|
||||
if (transport != null) {
|
||||
_sub = transport.inbox().listen(
|
||||
(message) async {
|
||||
if (message.fromPubkey != peerPubkey) return; // another conversation
|
||||
if (!_seen.add(_key(message))) return; // already shown / re-delivered
|
||||
await _store?.append(peerPubkey, message);
|
||||
emit(state.copyWith(messages: [...state.messages, message]));
|
||||
},
|
||||
onError: (Object e) => emit(state.copyWith(error: () => '$e')),
|
||||
);
|
||||
_sub = transport.inbox().listen((message) async {
|
||||
if (message.fromPubkey != peerPubkey) return; // another conversation
|
||||
if (!_seen.add(_key(message))) return; // already shown / re-delivered
|
||||
await _store?.append(peerPubkey, message);
|
||||
emit(state.copyWith(messages: [...state.messages, message]));
|
||||
}, onError: (Object e) => emit(state.copyWith(error: () => '$e')));
|
||||
}
|
||||
final history = await _store?.history(peerPubkey);
|
||||
if (history != null && history.isNotEmpty) {
|
||||
|
|
@ -102,7 +95,9 @@ class MessagesCubit extends Cubit<ChatState> {
|
|||
Future<void> send(String text) async {
|
||||
final transport = _transport;
|
||||
final trimmed = text.trim();
|
||||
if (transport == null || trimmed.isEmpty) return;
|
||||
// Links aren't allowed in messages (the UI warns; this is the backstop so
|
||||
// nothing sends a URL programmatically). See message_rules.dart.
|
||||
if (transport == null || trimmed.isEmpty || containsUrl(trimmed)) return;
|
||||
emit(state.copyWith(sending: true, error: () => null));
|
||||
try {
|
||||
await transport.send(toPubkey: peerPubkey, text: trimmed);
|
||||
|
|
@ -113,10 +108,7 @@ class MessagesCubit extends Cubit<ChatState> {
|
|||
);
|
||||
_seen.add(_key(mine));
|
||||
await _store?.append(peerPubkey, mine);
|
||||
emit(state.copyWith(
|
||||
sending: false,
|
||||
messages: [...state.messages, mine],
|
||||
));
|
||||
emit(state.copyWith(sending: false, messages: [...state.messages, mine]));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(sending: false, error: () => '$e'));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue