diff --git a/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java index 10e5867a..124abe73 100644 --- a/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/NotificationsAdapter.java @@ -76,16 +76,23 @@ public final class NotificationsAdapter extends ListAdapter sort(final List list) { final List listCopy = new ArrayList<>(list); Collections.sort(listCopy, (o1, o2) -> { - if (o1.getType() == o2.getType()) return 0; // keep requests at top - if (o1.getType() == NotificationType.REQUEST) return -1; - if (o2.getType() == NotificationType.REQUEST) return 1; - return 0; + if (o1.getType() == o2.getType() + && o1.getType() == NotificationType.REQUEST + && o2.getType() == NotificationType.REQUEST) return 0; + else if (o1.getType() == NotificationType.REQUEST) return -1; + else if (o2.getType() == NotificationType.REQUEST) return 1; + // timestamp + return o1.getTimestamp() > o2.getTimestamp() ? -1 : (o1.getTimestamp() == o2.getTimestamp() ? 0 : 1); }); return listCopy; } public interface OnNotificationClickListener { void onNotificationClick(final NotificationModel model); + + void onProfileClick(final String username); + + void onPreviewClick(final NotificationModel model); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java index ac1ef257..e695d07e 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/NotificationViewHolder.java @@ -24,10 +24,6 @@ public final class NotificationViewHolder extends RecyclerView.ViewHolder { public void bind(final NotificationModel model, final OnNotificationClickListener notificationClickListener) { if (model == null) return; - itemView.setOnClickListener(v -> { - if (notificationClickListener == null) return; - notificationClickListener.onNotificationClick(model); - }); int text = -1; CharSequence subtext = null; switch (model.getType()) { @@ -52,20 +48,47 @@ public final class NotificationViewHolder extends RecyclerView.ViewHolder { text = R.string.request_notif; subtext = model.getText(); break; + case COMMENT_LIKE: + case TAGGED_COMMENT: + case RESPONDED_STORY: + subtext = model.getText(); + break; } - binding.tvUsername.setText(model.getUsername()); - binding.tvComment.setText(text); - binding.tvSubComment.setText(subtext, subtext instanceof Spannable ? TextView.BufferType.SPANNABLE : TextView.BufferType.NORMAL); - // binding.tvSubComment.setMentionClickListener(mentionClickListener); + if (text == -1 && subtext != null) { + binding.tvComment.setText(subtext); + binding.tvSubComment.setVisibility(View.GONE); + } + else if (text != -1) { + binding.tvComment.setText(text); + binding.tvSubComment.setText(subtext); + binding.tvSubComment.setVisibility(subtext == null ? View.GONE : View.VISIBLE); + } + if (model.getType() != NotificationType.REQUEST) { binding.tvDate.setText(model.getDateTime()); } + + binding.tvUsername.setText(model.getUsername()); binding.ivProfilePic.setImageURI(model.getProfilePic()); + binding.ivProfilePic.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onProfileClick(model.getUsername()); + }); + if (TextUtils.isEmpty(model.getPreviewPic())) { - binding.ivPreviewPic.setVisibility(View.GONE); + binding.ivPreviewPic.setVisibility(View.INVISIBLE); } else { binding.ivPreviewPic.setVisibility(View.VISIBLE); binding.ivPreviewPic.setImageURI(model.getPreviewPic()); + binding.ivPreviewPic.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onPreviewClick(model); + }); } + + itemView.setOnClickListener(v -> { + if (notificationClickListener == null) return; + notificationClickListener.onNotificationClick(model); + }); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java b/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java index ff65a56b..1987443b 100644 --- a/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java +++ b/app/src/main/java/awais/instagrabber/asyncs/GetActivityAsyncTask.java @@ -16,12 +16,21 @@ import awais.instagrabber.utils.TextUtils; public class GetActivityAsyncTask extends AsyncTask { private static final String TAG = "GetActivityAsyncTask"; - private OnTaskCompleteListener onTaskCompleteListener; + private final OnTaskCompleteListener onTaskCompleteListener; public GetActivityAsyncTask(final OnTaskCompleteListener onTaskCompleteListener) { this.onTaskCompleteListener = onTaskCompleteListener; } + /* + This needs to be redone to fetch i inbox instead + Within inbox, data is (body JSON => counts) + Then we have these counts: + new_posts, activity_feed_dot_badge, relationships, campaign_notification + usertags, likes, comment_likes, shopping_notification, comments + photos_of_you (not sure about difference to usertags), requests + */ + protected NotificationCounts doInBackground(final String... cookiesArray) { if (cookiesArray == null) return null; final String cookie = cookiesArray[0]; @@ -70,11 +79,11 @@ public class GetActivityAsyncTask extends AsyncTask> fetchListener; + private final NewsService newsService; + private final boolean markAsSeen; + private boolean fetchedWeb = false; - public NotificationsFetcher(final FetchListener> fetchListener) { + public NotificationsFetcher(final boolean markAsSeen, + final FetchListener> fetchListener) { + this.markAsSeen = markAsSeen; this.fetchListener = fetchListener; + newsService = NewsService.getInstance(); } @Override protected List doInBackground(final Void... voids) { - List result = new ArrayList<>(); - final String url = "https://www.instagram.com/accounts/activity/?__a=1"; - try { - final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); - conn.setInstanceFollowRedirects(false); - conn.setUseCaches(false); - conn.setRequestProperty("Accept-Language", LocaleUtils.getCurrentLocale().getLanguage() + ",en-US;q=0.8"); - conn.connect(); + List notificationModels = new ArrayList<>(); - if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { - final JSONObject page = new JSONObject(NetworkUtils.readFromConnection(conn)) - .getJSONObject("graphql") - .getJSONObject("user"); - final JSONObject ewaf = page.getJSONObject("activity_feed") - .optJSONObject("edge_web_activity_feed"); - final JSONObject efr = page.optJSONObject("edge_follow_requests"); - JSONObject data; - JSONArray media; - if (ewaf != null - && (media = ewaf.optJSONArray("edges")) != null - && media.length() > 0 - && media.optJSONObject(0).optJSONObject("node") != null) { - for (int i = 0; i < media.length(); ++i) { - data = media.optJSONObject(i).optJSONObject("node"); - if (data == null) continue; - final String type = data.getString("__typename"); - final NotificationType notificationType = NotificationType.valueOfType(type); - if (notificationType == null) continue; - final JSONObject user = data.getJSONObject("user"); - result.add(new NotificationModel( - data.getString(Constants.EXTRAS_ID), - data.optString("text"), // comments or mentions - data.getLong("timestamp"), - user.getString("id"), - user.getString("username"), - user.getString("profile_pic_url"), - !data.isNull("media") ? data.getJSONObject("media").getString("shortcode") : null, - !data.isNull("media") ? data.getJSONObject("media").getString("thumbnail_src") : null, notificationType)); - } + newsService.fetchAppInbox(markAsSeen, new ServiceCallback>() { + @Override + public void onSuccess(final List result) { + if (result == null) return; + notificationModels.addAll(result); + if (fetchedWeb) { + fetchListener.onResult(notificationModels); } - - if (efr != null - && (media = efr.optJSONArray("edges")) != null - && media.length() > 0 - && media.optJSONObject(0).optJSONObject("node") != null) { - for (int i = 0; i < media.length(); ++i) { - data = media.optJSONObject(i).optJSONObject("node"); - if (data == null) continue; - result.add(new NotificationModel( - data.getString(Constants.EXTRAS_ID), - data.optString("full_name"), - 0L, - data.getString(Constants.EXTRAS_ID), - data.getString("username"), - data.getString("profile_pic_url"), - null, - null, NotificationType.REQUEST)); - } + else { + fetchedWeb = true; + newsService.fetchWebInbox(markAsSeen, this); } } - conn.disconnect(); - } catch (final Exception e) { - if (logCollector != null) - logCollector.appendException(e, LogCollector.LogFile.ASYNC_NOTIFICATION_FETCHER, "doInBackground"); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - return result; + + @Override + public void onFailure(final Throwable t) { + // Log.e(TAG, "onFailure: ", t); + if (fetchListener != null) { + fetchListener.onFailure(t); + } + } + }); + return notificationModels; } @Override protected void onPreExecute() { if (fetchListener != null) fetchListener.doBefore(); } - - @Override - protected void onPostExecute(final List result) { - if (fetchListener != null) fetchListener.onResult(result); - } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java index f8733382..1efc2bb1 100644 --- a/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/NotificationsViewerFragment.java @@ -18,6 +18,8 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.core.app.NotificationManagerCompat; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; @@ -30,8 +32,11 @@ import awais.instagrabber.adapters.NotificationsAdapter.OnNotificationClickListe import awais.instagrabber.asyncs.NotificationsFetcher; import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.databinding.FragmentNotificationsViewerBinding; +import awais.instagrabber.dialogs.ProfilePicDialogFragment; import awais.instagrabber.fragments.settings.MorePreferencesFragmentDirections; import awais.instagrabber.interfaces.MentionClickListener; +import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.NotificationModel; import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.repositories.responses.FriendshipRepoChangeRootResponse; import awais.instagrabber.utils.Constants; @@ -40,8 +45,10 @@ import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.NotificationViewModel; import awais.instagrabber.webservices.FriendshipService; +import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.NewsService; import awais.instagrabber.webservices.ServiceCallback; +import awais.instagrabber.webservices.StoriesService; import static awais.instagrabber.utils.Utils.settingsHelper; @@ -53,96 +60,153 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe private boolean shouldRefresh = true; private NotificationViewModel notificationViewModel; private FriendshipService friendshipService; + private MediaService mediaService; + private StoriesService storiesService; private String userId; private String csrfToken; private NewsService newsService; + private Context context; - private final OnNotificationClickListener clickListener = model -> { - if (model == null) return; - final String username = model.getUsername(); - final SpannableString title = new SpannableString(username + (TextUtils.isEmpty(model.getText()) ? "" : (":\n" + model.getText()))); - title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - String[] commentDialogList; - if (model.getShortCode() != null) { - commentDialogList = new String[]{ - getString(R.string.open_profile), - getString(R.string.view_post) - }; - } else if (model.getType() == NotificationType.REQUEST) { - commentDialogList = new String[]{ - getString(R.string.open_profile), - getString(R.string.request_approve), - getString(R.string.request_reject) - }; - } else { - commentDialogList = new String[]{getString(R.string.open_profile)}; + private final OnNotificationClickListener clickListener = new OnNotificationClickListener() { + @Override + public void onProfileClick(final String username) { + openProfile(username); } - final Context context = getContext(); - if (context == null) return; - final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> { - switch (which) { - case 0: - openProfile(model.getUsername()); - break; - case 1: - if (model.getType() == NotificationType.REQUEST) { - friendshipService.approve(userId, model.getUserId(), csrfToken, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRepoChangeRootResponse result) { - // Log.d(TAG, "onSuccess: " + result); - if (result.getStatus().equals("ok")) { - onRefresh(); - return; - } - Log.e(TAG, "approve: status was not ok!"); - } - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "approve: onFailure: ", t); - } - }); - return; - } - final AlertDialog alertDialog = new AlertDialog.Builder(context) - .setCancelable(false) - .setView(R.layout.dialog_opening_post) - .create(); - alertDialog.show(); - new PostFetcher(model.getShortCode(), feedModel -> { + @Override + public void onPreviewClick(final NotificationModel model) { + if (model.getType() == NotificationType.RESPONDED_STORY) { + showProfilePicDialog(model); + } + else { + mediaService.fetch(model.getPostId(), new ServiceCallback() { + @Override + public void onSuccess(final FeedModel feedModel) { final PostViewV2Fragment fragment = PostViewV2Fragment .builder(feedModel) .build(); - fragment.setOnShowListener(dialog1 -> alertDialog.dismiss()); fragment.show(getChildFragmentManager(), "post_view"); - }).execute(); - break; - case 2: - friendshipService.ignore(userId, model.getUserId(), csrfToken, new ServiceCallback() { - @Override - public void onSuccess(final FriendshipRepoChangeRootResponse result) { - // Log.d(TAG, "onSuccess: " + result); - if (result.getStatus().equals("ok")) { - onRefresh(); + } + + @Override + public void onFailure(final Throwable t) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + } + } + + @Override + public void onNotificationClick(final NotificationModel model) { + if (model == null) return; + final String username = model.getUsername(); + if (model.getType() == NotificationType.FOLLOW) { + openProfile(username); + } + else { + final SpannableString title = new SpannableString(username + (TextUtils.isEmpty(model.getText()) ? "" : (":\n" + model.getText()))); + title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + + String[] commentDialogList; + if (model.getType() == NotificationType.RESPONDED_STORY) { + commentDialogList = new String[]{ + getString(R.string.open_profile), + getString(R.string.view_story) + }; + } + else if (model.getPostId() != null) { + commentDialogList = new String[]{ + getString(R.string.open_profile), + getString(R.string.view_post) + }; + } + else if (model.getType() == NotificationType.REQUEST) { + commentDialogList = new String[]{ + getString(R.string.open_profile), + getString(R.string.request_approve), + getString(R.string.request_reject) + }; + } + else commentDialogList = null; // shouldn't happen + final Context context = getContext(); + if (context == null) return; + final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> { + switch (which) { + case 0: + openProfile(username); + break; + case 1: + if (model.getType() == NotificationType.REQUEST) { + friendshipService.approve(userId, model.getUserId(), csrfToken, new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRepoChangeRootResponse result) { + // Log.d(TAG, "onSuccess: " + result); + if (result.getStatus().equals("ok")) { + onRefresh(); + return; + } + Log.e(TAG, "approve: status was not ok!"); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "approve: onFailure: ", t); + } + }); return; } - Log.e(TAG, "ignore: status was not ok!"); - } + else if (model.getType() == NotificationType.RESPONDED_STORY) { + showProfilePicDialog(model); + return; + } + final AlertDialog alertDialog = new AlertDialog.Builder(context) + .setCancelable(false) + .setView(R.layout.dialog_opening_post) + .create(); + alertDialog.show(); + mediaService.fetch(model.getPostId(), new ServiceCallback() { + @Override + public void onSuccess(final FeedModel feedModel) { + final PostViewV2Fragment fragment = PostViewV2Fragment + .builder(feedModel) + .build(); + fragment.setOnShowListener(dialog1 -> alertDialog.dismiss()); + fragment.show(getChildFragmentManager(), "post_view"); + } - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "ignore: onFailure: ", t); - } - }); - break; + @Override + public void onFailure(final Throwable t) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + break; + case 2: + friendshipService.ignore(userId, model.getUserId(), csrfToken, new ServiceCallback() { + @Override + public void onSuccess(final FriendshipRepoChangeRootResponse result) { + // Log.d(TAG, "onSuccess: " + result); + if (result.getStatus().equals("ok")) { + onRefresh(); + return; + } + Log.e(TAG, "ignore: status was not ok!"); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "ignore: onFailure: ", t); + } + }); + break; + } + }; + new AlertDialog.Builder(context) + .setTitle(title) + .setItems(commentDialogList, profileDialogListener) + .setNegativeButton(R.string.cancel, null) + .show(); } - }; - new AlertDialog.Builder(context) - .setTitle(title) - .setItems(commentDialogList, profileDialogListener) - .setNegativeButton(R.string.cancel, null) - .show(); + } }; private final MentionClickListener mentionClickListener = (view, text, isHashtag, isLocation) -> { if (getContext() == null) return; @@ -158,7 +222,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - final Context context = getContext(); + context = getContext(); if (context == null) return; NotificationManagerCompat.from(context.getApplicationContext()).cancel(Constants.ACTIVITY_NOTIFICATION_ID); final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); @@ -167,6 +231,7 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe } friendshipService = FriendshipService.getInstance(); newsService = NewsService.getInstance(); + mediaService = MediaService.getInstance(); userId = CookieUtils.getUserIdFromCookie(cookie); csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); } @@ -205,22 +270,9 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe @Override public void onRefresh() { binding.swipeRefreshLayout.setRefreshing(true); - new NotificationsFetcher(notificationModels -> { + new NotificationsFetcher(true, notificationModels -> { binding.swipeRefreshLayout.setRefreshing(false); notificationViewModel.getList().postValue(notificationModels); - final String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - newsService.markChecked(timestamp, csrfToken, new ServiceCallback() { - @Override - public void onSuccess(@NonNull final Boolean result) { - // Log.d(TAG, "onResponse: body: " + result); - if (!result) Log.e(TAG, "onSuccess: Error marking activity checked, response is false"); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "onFailure: Error marking activity checked", t); - } - }); }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -229,4 +281,15 @@ public final class NotificationsViewerFragment extends Fragment implements Swipe .actionGlobalProfileFragment("@" + username); NavHostFragment.findNavController(this).navigate(action); } + + private void showProfilePicDialog(final NotificationModel model) { + final FragmentManager fragmentManager = getParentFragmentManager(); + final ProfilePicDialogFragment fragment = new ProfilePicDialogFragment(model.getPostId(), + model.getUsername(), + model.getPreviewPic()); + final FragmentTransaction ft = fragmentManager.beginTransaction(); + ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) + .add(fragment, "profilePicDialog") + .commit(); + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/NotificationModel.java b/app/src/main/java/awais/instagrabber/models/NotificationModel.java index d461a033..a7b78b47 100755 --- a/app/src/main/java/awais/instagrabber/models/NotificationModel.java +++ b/app/src/main/java/awais/instagrabber/models/NotificationModel.java @@ -13,7 +13,7 @@ public final class NotificationModel { private final String userId; private final String username; private final String profilePicUrl; - private final String shortCode; + private final String postId; private final String previewUrl; private final NotificationType type; private final CharSequence text; @@ -25,7 +25,7 @@ public final class NotificationModel { final String userId, final String username, final String profilePicUrl, - final String shortCode, + final String postId, final String previewUrl, final NotificationType type) { this.id = id; @@ -34,7 +34,7 @@ public final class NotificationModel { this.userId = userId; this.username = username; this.profilePicUrl = profilePicUrl; - this.shortCode = shortCode; + this.postId = postId; this.previewUrl = previewUrl; this.type = type; } @@ -47,6 +47,10 @@ public final class NotificationModel { return text; } + public long getTimestamp() { + return timestamp; + } + @NonNull public String getDateTime() { return Utils.datetimeParser.format(new Date(timestamp * 1000L)); @@ -64,8 +68,8 @@ public final class NotificationModel { return profilePicUrl; } - public String getShortCode() { - return shortCode; + public String getPostId() { + return postId; } public String getPreviewPic() { diff --git a/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java b/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java index acdadfbd..79c3b805 100755 --- a/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java +++ b/app/src/main/java/awais/instagrabber/models/enums/NotificationType.java @@ -5,11 +5,17 @@ import java.util.HashMap; import java.util.Map; public enum NotificationType implements Serializable { + // web LIKE("GraphLikeAggregatedStory"), FOLLOW("GraphFollowAggregatedStory"), COMMENT("GraphCommentMediaStory"), MENTION("GraphMentionStory"), TAGGED("GraphUserTaggedStory"), + // app story_type + COMMENT_LIKE("13"), + TAGGED_COMMENT("14"), + RESPONDED_STORY("213"), + // efr REQUEST("REQUEST"); private final String itemType; diff --git a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java index ba3db923..84dc3291 100644 --- a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java @@ -12,6 +12,9 @@ import retrofit2.http.Path; import retrofit2.http.QueryMap; public interface MediaRepository { + @GET("/api/v1/media/{mediaId}/info/") + Call fetch(@Path("mediaId") final String mediaId); + @GET("/api/v1/media/{mediaId}/likers/") Call fetchLikes(@Path("mediaId") final String mediaId); diff --git a/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java b/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java index e8e71116..6271cd81 100644 --- a/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/NewsRepository.java @@ -6,16 +6,19 @@ import awais.instagrabber.utils.Constants; import retrofit2.Call; import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; +import retrofit2.http.GET; import retrofit2.http.Header; import retrofit2.http.Headers; import retrofit2.http.POST; +import retrofit2.http.Query; public interface NewsRepository { - Call inbox(); - - @FormUrlEncoded @Headers("User-Agent: " + Constants.USER_AGENT) - @POST("https://www.instagram.com/web/activity/mark_checked/") - Call markChecked(@Header("x-csrftoken") String csrfToken, @FieldMap Map map); + @GET("https://www.instagram.com/accounts/activity/?__a=1") + Call webInbox(); + + @Headers("User-Agent: " + Constants.I_USER_AGENT) + @GET("/api/v1/news/inbox/") + Call appInbox(@Query(value = "mark_as_seen", encoded = true) boolean markAsSeen); } diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.java b/app/src/main/java/awais/instagrabber/webservices/MediaService.java index ed8304f3..65c978dd 100644 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.java +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.java @@ -16,9 +16,11 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.ProfileModel; import awais.instagrabber.repositories.MediaRepository; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.Utils; import retrofit2.Call; import retrofit2.Callback; @@ -46,6 +48,37 @@ public class MediaService extends BaseService { return instance; } + public void fetch(final String mediaId, + final ServiceCallback callback) { + final Call request = repository.fetch(mediaId); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (callback == null) return; + final String body = response.body(); + if (body == null) { + callback.onSuccess(null); + return; + } + try { + final JSONObject itemJson = new JSONObject(body).getJSONArray("items").getJSONObject(0); + callback.onSuccess(ResponseBodyUtils.parseItem(itemJson)); + } catch (JSONException e) { + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + public void like(final String mediaId, final String userId, final String csrfToken, diff --git a/app/src/main/java/awais/instagrabber/webservices/NewsService.java b/app/src/main/java/awais/instagrabber/webservices/NewsService.java index af56f058..76407003 100644 --- a/app/src/main/java/awais/instagrabber/webservices/NewsService.java +++ b/app/src/main/java/awais/instagrabber/webservices/NewsService.java @@ -1,14 +1,25 @@ package awais.instagrabber.webservices; +import android.util.Log; + import androidx.annotation.NonNull; +import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import awais.instagrabber.models.NotificationModel; +import awais.instagrabber.models.enums.NotificationType; import awais.instagrabber.repositories.NewsRepository; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.NetworkUtils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -35,25 +46,34 @@ public class NewsService extends BaseService { return instance; } - public void markChecked(final String timestamp, - final String csrfToken, - final ServiceCallback callback) { - final Map map = new HashMap<>(); - map.put("timestamp", timestamp); - final Call request = repository.markChecked(csrfToken, map); + public void fetchAppInbox(final boolean markAsSeen, + final ServiceCallback> callback) { + final List result = new ArrayList<>(); + final Call request = repository.appInbox(markAsSeen); request.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { final String body = response.body(); if (body == null) { - callback.onSuccess(false); + callback.onSuccess(null); return; } try { final JSONObject jsonObject = new JSONObject(body); - final String status = jsonObject.optString("status"); - callback.onSuccess(status.equals("ok")); + final JSONArray oldStories = jsonObject.getJSONArray("old_stories"), + newStories = jsonObject.getJSONArray("new_stories"); + for (int j = 0; j < newStories.length(); ++j) { + final NotificationModel newsItem = parseNewsItem(newStories.getJSONObject(j)); + if (newsItem != null) result.add(newsItem); + } + + for (int i = 0; i < oldStories.length(); ++i) { + final NotificationModel newsItem = parseNewsItem(oldStories.getJSONObject(i)); + if (newsItem != null) result.add(newsItem); + } + + callback.onSuccess(result); } catch (JSONException e) { callback.onFailure(e); } @@ -66,4 +86,112 @@ public class NewsService extends BaseService { } }); } + + public void fetchWebInbox(final boolean markAsSeen, + final ServiceCallback> callback) { + final List result = new ArrayList<>(); + final Call request = repository.webInbox(); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String body = response.body(); + if (body == null) { + callback.onSuccess(null); + return; + } + try { + final JSONObject page = new JSONObject(body) + .getJSONObject("graphql") + .getJSONObject("user"); + final JSONObject ewaf = page.getJSONObject("activity_feed") + .optJSONObject("edge_web_activity_feed"); + final JSONObject efr = page.optJSONObject("edge_follow_requests"); + JSONObject data; + JSONArray media; + if (ewaf != null + && (media = ewaf.optJSONArray("edges")) != null + && media.length() > 0 + && media.optJSONObject(0).optJSONObject("node") != null) { + for (int i = 0; i < media.length(); ++i) { + data = media.optJSONObject(i).optJSONObject("node"); + if (data == null) continue; + final String type = data.getString("__typename"); + final NotificationType notificationType = NotificationType.valueOfType(type); + if (notificationType == null) continue; + final JSONObject user = data.getJSONObject("user"); + result.add(new NotificationModel( + data.getString(Constants.EXTRAS_ID), + data.optString("text"), // comments or mentions + data.getLong("timestamp"), + user.getString("id"), + user.getString("username"), + user.getString("profile_pic_url"), + !data.isNull("media") ? data.getJSONObject("media").getString("id") : null, + !data.isNull("media") ? data.getJSONObject("media").getString("thumbnail_src") : null, notificationType)); + } + } + + if (efr != null + && (media = efr.optJSONArray("edges")) != null + && media.length() > 0 + && media.optJSONObject(0).optJSONObject("node") != null) { + for (int i = 0; i < media.length(); ++i) { + data = media.optJSONObject(i).optJSONObject("node"); + if (data == null) continue; + result.add(new NotificationModel( + data.getString(Constants.EXTRAS_ID), + data.optString("full_name"), + 0L, + data.getString(Constants.EXTRAS_ID), + data.getString("username"), + data.getString("profile_pic_url"), + null, + null, NotificationType.REQUEST)); + } + } + callback.onSuccess(result); + } catch (JSONException e) { + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + callback.onFailure(t); + // Log.e(TAG, "onFailure: ", t); + } + }); + } + + private NotificationModel parseNewsItem(final JSONObject itemJson) throws JSONException { + if (itemJson == null) return null; + final String type = itemJson.getString("story_type"); + final NotificationType notificationType = NotificationType.valueOfType(type); + if (notificationType == null) { + Log.d("austin_debug", "unhandled news type: "+itemJson); + return null; + } + final JSONObject data = itemJson.getJSONObject("args"); + return new NotificationModel( + data.getString("tuuid"), + data.has("text") ? data.getString("text") : cleanRichText(data.optString("rich_text", "")), + data.getLong("timestamp"), + data.getString("profile_id"), + data.getString("profile_name"), + data.getString("profile_image"), + !data.isNull("media") ? data.getJSONArray("media").getJSONObject(0).getString("id") : null, + !data.isNull("media") ? data.getJSONArray("media").getJSONObject(0).getString("image") : null, + notificationType); + } + + private String cleanRichText(final String raw) { + final Matcher matcher = Pattern.compile("\\{[\\p{L}\\d._]+\\|000000\\|1\\|user\\?id=\\d+\\}").matcher(raw); + String result = raw; + while (matcher.find()) { + final String richObject = raw.substring(matcher.start(), matcher.end()); + final String username = richObject.split("\\|")[0].substring(1); + result = result.replace(richObject, username); + } + return result; + } } diff --git a/app/src/main/res/layout/item_notification.xml b/app/src/main/res/layout/item_notification.xml index a3dee112..1ec1c187 100644 --- a/app/src/main/res/layout/item_notification.xml +++ b/app/src/main/res/layout/item_notification.xml @@ -105,13 +105,13 @@ android:gravity="end" android:paddingStart="8dp" android:paddingLeft="8dp" - android:paddingEnd="16dp" + android:paddingEnd="4dp" android:paddingRight="16dp" android:singleLine="true" android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:textStyle="italic" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/ivPreviewPic" app:layout_constraintStart_toEndOf="@id/ivProfilePic" app:layout_constraintTop_toBottomOf="@id/tvSubComment" tools:text="some long long long long long date" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e11ef39f..432a9b4b 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -145,6 +145,7 @@ Cannot delete currently in use account Are you sure you want to delete \'%s\'? Open profile + View story View profile picture You Shared a link