mirror of
https://codeberg.org/tom79/Fedilab.git
synced 2025-01-03 14:40:07 +02:00
Nitter instances
This commit is contained in:
parent
1b8211a341
commit
2caa24c79b
7 changed files with 268 additions and 3 deletions
|
@ -76,6 +76,7 @@ dependencies {
|
||||||
implementation "com.google.code.gson:gson:2.8.6"
|
implementation "com.google.code.gson:gson:2.8.6"
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||||
implementation 'com.squareup.retrofit2:converter-gson: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.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||||
implementation 'androidx.preference:preference:1.2.0'
|
implementation 'androidx.preference:preference:1.2.0'
|
||||||
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
implementation "org.conscrypt:conscrypt-android:2.5.2"
|
||||||
|
|
|
@ -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.MastodonList;
|
||||||
import app.fedilab.android.client.entities.api.Status;
|
import app.fedilab.android.client.entities.api.Status;
|
||||||
import app.fedilab.android.client.entities.misskey.MisskeyNote;
|
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 app.fedilab.android.client.entities.peertube.PeertubeVideo;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
|
@ -224,6 +226,17 @@ public interface MastodonTimelinesService {
|
||||||
@Query("count") int count
|
@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}")
|
@GET("api/v1/videos/{id}")
|
||||||
Call<PeertubeVideo.Video> getPeertubeVideo(
|
Call<PeertubeVideo.Video> getPeertubeVideo(
|
||||||
@Path("id") String id
|
@Path("id") String id
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -447,7 +447,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
||||||
attachment.peertubeId = matcherLink.group(3);
|
attachment.peertubeId = matcherLink.group(3);
|
||||||
attachmentList.add(attachment);
|
attachmentList.add(attachment);
|
||||||
statusToDeal.media_attachments = attachmentList;
|
statusToDeal.media_attachments = attachmentList;
|
||||||
adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal));
|
//adapter.notifyItemChanged(getPositionAsync(notificationList, statusList, statusToDeal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
@ -33,6 +34,7 @@ import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
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);
|
searchCache = getArguments().getString(Helper.ARG_SEARCH_KEYWORD_CACHE, null);
|
||||||
pinnedTimeline = (PinnedTimeline) getArguments().getSerializable(Helper.ARG_REMOTE_INSTANCE);
|
pinnedTimeline = (PinnedTimeline) getArguments().getSerializable(Helper.ARG_REMOTE_INSTANCE);
|
||||||
if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null) {
|
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);
|
tagTimeline = (TagTimeline) getArguments().getSerializable(Helper.ARG_TAG_TIMELINE);
|
||||||
|
@ -642,7 +649,24 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
||||||
|
|
||||||
//NITTER TIMELINES
|
//NITTER TIMELINES
|
||||||
if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) {
|
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
|
} //GNU TIMELINES
|
||||||
else if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.GNU) {
|
else if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.GNU) {
|
||||||
|
|
||||||
|
|
|
@ -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.StatusCache;
|
||||||
import app.fedilab.android.client.entities.app.StatusDraft;
|
import app.fedilab.android.client.entities.app.StatusDraft;
|
||||||
import app.fedilab.android.client.entities.misskey.MisskeyNote;
|
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.client.entities.peertube.PeertubeVideo;
|
||||||
import app.fedilab.android.exception.DBException;
|
import app.fedilab.android.exception.DBException;
|
||||||
import app.fedilab.android.helper.Helper;
|
import app.fedilab.android.helper.Helper;
|
||||||
|
@ -56,6 +57,7 @@ import retrofit2.Call;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
import retrofit2.Retrofit;
|
import retrofit2.Retrofit;
|
||||||
import retrofit2.converter.gson.GsonConverterFactory;
|
import retrofit2.converter.gson.GsonConverterFactory;
|
||||||
|
import retrofit2.converter.simplexml.SimpleXmlConverterFactory;
|
||||||
|
|
||||||
public class TimelinesVM extends AndroidViewModel {
|
public class TimelinesVM extends AndroidViewModel {
|
||||||
|
|
||||||
|
@ -91,6 +93,17 @@ public class TimelinesVM extends AndroidViewModel {
|
||||||
return retrofit.create(MastodonTimelinesService.class);
|
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) {
|
private MastodonTimelinesService init(String instance) {
|
||||||
Gson gson = new GsonBuilder().setDateFormat("MMM dd, yyyy HH:mm:ss").create();
|
Gson gson = new GsonBuilder().setDateFormat("MMM dd, yyyy HH:mm:ss").create();
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
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
|
* Public timeline for Misskey
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue