Browse Source

rewrite stories backend, close #319, close #372, close #405

renovate/org.robolectric-robolectric-4.x
Austin Huang 4 years ago
parent
commit
c24fd016b1
No known key found for this signature in database GPG Key ID: 84C23AA04587A91F
  1. 89
      app/src/main/java/awais/instagrabber/asyncs/QuizAction.java
  2. 88
      app/src/main/java/awais/instagrabber/asyncs/RespondAction.java
  3. 69
      app/src/main/java/awais/instagrabber/asyncs/VoteAction.java
  4. 185
      app/src/main/java/awais/instagrabber/fragments/StoryViewerFragment.java
  5. 5
      app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
  6. 10
      app/src/main/java/awais/instagrabber/models/StoryModel.java
  7. 53
      app/src/main/java/awais/instagrabber/models/stickers/SliderModel.java
  8. 20
      app/src/main/java/awais/instagrabber/repositories/StoriesRepository.java
  9. 20
      app/src/main/java/awais/instagrabber/repositories/responses/StoryStickerResponse.java
  10. 129
      app/src/main/java/awais/instagrabber/webservices/StoriesService.java
  11. 10
      app/src/main/res/layout/fragment_story_viewer.xml
  12. 6
      app/src/main/res/values/strings.xml

89
app/src/main/java/awais/instagrabber/asyncs/QuizAction.java

@ -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<Integer, Void, Integer> {
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);
}
}

88
app/src/main/java/awais/instagrabber/asyncs/RespondAction.java

@ -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<String, Void, Boolean> {
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);
}
}

69
app/src/main/java/awais/instagrabber/asyncs/VoteAction.java

@ -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<Integer, Void, Integer> {
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);
}
}

185
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<StoryModel> 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);
if (!TextUtils.isEmpty(cookie)) {
sticking = true;
storiesService.respondToPoll(
currentStory.getStoryMediaId().split("_")[0],
poll.getId(),
w,
userIdFromCookie,
CookieUtils.getCsrfTokenFromCookie(cookie),
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
poll.setMyChoice(w);
Toast.makeText(context, R.string.votef_story_poll, Toast.LENGTH_SHORT).show();
return;
}
@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();
}).execute(w);
}
});
}
})
.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) {
.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<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
} else
}
@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();
}).execute(input.getText().toString()))
}
});
})
.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);
if (quiz.getMyChoice() == -1 && !TextUtils.isEmpty(cookie)) {
sticking = true;
storiesService.respondToQuiz(
currentStory.getStoryMediaId().split("_")[0],
quiz.getId(),
w,
userIdFromCookie,
CookieUtils.getCsrfTokenFromCookie(cookie),
new ServiceCallback<StoryStickerResponse>() {
@Override
public void onSuccess(final StoryStickerResponse result) {
sticking = false;
quiz.setMyChoice(w);
Toast.makeText(context, R.string.answered_story, Toast.LENGTH_SHORT).show();
return;
}
@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();
}).execute(w);
}
});
}
})
.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<StoryStickerResponse>() {
@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();

5
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<List<FeedStoryModel>>() {
storiesService.getFeedStories(CookieUtils.getCsrfTokenFromCookie(cookie), new ServiceCallback<List<FeedStoryModel>>() {
@Override
public void onSuccess(final List<FeedStoryModel> result) {
feedStoriesViewModel.getList().postValue(result);

10
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;
}

53
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;
}
}

20
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<String> getStories(@QueryMap(encoded = true) Map<String, String> variables);
@FormUrlEncoded
@POST("/api/v1/feed/reels_tray/")
Call<String> getStories(@Header("User-Agent") String userAgent,
@FieldMap Map<String, String> form);
@GET
Call<String> getUserStory(@Header("User-Agent") String userAgent, @Url String url);
@FormUrlEncoded
@POST("/api/v1/media/{storyId}/{stickerId}/{action}/")
Call<StoryStickerResponse> 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<String, String> form);
}

20
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 + '\'' +
'}';
}
}

129
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<List<FeedStoryModel>> callback) {
public void getFeedStories(final String csrfToken, final ServiceCallback<List<FeedStoryModel>> callback) {
if (loadFromMock) {
final Handler handler = new Handler();
handler.postDelayed(() -> {
@ -81,10 +86,13 @@ public class StoriesService extends BaseService {
}, 1000);
return;
}
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("query_hash", "b7b84d884400bc5aa7cfe12ae843a091");
queryMap.put("variables", "{\"only_stories\":true,\"stories_prefetch\":false,\"stories_video_dash_manifest\":false}");
final Call<String> response = repository.getStories(queryMap);
final Map<String, Object> 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<String, String> signedForm = Utils.sign(form);
final Call<String> response = repository.getStories(Constants.I_USER_AGENT, signedForm);
response.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
@ -106,17 +114,12 @@ public class StoriesService extends BaseService {
private void parseStoriesBody(final String body, final ServiceCallback<List<FeedStoryModel>> callback) {
try {
final List<FeedStoryModel> 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<StoryStickerResponse> callback) {
final Map<String, Object> 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<String, String> signedForm = Utils.sign(form);
final Call<StoryStickerResponse> request =
repository.respondToSticker(Constants.I_USER_AGENT, storyId, stickerId, action, signedForm);
request.enqueue(new Callback<StoryStickerResponse>() {
@Override
public void onResponse(@NonNull final Call<StoryStickerResponse> call,
@NonNull final Response<StoryStickerResponse> response) {
if (callback != null) {
callback.onSuccess(response.body());
}
}
@Override
public void onFailure(@NonNull final Call<StoryStickerResponse> 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<StoryStickerResponse> 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<StoryStickerResponse> 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<StoryStickerResponse> 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<StoryStickerResponse> 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();

10
app/src/main/res/layout/fragment_story_viewer.xml

@ -85,6 +85,16 @@
android:visibility="gone"
app:backgroundTint="@color/btn_blue_background" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/story_slider"
android:textColor="@color/btn_blue_text_color"
android:visibility="gone"
app:backgroundTint="@color/btn_blue_background" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/swipeUp"
android:layout_width="match_parent"

6
app/src/main/res/values/strings.xml

@ -66,9 +66,15 @@
<string name="respond_story">Respond</string>
<string name="answer_hint">Answer…</string>
<string name="answered_story">Answer successful!</string>
<plurals name="slider_info" comment="For slider stickers in stories, eg. 3 responses averaging 17.38%">
<item quantity="one">%d response averaging %s</item>
<item quantity="other">%d responses averaging %s</item>
</plurals>
<string name="slider_answer">Your answer: %s</string>
<string name="reply_story">Reply to story</string>
<string name="reply_hint">Reply…</string>
<string name="story_quiz">Quiz</string>
<string name="story_slider">Slider</string>
<string name="story_quizzed">You have already answered!</string>
<string name="story_mentions">Mentions</string>
<string name="priv_acc">This Account is Private</string>

Loading…
Cancel
Save