From 408b6009f894a58d2973026f86c6e8ed48ff96e0 Mon Sep 17 00:00:00 2001 From: nuclearfog Date: Sun, 18 Jun 2023 22:24:39 +0200 Subject: [PATCH] added initial filter support --- .../twidda/backend/api/Connection.java | 23 ++ .../twidda/backend/api/mastodon/Mastodon.java | 79 +++++++ .../api/mastodon/impl/MastodonFilter.java | 185 ++++++++++++++++ .../backend/api/twitter/v1/TwitterV1.java | 20 ++ .../backend/helper/update/FilterUpdate.java | 207 ++++++++++++++++++ .../org/nuclearfog/twidda/model/Filter.java | 106 +++++++++ 6 files changed, 620 insertions(+) create mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonFilter.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/backend/helper/update/FilterUpdate.java create mode 100644 app/src/main/java/org/nuclearfog/twidda/model/Filter.java diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java index 89094d89..c2ddb6fd 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/Connection.java @@ -2,12 +2,14 @@ package org.nuclearfog.twidda.backend.api; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.backend.helper.update.FilterUpdate; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; import org.nuclearfog.twidda.backend.helper.update.PushUpdate; import org.nuclearfog.twidda.backend.helper.update.StatusUpdate; import org.nuclearfog.twidda.backend.helper.update.UserListUpdate; import org.nuclearfog.twidda.model.Account; import org.nuclearfog.twidda.model.Emoji; +import org.nuclearfog.twidda.model.Filter; import org.nuclearfog.twidda.model.Instance; import org.nuclearfog.twidda.model.Location; import org.nuclearfog.twidda.model.Notification; @@ -641,6 +643,27 @@ public interface Connection { */ List getIdBlocklist() throws ConnectionException; + /** + * returns used filter + * + * @return list of filter + */ + List getFilter() throws ConnectionException; + + /** + * create/update status filter + * + * @param update filter to update + */ + void updateFilter(FilterUpdate update) throws ConnectionException; + + /** + * delete status filter + * + * @param id ID of the filter to delete + */ + void deleteFilter(long id) throws ConnectionException; + /** * download image * diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java index 3a60c4ff..9d94ea85 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/Mastodon.java @@ -15,6 +15,7 @@ import org.nuclearfog.twidda.backend.api.Connection; import org.nuclearfog.twidda.backend.api.ConnectionException; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonAccount; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonEmoji; +import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonFilter; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonInstance; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonList; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonNotification; @@ -27,6 +28,7 @@ import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonTrend; import org.nuclearfog.twidda.backend.api.mastodon.impl.MastodonUser; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.backend.helper.update.FilterUpdate; import org.nuclearfog.twidda.backend.helper.update.PollUpdate; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; import org.nuclearfog.twidda.backend.helper.update.PushUpdate; @@ -37,6 +39,7 @@ import org.nuclearfog.twidda.backend.utils.StringUtils; import org.nuclearfog.twidda.config.GlobalSettings; import org.nuclearfog.twidda.model.Account; import org.nuclearfog.twidda.model.Emoji; +import org.nuclearfog.twidda.model.Filter; import org.nuclearfog.twidda.model.Instance; import org.nuclearfog.twidda.model.Location; import org.nuclearfog.twidda.model.Notification; @@ -68,6 +71,7 @@ import java.security.spec.ECPoint; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; @@ -140,6 +144,7 @@ public class Mastodon implements Connection { private static final String ENDPOINT_POLL = "/api/v1/polls/"; private static final String ENDPOINT_DOMAIN_BLOCK = "/api/v1/domain_blocks"; private static final String ENDPOINT_PUSH_UPDATE = "/api/v1/push/subscription"; + private static final String ENDPOINT_FILTER = "/api/v2/filters"; private static final MediaType TYPE_TEXT = MediaType.parse("text/plain"); private static final MediaType TYPE_STREAM = MediaType.parse("application/octet-stream"); @@ -941,6 +946,80 @@ public class Mastodon implements Connection { } + @Override + public List getFilter() throws ConnectionException { + try { + Response response = get(ENDPOINT_FILTER, new ArrayList<>()); + ResponseBody body = response.body(); + if (response.code() == 200 && body != null) { + JSONArray array = new JSONArray(body.string()); + List result = new LinkedList<>(); + for (int i = 0 ; i < array.length(); i++) { + result.add(new MastodonFilter(array.getJSONObject(i))); + } + return result; + } + throw new MastodonException(response); + } catch (IOException | JSONException exception) { + throw new MastodonException(exception); + } + } + + + @Override + public void updateFilter(FilterUpdate update) throws ConnectionException { + try { + List params = new ArrayList<>(); + params.add("title=" + update.getTitle()); + if (update.getExpirationTime() > 0) + params.add("expires_in=" + update.getExpirationTime()); + if (update.filterHomeSet()) + params.add("context[]=home"); + if (update.filterNotificationSet()) + params.add("context[]=notifications"); + if (update.filterPublicSet()) + params.add("context[]=public"); + if (update.filterThreadSet()) + params.add("context[]=thread"); + if (update.filterUserSet()) + params.add("context[]=account"); + if (update.getFilterAction() == Filter.ACTION_WARN) + params.add("filter_action=warn"); + else if (update.getFilterAction() == Filter.ACTION_HIDE) + params.add("filter_action=hide"); + if (update.wholeWord()) + params.add("keywords_attributes[][whole_word]=true"); + else + params.add("keywords_attributes[][whole_word]=false"); + for (String keyword : update.getKeywords()) + params.add("keywords_attributes[][keyword]=" + StringUtils.encode(keyword)); + Response response; + if (update.getId() != 0L) + response = put(ENDPOINT_FILTER + '/' + update.getId(), params); + else + response = post(ENDPOINT_FILTER, params); + if (response.code() != 200) { + throw new MastodonException(response); + } + } catch (IOException exception) { + throw new MastodonException(exception); + } + } + + + @Override + public void deleteFilter(long id) throws ConnectionException { + try { + Response response = delete(ENDPOINT_FILTER + '/' + id, new ArrayList<>()); + if (response.code() != 200) { + throw new MastodonException(response); + } + } catch (IOException exception) { + throw new MastodonException(exception); + } + } + + @Override public MediaStatus downloadImage(String link) throws MastodonException { try { diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonFilter.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonFilter.java new file mode 100644 index 00000000..7ea5f996 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/mastodon/impl/MastodonFilter.java @@ -0,0 +1,185 @@ +package org.nuclearfog.twidda.backend.api.mastodon.impl; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.nuclearfog.twidda.backend.utils.StringUtils; +import org.nuclearfog.twidda.model.Filter; + +/** + * Mastodon implementation of a status filter + * + * @author nuclearfog + */ +public class MastodonFilter implements Filter { + + private static final long serialVersionUID = -3363900731940590588L; + + private long id; + private long expiresAt = 0L; + private String title; + private Keyword[] keywords; + private int action; + private boolean filterHome, filterNotification, filterPublic, filterUser, filterThread; + + /** + * @param json Mastodon Filter json + */ + public MastodonFilter(JSONObject json) throws JSONException { + JSONArray typeArray = json.getJSONArray("context"); + JSONArray keywordArray = json.getJSONArray("keywords"); + String idStr = json.getString("id"); + String actionStr = json.getString("filter_action"); + String expiresStr = json.optString("expires_at"); + title = json.getString("title"); + switch (actionStr) { + default: + case "warn": + action = ACTION_WARN; + break; + + case "hide": + action = ACTION_HIDE; + break; + } + for (int i = 0 ; i < typeArray.length() ; i++) { + switch (typeArray.getString(i)) { + case "home": + filterHome = true; + break; + + case "notifications": + filterNotification = true; + break; + + case "public": + filterPublic = true; + break; + + case "account": + filterUser = true; + break; + + case "thread": + filterThread = true; + break; + } + } + keywords = new Keyword[keywordArray.length()]; + for (int i = 0 ; i < keywordArray.length() ; i++) { + keywords[i] = new MastodonKeyword(keywordArray.getJSONObject(i)); + } + if (!expiresStr.equals("null")) { + expiresAt = StringUtils.getTime(expiresStr, StringUtils.TIME_MASTODON); + } + try { + id = Long.parseLong(idStr); + } catch (NumberFormatException exception) { + throw new JSONException("Bad ID: " + idStr); + } + } + + + @Override + public long getId() { + return id; + } + + + @Override + public String getTitle() { + return title; + } + + + @Override + public long getExpirationTime() { + return expiresAt; + } + + + @Override + public Keyword[] getKeywords() { + return keywords; + } + + + @Override + public int getAction() { + return action; + } + + + @Override + public boolean filterHome() { + return filterHome; + } + + + @Override + public boolean filterNotifications() { + return filterNotification; + } + + + @Override + public boolean filterPublic() { + return filterPublic; + } + + + @Override + public boolean filterThreads() { + return filterThread; + } + + + @Override + public boolean filterUserTimeline() { + return filterUser; + } + + /** + * + */ + private static class MastodonKeyword implements Keyword { + + private static final long serialVersionUID = -2619670483101168015L; + + private long id; + private String keyword; + private boolean oneWord; + + /** + * @param json json object containing filter keyword + */ + private MastodonKeyword(JSONObject json) throws JSONException { + String idStr = json.getString("id"); + keyword = json.getString("keyword"); + oneWord = json.optBoolean("whole_word", false); + try { + id = Long.parseLong(idStr); + } catch (NumberFormatException exception) { + throw new JSONException("Bad ID: " + idStr); + } + } + + + @Override + public long getId() { + return id; + } + + + @Override + public String getKeyword() { + return keyword; + } + + + @Override + public boolean isOneWord() { + return oneWord; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java b/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java index 74e45bb5..6f79a303 100644 --- a/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java +++ b/app/src/main/java/org/nuclearfog/twidda/backend/api/twitter/v1/TwitterV1.java @@ -24,6 +24,7 @@ import org.nuclearfog.twidda.backend.api.twitter.v1.impl.UserListV1; import org.nuclearfog.twidda.backend.api.twitter.v1.impl.UserV1; import org.nuclearfog.twidda.backend.helper.ConnectionConfig; import org.nuclearfog.twidda.backend.helper.MediaStatus; +import org.nuclearfog.twidda.backend.helper.update.FilterUpdate; import org.nuclearfog.twidda.backend.helper.update.ProfileUpdate; import org.nuclearfog.twidda.backend.helper.update.PushUpdate; import org.nuclearfog.twidda.backend.helper.update.StatusUpdate; @@ -34,6 +35,7 @@ import org.nuclearfog.twidda.config.GlobalSettings; import org.nuclearfog.twidda.database.AppDatabase; import org.nuclearfog.twidda.model.Account; import org.nuclearfog.twidda.model.Emoji; +import org.nuclearfog.twidda.model.Filter; import org.nuclearfog.twidda.model.Instance; import org.nuclearfog.twidda.model.Location; import org.nuclearfog.twidda.model.Notification; @@ -1190,6 +1192,24 @@ public class TwitterV1 implements Connection { } + @Override + public List getFilter() throws ConnectionException { + throw new TwitterException("not implemented!"); + } + + + @Override + public void updateFilter(FilterUpdate update) throws ConnectionException { + throw new TwitterException("not implemented!"); + } + + + @Override + public void deleteFilter(long id) throws ConnectionException { + throw new TwitterException("not implemented!"); + } + + @Override public Notifications getNotifications(long minId, long maxId) throws TwitterException { List mentions = getTweets(TWEETS_MENTIONS, new ArrayList<>(), minId, maxId); diff --git a/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/FilterUpdate.java b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/FilterUpdate.java new file mode 100644 index 00000000..1d0bb4f7 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/backend/helper/update/FilterUpdate.java @@ -0,0 +1,207 @@ +package org.nuclearfog.twidda.backend.helper.update; + +import org.nuclearfog.twidda.model.Filter; + +import java.io.Serializable; + +/** + * Filter update class used to create or update an existing status filter + * + * @author nuclearfog + */ +public class FilterUpdate implements Serializable { + + private static final long serialVersionUID = 7408688572155707380L; + + private long id = 0L; + private String title; + private String[] keywords = {}; + private int expires_at = 0, action = Filter.ACTION_WARN; + private boolean filterHome, filterNotification, filterPublic, filterUser, filterThread, wholeWord; + + /** + * filter ID of an existing filter or '0' if a new filter should be created + * + * @return filter ID + */ + public long getId() { + return id; + } + + /** + * set filter ID of an existing filter + * + * @param id filter ID + */ + public void setId(long id) { + this.id = id; + } + + /** + * get filter title + * + * @return title + */ + public String getTitle() { + return title; + } + + /** + * set title of the filter + * + * @param title title (description) + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * get time to expiration in seconds + * + * @return expiration time + */ + public int getExpirationTime() { + return expires_at; + } + + /** + * set time to expire + * + * @param expires_at time until filter expires in seconds + */ + public void setExpirationTime(int expires_at) { + this.expires_at = expires_at; + } + + /** + * get an array of keywords to filter + * + * @return array of keywords + */ + public String[] getKeywords() { + return keywords; + } + + /** + * add keywords of the filter + * + * @param keywords array of keywords + */ + public void setKeywords(String[] keywords) { + this.keywords = keywords.clone(); + } + + /** + * get filter action + * + * @return filter action {@link Filter#ACTION_WARN,Filter#ACTION_HIDE} + */ + public int getFilterAction() { + return action; + } + + /** + * set filter action + * + * @param action filter action {@link Filter#ACTION_WARN,Filter#ACTION_HIDE} + */ + public void setFilterAction(int action) { + this.action = action; + } + + /** + * @return true if filter is set for home timeline + */ + public boolean filterHomeSet() { + return filterHome; + } + + /** + * enable/disable filter for home timeline + * + * @param filterHome true to enable filter + */ + public void setFilterHome(boolean filterHome) { + this.filterHome = filterHome; + } + + /** + * @return true if filter is set for notifications + */ + public boolean filterNotificationSet() { + return filterNotification; + } + + /** + * enable/disable notification filter + * + * @param filterNotification true to enable filter + */ + public void setFilterNotification(boolean filterNotification) { + this.filterNotification = filterNotification; + } + + /** + * @return true if filter is set for public timeline + */ + public boolean filterPublicSet() { + return filterPublic; + } + + /** + * enable/disable filter for public timeline + * + * @param filterPublic true to enable filter + */ + public void setFilterPublic(boolean filterPublic) { + this.filterPublic = filterPublic; + } + + /** + * @return true if filter is set for user timeline + */ + public boolean filterUserSet() { + return filterUser; + } + + /** + * enable/disable filter for user timeline + * + * @param filterUser true to enable filter + */ + public void setFilterUser(boolean filterUser) { + this.filterUser = filterUser; + } + + /** + * @return true if filter is set for threads + */ + public boolean filterThreadSet() { + return filterThread; + } + + /** + * enable/disable filter for threads + * + * @param filterThread true to enable filter + */ + public void setFilterThread(boolean filterThread) { + this.filterThread = filterThread; + } + + /** + * check if words of a single keyword should be interpreted as one word + * + * @return true if keyword should be interpreted as one word + */ + public boolean wholeWord() { + return wholeWord; + } + + /** + * enable/disable option to interpret words of a keyword as a single word + */ + public void setWholeWord(boolean wholeWord) { + this.wholeWord = wholeWord; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nuclearfog/twidda/model/Filter.java b/app/src/main/java/org/nuclearfog/twidda/model/Filter.java new file mode 100644 index 00000000..a43e3222 --- /dev/null +++ b/app/src/main/java/org/nuclearfog/twidda/model/Filter.java @@ -0,0 +1,106 @@ +package org.nuclearfog.twidda.model; + +import java.io.Serializable; + +/** + * Status filter interface used to filter status containing words from timelines + * + * @author nuclearfog + */ +public interface Filter extends Serializable { + + /** + * warn on filter match + */ + int ACTION_WARN = 1; + + /** + * hide status on filter match + */ + int ACTION_HIDE = 2; + + /** + * get filter ID + * + * @return filter ID + */ + long getId(); + + /** + * get title of the filter + * + * @return title string + */ + String getTitle(); + + /** + * get date time where the filter expires + * + * @return date time or '0' if not defined + */ + long getExpirationTime(); + + /** + * get an array of keywords to filter + * + * @return array of keywords + */ + Keyword[] getKeywords(); + + /** + * get action to take when filtering a status + * + * @return action type {@link #ACTION_HIDE,#ACTION_WARN} + */ + int getAction(); + + /** + * @return true to filter home timeline + */ + boolean filterHome(); + + /** + * @return true to filter notification timeline + */ + boolean filterNotifications(); + + /** + * @return true to filter public timelines + */ + boolean filterPublic(); + + /** + * @return true to apply filter at threads + */ + boolean filterThreads(); + + /** + * @return true to apply filter at user timelines + */ + boolean filterUserTimeline(); + + /** + * Filter keyword used to filter statuses from timeline containing one of these words + */ + interface Keyword extends Serializable { + + /** + * get keyword ID + * + * @return ID + */ + long getId(); + + /** + * get used keyword + * + * @return keyword text + */ + String getKeyword(); + + /** + * @return true if single words should be interpreted as one word + */ + boolean isOneWord(); + } +} \ No newline at end of file