Add domain blocking capability to low level filters

codemagic-setup
Hank Grabowski 2023-05-05 14:18:49 -04:00
rodzic ed1b800e82
commit 5eb7dcf7fe
4 zmienionych plików z 120 dodań i 17 usunięć

Wyświetl plik

@ -1,6 +1,7 @@
enum ComparisonType {
containsCaseSensitive,
containsCaseInsensitive,
contains,
containsIgnoreCase,
endsWithIgnoreCase,
equals,
equalsIgnoreCase,
;

Wyświetl plik

@ -18,6 +18,7 @@ class TimelineEntryFilter {
final TimelineEntryFilterAction action;
final String name;
final List<StringFilter> authorFilters;
final List<StringFilter> domainFilters;
final List<StringFilter> contentFilters;
final List<StringFilter> hashtagFilters;
@ -25,6 +26,7 @@ class TimelineEntryFilter {
required this.action,
required this.name,
required this.authorFilters,
required this.domainFilters,
required this.contentFilters,
required this.hashtagFilters,
});
@ -33,6 +35,7 @@ class TimelineEntryFilter {
required TimelineEntryFilterAction action,
required String name,
List<Connection> authors = const [],
List<String> domains = const [],
List<String> keywords = const [],
List<String> hashtags = const [],
}) {
@ -43,9 +46,20 @@ class TimelineEntryFilter {
.map((a) =>
StringFilter(filterString: a.id, type: ComparisonType.equals))
.toList(),
domainFilters: domains
.map((d) => d.startsWith('*')
? StringFilter(
filterString: d.substring(1),
type: ComparisonType.endsWithIgnoreCase,
)
: StringFilter(
filterString: d,
type: ComparisonType.equalsIgnoreCase,
))
.toList(),
contentFilters: keywords
.map((k) => StringFilter(
filterString: k, type: ComparisonType.containsCaseInsensitive))
filterString: k, type: ComparisonType.containsIgnoreCase))
.toList(),
hashtagFilters: hashtags
.map((h) => StringFilter(
@ -58,6 +72,7 @@ class TimelineEntryFilter {
'action': action.name,
'name': name,
'authorFilters': authorFilters.map((f) => f.toJson()),
'domainFilters': domainFilters.map((f) => f.toJson()),
'contentFilters': contentFilters.map((f) => f.toJson()),
'hashtagFilters': hashtagFilters.map((f) => f.toJson()),
};
@ -69,6 +84,9 @@ class TimelineEntryFilter {
authorFilters: (json['authorFilters'] as List<dynamic>)
.map((json) => StringFilter.fromJson(json))
.toList(),
domainFilters: (json['domainFilters'] as List<dynamic>)
.map((json) => StringFilter.fromJson(json))
.toList(),
contentFilters: (json['contentFilters'] as List<dynamic>)
.map((json) => StringFilter.fromJson(json))
.toList(),

Wyświetl plik

@ -46,14 +46,16 @@ FilterResult runFilters(
extension StringFilterOps on StringFilter {
bool isFiltered(String value) {
switch (type) {
case ComparisonType.containsCaseSensitive:
case ComparisonType.contains:
return value.contains(filterString);
case ComparisonType.containsCaseInsensitive:
case ComparisonType.containsIgnoreCase:
return value.toLowerCase().contains(filterString.toLowerCase());
case ComparisonType.equals:
return value == filterString;
case ComparisonType.equalsIgnoreCase:
return value.toLowerCase() == filterString.toLowerCase();
case ComparisonType.endsWithIgnoreCase:
return value.toLowerCase().endsWith(filterString.toLowerCase());
}
}
}
@ -61,6 +63,7 @@ extension StringFilterOps on StringFilter {
extension TimelineEntryFilterOps on TimelineEntryFilter {
bool isFiltered(TimelineEntry entry) {
if (authorFilters.isEmpty &&
domainFilters.isEmpty &&
hashtagFilters.isEmpty &&
contentFilters.isEmpty) {
return false;
@ -84,6 +87,16 @@ extension TimelineEntryFilterOps on TimelineEntryFilter {
}
}
var domainFiltered = domainFilters.isEmpty ? true : false;
for (final filter in domainFilters) {
final domain =
Uri.tryParse(entry.externalLink)?.host ?? entry.externalLink;
if (filter.isFiltered(domain)) {
domainFiltered = true;
break;
}
}
var contentFiltered = contentFilters.isEmpty ? true : false;
for (final filter in contentFilters) {
if (filter.isFiltered(entry.body)) {
@ -92,6 +105,9 @@ extension TimelineEntryFilterOps on TimelineEntryFilter {
}
}
return authorFiltered && hashtagFiltered && contentFiltered;
return authorFiltered &&
domainFiltered &&
hashtagFiltered &&
contentFiltered;
}
}

Wyświetl plik

@ -7,13 +7,47 @@ import 'package:relatica/utils/filter_runner.dart';
void main() {
final entries = [
TimelineEntry(body: 'Hello world', authorId: '1', tags: ['greeting']),
TimelineEntry(body: 'Goodbye', authorId: '1', tags: ['SendOff']),
TimelineEntry(body: 'Lorem ipsum', authorId: '1', tags: ['latin']),
TimelineEntry(body: 'Hello world', authorId: '2', tags: ['greeting']),
TimelineEntry(body: 'Goodbye', authorId: '2', tags: ['SendOff']),
TimelineEntry(body: 'Lorem ipsum', authorId: '2', tags: ['LATIN']),
TimelineEntry(body: 'Chao', authorId: '2', tags: ['sendoff']),
TimelineEntry(
body: 'Hello world',
authorId: '1',
tags: ['greeting'],
externalLink: 'http://mastodon.social/@user1/1234',
),
TimelineEntry(
body: 'Goodbye',
authorId: '1',
tags: ['SendOff'],
externalLink: 'http://mastodon.social/@user1/4567'),
TimelineEntry(
body: 'Lorem ipsum',
authorId: '1',
tags: ['latin'],
externalLink: 'http://mastodon.social/@user1/7890',
),
TimelineEntry(
body: 'Hello world',
authorId: '2',
tags: ['greeting'],
externalLink: 'http://trolltodon.social/@user2/12',
),
TimelineEntry(
body: 'Goodbye',
authorId: '2',
tags: ['SendOff'],
externalLink: 'http://trolltodon.social/@user2/34',
),
TimelineEntry(
body: 'Lorem ipsum',
authorId: '2',
tags: ['LATIN'],
externalLink: 'http://trolltodon.social/@user2/56',
),
TimelineEntry(
body: 'Chao',
authorId: '2',
tags: ['sendoff'],
externalLink: 'http://trolltodon.social/@user2/78',
),
];
group('Test StringFilter', () {
@ -37,20 +71,30 @@ void main() {
expect(filter.isFiltered('hello!'), equals(false));
expect(filter.isFiltered('help'), equals(false));
});
test('Test containsCaseSensitive', () {
test('Test endsWithIgnoresCase', () {
const filter = StringFilter(
filterString: 'world',
type: ComparisonType.endsWithIgnoreCase,
);
expect(filter.isFiltered('world'), equals(true));
expect(filter.isFiltered('hello WORld'), equals(true));
expect(filter.isFiltered('worldwide'), equals(false));
expect(filter.isFiltered('hello world!'), equals(false));
});
test('Test contains', () {
const filter = StringFilter(
filterString: 'hello',
type: ComparisonType.containsCaseSensitive,
type: ComparisonType.contains,
);
expect(filter.isFiltered('hello world'), equals(true));
expect(filter.isFiltered('Hello World'), equals(false));
expect(filter.isFiltered('hello world'), equals(true));
expect(filter.isFiltered('help'), equals(false));
});
test('Test containsCaseInsensitive', () {
test('Test containsIgnoreCase', () {
const filter = StringFilter(
filterString: 'hello',
type: ComparisonType.containsCaseInsensitive,
type: ComparisonType.containsIgnoreCase,
);
expect(filter.isFiltered('hello world'), equals(true));
expect(filter.isFiltered('Hello World'), equals(true));
@ -91,6 +135,30 @@ void main() {
expect(actual, equals(expected));
});
group('Test Domain Filter', () {
test('Exact match', () {
final filter = TimelineEntryFilter.create(
action: TimelineEntryFilterAction.hide,
name: 'filter',
domains: ['trolltodon.social'],
);
final expected = [false, false, false, true, true, true, true];
final actual = entries.map((e) => filter.isFiltered(e)).toList();
expect(actual, equals(expected));
});
test('Start wildcard', () {
final filter = TimelineEntryFilter.create(
action: TimelineEntryFilterAction.hide,
name: 'filter',
domains: ['*odon.social'],
);
final expected = [true, true, true, true, true, true, true];
final actual = entries.map((e) => filter.isFiltered(e)).toList();
expect(actual, equals(expected));
});
});
test('Test Tag Filter', () {
final filter = TimelineEntryFilter.create(
action: TimelineEntryFilterAction.hide,