improved filters

This commit is contained in:
Mariotaku Lee 2017-09-16 19:11:27 +08:00
parent cdad3eb532
commit d43d6220bd
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
24 changed files with 451 additions and 466 deletions

View File

@ -34,10 +34,10 @@ import org.mariotaku.commons.objectcursor.LoganSquareCursorFieldConverter;
import org.mariotaku.library.objectcursor.annotation.AfterCursorObjectCreated;
import org.mariotaku.library.objectcursor.annotation.CursorField;
import org.mariotaku.library.objectcursor.annotation.CursorObject;
import org.mariotaku.twidere.model.util.LineSeparatedStringArrayConverter;
import org.mariotaku.twidere.model.util.FilterStringsFieldConverter;
import org.mariotaku.twidere.model.util.FilterUserKeysFieldConverter;
import org.mariotaku.twidere.model.util.UserKeyConverter;
import org.mariotaku.twidere.model.util.UserKeyCursorFieldConverter;
import org.mariotaku.twidere.model.util.UserKeysCursorFieldConverter;
import org.mariotaku.twidere.provider.TwidereDataStore;
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses;
@ -330,16 +330,16 @@ public class ParcelableStatus implements Parcelable, Comparable<ParcelableStatus
@CursorField(Statuses.FILTER_FLAGS)
public long filter_flags;
@CursorField(value = Statuses.FILTER_USERS, converter = UserKeysCursorFieldConverter.class)
@CursorField(value = Statuses.FILTER_USERS, converter = FilterUserKeysFieldConverter.class)
public UserKey[] filter_users;
@CursorField(value = Statuses.FILTER_SOURCES, converter = LineSeparatedStringArrayConverter.class)
@CursorField(value = Statuses.FILTER_SOURCES, converter = FilterStringsFieldConverter.class)
public String[] filter_sources;
@CursorField(value = Statuses.FILTER_LINKS, converter = LineSeparatedStringArrayConverter.class)
@CursorField(value = Statuses.FILTER_LINKS, converter = FilterStringsFieldConverter.class)
public String[] filter_links;
@CursorField(value = Statuses.FILTER_NAMES, converter = LineSeparatedStringArrayConverter.class)
@CursorField(value = Statuses.FILTER_NAMES, converter = FilterStringsFieldConverter.class)
public String[] filter_names;
@CursorField(value = Statuses.FILTER_TEXTS)

View File

@ -20,11 +20,7 @@ package org.mariotaku.twidere.model.util;
import org.mariotaku.commons.objectcursor.AbsArrayCursorFieldConverter;
/**
* Created by mariotaku on 2017/9/12.
*/
public class LineSeparatedStringArrayConverter extends AbsArrayCursorFieldConverter<String> {
public class FilterStringsFieldConverter extends AbsArrayCursorFieldConverter<String> {
@Override
protected String[] newArray(int size) {
return new String[size];
@ -32,12 +28,16 @@ public class LineSeparatedStringArrayConverter extends AbsArrayCursorFieldConver
@Override
protected String convertToString(String item) {
return item;
if (item == null || item.isEmpty()) return "";
return '\\' + item + '\\';
}
@Override
protected String parseItem(String str) {
return str;
if (str == null || str.isEmpty()) return null;
final int len = str.length();
if (str.charAt(0) != '\\' || str.charAt(len - 1) != '\\') return str;
return str.substring(1, len - 1);
}
@Override

View File

@ -0,0 +1,52 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.twidere.model.util;
import org.mariotaku.commons.objectcursor.AbsArrayCursorFieldConverter;
import org.mariotaku.twidere.model.UserKey;
/**
* Created by mariotaku on 2017/9/12.
*/
public class FilterUserKeysFieldConverter extends AbsArrayCursorFieldConverter<UserKey> {
@Override
protected UserKey[] newArray(int size) {
return new UserKey[size];
}
@Override
protected String convertToString(UserKey item) {
if (item == null) return "";
return '\\' + item.toString() + '\\';
}
@Override
protected UserKey parseItem(String str) {
if (str == null || str.isEmpty()) return null;
final int len = str.length();
if (str.charAt(0) != '\\' || str.charAt(len - 1) != '\\') return UserKey.valueOf(str);
return UserKey.valueOf(str.substring(1, len - 1));
}
@Override
protected char separatorChar() {
return '\n';
}
}

View File

@ -1,8 +1,6 @@
package org.mariotaku.twidere.util;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
@ -18,10 +16,7 @@ import org.mariotaku.microblog.library.twitter.model.User;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.extension.model.api.StatusExtensionsKt;
import org.mariotaku.twidere.model.ConsumerKeyType;
import org.mariotaku.twidere.model.ParcelableStatus;
import org.mariotaku.twidere.model.SpanItem;
import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.util.database.FilterQueryBuilder;
import java.nio.charset.Charset;
import java.util.zip.CRC32;
@ -38,44 +33,6 @@ public class InternalTwitterContentUtils {
private InternalTwitterContentUtils() {
}
public static boolean isFiltered(final SQLiteDatabase database, final UserKey userKey,
final String textPlain, final String quotedTextPlain,
final SpanItem[] spans, final SpanItem[] quotedSpans,
final String source, final String quotedSource,
final UserKey retweetedByKey, final UserKey quotedUserKey) {
return isFiltered(database, userKey, textPlain, quotedTextPlain, spans, quotedSpans, source,
quotedSource, retweetedByKey, quotedUserKey, true);
}
public static boolean isFiltered(final SQLiteDatabase database, final UserKey userKey,
final String textPlain, final String quotedTextPlain,
final SpanItem[] spans, final SpanItem[] quotedSpans,
final String source, final String quotedSource,
final UserKey retweetedByKey, final UserKey quotedUserKey,
final boolean filterRts) {
if (textPlain == null && spans == null && userKey == null && source == null)
return false;
final Pair<String, String[]> query = FilterQueryBuilder.INSTANCE.isFilteredQuery(userKey,
textPlain, quotedTextPlain, spans, quotedSpans, source, quotedSource, retweetedByKey,
quotedUserKey, filterRts);
final Cursor cur = database.rawQuery(query.getFirst(), query.getSecond());
if (cur == null) return false;
try {
return cur.moveToFirst() && cur.getInt(0) != 0;
} finally {
cur.close();
}
}
public static boolean isFiltered(@NonNull final SQLiteDatabase database,
@NonNull final ParcelableStatus status, final boolean filterRTs) {
return isFiltered(database, status.user_key, status.text_plain, status.quoted_text_plain,
status.spans, status.quoted_spans, status.source, status.quoted_source,
status.retweeted_by_user_key, status.quoted_user_key, filterRTs);
}
@NonNull
public static String getBestBannerUrl(@NonNull final String baseUrl, final int width, final int height) {
final String type;

View File

@ -83,85 +83,6 @@ inline val ParcelableStatus.can_retweet: Boolean
}
}
fun ParcelableStatus.toSummaryLine(): ParcelableActivity.SummaryLine {
val result = ParcelableActivity.SummaryLine()
result.key = user_key
result.name = user_name
result.screen_name = user_screen_name
result.content = text_unescaped
return result
}
fun ParcelableStatus.extractFanfouHashtags(): List<String> {
return spans?.filter { span ->
var link = span.link
if (link.startsWith("/")) {
link = "http://fanfou.com$link"
}
if (UriUtils.getAuthority(link) != "fanfou.com") {
return@filter false
}
if (span.start <= 0 || span.end > text_unescaped.lastIndex) return@filter false
if (text_unescaped[span.start - 1] == '#' && text_unescaped[span.end] == '#') {
return@filter true
}
return@filter false
}?.map { text_unescaped.substring(it.start, it.end) }.orEmpty()
}
fun ParcelableStatus.makeOriginal() {
if (!is_retweet) return
id = retweet_id
is_retweet = false
retweeted_by_user_key = null
retweeted_by_user_name = null
retweeted_by_user_screen_name = null
retweeted_by_user_profile_image = null
retweet_timestamp = -1
retweet_id = null
}
fun ParcelableStatus.addFilterFlag(@ParcelableStatus.FilterFlags flags: Long) {
filter_flags = filter_flags or flags
}
fun ParcelableStatus.updateFilterInfo(descriptions: Collection<String?>?) {
val links = mutableSetOf<String>()
spans?.mapNotNullTo(links) { span ->
if (span.type != SpanItem.SpanType.LINK) return@mapNotNullTo null
return@mapNotNullTo span.link
}
quoted_spans?.mapNotNullTo(links) { span ->
if (span.type != SpanItem.SpanType.LINK) return@mapNotNullTo null
return@mapNotNullTo span.link
}
filter_links = links.toTypedArray()
filter_sources = setOf(source?.plainText, quoted_source?.plainText).filterNotNull().toTypedArray()
filter_users = setOf(user_key, quoted_user_key, retweeted_by_user_key).filterNotNull().toTypedArray()
filter_names = setOf(user_name, quoted_user_name, retweeted_by_user_name).filterNotNull().toTypedArray()
val texts = StringBuilder()
texts.appendNonEmptyLine(text_unescaped)
texts.appendNonEmptyLine(quoted_text_unescaped)
media?.forEach { item ->
texts.appendNonEmptyLine(item.alt_text)
}
quoted_media?.forEach { item ->
texts.appendNonEmptyLine(item.alt_text)
}
filter_texts = texts.toString()
filter_descriptions = descriptions?.filterNotNull()?.joinToString("\n")
}
fun ParcelableStatus.updateExtraInformation(details: AccountDetails) {
account_color = details.color
}
val ParcelableStatus.quoted: ParcelableStatus?
get() {
val obj = ParcelableStatus()
@ -182,6 +103,95 @@ val ParcelableStatus.quoted: ParcelableStatus?
return obj
}
fun ParcelableStatus.toSummaryLine(): ParcelableActivity.SummaryLine {
val result = ParcelableActivity.SummaryLine()
result.key = user_key
result.name = user_name
result.screen_name = user_screen_name
result.content = text_unescaped
return result
}
fun ParcelableStatus.extractFanfouHashtags(): List<String> {
return spans?.filter { span ->
var link = span.link
if (link.startsWith("/")) {
link = "http://fanfou.com$link"
}
if (UriUtils.getAuthority(link) != "fanfou.com") {
return@filter false
}
if (span.start <= 0 || span.end > text_unescaped.lastIndex) return@filter false
if (text_unescaped[span.start - 1] == '#' && text_unescaped[span.end] == '#') {
return@filter true
}
return@filter false
}?.map { text_unescaped.substring(it.start, it.end) }.orEmpty()
}
fun ParcelableStatus.makeOriginal() {
if (!is_retweet) return
id = retweet_id
is_retweet = false
retweeted_by_user_key = null
retweeted_by_user_name = null
retweeted_by_user_screen_name = null
retweeted_by_user_profile_image = null
retweet_timestamp = -1
retweet_id = null
}
fun ParcelableStatus.addFilterFlag(@ParcelableStatus.FilterFlags flags: Long) {
filter_flags = filter_flags or flags
}
fun ParcelableStatus.updateFilterInfo(descriptions: Collection<String?>?) {
updateContentFilterInfo()
filter_users = setOf(user_key, quoted_user_key, retweeted_by_user_key).filterNotNull().toTypedArray()
filter_names = setOf(user_name, quoted_user_name, retweeted_by_user_name).filterNotNull().toTypedArray()
filter_descriptions = descriptions?.filterNotNull()?.joinToString("\n")
}
fun ParcelableStatus.updateContentFilterInfo() {
filter_links = generateFilterLinks()
filter_texts = generateFilterTexts()
filter_sources = setOf(source?.plainText, quoted_source?.plainText).filterNotNull().toTypedArray()
}
fun ParcelableStatus.generateFilterTexts(): String {
val texts = StringBuilder()
texts.appendNonEmptyLine(text_unescaped)
texts.appendNonEmptyLine(quoted_text_unescaped)
media?.forEach { item ->
texts.appendNonEmptyLine(item.alt_text)
}
quoted_media?.forEach { item ->
texts.appendNonEmptyLine(item.alt_text)
}
return texts.toString()
}
fun ParcelableStatus.generateFilterLinks(): Array<String> {
val links = mutableSetOf<String>()
spans?.mapNotNullTo(links) { span ->
if (span.type != SpanItem.SpanType.LINK) return@mapNotNullTo null
return@mapNotNullTo span.link
}
quoted_spans?.mapNotNullTo(links) { span ->
if (span.type != SpanItem.SpanType.LINK) return@mapNotNullTo null
return@mapNotNullTo span.link
}
return links.toTypedArray()
}
fun ParcelableStatus.updateExtraInformation(details: AccountDetails) {
account_color = details.color
}
internal inline val String.plainText: String get() = HtmlEscapeHelper.toPlainText(this)
private fun parcelableUserMention(key: UserKey, name: String, screenName: String) = ParcelableUserMention().also {
it.key = key
it.name = name
@ -193,5 +203,3 @@ private fun StringBuilder.appendNonEmptyLine(line: CharSequence?) {
append(line)
append('\n')
}
private val String.plainText: String get() = HtmlEscapeHelper.toPlainText(this)

View File

@ -33,6 +33,7 @@ import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.exception.MalformedResponseException
import org.mariotaku.twidere.extension.model.addFilterFlag
import org.mariotaku.twidere.extension.model.toParcelable
import org.mariotaku.twidere.extension.model.updateContentFilterInfo
import org.mariotaku.twidere.extension.model.updateFilterInfo
import org.mariotaku.twidere.extension.toSpanItem
import org.mariotaku.twidere.model.*
@ -46,19 +47,23 @@ import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.InternalTwitterContentUtils.getMediaUrl
import org.mariotaku.twidere.util.InternalTwitterContentUtils.getStartEndForEntity
fun Status.toParcelable(details: AccountDetails, profileImageSize: String = "normal"): ParcelableStatus {
return toParcelable(details.key, details.type, profileImageSize).apply {
fun Status.toParcelable(details: AccountDetails, profileImageSize: String = "normal",
updateFilterInfoAction: (Status, ParcelableStatus) -> Unit = ::updateFilterInfoDefault): ParcelableStatus {
return toParcelable(details.key, details.type, profileImageSize, updateFilterInfoAction).apply {
account_color = details.color
}
}
fun Status.toParcelable(accountKey: UserKey, accountType: String, profileImageSize: String = "normal"): ParcelableStatus {
fun Status.toParcelable(accountKey: UserKey, accountType: String, profileImageSize: String = "normal",
updateFilterInfoAction: (Status, ParcelableStatus) -> Unit = ::updateFilterInfoDefault): ParcelableStatus {
val result = ParcelableStatus()
applyTo(accountKey, accountType, profileImageSize, result)
applyTo(accountKey, accountType, profileImageSize, result, updateFilterInfoAction)
return result
}
fun Status.applyTo(accountKey: UserKey, accountType: String, profileImageSize: String = "normal", result: ParcelableStatus) {
fun Status.applyTo(accountKey: UserKey, accountType: String, profileImageSize: String = "normal",
result: ParcelableStatus,
updateFilterInfoAction: (Status, ParcelableStatus) -> Unit = ::updateFilterInfoDefault) {
val extras = ParcelableStatus.Extras()
result.account_key = accountKey
result.id = id
@ -201,10 +206,7 @@ fun Status.applyTo(accountKey: UserKey, accountType: String, profileImageSize: S
result.addFilterFlag(ParcelableStatus.FilterFlags.HAS_MEDIA)
}
result.updateFilterInfo(setOf(userDescriptionUnescaped, retweetedStatus?.userDescriptionUnescaped,
quotedStatus?.userDescriptionUnescaped, this.user.location, retweetedStatus?.user?.location,
quotedStatus?.user?.location, userUrlExpanded, retweetedStatus?.userUrlExpanded,
quotedStatus?.userUrlExpanded))
updateFilterInfoAction(this, result)
}
@ -228,7 +230,6 @@ fun Status.formattedTextWithIndices(): StatusTextWithIndices {
return textWithIndices
}
fun CodePointArray.findResultRangeLength(spans: Array<SpanItem>, origStart: Int, origEnd: Int): Int {
val findResult = findByOrigRange(spans, origStart, origEnd)
if (findResult.isEmpty()) {
@ -241,7 +242,6 @@ fun CodePointArray.findResultRangeLength(spans: Array<SpanItem>, origStart: Int,
return charCount(origStart, first.orig_start) + (last.end - first.start) + charCount(first.orig_end, origEnd)
}
fun HtmlBuilder.addEntities(entities: EntitySupport) {
// Format media.
var mediaEntities: Array<MediaEntity>? = null
@ -268,16 +268,62 @@ fun HtmlBuilder.addEntities(entities: EntitySupport) {
}
}
fun updateFilterInfoDefault(status: Status, result: ParcelableStatus) {
result.updateFilterInfo(setOf(
status.userDescriptionUnescaped,
status.userUrlExpanded,
status.userLocation,
status.retweetedStatus?.userDescriptionUnescaped,
status.retweetedStatus?.userLocation,
status.retweetedStatus?.userUrlExpanded,
status.quotedStatus?.userDescriptionUnescaped,
status.quotedStatus?.userLocation,
status.quotedStatus?.userUrlExpanded
))
}
/**
* Ignores status user info
*/
fun updateFilterInfoForUserTimeline(status: Status, result: ParcelableStatus) {
result.updateContentFilterInfo()
if (result.is_retweet) {
result.filter_users = setOf(result.user_key, result.quoted_user_key).filterNotNull().toTypedArray()
result.filter_names = setOf(result.user_name, result.quoted_user_name).filterNotNull().toTypedArray()
result.filter_descriptions = setOf(
status.retweetedStatus?.userDescriptionUnescaped,
status.retweetedStatus?.userUrlExpanded,
status.retweetedStatus?.userLocation,
status.quotedStatus?.userDescriptionUnescaped,
status.quotedStatus?.userLocation,
status.quotedStatus?.userUrlExpanded
).filterNotNull().joinToString("\n")
} else {
result.filter_users = setOf(result.quoted_user_key).filterNotNull().toTypedArray()
result.filter_names = setOf(result.quoted_user_name).filterNotNull().toTypedArray()
result.filter_descriptions = setOf(
status.quotedStatus?.userDescriptionUnescaped,
status.quotedStatus?.userLocation,
status.quotedStatus?.userUrlExpanded
).filterNotNull().joinToString("\n")
}
}
private fun String.twitterUnescaped(): String {
return twitterRawTextTranslator.translate(this)
}
private val Status.userDescriptionUnescaped: String?
private inline val Status.userDescriptionUnescaped: String?
get() = user?.let { InternalTwitterContentUtils.formatUserDescription(it)?.first }
private val Status.userUrlExpanded: String?
private inline val Status.userUrlExpanded: String?
get() = user?.urlEntities?.firstOrNull()?.expandedUrl
private inline val Status.userLocation: String?
get() = user?.location
/**
* @param spans Ordered spans
* *

View File

@ -22,7 +22,6 @@ package org.mariotaku.twidere.loader.statuses
import android.accounts.AccountManager
import android.content.Context
import android.content.SharedPreferences
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.kpreferences.get
import org.mariotaku.microblog.library.MicroBlogException
@ -30,7 +29,6 @@ import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.app.TwidereApplication
import org.mariotaku.twidere.constant.loadItemLimitKey
import org.mariotaku.twidere.extension.model.api.applyLoadLimit
import org.mariotaku.twidere.loader.iface.IPaginationLoader
@ -170,8 +168,7 @@ abstract class AbsRequestStatusesLoader(
data.addAll(statuses)
}
val db = TwidereApplication.getInstance(context).sqLiteDatabase
data.forEach { it.is_filtered = shouldFilterStatus(db, it) }
data.forEach { it.is_filtered = shouldFilterStatus(it) }
if (comparator != null) {
data.sortWith(comparator!!)
@ -188,8 +185,7 @@ abstract class AbsRequestStatusesLoader(
}
@WorkerThread
protected abstract fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean
protected abstract fun shouldFilterStatus(status: ParcelableStatus): Boolean
protected open fun processPaging(paging: Paging, details: AccountDetails, loadItemLimit: Int) {
paging.applyLoadLimit(details, loadItemLimit)

View File

@ -20,7 +20,6 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.attoparser.config.ParseConfiguration
import org.attoparser.dom.DOMMarkupParser
@ -48,7 +47,7 @@ import org.mariotaku.twidere.model.pagination.PaginatedArrayList
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.model.pagination.Pagination
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
import java.text.ParseException
import java.util.*
@ -120,8 +119,8 @@ class ConversationLoader(
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, false)
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
return ContentFiltersUtils.isFiltered(context.contentResolver, status, false, 0)
}
@Throws(MicroBlogException::class)

View File

@ -1,32 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import org.mariotaku.twidere.model.ListResponse
import org.mariotaku.twidere.model.ParcelableStatus
class DummyParcelableStatusesLoader @JvmOverloads constructor(context: Context, data: List<ParcelableStatus>? = null, fromUser: Boolean = false) : ParcelableStatusesLoader(context, data, -1, fromUser) {
override fun loadInBackground(): ListResponse<ParcelableStatus> {
return ListResponse.getListInstance(data)
}
}

View File

@ -20,13 +20,13 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.exception.APINotSupportedException
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
@ -34,7 +34,7 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
class GroupTimelineLoader(
context: Context,
@ -57,18 +57,19 @@ class GroupTimelineLoader(
}
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, true)
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
return ContentFiltersUtils.isFiltered(context.contentResolver, status, true,
FilterScope.LIST_GROUP_TIMELINE)
}
private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): List<Status> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
when {
return when {
groupId != null -> {
return microBlog.getGroupStatuses(groupId, paging)
microBlog.getGroupStatuses(groupId, paging)
}
groupName != null -> {
return microBlog.getGroupStatusesByName(groupName, paging)
microBlog.getGroupStatusesByName(groupName, paging)
}
else -> {
throw MicroBlogException("No group name or id given")

View File

@ -1,43 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.os.Bundle
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_STATUSES
import org.mariotaku.twidere.model.ListResponse
import org.mariotaku.twidere.model.ParcelableStatus
import java.util.*
class IntentExtrasStatusesLoader(context: Context, private val mExtras: Bundle?,
data: List<ParcelableStatus>, fromUser: Boolean) : ParcelableStatusesLoader(context, data, -1, fromUser) {
override fun loadInBackground(): ListResponse<ParcelableStatus> {
if (mExtras != null && mExtras.containsKey(EXTRA_STATUSES)) {
val users = mExtras.getParcelableArrayList<ParcelableStatus>(EXTRA_STATUSES)
if (users != null) {
data.addAll(users)
Collections.sort(data)
}
}
return ListResponse.getListInstance(data)
}
}

View File

@ -20,7 +20,6 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.ktextension.isNullOrEmpty
import org.mariotaku.microblog.library.MicroBlog
@ -30,6 +29,7 @@ import org.mariotaku.microblog.library.twitter.model.SearchQuery
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.microblog.library.twitter.model.UniversalSearchQuery
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.extension.model.official
@ -37,7 +37,7 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
open class MediaStatusesSearchLoader(
context: Context,
@ -60,9 +60,11 @@ open class MediaStatusesSearchLoader(
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
if (status.media.isNullOrEmpty()) return true
return InternalTwitterContentUtils.isFiltered(database, status, true)
val allowed = query?.split(' ')?.toTypedArray()
return ContentFiltersUtils.isFiltered(context.contentResolver, status, true,
FilterScope.SEARCH_RESULTS, allowed)
}
override fun processPaging(paging: Paging, details: AccountDetails, loadItemLimit: Int) {

View File

@ -20,7 +20,6 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.ktextension.isNullOrEmpty
import org.mariotaku.microblog.library.MicroBlog
@ -29,10 +28,12 @@ import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.*
import org.mariotaku.twidere.alias.MastodonTimelineOption
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.api.tryShowUser
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.api.updateFilterInfoForUserTimeline
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
@ -40,7 +41,7 @@ import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
class MediaTimelineLoader(
context: Context,
@ -69,21 +70,20 @@ class MediaTimelineLoader(
@Throws(MicroBlogException::class)
override fun getStatuses(account: AccountDetails, paging: Paging): PaginatedList<ParcelableStatus> {
when (account.type) {
AccountType.MASTODON -> return getMastodonStatuses(account, paging)
else -> return getMicroBlogStatuses(account, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
return when (account.type) {
AccountType.MASTODON -> getMastodonStatuses(account, paging)
else -> getMicroBlogStatuses(account, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize = profileImageSize,
updateFilterInfoAction = ::updateFilterInfoForUserTimeline)
}
}
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
if (status.media.isNullOrEmpty()) return false
val retweetUserKey = status.user_key.takeIf { status.is_retweet }
return !isMyTimeline && InternalTwitterContentUtils.isFiltered(database, retweetUserKey,
status.text_plain, status.quoted_text_plain, status.spans, status.quoted_spans,
status.source, status.quoted_source, null, status.quoted_user_key)
return !isMyTimeline && ContentFiltersUtils.isFiltered(context.contentResolver, status,
true, FilterScope.USER_TIMELINE)
}
private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): ResponseList<Status> {

View File

@ -20,13 +20,13 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.exception.APINotSupportedException
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
@ -36,7 +36,7 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
class NetworkPublicTimelineLoader(
context: Context,
@ -68,7 +68,8 @@ class NetworkPublicTimelineLoader(
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, true)
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
return ContentFiltersUtils.isFiltered(context.contentResolver, status, true,
FilterScope.SEARCH_RESULTS)
}
}

View File

@ -20,13 +20,13 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
@ -35,7 +35,7 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
class PublicTimelineLoader(
context: Context,
@ -66,7 +66,8 @@ class PublicTimelineLoader(
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, true)
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
return ContentFiltersUtils.isFiltered(context.contentResolver, status, true,
FilterScope.SEARCH_RESULTS)
}
}

View File

@ -20,7 +20,6 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
@ -30,6 +29,7 @@ import org.mariotaku.microblog.library.twitter.model.SearchQuery
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.microblog.library.twitter.model.UniversalSearchQuery
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
@ -41,7 +41,7 @@ import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.model.pagination.Pagination
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
open class TweetSearchLoader(
context: Context,
@ -59,22 +59,24 @@ open class TweetSearchLoader(
@Throws(MicroBlogException::class)
override fun getStatuses(account: AccountDetails, paging: Paging): PaginatedList<ParcelableStatus> {
when (account.type) {
AccountType.MASTODON -> return getMastodonStatuses(account, paging)
else -> return getMicroBlogStatuses(account, paging).mapMicroBlogToPaginated {
return when (account.type) {
AccountType.MASTODON -> getMastodonStatuses(account, paging)
else -> getMicroBlogStatuses(account, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize)
}
}
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, true)
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
val allowed = query?.split(' ')?.toTypedArray()
return ContentFiltersUtils.isFiltered(context.contentResolver, status, true,
FilterScope.SEARCH_RESULTS, allowed)
}
protected open fun processQuery(details: AccountDetails, query: String): String {
if (details.type == AccountType.TWITTER) {
if (details.extras?.official ?: false) {
if (details.extras?.official == true) {
return smQuery(query, pagination)
}
return "$query exclude:retweets"

View File

@ -20,7 +20,6 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
@ -29,6 +28,7 @@ import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
@ -37,7 +37,7 @@ import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
class UserFavoritesLoader(
context: Context,
@ -64,8 +64,9 @@ class UserFavoritesLoader(
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, false)
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
return ContentFiltersUtils.isFiltered(context.contentResolver, status, false,
FilterScope.FAVORITES)
}
private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): ResponseList<Status> {

View File

@ -20,20 +20,20 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.pagination.PaginatedList
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.database.ContentFiltersUtils
class UserListTimelineLoader(
context: Context,
@ -57,8 +57,9 @@ class UserListTimelineLoader(
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
return InternalTwitterContentUtils.isFiltered(database, status, true)
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
return ContentFiltersUtils.isFiltered(context.contentResolver, status, true,
FilterScope.LIST_GROUP_TIMELINE)
}
private fun getMicroBlogStatuses(account: AccountDetails, paging: Paging): ResponseList<Status> {

View File

@ -43,7 +43,7 @@ class UserMentionsLoader(
override fun processQuery(details: AccountDetails, query: String): String {
val screenName = query.substringAfter("@")
if (details.type == AccountType.TWITTER) {
if (details.extras?.official ?: false) {
if (details.extras?.official == true) {
return smQuery("to:$screenName", pagination)
}
return "to:$screenName exclude:retweets"

View File

@ -20,7 +20,6 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import android.text.TextUtils
import okhttp3.HttpUrl
@ -42,18 +41,20 @@ import org.mariotaku.restfu.http.mime.SimpleBody
import org.mariotaku.twidere.alias.MastodonStatus
import org.mariotaku.twidere.alias.MastodonTimelineOption
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.api.tryShowUser
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.api.updateFilterInfoForUserTimeline
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.timeline.UserTimelineFilter
import org.mariotaku.twidere.util.InternalTwitterContentUtils
import org.mariotaku.twidere.util.JsonSerializer
import org.mariotaku.twidere.util.dagger.DependencyHolder
import org.mariotaku.twidere.util.database.ContentFiltersUtils
import java.io.IOException
import java.util.concurrent.atomic.AtomicReference
@ -86,12 +87,13 @@ class UserTimelineLoader(
it.toParcelable(account)
}
else -> getMicroBlogStatuses(account, paging).mapMicroBlogToPaginated {
it.toParcelable(account, profileImageSize = profileImageSize)
it.toParcelable(account, profileImageSize = profileImageSize,
updateFilterInfoAction = ::updateFilterInfoForUserTimeline)
}
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
override fun shouldFilterStatus(status: ParcelableStatus): Boolean {
if (timelineFilter != null) {
if (status.is_retweet && !timelineFilter.isIncludeRetweets) {
return true
@ -99,10 +101,8 @@ class UserTimelineLoader(
}
if (accountKey != null && userKey != null && TextUtils.equals(accountKey.id, userKey.id))
return false
val retweetUserKey = status.user_key.takeIf { status.is_retweet }
return InternalTwitterContentUtils.isFiltered(database, retweetUserKey, status.text_plain,
status.quoted_text_plain, status.spans, status.quoted_spans, status.source,
status.quoted_source, null, status.quoted_user_key)
return ContentFiltersUtils.isFiltered(context.contentResolver, status, true,
FilterScope.USER_TIMELINE)
}
private fun getMastodonStatuses(account: AccountDetails, paging: Paging): LinkHeaderList<MastodonStatus> {

View File

@ -493,13 +493,19 @@ object DataStoreUtils {
fun ContainsExpression(dataField: String, filterTable: String, filterField: String) =
Expression.likeRaw(Column(Table(table), dataField), "'%'||$filterTable.$filterField||'%'")
fun LineContainsExpression(dataField: String, filterTable: String, filterField: String) =
Expression.likeRaw(Column(Table(table), dataField), "'\\%'||$filterTable.$filterField||'%\\'")
fun LineMatchExpression(dataField: String, filterTable: String, filterField: String) =
Expression.likeRaw(Column(Table(table), dataField), "'%\\'||$filterTable.$filterField||'\\%'")
val filteredUsersWhere = Expression.and(
ScopeMatchesExpression(Filters.Users.TABLE_NAME, Filters.Users.SCOPE),
ContainsExpression(Statuses.FILTER_USERS, Filters.Users.TABLE_NAME, Filters.Users.USER_KEY)
LineMatchExpression(Statuses.FILTER_USERS, Filters.Users.TABLE_NAME, Filters.Users.USER_KEY)
)
val filteredSourcesWhere = Expression.and(
ScopeMatchesExpression(Filters.Sources.TABLE_NAME, Filters.Sources.SCOPE),
ContainsExpression(Statuses.FILTER_SOURCES, Filters.Sources.TABLE_NAME, Filters.Sources.VALUE)
LineMatchExpression(Statuses.FILTER_SOURCES, Filters.Sources.TABLE_NAME, Filters.Sources.VALUE)
)
val filteredTextKeywordsWhere = Expression.or(
Expression.and(
@ -513,7 +519,7 @@ object DataStoreUtils {
Expression.and(
Expression.notEquals("${Filters.Keywords.TABLE_NAME}.${Filters.Keywords.SCOPE} & ${FilterScope.TARGET_NAME}", 0),
ScopeMatchesExpression(Filters.Keywords.TABLE_NAME, Filters.Keywords.SCOPE),
ContainsExpression(Statuses.FILTER_NAMES, Filters.Keywords.TABLE_NAME, Filters.Keywords.VALUE)
LineMatchExpression(Statuses.FILTER_NAMES, Filters.Keywords.TABLE_NAME, Filters.Keywords.VALUE)
),
Expression.and(
Expression.notEquals("${Filters.Keywords.TABLE_NAME}.${Filters.Keywords.SCOPE} & ${FilterScope.TARGET_DESCRIPTION}", 0),
@ -523,7 +529,7 @@ object DataStoreUtils {
)
val filteredLinksWhere = Expression.and(
ScopeMatchesExpression(Filters.Links.TABLE_NAME, Filters.Links.SCOPE),
ContainsExpression(Statuses.FILTER_LINKS, Filters.Links.TABLE_NAME, Filters.Links.VALUE)
LineContainsExpression(Statuses.FILTER_LINKS, Filters.Links.TABLE_NAME, Filters.Links.VALUE)
)
val filteredIdsQueryBuilder = SQLQueryBuilder
.select(Column(Table(table), Statuses._ID))

View File

@ -0,0 +1,157 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.database
import android.content.ContentResolver
import org.mariotaku.twidere.annotation.FilterScope
import org.mariotaku.twidere.extension.rawQuery
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.Filters.*
/**
* Created by mariotaku on 2017/2/16.
*/
object ContentFiltersUtils {
fun isFiltered(cr: ContentResolver, status: ParcelableStatus, filterRts: Boolean,
@FilterScope scope: Int, allowedKeywords: Array<String>? = null): Boolean {
return isFiltered(cr, status.filter_users, status.filter_texts,
status.filter_sources, status.filter_links, status.filter_names,
status.filter_descriptions, filterRts, scope, allowedKeywords)
}
fun isFiltered(cr: ContentResolver, users: Array<UserKey>?, texts: String?, sources: Array<String>?,
links: Array<String>?, names: Array<String>?, descriptions: String?, filterRts: Boolean,
@FilterScope scope: Int, allowedKeywords: Array<String>? = null): Boolean {
val query = isFilteredQuery(users, texts, sources, links, names, descriptions, true,
scope, allowedKeywords)
val cur = cr.rawQuery(query.first, query.second) ?: return false
@Suppress("ConvertTryFinallyToUseCall")
try {
return cur.moveToFirst() && cur.getInt(0) != 0
} finally {
cur.close()
}
}
fun isFilteredQuery(users: Array<UserKey>?, texts: String?, sources: Array<String>?,
links: Array<String>?, names: Array<String>?, descriptions: String?,
filterRts: Boolean, @FilterScope scope: Int, allowedKeywords: Array<String>? = null): Pair<String, Array<String>> {
val selectionArgs = mutableListOf<String>()
val queryBuilder = StringBuilder("SELECT ")
fun addExpression(ruleTable: String, ruleField: String, scopeField: String,
@FilterScope expressionScope: Int, noScopeAsTrue: Boolean, matchType: Int,
value: String, extraWhereAppend: ((StringBuilder, MutableList<String>, String) -> Unit)? = null) {
if (selectionArgs.isNotEmpty()) {
queryBuilder.append(" OR ")
}
selectionArgs.add(value)
queryBuilder.append("1 IN (SELECT ? LIKE ")
when (matchType) {
LINE_MATCH -> {
queryBuilder.append("'%\\'||")
queryBuilder.append(ruleField)
queryBuilder.append("||'\\%'")
}
LINE_CONTAINS -> {
queryBuilder.append("'\\%'||")
queryBuilder.append(ruleField)
queryBuilder.append("||'%\\'")
}
CONTAINS -> {
queryBuilder.append("'%'||")
queryBuilder.append(ruleField)
queryBuilder.append("||'%'")
}
}
queryBuilder.append(" FROM ")
queryBuilder.append(ruleTable)
queryBuilder.append(" WHERE ")
if (noScopeAsTrue) {
queryBuilder.append("(")
queryBuilder.append(scopeField)
queryBuilder.append(" = 0 OR ")
}
queryBuilder.append(scopeField)
queryBuilder.append(" & ")
queryBuilder.append(expressionScope)
queryBuilder.append(" != 0")
if (noScopeAsTrue) {
queryBuilder.append(")")
}
extraWhereAppend?.invoke(queryBuilder, selectionArgs, ruleField)
queryBuilder.append(")")
}
fun allowKeywordsWhere(sb: StringBuilder, args: MutableList<String>, ruleField: String) {
val allowed = allowedKeywords?.takeUnless(Array<*>::isEmpty) ?: return
sb.append(" AND NOT (")
allowed.forEachIndexed { i, s ->
args.add(s)
if (i != 0) {
sb.append(" OR ")
}
sb.append("? LIKE ")
sb.append(ruleField)
}
sb.append(")")
}
if (users != null) {
addExpression(Users.TABLE_NAME, Users.USER_KEY, Users.SCOPE, scope, true,
LINE_MATCH, users.joinToString("\n", "\\", "\\"))
}
if (sources != null) {
addExpression(Sources.TABLE_NAME, Sources.VALUE, Sources.SCOPE, scope, true,
LINE_MATCH, sources.joinToString("\n", "\\", "\\"))
}
if (links != null) {
addExpression(Links.TABLE_NAME, Links.VALUE, Links.SCOPE, scope, true,
LINE_CONTAINS, links.joinToString("\n", "\\", "\\"))
}
if (texts != null) {
addExpression(Keywords.TABLE_NAME, Keywords.VALUE, Keywords.SCOPE,
FilterScope.TARGET_TEXT or scope, true, CONTAINS,
texts, ::allowKeywordsWhere)
}
if (names != null) {
addExpression(Keywords.TABLE_NAME, Keywords.VALUE, Keywords.SCOPE,
FilterScope.TARGET_NAME or scope, false, LINE_CONTAINS,
names.joinToString("\n", "\\", "\\"), ::allowKeywordsWhere)
}
if (descriptions != null) {
addExpression(Keywords.TABLE_NAME, Keywords.VALUE, Keywords.SCOPE,
FilterScope.TARGET_DESCRIPTION or scope, false, CONTAINS,
descriptions, ::allowKeywordsWhere)
}
if (queryBuilder.isEmpty())
return Pair("SELECT 0", emptyArray())
return Pair(queryBuilder.toString(), selectionArgs.toTypedArray())
}
private const val LINE_MATCH = 0
private const val LINE_CONTAINS = 1
private const val CONTAINS = 2
}

View File

@ -1,170 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.database
import android.content.ContentResolver
import org.mariotaku.twidere.extension.rawQuery
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.SpanItem
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.Filters
import java.util.*
/**
* Created by mariotaku on 2017/2/16.
*/
object FilterQueryBuilder {
fun isFiltered(cr: ContentResolver, activity: ParcelableStatus): Boolean {
return isFiltered(cr, activity.user_key, activity.text_plain,
activity.quoted_text_plain, activity.spans, activity.quoted_spans,
activity.source, activity.quoted_source, activity.retweeted_by_user_key,
activity.quoted_user_key)
}
fun isFiltered(cr: ContentResolver, userKey: UserKey?, textPlain: String?, quotedTextPlain: String?,
spans: Array<SpanItem>?, quotedSpans: Array<SpanItem>?, source: String?, quotedSource: String?,
retweetedByKey: UserKey?, quotedUserKey: UserKey?): Boolean {
val query = FilterQueryBuilder.isFilteredQuery(userKey,
textPlain, quotedTextPlain, spans, quotedSpans, source, quotedSource, retweetedByKey,
quotedUserKey, true)
val cur = cr.rawQuery(query.first, query.second) ?: return false
@Suppress("ConvertTryFinallyToUseCall")
try {
return cur.moveToFirst() && cur.getInt(0) != 0
} finally {
cur.close()
}
}
fun isFilteredQuery(userKey: UserKey?, textPlain: String?, quotedTextPlain: String?,
spans: Array<SpanItem>?, quotedSpans: Array<SpanItem>?, source: String?, quotedSource: String?,
retweetedByKey: UserKey?, quotedUserKey: UserKey?, filterRts: Boolean): Pair<String, Array<String>> {
val builder = StringBuilder()
val selectionArgs = ArrayList<String>()
builder.append("SELECT ")
if (textPlain != null) {
selectionArgs.add(textPlain)
addTextPlainStatement(builder)
}
if (quotedTextPlain != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
selectionArgs.add(quotedTextPlain)
addTextPlainStatement(builder)
}
if (spans != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
addSpansStatement(spans, builder, selectionArgs)
}
if (quotedSpans != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
addSpansStatement(quotedSpans, builder, selectionArgs)
}
if (userKey != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
selectionArgs.add(userKey.toString())
createUserKeyStatement(builder)
}
if (retweetedByKey != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
selectionArgs.add(retweetedByKey.toString())
createUserKeyStatement(builder)
}
if (quotedUserKey != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
selectionArgs.add(quotedUserKey.toString())
createUserKeyStatement(builder)
}
if (source != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
selectionArgs.add(source)
appendSourceStatement(builder)
}
if (quotedSource != null) {
if (!selectionArgs.isEmpty()) {
builder.append(" OR ")
}
selectionArgs.add(quotedSource)
appendSourceStatement(builder)
}
return Pair(builder.toString(), selectionArgs.toTypedArray())
}
private fun createUserKeyStatement(builder: StringBuilder) {
builder.append("(SELECT ")
.append("?")
.append(" IN (SELECT ")
.append(Filters.Users.USER_KEY)
.append(" FROM ")
.append(Filters.Users.TABLE_NAME)
.append("))")
}
private fun appendSourceStatement(builder: StringBuilder) {
builder.append("(SELECT 1 IN (SELECT ? LIKE '%>'||")
.append(Filters.Sources.TABLE_NAME).append(".")
.append(Filters.VALUE).append("||'</a>%' FROM ")
.append(Filters.Sources.TABLE_NAME)
.append("))")
}
private fun addTextPlainStatement(builder: StringBuilder) {
builder.append("(SELECT 1 IN (SELECT ? LIKE '%'||")
.append(Filters.Keywords.TABLE_NAME)
.append(".")
.append(Filters.VALUE)
.append("||'%' FROM ")
.append(Filters.Keywords.TABLE_NAME)
.append("))")
}
private fun addSpansStatement(spans: Array<SpanItem>, builder: StringBuilder, selectionArgs: MutableList<String>) {
val spansFlat = StringBuilder()
for (span in spans) {
spansFlat.append(span.link)
spansFlat.append(' ')
}
selectionArgs.add(spansFlat.toString())
builder.append("(SELECT 1 IN (SELECT ? LIKE '%'||")
.append(Filters.Links.TABLE_NAME)
.append(".")
.append(Filters.VALUE)
.append("||'%' FROM ")
.append(Filters.Links.TABLE_NAME)
.append("))")
}
}

View File

@ -62,7 +62,7 @@ import org.mariotaku.twidere.receiver.NotificationReceiver
import org.mariotaku.twidere.service.LengthyOperationsService
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.Utils
import org.mariotaku.twidere.util.database.FilterQueryBuilder
import org.mariotaku.twidere.util.database.ContentFiltersUtils
import org.oshkimaadziig.george.androidutils.SpanFormatter
class ContentNotificationManager(
@ -219,7 +219,7 @@ class ContentNotificationManager(
if (pref.isNotificationMentionsOnly && activity.action !in Activity.Action.MENTION_ACTIONS) {
return@forEachRow false
}
if (FilterQueryBuilder.isFiltered(cr, activity)) {
if (ContentFiltersUtils.isFiltered(cr, activity, true, FilterScope.INTERACTIONS)) {
return@forEachRow false
}
val sources = ParcelableActivityUtils.filterSources(activity.sources_lite,