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

View File

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:get_it/get_it.dart';
import 'package:logging/logging.dart';
import 'package:moxxmpp/moxxmpp.dart';
import 'package:moxxyv2/service/database/database.dart';
import 'package:moxxyv2/service/service.dart';
import 'package:moxxyv2/shared/events.dart';
@ -9,35 +12,93 @@ enum BlockPushType {
}
class BlocklistService {
BlocklistService();
List<String>? _blocklist;
bool _requested = false;
bool? _supported;
final Logger _log = Logger('BlocklistService');
BlocklistService() :
_blocklistCache = List.empty(growable: true),
_requestedBlocklist = false;
final List<String> _blocklistCache;
bool _requestedBlocklist;
void onNewConnection() {
// Invalidate the caches
_blocklist = null;
_requested = false;
_supported = null;
}
Future<List<String>> _requestBlocklist() async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
_blocklistCache
..clear()
..addAll(await manager.getBlocklist());
_requestedBlocklist = true;
return _blocklistCache;
Future<bool> _checkSupport() async {
return _supported ??= await GetIt.I.get<XmppConnection>()
.getManagerById<BlockingManager>(blockingManager)!
.isSupported();
}
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
Future<List<String>> getBlocklist() async {
if (!_requestedBlocklist) {
_blocklistCache
..clear()
..addAll(await _requestBlocklist());
if (_blocklist == null) {
_blocklist = await GetIt.I.get<DatabaseService>().getBlocklistEntries();
if (!_requested) {
unawaited(_requestBlocklist());
}
return _blocklist!;
}
return _blocklistCache;
if (!_requested) {
unawaited(_requestBlocklist());
}
return _blocklist!;
}
void onUnblockAllPush() {
_blocklistCache.clear();
_blocklist = List<String>.empty(growable: true);
sendEvent(
BlocklistUnblockAllEvent(),
);
@ -45,21 +106,25 @@ class BlocklistService {
Future<void> onBlocklistPush(BlockPushType type, List<String> items) async {
// We will fetch it later when getBlocklist is called
if (!_requestedBlocklist) return;
if (!_requested) return;
final newBlocks = List<String>.empty(growable: true);
final removedBlocks = List<String>.empty(growable: true);
for (final item in items) {
switch (type) {
case BlockPushType.block: {
if (_blocklistCache.contains(item)) continue;
_blocklistCache.add(item);
if (_blocklist!.contains(item)) continue;
_blocklist!.add(item);
newBlocks.add(item);
await GetIt.I.get<DatabaseService>().addBlocklistEntry(item);
}
break;
case BlockPushType.unblock: {
_blocklistCache.removeWhere((i) => i == item);
_blocklist!.removeWhere((i) => i == item);
removedBlocks.add(item);
await GetIt.I.get<DatabaseService>().removeBlocklistEntry(item);
}
break;
}
@ -74,17 +139,47 @@ class BlocklistService {
}
Future<bool> blockJid(String jid) async {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
return manager.block([ jid ]);
// Check if blocking is supported
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 {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
return manager.unblock([ jid ]);
// Check if blocking is supported
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 {
final manager = GetIt.I.get<XmppConnection>().getManagerById<BlockingManager>(blockingManager)!;
return manager.unblockAll();
// Check if blocking is supported
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 stickersTable = 'Stickers';
const stickerPacksTable = 'StickerPacks';
const blocklistTable = 'Blocklist';
const typeString = 0;
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
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/creation.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_avatar.dart';
import 'package:moxxyv2/service/database/migrations/0000_contacts_integration_pseudo.dart';
@ -79,7 +80,7 @@ class DatabaseService {
_db = await openDatabase(
dbPath,
password: key,
version: 22,
version: 23,
onCreate: createDatabase,
onConfigure: (db) async {
// 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');
await upgradeFromV21ToV22(db);
}
if (oldVersion < 23) {
_log.finest('Running migration for database version 23');
await upgradeFromV22ToV23(db);
}
},
);
@ -1257,4 +1262,35 @@ class DatabaseService {
.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<FetchStickerPackCommand>(performFetchStickerPack),
EventTypeMatcher<InstallStickerPackCommand>(performStickerPackInstall),
EventTypeMatcher<GetBlocklistCommand>(performGetBlocklist),
]);
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.
final file = await File(downloadedPath).create();
final fileSink = file.openWrite(mode: FileMode.writeOnlyAppend);
final downloadCompleter = Completer();
final downloadCompleter = Completer<void>();
dio.Response<dio.ResponseBody>? response;

View File

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

View File

@ -1,7 +1,11 @@
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:get_it/get_it.dart';
import 'package:moxplatform/moxplatform.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_event.dart';
@ -9,11 +13,44 @@ part 'blocklist_state.dart';
class BlocklistBloc extends Bloc<BlocklistEvent, BlocklistState> {
BlocklistBloc() : super(BlocklistState()) {
on<BlocklistRequestedEvent>(_onBlocklistRequested);
on<UnblockedJidEvent>(_onJidUnblocked);
on<UnblockedAllEvent>(_onUnblockedAll);
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 {
await MoxplatformPlugin.handler.getDataSender().sendData(
UnblockJidCommand(

View File

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

View File

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

View File

@ -23,59 +23,74 @@ class BlocklistPage extends StatelessWidget {
Widget _buildListView(BlocklistState state) {
// ignore: non_bool_condition,avoid_dynamic_calls
if (state.blocklist.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: paddingVeryLarge),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 8),
child: Image.asset('assets/images/happy_news.png'),
return Column(
children: [
if (state.isWorking)
const LinearProgressIndicator(),
Padding(
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(
itemCount: state.blocklist.length,
itemBuilder: (BuildContext context, int index) {
// ignore: avoid_dynamic_calls
final jid = state.blocklist[index];
return Column(
children: [
if (state.isWorking)
const LinearProgressIndicator(),
return Padding(
padding: const EdgeInsets.symmetric(
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,
);
ListView.builder(
shrinkWrap: true,
itemCount: state.blocklist.length,
itemBuilder: (BuildContext context, int index) {
// ignore: avoid_dynamic_calls
final jid = state.blocklist[index];
if (result) {
// ignore: use_build_context_synchronously
context.read<BlocklistBloc>().add(UnblockedJidEvent(jid));
}
},
return Padding(
padding: const EdgeInsets.symmetric(
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),
itemBuilder: (BuildContext context) => [
PopupMenuItem(
enabled: state.blocklist.isNotEmpty,
value: BlocklistOptions.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:get_it/get_it.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/constants.dart';
import 'package:moxxyv2/ui/helpers.dart';
@ -76,7 +77,9 @@ class SettingsPage extends StatelessWidget {
child: Icon(Icons.block),
),
onTap: () {
Navigator.pushNamed(context, blocklistRoute);
GetIt.I.get<BlocklistBloc>().add(
BlocklistRequestedEvent(),
);
},
),
SettingsRow(