Nitter instances

This commit is contained in:
Thomas 2022-06-28 19:30:45 +02:00
parent 1b8211a341
commit 2caa24c79b
7 changed files with 268 additions and 3 deletions

View file

@ -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"

View file

@ -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

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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) {
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) {

View file

@ -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()
@ -147,6 +160,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
*