Follow Nitter accounts

This commit is contained in:
tom79 2019-12-08 16:54:47 +01:00
parent fbdc245205
commit f7a035060d
5 changed files with 326 additions and 79 deletions

View File

@ -188,7 +188,6 @@ public class ReorderTimelinesActivity extends BaseActivity implements OnStartDra
} else if (radioGroup.getCheckedRadioButtonId() == R.id.gnu_instance) {
new HttpsConnection(ReorderTimelinesActivity.this, null).get("https://" + instanceName + "/api/statuses/public_timeline.json", 10, null, null);
}
runOnUiThread(new Runnable() {
public void run() {
dialog.dismiss();
@ -264,78 +263,84 @@ public class ReorderTimelinesActivity extends BaseActivity implements OnStartDra
@Override
public void afterTextChanged(Editable s) {
Pattern host = Pattern.compile("([\\da-z\\.-]+\\.[a-z\\.]{2,12})");
Matcher matcher = host.matcher(s.toString().trim());
if (s.toString().trim().length() == 0 || !matcher.find()) {
alertDialog.getButton(
AlertDialog.BUTTON_POSITIVE).setEnabled(false);
} else {
// Something into edit text. Enable the button.
if (radioGroup.getCheckedRadioButtonId() != R.id.twitter_accounts){
Pattern host = Pattern.compile("([\\da-z\\.-]+\\.[a-z\\.]{2,12})");
Matcher matcher = host.matcher(s.toString().trim());
if (s.toString().trim().length() == 0 || !matcher.find()) {
alertDialog.getButton(
AlertDialog.BUTTON_POSITIVE).setEnabled(false);
} else {
// Something into edit text. Enable the button.
alertDialog.getButton(
AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
if (s.length() > 2 && !isLoadingInstance) {
final String action = "/instances/search";
final HashMap<String, String> parameters = new HashMap<>();
parameters.put("q", s.toString().trim());
parameters.put("count", String.valueOf(1000));
parameters.put("name", String.valueOf(true));
isLoadingInstance = true;
if (oldSearch == null || !oldSearch.equals(s.toString().trim()))
new Thread(new Runnable() {
@Override
public void run() {
try {
final String response = new HttpsConnection(ReorderTimelinesActivity.this, null).get("https://instances.social/api/1.0" + action, 30, parameters, Helper.THEKINRAR_SECRET_TOKEN);
runOnUiThread(new Runnable() {
public void run() {
isLoadingInstance = false;
String[] instances;
try {
JSONObject jsonObject = new JSONObject(response);
JSONArray jsonArray = jsonObject.getJSONArray("instances");
if (jsonArray != null) {
int length = 0;
for (int i = 0; i < jsonArray.length(); i++) {
if (!jsonArray.getJSONObject(i).get("name").toString().contains("@") && jsonArray.getJSONObject(i).get("up").toString().equals("true"))
length++;
}
instances = new String[length];
int j = 0;
for (int i = 0; i < jsonArray.length(); i++) {
if (!jsonArray.getJSONObject(i).get("name").toString().contains("@") && jsonArray.getJSONObject(i).get("up").toString().equals("true")) {
instances[j] = jsonArray.getJSONObject(i).get("name").toString();
j++;
}
}
} else {
instances = new String[]{};
}
instance_list.setAdapter(null);
ArrayAdapter<String> adapter =
new ArrayAdapter<>(ReorderTimelinesActivity.this, android.R.layout.simple_list_item_1, instances);
instance_list.setAdapter(adapter);
if (instance_list.hasFocus() && !ReorderTimelinesActivity.this.isFinishing())
instance_list.showDropDown();
oldSearch = s.toString().trim();
} catch (JSONException ignored) {
isLoadingInstance = false;
}
}
});
} catch (HttpsConnection.HttpsConnectionException e) {
isLoadingInstance = false;
} catch (Exception e) {
isLoadingInstance = false;
}
}
}).start();
else
isLoadingInstance = false;
}
}else {
alertDialog.getButton(
AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
if (s.length() > 2 && !isLoadingInstance) {
final String action = "/instances/search";
final HashMap<String, String> parameters = new HashMap<>();
parameters.put("q", s.toString().trim());
parameters.put("count", String.valueOf(1000));
parameters.put("name", String.valueOf(true));
isLoadingInstance = true;
if (oldSearch == null || !oldSearch.equals(s.toString().trim()))
new Thread(new Runnable() {
@Override
public void run() {
try {
final String response = new HttpsConnection(ReorderTimelinesActivity.this, null).get("https://instances.social/api/1.0" + action, 30, parameters, Helper.THEKINRAR_SECRET_TOKEN);
runOnUiThread(new Runnable() {
public void run() {
isLoadingInstance = false;
String[] instances;
try {
JSONObject jsonObject = new JSONObject(response);
JSONArray jsonArray = jsonObject.getJSONArray("instances");
if (jsonArray != null) {
int length = 0;
for (int i = 0; i < jsonArray.length(); i++) {
if (!jsonArray.getJSONObject(i).get("name").toString().contains("@") && jsonArray.getJSONObject(i).get("up").toString().equals("true"))
length++;
}
instances = new String[length];
int j = 0;
for (int i = 0; i < jsonArray.length(); i++) {
if (!jsonArray.getJSONObject(i).get("name").toString().contains("@") && jsonArray.getJSONObject(i).get("up").toString().equals("true")) {
instances[j] = jsonArray.getJSONObject(i).get("name").toString();
j++;
}
}
} else {
instances = new String[]{};
}
instance_list.setAdapter(null);
ArrayAdapter<String> adapter =
new ArrayAdapter<>(ReorderTimelinesActivity.this, android.R.layout.simple_list_item_1, instances);
instance_list.setAdapter(adapter);
if (instance_list.hasFocus() && !ReorderTimelinesActivity.this.isFinishing())
instance_list.showDropDown();
oldSearch = s.toString().trim();
} catch (JSONException ignored) {
isLoadingInstance = false;
}
}
});
} catch (HttpsConnection.HttpsConnectionException e) {
isLoadingInstance = false;
} catch (Exception e) {
isLoadingInstance = false;
}
}
}).start();
else
isLoadingInstance = false;
}
}
});
}

View File

@ -249,7 +249,15 @@ public class RetrieveFeedsAsyncTask extends AsyncTask<Void, Void, Void> {
status.setType(action);
}
}
} else if (remoteInstanceObj != null && remoteInstanceObj.size() > 0 && remoteInstanceObj.get(0).getType().equals("PIXELFED")) {
} else if (remoteInstanceObj != null && remoteInstanceObj.size() > 0 && remoteInstanceObj.get(0).getType().equals("NITTER")) {
apiResponse = api.getNitter(this.instanceName, max_id);
List<app.fedilab.android.client.Entities.Status> statusesTemp = apiResponse.getStatuses();
if (statusesTemp != null) {
for (app.fedilab.android.client.Entities.Status status : statusesTemp) {
status.setType(action);
}
}
}else if (remoteInstanceObj != null && remoteInstanceObj.size() > 0 && remoteInstanceObj.get(0).getType().equals("PIXELFED")) {
apiResponse = api.getPixelfedTimeline(instanceName, max_id);
} else if (remoteInstanceObj != null && remoteInstanceObj.size() > 0 && remoteInstanceObj.get(0).getType().equals("GNU")) {
apiResponse = api.getGNUTimeline(instanceName, max_id);

View File

@ -19,6 +19,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -28,15 +29,21 @@ import com.google.gson.JsonObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
@ -45,6 +52,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import app.fedilab.android.R;
import app.fedilab.android.activities.MainActivity;
@ -760,6 +769,147 @@ public class API {
return status;
}
/**
* Parse xml response for Nitter
*
* @param xml String
* @return List<Status>
*/
private List<Status> parseNitter(String xml) {
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
String nitterHost = sharedpreferences.getString(Helper.SET_NITTER_HOST, Helper.DEFAULT_NITTER_HOST).toLowerCase();
List<Status> statuses = new ArrayList<>();
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser xpp = factory.newPullParser();
xpp.setInput( new StringReader( xml ) );
int eventType = xpp.getEventType();
Account account = null;
Status status = null;
HashMap<String, String> mappedProfile = new HashMap<>();
while (eventType != XmlPullParser.END_DOCUMENT) {
if(eventType == XmlPullParser.START_TAG) {
if( xpp.getName().compareTo("item") == 0 ){
status = new Status();
status.setReplies_count(0);
status.setFavourites_count(0);
status.setReblogs_count(0);
status.setFavourited(false);
status.setReblogged(false);
status.setReblog(null);
status.setMedia_attachments(new ArrayList<>());
status.setMentions(new ArrayList<>());
status.setTags(new ArrayList<>());
status.setEmojis(new ArrayList<>());
status.setEmojiFound(true);
status.setImageFound(true);
status.setPollEmojiFound(true);
status.setEmojiTranslateFound(true);
status.setItemViewType(1);
account = new Account();
}else if( xpp.getName().compareTo("creator") == 0 ){
eventType = xpp.next();
if(eventType == XmlPullParser.TEXT) {
if( account != null ){
account.setAcct(xpp.getText().replace("@",""));
account.setDisplay_name(xpp.getText().replace("@",""));
account.setUsername(xpp.getText().replace("@",""));
account.setId("https://" + nitterHost + "/" + xpp.getText());
account.setUuid("https://" + nitterHost + "/" + xpp.getText());
account.setUrl("https://" + nitterHost + "/" + xpp.getText());
if( !mappedProfile.containsKey(xpp.getText()) ){
HttpsConnection httpsConnection = new HttpsConnection(context, nitterHost);
try {
String response = httpsConnection.get("https://" + nitterHost + "/" + xpp.getText() + "/rss", 10, null, null);
XmlPullParserFactory factory2 = XmlPullParserFactory.newInstance();
factory2.setNamespaceAware(true);
XmlPullParser xpp2 = factory2.newPullParser();
xpp2.setInput( new StringReader( response ) );
int eventType2 = xpp2.getEventType();
while (eventType2 != XmlPullParser.END_DOCUMENT) {
if (eventType2 == XmlPullParser.START_TAG) {
if (xpp2.getName().compareTo("url") == 0) {
eventType2 = xpp2.next();
if(eventType2 == XmlPullParser.TEXT ) {
mappedProfile.put(xpp.getText(), xpp2.getText());
}
break;
}
}
eventType2 = xpp2.next();
}
} catch (NoSuchAlgorithmException | KeyManagementException | HttpsConnection.HttpsConnectionException e) {
e.printStackTrace();
}
}
account.setAvatar(mappedProfile.get(xpp.getText()));
}
}
} else if( xpp.getName().compareTo("pubDate") == 0 ){
eventType = xpp.next();
if(eventType == XmlPullParser.TEXT && status != null) {
if( xpp.getText() != null ) {
try {
DateFormat formatter = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
Date date = formatter.parse(xpp.getText());
status.setCreated_at(date);
} catch (ParseException e) {
status.setCreated_at(new Date());
}
}
}
}else if( xpp.getName().compareTo("description") == 0 ){
eventType = xpp.next();
if(eventType == XmlPullParser.TEXT && status != null) {
if( xpp.getText() != null ) {
status.setContent(context, xpp.getText());
}
}
}else if( xpp.getName().compareTo("guid") == 0 ){
eventType = xpp.next();
if(eventType == XmlPullParser.TEXT && status != null) {
if( xpp.getText() != null ) {
status.setUri(xpp.getText());
Pattern idPattern = Pattern.compile("([0-9])+");
Matcher matcher = idPattern.matcher(xpp.getText());
while (matcher.find()) {
status.setId(matcher.group(0));
}
}
}
}else if( xpp.getName().compareTo("link") == 0 ){
eventType = xpp.next();
if(eventType == XmlPullParser.TEXT && status != null) {
if( xpp.getText() != null ) {
status.setUrl(xpp.getText());
}
}
}
} else if(eventType == XmlPullParser.END_TAG) {
if( xpp.getName().compareTo("item") == 0 ){
if (status != null) {
status.setAccount(account);
statuses.add(status);
}
account = null;
status = null;
}
}
eventType = xpp.next();
}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
return statuses;
}
/**
* Parse json response for several notes (Misskey)
*
@ -3237,8 +3387,45 @@ public class API {
return apiResponse;
}
/**
* Retrieves Peertube videos from an instance *synchronously*
* Retrieves Nitter timeline from accounts *synchronously*
*
* @return APIResponse
*/
public APIResponse getNitter(String instance, String max_id) {
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, Context.MODE_PRIVATE);
String nitterHost = sharedpreferences.getString(Helper.SET_NITTER_HOST, Helper.DEFAULT_NITTER_HOST).toLowerCase();
String[] usernames = instance.split(" ");
if( usernames.length == 0 ){
Error error = new Error();
error.setError(context.getString(R.string.toast_error));
error.setStatusCode(404);
apiResponse.setError(error);
return apiResponse;
}
StringBuilder params = new StringBuilder();
for(String param: usernames){
params.append(param.trim()).append(",");
}
try {
statuses = new ArrayList<>();
HttpsConnection httpsConnection = new HttpsConnection(context, this.instance);
String response = httpsConnection.get("https://" + nitterHost + "/" + params + "/rss", 10, null, null);
statuses = parseNitter(response);
} catch (HttpsConnection.HttpsConnectionException e) {
setError(e.getStatusCode(), e);
} catch (NoSuchAlgorithmException | IOException | KeyManagementException e) {
e.printStackTrace();
}
apiResponse.setStatuses(statuses);
return apiResponse;
}
/**
* Retrieves Misskey timeline from an instance *synchronously*
*
* @return APIResponse
*/

View File

@ -227,6 +227,7 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
private List<ViewHolder> lstHolders;
private List<Emojis> emojisPicker;
private Status statusForQuickReply;
private String instanceType;
private Runnable updateAnimatedEmoji = new Runnable() {
@Override
@ -266,6 +267,21 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
currentToId = -1;
}
public StatusListAdapter(String instanceType, RetrieveFeedsAsyncTask.Type type, String targetedId, boolean isOnWifi, List<Status> statuses) {
super();
this.statuses = statuses;
this.isOnWifi = isOnWifi;
statusListAdapter = this;
this.type = type;
this.targetedId = targetedId;
redraft = false;
lstHolders = new ArrayList<>();
toot_content = null;
toot_cw_content = null;
tootReply = null;
currentToId = -1;
this.instanceType = instanceType;
}
public StatusListAdapter(TagTimeline tagTimeline, String targetedId, boolean isOnWifi, List<Status> statuses) {
super();
@ -283,6 +299,23 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
currentToId = -1;
}
public StatusListAdapter(String instanceType, TagTimeline tagTimeline, String targetedId, boolean isOnWifi, List<Status> statuses) {
super();
this.statuses = statuses;
this.isOnWifi = isOnWifi;
statusListAdapter = this;
this.type = RetrieveFeedsAsyncTask.Type.TAG;
this.targetedId = targetedId;
redraft = false;
this.tagTimeline = tagTimeline;
this.instanceType = instanceType;
lstHolders = new ArrayList<>();
toot_content = null;
toot_cw_content = null;
tootReply = null;
currentToId = -1;
}
public StatusListAdapter(int position, String targetedId, boolean isOnWifi, List<Status> statuses) {
this.statuses = statuses;
this.isOnWifi = isOnWifi;
@ -616,15 +649,20 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
show_boosts = ((ShowAccountActivity) context).showBoosts();
show_replies = ((ShowAccountActivity) context).showReplies();
}
if (type != RetrieveFeedsAsyncTask.Type.REMOTE_INSTANCE && type != RetrieveFeedsAsyncTask.Type.NEWS && !Helper.filterToots(statuses.get(position), type, context instanceof ShowAccountActivity, show_boosts, show_replies))
if (type != RetrieveFeedsAsyncTask.Type.REMOTE_INSTANCE && type != RetrieveFeedsAsyncTask.Type.NEWS && !Helper.filterToots(statuses.get(position), type, context instanceof ShowAccountActivity, show_boosts, show_replies)) {
return HIDDEN_STATUS;
}
if (statuses.get(position).isFocused() && type == RetrieveFeedsAsyncTask.Type.CONTEXT && statuses.get(position).getViewType() != CONSOLE_STATUS)
return FOCUSED_STATUS;
else {
if (social == UpdateAccountInfoAsyncTask.SOCIAL.PIXELFED && type == RetrieveFeedsAsyncTask.Type.CONTEXT) {
return COMPACT_STATUS;
} else {
return statuses.get(position).getViewType();
if( instanceType == null || instanceType.compareTo("NITTER") != 0 ) {
return statuses.get(position).getViewType();
}else{
return COMPACT_STATUS;
}
}
}
}
@ -653,6 +691,7 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
final SharedPreferences sharedpreferences = context.getSharedPreferences(Helper.APP_PREFS, MODE_PRIVATE);
final String userId = sharedpreferences.getString(Helper.PREF_KEY_ID, null);
context = viewHolder.itemView.getContext();
if (viewHolder.getItemViewType() != HIDDEN_STATUS) {
final ViewHolder holder = (ViewHolder) viewHolder;
synchronized (lock) {
@ -662,9 +701,10 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
holder.startUpdateTimer();
final Status status = statuses.get(i);
if (status == null)
return;
//TODO:It sounds that sometimes this value is null - need deeper investigation
if (status.getVisibility() == null) {
status.setVisibility("public");
@ -1301,6 +1341,9 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
status.setImageFound(true);
Status.makeImage(context, this, status);
}
if (instanceType != null && instanceType.compareTo("NITTER") == 0) {
holder.status_action_container.setVisibility(View.GONE);
}
holder.status_content.setOnTouchListener(new View.OnTouchListener() {
@Override
@ -3225,6 +3268,7 @@ public class StatusListAdapter extends RecyclerView.Adapter implements OnPostAct
}
}
}
private void loadAttachments(final Status status, final ViewHolder holder, boolean blur) {

View File

@ -206,7 +206,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
lastReadToot = sharedpreferences.getString(Helper.LAST_READ_TOOT_ID + userId + instance, null);
lastReadTootDate = Helper.stringToDate(context, sharedpreferences.getString(Helper.LAST_READ_TOOT_DATE + userId + instance, null));
}
if (instanceType == null || instanceType.equals("MASTODON") || instanceType.equals("MISSKEY") || instanceType.equals("GNU")) {
if (instanceType == null || instanceType.equals("MASTODON") || instanceType.equals("MISSKEY") || instanceType.equals("GNU") || instanceType.equals("NITTER")) {
if (type == RetrieveFeedsAsyncTask.Type.TAG && tag != null) {
BaseMainActivity.displayPeertube = null;
List<TagTimeline> tagTimelines = new SearchDAO(context, db).getTimelineInfo(tag);
@ -217,7 +217,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
}
} else {
BaseMainActivity.displayPeertube = null;
statusListAdapter = new StatusListAdapter(type, targetedId, isOnWifi, this.statuses);
statusListAdapter = new StatusListAdapter(instanceType, type, targetedId, isOnWifi, this.statuses);
lv_status.setAdapter(statusListAdapter);
}
} else if (instanceType.equals("PEERTUBE")) {
@ -580,7 +580,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
}
//Let's deal with statuses
if (statuses != null && statuses.size() > 0) {
if (statusListAdapter != null && (instanceType.equals("MASTODON") || instanceType.equals("MISSKEY") || instanceType.equals("GNU"))) {
if (statusListAdapter != null && (instanceType.equals("MASTODON") || instanceType.equals("NITTER") || instanceType.equals("MISSKEY") || instanceType.equals("GNU"))) {
this.statuses.addAll(statuses);
statusListAdapter.notifyItemRangeInserted(previousPosition, statuses.size());
} else if (artListAdapter != null && instanceType.equals("ART")) {
@ -863,6 +863,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
switch (instanceType) {
case "MASTODON":
case "MISSKEY":
case "NITTER":
case "GNU":
statusListAdapter.notifyItemRangeChanged(0, this.statuses.size());
break;
@ -892,7 +893,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
Status status = it.next();
for (Status status1 : statuses) {
if (status.getConversationId() != null && status.getConversationId().equals(status1.getConversationId())) {
if (instanceType.equals("MASTODON") || instanceType.equals("MISSKEY") || instanceType.equals("GNU"))
if (instanceType.equals("MASTODON") || instanceType.equals("MISSKEY") || instanceType.equals("NITTER")|| instanceType.equals("GNU"))
statusListAdapter.notifyItemRemoved(position);
else if (instanceType.equals("PIXELFED"))
pixelfedListAdapter.notifyItemRemoved(position);
@ -931,7 +932,8 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
}
}
}
if (statusListAdapter != null && (instanceType.equals("MASTODON") || instanceType.equals("MISSKEY") || instanceType.equals("GNU")))
if (statusListAdapter != null && (instanceType.equals("MASTODON") || instanceType.equals("MISSKEY") || instanceType.equals("NITTER") || instanceType.equals("GNU")))
statusListAdapter.notifyItemRangeInserted(0, inserted);
else if (pixelfedListAdapter != null && instanceType.equals("PIXELFED"))
pixelfedListAdapter.notifyItemRangeInserted(0, inserted);
@ -1158,6 +1160,7 @@ public class DisplayStatusFragment extends Fragment implements OnRetrieveFeedsIn
switch (instanceType) {
case "MASTODON":
case "MISSKEY":
case "NITTER":
case "GNU":
statusListAdapter.notifyItemRangeRemoved(0, lenght);
break;