Compare commits

..

2 Commits

Author SHA1 Message Date
76a03cc2fa feat(service): Rework the blocklist service
Maybe fixes #14.
2022-12-25 01:25:12 +01:00
3774760548 fix(service): Fix missing type 2022-12-24 22:33:54 +01:00
14 changed files with 321 additions and 81 deletions

View File

@ -106,6 +106,13 @@ files:
extends: BackgroundEvent extends: BackgroundEvent
implements: implements:
- JsonImplementation - JsonImplementation
# Triggered in response to a [GetBlocklistCommand]
- name: GetBlocklistResultEvent
extends: BackgroundEvent
implements:
- JsonImplementation
attributes:
entries: List<String>
# Triggered by DownloadService or UploadService. # Triggered by DownloadService or UploadService.
- name: ProgressEvent - name: ProgressEvent
extends: BackgroundEvent extends: BackgroundEvent
@ -527,6 +534,11 @@ files:
stickerPack: stickerPack:
type: StickerPack type: StickerPack
deserialise: true deserialise: true
- name: GetBlocklistCommand
extends: BackgroundCommand
implements:
- JsonImplementation
attributes:
generate_builder: true generate_builder: true
# get${builder_Name}FromJson # get${builder_Name}FromJson
builder_name: "Command" builder_name: "Command"

View File

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/service.dart'; import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/events.dart';
@ -9,35 +12,93 @@ enum BlockPushType {
} }
class BlocklistService { class BlocklistService {
BlocklistService();
List<String>? _blocklist;
bool _requested = false;
bool? _supported;
final Logger _log = Logger('BlocklistService');
BlocklistService() : void onNewConnection() {
_blocklistCache = List.empty(growable: true), // Invalidate the caches
_requestedBlocklist = false; _blocklist = null;
final List<String> _blocklistCache; _requested = false;
bool _requestedBlocklist; _supported = null;
}
Future<List<String>> _requestBlocklist() async { Future<bool> _checkSupport() async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!; return _supported ??= await GetIt.I.get<XmppConnection>()
_blocklistCache .getManagerById<BlockingManager>(blockingManager)!
..clear() .isSupported();
..addAll(await manager.getBlocklist()); }
_requestedBlocklist = true;
return _blocklistCache; Future<void> _requestBlocklist() async {
assert(_blocklist != null, 'The blocklist must be loaded from the database before requesting');
// Check if blocking is supported
if (!(await _checkSupport())) {
_log.warning('Blocklist requested but server does not support it.');
return;
}
final blocklist = await GetIt.I.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.getBlocklist();
// Diff the received blocklist with the cache
final newItems = List<String>.empty(growable: true);
final removedItems = List<String>.empty(growable: true);
final db = GetIt.I.get<DatabaseService>();
for (final item in blocklist) {
if (!_blocklist!.contains(item)) {
await db.addBlocklistEntry(item);
_blocklist!.add(item);
newItems.add(item);
}
}
// Diff the cache with the received blocklist
for (final item in _blocklist!) {
if (!blocklist.contains(item)) {
await db.removeBlocklistEntry(item);
_blocklist!.remove(item);
removedItems.add(item);
}
}
_requested = true;
// Trigger an UI event if we have anything to tell the UI
if (newItems.isNotEmpty || removedItems.isNotEmpty) {
sendEvent(
BlocklistPushEvent(
added: newItems,
removed: removedItems,
),
);
}
} }
/// Returns the blocklist from the database /// Returns the blocklist from the database
Future<List<String>> getBlocklist() async { Future<List<String>> getBlocklist() async {
if (!_requestedBlocklist) { if (_blocklist == null) {
_blocklistCache _blocklist = await GetIt.I.get<DatabaseService>().getBlocklistEntries();
..clear()
..addAll(await _requestBlocklist()); if (!_requested) {
unawaited(_requestBlocklist());
}
return _blocklist!;
} }
return _blocklistCache; if (!_requested) {
unawaited(_requestBlocklist());
}
return _blocklist!;
} }
void onUnblockAllPush() { void onUnblockAllPush() {
_blocklistCache.clear(); _blocklist = List<String>.empty(growable: true);
sendEvent( sendEvent(
BlocklistUnblockAllEvent(), BlocklistUnblockAllEvent(),
); );
@ -45,21 +106,25 @@ class BlocklistService {
Future<void> onBlocklistPush(BlockPushType type, List<String> items) async { Future<void> onBlocklistPush(BlockPushType type, List<String> items) async {
// We will fetch it later when getBlocklist is called // We will fetch it later when getBlocklist is called
if (!_requestedBlocklist) return; if (!_requested) return;
final newBlocks = List<String>.empty(growable: true); final newBlocks = List<String>.empty(growable: true);
final removedBlocks = List<String>.empty(growable: true); final removedBlocks = List<String>.empty(growable: true);
for (final item in items) { for (final item in items) {
switch (type) { switch (type) {
case BlockPushType.block: { case BlockPushType.block: {
if (_blocklistCache.contains(item)) continue; if (_blocklist!.contains(item)) continue;
_blocklistCache.add(item); _blocklist!.add(item);
newBlocks.add(item); newBlocks.add(item);
await GetIt.I.get<DatabaseService>().addBlocklistEntry(item);
} }
break; break;
case BlockPushType.unblock: { case BlockPushType.unblock: {
_blocklistCache.removeWhere((i) => i == item); _blocklist!.removeWhere((i) => i == item);
removedBlocks.add(item); removedBlocks.add(item);
await GetIt.I.get<DatabaseService>().removeBlocklistEntry(item);
} }
break; break;
} }
@ -74,17 +139,47 @@ class BlocklistService {
} }
Future<bool> blockJid(String jid) async { Future<bool> blockJid(String jid) async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!; // Check if blocking is supported
return manager.block([ jid ]); if (!(await _checkSupport())) {
_log.warning('Blocking $jid requested but server does not support it.');
return false;
}
_blocklist!.add(jid);
await GetIt.I.get<DatabaseService>()
.addBlocklistEntry(jid);
return GetIt.I.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.block([jid]);
} }
Future<bool> unblockJid(String jid) async { Future<bool> unblockJid(String jid) async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!; // Check if blocking is supported
return manager.unblock([ jid ]); if (!(await _checkSupport())) {
_log.warning('Unblocking $jid requested but server does not support it.');
return false;
}
_blocklist!.remove(jid);
await GetIt.I.get<DatabaseService>()
.removeBlocklistEntry(jid);
return GetIt.I.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.unblock([jid]);
} }
Future<bool> unblockAll() async { Future<bool> unblockAll() async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!; // Check if blocking is supported
return manager.unblockAll(); if (!(await _checkSupport())) {
_log.warning('Unblocking all JIDs requested but server does not support it.');
return false;
}
_blocklist!.clear();
await GetIt.I.get<DatabaseService>()
.removeAllBlocklistEntries();
return GetIt.I.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.unblockAll();
} }
} }

View File

@ -14,6 +14,7 @@ const xmppStateTable = 'XmppState';
const contactsTable = 'Contacts'; const contactsTable = 'Contacts';
const stickersTable = 'Stickers'; const stickersTable = 'Stickers';
const stickerPacksTable = 'StickerPacks'; const stickerPacksTable = 'StickerPacks';
const blocklistTable = 'Blocklist';
const typeString = 0; const typeString = 0;
const typeInt = 1; const typeInt = 1;

View File

@ -159,6 +159,15 @@ Future<void> createDatabase(Database db, int version) async {
)''', )''',
); );
// Blocklist
await db.execute(
'''
CREATE TABLE $blocklistTable (
jid TEXT PRIMARY KEY
);
''',
);
// OMEMO // OMEMO
await db.execute( await db.execute(
''' '''

View File

@ -8,6 +8,7 @@ import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/constants.dart'; import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/service/database/creation.dart'; import 'package:moxxyv2/service/database/creation.dart';
import 'package:moxxyv2/service/database/helpers.dart'; import 'package:moxxyv2/service/database/helpers.dart';
import 'package:moxxyv2/service/database/migrations/0000_blocklist.dart';
import 'package:moxxyv2/service/database/migrations/0000_contacts_integration.dart'; import 'package:moxxyv2/service/database/migrations/0000_contacts_integration.dart';
import 'package:moxxyv2/service/database/migrations/0000_contacts_integration_avatar.dart'; import 'package:moxxyv2/service/database/migrations/0000_contacts_integration_avatar.dart';
import 'package:moxxyv2/service/database/migrations/0000_contacts_integration_pseudo.dart'; import 'package:moxxyv2/service/database/migrations/0000_contacts_integration_pseudo.dart';
@ -79,7 +80,7 @@ class DatabaseService {
_db = await openDatabase( _db = await openDatabase(
dbPath, dbPath,
password: key, password: key,
version: 22, version: 23,
onCreate: createDatabase, onCreate: createDatabase,
onConfigure: (db) async { onConfigure: (db) async {
// In order to do schema changes during database upgrades, we disable foreign // In order to do schema changes during database upgrades, we disable foreign
@ -176,6 +177,10 @@ class DatabaseService {
_log.finest('Running migration for database version 22'); _log.finest('Running migration for database version 22');
await upgradeFromV21ToV22(db); await upgradeFromV21ToV22(db);
} }
if (oldVersion < 23) {
_log.finest('Running migration for database version 23');
await upgradeFromV22ToV23(db);
}
}, },
); );
@ -1257,4 +1262,35 @@ class DatabaseService {
.toList(), .toList(),
); );
} }
Future<void> addBlocklistEntry(String jid) async {
await _db.insert(
blocklistTable,
{
'jid': jid,
},
);
}
Future<void> removeBlocklistEntry(String jid) async {
await _db.delete(
blocklistTable,
where: 'jid = ?',
whereArgs: [jid],
);
}
Future<void> removeAllBlocklistEntries() async {
await _db.delete(
blocklistTable,
);
}
Future<List<String>> getBlocklistEntries() async {
final result = await _db.query(blocklistTable);
return result
.map((m) => m['jid']! as String)
.toList();
}
} }

View File

@ -0,0 +1,13 @@
import 'package:moxxyv2/service/database/constants.dart';
import 'package:moxxyv2/shared/models/preference.dart';
import 'package:sqflite_sqlcipher/sqflite.dart';
Future<void> upgradeFromV22ToV23(Database db) async {
await db.execute(
'''
CREATE TABLE $blocklistTable (
jid TEXT PRIMARY KEY
);
''',
);
}

View File

@ -79,6 +79,7 @@ void setupBackgroundEventHandler() {
EventTypeMatcher<RemoveStickerPackCommand>(performRemoveStickerPack), EventTypeMatcher<RemoveStickerPackCommand>(performRemoveStickerPack),
EventTypeMatcher<FetchStickerPackCommand>(performFetchStickerPack), EventTypeMatcher<FetchStickerPackCommand>(performFetchStickerPack),
EventTypeMatcher<InstallStickerPackCommand>(performStickerPackInstall), EventTypeMatcher<InstallStickerPackCommand>(performStickerPackInstall),
EventTypeMatcher<GetBlocklistCommand>(performGetBlocklist),
]); ]);
GetIt.I.registerSingleton<EventHandler>(handler); GetIt.I.registerSingleton<EventHandler>(handler);
@ -890,3 +891,15 @@ Future<void> performStickerPackInstall(InstallStickerPackCommand command, { dyna
); );
} }
} }
Future<void> performGetBlocklist(GetBlocklistCommand command, { dynamic extra }) async {
final id = extra as String;
final result = await GetIt.I.get<BlocklistService>().getBlocklist();
sendEvent(
GetBlocklistResultEvent(
entries: result,
),
id: id,
);
}

View File

@ -361,7 +361,7 @@ class HttpFileTransferService {
// Prepare file and completer. // Prepare file and completer.
final file = await File(downloadedPath).create(); final file = await File(downloadedPath).create();
final fileSink = file.openWrite(mode: FileMode.writeOnlyAppend); final fileSink = file.openWrite(mode: FileMode.writeOnlyAppend);
final downloadCompleter = Completer(); final downloadCompleter = Completer<void>();
dio.Response<dio.ResponseBody>? response; dio.Response<dio.ResponseBody>? response;

View File

@ -665,6 +665,9 @@ class XmppService {
unawaited(_initializeOmemoService(settings.jid.toString())); unawaited(_initializeOmemoService(settings.jid.toString()));
if (!event.resumed) { if (!event.resumed) {
// Reset the blocking service's cache
GetIt.I.get<BlocklistService>().onNewConnection();
// Enable carbons // Enable carbons
final carbonsResult = await connection final carbonsResult = await connection
.getManagerById<CarbonsManager>(carbonsManager)! .getManagerById<CarbonsManager>(carbonsManager)!

View File

@ -1,7 +1,11 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform/moxplatform.dart';
import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/commands.dart';
import 'package:moxxyv2/shared/events.dart';
import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
import 'package:moxxyv2/ui/constants.dart';
part 'blocklist_bloc.freezed.dart'; part 'blocklist_bloc.freezed.dart';
part 'blocklist_event.dart'; part 'blocklist_event.dart';
@ -9,11 +13,44 @@ part 'blocklist_state.dart';
class BlocklistBloc extends Bloc<BlocklistEvent, BlocklistState> { class BlocklistBloc extends Bloc<BlocklistEvent, BlocklistState> {
BlocklistBloc() : super(BlocklistState()) { BlocklistBloc() : super(BlocklistState()) {
on<BlocklistRequestedEvent>(_onBlocklistRequested);
on<UnblockedJidEvent>(_onJidUnblocked); on<UnblockedJidEvent>(_onJidUnblocked);
on<UnblockedAllEvent>(_onUnblockedAll); on<UnblockedAllEvent>(_onUnblockedAll);
on<BlocklistPushedEvent>(_onBlocklistPushed); on<BlocklistPushedEvent>(_onBlocklistPushed);
} }
Future<void> _onBlocklistRequested(BlocklistRequestedEvent event, Emitter<BlocklistState> emit) async {
final mustDoWork = state.blocklist.isEmpty;
if (mustDoWork) {
emit(
state.copyWith(
isWorking: true,
),
);
}
GetIt.I.get<NavigationBloc>().add(
PushedNamedEvent(
const NavigationDestination(blocklistRoute),
),
);
if (state.blocklist.isEmpty) {
// ignore: cast_nullable_to_non_nullable
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
GetBlocklistCommand(),
) as GetBlocklistResultEvent;
emit(
state.copyWith(
blocklist: result.entries,
isWorking: false,
),
);
}
}
Future<void> _onJidUnblocked(UnblockedJidEvent event, Emitter<BlocklistState> emit) async { Future<void> _onJidUnblocked(UnblockedJidEvent event, Emitter<BlocklistState> emit) async {
await MoxplatformPlugin.handler.getDataSender().sendData( await MoxplatformPlugin.handler.getDataSender().sendData(
UnblockJidCommand( UnblockJidCommand(

View File

@ -2,9 +2,11 @@ part of 'blocklist_bloc.dart';
abstract class BlocklistEvent {} abstract class BlocklistEvent {}
/// Triggered when the blocklist page has been requested
class BlocklistRequestedEvent extends BlocklistEvent {}
/// Triggered when a JID is unblocked /// Triggered when a JID is unblocked
class UnblockedJidEvent extends BlocklistEvent { class UnblockedJidEvent extends BlocklistEvent {
UnblockedJidEvent(this.jid); UnblockedJidEvent(this.jid);
final String jid; final String jid;
} }
@ -16,7 +18,6 @@ class UnblockedAllEvent extends BlocklistEvent {
/// Triggered when we receive a blocklist push /// Triggered when we receive a blocklist push
class BlocklistPushedEvent extends BlocklistEvent { class BlocklistPushedEvent extends BlocklistEvent {
BlocklistPushedEvent(this.added, this.removed); BlocklistPushedEvent(this.added, this.removed);
final List<String> added; final List<String> added;
final List<String> removed; final List<String> removed;

View File

@ -4,5 +4,6 @@ part of 'blocklist_bloc.dart';
class BlocklistState with _$BlocklistState { class BlocklistState with _$BlocklistState {
factory BlocklistState({ factory BlocklistState({
@Default(<String>[]) List<String> blocklist, @Default(<String>[]) List<String> blocklist,
@Default(false) bool isWorking,
}) = _BlocklistState; }) = _BlocklistState;
} }

View File

@ -23,59 +23,74 @@ class BlocklistPage extends StatelessWidget {
Widget _buildListView(BlocklistState state) { Widget _buildListView(BlocklistState state) {
// ignore: non_bool_condition,avoid_dynamic_calls // ignore: non_bool_condition,avoid_dynamic_calls
if (state.blocklist.isEmpty) { if (state.blocklist.isEmpty) {
return Padding( return Column(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge), children: [
child: Column( if (state.isWorking)
children: [ const LinearProgressIndicator(),
Padding(
padding: const EdgeInsets.only(top: 8), Padding(
child: Image.asset('assets/images/happy_news.png'), padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8),
child: Image.asset('assets/images/happy_news.png'),
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(t.pages.blocklist.noUsersBlocked),
)
],
), ),
Padding( ),
padding: const EdgeInsets.only(top: 8), ],
child: Text(t.pages.blocklist.noUsersBlocked),
)
],
),
); );
} }
return ListView.builder( return Column(
itemCount: state.blocklist.length, children: [
itemBuilder: (BuildContext context, int index) { if (state.isWorking)
// ignore: avoid_dynamic_calls const LinearProgressIndicator(),
final jid = state.blocklist[index];
return Padding( ListView.builder(
padding: const EdgeInsets.symmetric( shrinkWrap: true,
horizontal: 32, itemCount: state.blocklist.length,
vertical: 16, itemBuilder: (BuildContext context, int index) {
), // ignore: avoid_dynamic_calls
child: Row( final jid = state.blocklist[index];
children: [
Expanded(
child: Text(jid),
),
IconButton(
icon: const Icon(Icons.delete),
color: Colors.red,
onPressed: () async {
final result = await showConfirmationDialog(
t.pages.blocklist.unblockJidConfirmTitle(jid: jid),
t.pages.blocklist.unblockJidConfirmBody(jid: jid),
context,
);
if (result) { return Padding(
// ignore: use_build_context_synchronously padding: const EdgeInsets.symmetric(
context.read<BlocklistBloc>().add(UnblockedJidEvent(jid)); horizontal: 32,
} vertical: 16,
},
), ),
], child: Row(
), children: [
); Expanded(
}, child: Text(jid),
),
IconButton(
icon: const Icon(Icons.delete),
color: Colors.red,
onPressed: () async {
final result = await showConfirmationDialog(
t.pages.blocklist.unblockJidConfirmTitle(jid: jid),
t.pages.blocklist.unblockJidConfirmBody(jid: jid),
context,
);
if (result) {
// ignore: use_build_context_synchronously
context.read<BlocklistBloc>().add(UnblockedJidEvent(jid));
}
},
),
],
),
);
},
),
],
); );
} }
@ -108,6 +123,7 @@ class BlocklistPage extends StatelessWidget {
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
itemBuilder: (BuildContext context) => [ itemBuilder: (BuildContext context) => [
PopupMenuItem( PopupMenuItem(
enabled: state.blocklist.isNotEmpty,
value: BlocklistOptions.unblockAll, value: BlocklistOptions.unblockAll,
child: Text(t.pages.blocklist.unblockAll), child: Text(t.pages.blocklist.unblockAll),
), ),

View File

@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:moxxyv2/i18n/strings.g.dart'; import 'package:moxxyv2/i18n/strings.g.dart';
import 'package:moxxyv2/ui/bloc/blocklist_bloc.dart';
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart'; import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
import 'package:moxxyv2/ui/constants.dart'; import 'package:moxxyv2/ui/constants.dart';
import 'package:moxxyv2/ui/helpers.dart'; import 'package:moxxyv2/ui/helpers.dart';
@ -76,7 +77,9 @@ class SettingsPage extends StatelessWidget {
child: Icon(Icons.block), child: Icon(Icons.block),
), ),
onTap: () { onTap: () {
Navigator.pushNamed(context, blocklistRoute); GetIt.I.get<BlocklistBloc>().add(
BlocklistRequestedEvent(),
);
}, },
), ),
SettingsRow( SettingsRow(