From c24fd016b14a64202f47bc3198ee50e6c1af7471 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Thu, 17 Dec 2020 15:13:41 -0500 Subject: [PATCH] rewrite stories backend, close #319, close #372, close #405 --- .../awais/instagrabber/asyncs/QuizAction.java | 89 -------- .../instagrabber/asyncs/RespondAction.java | 88 -------- .../awais/instagrabber/asyncs/VoteAction.java | 69 ------ .../fragments/StoryViewerFragment.java | 201 +++++++++++++++--- .../fragments/main/FeedFragment.java | 5 +- .../awais/instagrabber/models/StoryModel.java | 10 + .../models/stickers/SliderModel.java | 53 +++++ .../repositories/StoriesRepository.java | 20 +- .../responses/StoryStickerResponse.java | 20 ++ .../webservices/StoriesService.java | 129 +++++++++-- .../main/res/layout/fragment_story_viewer.xml | 10 + app/src/main/res/values/strings.xml | 6 + 12 files changed, 408 insertions(+), 292 deletions(-) delete mode 100644 app/src/main/java/awais/instagrabber/asyncs/QuizAction.java delete mode 100644 app/src/main/java/awais/instagrabber/asyncs/RespondAction.java delete mode 100644 app/src/main/java/awais/instagrabber/asyncs/VoteAction.java create mode 100755 app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java create mode 100644 app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java diff --git a/app/src/main/java/awais/instagrabber/asyncs/QuizAction.java b/app/src/main/java/awais/instagrabber/asyncs/QuizAction.java deleted file mode 100644 index 4042b875..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/QuizAction.java +++ /dev/null @@ -1,89 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import org.json.JSONObject; - -import java.io.DataOutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.UUID; - -import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.stickers.QuizModel; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.Utils; - -import static awais.instagrabber.utils.Utils.settingsHelper; - -public class QuizAction extends AsyncTask { - private static final String TAG = "QuizAction"; - - private final StoryModel storyModel; - private final QuizModel quizModel; - private final String cookie; - private final OnTaskCompleteListener onTaskCompleteListener; - - public QuizAction(final StoryModel storyModel, - final QuizModel quizModel, - final String cookie, - final OnTaskCompleteListener onTaskCompleteListener) { - this.storyModel = storyModel; - this.quizModel = quizModel; - this.cookie = cookie; - this.onTaskCompleteListener = onTaskCompleteListener; - } - - protected Integer doInBackground(Integer... rawChoice) { - int choice = rawChoice[0]; - final String url = "https://i.instagram.com/api/v1/media/" + storyModel.getStoryMediaId().split("_")[0] + "/" + quizModel.getId() + "/story_quiz_answer/"; - HttpURLConnection urlConnection = null; - try { - JSONObject ogBody = new JSONObject("{\"client_context\":\"" + UUID.randomUUID().toString() - + "\",\"mutation_token\":\"" + UUID.randomUUID().toString() - + "\",\"_csrftoken\":\"" + cookie.split("csrftoken=")[1].split(";")[0] - + "\",\"_uid\":\"" + CookieUtils.getUserIdFromCookie(cookie) - + "\",\"__uuid\":\"" + settingsHelper.getString(Constants.DEVICE_UUID) - + "\"}"); - ogBody.put("answer", String.valueOf(choice)); - String urlParameters = Utils.sign(ogBody.toString()); - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setUseCaches(false); - urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT); - urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - if (urlParameters != null) { - urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length)); - } - urlConnection.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); - wr.writeBytes(urlParameters); - wr.flush(); - wr.close(); - Log.d(TAG, "quiz: " + url + " " + cookie + " " + urlParameters); - urlConnection.connect(); - if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { - return choice; - } - } catch (Throwable ex) { - Log.e(TAG, "quiz: " + ex); - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - return -1; - } - - @Override - protected void onPostExecute(final Integer choice) { - if (onTaskCompleteListener == null || choice == null) return; - onTaskCompleteListener.onTaskComplete(choice); - } - - public interface OnTaskCompleteListener { - void onTaskComplete(final int choice); - } -} diff --git a/app/src/main/java/awais/instagrabber/asyncs/RespondAction.java b/app/src/main/java/awais/instagrabber/asyncs/RespondAction.java deleted file mode 100644 index 836d6b76..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/RespondAction.java +++ /dev/null @@ -1,88 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import org.json.JSONObject; - -import java.io.DataOutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.UUID; - -import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.stickers.QuestionModel; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.Utils; - -import static awais.instagrabber.utils.Utils.settingsHelper; - -public class RespondAction extends AsyncTask { - - private final StoryModel storyModel; - private final QuestionModel questionModel; - private final String cookie; - private final OnTaskCompleteListener onTaskCompleteListener; - - public RespondAction(final StoryModel storyModel, - final QuestionModel questionModel, - final String cookie, - final OnTaskCompleteListener onTaskCompleteListener) { - this.storyModel = storyModel; - this.questionModel = questionModel; - this.cookie = cookie; - this.onTaskCompleteListener = onTaskCompleteListener; - } - - protected Boolean doInBackground(String... rawChoice) { - final String url = "https://i.instagram.com/api/v1/media/" - + storyModel.getStoryMediaId().split("_")[0] + "/" + questionModel.getId() + "/story_question_response/"; - HttpURLConnection urlConnection = null; - try { - final JSONObject ogbody = new JSONObject("{\"client_context\":\"" + UUID.randomUUID().toString() - + "\",\"mutation_token\":\"" + UUID.randomUUID().toString() - + "\",\"_csrftoken\":\"" + cookie.split("csrftoken=")[1].split(";")[0] - + "\",\"_uid\":\"" + CookieUtils.getUserIdFromCookie(cookie) - + "\",\"__uuid\":\"" + settingsHelper.getString(Constants.DEVICE_UUID) - + "\"}"); - String choice = rawChoice[0].replaceAll("\"", ("\\\"")); - ogbody.put("response", choice); - String urlParameters = Utils.sign(ogbody.toString()); - urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setUseCaches(false); - urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT); - urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length)); - urlConnection.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); - wr.writeBytes(urlParameters); - wr.flush(); - wr.close(); - urlConnection.connect(); - if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { - return true; - } - - } catch (Throwable ex) { - Log.e("austin_debug", "respond: " + ex); - } finally { - if (urlConnection != null) { - urlConnection.disconnect(); - } - } - return null; - } - - @Override - protected void onPostExecute(final Boolean ok) { - if (onTaskCompleteListener == null) return; - onTaskCompleteListener.onTaskComplete(ok); - - } - - public interface OnTaskCompleteListener { - void onTaskComplete(final boolean result); - } -} diff --git a/app/src/main/java/awais/instagrabber/asyncs/VoteAction.java b/app/src/main/java/awais/instagrabber/asyncs/VoteAction.java deleted file mode 100644 index cfa91c77..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/VoteAction.java +++ /dev/null @@ -1,69 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; - -import java.io.DataOutputStream; -import java.net.HttpURLConnection; -import java.net.URL; - -import awais.instagrabber.models.StoryModel; -import awais.instagrabber.models.stickers.PollModel; -import awais.instagrabber.utils.Constants; - -public class VoteAction extends AsyncTask { - - private static final String TAG = "VoteAction"; - - private final StoryModel storyModel; - private final PollModel pollModel; - private final String cookie; - private final OnTaskCompleteListener onTaskCompleteListener; - - public VoteAction(final StoryModel storyModel, - final PollModel pollModel, - final String cookie, - final OnTaskCompleteListener onTaskCompleteListener) { - this.storyModel = storyModel; - this.pollModel = pollModel; - this.cookie = cookie; - this.onTaskCompleteListener = onTaskCompleteListener; - } - - protected Integer doInBackground(Integer... rawChoice) { - int choice = rawChoice[0]; - final String url = "https://www.instagram.com/media/" + storyModel.getStoryMediaId().split("_")[0] + "/" + pollModel.getId() + "/story_poll_vote/"; - try { - final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod("POST"); - urlConnection.setUseCaches(false); - urlConnection.setRequestProperty("User-Agent", Constants.USER_AGENT); - urlConnection.setRequestProperty("x-csrftoken", cookie.split("csrftoken=")[1].split(";")[0]); - urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - urlConnection.setRequestProperty("Content-Length", "6"); - urlConnection.setDoOutput(true); - DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); - wr.writeBytes("vote=" + choice); - wr.flush(); - wr.close(); - urlConnection.connect(); - if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { - return choice; - } - urlConnection.disconnect(); - } catch (Exception ex) { - Log.e(TAG, "Error", ex); - } - return -1; - } - - @Override - protected void onPostExecute(final Integer result) { - if (result == null || onTaskCompleteListener == null) return; - onTaskCompleteListener.onTaskComplete(result); - } - - public interface OnTaskCompleteListener { - void onTaskComplete(final int choice); - } -} diff --git a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java index 40f04fd3..1a90128c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java @@ -11,6 +11,7 @@ import android.os.Handler; import android.util.Log; import android.util.Pair; import android.view.GestureDetector; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -20,6 +21,9 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -55,6 +59,7 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.text.NumberFormat; import java.util.Collections; import java.util.Date; import java.util.List; @@ -63,10 +68,7 @@ import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.adapters.StoriesAdapter; import awais.instagrabber.asyncs.PostFetcher; -import awais.instagrabber.asyncs.QuizAction; -import awais.instagrabber.asyncs.RespondAction; import awais.instagrabber.asyncs.SeenAction; -import awais.instagrabber.asyncs.VoteAction; import awais.instagrabber.asyncs.direct_messages.CreateThreadAction; import awais.instagrabber.asyncs.direct_messages.DirectThreadBroadcaster; import awais.instagrabber.customviews.helpers.SwipeGestureListener; @@ -80,7 +82,10 @@ import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.models.stickers.PollModel; import awais.instagrabber.models.stickers.QuestionModel; import awais.instagrabber.models.stickers.QuizModel; +import awais.instagrabber.models.stickers.SliderModel; +import awais.instagrabber.repositories.responses.StoryStickerResponse; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -117,13 +122,15 @@ public class StoryViewerFragment extends Fragment { private QuestionModel question; private String[] mentions; private QuizModel quiz; + private SliderModel slider; private MenuItem menuDownload; private MenuItem menuDm; private SimpleExoPlayer player; private boolean isHashtag, isLoc; private String highlight; - private boolean fetching = false; + private boolean fetching = false, sticking = false; private int currentFeedStoryIndex; + private double sliderValue; private StoriesViewModel storiesViewModel; private boolean shouldRefresh = true; private StoryViewerFragmentArgs fragmentArgs; @@ -190,7 +197,7 @@ public class StoryViewerFragment extends Fragment { new AlertDialog.Builder(context) .setTitle(R.string.reply_story) .setView(input) - .setPositiveButton(R.string.ok, (d, w) -> new CreateThreadAction(cookie, currentStory.getUserId(), threadId -> { + .setPositiveButton(R.string.confirm, (d, w) -> new CreateThreadAction(cookie, currentStory.getUserId(), threadId -> { try { final DirectThreadBroadcaster.StoryReplyBroadcastOptions options = new DirectThreadBroadcaster.StoryReplyBroadcastOptions( input.getText().toString(), @@ -273,6 +280,7 @@ public class StoryViewerFragment extends Fragment { @SuppressLint("ClickableViewAccessibility") private void setupListeners() { + final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); final boolean hasFeedStories; List models = null; if (currentFeedStoryIndex >= 0) { @@ -297,6 +305,10 @@ public class StoryViewerFragment extends Fragment { swipeEvent = isRightSwipe -> { final List storyModels = storiesViewModel.getList().getValue(); final int storiesLen = storyModels == null ? 0 : storyModels.size(); + if (sticking) { + Toast.makeText(context, R.string.follower_wait_to_load, Toast.LENGTH_SHORT).show(); + return; + } if (storiesLen <= 0) return; final boolean isLeftSwipe = !isRightSwipe; final boolean endOfCurrentStories = slidePos + 1 >= storiesLen; @@ -407,15 +419,30 @@ public class StoryViewerFragment extends Fragment { poll.getLeftChoice() + " (" + poll.getLeftCount() + ")", poll.getRightChoice() + " (" + poll.getRightCount() + ")" }), (d, w) -> { - if (!TextUtils.isEmpty(cookie)) - new VoteAction(currentStory, poll, cookie, choice -> { - if (choice > -1) { - poll.setMyChoice(choice); - Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); - return; - } - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - }).execute(w); + if (!TextUtils.isEmpty(cookie)) { + sticking = true; + storiesService.respondToPoll( + currentStory.getStoryMediaId().split("_")[0], + poll.getId(), + w, + userIdFromCookie, + CookieUtils.getCsrfTokenFromCookie(cookie), + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + poll.setMyChoice(w); + Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + } }) .setPositiveButton(R.string.cancel, null) .show(); @@ -427,12 +454,29 @@ public class StoryViewerFragment extends Fragment { new AlertDialog.Builder(context) .setTitle(question.getQuestion()) .setView(input) - .setPositiveButton(R.string.ok, (d, w) -> new RespondAction(currentStory, question, cookie, result -> { - if (result) { - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - } else - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - }).execute(input.getText().toString())) + .setPositiveButton(R.string.confirm, (d, w) -> { + sticking = true; + storiesService.respondToQuestion( + currentStory.getStoryMediaId().split("_")[0], + question.getId(), + input.getText().toString(), + userIdFromCookie, + CookieUtils.getCsrfTokenFromCookie(cookie), + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + }) .setNegativeButton(R.string.cancel, null) .show(); } else if (tag instanceof String[]) { @@ -452,24 +496,119 @@ public class StoryViewerFragment extends Fragment { new AlertDialog.Builder(context) .setTitle(quiz.getMyChoice() > -1 ? getString(R.string.story_quizzed) : quiz.getQuestion()) .setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, choices), (d, w) -> { - if (quiz.getMyChoice() == -1 && !TextUtils.isEmpty(cookie)) - new QuizAction(currentStory, quiz, cookie, choice -> { - if (choice > -1) { - quiz.setMyChoice(choice); - Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); - return; - } - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - }).execute(w); + if (quiz.getMyChoice() == -1 && !TextUtils.isEmpty(cookie)) { + sticking = true; + storiesService.respondToQuiz( + currentStory.getStoryMediaId().split("_")[0], + quiz.getId(), + w, + userIdFromCookie, + CookieUtils.getCsrfTokenFromCookie(cookie), + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + quiz.setMyChoice(w); + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + } }) .setPositiveButton(R.string.cancel, null) .show(); + } else if (tag instanceof SliderModel) { + slider = (SliderModel) tag; + NumberFormat percentage = NumberFormat.getPercentInstance(); + percentage.setMaximumFractionDigits(2); + LinearLayout sliderView = new LinearLayout(context); + sliderView.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + sliderView.setOrientation(LinearLayout.VERTICAL); + TextView tv = new TextView(context); + tv.setGravity(Gravity.CENTER_HORIZONTAL); + final SeekBar input = new SeekBar(context); + Double avg = slider.getAverage() * 100; + input.setProgress(avg.intValue()); + sliderView.addView(input); + sliderView.addView(tv); + if (slider.getMyChoice().isNaN() && slider.canVote()) { + input.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + sliderValue = progress / 100.0; + tv.setText(percentage.format(sliderValue)); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + new AlertDialog.Builder(context) + .setTitle(TextUtils.isEmpty(slider.getQuestion()) ? slider.getEmoji() : slider.getQuestion()) + .setMessage(getResources().getQuantityString(R.plurals.slider_info, + slider.getVoteCount(), + slider.getVoteCount(), + percentage.format(slider.getAverage()))) + .setView(sliderView) + .setPositiveButton(R.string.confirm, (d, w) -> { + sticking = true; + storiesService.respondToSlider( + currentStory.getStoryMediaId().split("_")[0], + slider.getId(), + sliderValue, + userIdFromCookie, + CookieUtils.getCsrfTokenFromCookie(cookie), + new ServiceCallback() { + @Override + public void onSuccess(final StoryStickerResponse result) { + sticking = false; + slider.setMyChoice(sliderValue); + Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show(); + } + + @Override + public void onFailure(final Throwable t) { + sticking = false; + Log.e(TAG, "Error responding", t); + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + else { + input.setEnabled(false); + tv.setText(getString(R.string.slider_answer, percentage.format(slider.getMyChoice()))); + new AlertDialog.Builder(context) + .setTitle(TextUtils.isEmpty(slider.getQuestion()) ? slider.getEmoji() : slider.getQuestion()) + .setMessage(getResources().getQuantityString(R.plurals.slider_info, + slider.getVoteCount(), + slider.getVoteCount(), + percentage.format(slider.getAverage()))) + .setView(sliderView) + .setPositiveButton(R.string.ok, null) + .show(); + } } }; binding.poll.setOnClickListener(storyActionListener); binding.answer.setOnClickListener(storyActionListener); binding.mention.setOnClickListener(storyActionListener); binding.quiz.setOnClickListener(storyActionListener); + binding.slider.setOnClickListener(storyActionListener); } private void resetView() { @@ -588,6 +727,10 @@ public class StoryViewerFragment extends Fragment { binding.quiz.setVisibility(quiz != null ? View.VISIBLE : View.GONE); binding.quiz.setTag(quiz); + slider = currentStory.getSlider(); + binding.slider.setVisibility(slider != null ? View.VISIBLE : View.GONE); + binding.slider.setTag(slider); + releasePlayer(); if (isHashtag || isLoc) { final ActionBar actionBar = fragmentActivity.getSupportActionBar(); diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index afc14848..91fbb6f8 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -47,6 +47,7 @@ import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.FeedStoryModel; import awais.instagrabber.models.PostsLayoutPreferences; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.FeedStoriesViewModel; @@ -55,6 +56,7 @@ import awais.instagrabber.webservices.StoriesService; import static androidx.core.content.PermissionChecker.checkSelfPermission; import static awais.instagrabber.utils.DownloadUtils.WRITE_PERMISSION; +import static awais.instagrabber.utils.Utils.settingsHelper; public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "FeedFragment"; @@ -367,9 +369,10 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre } private void fetchStories() { + final String cookie = settingsHelper.getString(Constants.COOKIE); storiesFetching = true; updateSwipeRefreshState(); - storiesService.getFeedStories(new ServiceCallback>() { + storiesService.getFeedStories(CookieUtils.getCsrfTokenFromCookie(cookie), new ServiceCallback>() { @Override public void onSuccess(final List result) { feedStoriesViewModel.getList().postValue(result); diff --git a/app/src/main/java/awais/instagrabber/models/StoryModel.java b/app/src/main/java/awais/instagrabber/models/StoryModel.java index 9d3959c8..c9615c8b 100755 --- a/app/src/main/java/awais/instagrabber/models/StoryModel.java +++ b/app/src/main/java/awais/instagrabber/models/StoryModel.java @@ -6,6 +6,7 @@ import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.models.stickers.PollModel; import awais.instagrabber.models.stickers.QuestionModel; import awais.instagrabber.models.stickers.QuizModel; +import awais.instagrabber.models.stickers.SliderModel; import awais.instagrabber.models.stickers.SwipeUpModel; public final class StoryModel implements Serializable { @@ -21,6 +22,7 @@ public final class StoryModel implements Serializable { private String spotify; private PollModel poll; private QuestionModel question; + private SliderModel slider; private QuizModel quiz; private SwipeUpModel swipeUp; private String[] mentions; @@ -71,6 +73,10 @@ public final class StoryModel implements Serializable { return question; } + public SliderModel getSlider() { + return slider; + } + public QuizModel getQuiz() { return quiz; } @@ -109,6 +115,10 @@ public final class StoryModel implements Serializable { this.question = question; } + public void setSlider(final SliderModel slider) { + this.slider = slider; + } + public void setQuiz(final QuizModel quiz) { this.quiz = quiz; } diff --git a/app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java b/app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java new file mode 100755 index 00000000..ff5c0f20 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java @@ -0,0 +1,53 @@ +package awais.instagrabber.models.stickers; + +import java.io.Serializable; + +public final class SliderModel implements Serializable { + private final int voteCount; + private final Double average; + private Double myChoice; + private final boolean canVote; + private final String id, question, emoji; + + public SliderModel(final String id, final String question, final String emoji, final boolean canVote, + final Double average, final int voteCount, final Double myChoice) { + this.id = id; + this.question = question; + this.emoji = emoji; + this.canVote = canVote; + this.average = average; + this.voteCount = voteCount; + this.myChoice = myChoice; + } + + public String getId() { + return id; + } + + public String getQuestion() { + return question; + } + + public String getEmoji() { + return emoji; + } + + public boolean canVote() { + return canVote; + } + + public int getVoteCount() { + return voteCount; + } + + public Double getAverage() { + return average; + } + + public Double getMyChoice() { return myChoice; } + + public Double setMyChoice(final Double choice) { + this.myChoice = choice; + return choice; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java b/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java index 42f262d6..8ed0da74 100644 --- a/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java @@ -2,17 +2,33 @@ package awais.instagrabber.repositories; import java.util.Map; +import awais.instagrabber.repositories.responses.StoryStickerResponse; import retrofit2.Call; +import retrofit2.http.FieldMap; +import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.Header; +import retrofit2.http.Path; +import retrofit2.http.POST; import retrofit2.http.QueryMap; import retrofit2.http.Url; public interface StoriesRepository { - @GET("graphql/query/") - Call getStories(@QueryMap(encoded = true) Map variables); + @FormUrlEncoded + @POST("/api/v1/feed/reels_tray/") + Call getStories(@Header("User-Agent") String userAgent, + @FieldMap Map form); @GET Call getUserStory(@Header("User-Agent") String userAgent, @Url String url); + + @FormUrlEncoded + @POST("/api/v1/media/{storyId}/{stickerId}/{action}/") + Call respondToSticker(@Header("User-Agent") String userAgent, + @Path("storyId") String storyId, + @Path("stickerId") String stickerId, + @Path("action") String action, + // story_poll_vote, story_question_response, story_slider_vote, story_quiz_answer + @FieldMap Map form); } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java new file mode 100644 index 00000000..8ac2f4a9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java @@ -0,0 +1,20 @@ +package awais.instagrabber.repositories.responses; + +public class StoryStickerResponse { + private final String status; + + public StoryStickerResponse(final String status) { + this.status = status; + } + + public String getStatus() { + return status; + } + + @Override + public String toString() { + return "StoryStickerResponse{" + + "status='" + status + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java index ecc657d1..c895a3fa 100644 --- a/app/src/main/java/awais/instagrabber/webservices/StoriesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/StoriesService.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import awais.instagrabber.models.FeedStoryModel; import awais.instagrabber.models.ProfileModel; @@ -26,9 +27,13 @@ import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.models.stickers.PollModel; import awais.instagrabber.models.stickers.QuestionModel; import awais.instagrabber.models.stickers.QuizModel; +import awais.instagrabber.models.stickers.SliderModel; +import awais.instagrabber.models.stickers.SwipeUpModel; import awais.instagrabber.repositories.StoriesRepository; +import awais.instagrabber.repositories.responses.StoryStickerResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.ResponseBodyUtils; +import awais.instagrabber.utils.Utils; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -44,7 +49,7 @@ public class StoriesService extends BaseService { private StoriesService() { final Retrofit retrofit = getRetrofitBuilder() - .baseUrl("https://www.instagram.com") + .baseUrl("https://i.instagram.com") .build(); repository = retrofit.create(StoriesRepository.class); } @@ -56,7 +61,7 @@ public class StoriesService extends BaseService { return instance; } - public void getFeedStories(final ServiceCallback> callback) { + public void getFeedStories(final String csrfToken, final ServiceCallback> callback) { if (loadFromMock) { final Handler handler = new Handler(); handler.postDelayed(() -> { @@ -81,10 +86,13 @@ public class StoriesService extends BaseService { }, 1000); return; } - final Map queryMap = new HashMap<>(); - queryMap.put("query_hash", "b7b84d884400bc5aa7cfe12ae843a091"); - queryMap.put("variables", "{\"only_stories\":true,\"stories_prefetch\":false,\"stories_video_dash_manifest\":false}"); - final Call response = repository.getStories(queryMap); + final Map form = new HashMap<>(4); + form.put("reason", "cold_start"); + form.put("_csrftoken", csrfToken); + form.put("_uuid", UUID.randomUUID().toString()); + form.put("supported_capabilities_new", Constants.SUPPORTED_CAPABILITIES); + final Map signedForm = Utils.sign(form); + final Call response = repository.getStories(Constants.I_USER_AGENT, signedForm); response.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -106,17 +114,12 @@ public class StoriesService extends BaseService { private void parseStoriesBody(final String body, final ServiceCallback> callback) { try { final List feedStoryModels = new ArrayList<>(); - final JSONArray feedStoriesReel = new JSONObject(body) - .getJSONObject("data") - .getJSONObject(Constants.EXTRAS_USER) - .getJSONObject("feed_reels_tray") - .getJSONObject("edge_reels_tray_to_reel") - .getJSONArray("edges"); + final JSONArray feedStoriesReel = new JSONObject(body).getJSONArray("tray"); for (int i = 0; i < feedStoriesReel.length(); ++i) { - final JSONObject node = feedStoriesReel.getJSONObject(i).getJSONObject("node"); + final JSONObject node = feedStoriesReel.getJSONObject(i); final JSONObject user = node.getJSONObject(node.has("user") ? "user" : "owner"); final ProfileModel profileModel = new ProfileModel(false, false, false, - user.getString("id"), + user.getString("pk"), user.getString("username"), null, null, null, user.getString("profile_pic_url"), @@ -240,6 +243,27 @@ public class StoriesService extends BaseService { )); } } + if (data.has("story_cta") && data.has("link_text")) { + JSONObject tappableObject = data.getJSONArray("story_cta").getJSONObject(0).getJSONArray("links").getJSONObject(0); + String swipeUpUrl = tappableObject.getString("webUri"); + if (swipeUpUrl.startsWith("http")) { + model.setSwipeUp(new SwipeUpModel(swipeUpUrl, data.getString("link_text"))); + } + } + if (data.has("story_sliders")) { + final JSONObject tappableObject = data.getJSONArray("story_sliders").getJSONObject(0) + .optJSONObject("slider_sticker"); + if (tappableObject != null) + model.setSlider(new SliderModel( + String.valueOf(tappableObject.getLong("slider_id")), + tappableObject.getString("question"), + tappableObject.getString("emoji"), + tappableObject.getBoolean("viewer_can_vote"), + tappableObject.getDouble("slider_vote_average"), + tappableObject.getInt("slider_vote_count"), + tappableObject.optDouble("viewer_vote") + )); + } JSONArray hashtags = data.optJSONArray("story_hashtags"); JSONArray locations = data.optJSONArray("story_locations"); JSONArray atmarks = data.optJSONArray("reel_mentions"); @@ -283,6 +307,83 @@ public class StoriesService extends BaseService { }); } + private void respondToSticker(final String storyId, + final String stickerId, + final String action, + final String arg1, + final String arg2, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + final Map form = new HashMap<>(); + form.put("_csrftoken", csrfToken); + form.put("_uid", userId); + form.put("_uuid", UUID.randomUUID().toString()); + form.put("mutation_token", UUID.randomUUID().toString()); + form.put("client_context", UUID.randomUUID().toString()); + form.put("radio_type", "wifi-none"); + form.put(arg1, arg2); + final Map signedForm = Utils.sign(form); + final Call request = + repository.respondToSticker(Constants.I_USER_AGENT, storyId, stickerId, action, signedForm); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (callback != null) { + callback.onSuccess(response.body()); + } + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } + + // RespondAction.java + public void respondToQuestion(final String storyId, + final String stickerId, + final String answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_question_response", "response", answer, userId, csrfToken, callback); + } + + // QuizAction.java + public void respondToQuiz(final String storyId, + final String stickerId, + final int answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), userId, csrfToken, callback); + } + + // VoteAction.java + public void respondToPoll(final String storyId, + final String stickerId, + final int answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); + } + + public void respondToSlider(final String storyId, + final String stickerId, + final double answer, + final String userId, + final String csrfToken, + final ServiceCallback callback) { + respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); + } + private String buildUrl(final String id, final boolean isLoc, final boolean isHashtag, final boolean highlight) { final String userId = id.replace(":", "%3A"); final StringBuilder builder = new StringBuilder(); diff --git a/app/src/main/res/layout/fragment_story_viewer.xml b/app/src/main/res/layout/fragment_story_viewer.xml index fb16343a..d9893f08 100644 --- a/app/src/main/res/layout/fragment_story_viewer.xml +++ b/app/src/main/res/layout/fragment_story_viewer.xml @@ -85,6 +85,16 @@ android:visibility="gone" app:backgroundTint="@color/btn_blue_background" /> + + Respond Answer… Answer successful! + + %d response averaging %s + %d responses averaging %s + + Your answer: %s Reply to story Reply… Quiz + Slider You have already answered! Mentions This Account is Private