Commit 00b8ca8c authored by Thomas's avatar Thomas
Browse files

Improve filters

parent 5eb6545b
Loading
Loading
Loading
Loading
+188 −101
Original line number Diff line number Diff line
@@ -75,7 +75,7 @@ public class TimelineHelper {
     * @return filtered List<Status>
     */
    public static List<Status> filterStatus(Context context, List<Status> statuses, Timeline.TimeLineEnum filterTimeLineType) {
        //A security to make sure filters have been fetched before displaying messages
        // Ensure filters have been fetched before displaying messages
        if (!BaseMainActivity.filterFetched && BaseMainActivity.filterFetchedRetry < 3) {
            MastodonFiltersService mastodonFiltersService = initv2(context);
            List<Filter> filterList;
@@ -95,27 +95,40 @@ public class TimelineHelper {
            BaseMainActivity.filterFetchedRetry++;
        }

        //If there are filters:
        if (BaseMainActivity.mainFilters != null && !BaseMainActivity.mainFilters.isEmpty() && statuses != null && !statuses.isEmpty()) {
        if (statuses == null || statuses.isEmpty()) {
            return statuses;
        }

            //Loop through filters
        // Precompile patterns for all active filters
        List<CompiledFilter> compiledFilters = new ArrayList<>();
        if (BaseMainActivity.mainFilters != null && !BaseMainActivity.mainFilters.isEmpty()) {
            Date now = new Date();
            for (Filter filter : BaseMainActivity.mainFilters) {
                if (filter.expires_at != null && filter.expires_at.before(new Date())) {
                    //Expired filter
                // Skip expired filters
                if (filter.expires_at != null && filter.expires_at.before(now)) {
                    continue;
                }

                // Check context
                boolean contextMatches = false;
                if (filterTimeLineType == Timeline.TimeLineEnum.HOME || filterTimeLineType == Timeline.TimeLineEnum.LIST) {
                    if (!filter.context.contains("home")) continue;
                    contextMatches = filter.context.contains("home");
                } else if (filterTimeLineType == Timeline.TimeLineEnum.NOTIFICATION) {
                    if (!filter.context.contains("notifications")) continue;
                    contextMatches = filter.context.contains("notifications");
                } else if (filterTimeLineType == Timeline.TimeLineEnum.CONTEXT) {
                    if (!filter.context.contains("thread")) continue;
                    contextMatches = filter.context.contains("thread");
                } else if (filterTimeLineType == Timeline.TimeLineEnum.ACCOUNT_TIMELINE) {
                    if (!filter.context.contains("account")) continue;
                    contextMatches = filter.context.contains("account");
                } else {
                    if (!filter.context.contains("public")) continue;
                    contextMatches = filter.context.contains("public");
                }
                if (filter.keywords != null && !filter.keywords.isEmpty()) {

                if (!contextMatches || filter.keywords == null || filter.keywords.isEmpty()) {
                    continue;
                }

                // Precompile patterns for this filter
                List<Pattern> patterns = new ArrayList<>();
                for (Filter.KeywordsAttributes filterKeyword : filter.keywords) {
                    String sb = Pattern.compile("\\A[A-Za-z0-9_]").matcher(filterKeyword.keyword).find() ? "\\b" : "";
                    String eb = Pattern.compile("[A-Za-z0-9_]\\z").matcher(filterKeyword.keyword).find() ? "\\b" : "";
@@ -125,103 +138,177 @@ public class TimelineHelper {
                    } else {
                        p = Pattern.compile("(" + Pattern.quote(filterKeyword.keyword) + ")", Pattern.CASE_INSENSITIVE);
                    }
                    patterns.add(p);
                }
                compiledFilters.add(new CompiledFilter(filter, patterns));
            }
        }

        // Apply filters to statuses (inverted loop order for early exit)
        for (Status status : statuses) {
            // Skip user's own statuses
            if (status.account.id.equals(MainActivity.currentUserID)) {
                continue;
            }
                            String content;
                            try {
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                    content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString();
                                else
                                    content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString();
                                if(status.reblog == null && status.getQuote() != null) {
                                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                        content += Html.fromHtml(status.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                                    else
                                        content += Html.fromHtml(status.getQuote().content).toString();
                                } else if(status.reblog != null && status.reblog.getQuote() != null) {
                                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                        content += Html.fromHtml(status.reblog.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                                    else
                                        content += Html.fromHtml(status.reblog.getQuote().content).toString();

            // Cache parsed HTML content
            String content = parseStatusContent(status);
            String spoilerText = parseStatusSpoiler(status);
            List<Attachment> mediaAttachments = status.reblog != null ? status.reblog.media_attachments : status.media_attachments;

            // Check against all compiled filters
            for (CompiledFilter compiledFilter : compiledFilters) {
                boolean matched = false;

                // Check content
                for (Pattern pattern : compiledFilter.patterns) {
                    if (pattern.matcher(content).find()) {
                        matched = true;
                        break;
                    }
                            } catch (Exception e) {
                                content = status.reblog != null ? status.reblog.content : status.content;
                }
                            Matcher m = p.matcher(content);
                            if (m.find()) {
                                status.filteredByApp = filter;
                                continue;

                // Check spoiler text
                if (!matched && spoilerText != null) {
                    for (Pattern pattern : compiledFilter.patterns) {
                        if (pattern.matcher(spoilerText).find()) {
                            matched = true;
                            break;
                        }
                            if (status.spoiler_text != null) {
                                String spoilerText;
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
                                    spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
                                else
                                    spoilerText = Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString();
                                Matcher ms = p.matcher(spoilerText);
                                if (ms.find()) {
                                    status.filteredByApp = filter;
                                    continue;
                    }
                }
                            List<Attachment> mediaAttachments = status.reblog != null ? status.reblog.media_attachments : status.media_attachments;
                            if(mediaAttachments != null && !mediaAttachments.isEmpty()) {

                // Check media attachments
                if (!matched && mediaAttachments != null && !mediaAttachments.isEmpty()) {
                    for (Attachment attachment : mediaAttachments) {
                        if (attachment.description != null) {
                                        Matcher ms = p.matcher(attachment.description );
                                        if (ms.find()) {
                                            status.filteredByApp = filter;
                            for (Pattern pattern : compiledFilter.patterns) {
                                if (pattern.matcher(attachment.description).find()) {
                                    matched = true;
                                    break;
                                }
                            }
                            if (matched) break;
                        }
                    }
                }

                if (matched) {
                    status.filteredByApp = compiledFilter.filter;
                    break; // Stop checking other filters for this status
                }
            }
        }
        }
        if (statuses != null && !statuses.isEmpty()) {

        // Apply additional filters for HOME timeline
        if (filterTimeLineType == Timeline.TimeLineEnum.HOME) {
            SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
            boolean groupReblogs = sharedpreferences.getBoolean(context.getString(R.string.SET_GROUP_REBLOGS), true);
            if (filterTimeLineType == Timeline.TimeLineEnum.HOME) {

            // Index seen reblog IDs for O(1) lookup
            java.util.HashSet<String> seenReblogIds = new java.util.HashSet<>();

            for (int i = 0; i < statuses.size(); i++) {
                Status currentStatus = statuses.get(i);

                // Check filtered accounts
                if (filteredAccounts != null && !filteredAccounts.isEmpty()) {
                    for (Account account : filteredAccounts) {
                            if (account.acct.equals(statuses.get(i).account.acct) || (statuses.get(i).reblog != null && account.acct.equals(statuses.get(i).reblog.account.acct))) {
                        if (account.acct.equals(currentStatus.account.acct) ||
                            (currentStatus.reblog != null && account.acct.equals(currentStatus.reblog.account.acct))) {
                            Filter filterCustom = new Filter();
                            filterCustom.filter_action = "hide";
                            ArrayList<String> contextCustom = new ArrayList<>();
                            contextCustom.add("home");
                            filterCustom.title = "Fedilab";
                            filterCustom.context = contextCustom;
                                statuses.get(i).filteredByApp = filterCustom;
                            currentStatus.filteredByApp = filterCustom;
                            break;
                        }
                    }
                }
                    //Group boosts
                    if (groupReblogs && statuses.get(i).filteredByApp == null && statuses.get(i).reblog != null) {
                        for (int j = 0; j < i; j++) {
                            if (statuses.get(j).reblog != null && statuses.get(j).reblog.id.equals(statuses.get(i).reblog.id)) {

                // Group duplicate reblogs using HashSet
                if (groupReblogs && currentStatus.filteredByApp == null && currentStatus.reblog != null) {
                    if (seenReblogIds.contains(currentStatus.reblog.id)) {
                        Filter filterCustom = new Filter();
                        filterCustom.filter_action = "hide";
                        ArrayList<String> contextCustom = new ArrayList<>();
                        contextCustom.add("home");
                        filterCustom.title = "Fedilab reblog";
                        filterCustom.context = contextCustom;
                                statuses.get(i).filteredByApp = filterCustom;
                        currentStatus.filteredByApp = filterCustom;
                    } else {
                        seenReblogIds.add(currentStatus.reblog.id);
                    }
                }
            }
        }

        return statuses;
    }

    /**
     * Parse HTML content from status (cached per status)
     */
    private static String parseStatusContent(Status status) {
        try {
            String content;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content, Html.FROM_HTML_MODE_LEGACY).toString();
            } else {
                content = Html.fromHtml(status.reblog != null ? status.reblog.content : status.content).toString();
            }

            // Append quote content if present
            if (status.reblog == null && status.getQuote() != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    content += Html.fromHtml(status.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                } else {
                    content += Html.fromHtml(status.getQuote().content).toString();
                }
            } else if (status.reblog != null && status.reblog.getQuote() != null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    content += Html.fromHtml(status.reblog.getQuote().content, Html.FROM_HTML_MODE_LEGACY).toString();
                } else {
                    content += Html.fromHtml(status.reblog.getQuote().content).toString();
                }
            }
            return content;
        } catch (Exception e) {
            return status.reblog != null ? status.reblog.content : status.content;
        }
    }

    /**
     * Parse HTML spoiler text from status
     */
    private static String parseStatusSpoiler(Status status) {
        if (status.spoiler_text == null) {
            return null;
        }
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                return Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text, Html.FROM_HTML_MODE_LEGACY).toString();
            } else {
                return Html.fromHtml(status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text).toString();
            }
        } catch (Exception e) {
            return status.reblog != null ? status.reblog.spoiler_text : status.spoiler_text;
        }
    }

    /**
     * Helper class to hold precompiled filter patterns
     */
    private static class CompiledFilter {
        final Filter filter;
        final List<Pattern> patterns;

        CompiledFilter(Filter filter, List<Pattern> patterns) {
            this.filter = filter;
            this.patterns = patterns;
        }
        return statuses;
    }

    /**