├── .gitignore
├── README.md
├── pom.xml
└── src
└── main
└── java
└── com
└── habosa
└── javasnap
├── Encryption.java
├── Friend.java
├── JSONBinder.java
├── Main.java
├── Message.java
├── Snap.java
├── Snapchat.java
├── Story.java
├── StoryEncryption.java
├── TokenLib.java
└── Viewer.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Images
2 | *.jpg
3 |
4 | # Mac
5 | .DS_Store
6 |
7 | # IntelliJ
8 | .idea/
9 | *.iml
10 | *.iws
11 |
12 | # Maven
13 | log/
14 | target/
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](http://github.com/badges/stability-badges)
2 |
3 | **WARNING:** This library is deprecated and has not been in working order for years. Issues will not be answered.
4 |
5 |
6 |
7 | # JavaSnap - Unofficial Java API Client for Snapchat
8 |
9 | ## Overview
10 | JavaSnap provides a simple Java interface to the Snapchat API, which has been unofficially documented. It could be used to do the following, and more:
11 |
12 | * Download Snaps to your computer or Android device.
13 | * Send a local File as a Snap to your friends.
14 | * Most features from the original Snapchat app.
15 |
16 | ## Usage
17 | ### Build
18 |
19 | Build a `jar` with Maven using:
20 |
21 | mvn clean compile package
22 |
23 | ### Command Line Execution
24 | Run the `jar` in `target/` with:
25 |
26 | java -jar target/JavaSnap-1.1-SNAPSHOT-withDependency-ShadedForAndroid.jar
27 |
28 | It should look something like this:
29 |
30 | samuelsternsmbp:JavaSnap samstern$ java -jar target/JavaSnap-1.0-SNAPSHOT-jar-with-dependencies.jar
31 | Snapchat username:
32 | YOUR_USERNAME
33 | Snapchat password:
34 | YOUR_PASSWORD
35 | Logging in...
36 |
37 |
38 | Running the java library via the command line will allow you to send a local 'jpg' to a friend as a Snap or a Story, download all of your received 'jpg' snaps as well as downloading all of your friends stories.
39 |
40 | ### Using Library Functions
41 | #### Logging In
42 |
43 | Snapchat snapchat = Snapchat.login(username, password);
44 |
45 | #### Get Friends
46 | You can use the following code to get all your friends. Snapchat usernames and real names (as you have assigned them):
47 |
48 | Friend[] friends = snapchat.getFriends();
49 |
50 | #### Get Snaps
51 | You can use the following code to get all your snaps :
52 |
53 | Snap[] snaps = snapchat.getSnaps();
54 |
55 | A separate API call will be needed to download each `Snap`. The `getSnaps()` method will return some Snaps that are not available to download, such as already-viewed Snaps or snaps that don't contain media (such as friend requests). A `Snap` can be downloaded if `snap.isIncoming() == true`, `snap.isMedia() == true`, and `snap.isViewed() == false`.
56 | To get a list of only such snaps, you can pass the `Snap[]` to method `Snapchat.filterDownloadable(Snap[] snaps)`. You can also use `snaps[#].isDownloadable()`.
57 |
58 | #### Download a Snap
59 | Once you have determined a Snap candidate for downloading using the methods above, the following code will fetch the actual media and save it to a file:
60 |
61 | byte[] snapBytes = snapchat.getSnap(snap);
62 | File snapFile = new File(...);
63 | FileOutputStream snapOs = new FileOutputStream(snapFile);
64 | snapOs.write(snapBytes);
65 | snapOs.close();
66 |
67 | #### Sending a Snap
68 | Sending a Snap consists of two steps: uploading and sending. When you upload a Snap, you provide a unique identifier called `media_id` which you will use when sending the snap to its eventual recipients.
69 | Lucky you, the API will do everything for you in the background.
70 |
71 | The following code demonstrates uploading a `File` as a Snap:
72 |
73 | File file = new File(...);
74 | boolean video = false; //whether or not 'file' is a video or not.
75 | boolean story = false; //whether or not add this to your story.
76 | int time = 10; //How long will the snap last. Max = 10.
77 | List recipients = (...);
78 | String mediaId = Snapchat.upload(file, recipients, video, story, time);
79 |
80 | #### Setting a Story
81 | Setting a Story consists of two steps: uploading and setting. When you upload a Story, you provide a unique identifier called `media_id` which you will use when sentting the story.
82 | Lucky you, the API will do everything for you in the background.
83 |
84 | The following code demonstrates uploading a `File` as a Story:
85 |
86 | File file = new File(...);
87 | boolean video = false; //whether or not 'file' is a video or not.
88 | int time = 10; //How long will the snap last. Max = 10.
89 | String caption = "My Story"; //Can be anything. We couldn't find any effect.
90 | boolean result = snapchat.sendStory(file, video, time, caption);
91 |
92 | #### Get Stories
93 | You can use the following code to get all your stories :
94 |
95 | Story[] storyObjs = snapchat.getStories();
96 | Story[] downloadable = Story.filterDownloadable(storyObjs); //All stories are downloadable but this makes the Story object in the same format as the Snap one.
97 |
98 | A separate API call will be needed to download each `Story`, you will need to pass the Story[] you want to download as argument.
99 |
100 | byte[] storyBytes = Snapchat.getStory(story);
101 |
102 | #### Update Snap information
103 | This method allows you to change the status of a specific snap/story. For example, marking the snap as viewed/screenshot/replayed.
104 | You need to pass in the snap object for the snap you want to update, a boolean for seen/screenshot/replayed.
105 |
106 | snapchat.setSnapFlags(snap, seen, screenshot, replayed)
107 |
108 |
109 |
110 | ## Other Information
111 |
112 | * This code is based on the Gibson Security guide to the Snapchat API [here](http://gibsonsec.org/snapchat/fulldisclosure/).
113 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | com.habosa
8 | JavaSnap
9 | 2.0-SNAPSHOT
10 | jar
11 |
12 |
13 |
14 |
15 | org.apache.maven.plugins
16 | maven-shade-plugin
17 | 2.2
18 |
19 |
20 | package
21 |
22 | shade
23 |
24 |
25 |
26 |
27 | com.habosa.javasnap.Main
28 |
29 |
30 | ${project.artifactId}-${project.version}-withDependency-ShadedForAndroid
31 |
32 |
33 | com.mashape.unirest:unirest-java
34 | org.apache.httpcomponents:httpclient
35 | org.apache.httpcomponents:httpcore
36 | org.apache.httpcomponents:httpcore-nio
37 | org.apache.httpcomponents:httpasyncclient
38 | org.apache.httpcomponents:httpmime
39 | org.json:json
40 | commons-logging:commons-logging
41 | commons-codec:commons-codec
42 | commons-io:commons-io
43 |
44 |
45 |
46 |
47 | org.apache.http
48 | com.mashape.relocation
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | com.mashape.unirest
61 | unirest-java
62 | 1.3.27
63 |
64 |
65 |
66 | commons-io
67 | commons-io
68 | 2.4
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Encryption.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import javax.crypto.*;
4 | import javax.crypto.spec.SecretKeySpec;
5 | import java.security.InvalidKeyException;
6 | import java.security.Key;
7 | import java.security.NoSuchAlgorithmException;
8 | import java.security.NoSuchProviderException;
9 |
10 | /**
11 | * Author: samstern
12 | * Date: 12/28/13
13 | */
14 | public class Encryption {
15 |
16 | private static final String KEY_ALG = "AES";
17 | private static final String AES_KEY = "M02cnQ51Ji97vwT4";
18 | private static final String CIPHER_MODE = "AES/ECB/PKCS5Padding";
19 |
20 | public static byte[] encrypt(byte[] data) throws EncryptionException {
21 |
22 | // Get AES-ECB with the right padding
23 | Cipher cipher = null;
24 | try {
25 | cipher = Cipher.getInstance(CIPHER_MODE, "BC"); //Try and use the BC provider for devices which throw key length errors.
26 | } catch (NoSuchAlgorithmException e) {
27 | throw new EncryptionException(e);
28 | } catch (NoSuchPaddingException e) {
29 | throw new EncryptionException(e);
30 | } catch (NoSuchProviderException e) {
31 | try{
32 | cipher = Cipher.getInstance(CIPHER_MODE); //Use this if BC provider not found.
33 | }
34 | catch(Exception er){
35 | throw new EncryptionException(er);
36 | }
37 | }
38 |
39 | // Set the key
40 | byte[] keyBytes = AES_KEY.getBytes();
41 | SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, KEY_ALG);
42 |
43 | // Initialize the Cipher
44 | try {
45 | cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
46 | } catch (InvalidKeyException e) {
47 | throw new EncryptionException(e);
48 | }
49 |
50 | // Encrypt the data
51 | try {
52 | byte[] result = cipher.doFinal(data);
53 | return result;
54 | } catch (IllegalBlockSizeException e) {
55 | throw new EncryptionException(e);
56 | } catch (BadPaddingException e) {
57 | throw new EncryptionException(e);
58 | }
59 | }
60 |
61 | public static byte[] decrypt(byte[] data) throws EncryptionException {
62 |
63 | Cipher cipher = null;
64 | try {
65 | cipher = Cipher.getInstance(CIPHER_MODE, "BC"); //Try and use the BC provider for devices which throw key length errors.
66 | } catch (NoSuchAlgorithmException e) {
67 | throw new EncryptionException(e);
68 | } catch (NoSuchPaddingException e) {
69 | throw new EncryptionException(e);
70 | } catch (NoSuchProviderException e) {
71 | try{
72 | cipher = Cipher.getInstance(CIPHER_MODE); //Use this if BC provider not found.
73 | }
74 | catch(Exception er){
75 | throw new EncryptionException(er);
76 | }
77 | }
78 |
79 | byte[] keyBytes = AES_KEY.getBytes();
80 | SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, KEY_ALG);
81 |
82 | // Only difference from encrypt method
83 | try {
84 | cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
85 | } catch (InvalidKeyException e) {
86 | throw new EncryptionException(e);
87 | }
88 |
89 | try {
90 | byte[] result = cipher.doFinal(data);
91 | return result;
92 | } catch (IllegalBlockSizeException e) {
93 | throw new EncryptionException(e);
94 | } catch (BadPaddingException e) {
95 | throw new EncryptionException(e);
96 | }
97 | }
98 |
99 | public static class EncryptionException extends Exception {
100 |
101 | private Exception cause;
102 |
103 | public EncryptionException(Exception e) {
104 | this.cause = e;
105 | }
106 |
107 | @Override
108 | public void printStackTrace() {
109 | cause.printStackTrace();
110 | this.printStackTrace();
111 | }
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Friend.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import org.json.JSONException;
4 | import org.json.JSONObject;
5 |
6 | /**
7 | * Author: samstern
8 | * Date: 12/31/13
9 | */
10 | public class Friend implements JSONBinder {
11 |
12 | private static final String USERNAME_KEY = "name";
13 | private static final String DISPLAY_NAME_KEY = "display";
14 |
15 | private String username;
16 | private String displayName;
17 |
18 | public Friend() { }
19 |
20 | public Friend bind(JSONObject obj) {
21 | try {
22 | this.username = obj.getString(USERNAME_KEY);
23 | this.displayName = obj.getString(DISPLAY_NAME_KEY);
24 | } catch (JSONException e) {
25 | return this;
26 | }
27 |
28 | return this;
29 | }
30 |
31 | public String getUsername() {
32 | return username;
33 | }
34 |
35 | public String getDisplayName() {
36 | return displayName;
37 | }
38 |
39 | @Override
40 | public String toString() {
41 | return username + " ~> " + displayName;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/JSONBinder.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import org.json.JSONObject;
4 |
5 | /**
6 | * Author: samstern
7 | * Date: 12/31/13
8 | */
9 | public interface JSONBinder {
10 |
11 | /**
12 | * Populate the fields of this object from a JSONObject.
13 | *
14 | * @param obj the JSONObject to use as a data source
15 | * @return this object.
16 | */
17 | public T bind(JSONObject obj);
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Main.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import java.io.File;
4 | import java.io.FileNotFoundException;
5 | import java.io.FileOutputStream;
6 | import java.io.IOException;
7 | import java.util.*;
8 |
9 | public class Main {
10 | private static Snapchat snapchat;
11 |
12 | public static void main(String[] args) throws Exception {
13 | // Get username and password
14 | Scanner scanner = new Scanner(System.in);
15 | System.out.println("Snapchat username: ");
16 | String username = scanner.nextLine();
17 | System.out.println("Snapchat password: ");
18 | String password = scanner.nextLine();
19 |
20 | // Test logging in
21 | System.out.println("Logging in...");
22 | snapchat = Snapchat.login(username, password);
23 | if (snapchat != null) {
24 | System.out.println("Logged in.");
25 | } else {
26 | System.out.println("Failed to log in.");
27 | return;
28 | }
29 |
30 | // Ask the user what they want to do
31 | System.out.println();
32 | System.out.println("Choose an option:");
33 | System.out.println("\t1) Download un-viewed snaps");
34 | System.out.println("\t2) Send a snap");
35 | System.out.println("\t3) Set a Story");
36 | System.out.println("\t4) Download Stories");
37 | System.out.println();
38 |
39 | int option = scanner.nextInt();
40 | scanner.nextLine();
41 | switch (option) {
42 | case 1:
43 | fetchSnaps();
44 | break;
45 | case 2:
46 | System.out.println("Enter path to image file:");
47 | String snapFileName = scanner.nextLine();
48 | System.out.println("Enter recipient Snapchat username:");
49 | String recipient = scanner.nextLine();
50 | sendSnap(username, recipient, snapFileName);
51 | break;
52 | case 3:
53 | System.out.println("Enter path to image file:");
54 | String storyFileName = scanner.nextLine();
55 | setStory(username, storyFileName);
56 | break;
57 | case 4:
58 | Story[] storyObjs = snapchat.getStories();
59 | Story[] downloadable = Story.filterDownloadable(storyObjs);
60 | for (Story s : downloadable) {
61 | String extension = ".jpg";
62 | if(!s.isImage()){
63 | extension = ".mp4";
64 | }
65 | System.out.println("Downloading story from " + s.getSender());
66 | byte[] storyBytes = Snapchat.getStory(s);
67 | File storyFile = new File(s.getSender() + "-" + s.getId() + extension);
68 | FileOutputStream storyOs = new FileOutputStream(storyFile);
69 | storyOs.write(storyBytes);
70 | storyOs.close();
71 | }
72 | System.out.println("Done.");
73 | break;
74 | default:
75 | System.out.println("Invalid option.");
76 | break;
77 | }
78 |
79 | }
80 |
81 | public static void fetchSnaps() throws IOException {
82 | // Try fetching all snaps
83 | System.out.println("Fetching snaps...");
84 | Snap[] snapObjs = snapchat.getSnaps();
85 | Snap[] downloadable = Snap.filterDownloadable(snapObjs);
86 | for (Snap s : downloadable) {
87 | // TODO(samstern): Support video
88 | if (s.isImage()) {
89 | System.out.println("Downloading snap from " + s.getSender());
90 | byte[] snapBytes = snapchat.getSnap(s);
91 | File snapFile = new File(s.getSender() + "-" + s.getId() + ".jpg");
92 | FileOutputStream snapOs = new FileOutputStream(snapFile);
93 | snapOs.write(snapBytes);
94 | snapOs.close();
95 | }
96 | }
97 | System.out.println("Done.");
98 | }
99 |
100 | public static void sendSnap(String username, String recipient, String filename)
101 | throws FileNotFoundException {
102 |
103 | // Get file
104 | File file = new File(filename);
105 |
106 | // Try sending it
107 | List recipients = new ArrayList();
108 | recipients.add(recipient);
109 |
110 | // Send and print
111 | System.out.println("Sending...");
112 | boolean postStory = false; //set as true to make this your story as well...
113 |
114 | boolean isVideo = false;
115 | if(filename.toLowerCase().endsWith("mp4")){
116 | isVideo = true;
117 | }
118 |
119 | // TODO(samstern): User-specified time, not automatically 10 seconds
120 | boolean result = snapchat.sendSnap(file, recipients, isVideo, postStory, 10);
121 | if (result) {
122 | System.out.println("Sent.");
123 | } else {
124 | System.out.println("Could not send.");
125 | }
126 | }
127 |
128 | public static void setStory(String username, String filename)
129 | throws FileNotFoundException {
130 |
131 | boolean video = false; //TODO(liamcottle) upload video snaps from command line.
132 | // Get file
133 | File file = new File(filename);
134 |
135 | // Send and print
136 | System.out.println("Setting...");
137 | boolean postStory = false; //set as true to make this your story as well...
138 |
139 | // TODO(samstern): User-specified time, not automatically 10 seconds
140 | boolean result = snapchat.sendStory(file, video, 10, "My Story");
141 | if (result) {
142 | System.out.println("Set.");
143 | } else {
144 | System.out.println("Could not set.");
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Message.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import org.json.JSONArray;
4 | import org.json.JSONException;
5 | import org.json.JSONObject;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | /**
11 | * Created by James on 2014-06-17.
12 | */
13 | public class Message implements JSONBinder {
14 |
15 | /**
16 | * Types of Message.
17 | */
18 | private final static String TYPE_MEDIA = "media";
19 | private final static String TYPE_TEXT = "text";
20 |
21 | /**
22 | * Paths for various inner json objects.
23 | */
24 | private final static String HEADER_KEY = "header";
25 | private final static String BODY_KEY = "body";
26 | private final static String MEDIA_KEY = "media"; //Only exist for media
27 |
28 | /**
29 | * Various key to get specific informations
30 | */
31 | private final static String CHAT_MESSAGE_ID = "chat_message_id"; //Same as media_id
32 | private final static String ID_KEY = "id";
33 | private final static String FROM_KEY = "from";
34 | private final static String TO_KEY = "to";
35 | private final static String WIDTH_KEY = "width"; //Only exist for media
36 | private final static String HEIGHT_KEY = "height"; //Only exist for media
37 | private final static String IV_KEY = "iv"; //Only exist for media
38 | private final static String KEY_KEY = "key"; //Only exist for media
39 | private final static String MEDIA_ID_KEY = "media_id"; //Only exist for media. Same as chat_message_id
40 | private final static String TEXT_KEY = "text";
41 | private final static String TYPE_KEY = "type";
42 | private final static String TIMESTAMP_KEY = "timestamp";
43 |
44 | /**
45 | * Local variables
46 | */
47 | private String sender;
48 | private List recipients;
49 | private String chat_message_id;
50 | private String id;
51 | private String media_id;
52 | private int width;
53 | private int height;
54 | private String key;
55 | private String iv;
56 | private String type;
57 | private long sent_time;
58 | private String text;
59 |
60 |
61 |
62 |
63 | @Override
64 | public Message bind(JSONObject obj) {
65 | try{
66 | //Inner json objects
67 | JSONObject header = obj.getJSONObject(HEADER_KEY);
68 | JSONObject body = obj.getJSONObject(BODY_KEY);
69 |
70 | //Root
71 | this.chat_message_id = obj.getString(CHAT_MESSAGE_ID);
72 | this.id = obj.getString(ID_KEY);
73 | this.sent_time = obj.getLong(TIMESTAMP_KEY);
74 |
75 | //Header
76 | this.sender = header.getString(FROM_KEY);
77 | this.recipients = new ArrayList();
78 | JSONArray jsonRecipients = header.getJSONArray(TO_KEY);
79 | for(int i = 0; i < jsonRecipients.length(); i++){
80 | this.recipients.add(jsonRecipients.getString(i));
81 | }
82 |
83 | //Body
84 | this.type = body.getString(TYPE_KEY);
85 | if(this.type.equalsIgnoreCase(TYPE_MEDIA)){
86 | JSONObject media = body.getJSONObject(MEDIA_KEY);
87 | this.width = media.getInt(WIDTH_KEY);
88 | this.height = media.getInt(HEIGHT_KEY);
89 | this.media_id = media.getString(MEDIA_ID_KEY);
90 | this.key = media.getString(KEY_KEY);
91 | this.iv = media.getString(IV_KEY);
92 | }else if(this.type.equalsIgnoreCase(TYPE_TEXT)){
93 | this.text = body.getString(TEXT_KEY);
94 | }
95 | }catch(JSONException e){
96 | e.printStackTrace();
97 | return this;
98 | }
99 | return this;
100 | }
101 |
102 | /**
103 | * Get sender username.
104 | *
105 | * @return sender username.
106 | */
107 | public String getSender(){
108 | return this.sender;
109 | }
110 |
111 | /**
112 | * Get all recipients.
113 | *
114 | * @return recipients.
115 | */
116 | public List getRecipients(){
117 | return this.recipients;
118 | }
119 |
120 | /**
121 | * Get the text of this message.
122 | *
123 | * @return the text.
124 | */
125 | public String getText(){
126 | return this.text;
127 | }
128 |
129 | /**
130 | * Get the date of when this message was sent.
131 | *
132 | * @return unix timestamp of when this message was sent.
133 | */
134 | public long getSentTime(){
135 | return this.sent_time;
136 | }
137 |
138 | /**
139 | * Check if this message was an image.
140 | *
141 | * @return true if it is an image, otherwise false.
142 | */
143 | public boolean isMedia(){
144 | return this.type.equalsIgnoreCase(TYPE_MEDIA);
145 | }
146 |
147 | /**
148 | * Check if this message is a text message.
149 | *
150 | * @return true if it is a text message, otherwise false.
151 | */
152 | public boolean isTextMessage(){
153 | return this.type.equalsIgnoreCase(TYPE_TEXT);
154 | }
155 |
156 | /**
157 | * Get the width of this media.
158 | *
159 | * @return the width of this media. If this is not a media, returns -1.
160 | */
161 | public int getWidth(){
162 | if(!this.isMedia()){
163 | return -1;
164 | }
165 | return this.width;
166 | }
167 |
168 | /**
169 | * Get the height of this media.
170 | *
171 | * @return the height of this media. -1 if this is not a media.
172 | */
173 | public int getHeight(){
174 | if(!this.isMedia()){
175 | return -1;
176 | }
177 | return this.height;
178 | }
179 |
180 | /**
181 | * Get the key of this media. Used for decryption.
182 | *
183 | * @return the key of this media. Null if this is not a media.
184 | */
185 | public String getKey(){
186 | if(!this.isMedia()){
187 | return null;
188 | }
189 | return this.key;
190 | }
191 |
192 | /**
193 | * Get the iv key of this media. Used for decryption.
194 | *
195 | * @return the iv key of this media. Null if this is not a media.
196 | */
197 | public String getIVKey(){
198 | if(!this.isMedia()){
199 | return null;
200 | }
201 | return this.iv;
202 | }
203 |
204 | /**
205 | * Get the ID.
206 | *
207 | * @return the id.
208 | */
209 | public String getID(){
210 | return this.id;
211 | }
212 |
213 | /**
214 | * Get the Chat Message ID.
215 | *
216 | * @return the chat message id.
217 | */
218 | public String getChatMessageID(){
219 | return this.chat_message_id;
220 | }
221 |
222 | /**
223 | * Get the media id. Basically same has Message#getChatMessageID
224 | *
225 | * @return the media id. Null if not a media.
226 | */
227 | public String getMediaID(){
228 | if(!this.isMedia()){
229 | return null;
230 | }
231 | return this.media_id;
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Snap.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import org.json.JSONException;
4 | import org.json.JSONObject;
5 |
6 | import java.util.ArrayList;
7 | import java.util.Arrays;
8 |
9 | public class Snap implements JSONBinder {
10 |
11 | private static final int TYPE_IMAGE = 0;
12 | private static final int TYPE_VIDEO = 1;
13 | private static final int TYPE_VIDEO_NOAUDIO = 2;
14 | private static final int TYPE_FRIEND_REQUEST = 3;
15 | private static final int TYPE_FRIEND_REQUEST_IMAGE = 4;
16 | private static final int TYPE_FRIEND_REQUEST_VIDEO = 5;
17 | private static final int TYPE_FRIEND_REQUEST_VIDEO_NOAUDIO = 6;
18 |
19 | private static final int NONE = -1;
20 | private static final int SENT = 0;
21 | private static final int DELIVERED = 1;
22 | private static final int VIEWED = 2;
23 | private static final int SCREENSHOT = 3;
24 |
25 | private static final String ID_KEY = "id"; //Always : Snap ID.
26 | private static final String SENTTIME_KEY = "sts"; //Always : Snaps sent time.
27 | private static final String LAST_INTERACTION_TIME_KEY = "ts"; //Always : Recipient : ts == sts. Sender : Last interaction time.
28 | private static final String TYPE_KEY = "m"; //Always : Image or Video
29 | private static final String STATE_KEY = "st"; //Always : Sent, Delivered, Viewed, Screnshot.
30 | private static final String SENDER_KEY = "sn"; //Only there for recipient : Sender username.
31 | private static final String RECIPENT_KEY = "rp"; //Only there for sender : Recipient username.
32 | private static final String TIME_KEY = "t"; //Unseen snaps only : How long can it be viewed for.
33 |
34 | private String id;
35 | private String sender;
36 | private String recipient;
37 | private int type;
38 | private int state;
39 | private int time;
40 | private long senttime;
41 | private long lastInteractionTime;
42 |
43 | private String caption;
44 |
45 | public Snap() { }
46 |
47 | public Snap bind(JSONObject obj) {
48 | // Check for fields that always exist
49 | try {
50 | this.id = obj.getString(ID_KEY);
51 | this.type = obj.getInt(TYPE_KEY);
52 | this.state = obj.getInt(STATE_KEY);
53 | this.lastInteractionTime = obj.getLong(LAST_INTERACTION_TIME_KEY);
54 | this.senttime = obj.getLong(SENTTIME_KEY);
55 |
56 | if(obj.has(SENDER_KEY)){
57 | this.sender = obj.getString(SENDER_KEY);
58 | }
59 |
60 | if(obj.has(RECIPENT_KEY)){
61 | this.recipient = obj.getString(RECIPENT_KEY);
62 | }
63 |
64 | } catch (JSONException e) {
65 | e.printStackTrace();
66 | return this;
67 | }
68 |
69 | // Check for time separately because it may not exist.
70 | // Only exist when the snap hasn't been viewed.
71 | try {
72 | this.time = obj.getInt(TIME_KEY);
73 | } catch (JSONException e) {
74 | return this;
75 | }
76 |
77 | return this;
78 | }
79 |
80 | /**
81 | * Take an array of Snaps and return only those that are downloadable.
82 | *
83 | * @param input the array of Snaps to filter.
84 | * @return the snaps that are downloadable (media and unviewed).
85 | */
86 | public static Snap[] filterDownloadable(Snap[] input) {
87 | ArrayList result = new ArrayList();
88 | for (Snap s : input) {
89 | if(s.isDownloadable()){
90 | result.add(s);
91 | }
92 | }
93 |
94 | return result.toArray(new Snap[result.size()]);
95 | }
96 |
97 | /**
98 | * Check if this snap can be downloaded
99 | *
100 | * @return is downloadable
101 | */
102 | public boolean isDownloadable(){
103 | if (this.isMedia() && this.isIncoming() && !this.isViewed()) {
104 | return true;
105 | }
106 | return false;
107 | }
108 |
109 | /**
110 | * Determine if a Snap has already been viewed. If not, it can be downloaded.
111 | *
112 | * @return true if it has been viewed, false otherwise.
113 | */
114 | public boolean isViewed() {
115 | return (state == VIEWED || state == SCREENSHOT);
116 | }
117 |
118 | /**
119 | * Determine if a Snap is a still image.
120 | *
121 | * @return true if it is an image, false if it is a video or other.
122 | */
123 | public boolean isImage() {
124 | return (type == TYPE_IMAGE);
125 | }
126 |
127 | /**
128 | * Determine if a Snap is a video.
129 | *
130 | * @return true if it is a video, false if it is an image or other.
131 | */
132 | public boolean isVideo() {
133 | return (type == TYPE_VIDEO || type == TYPE_VIDEO_NOAUDIO);
134 | }
135 |
136 | /**
137 | * Determine if a Snap is a video or image. If not, can't be downloaded and viewed.
138 | *
139 | * @return true if it is a video or an image, false if other.
140 | */
141 | public boolean isMedia() {
142 | return (isImage() || isVideo());
143 | }
144 |
145 | /**
146 | * Determine if a snap has been screenshoted.
147 | *
148 | * @return true if it is screenshoted.
149 | */
150 | public boolean isScreenshoted(){
151 | return state == SCREENSHOT;
152 | }
153 |
154 | /**
155 | * Determine if a Snap is incoming or outgoing.
156 | *
157 | * @return true if a snap is incoming, false otherwise.
158 | */
159 | public boolean isIncoming() {
160 | return (id.endsWith("r"));
161 | }
162 |
163 | public boolean isFriendRequest(){
164 | return (type == TYPE_FRIEND_REQUEST || type == TYPE_FRIEND_REQUEST_IMAGE || type == TYPE_FRIEND_REQUEST_VIDEO || type == TYPE_FRIEND_REQUEST_VIDEO_NOAUDIO);
165 | }
166 |
167 | /**
168 | * Get this snap ID.
169 | *
170 | * @return the snap ID
171 | */
172 | public String getId() {
173 | return id;
174 | }
175 |
176 | /**
177 | * Get this snap sender username.
178 | *
179 | * @return the sender username.
180 | */
181 | public String getSender() {
182 | return sender;
183 | }
184 |
185 | /**
186 | * Get this snap recipient username.
187 | *
188 | * @return the recipient username.
189 | */
190 | public String getRecipient() {
191 | return recipient;
192 | }
193 |
194 | public int getTime() {
195 | return time;
196 | }
197 |
198 | /**
199 | * Last interaction time. For recipients, this is the same as sent time.
200 | *
201 | * @return last interaction time.
202 | */
203 | public long getLastInteractionTime(){
204 | return this.lastInteractionTime;
205 | }
206 |
207 | public long getSentTime() {
208 | return senttime;
209 | }
210 |
211 | public String getCaption() {
212 | return caption;
213 | }
214 |
215 | @Override
216 | public String toString() {
217 | String[] attrs = new String[]{
218 | id,
219 | sender,
220 | recipient,
221 | Integer.toString(type),
222 | Integer.toString(state),
223 | Integer.toString(time),
224 | Long.toString(senttime),
225 | caption
226 | };
227 | return Arrays.toString(attrs);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Snapchat.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import com.mashape.unirest.http.HttpResponse;
4 | import com.mashape.unirest.http.JsonNode;
5 | import com.mashape.unirest.http.Unirest;
6 | import com.mashape.unirest.http.exceptions.UnirestException;
7 | import com.mashape.unirest.request.HttpRequest;
8 | import com.mashape.unirest.request.body.MultipartBody;
9 | import org.apache.commons.io.IOUtils;
10 | import org.json.JSONArray;
11 | import org.json.JSONException;
12 | import org.json.JSONObject;
13 |
14 | import java.io.*;
15 | import java.util.*;
16 |
17 | /**
18 | * Author: samstern
19 | * Date: 12/29/13
20 | */
21 | public class Snapchat {
22 |
23 | /**
24 | * Last response received. Used for error reporting.
25 | */
26 | public static String lastRequestPath;
27 | public static HttpResponse lastResponse;
28 | public static Class lastResponseBodyClass;
29 |
30 | /**
31 | * POST parameter keys for sending requests to Snapchat.
32 | */
33 | private static final String USERNAME_KEY = "username";
34 | private static final String PASSWORD_KEY = "password";
35 | private static final String TIMESTAMP_KEY = "timestamp";
36 | private static final String REQ_TOKEN_KEY = "req_token";
37 | private static final String AUTH_TOKEN_KEY = "auth_token";
38 | private static final String ID_KEY = "id";
39 | private static final String SNAP_KEY = "snap";
40 | private static final String CHAT_MESSAGE_KEY = "chat_message";
41 | private static final String MESSAGES_KEY = "messages";
42 | private static final String FRIEND_STORIES_KEY = "friend_stories";
43 | private static final String STORIES_KEY = "stories";
44 | private static final String FRIENDS_KEY = "friends";
45 | private static final String MEDIA_ID_KEY = "media_id";
46 | private static final String CLIENT_ID_KEY = "client_id";
47 | private static final String CAPTION_TEXT_DISPLAY_KEY = "caption_text_display";
48 | private static final String TYPE_KEY = "type";
49 | private static final String DATA_KEY = "data";
50 | private static final String ZIPPED_KEY = "zipped";
51 | private static final String TIME_KEY = "time";
52 | private static final String RECIPIENTS_KEY = "recipients";
53 | private static final String FEATURES_MAP_KEY = "features_map";
54 | private static final String ADDED_FRIENDS_TIMESTAMP_KEY = "added_friends_timestamp";
55 | private static final String JSON_KEY = "json";
56 | private static final String EVENTS_KEY = "events";
57 | private static final String LOGGED_KEY = "logged";
58 | private static final String CONVERSATION_MESSAGES_KEY = "conversation_messages";
59 | private static final String RECIPIENT_USERNAMES = "recipient_usernames";
60 | private static final String ACTION_KEY = "action";
61 | private static final String FRIEND_KEY = "friend";
62 | private static final String DISPLAY_KEY = "display";
63 | private static final String MY_STORIES_KEY = "my_stories";
64 |
65 | /**
66 | * Paths for various Snapchat groups in loginObj_full
67 | */
68 | private static final String UPDATES_RESPONSE_KEY = "updates_response";
69 | private static final String MESSAGING_GATEWAY_INFO_RESPONSE_KEY = "messaging_gateway_info";
70 | private static final String STORIES_RESPONSE_KEY = "stories_response";
71 | private static final String CONVERSATIONS_RESPONSE_KEY = "conversations_response";
72 |
73 | /**
74 | * Paths for various Snapchat actions, relative to BASE_URL.
75 | */
76 | private static final String LOGIN_PATH = "loq/login";
77 | private static final String ALL_UPDATES_PATH = "/loq/all_updates";
78 | private static final String UPLOAD_PATH = "ph/upload";
79 | private static final String SEND_PATH = "loq/send";
80 | private static final String STORY_PATH = "bq/post_story";
81 | private static final String DOUBLE_PATH = "bq/double_post"; //TODO : UPDATE PATH
82 | private static final String BLOB_PATH = "ph/blob";
83 | private static final String FRIEND_STORIES_PATH = "bq/stories";
84 | private static final String STORY_BLOB_PATH = "bq/story_blob";
85 | private static final String UPDATE_SNAPS_PATH = "bq/update_snaps";
86 | private static final String CHAT_TYPING_PATH = "bq/chat_typing";
87 | private static final String FRIEND_PATH = "bq/friend";
88 |
89 | /**
90 | * Static members for forming HTTP requests.
91 | */
92 | private static final String BASE_URL = "https://feelinsonice-hrd.appspot.com/";
93 | private static final String JSON_TYPE_KEY = "accept";
94 | private static final String JSON_TYPE = "application/json";
95 | private static final String USER_AGENT_KEY = "user-agent";
96 | private static final String USER_AGENT = "Snapchat/8.1.0.8 Beta (A0001; Android 21; gzip)";
97 | private static final String ACCEPT_LANGUAGE_KEY = "Accept-Language";
98 | private static final String ACCEPT_LOCALE_KEY = "Accept-Locale";
99 |
100 | /**
101 | * Local variables
102 | */
103 | private JSONObject loginObj_full;
104 | private JSONObject loginObj_updates;
105 | private JSONObject loginObj_messaging_gateway_info;
106 | private JSONObject loginObj_stories;
107 | private JSONArray loginObj_conversations;
108 |
109 | private String username;
110 | private String authToken;
111 | private long friendsTimestamp;
112 | private Friend[] friends;
113 | private Story[] stories;
114 | private Story[] mystories;
115 | private Snap[] snaps;
116 | private Message[] messages;
117 | private long lastRefreshed;
118 |
119 | /**
120 | * Build the Snapchat object
121 | * @see Snapchat#login(String, String)
122 | */
123 | private Snapchat(JSONObject loginObj_full){
124 | try {
125 | //Setup all inner json objects
126 | setupLoginJSONObjects(loginObj_full);
127 |
128 | //Setup all local variables
129 | this.username = this.loginObj_updates.getString(USERNAME_KEY);
130 | this.authToken = this.loginObj_updates.getString(AUTH_TOKEN_KEY);
131 | this.friendsTimestamp = this.loginObj_updates.getLong(Snapchat.ADDED_FRIENDS_TIMESTAMP_KEY);
132 | } catch (JSONException e) {
133 | //TODO Something is wrong with the loginObj_full
134 | e.printStackTrace();
135 | }
136 | }
137 |
138 | /**
139 | * Log in to Snapchat.
140 | *
141 | * @param username the Snapchat username.
142 | * @param password the Snapchat password.
143 | * @return the entire JSON login response.
144 | */
145 | public static Snapchat login(String username, String password) {
146 | Map params = new HashMap();
147 |
148 | // Add username and password
149 | params.put(USERNAME_KEY, username);
150 | params.put(PASSWORD_KEY, password);
151 |
152 | // Add timestamp and requestJson token made using static auth token
153 | Long timestamp = getTimestamp();
154 | String reqToken = TokenLib.staticRequestToken(timestamp);
155 |
156 | params.put(TIMESTAMP_KEY, timestamp.toString());
157 | params.put(REQ_TOKEN_KEY, reqToken);
158 |
159 | try {
160 | HttpResponse resp = requestJson(LOGIN_PATH, params, null);
161 | JSONObject obj = resp.getBody().getObject();
162 | if (obj.has(UPDATES_RESPONSE_KEY) && obj.getJSONObject(UPDATES_RESPONSE_KEY).getBoolean(LOGGED_KEY)){
163 | return new Snapchat(obj);
164 | }
165 | return null;
166 | } catch (UnirestException e) {
167 | e.printStackTrace();
168 | return null;
169 | } catch (JSONException e) {
170 | e.printStackTrace();
171 | return null;
172 | }
173 | }
174 |
175 | /**
176 | * Refresh your snaps, friends, stories.
177 | *
178 | * @return true if successful, otherwise false.
179 | */
180 | public boolean refresh() {
181 | if(updateLoginObj()){
182 | this.snaps = null;
183 | this.messages = null;
184 | this.stories = null;
185 | this.mystories = null;
186 | this.friends = null;
187 |
188 | getSnaps();
189 | getMessages();
190 | getStories();
191 | getMyStories();
192 | getFriends();
193 |
194 | lastRefreshed = new Date().getTime();
195 | return true;
196 | }
197 | return false;
198 | }
199 |
200 | /**
201 | * Get your friends
202 | * @return a Friend[]
203 | */
204 | public Friend[] getFriends() {
205 | if(this.friends != null){
206 | return this.friends;
207 | }else{
208 | try {
209 | JSONArray friendsArr = this.loginObj_updates.getJSONArray(FRIENDS_KEY);
210 | List resultList = bindArray(friendsArr, Friend.class);
211 | this.friends = resultList.toArray(new Friend[resultList.size()]);
212 | return this.friends;
213 | } catch (JSONException e) {
214 | return new Friend[0];
215 | }
216 | }
217 | }
218 |
219 | /**
220 | * Get received and sent snaps.
221 | * @return a Snap[]
222 | */
223 | public Snap[] getSnaps() {
224 | if(this.snaps != null){
225 | return this.snaps;
226 | }else{
227 | parseSnapsAndMessages();
228 | return this.snaps;
229 | }
230 | }
231 |
232 | /**
233 | * Get all received messages.
234 | * @return an array of Message.
235 | */
236 | public Message[] getMessages(){
237 | if(this.messages != null){
238 | return this.messages;
239 | }else{
240 | parseSnapsAndMessages();
241 | return this.messages;
242 | }
243 | }
244 |
245 | /**
246 | * Get Friends Stories from Snapchat.
247 | * @return a Story[]
248 | */
249 | public Story[] getStories() {
250 | if(this.stories != null){
251 | return this.stories;
252 | }else{
253 | try {
254 | JSONArray stories_list = new JSONArray();
255 | JSONArray friend_stories = this.loginObj_stories.getJSONArray(FRIEND_STORIES_KEY);
256 | //For each friend having posted a story
257 | for(int i = 0; i < friend_stories.length(); i++){
258 | //Get friend story/stories
259 | JSONArray stories = friend_stories.getJSONObject(i).getJSONArray(STORIES_KEY);
260 | //For each story this friend has posted
261 | for(int s = 0; s < stories.length(); s++){
262 | stories_list.put(stories.get(s));
263 | }
264 | }
265 | List stories = bindArray(stories_list, Story.class);
266 | this.stories = stories.toArray(new Story[stories.size()]);
267 | return this.stories;
268 | } catch (JSONException ex) {
269 | ex.printStackTrace();
270 | return new Story[0];
271 | }
272 | }
273 | }
274 |
275 | /**
276 | * Get My Stories from Snapchat.
277 | * @return a Story[]
278 | */
279 | public Story[] getMyStories() {
280 | if(this.mystories != null){
281 | return this.mystories;
282 | }else{
283 | try {
284 | JSONArray mystories_list = new JSONArray();
285 | JSONArray my_stories = this.loginObj_stories.getJSONArray(MY_STORIES_KEY);
286 | List mystories = bindArray(my_stories, Story.class);
287 | this.mystories = mystories.toArray(new Story[mystories.size()]);
288 | return this.mystories;
289 | } catch (JSONException ex) {
290 | ex.printStackTrace();
291 | return new Story[0];
292 | }
293 | }
294 | }
295 |
296 | /**
297 | * Download and un-encrypt a Snap from the server.
298 | *
299 | * @param snap the Snap to download.
300 | * @return a byte[] containing decrypted image or video data.
301 | */
302 | public byte[] getSnap(Snap snap) {
303 | try {
304 | Map params = new HashMap();
305 | params.put(USERNAME_KEY, username);
306 | Long timestamp = getTimestamp();
307 | params.put(TIMESTAMP_KEY, timestamp);
308 | params.put(REQ_TOKEN_KEY, TokenLib.requestToken(authToken, timestamp));
309 | params.put(ID_KEY, snap.getId());
310 |
311 | HttpResponse resp = requestBinary(BLOB_PATH, params, null);
312 | InputStream is = resp.getBody();
313 | byte[] encryptedBytes = IOUtils.toByteArray(is);
314 | byte[] decryptedBytes = Encryption.decrypt(encryptedBytes);
315 | return decryptedBytes;
316 | } catch (UnirestException e) {
317 | return new byte[0];
318 | } catch (IOException e) {
319 | return new byte[0];
320 | } catch (Encryption.EncryptionException e) {
321 | return new byte[0];
322 | }
323 | }
324 |
325 | /**
326 | * Download and un-encrypt a Story from the server. Added by Liam Cottle
327 | *
328 | * @param story the Story to download.
329 | * @return a byte[] containing decrypted image or video data.
330 | */
331 | public static byte[] getStory(Story story) {
332 | try {
333 | HttpResponse resp = requestStoryBinary(STORY_BLOB_PATH + "?story_id=" + story.getId());
334 | InputStream is = resp.getBody();
335 | byte[] encryptedBytes = IOUtils.toByteArray(is);
336 | byte[] decryptedBytes = StoryEncryption.decrypt(encryptedBytes,story.getMediaKey(),story.getMediaIV());
337 | return decryptedBytes;
338 | } catch (UnirestException e) {
339 | return new byte[0];
340 | } catch (IOException e) {
341 | return new byte[0];
342 | }
343 | }
344 |
345 | /**
346 | * Send a snap image or video
347 | *
348 | * @param image the image/video file to upload.
349 | * @param recipients a list of Snapchat usernames to send to.
350 | * @param story true if this should be uploaded to the sender's story as well.
351 | * @param video true if video, otherwise false.
352 | * @param time the time (max 10) for which this snap should be visible.
353 | * @return true if success, otherwise false.
354 | */
355 | public boolean sendSnap(File image, List recipients, boolean video, boolean story, int time){
356 | String upload_media_id = upload(image, video);
357 | if(upload_media_id != null){
358 | return send(upload_media_id, recipients, story, time);
359 | }
360 | return false;
361 | }
362 |
363 | /**
364 | * Add a snap to your story
365 | *
366 | * @param image the image/video file to upload.
367 | * @param video true if video, otherwise false.
368 | * @param time the time (max 10) for which this story should be visible.
369 | * @param caption a caption. Nobody knows what it is used for. eg. "My Story"
370 | * @return true if success, otherwise false.
371 | */
372 | public boolean sendStory(File image, boolean video, int time, String caption){
373 | String upload_media_id = upload(image, video);
374 | if(upload_media_id != null){
375 | return sendStory(upload_media_id, time, video, caption);
376 | }
377 | return false;
378 | }
379 |
380 | /**
381 | * Make a change to a snap/story, eg mark it as viewed or screenshot or seen.
382 | *
383 | * @param snap the snap object we are interacting with
384 | * @param seen boolean stating if we have seen this snap or not.
385 | * @param screenshot boolean stating if we have screenshot this snap or not.
386 | * @param replayed integer stating how many times we have replayed this snap.
387 | * @return true if successful, false otherwise.
388 | */
389 | public boolean setSnapFlags(Snap snap, boolean seen, boolean screenshot, boolean replayed){
390 | return updateSnap(snap, seen, screenshot, replayed);
391 | }
392 |
393 | /**
394 | * Tell your recipient that you are typing a chat message.
395 | *
396 | * @param recipient username to tell.
397 | * @return true if successful, otherwise false.
398 | */
399 | public boolean tellIAmTyping(String recipient){
400 | try {
401 | Map params = new HashMap();
402 |
403 | // Add timestamp and requestJson token made using auth token
404 | Long timestamp = getTimestamp();
405 | String reqToken = TokenLib.requestToken(this.authToken, timestamp);
406 |
407 | //Add params
408 | params.put(USERNAME_KEY, this.username);
409 | params.put(TIMESTAMP_KEY, timestamp.toString());
410 | params.put(REQ_TOKEN_KEY, reqToken);
411 | //Odd : Requires a JSONArray but only works with 1 username.
412 | params.put(RECIPIENT_USERNAMES, new JSONArray(new String[]{recipient}).toString());
413 |
414 | //Make the request
415 | HttpResponse resp = requestString(CHAT_TYPING_PATH, params, null);
416 | System.out.println(resp.getBody().toString());
417 | if (resp.getStatus() == 200 || resp.getStatus() == 201) {
418 | return true;
419 | }
420 | } catch (UnirestException e) {
421 | e.printStackTrace();
422 | } catch (JSONException e) {
423 | e.printStackTrace();
424 | }
425 | return false;
426 | }
427 |
428 | /**
429 | * Delete a friend
430 | *
431 | * @param friend username to add.
432 | * @return true if successful, otherwise false.
433 | */
434 | public boolean deleteFriend(String friend){
435 | try {
436 | Map params = new HashMap();
437 |
438 | // Add timestamp and requestJson token made using auth token
439 | Long timestamp = getTimestamp();
440 | String reqToken = TokenLib.requestToken(this.authToken, timestamp);
441 |
442 | //Add params
443 | params.put(USERNAME_KEY, this.username);
444 | params.put(TIMESTAMP_KEY, timestamp.toString());
445 | params.put(REQ_TOKEN_KEY, reqToken);
446 | params.put(ACTION_KEY, "delete");
447 | params.put(FRIEND_KEY, friend);
448 |
449 | //Make the request
450 | HttpResponse resp = requestString(FRIEND_PATH, params, null);
451 | //The request seems to be a success even if you weren't already friends...
452 | if (resp.getStatus() == 200 || resp.getStatus() == 201) {
453 | return true;
454 | }
455 | } catch (UnirestException e) {
456 | e.printStackTrace();
457 | }
458 | return false;
459 | }
460 |
461 | /**
462 | * Parse a JSONArray into a list of type
463 | *
464 | * @param arr the JSON array
465 | * @param clazz the class of type
466 | * @return a list of type
467 | */
468 | public static List bindArray(JSONArray arr, Class extends JSONBinder> clazz) {
469 | try {
470 | int length = arr.length();
471 | List result = new ArrayList();
472 | for (int i = 0; i < length; i++) {
473 | JSONObject obj = arr.getJSONObject(i);
474 | T bound = clazz.newInstance().bind(obj);
475 | result.add(bound);
476 | }
477 | return result;
478 | } catch (JSONException e) {
479 | return new ArrayList();
480 | } catch (InstantiationException e) {
481 | return new ArrayList();
482 | } catch (IllegalAccessException e) {
483 | return new ArrayList();
484 | }
485 | }
486 |
487 | /**
488 | * ================================================== PRIVATE NON-STATIC METHODS REGION ==================================================
489 | */
490 |
491 | /**
492 | * Parses Snaps and Messages from the loginObj_conversations.
493 | * Saves the result in variables.
494 | */
495 | private void parseSnapsAndMessages(){
496 | try {
497 | JSONArray snapsArray = new JSONArray();
498 | JSONArray messagesArray = new JSONArray();
499 | //For each inner JSONObject containing conversations per username
500 | for(int i = 0; i < this.loginObj_conversations.length(); i++){
501 | //Inner JSONObject containing conversations (snaps & chat messages)
502 | JSONObject conversation_messages = this.loginObj_conversations.getJSONObject(i).getJSONObject(CONVERSATION_MESSAGES_KEY);
503 | //Array of messages (snap or chat message)
504 | JSONArray messages = conversation_messages.getJSONArray(MESSAGES_KEY);
505 | for(int m = 0; m < messages.length(); m++){
506 | //Get the JSONObject representing the message(snap or chat message)
507 | JSONObject message = messages.getJSONObject(m);
508 | //if it is a snap
509 | if(message.has(SNAP_KEY)){
510 | snapsArray.put(message.getJSONObject(SNAP_KEY));
511 | }else if(message.has(CHAT_MESSAGE_KEY)){
512 | messagesArray.put(message.getJSONObject(CHAT_MESSAGE_KEY));
513 | }
514 | }
515 | }
516 | List snapsList = bindArray(snapsArray, Snap.class);
517 | List messagesList = bindArray(messagesArray, Message.class);
518 | this.snaps = snapsList.toArray(new Snap[snapsList.size()]);
519 | this.messages = messagesList.toArray(new Message[messagesList.size()]);
520 | } catch (JSONException e) {
521 | this.snaps = new Snap[0];
522 | this.messages = new Message[0];
523 | }
524 | }
525 |
526 | /**
527 | * Send a snap that has already been uploaded.
528 | *
529 | * @param mediaId the media_id of the uploaded snap.
530 | * @param recipients a list of Snapchat usernames to send to.
531 | * @param story true if this should be uploaded to the sender's story as well.
532 | * @param time the time (max 10) for which this snap should be visible.
533 | * @return true if successful, false otherwise.
534 | */
535 | private boolean send(String mediaId, List recipients, boolean story, int time) {
536 | try {
537 | // Prepare parameters
538 | Long timestamp = getTimestamp();
539 | String requestToken = TokenLib.requestToken(authToken, timestamp);
540 | int snapTime = Math.min(10, time);
541 |
542 | // Create comma-separated recipient string
543 | StringBuilder sb = new StringBuilder();
544 | if (recipients.size() == 0 && !story) {
545 | // Can't send to nobody
546 | return false;
547 | }else if(recipients.size() == 0 && story){
548 | // Send to story only
549 | //TODO : Send to story only
550 | return false;
551 | }
552 |
553 | JSONArray recipientsArray = new JSONArray();
554 | for(String recipient : recipients){
555 | recipientsArray.put(recipient);
556 | }
557 |
558 | // Make parameter map
559 | Map params = new HashMap();
560 | params.put(USERNAME_KEY, username);
561 | params.put(TIMESTAMP_KEY, timestamp.toString());
562 | params.put(REQ_TOKEN_KEY, requestToken);
563 | params.put(MEDIA_ID_KEY, mediaId);
564 | params.put(TIME_KEY, Double.toString(snapTime));
565 | params.put(RECIPIENTS_KEY, recipientsArray.toString());
566 | params.put(ZIPPED_KEY, "0");
567 | params.put(FEATURES_MAP_KEY, new JSONObject().toString());
568 |
569 | // Sending path
570 | String path = SEND_PATH;
571 |
572 | // Add to story, maybe
573 | if (story) {
574 | path = DOUBLE_PATH;
575 | params.put(CAPTION_TEXT_DISPLAY_KEY, "My Story");
576 | params.put(CLIENT_ID_KEY, mediaId);
577 | params.put(TYPE_KEY, "0");
578 | }
579 |
580 | // Execute request
581 | HttpResponse resp = requestString(path, params, null);
582 | if (resp.getStatus() == 200 || resp.getStatus() == 202) {
583 | return true;
584 | } else {
585 | return false;
586 | }
587 | } catch (UnirestException e) {
588 | return false;
589 | }
590 | }
591 |
592 | /**
593 | * Set a story from media already uploaded.
594 | *
595 | * @param mediaId the media_id of the uploaded snap.
596 | * @param time the time (max 10) for which this story should be visible.
597 | * @param video is video
598 | * @param caption the caption
599 | * @return true if successful, false otherwise.
600 | */
601 | private boolean sendStory(String mediaId, int time, boolean video, String caption) {
602 | try {
603 | // Prepare parameters
604 | Long timestamp = getTimestamp();
605 | String requestToken = TokenLib.requestToken(authToken, timestamp);
606 | int snapTime = Math.min(10, time);
607 |
608 | // Make parameter map
609 | Map params = new HashMap();
610 | params.put(USERNAME_KEY, username);
611 | params.put(TIMESTAMP_KEY, timestamp.toString());
612 | params.put(REQ_TOKEN_KEY, requestToken);
613 | params.put(MEDIA_ID_KEY, mediaId);
614 | params.put(CLIENT_ID_KEY, mediaId);
615 | params.put(TIME_KEY, Integer.toString(snapTime));
616 | params.put(CAPTION_TEXT_DISPLAY_KEY, caption);
617 | params.put(ZIPPED_KEY, "0");
618 | if(video){
619 | params.put(TYPE_KEY, "1");
620 | }
621 | else{
622 | params.put(TYPE_KEY, "0");
623 | }
624 |
625 | // Execute request
626 | HttpResponse resp = requestString(STORY_PATH, params, null);
627 | if (resp.getStatus() == 200 || resp.getStatus() == 202) {
628 | return true;
629 | } else {
630 | return false;
631 | }
632 | } catch (UnirestException e) {
633 | return false;
634 | }
635 | }
636 |
637 | /**
638 | * Setup all loginObj variables from the full loginObj
639 | *
640 | * @param newLoginObj_full full loginObj received from Snapchat Server.
641 | * @return true if successful, otherwise false.
642 | */
643 | private boolean setupLoginJSONObjects(JSONObject newLoginObj_full){
644 | try {
645 | this.loginObj_full = newLoginObj_full;
646 | this.loginObj_updates = loginObj_full.getJSONObject(UPDATES_RESPONSE_KEY);
647 | this.loginObj_messaging_gateway_info = loginObj_full.getJSONObject(MESSAGING_GATEWAY_INFO_RESPONSE_KEY);
648 | this.loginObj_stories = loginObj_full.getJSONObject(STORIES_RESPONSE_KEY);
649 | this.loginObj_conversations = loginObj_full.getJSONArray(CONVERSATIONS_RESPONSE_KEY);
650 | return true;
651 | } catch (JSONException e) {
652 | e.printStackTrace();
653 | return false;
654 | }
655 | }
656 |
657 | /**
658 | * Fetch latest version of full loginObj from Snapchat Server.
659 | * @return true if the update is successful, otherwise false.
660 | */
661 | private boolean updateLoginObj(){
662 | Map params = new HashMap();
663 |
664 | // Add username and password
665 | params.put(USERNAME_KEY, username);
666 |
667 | // Add timestamp and requestJson token made using auth token
668 | Long timestamp = getTimestamp();
669 | String reqToken = TokenLib.requestToken(this.authToken, timestamp);
670 |
671 | params.put(TIMESTAMP_KEY, timestamp.toString());
672 | params.put(REQ_TOKEN_KEY, reqToken);
673 |
674 | try {
675 | HttpResponse resp = requestJson(ALL_UPDATES_PATH, params, null);
676 | JSONObject obj = resp.getBody().getObject();
677 | if(obj.has(UPDATES_RESPONSE_KEY) && obj.getJSONObject(UPDATES_RESPONSE_KEY).getBoolean(LOGGED_KEY)){
678 | return setupLoginJSONObjects(obj);
679 | }
680 | return false;
681 | } catch (UnirestException e) {
682 | e.printStackTrace();
683 | return false;
684 | } catch (JSONException e) {
685 | e.printStackTrace();
686 | return false;
687 | }
688 | }
689 |
690 | /**
691 | * Make a change to a snap/story, eg mark it as viewed or screenshot or seen.
692 | *
693 | * @param snap the snap object we are interacting with
694 | * @param seen boolean stating if we have seen this snap or not.
695 | * @param screenshot boolean stating if we have screenshot this snap or not.
696 | * @param replayed integer stating how many times we have replayed this snap.
697 | * @return true if successful, false otherwise.
698 | */
699 | private boolean updateSnap(Snap snap, boolean seen, boolean screenshot, boolean replayed) {
700 | try {
701 | // Prepare parameters
702 | Long timestamp = getTimestamp();
703 | String requestToken = TokenLib.requestToken(authToken, timestamp);
704 |
705 | int statusInt = 0;
706 | int replayedInt = 0;
707 |
708 | if(seen){
709 | statusInt = 0;
710 | }
711 | else if(screenshot){
712 | statusInt = 1;
713 | }
714 |
715 | if(replayed){
716 | replayedInt = 1;
717 | }
718 |
719 | String jsonString = "{\"" + snap.getId() + "\":{\"c\":" + statusInt + ",\"t\":" + timestamp + ",\"replayed\":" + replayedInt + "}}";
720 |
721 | String eventsString = "[]";
722 |
723 | // Make parameter map
724 | Map params = new HashMap();
725 | params.put(USERNAME_KEY, username);
726 | params.put(TIMESTAMP_KEY, timestamp.toString());
727 | params.put(REQ_TOKEN_KEY, requestToken);
728 | params.put(ADDED_FRIENDS_TIMESTAMP_KEY, friendsTimestamp);
729 | params.put(JSON_KEY, jsonString);
730 | params.put(EVENTS_KEY, eventsString);
731 | //params.put(TIME_KEY, Integer.toString(snapTime));
732 |
733 | // Sending path
734 | String path = UPDATE_SNAPS_PATH;
735 |
736 | // Execute request
737 | HttpResponse resp = requestString(path, params, null);
738 | if (resp.getStatus() == 200 || resp.getStatus() == 202) {
739 | return true;
740 | } else {
741 | return false;
742 | }
743 | } catch (UnirestException e) {
744 | return false;
745 | }
746 | }
747 |
748 | /**
749 | * Upload a file and return the media_id for sending.
750 | *
751 | * @param image the image file to upload.
752 | * @param video is a video
753 | * @return the new upload's media_id. Returns null if there is an error.
754 | */
755 | private String upload(File image, boolean video) {
756 | try {
757 | // Open file and ecnrypt it
758 | byte[] fileBytes = IOUtils.toByteArray(new FileInputStream(image));
759 | byte[] encryptedBytes = Encryption.encrypt(fileBytes);
760 |
761 | // Write to a temporary file
762 | File encryptedFile = File.createTempFile("encr", "snap");
763 |
764 | FileOutputStream fos = new FileOutputStream(encryptedFile);
765 | fos.write(encryptedBytes);
766 | fos.close();
767 |
768 | // Create other params
769 | Long timestamp = getTimestamp();
770 | String requestToken = TokenLib.requestToken(authToken, timestamp);
771 | String mediaId = Snapchat.getNewMediaId(username);
772 |
773 | // Make parameter map
774 | Map params = new HashMap();
775 | params.put(USERNAME_KEY, username);
776 | params.put(TIMESTAMP_KEY, timestamp);
777 | params.put(REQ_TOKEN_KEY, requestToken);
778 | params.put(MEDIA_ID_KEY, mediaId);
779 | if(video){
780 | params.put(TYPE_KEY, 1);
781 | }
782 | else{
783 | params.put(TYPE_KEY, 0);
784 | }
785 |
786 | // Perform request and check for 200
787 | HttpResponse resp = requestString(UPLOAD_PATH, params, encryptedFile);
788 | if (resp.getStatus() == 200) {
789 | return mediaId;
790 | } else {
791 | System.out.println("Upload failed, Response Code: " + resp.getStatus());
792 | return null;
793 | }
794 | } catch (IOException e) {
795 | e.printStackTrace();
796 | return null;
797 | } catch (Encryption.EncryptionException e) {
798 | e.printStackTrace();
799 | return null;
800 | } catch (UnirestException e) {
801 | e.printStackTrace();
802 | return null;
803 | } catch (Exception e) {
804 | e.printStackTrace();
805 | return null;
806 | } catch(OutOfMemoryError e){
807 | e.printStackTrace();
808 | return null;
809 | }
810 | }
811 |
812 | /**
813 | * Add a friend
814 | *
815 | * @param friend username to add.
816 | * @return true if successful, otherwise false.
817 | */
818 | public boolean addFriend(String friend) {
819 | try {
820 | Map params = new HashMap();
821 |
822 | // Add timestamp and requestJson token made using auth token
823 | Long timestamp = getTimestamp();
824 | String reqToken = TokenLib.requestToken(this.authToken, timestamp);
825 |
826 | //Add params
827 | params.put(USERNAME_KEY, this.username);
828 | params.put(TIMESTAMP_KEY, timestamp.toString());
829 | params.put(REQ_TOKEN_KEY, reqToken);
830 | params.put(ACTION_KEY, "add");
831 | params.put(FRIEND_KEY, friend);
832 |
833 | //Make the request
834 | HttpResponse resp = requestString(FRIEND_PATH, params, null);
835 | if (resp.getStatus() == 200 || resp.getStatus() == 201) {
836 | if (resp.getBody().toString().toLowerCase().contains("Sorry!".toLowerCase())) {
837 | return false;
838 | }
839 | return true;
840 | }
841 | } catch (UnirestException e) {
842 | e.printStackTrace();
843 | }
844 | return false;
845 | }
846 |
847 | /**
848 | * Set Display for a Friend
849 | *
850 | * @param friend username to edit.
851 | * @param display display name to set.
852 | * @return true if successful, otherwise false.
853 | */
854 | public boolean setFriendDisplay(String friend, String display){
855 | try {
856 | Map params = new HashMap();
857 |
858 | Long timestamp = getTimestamp();
859 | String reqToken = TokenLib.requestToken(this.authToken, timestamp);
860 |
861 | //Add params
862 | params.put(USERNAME_KEY, this.username);
863 | params.put(TIMESTAMP_KEY, timestamp.toString());
864 | params.put(REQ_TOKEN_KEY, reqToken);
865 | params.put(ACTION_KEY, "display");
866 | params.put(FRIEND_KEY, friend);
867 | params.put(DISPLAY_KEY, display);
868 |
869 | //Make the request
870 | HttpResponse resp = requestString(FRIEND_PATH, params, null);
871 | System.out.println(resp.getBody().toString());
872 | if (resp.getStatus() == 200 || resp.getStatus() == 201) {
873 | return true;
874 | }
875 | } catch (UnirestException e) {
876 | e.printStackTrace();
877 | }
878 | return false;
879 | }
880 |
881 | /**
882 | * ==================================================== PRIVATE STATIC METHODS REGION ====================================================
883 | */
884 |
885 | /**
886 | * Get a new, random media_id for uploading media to Snapchat.
887 | *
888 | * @param username your Snapchat username.
889 | * @return a media_id as a String.
890 | */
891 | private static String getNewMediaId(String username) {
892 | String uuid = UUID.randomUUID().toString();
893 | return username.toUpperCase() + "~" + uuid;
894 | }
895 |
896 | /**
897 | * Get a new timestamp to use in a request.
898 | *
899 | * @return a timestamp.
900 | */
901 | private static Long getTimestamp() {
902 | Long timestamp = (new Date()).getTime() / 1000L;
903 | return timestamp;
904 | }
905 |
906 | public static String acceptLanguageString() {
907 | Locale defaultLocale = Locale.getDefault();
908 | String langStr = defaultLocale.getLanguage();
909 | if (!langStr.equals(Locale.ENGLISH.getLanguage())) {
910 | langStr = langStr + ";q=1, en;q=0.9";
911 | }
912 |
913 | return langStr;
914 | }
915 |
916 | private static MultipartBody prepareRequest(String path, Map params, File file) {
917 | Locale defaultLocale = Locale.getDefault();
918 |
919 | // Set up a JSON request
920 | MultipartBody req = Unirest.post(BASE_URL + path)
921 | .header(JSON_TYPE_KEY, JSON_TYPE)
922 | .header(USER_AGENT_KEY, USER_AGENT)
923 | .header(ACCEPT_LANGUAGE_KEY, acceptLanguageString())
924 | .header(ACCEPT_LOCALE_KEY, defaultLocale.toString())
925 | .fields(params);
926 |
927 | // Add file if there is one
928 | if (file != null) {
929 | return req.field(DATA_KEY, file);
930 | }
931 |
932 | return req;
933 | }
934 |
935 | private static HttpResponse requestBinary(String path, Map params, File file) throws UnirestException {
936 | MultipartBody req = prepareRequest(path, params, file);
937 |
938 | // Execute and return as bytes
939 | HttpResponse resp = req.asBinary();
940 |
941 | // Record
942 | lastRequestPath = path;
943 | lastResponse = resp;
944 | lastResponseBodyClass = InputStream.class;
945 |
946 | return resp;
947 | }
948 |
949 | private static HttpResponse requestJson(String path, Map params, File file) throws UnirestException {
950 | MultipartBody req = prepareRequest(path, params, file);
951 |
952 | // Execute and return response as JSON
953 | HttpResponse resp = req.asJson();
954 |
955 | // Record
956 | lastRequestPath = path;
957 | lastResponse = resp;
958 | lastResponseBodyClass = JsonNode.class;
959 |
960 | return resp;
961 | }
962 |
963 | private static HttpResponse requestStoryBinary(String path) throws UnirestException {
964 | HttpRequest req = Unirest.get(BASE_URL + path)
965 | .header(JSON_TYPE_KEY, JSON_TYPE)
966 | .header(USER_AGENT_KEY, USER_AGENT);
967 |
968 |
969 | // Execute and return as bytes
970 | HttpResponse resp = req.asBinary();
971 |
972 | // Record
973 | lastRequestPath = path;
974 | lastResponse = resp;
975 | lastResponseBodyClass = InputStream.class;
976 |
977 | return resp;
978 | }
979 |
980 | private static HttpResponse requestString(String path, Map params, File file) throws UnirestException {
981 | MultipartBody req = prepareRequest(path, params, file);
982 |
983 | // Execute and return response as String
984 | HttpResponse resp = req.asString();
985 |
986 | // Record
987 | lastRequestPath = path;
988 | lastResponse = resp;
989 | lastResponseBodyClass = String.class;
990 |
991 | return resp;
992 | }
993 |
994 | }
995 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Story.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import org.json.JSONArray;
4 | import org.json.JSONException;
5 | import org.json.JSONObject;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Arrays;
9 | import java.util.List;
10 |
11 | public class Story implements JSONBinder {
12 |
13 | private static final int TYPE_IMAGE = 0;
14 | private static final int TYPE_VIDEO = 1;
15 | private static final int TYPE_VIDEO_NOAUDIO = 2;
16 |
17 | private static final String STORY_KEY = "story";
18 | private static final String STORY_NOTES_KEY = "story_notes";
19 | private static final String STORY_EXTRAS_KEY = "story_extras";
20 | private static final String TIMESTAMP_KEY = "timestamp";
21 |
22 | private static final String MEDIA_ID_KEY = "media_id";
23 | private static final String MEDIA_KEY_KEY = "media_key";
24 | private static final String MEDIA_IV_KEY = "media_iv";
25 | private static final String MEDIA_TYPE_KEY = "media_type";
26 | private static final String SENDER_KEY = "username";
27 | private static final String TIME_KEY = "time";
28 | private static final String TIME_LEFT_KEY = "time_left";
29 | private static final String CAPTION_KEY = "caption_text_display";
30 | private static final String VIEWED_KEY = "viewed";
31 | private static final String SCREENSHOT_COUNT_KEY = "screenshot_count";
32 | private static final String VIEW_COUNT_KEY = "view_count";
33 |
34 | private String id;
35 | private String media_key;
36 | private String media_iv;
37 | private String sender;
38 | private int type;
39 | private int time;
40 | private int time_left;
41 | private Long timestamp;
42 | private boolean viewed;
43 | private boolean isMine;
44 | private Viewer[] viewers;
45 | private int screenshot_count;
46 | private int views;
47 |
48 | private String caption;
49 |
50 | public Story() {
51 | }
52 |
53 |
54 | public Story bind(JSONObject obj) {
55 | try {
56 | JSONObject storyObj = obj.getJSONObject(STORY_KEY);
57 | try {
58 | this.viewed = obj.getBoolean(VIEWED_KEY); //For some reason, this is not in the inner story json obj.
59 | this.isMine = false; //Viewed key only exist for friend's stories.
60 | } catch (JSONException e) {
61 | this.isMine = true;
62 | }
63 |
64 |
65 | this.id = storyObj.getString(MEDIA_ID_KEY);
66 | this.media_key = storyObj.getString(MEDIA_KEY_KEY);
67 | this.media_iv = storyObj.getString(MEDIA_IV_KEY);
68 | this.type = storyObj.getInt(MEDIA_TYPE_KEY);
69 | this.sender = storyObj.getString(SENDER_KEY);
70 | this.timestamp = storyObj.getLong(TIMESTAMP_KEY);
71 | this.time_left = storyObj.getInt(TIME_LEFT_KEY);
72 | this.caption = storyObj.has(CAPTION_KEY) ? storyObj.getString(CAPTION_KEY) : null;
73 | this.time = storyObj.getInt(TIME_KEY);
74 |
75 | if (this.isMine) {
76 | //Who have seen my story
77 | JSONArray notesObj = obj.getJSONArray(STORY_NOTES_KEY);
78 | List viewers = Snapchat.bindArray(notesObj, Viewer.class);
79 | this.viewers = viewers.toArray(new Viewer[viewers.size()]);
80 | //Statistics of my story
81 | JSONObject extrasObj = obj.getJSONObject(STORY_EXTRAS_KEY);
82 | screenshot_count = extrasObj.getInt(SCREENSHOT_COUNT_KEY);
83 | views = extrasObj.getInt(VIEW_COUNT_KEY);
84 | }
85 | } catch (JSONException e) {
86 | System.out.println("Error parsing story : " + obj.toString());
87 | e.printStackTrace();
88 | }
89 | return this;
90 | }
91 |
92 | /**
93 | * Take an array of Stories and returns what is downloadable.
94 | * USELESS: Stories are always downloadable.
95 | *
96 | * @param input the array of Snaps to filter.
97 | * @return the snaps that are downloadable.
98 | */
99 | public static Story[] filterDownloadable(Story[] input) {
100 | List downloadable = new ArrayList();
101 | for (Story s : input) {
102 | if (s.isDownloadable()) {
103 | downloadable.add(s);
104 | }
105 | }
106 | return downloadable.toArray(new Story[downloadable.size()]);
107 | }
108 |
109 | /**
110 | * Check if the story is downloadable.
111 | * USELESS: Stories are always downloadable.
112 | *
113 | * @return true if the story is downloadable.
114 | */
115 | public boolean isDownloadable() {
116 | return true;
117 | }
118 |
119 | /**
120 | * Determine if a Story is a still image.
121 | *
122 | * @return true if it is an image, false if it is a video or other.
123 | */
124 | public boolean isImage() {
125 | return (type == TYPE_IMAGE);
126 | }
127 |
128 | /**
129 | * Determine if a Story is a video.
130 | *
131 | * @return true if it is a video, false if it is an image or other.
132 | */
133 | public boolean isVideo() {
134 | return (type == TYPE_VIDEO || type == TYPE_VIDEO_NOAUDIO);
135 | }
136 |
137 | /**
138 | * Determine if a Story is a video or image.
139 | *
140 | * @return true if it is a video or an image, false if other.
141 | */
142 | public boolean isMedia() {
143 | return (type <= TYPE_VIDEO_NOAUDIO);
144 | }
145 |
146 | /**
147 | * Get this Story ID.
148 | *
149 | * @return the ID.
150 | */
151 | public String getId() {
152 | return id;
153 | }
154 |
155 | /**
156 | * Get the media key of this story. Used for decryption.
157 | *
158 | * @return the media key.
159 | */
160 | public String getMediaKey() {
161 | return media_key;
162 | }
163 |
164 | /**
165 | * Get the media iv. Used for decryption.
166 | *
167 | * @return the media iv.
168 | */
169 | public String getMediaIV() {
170 | return media_iv;
171 | }
172 |
173 | /**
174 | * If you have seen this story.
175 | *
176 | * @return true if seen, otherwise false.
177 | */
178 | public boolean isViewed() {
179 | return this.viewed;
180 | }
181 |
182 | public String getSender() {
183 | return sender;
184 | }
185 |
186 | /**
187 | * Get the maximum allowed time to view this story.
188 | *
189 | * @return maximum time allowed.
190 | */
191 | public int getTime() {
192 | return time;
193 | }
194 |
195 | /**
196 | * Get the expiration date of this story.
197 | *
198 | * @return unix timespamp of the expiration date.
199 | */
200 | public int getTimeLeft() {
201 | return time_left;
202 | }
203 |
204 | /**
205 | * Get the timestamp of creation date for this story.
206 | *
207 | * @return unix timestamp of the creation date.
208 | */
209 | public Long getTimestamp() {
210 | return timestamp;
211 | }
212 |
213 | /**
214 | * Get the text of this story.
215 | *
216 | * @return the text.
217 | */
218 | public String getCaption() {
219 | return caption;
220 | }
221 |
222 | /**
223 | * If the story belongs to us, thus will have viewers...
224 | *
225 | * @return boolean
226 | */
227 | public boolean isMine() {
228 | return this.isMine;
229 | }
230 |
231 | /**
232 | * Get the viewers of this story.
233 | *
234 | * @return Viewer[] the viewers or null.
235 | */
236 | public Viewer[] getViewers() {
237 | return viewers;
238 | }
239 |
240 | /**
241 | * Get the views count of this story.
242 | *
243 | * @return int the viewer count or 0 if not my story.
244 | */
245 | public int getViewCount() {
246 | return views;
247 | }
248 |
249 | /**
250 | * Get the screenshots count of this story.
251 | *
252 | * @return int the screenshot count or 0 if not my story.
253 | */
254 | public int getScreenshotCount() {
255 | return screenshot_count;
256 | }
257 |
258 | @Override
259 | public String toString() {
260 | String[] attrs = new String[]{
261 | id,
262 | sender,
263 | Integer.toString(type),
264 | Integer.toString(time),
265 | Integer.toString(time_left),
266 | Integer.toString(views),
267 | Integer.toString(screenshot_count),
268 | caption
269 | };
270 | return Arrays.toString(attrs);
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/StoryEncryption.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import javax.crypto.Cipher;
4 | import javax.crypto.spec.IvParameterSpec;
5 | import javax.crypto.spec.SecretKeySpec;
6 | import org.apache.commons.codec.binary.Base64;
7 |
8 | /**
9 | * Modified version of Snap Encryption for Stories by Liam Cottle
10 | * Date: 06/04/2014
11 | */
12 | public class StoryEncryption {
13 |
14 | private static final char[] HEX_CHARS = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
15 |
16 | public static byte[] decrypt(byte[] storyData, String MediaKey, String MediaIV) {
17 |
18 | byte[] key = Base64.decodeBase64(MediaKey.getBytes());
19 | byte[] iv = Base64.decodeBase64(MediaIV.getBytes());
20 |
21 | IvParameterSpec ivspec = new IvParameterSpec(iv);
22 | SecretKeySpec keyspec = new SecretKeySpec(key, "AES");
23 |
24 | Cipher cipher = null;
25 | byte[] decrypted = null;
26 | try {
27 | cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //Uses PKCS7Padding which is same as PKCS5Padding
28 | cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
29 | decrypted = cipher.doFinal(storyData);
30 | } catch (Exception e) {
31 | e.printStackTrace();
32 | }
33 | return decrypted;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/TokenLib.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import java.security.MessageDigest;
4 | import java.security.NoSuchAlgorithmException;
5 |
6 | /**
7 | * Author: samstern
8 | * Date: 12/27/13
9 | */
10 | public class TokenLib {
11 |
12 | private static final String SECRET = "iEk21fuwZApXlz93750dmW22pw389dPwOk";
13 | private static final String PATTERN = "0001110111101110001111010101111011010001001110011000110001000110";
14 |
15 | private static final String STATIC_TOKEN = "m198sOkJEn37DjqZ32lpRu76xmw288xSQ9";
16 |
17 | private static final String SHA256 = "SHA-256";
18 |
19 | /**
20 | * Generate a SnapChat Request Token from an Auth Token and a UNIX Timestamp.
21 | *
22 | * @param authToken the SnapChat Auth Token
23 | * @param timestamp the UNIX timestamp (seconds)
24 | * @return the Request Token.
25 | */
26 | public static String requestToken(String authToken, Long timestamp) {
27 | // Create bytesToHex of secret + authToken
28 | String firstHex = hexDigest(SECRET + authToken);
29 |
30 | // Create bytesToHex of timestamp + secret
31 | String secondHex = hexDigest(timestamp.toString() + SECRET);
32 |
33 | // Combine according to pattern
34 | StringBuilder sb = new StringBuilder();
35 | char[] patternChars = PATTERN.toCharArray();
36 | for (int i = 0; i < patternChars.length; i++) {
37 | char c = patternChars[i];
38 | if (c == '0') {
39 | sb.append(firstHex.charAt(i));
40 | } else {
41 | sb.append(secondHex.charAt(i));
42 | }
43 | }
44 |
45 | return sb.toString();
46 | }
47 |
48 | /**
49 | * Get a Request Token for the login requestJson, which uses a static Auth Token.
50 | *
51 | * @param timestamp the UNIX timestamp (seconds)
52 | * @return the Request Token.
53 | */
54 | public static String staticRequestToken(Long timestamp) {
55 | return requestToken(STATIC_TOKEN, timestamp);
56 | }
57 |
58 | /**
59 | * Get the SHA-256 Digest of a String in Hexadecimal.
60 | */
61 | private static String hexDigest(String toDigest) {
62 | try {
63 | MessageDigest sha256 = MessageDigest.getInstance(SHA256);
64 | byte[] digested = sha256.digest(toDigest.getBytes());
65 | return bytesToHex(digested);
66 | } catch (NoSuchAlgorithmException e) {
67 | e.printStackTrace();
68 | return null;
69 | }
70 | }
71 |
72 | /**
73 | * Convert a byte array to a hex string.
74 | * Source: http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
75 | */
76 | private static String bytesToHex(byte[] digested) {
77 | char[] hexArray = "0123456789abcdef".toCharArray();
78 | char[] hexChars = new char[digested.length * 2];
79 |
80 | for (int i = 0; i < digested.length; i++) {
81 | int v = digested[i] & 0xFF;
82 | hexChars[i * 2] = hexArray[v >>> 4];
83 | hexChars[(i * 2) + 1] = hexArray[v & 0x0F];
84 | }
85 |
86 | return (new String(hexChars));
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/com/habosa/javasnap/Viewer.java:
--------------------------------------------------------------------------------
1 | package com.habosa.javasnap;
2 |
3 | import org.json.JSONException;
4 | import org.json.JSONObject;
5 |
6 | import java.util.ArrayList;
7 | import java.util.Arrays;
8 |
9 | public class Viewer implements JSONBinder {
10 |
11 | private static final String VIEWER_KEY = "viewer"; //Always : Viewer Username.
12 | private static final String SCREENSHOTTED_KEY = "screenshotted"; //Always : If your story has been viewed or not.
13 | private static final String TIMESTAMP_KEY = "timestamp"; //Always : Timestamp of when the story was viewed by this viewer.
14 |
15 | private String viewer;
16 | private Boolean screenshotted;
17 | private Long timestamp;
18 |
19 | public Viewer() { }
20 |
21 | public Viewer bind(JSONObject obj) {
22 | // Check for fields that always exist
23 | try {
24 | this.viewer = obj.getString(VIEWER_KEY);
25 | this.screenshotted = obj.getBoolean(SCREENSHOTTED_KEY);
26 | this.timestamp = obj.getLong(TIMESTAMP_KEY);
27 | } catch (JSONException e) {
28 | e.printStackTrace();
29 | }
30 | return this;
31 | }
32 |
33 | /**
34 | * Has user screenshoted this story.
35 | *
36 | * @return true if user has screenshotted.
37 | */
38 | public boolean isScreenshoted(){
39 | return screenshotted;
40 | }
41 |
42 | /**
43 | * Get this viewer username.
44 | *
45 | * @return the viewer username.
46 | */
47 | public String getViewer() {
48 | return viewer;
49 | }
50 |
51 | /**
52 | * Get when the user has seen this story.
53 | *
54 | * @return unix timestamp of when the user has seen this story.
55 | */
56 | public Long getTimestamp() {
57 | return timestamp;
58 | }
59 |
60 | @Override
61 | public String toString() {
62 | String[] attrs = new String[]{
63 | viewer,
64 | String.valueOf(screenshotted),
65 | String.valueOf(timestamp)
66 | };
67 | return Arrays.toString(attrs);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------