mirror of
https://codeberg.org/tom79/Fedilab.git
synced 2025-06-23 21:50:10 +03:00
Fix Nitter using web calls
This commit is contained in:
parent
c6494d7e04
commit
fae30e63a8
3 changed files with 147 additions and 14 deletions
|
@ -61,6 +61,7 @@ import android.provider.OpenableColumns;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
@ -647,18 +648,12 @@ public class Helper {
|
||||||
public static Date stringToDateWithFormat(Context context, String stringDate, String format) {
|
public static Date stringToDateWithFormat(Context context, String stringDate, String format) {
|
||||||
if (stringDate == null)
|
if (stringDate == null)
|
||||||
return null;
|
return null;
|
||||||
Locale userLocale;
|
SimpleDateFormat dateFormat = new SimpleDateFormat(format, Locale.US);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
userLocale = context.getResources().getConfiguration().getLocales().get(0);
|
|
||||||
} else {
|
|
||||||
userLocale = context.getResources().getConfiguration().locale;
|
|
||||||
}
|
|
||||||
SimpleDateFormat dateFormat = new SimpleDateFormat(format, userLocale);
|
|
||||||
Date date = null;
|
Date date = null;
|
||||||
try {
|
try {
|
||||||
date = dateFormat.parse(stringDate);
|
date = dateFormat.parse(stringDate);
|
||||||
} catch (java.text.ParseException ignored) {
|
} catch (java.text.ParseException ignored) {
|
||||||
|
ignored.printStackTrace();
|
||||||
}
|
}
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1046,18 +1046,18 @@ 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) {
|
if (direction == null) {
|
||||||
timelinesVM.getNitter(pinnedTimeline.remoteInstance.host, null)
|
timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.host, null)
|
||||||
.observe(getViewLifecycleOwner(), nitterStatuses -> {
|
.observe(getViewLifecycleOwner(), nitterStatuses -> {
|
||||||
initialStatuses = nitterStatuses;
|
initialStatuses = nitterStatuses;
|
||||||
initializeStatusesCommonView(nitterStatuses);
|
initializeStatusesCommonView(nitterStatuses);
|
||||||
});
|
});
|
||||||
} else if (direction == DIRECTION.BOTTOM) {
|
} else if (direction == DIRECTION.BOTTOM) {
|
||||||
timelinesVM.getNitter(pinnedTimeline.remoteInstance.host, max_id)
|
timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.host, max_id)
|
||||||
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false, true, fetchStatus));
|
.observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false, true, fetchStatus));
|
||||||
} else if (direction == DIRECTION.TOP) {
|
} else if (direction == DIRECTION.TOP) {
|
||||||
flagLoading = false;
|
flagLoading = false;
|
||||||
} else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) {
|
} else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) {
|
||||||
timelinesVM.getNitter(pinnedTimeline.remoteInstance.host, null)
|
timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.host, null)
|
||||||
.observe(getViewLifecycleOwner(), statusesRefresh -> {
|
.observe(getViewLifecycleOwner(), statusesRefresh -> {
|
||||||
if (statusAdapter != null) {
|
if (statusAdapter != null) {
|
||||||
dealWithPagination(statusesRefresh, direction, true, true, fetchStatus);
|
dealWithPagination(statusesRefresh, direction, true, true, fetchStatus);
|
||||||
|
|
|
@ -31,16 +31,25 @@ import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.jsoup.Jsoup;
|
||||||
|
import org.jsoup.nodes.Document;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.IDN;
|
import java.net.IDN;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import app.fedilab.android.R;
|
import app.fedilab.android.R;
|
||||||
import app.fedilab.android.activities.MainActivity;
|
import app.fedilab.android.activities.MainActivity;
|
||||||
import app.fedilab.android.mastodon.client.endpoints.MastodonTimelinesService;
|
import app.fedilab.android.mastodon.client.endpoints.MastodonTimelinesService;
|
||||||
import app.fedilab.android.mastodon.client.endpoints.PixelfedTimelinesService;
|
|
||||||
import app.fedilab.android.mastodon.client.entities.api.Account;
|
import app.fedilab.android.mastodon.client.entities.api.Account;
|
||||||
|
import app.fedilab.android.mastodon.client.entities.api.Attachment;
|
||||||
import app.fedilab.android.mastodon.client.entities.api.Conversation;
|
import app.fedilab.android.mastodon.client.entities.api.Conversation;
|
||||||
import app.fedilab.android.mastodon.client.entities.api.Conversations;
|
import app.fedilab.android.mastodon.client.entities.api.Conversations;
|
||||||
import app.fedilab.android.mastodon.client.entities.api.Marker;
|
import app.fedilab.android.mastodon.client.entities.api.Marker;
|
||||||
|
@ -62,7 +71,9 @@ import app.fedilab.android.mastodon.helper.Helper;
|
||||||
import app.fedilab.android.mastodon.helper.MastodonHelper;
|
import app.fedilab.android.mastodon.helper.MastodonHelper;
|
||||||
import app.fedilab.android.mastodon.helper.TimelineHelper;
|
import app.fedilab.android.mastodon.helper.TimelineHelper;
|
||||||
import app.fedilab.android.mastodon.ui.fragment.timeline.FragmentMastodonTimeline;
|
import app.fedilab.android.mastodon.ui.fragment.timeline.FragmentMastodonTimeline;
|
||||||
|
import okhttp3.Callback;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
import retrofit2.Retrofit;
|
import retrofit2.Retrofit;
|
||||||
|
@ -169,6 +180,14 @@ public class TimelinesVM extends AndroidViewModel {
|
||||||
return retrofit.create(MastodonTimelinesService.class);
|
return retrofit.create(MastodonTimelinesService.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MastodonTimelinesService initInstanceHtmlOnly(String instance) {
|
||||||
|
Retrofit retrofit = new Retrofit.Builder()
|
||||||
|
.baseUrl("https://" + (instance != null ? IDN.toASCII(instance, IDN.ALLOW_UNASSIGNED) : null))
|
||||||
|
.client(okHttpClient)
|
||||||
|
.build();
|
||||||
|
return retrofit.create(MastodonTimelinesService.class);
|
||||||
|
}
|
||||||
|
|
||||||
private MastodonTimelinesService init(String instance) {
|
private MastodonTimelinesService init(String instance) {
|
||||||
Retrofit retrofit = new Retrofit.Builder()
|
Retrofit retrofit = new Retrofit.Builder()
|
||||||
.baseUrl("https://" + (instance != null ? IDN.toASCII(instance, IDN.ALLOW_UNASSIGNED) : null) + "/api/v1/")
|
.baseUrl("https://" + (instance != null ? IDN.toASCII(instance, IDN.ALLOW_UNASSIGNED) : null) + "/api/v1/")
|
||||||
|
@ -233,14 +252,14 @@ public class TimelinesVM extends AndroidViewModel {
|
||||||
* @param max_position Return results older than this id
|
* @param max_position Return results older than this id
|
||||||
* @return {@link LiveData} containing a {@link Statuses}
|
* @return {@link LiveData} containing a {@link Statuses}
|
||||||
*/
|
*/
|
||||||
public LiveData<Statuses> getNitter(
|
public LiveData<Statuses> getNitterRSS(
|
||||||
String accountsStr,
|
String accountsStr,
|
||||||
String max_position) {
|
String max_position) {
|
||||||
Context context = getApplication().getApplicationContext();
|
Context context = getApplication().getApplicationContext();
|
||||||
SharedPreferences sharedpreferences = PreferenceManager
|
SharedPreferences sharedpreferences = PreferenceManager
|
||||||
.getDefaultSharedPreferences(context);
|
.getDefaultSharedPreferences(context);
|
||||||
String instance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
|
String instance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
|
||||||
if (instance.trim().equals("")) {
|
if (instance.trim().isEmpty()) {
|
||||||
instance = context.getString(R.string.DEFAULT_NITTER_HOST);
|
instance = context.getString(R.string.DEFAULT_NITTER_HOST);
|
||||||
}
|
}
|
||||||
MastodonTimelinesService mastodonTimelinesService = initInstanceXMLOnly(instance);
|
MastodonTimelinesService mastodonTimelinesService = initInstanceXMLOnly(instance);
|
||||||
|
@ -282,6 +301,125 @@ public class TimelinesVM extends AndroidViewModel {
|
||||||
return statusesMutableLiveData;
|
return statusesMutableLiveData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public timeline for Nitter
|
||||||
|
*
|
||||||
|
* @param max_position Return results older than this id
|
||||||
|
* @return {@link LiveData} containing a {@link Statuses}
|
||||||
|
*/
|
||||||
|
public LiveData<Statuses> getNitterHTML(
|
||||||
|
String accountsStr,
|
||||||
|
String max_position) {
|
||||||
|
statusesMutableLiveData = new MutableLiveData<>();
|
||||||
|
Context context = getApplication().getApplicationContext();
|
||||||
|
SharedPreferences sharedpreferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context);
|
||||||
|
String instance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
|
||||||
|
if (instance.trim().isEmpty()) {
|
||||||
|
instance = context.getString(R.string.DEFAULT_NITTER_HOST);
|
||||||
|
}
|
||||||
|
//TODO: remove after tests
|
||||||
|
instance = "nitter.privacydev.net";
|
||||||
|
|
||||||
|
accountsStr = accountsStr.replaceAll("\\s", ",").replaceAll(",,",",");
|
||||||
|
String maxposition = max_position == null ? "" : "?max_position="+max_position;
|
||||||
|
String url = "https://" + instance + "/" + accountsStr + "/with_replies"+maxposition;
|
||||||
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
|
.connectTimeout(10, TimeUnit.SECONDS)
|
||||||
|
|
||||||
|
.writeTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.readTimeout(10, TimeUnit.SECONDS).build();
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
|
||||||
|
.header("accept-language","en-US;q=0.6")
|
||||||
|
.header("dnt","1")
|
||||||
|
.header("user-agent","Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0")
|
||||||
|
.get()
|
||||||
|
.build();
|
||||||
|
String finalInstance = instance;
|
||||||
|
String finalInstance1 = instance;
|
||||||
|
client.newCall(request).enqueue(new Callback() {
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull okhttp3.Call call, @NonNull IOException e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull okhttp3.Call call, @NonNull okhttp3.Response response) throws IOException {
|
||||||
|
|
||||||
|
Statuses statuses = new Statuses();
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
try {
|
||||||
|
String data = response.body().string();
|
||||||
|
|
||||||
|
Document doc = Jsoup.parse(data);
|
||||||
|
Elements timelineItems = doc.select(".timeline-item");
|
||||||
|
|
||||||
|
List<Status> statusList = new ArrayList<>();
|
||||||
|
for(Element timelineItem: timelineItems) {
|
||||||
|
|
||||||
|
//Not a RT
|
||||||
|
if(timelineItem.select(".icon-retweet").html().trim().isEmpty()) {
|
||||||
|
Status status = new Status();
|
||||||
|
Account account = new Account();
|
||||||
|
|
||||||
|
String[] splitLink = timelineItem.select(".tweet-link").text().split("/");
|
||||||
|
String status_id = splitLink[splitLink.length-1];
|
||||||
|
String pubDate = timelineItem.select(".tweet-date").select("a").attr("title");
|
||||||
|
String name = timelineItem.select(".fullname").text();
|
||||||
|
String userName = timelineItem.select(".username").text();
|
||||||
|
String avatar = "https://"+ finalInstance + timelineItem.select(".avatar").attr("src");
|
||||||
|
account.id = userName;
|
||||||
|
account.acct = userName;
|
||||||
|
account.username = userName;
|
||||||
|
account.display_name = name;
|
||||||
|
account.avatar = avatar;
|
||||||
|
account.avatar_static = avatar;
|
||||||
|
account.url = "https://"+ finalInstance +"/" + userName;
|
||||||
|
|
||||||
|
status.id = status_id;
|
||||||
|
status.account = account;
|
||||||
|
status.url = "https://"+ finalInstance +timelineItem.select(".tweet-link").attr("href");
|
||||||
|
status.content = timelineItem.select(".tweet-content").text();
|
||||||
|
Pattern imgPattern = Pattern.compile("<img [^>]*src=\"([^\"]+)\"[^>]*>");
|
||||||
|
Matcher matcher = imgPattern.matcher(status.content);
|
||||||
|
String description = status.content;
|
||||||
|
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.visibility = "public";
|
||||||
|
status.media_attachments = attachmentList;
|
||||||
|
String dateformat = "MMM d', 'yyyy' · 'h:m a' UTC'";
|
||||||
|
status.created_at = Helper.stringToDateWithFormat(context, pubDate, dateformat);
|
||||||
|
statusList.add(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statuses.statuses = statusList;
|
||||||
|
String max_id = response.headers().get("min-id");
|
||||||
|
statuses.pagination = new Pagination();
|
||||||
|
statuses.pagination.max_id = max_id;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
Runnable myRunnable = () -> statusesMutableLiveData.setValue(statuses);
|
||||||
|
mainHandler.post(myRunnable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return statusesMutableLiveData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public timeline for Misskey
|
* Public timeline for Misskey
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in a new issue