mirror of
https://codeberg.org/tom79/Fedilab.git
synced 2024-12-22 16:50:04 +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.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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
attachmentList.add(attachment);
|
||||
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.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) {
|
||||
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
Loading…
Reference in a new issue