├── .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 | [![deprecated](http://badges.github.io/stability-badges/dist/deprecated.svg)](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> 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 | --------------------------------------------------------------------------------