├── .gitignore ├── LICENSE ├── README.md ├── SnapWrap ├── Client │ ├── __init__.py │ └── utils.py ├── __init__.py ├── constants.py ├── snap.py ├── snapchat.py └── utils.py ├── example.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### Misc ### 2 | *.pyc 3 | *.pem 4 | .settings 5 | SnapWrap/example.py 6 | 7 | ### Python ### 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | 65 | ### Eclipse ### 66 | *.pydevproject 67 | .metadata 68 | .gradle 69 | bin/ 70 | tmp/ 71 | *.tmp 72 | *.bak 73 | *.swp 74 | *~.nib 75 | local.properties 76 | .settings/ 77 | .loadpath 78 | 79 | # Eclipse Core 80 | .project 81 | 82 | # External tool builders 83 | .externalToolBuilders/ 84 | 85 | # Locally stored "Eclipse launch configurations" 86 | *.launch 87 | 88 | # CDT-specific 89 | .cproject 90 | 91 | # JDT-specific (Eclipse Java Development Tools) 92 | .classpath 93 | 94 | # PDT-specific 95 | .buildpath 96 | 97 | # sbteclipse plugin 98 | .target 99 | 100 | # TeXlipse plugin 101 | .texlipse 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SnapWrap 2 | ### Wrapper for the unofficial and undocumented Snapchat API. 3 | --- 4 | 5 | SnapWrap is essentially an API wrapper for Snapchat's API but includes many functions that increase functionality and easability when conversing with Snapchat's API. 6 | This library would not be possible without [agermanidis](https://github.com/agermanidis/SnapchatBot) or [martinp](https://github.com/martinp/pysnap). 7 | 8 | SnapWrap's is based directly off agermanidis' library which includes martinp's library (to deal with low level access to the API) - this library includes the newest endpoints of Snapchat's API (`loq`) and are used when possible. 9 | 10 | While finding, inspecting and implementing the newest endpoints I've documented everything I thought necessary. Documentation on the API will expand. 11 | 12 | This library does slightly differ to agermanidis' apart from the endpoints. I've added more functions - such as an easier way to save snaps and the ability to mark a snap as replayed. 13 | 14 | --- 15 | # Installation 16 | 17 | `python setup.py install` 18 | 19 | # Usage 20 | 21 | The following code will simply initiate a class with three "listener" functions that are called appropriately after you call the `begin()` method. The library is being imported, logging in with the given credentials and then running a constant loop that will check for new updates (snap, friends, etc). 22 | 23 | from snapchat import Snapchat 24 | 25 | class CustomBot(Snapchat): 26 | def on_snap(self, sender, snap): 27 | self.send_snap(snap, sender) 28 | 29 | def on_friend_add(self, friend): 30 | self.add_friend(friend) 31 | 32 | def on_friend_delete(self, friend): 33 | self.delete_friend(friend) 34 | 35 | bot = CustomBot(*["user", "pass"]) 36 | bot.begin() 37 | 38 | # Disclosure 39 | 40 | [http://rob--.github.io/](http://rob--.github.io/) 41 | 42 | # Functions 43 | 44 | #### Core Functions 45 | 46 | `begin(timeout, mark_viewed, mark_screenshotted, mark_replayed)` - starts a permanent cycle (with a delay on each iteration) of looking for new snaps and checking for newly added/deleted users. Parameters are optional. 47 | 48 | `get_snaps(mark_viewed, mark_screenshotted, mark_replayed)` - retrieves all new snaps. Parameters are optional. 49 | 50 | `register(username, password, birthday, email)` - registers an account with the corresponding information. Birthdays need to be in the format 'YYYY-MM-DD'. 51 | 52 | #### Snap Functions 53 | 54 | `send_snap(snap, recipients)` - sends a snap. Recipients can either be a string or a list. 55 | 56 | `save_snap(snap, dir)` - saves a snap to a given directory. 57 | 58 | `post_story(snap)` - posts a snap to your story. 59 | 60 | `delete_story(snap)` - deletes a snap from your story. 61 | 62 | `get_friend_stories()` - returns a dict of your friends' usernames and their stories (`{'user1' : [storyObj1, storyObj2], "user2": [storyObj1]}`). 63 | 64 | #### Friends 65 | 66 | `get_friends()` - returns a list/array of people that you've added. 67 | 68 | `get_best_friends()` - returns a list/array of your best friends. 69 | 70 | `get_added_me()` - returns a list/array of people that have added you. 71 | 72 | `get_blocked()` - returns a list/array of people you've blocked. 73 | 74 | `add_friend(username)` - adds a user to your friends list. 75 | 76 | `delete_friend(username)` - deletes a user from your friends list. 77 | 78 | `block(username)` - blocks a user. 79 | 80 | `unblock(username)` - unblocks a user. 81 | 82 | #### Misc 83 | 84 | `update_privacy(friends_only)` - updates your privacy settings. If `friends_only` is true, only friends can send you snaps/see your story. If it's false, anyone can. 85 | 86 | `logout()` - logs the account out. 87 | 88 | `from_file(dir)` - upload the snap from a file, e.g. `self.from_file("C:\image.jpg")`. 89 | 90 | `clear_feed()` - clears the snap feed. 91 | 92 | `clear_conversation(username)`- clears a conversation with the given user. 93 | -------------------------------------------------------------------------------- /SnapWrap/Client/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json, os.path 4 | from time import time 5 | from datetime import date 6 | from SnapWrap.Client.utils import (encrypt, decrypt, decrypt_story, make_media_id, request, timestamp, requests) 7 | from SnapWrap.constants import (DEFAULT_DURATION, MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO_NO_AUDIO, MEDIA_TYPE_IMAGE, PRIVACY_FRIENDS, PRIVACY_EVERYONE, FRIEND_BLOCKED) 8 | 9 | def is_video(data): 10 | return len(data) > 1 and data[0:2] == b'\x00\x00' 11 | 12 | 13 | def is_image(data): 14 | return len(data) > 1 and data[0:2] == b'\xFF\xD8' 15 | 16 | 17 | def is_zip(data): 18 | return len(data) > 1 and data[0:2] == b'PK' 19 | 20 | 21 | def get_file_extension(media_type): 22 | if media_type in (MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO_NO_AUDIO): 23 | return 'mp4' 24 | if media_type == MEDIA_TYPE_IMAGE: 25 | return 'jpg' 26 | return '' 27 | 28 | def get_media_type(data): 29 | if is_video(data): 30 | return MEDIA_TYPE_VIDEO 31 | if is_image(data): 32 | return MEDIA_TYPE_IMAGE 33 | return None 34 | 35 | def _map_keys(snap): 36 | return { 37 | u'id': (snap['id'] if 'id' in snap else None), 38 | u'media_id': (snap['c_id'] if 'c_id' in snap else None), 39 | u'media_type': (snap['m'] if 'm' in snap else None), 40 | u'time': (snap['t'] if 't' in snap else None), 41 | u'sender': (snap['sn'] if 'sn' in snap else None), 42 | u'recipient': (snap['rp'] if '' in snap else None), 43 | u'status': (snap['st'] if 'st' in snap else None), 44 | u'screenshot_count': (snap['c'] if 'c' in snap else None), 45 | u'sent': (snap['sts'] if 'sts' in snap else None), 46 | u'opened': (snap['ts'] if 'ts' in snap else None) 47 | } 48 | 49 | class Snapchat(object): 50 | def __init__(self): 51 | self._reset() 52 | 53 | def _request(self, endpoint, data=None, files=None, raise_for_status=True, req_type='post'): 54 | return request(endpoint, self.auth_token, data, files, raise_for_status, req_type) 55 | 56 | def _reset(self): 57 | self.username = None 58 | self.auth_token = None 59 | 60 | def register(self, username, password, birthday, email): 61 | """ 62 | Registers a Snapchat account. 63 | Returns: response identical to the one recieved when logging in if successful, 64 | response from the request (includes a message/status) if unsuccessful. 65 | 66 | :param username: the username you wish to register. 67 | :param password: the password. 68 | :param birthday: the birthday. Needs to be in the format (string) 'YYYY-MM-DD', e.g. 1995-01-01. 69 | :param email: the email address tied to the account. 70 | """ 71 | result = self._request('loq/register', { 72 | 'age': date.today().year - int(birthday.split("-")[0]), 73 | 'birthday': birthday, 74 | 'dsig': 'd56e1a29cdcd6b0924cf', 75 | 'dtoken1i': self._request('loq/device_id', {}).json()["dtoken1i"], 76 | 'email': email, 77 | 'password': password 78 | }).json() 79 | 80 | if result['logged'] is False: 81 | if 'status' in result: 82 | print("Register failed, %s - %s" % (result['status'], result['message'])) 83 | else: 84 | print("Register failed, %s" % (result['message'])) 85 | return result 86 | 87 | if 'auth_token' in result: 88 | self.auth_token = result['auth_token'] 89 | 90 | result = self._request('loq/register_username', { 91 | 'username': email, 92 | 'selected_username' : username 93 | }).json() 94 | 95 | if 'logged' in result: 96 | if result['logged'] is False: 97 | if 'status' in result: 98 | print("Register failed, %s - %s" % (result['status'], result['message'])) 99 | else: 100 | print("Register failed, %s" % (result['message'])) 101 | return result 102 | 103 | if 'auth_token' in result['updates_response']: 104 | self.auth_token = result['updates_response']['auth_token'] 105 | if 'username' in result['updates_response']: 106 | self.username = result['updates_response']['username'] 107 | return result 108 | 109 | def login(self, username, password): 110 | self._reset() 111 | import pdb 112 | pdb.set_trace() 113 | result = self._request('loq/login', { 114 | 'username': username, 115 | 'password': password, 116 | 'ptoken': str(requests.post( 117 | "https://android.clients.google.com/c2dm/register3", 118 | data={ 119 | 'X-GOOG.USER_AID':'4002600885140111980', 120 | 'app':'com.snapchat.android', 121 | 'sender':'191410808405', 122 | 'cert':'49f6badb81d89a9e38d65de76f09355071bd67e7', 123 | 'device':'4002600885140111980', 124 | 'app_ver':'508', 125 | 'info':'', 126 | }, 127 | headers={ 128 | 'app': 'com.snapchat.android', 129 | 'User-Agent': 'Android-GCM/1.4 (mako JDQ39)', 130 | 'Authorization' : 'AidLogin 4002600885140111980:7856388705669173275' 131 | } 132 | ).content 133 | ).replace("token=", ""), 134 | 'retry': '0', 135 | 'dtoken1i': self._request('loq/device_id', {}).json()["dtoken1i"], 136 | 'dsig': 'd56e1a29cdcd6b0924cf', 137 | }).json() 138 | 139 | if "status" in result: 140 | return result 141 | if 'auth_token' in result['updates_response']: 142 | self.auth_token = result['updates_response']['auth_token'] 143 | if 'username' in result['updates_response']: 144 | self.username = result['updates_response']['username'] 145 | return result 146 | 147 | def logout(self): 148 | """ 149 | Logout of the Snapchat account. 150 | Returns: True if successful. False is unsuccessful. 151 | """ 152 | return len(self._request('logout', { 153 | 'username': self.username 154 | }).content) == 0 155 | 156 | def get_updates(self, update_timestamp=0): 157 | """ 158 | Gets updates - user, friend, snap and conversation information. 159 | Returns: dict containing the response (in JSON format). 160 | 161 | :param update_timestamp: optional timestamp (epoch in seconds) to limit updates. 162 | """ 163 | result = self._request('loq/all_updates', { 164 | 'username': self.username, 165 | 'features_map': '{"conversations_delta_response":true,"stories_delta_response":true}' 166 | }).json() 167 | if 'auth_token' in result['updates_response']: 168 | self.auth_token = result['updates_response']['auth_token'] 169 | return result 170 | 171 | def get_snaps(self, update_timestamp=0): 172 | """ 173 | Gets snaps - finds all new (unopened) snaps. 174 | Returns: list of snap object containing meta data. 175 | 176 | :param update_timestamp: optional timestamp (epoch in seconds) to limit updates. 177 | """ 178 | return[_map_keys(snap) for snap in self.get_updates(update_timestamp)['conversations_response'][0]['pending_received_snaps'] 179 | if 'c_id' not in snap] 180 | 181 | def get_friend_stories(self, update_timestamp=0): 182 | """ 183 | Get your friends' stories. 184 | Returns: Dict containing users and their stories. 185 | 186 | Structure: 187 | {'user1' : [storyObj1, storyObj2], "user2": [storyObj1]} 188 | 189 | :param update_timestamp: optional timestamp (epoch in seconds) to limit updates. 190 | """ 191 | all = {} 192 | for friend in self.get_updates(update_timestamp)['stories_response']['friend_stories']: 193 | stories = [] 194 | for story in friend['stories']: 195 | obj = story['story'] 196 | obj['sender'] = friend['username'] 197 | stories.append(obj) 198 | all[friend['username']] = stories 199 | return all 200 | 201 | def get_story_blob(self, story_id, story_key, story_iv): 202 | """ 203 | Gets the image/video of a given story snap. 204 | Returns: the decrypted data of given story snap, None if the data is invalid. 205 | 206 | :param story_id: media id of the story snap. 207 | :param story_key: encryption key of the story. 208 | :param story_iv: encryption IV of the story. 209 | """ 210 | r = self._request('story_blob', { 211 | 'story_id': story_id 212 | }, raise_for_status=False, req_type='get') 213 | 214 | data = decrypt_story(r.content, story_key, story_iv) 215 | if any((is_image(data), is_video(data), is_zip(data))): 216 | return data 217 | return None 218 | 219 | def get_blob(self, snap_id): 220 | """ 221 | Gets the image/video of a given snap. 222 | Returns: the decrypted data of given snap, None if the data is invalid. 223 | 224 | param: snap_id: id of the given snap. 225 | """ 226 | data = decrypt(self._request('ph/blob', { 227 | 'username': self.username, 'id': snap_id 228 | }, raise_for_status=False).content) 229 | 230 | if any((is_image(data), is_video(data), is_zip(data))): 231 | return data 232 | return None 233 | 234 | def send_events(self, events, data=None): 235 | """ 236 | Send event data. This is used update information about snaps, users and conversations. 237 | Returns: True if successful, False if unsuccessful. 238 | 239 | :param events: list of events to send (list of dicts). 240 | :param data: dict of additional data to send. 241 | """ 242 | if data is None: 243 | data = {} 244 | return len(self._request('bq/update_snaps', { 245 | 'username': self.username, 246 | 'events': json.dumps(events), 247 | 'json': json.dumps(data) 248 | }).content) == 0 249 | 250 | def mark_viewed(self, snap_id, sender, view_duration=1, replayed=False): 251 | """ 252 | Marks a given snap as viewed. 253 | Returns: True if successful, False if unsuccessful. 254 | 255 | :param snap_id: the id of the snap. 256 | :param sender: the sender of the snap. 257 | :param view_duration: number of seconds the snap was viewed. 258 | :param replayed: mark the snap as having been replayed. 259 | """ 260 | now = time() 261 | data = {snap_id: {u"c": 0, "replayed": 1 if replayed else 0, u"sv": view_duration, u"t": now}} 262 | events = [ 263 | { 264 | u"eventName": u"SNAP_VIEW", 265 | u"params": { 266 | "time": view_duration, 267 | "id": snap_id, 268 | "type":"IMAGE", 269 | "sender": sender 270 | }, 271 | u'ts': int(round(now)) - view_duration 272 | } 273 | ] 274 | return self.send_events(events, data) 275 | 276 | def mark_screenshot(self, snap_id, sender, view_duration=1, replayed=False): 277 | """Mark a snap as screenshotted 278 | Returns true on success. 279 | 280 | :param snap_id: Snap id to mark as viewed 281 | :param sender: the sender of the snap. 282 | :param view_duration: Number of seconds snap was viewed 283 | :param replayed: mark the snap as having been replayed. 284 | """ 285 | now = time() 286 | data = {snap_id: {u"c": 0, "replayed": 1 if replayed else 0, u"sv": view_duration, u"t": now}} 287 | events = [ 288 | { 289 | u"eventName": u"SNAP_SCREENSHOT", 290 | u"params": { 291 | "time": view_duration, 292 | "id": snap_id, 293 | "type":"IMAGE", 294 | "sender": sender 295 | }, 296 | u'ts': int(round(now)) - view_duration 297 | } 298 | ] 299 | return self.send_events(events, data) 300 | 301 | def update_privacy(self, friends_only): 302 | """ 303 | Change privacy settings (from whom can you receive snaps from). 304 | Returns: True if successful, False if unsuccessful. 305 | 306 | :param friends_only: True is friends only, False if everyone. 307 | """ 308 | return self._request('ph/settings', { 309 | 'username': self.username, 310 | 'action': 'updatePrivacy', 311 | 'privacySetting': PRIVACY_FRIENDS if friends_only else PRIVACY_EVERYONE 312 | }).json()['param'] == str(PRIVACY_FRIENDS if friends_only else PRIVACY_EVERYONE) 313 | 314 | def update_story_privacy(self, friends_only): 315 | """ 316 | Change story privacy settings (from whom can you receive snaps from). 317 | Returns: True if successful, False if unsuccessful. 318 | 319 | :param friends_only: True is friends only, False if everyone. 320 | """ 321 | return self._request('ph/settings', { 322 | 'username': self.username, 323 | 'action': 'updateStoryPrivacy', 324 | 'privacySetting': "FRIENDS" if friends_only else "EVERYONE" 325 | }).json()['param'] == str("FRIENDS" if friends_only else "EVERYONE") 326 | 327 | def update_birthday(self, birthday): 328 | """ 329 | Change your birthday. 330 | Returns: True if successful, False if unsuccessful. 331 | 332 | :param birthday: the birthday, needs to be in the format "MM-DD". 333 | """ 334 | return self._request('ph/settings', { 335 | 'username': self.username, 336 | 'action': 'updateBirthday', 337 | 'birthday': birthday 338 | }).json()["logged"] 339 | 340 | def update_email(self, email): 341 | """ 342 | Change your email. 343 | Returns: True if successful, False if unsuccessful. 344 | 345 | :param email: the email address. 346 | """ 347 | return self._request('ph/settings', { 348 | 'username': self.username, 349 | 'action': 'updateEmail', 350 | 'email': email 351 | }).json()["logged"] 352 | 353 | def update_number_of_best_friends(self, number): 354 | """ 355 | Change the number of best friends to be displayed. 356 | Returns: True if successful, False if unsuccessful. 357 | 358 | :param number: the number of best friends to be displayed. 359 | """ 360 | return self._request('bq/set_num_best_friends', { 361 | 'username': self.username, 362 | 'num_best_friends': number 363 | }).json()["logged"] 364 | 365 | def get_friends(self): 366 | """ 367 | Retrieves the section of the response from getting updates about friends. 368 | Returns: JSON that consists of 'bests', 'friends', and 'added_friends'. 369 | """ 370 | return self.get_updates()['friends_response'] 371 | 372 | def add_friend(self, username): 373 | """ 374 | Add a user to your friends list. 375 | Returns: True if successful, False if unsuccessful. 376 | 377 | :param username: Username to add as a friend 378 | """ 379 | return self._request('bq/friend', { 380 | 'friend_source': 'ADDED_BY_USERNAME', 381 | 'username': self.username, 382 | 'action': 'add', 383 | 'friend': username, 384 | }).json()['logged'] 385 | 386 | def delete_friend(self, username): 387 | """ 388 | Delete a user from your friends list. 389 | Returns: True if successful, False if unsuccessful. 390 | 391 | :param username: username of the user to remove. 392 | """ 393 | return self._request('bq/friend', { 394 | 'friend_source': 'ADDED_BY_USERNAME', 395 | 'username': self.username, 396 | 'action': 'delete', 397 | 'friend': username, 398 | }).json()['logged'] 399 | 400 | def delete_story(self, story_id): 401 | if story_id is None: 402 | return 403 | self.client._request('bq/delete_story', { 404 | 'username': self.username, 405 | 'story_id': story_id 406 | }) 407 | 408 | def block(self, username): 409 | """ 410 | Block a user. 411 | Returns: True if successful, False if unsuccessful. 412 | 413 | :param username: username of the user to block. 414 | """ 415 | return self._request('bq/friend', { 416 | 'action': 'block', 417 | 'friend': username, 418 | 'username': self.username 419 | }).json()['logged'] 420 | 421 | def unblock(self, username): 422 | """ 423 | Unblock a user. 424 | Returns: True if successful, False if unsuccessful. 425 | 426 | :param username: username of the user to unblock. 427 | """ 428 | return self._request('bq/friend', { 429 | 'action': 'unblock', 430 | 'friend': username, 431 | 'username': self.username 432 | }).json()['logged'] 433 | 434 | def get_blocked(self): 435 | """ 436 | Finds blocked users. 437 | Returns: list of currently blocked users. 438 | """ 439 | return [f for f in self.get_friends()['friends'] if f['type'] == FRIEND_BLOCKED] 440 | 441 | def upload(self, path): 442 | """ 443 | Uploads media from a given path. 444 | Returns: media id if successful, None if unsuccessful. 445 | """ 446 | if not os.path.exists(path): 447 | raise ValueError('No such file: {0}'.format(path)) 448 | 449 | with open(path, 'rb') as f: 450 | data = f.read() 451 | 452 | media_type = get_media_type(data) 453 | if media_type is None: 454 | raise ValueError('Could not determine media type for given data.') 455 | 456 | media_id = make_media_id(self.username) 457 | r = self._request('ph/upload', { 458 | 'media_id': media_id, 459 | 'username': self.username, 460 | 'type': media_type 461 | }, files={'data': encrypt(data)}) 462 | return media_id if len(r.content) == 0 else None 463 | 464 | def send(self, media_id, recipients, duration=DEFAULT_DURATION): 465 | """ 466 | Sends a snap. 467 | The snap needs to be uploaded first as this returns a media_id that is used in this method. 468 | Returns: True if successful, False if unsuccessful. 469 | """ 470 | return len(self._request('loq/send', { 471 | 'media_id': media_id, 472 | 'time': int(duration), 473 | 'username': self.username, 474 | 'zipped': 0, 475 | 'recipients': json.dumps(recipients), 476 | }).content) == 0 477 | 478 | def send_to_story(self, snap): 479 | """ 480 | Sends a snap to your story. 481 | The snap needs to be uploaded first as this returns a media_id that is used in this method. 482 | Returns: The story id of the snap. 483 | """ 484 | return self._request('bq/post_story', { 485 | 'caption_text_display': snap.sender, 486 | 'story_timestamp': timestamp(), 487 | 'media_id': snap.media_id, 488 | 'client_id': snap.media_id, 489 | 'time': snap.duration, 490 | 'username': self.username, 491 | 'my_story': 'true', 492 | 'zipped': '0', 493 | 'type': snap.media_type 494 | })['json']['story']['id'] 495 | 496 | def clear_feed(self): 497 | """ 498 | Clears the user's feed 499 | Returns: True if successful, False if unsuccessful. 500 | """ 501 | return len(self._request('ph/clear', { 502 | 'username': self.username 503 | }).content) == 0 504 | 505 | def clear_conversation(self, user): 506 | """ 507 | Clears conversations for the given users. 508 | Returns: True if successful, False if unsuccessful. 509 | """ 510 | return len(self._request('loq/clear_conversation', { 511 | 'conversation_id': '{u}~{username}'.format(u=user, username=self.username), 512 | 'username': self.username 513 | }).content) == 0 -------------------------------------------------------------------------------- /SnapWrap/Client/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | This module contains methods for creating request tokens and 5 | encryption/decryption of snaps 6 | """ 7 | 8 | from hashlib import sha256 9 | from time import time 10 | from uuid import uuid4 11 | 12 | import requests 13 | from Crypto.Cipher import AES 14 | 15 | URL = 'https://feelinsonice-hrd.appspot.com/' 16 | 17 | SECRET = b'iEk21fuwZApXlz93750dmW22pw389dPwOk' 18 | STATIC_TOKEN = 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9' 19 | BLOB_ENCRYPTION_KEY = 'M02cnQ51Ji97vwT4' 20 | HASH_PATTERN = ('00011101111011100011110101011110' 21 | '11010001001110011000110001000110') 22 | 23 | def make_request_token(a, b): 24 | hash_a = sha256(SECRET + a.encode('utf-8')).hexdigest() 25 | hash_b = sha256(b.encode('utf-8') + SECRET).hexdigest() 26 | return ''.join((hash_b[i] if c == '1' else hash_a[i] 27 | for i, c in enumerate(HASH_PATTERN))) 28 | 29 | def get_token(auth_token=None): 30 | return STATIC_TOKEN if auth_token is None else auth_token 31 | 32 | def pkcs5_pad(data, blocksize=16): 33 | pad_count = blocksize - len(data) % blocksize 34 | return data + (chr(pad_count) * pad_count).encode('utf-8') 35 | 36 | def decrypt(data): 37 | cipher = AES.new(BLOB_ENCRYPTION_KEY, AES.MODE_ECB) 38 | return cipher.decrypt(pkcs5_pad(data)) 39 | 40 | def decrypt_story(data, key, iv): 41 | cipher = AES.new(key, AES.MODE_CBC, iv) 42 | return cipher.decrypt(pkcs5_pad(data)) 43 | 44 | def encrypt(data): 45 | return AES.new(BLOB_ENCRYPTION_KEY, AES.MODE_ECB).encrypt(pkcs5_pad(data)) 46 | 47 | def timestamp(): 48 | return int(round(time() * 1000)) 49 | 50 | def request(endpoint, auth_token, data=None, files=None, 51 | raise_for_status=True, req_type='post'): 52 | """ 53 | Method to send the request to Snapchat's API. 54 | Automatically adds two common fields: `req_token` and `timestamp`. 55 | 56 | :param endpoint: the api endpoint. 57 | :param data: dict containing data 58 | :param raise_for_status: aise exception for 4xx and 5xx status codes 59 | :param req_type: the request type ('GET', 'POST'), default is 'POST'. 60 | """ 61 | now = timestamp() 62 | headers = { 63 | #'User-Agent': 'Snapchat/9.2.0.0 (Nexus 5; Android 5.0.1#1602158#21; gzip)', 64 | 'User-Agent': 'Snapchat/8.1.1 (iPhone5,1; iOS 6.1.4; gzip)', 65 | 'Accept-Language': 'en-US;q=1, en;q=0.9', 66 | 'Accept-Locale': 'en' 67 | } 68 | if req_type == 'post': 69 | data.update({ 70 | 'timestamp': now, 71 | 'req_token': make_request_token(auth_token or STATIC_TOKEN, str(now)), 72 | }) 73 | r = requests.post(URL + endpoint, data=data if data is not None else {}, files=files, headers=headers, verify=False) 74 | else: 75 | r = requests.get(URL + endpoint, params=data if data is not None else {}, headers=headers) 76 | if raise_for_status: 77 | r.raise_for_status() 78 | return r 79 | 80 | def make_media_id(username): 81 | """Create a unique media identifier. Used when uploading media""" 82 | return '{username}~{uuid}'.format(username=username.upper(), uuid=str(uuid4())) 83 | -------------------------------------------------------------------------------- /SnapWrap/__init__.py: -------------------------------------------------------------------------------- 1 | from snap import Snap 2 | from snapchat import Snapchat 3 | import requests.packages, logging 4 | 5 | requests.packages.urllib3.disable_warnings() 6 | logging.getLogger("requests").setLevel(logging.WARNING) -------------------------------------------------------------------------------- /SnapWrap/constants.py: -------------------------------------------------------------------------------- 1 | DEFAULT_TIMEOUT = 15 2 | DEFAULT_DURATION = 5 3 | 4 | SNAP_IMAGE_DIMENSIONS = (1334, 750) 5 | 6 | MEDIA_TYPE_UNKNOWN = -1 7 | MEDIA_TYPE_IMAGE = 0 8 | MEDIA_TYPE_VIDEO = 1 9 | MEDIA_TYPE_VIDEO_NO_AUDIO= 2 10 | 11 | FRIEND_CONFIRMED = 0 12 | FRIEND_UNCONFIRMED = 1 13 | FRIEND_BLOCKED = 2 14 | PRIVACY_EVERYONE = 0 15 | PRIVACY_FRIENDS = 1 -------------------------------------------------------------------------------- /SnapWrap/snap.py: -------------------------------------------------------------------------------- 1 | import subprocess, uuid 2 | from PIL import Image 3 | from StringIO import StringIO 4 | 5 | from SnapWrap.utils import guess_type, create_temporary_file, get_video_duration, resize_image, file_extension_for_type 6 | from constants import MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO_NO_AUDIO, DEFAULT_DURATION 7 | 8 | class UnknownMediaType(Exception): 9 | pass 10 | 11 | class Snap(object): 12 | @staticmethod 13 | def from_file(path, duration=DEFAULT_DURATION, username=None): 14 | media_type = guess_type(path) 15 | 16 | if media_type is MEDIA_TYPE_VIDEO or media_type is MEDIA_TYPE_VIDEO_NO_AUDIO: 17 | if duration is None: duration = get_video_duration(path) 18 | tmp = create_temporary_file(".snap.mp4") 19 | output_path = tmp.name 20 | subprocess.Popen(["ffmpeg", "-y", "-i", path, output_path]).wait() 21 | 22 | elif media_type is MEDIA_TYPE_IMAGE: 23 | image = Image.open(path) 24 | tmp = create_temporary_file(".jpg") 25 | output_path = tmp.name 26 | resize_image(image, output_path) 27 | if duration is None: duration = DEFAULT_DURATION 28 | else: 29 | raise UnknownMediaType("Could not determine media type of the file.") 30 | 31 | return Snap(path=output_path, media_type=media_type, duration=duration, self_post=True, username=username) 32 | 33 | @staticmethod 34 | def from_image(img, duration=DEFAULT_DURATION): 35 | f = create_temporary_file(".jpg") 36 | resize_image(img, f.name) 37 | return Snap(path=f.name, media_type=MEDIA_TYPE_IMAGE, duration=duration) 38 | 39 | def upload(self, bot): 40 | self.media_id = bot.client.upload(self.file.name) 41 | self.uploaded = True 42 | 43 | def __init__(self, **opts): 44 | self.uploaded = False 45 | self.duration = opts['duration'] 46 | self.media_type = opts['media_type'] 47 | self.story_id = None 48 | 49 | if 'self_post' in opts: 50 | self.sender = opts['username'] 51 | self.from_me = True 52 | 53 | if 'sender' in opts: 54 | self.sender = opts['sender'] 55 | self.snap_id = opts['snap_id'] 56 | self.from_me = False 57 | else: 58 | self.snap_id = uuid.uuid4().hex 59 | self.from_me = True 60 | 61 | if 'data' in opts: 62 | self.media_type = opts['media_type'] 63 | self.file = create_temporary_file("." + file_extension_for_type(opts['media_type'])) 64 | 65 | if self.media_type is MEDIA_TYPE_VIDEO or self.media_type is MEDIA_TYPE_VIDEO_NO_AUDIO: 66 | self.file.write(opts['data']) 67 | self.file.flush() 68 | else: 69 | resize_image(Image.open(StringIO(opts['data'])), self.file.name) 70 | else: 71 | self.file = open(opts['path']) 72 | -------------------------------------------------------------------------------- /SnapWrap/snapchat.py: -------------------------------------------------------------------------------- 1 | from time import sleep, strftime 2 | from SnapWrap import Client, Snap 3 | from SnapWrap.utils import save_snap 4 | from constants import DEFAULT_TIMEOUT 5 | 6 | class Snapchat(object): 7 | def __init__(self, username, password, **kwargs): 8 | self.username = username 9 | self.password = password 10 | 11 | self.client = Client.Snapchat() 12 | info = self.client.login(username, password) 13 | if self.client.username is None and self.client.auth_token is None: 14 | self.log('Login failed, %s - %s' % (info['status'], info['message'])) 15 | raise SystemExit(1) 16 | 17 | self.current_friends = self.get_friends() 18 | self.added_me = self.get_added_me() 19 | 20 | if hasattr(self, "initialize"): 21 | self.initialize(**kwargs) 22 | 23 | def log(self, message): 24 | print("[%s %s] %s" % (strftime("%x"), strftime("%X"), message)) 25 | 26 | @staticmethod 27 | def process_snap(snap_obj, data): 28 | return Snap( 29 | data=data, 30 | snap_id=snap_obj['id'], 31 | media_type=snap_obj["media_type"], 32 | duration=snap_obj['time'], 33 | sender=snap_obj["sender"] 34 | ) 35 | 36 | def begin(self, timeout=DEFAULT_TIMEOUT, mark_viewed=True, mark_screenshotted=False, mark_replayed=False): 37 | while True: 38 | self.log("Querying for new snaps...") 39 | snaps = self.get_snaps(mark_viewed, mark_screenshotted, mark_replayed) 40 | 41 | if hasattr(self, "on_snap"): 42 | for snap in snaps: 43 | self.on_snap(snap.sender, snap) 44 | 45 | added_me = self.get_added_me() 46 | 47 | newly_added = set(added_me).difference(self.added_me) 48 | newly_deleted = set(self.added_me).difference(added_me) 49 | 50 | self.added_me = added_me 51 | 52 | for friend in newly_added: 53 | self.log("%s has added me." % friend) 54 | if hasattr(self, "on_friend_add"): 55 | self.on_friend_add(friend) 56 | 57 | for friend in newly_deleted: 58 | self.log("%s has deleted me." % friend) 59 | if hasattr(self, "on_friend_delete"): 60 | self.on_friend_delete(friend) 61 | 62 | sleep(timeout) 63 | 64 | def get_friends(self): 65 | return map(lambda fr: fr['name'], self.client.get_friends()['friends']) 66 | 67 | def get_best_friends(self): 68 | return map(lambda fr: fr['name'], self.client.get_friends()['bests']) 69 | 70 | def get_added_me(self): 71 | return map(lambda fr: fr['name'], self.client.get_friends()['added_friends']) 72 | 73 | def get_blocked(self): 74 | return self.client.get_blocked() 75 | 76 | def send_snap(self, snap, recipients): 77 | if not snap.uploaded: 78 | self.log("Status: uploading, id: %s." % snap.snap_id) 79 | snap.upload(self) 80 | 81 | if type(recipients) is not list: 82 | recipients = [recipients] 83 | 84 | self.log("Status: sending snap to %s, id: %s." % (", ".join(recipients), snap.snap_id)) 85 | self.client.send(snap.media_id, recipients, snap.duration) 86 | 87 | def get_friend_stories(self, update_timestamp=0): 88 | self.client.get_friend_stories(update_timestamp) 89 | 90 | def update_privacy(self, friends_only=False): 91 | return self.client.update_privacy(friends_only) 92 | 93 | def update_story_privacy(self, friends_only=False): 94 | return self.client.update_story_privacy(friends_only) 95 | 96 | def update_birthday(self, birthday): 97 | return self.client.update_birthday(birthday) 98 | 99 | def update_email(self, email): 100 | return self.client.update_email(email) 101 | 102 | def update_number_of_best_friends(self, number): 103 | return self.client.update_number_of_best_friends(number) 104 | 105 | def post_story(self, snap): 106 | if not snap.uploaded: 107 | self.log("Status: uploading, id: %s." % snap.snap_id) 108 | snap.upload(self) 109 | 110 | self.log("Status: sending snap to story, id: %s." % snap.snap_id) 111 | try: 112 | snap.story_id = self.client.send_to_story(snap) 113 | except: 114 | pass 115 | 116 | def delete_story(self, snap): 117 | self.client.delete_story(snap.story_id) 118 | 119 | def add_friend(self, username): 120 | return self.client.add_friend(username) 121 | 122 | def delete_friend(self, username): 123 | return self.client.delete_friend(username) 124 | 125 | def block(self, username): 126 | return self.client.block(username) 127 | 128 | def unblock(self, username): 129 | return self.client.unblock(username) 130 | 131 | def logout(self): 132 | return self.client.logout() 133 | 134 | def clear_feed(self): 135 | return self.client.clear_feed() 136 | 137 | def clear_conversation(self, username): 138 | return self.client.clear_conversation(username) 139 | 140 | def save_snap(self, snap, dir): 141 | self.log("Saving snap to '%s', id: %s." % (dir, snap.snap_id)) 142 | save_snap(snap, dir) 143 | 144 | def from_file(self, dir): 145 | return Snap.from_file(dir, username=self.client.username) 146 | 147 | def register(self, username, password, birthday, email): 148 | self.log("Registering account %s, password: %s, birthday: %s, email: %s..." % (username, password, birthday, email)) 149 | return self.client.register(username, password, birthday, email) 150 | 151 | def get_snaps(self, mark_viewed=True, mark_screenshotted=False, mark_replayed=False): 152 | new_snaps = self.client.get_snaps() 153 | snaps = [] 154 | 155 | for snap in new_snaps: 156 | if snap['status'] == 2: 157 | continue 158 | 159 | data = self.client.get_blob(snap["id"]) 160 | 161 | if data is None: 162 | continue 163 | 164 | snap = self.process_snap(snap, data) 165 | 166 | if mark_viewed: 167 | self.client.mark_viewed(snap_id=snap.snap_id, sender=snap.sender, replayed=mark_replayed) 168 | if mark_screenshotted: 169 | self.client.mark_screenshot(snap_id=snap.snap_id, sender=snap.sender, replayed=mark_replayed) 170 | 171 | snaps.append(snap) 172 | 173 | return snaps -------------------------------------------------------------------------------- /SnapWrap/utils.py: -------------------------------------------------------------------------------- 1 | import tempfile, mimetypes, datetime, subprocess, re, math, os 2 | from PIL import Image 3 | from constants import MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, SNAP_IMAGE_DIMENSIONS, MEDIA_TYPE_UNKNOWN 4 | from shutil import copy 5 | 6 | def save_snap(snap, dir): 7 | dir = os.path.join(dir, "") 8 | copy(snap.file.name, dir) 9 | fn = os.path.basename(snap.file.name) 10 | os.rename(os.path.join(dir, fn), os.path.join(dir, snap.sender + "_" + snap.snap_id + "." + fn.split(".")[2])) 11 | 12 | def file_extension_for_type(media_type): 13 | if media_type is MEDIA_TYPE_IMAGE: 14 | return ".jpg" 15 | else: 16 | return ".mp4" 17 | 18 | def create_temporary_file(suffix): 19 | return tempfile.NamedTemporaryFile(suffix=suffix, delete=False) 20 | 21 | def is_video_file(path): 22 | return mimetypes.guess_type(path)[0].startswith("video") 23 | 24 | def is_image_file(path): 25 | return mimetypes.guess_type(path)[0].startswith("image") 26 | 27 | def guess_type(path): 28 | if is_video_file(path): return MEDIA_TYPE_VIDEO 29 | if is_image_file(path): return MEDIA_TYPE_IMAGE 30 | return MEDIA_TYPE_UNKNOWN 31 | 32 | def resize_image(im, output_path): 33 | im.thumbnail(SNAP_IMAGE_DIMENSIONS, Image.ANTIALIAS) 34 | im.save(output_path, quality = 100) 35 | 36 | def duration_string_to_timedelta(s): 37 | [hours, minutes, seconds] = map(int, s.split(':')) 38 | seconds = seconds + minutes * 60 + hours * 3600 39 | return datetime.timedelta(seconds=seconds) 40 | 41 | def get_video_duration(path): 42 | result = subprocess.Popen(["ffprobe", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 43 | matches = [x for x in result.stdout.readlines() if "Duration" in x] 44 | duration_string = re.findall(r'Duration: ([0-9:]*)', matches[0])[0] 45 | return math.ceil(duration_string_to_timedelta(duration_string).seconds) 46 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from SnapWrap import Snapchat 2 | 3 | class CustomBot(Snapchat): 4 | def on_snap(self, sender, snap): 5 | self.send_snap(snap, sender) 6 | self.save_snap(snap, "C:\Snapchat\Snaps") 7 | 8 | def on_friend_add(self, friend): 9 | self.add_friend(friend) 10 | self.send_snap(self.from_file("C:\Snapchat\welcome.jpg", friend)) 11 | 12 | def on_friend_delete(self, friend): 13 | self.delete_friend(friend) 14 | 15 | bot = CustomBot(*["user", "pass"]) 16 | bot.begin() 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | setup( 9 | name='SnapWrap', 10 | version='0.2', 11 | description="Wrapper for Snapchat's API!", 12 | long_description="This is an API wrapper for Snapchat's unofficial and undocumented API.", 13 | author='Robert', 14 | url=' https://github.com/Rob--/SnapWrap', 15 | packages=['SnapWrap', 'SnapWrap.Client'], 16 | install_requires=[ 17 | 'schedule>=0.3.1', 18 | 'requests>=2.5.1', 19 | 'Pillow>=2.7.0' 20 | ], 21 | license='The MIT License (MIT) - see LICENSE' 22 | ) --------------------------------------------------------------------------------