summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas <tschneider.ac@gmail.com>2022-06-28 19:30:45 +0200
committerThomas <tschneider.ac@gmail.com>2022-06-28 19:30:45 +0200
commit2caa24c79b7f007c4effd00677fb8f346f881dba (patch)
treee6c2b2cc3a8a175a5b95151188844d03bc072faa
parent1b8211a341513aeb7eefb03bc6be234241aa2e2a (diff)
Nitter instances
-rw-r--r--app/build.gradle1
-rw-r--r--app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java13
-rw-r--r--app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java136
-rw-r--r--app/src/main/java/app/fedilab/android/client/entities/nitter/NitterAccount.java36
-rw-r--r--app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java2
-rw-r--r--app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java28
-rw-r--r--app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java55
7 files changed, 268 insertions, 3 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 414114b42..f3ab87508 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -76,6 +76,7 @@ dependencies {
implementation "com.google.code.gson:gson:2.8.6"
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
+ implementation 'com.squareup.retrofit2:converter-simplexml:2.9.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.preference:preference:1.2.0'
implementation "org.conscrypt:conscrypt-android:2.5.2"
diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java
index 5602c02a6..de2968287 100644
--- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java
+++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java
@@ -22,6 +22,8 @@ import app.fedilab.android.client.entities.api.Marker;
import app.fedilab.android.client.entities.api.MastodonList;
import app.fedilab.android.client.entities.api.Status;
import app.fedilab.android.client.entities.misskey.MisskeyNote;
+import app.fedilab.android.client.entities.nitter.Nitter;
+import app.fedilab.android.client.entities.nitter.NitterAccount;
import app.fedilab.android.client.entities.peertube.PeertubeVideo;
import retrofit2.Call;
import retrofit2.http.Body;
@@ -224,6 +226,17 @@ public interface MastodonTimelinesService {
@Query("count") int count
);
+ @GET("{names}/rss")
+ Call<Nitter> getNitter(
+ @Path("names") String id,
+ @Query("max_position") String max_position
+ );
+
+ @GET("{account}/rss")
+ Call<NitterAccount> getNitterAccount(
+ @Path("account") String account
+ );
+
@GET("api/v1/videos/{id}")
Call<PeertubeVideo.Video> getPeertubeVideo(
@Path("id") String id
diff --git a/app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java b/app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java
new file mode 100644
index 000000000..5dc87f847
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/client/entities/nitter/Nitter.java
@@ -0,0 +1,136 @@
+package app.fedilab.android.client.entities.nitter;
+
+
+import android.content.Context;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import org.simpleframework.xml.Element;
+import org.simpleframework.xml.ElementList;
+import org.simpleframework.xml.Root;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import app.fedilab.android.client.endpoints.MastodonTimelinesService;
+import app.fedilab.android.client.entities.api.Attachment;
+import app.fedilab.android.client.entities.api.Status;
+import app.fedilab.android.helper.Helper;
+import okhttp3.OkHttpClient;
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+import retrofit2.converter.simplexml.SimpleXmlConverterFactory;
+
+@Root(name = "rss", strict = false)
+public class Nitter implements Serializable {
+
+
+ public static HashMap<String, NitterAccount> accounts = new HashMap<>();
+ @Element(name = "channel")
+ public Channel channel;
+
+ public static MastodonTimelinesService initInstanceXMLOnly(Context context, String instance) {
+ Gson gson = new GsonBuilder().setDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").create();
+ OkHttpClient okHttpClient = new OkHttpClient.Builder()
+ .readTimeout(60, TimeUnit.SECONDS)
+ .connectTimeout(60, TimeUnit.SECONDS)
+ .callTimeout(60, TimeUnit.SECONDS)
+ .proxy(Helper.getProxy(context))
+ .build();
+ Retrofit retrofit = new Retrofit.Builder()
+ .baseUrl("https://" + instance)
+ .addConverterFactory(GsonConverterFactory.create(gson))
+ .addConverterFactory(SimpleXmlConverterFactory.create())
+ .client(okHttpClient)
+ .build();
+ return retrofit.create(MastodonTimelinesService.class);
+ }
+
+ public static Status convert(Context context, String instance, FeedItem feedItem) {
+ Status status = new Status();
+ status.id = feedItem.pubDate.toString();
+ status.content = feedItem.title;
+ status.text = feedItem.title;
+ status.visibility = "public";
+ status.created_at = feedItem.pubDate;
+ status.uri = feedItem.guid;
+ status.url = feedItem.link;
+ if (feedItem.creator != null && !accounts.containsValue(feedItem.creator)) {
+ MastodonTimelinesService mastodonTimelinesService = initInstanceXMLOnly(context, instance);
+ Call<NitterAccount> accountCall = mastodonTimelinesService.getNitterAccount(instance);
+ if (accountCall != null) {
+ try {
+ Response<NitterAccount> publicTlResponse = accountCall.execute();
+ if (publicTlResponse.isSuccessful()) {
+ NitterAccount nitterAccount = publicTlResponse.body();
+ accounts.put(feedItem.creator, nitterAccount);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ NitterAccount nitterAccount = accounts.get(feedItem.creator);
+ if (nitterAccount != null) {
+ app.fedilab.android.client.entities.api.Account account = new app.fedilab.android.client.entities.api.Account();
+ String[] names = nitterAccount.channel.image.title.split("/");
+ account.id = feedItem.guid;
+ account.acct = names[1];
+ account.username = names[1];
+ account.display_name = names[0];
+ account.avatar = nitterAccount.channel.image.url;
+ account.avatar_static = nitterAccount.channel.image.url;
+ account.url = nitterAccount.channel.image.link;
+ status.account = account;
+ }
+
+ Pattern imgPattern = Pattern.compile("<img [^>]*src=\"([^\"]+)\"[^>]*>");
+ Matcher matcher = imgPattern.matcher(feedItem.description);
+ String description = feedItem.description;
+ ArrayList<Attachment> attachmentList = new ArrayList<>();
+ while (matcher.find()) {
+ description = description.replaceAll(Pattern.quote(matcher.group()), "");
+ Attachment attachment = new Attachment();
+ attachment.type = "image";
+ attachment.url = matcher.group(1);
+ attachment.preview_url = matcher.group(1);
+ attachment.id = matcher.group(1);
+ attachmentList.add(attachment);
+ }
+ status.media_attachments = attachmentList;
+
+ return status;
+ }
+
+ @Root(name = "channel", strict = false)
+ public static class Channel implements Serializable {
+ @ElementList(name = "item")
+ public List<FeedItem> mFeedItems;
+ }
+
+ @Root(name = "item", strict = false)
+ public static class FeedItem implements Serializable {
+ @ElementList(name = "dc:creator", required = false)
+ public String creator;
+ @ElementList(name = "title")
+ public String title;
+ @ElementList(name = "description", required = false)
+ public String description;
+ @ElementList(name = "pubDate")
+ public Date pubDate;
+ @ElementList(name = "guid", required = false)
+ public String guid;
+ @ElementList(name = "link", required = false)
+ public String link;
+ }
+
+}
diff --git a/app/src/main/java/app/fedilab/android/client/entities/nitter/NitterAccount.java b/app/src/main/java/app/fedilab/android/client/entities/nitter/NitterAccount.java
new file mode 100644
index 000000000..b17fbf381
--- /dev/null
+++ b/app/src/main/java/app/fedilab/android/client/entities/nitter/NitterAccount.java
@@ -0,0 +1,36 @@
+package app.fedilab.android.client.entities.nitter;
+
+
+import org.simpleframework.xml.Element;
+import org.simpleframework.xml.Root;
+
+import java.io.Serializable;
+import java.util.HashMap;
+
+@Root(name = "rss", strict = false)
+public class NitterAccount implements Serializable {
+
+
+ public static HashMap<String, NitterAccount> accounts = new HashMap<>();
+ @Element(name = "channel")
+ public Channel channel;
+
+ @Root(name = "channel", strict = false)
+ public static class Channel implements Serializable {
+ @Element(name = "image")
+ public Image image;
+
+ }
+
+ @Root(name = "image", strict = false)
+ public static class Image implements Serializable {
+ @Element(name = "title")
+ public String title;
+ @Element(name = "url")
+ public String url;
+ @Element(name = "link")
+ public String link;
+ }
+
+
+}
diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java
index a8e0c6505..88f60ab65 100644
--- a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java
+++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java
@@ -447,7 +447,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
attachment.peertubeId = matcherLink.group(3);
attachmentList.add(attachment);
statusToDeal.media_attachments = attachmentList;
- adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal));
+ //adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal));
}
}
diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java
index 3e041c0a9..7eb496959 100644
--- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java
+++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java
@@ -21,6 +21,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -33,6 +34,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -204,7 +206,12 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
searchCache = getArguments().getString(Helper.ARG_SEARCH_KEYWORD_CACHE, null);
pinnedTimeline = (PinnedTimeline) getArguments().getSerializable(Helper.ARG_REMOTE_INSTANCE);
if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null) {
- remoteInstance = pinnedTimeline.remoteInstance.host;
+ if (pinnedTimeline.remoteInstance.type != RemoteInstance.InstanceType.NITTER) {
+ remoteInstance = pinnedTimeline.remoteInstance.host;
+ } else {
+ SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
+ remoteInstance = sharedpreferences.getString(getString(R.string.SET_NITTER_HOST), getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
+ }
}
tagTimeline = (TagTimeline) getArguments().getSerializable(Helper.ARG_TAG_TIMELINE);
@@ -642,7 +649,24 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
//NITTER TIMELINES
if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) {
-
+ if (direction == null) {
+ timelinesVM.getNitter(remoteInstance, pinnedTimeline.remoteInstance.host, null)
+ .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView);
+ } else if (direction == DIRECTION.BOTTOM) {
+ timelinesVM.getNitter(remoteInstance, pinnedTimeline.remoteInstance.host, max_id)
+ .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false));
+ } else if (direction == DIRECTION.TOP) {
+ flagLoading = false;
+ } else if (direction == DIRECTION.REFRESH) {
+ timelinesVM.getNitter(remoteInstance, pinnedTimeline.remoteInstance.host, null)
+ .observe(getViewLifecycleOwner(), statusesRefresh -> {
+ if (statusAdapter != null) {
+ dealWithPagination(statusesRefresh, DIRECTION.REFRESH, true);
+ } else {
+ initializeStatusesCommonView(statusesRefresh);
+ }
+ });
+ }
} //GNU TIMELINES
else if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.GNU) {
diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java
index 554071147..8b35c02a2 100644
--- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java
+++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java
@@ -45,6 +45,7 @@ import app.fedilab.android.client.entities.app.BaseAccount;
import app.fedilab.android.client.entities.app.StatusCache;
import app.fedilab.android.client.entities.app.StatusDraft;
import app.fedilab.android.client.entities.misskey.MisskeyNote;
+import app.fedilab.android.client.entities.nitter.Nitter;
import app.fedilab.android.client.entities.peertube.PeertubeVideo;
import app.fedilab.android.exception.DBException;
import app.fedilab.android.helper.Helper;
@@ -56,6 +57,7 @@ import retrofit2.Call;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
+import retrofit2.converter.simplexml.SimpleXmlConverterFactory;
public class TimelinesVM extends AndroidViewModel {
@@ -91,6 +93,17 @@ public class TimelinesVM extends AndroidViewModel {
return retrofit.create(MastodonTimelinesService.class);
}
+ private MastodonTimelinesService initInstanceXMLOnly(String instance) {
+ Gson gson = new GsonBuilder().setDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz").create();
+ Retrofit retrofit = new Retrofit.Builder()
+ .baseUrl("https://" + instance)
+ //.addConverterFactory(GsonConverterFactory.create(gson))
+ .addConverterFactory(SimpleXmlConverterFactory.create())
+ .client(okHttpClient)
+ .build();
+ return retrofit.create(MastodonTimelinesService.class);
+ }
+
private MastodonTimelinesService init(String instance) {
Gson gson = new GsonBuilder().setDateFormat("MMM dd, yyyy HH:mm:ss").create();
Retrofit retrofit = new Retrofit.Builder()
@@ -148,6 +161,48 @@ public class TimelinesVM extends AndroidViewModel {
/**
+ * Public timeline for Nitter
+ *
+ * @param max_position Return results older than this id
+ * @return {@link LiveData} containing a {@link Statuses}
+ */
+ public LiveData<Statuses> getNitter(@NonNull String instance,
+ String accountsStr,
+ String max_position) {
+ MastodonTimelinesService mastodonTimelinesService = initInstanceXMLOnly(instance);
+ statusesMutableLiveData = new MutableLiveData<>();
+ new Thread(() -> {
+ Call<Nitter> publicTlCall = mastodonTimelinesService.getNitter(accountsStr, max_position);
+ Statuses statuses = new Statuses();
+ if (publicTlCall != null) {
+ try {
+ Response<Nitter> publicTlResponse = publicTlCall.execute();
+ if (publicTlResponse.isSuccessful()) {
+ Nitter rssResponse = publicTlResponse.body();
+ List<Status> statusList = new ArrayList<>();
+ if (rssResponse != null && rssResponse.channel != null && rssResponse.channel.mFeedItems != null) {
+ for (Nitter.FeedItem feedItem : rssResponse.channel.mFeedItems) {
+ Status status = Nitter.convert(getApplication(), instance, feedItem);
+ statusList.add(status);
+ }
+ }
+ List<Status> filteredStatuses = TimelineHelper.filterStatus(getApplication(), statusList, TimelineHelper.FilterTimeLineType.PUBLIC);
+ statuses.statuses = SpannableHelper.convertStatus(getApplication().getApplicationContext(), filteredStatuses);
+ statuses.pagination = MastodonHelper.getPagination(publicTlResponse.headers());
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ Handler mainHandler = new Handler(Looper.getMainLooper());
+ Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses);
+ mainHandler.post(myRunnable);
+ }).start();
+ return statusesMutableLiveData;
+ }
+
+ /**
* Public timeline for Misskey
*
* @param untilId Return results older than this id