├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt └── src ├── download_snaps.py ├── example.py └── snapchat.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Pycharm 39 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Val 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PROJECT IS DEPRECATED 2 | 3 | This uses APIs from many years ago for Snapchat. These APIs have since been changed/updated. This project is provided only for reference/archival purposes. 4 | 5 | 6 | Snapchat for Python 7 | =============== 8 | 9 | Implementation of the Snapchat protocol in Python. Heavily based on [php-snapchat](https://github.com/dstelljes/php-snapchat). 10 | 11 | Install 12 | ------- 13 | 14 | ``` 15 | pip install requests 16 | easy_install pycrypto 17 | git clone https://github.com/niothiel/snapchat-python.git 18 | cd snapchat-python/src 19 | python example.py 20 | ``` 21 | 22 | Example 23 | ------- 24 | To get started, download snapchat.py and in another file enter the following: 25 | 26 | ``` 27 | from snapchat import Snapchat 28 | 29 | s = Snapchat() 30 | s.login('USERNAME', 'PASSWORD') 31 | 32 | # Send a snapchat 33 | media_id = s.upload(Snapchat.MEDIA_IMAGE, 'filename.jpg') 34 | s.send(media_id, 'recipient') 35 | 36 | # Get all snaps 37 | snaps = s.get_snaps() 38 | 39 | # Download a snap 40 | s.get_media(snap['id']) 41 | 42 | # Clear snapchat history 43 | s.clear_feed() 44 | ``` 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycrypto==2.6.1 2 | requests==2.0.1 -------------------------------------------------------------------------------- /src/download_snaps.py: -------------------------------------------------------------------------------- 1 | import os 2 | from snapchat import Snapchat 3 | 4 | PATH = './snaps/' 5 | EXTENSIONS = [ 6 | 'jpeg', 7 | 'jpg', 8 | 'mp4' 9 | ] 10 | 11 | def get_downloaded(): 12 | """Gets the snapchat IDs that have already been downloaded and returns them in a set.""" 13 | 14 | result = set() 15 | 16 | for name in os.listdir(PATH): 17 | filename, ext = name.split('.') 18 | if ext not in EXTENSIONS: 19 | continue 20 | 21 | ts, username, id = filename.split('+') 22 | result.add(id) 23 | return result 24 | 25 | def download(s, snap): 26 | """Download a specific snap, given output from s.get_snaps().""" 27 | 28 | id = snap['id'] 29 | name = snap['sender'] 30 | ts = str(snap['sent']).replace(':', '-') 31 | 32 | result = s.get_media(id) 33 | 34 | if not result: 35 | return False 36 | 37 | ext = s.is_media(result) 38 | filename = '{}+{}+{}.{}'.format(ts, name, id, ext) 39 | path = PATH + filename 40 | with open(path, 'wb') as fout: 41 | fout.write(result) 42 | return True 43 | 44 | def download_snaps(s): 45 | """Download all snaps that haven't already been downloaded.""" 46 | 47 | existing = get_downloaded() 48 | 49 | snaps = s.get_snaps() 50 | for snap in snaps: 51 | id = snap['id'] 52 | if id[-1] == 's' or id in existing: 53 | print 'Skipping:', id 54 | continue 55 | 56 | result = download(s, snap) 57 | 58 | if not result: 59 | print 'FAILED:', id 60 | else: 61 | print 'Downloaded:', id 62 | 63 | if __name__ == '__main__': 64 | s = Snapchat() 65 | s.login('USERNAME', 'PASSWORD') 66 | download_snaps(s) 67 | -------------------------------------------------------------------------------- /src/example.py: -------------------------------------------------------------------------------- 1 | from snapchat import Snapchat 2 | import getpass 3 | from pprint import pprint 4 | 5 | s = Snapchat() 6 | 7 | username = raw_input('Enter your username: ') 8 | password = getpass.getpass('Enter your password: ') 9 | s.login(username, password) 10 | 11 | snaps = s.get_snaps() 12 | pprint(snaps) -------------------------------------------------------------------------------- /src/snapchat.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import hashlib 3 | import json 4 | import time 5 | 6 | from datetime import datetime 7 | from Crypto.Cipher import AES 8 | 9 | 10 | if False: 11 | import logging 12 | import httplib 13 | httplib.HTTPConnection.debuglevel = 1 14 | 15 | class Snapchat(object): 16 | 17 | URL = 'https://feelinsonice-hrd.appspot.com/bq' 18 | SECRET = 'iEk21fuwZApXlz93750dmW22pw389dPwOk' # API Secret 19 | STATIC_TOKEN = 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9' # API Static Token 20 | BLOB_ENCRYPTION_KEY = 'M02cnQ51Ji97vwT4' # Blob Encryption Key 21 | HASH_PATTERN = '0001110111101110001111010101111011010001001110011000110001000110'; # Hash pattern 22 | USERAGENT = 'Snapchat/6.0.0 (iPhone; iOS 7.0.2; gzip)' # The default useragent 23 | SNAPCHAT_VERSION = '4.0.0' # Snapchat Application Version 24 | 25 | MEDIA_IMAGE = 0 # Media: Image 26 | MEDIA_VIDEO = 1 # Media: Video 27 | MEDIA_VIDEO_NOAUDIO = 2 # Media: Video without audio 28 | MEDIA_FRIEND_REQUEST = 3 # Media: Friend Request 29 | MEDIA_FRIEND_REQUEST_IMAGE = 4 # Media: Image from unconfirmed friend 30 | MEDIA_FRIEND_REQUEST_VIDEO = 5 # Media: Video from unconfirmed friend 31 | MEDIA_FRIEND_REQUEST_VIDEO_NOAUDIO = 6 # Media: Video without audio from unconfirmed friend 32 | 33 | STATUS_NONE = -1 # Snap status: None 34 | STATUS_SENT = 0 # Snap status: Sent 35 | STATUS_DELIVERED = 1 # Snap status: Delivered 36 | STATUS_OPENED = 2 # Snap status: Opened 37 | STATUS_SCREENSHOT = 3 # Snap status: Screenshot 38 | 39 | FRIEND_CONFIRMED = 0 # Friend status: Confirmed 40 | FRIEND_UNCONFIRMED = 1 # Friend status: Unconfirmed 41 | FRIEND_BLOCKED = 2 # Friend status: Blocked 42 | FRIEND_DELETED = 3 # Friend status: Deleted 43 | 44 | PRIVACY_EVERYONE = 0 # Privacy setting: Accept snaps from everyone 45 | PRIVACY_FRIENDS = 1 # Privacy setting: Accept snaps only from friends 46 | 47 | def __init__(self, username=None, password=None): 48 | self.username = None 49 | self.auth_token = None 50 | self.logged_in = False 51 | 52 | self.cipher = AES.new(Snapchat.BLOB_ENCRYPTION_KEY, AES.MODE_ECB) 53 | 54 | if username and password: 55 | self.login(username, password) 56 | 57 | def _pad(self, data, blocksize=16): 58 | """Pads data using PKCS5.""" 59 | 60 | pad = blocksize - (len(data) % blocksize) 61 | return data + chr(pad) * pad 62 | 63 | def _hash(self, first, second): 64 | """Implementation of Snapchat's weird hashing function.""" 65 | 66 | # Append the secret key to the values. 67 | first = Snapchat.SECRET + str(first) 68 | second = str(second) + Snapchat.SECRET 69 | 70 | # Hash the values. 71 | hash1 = hashlib.sha256(first).hexdigest() 72 | hash2 = hashlib.sha256(second).hexdigest() 73 | 74 | # Create the final hash by combining the two we just made. 75 | result = '' 76 | for pos, included in enumerate(Snapchat.HASH_PATTERN): 77 | if included == '0': 78 | result += hash1[pos] 79 | else: 80 | result += hash2[pos] 81 | 82 | return result 83 | 84 | def _timestamp(self): 85 | """Generates a timestamp in microseconds.""" 86 | 87 | return int(time.time() * 1000) 88 | 89 | def _encrypt(self, data): 90 | """Encrypt the blob.""" 91 | 92 | data = self._pad(data) 93 | return self.cipher.encrypt(data) 94 | 95 | def _decrypt(self, data): 96 | """Decrypt the blob.""" 97 | 98 | data = self._pad(data) 99 | return self.cipher.decrypt(data) 100 | 101 | def _parse_field(self, dictionary, key, bool=False): 102 | """Correctly parse a field from a dictionary object. 103 | 104 | Takes care of missing keys, and empty fields. 105 | 106 | :param dictionary: The dictionary. 107 | :param key: The key for the dictionary. 108 | :param bool: Whether or not the value should be a boolean""" 109 | 110 | if key not in dictionary: 111 | if bool: 112 | return False 113 | return None 114 | 115 | value = dictionary[key] 116 | if not value: 117 | if bool: 118 | return False 119 | return None 120 | 121 | return value 122 | 123 | def _parse_datetime(self, dt): 124 | """Gracefully concert and parse a text timestamp in microseconds.""" 125 | 126 | try: 127 | return datetime.fromtimestamp(dt / 1000) 128 | except: 129 | return dt 130 | 131 | def is_media(self, data): 132 | """Check if the blob is a valid media type.""" 133 | 134 | # Check for JPG header. 135 | if data[0] == chr(0xff) and data[1] == chr(0xd8): 136 | return 'jpg' 137 | 138 | # Check for MP4 header. 139 | if data[0] == chr(0x00) and data[1] == chr(0x00): 140 | return 'mp4' 141 | 142 | return False 143 | 144 | def post(self, endpoint, data, params, file=None): 145 | """Submit a post request to the Snapchat API. 146 | 147 | :param endpoint: The service to submit the request to, i.e. '/upload'. 148 | :param data: The data to upload. 149 | :param params: Request specific authentication, typically a tuple of form (KEY, TIME). 150 | :param file: Optional field for submitting file content in multipart messages. 151 | """ 152 | 153 | data['req_token'] = self._hash(params[0], params[1]) 154 | data['version'] = Snapchat.SNAPCHAT_VERSION 155 | 156 | headers = { 157 | 'User-Agent': Snapchat.USERAGENT 158 | } 159 | 160 | url = Snapchat.URL + endpoint 161 | 162 | if file: 163 | r = requests.post(url, data, headers=headers, files={'data': file}) 164 | else: 165 | r = requests.post(url, data, headers=headers) 166 | 167 | # If the status code isn't 200, it's a failed request. 168 | if r.status_code != 200: 169 | if False: 170 | print 'Post returned code: ', r.status_code, 'for request', endpoint, data 171 | print 'Error content:' 172 | print r.content 173 | return False 174 | 175 | # If possible, try to return a json object. 176 | try: 177 | return json.loads(r.content) 178 | except: 179 | return r.content 180 | 181 | def login(self, username, password): 182 | """Login to Snapchat.""" 183 | 184 | timestamp = self._timestamp() 185 | 186 | data = { 187 | 'username': username, 188 | 'password': password, 189 | 'timestamp': timestamp 190 | } 191 | 192 | params = [ 193 | Snapchat.STATIC_TOKEN, 194 | timestamp 195 | ] 196 | 197 | result = self.post('/login', data, params) 198 | 199 | if 'auth_token' in result: 200 | self.auth_token = result['auth_token'] 201 | 202 | if 'username' in result: 203 | self.username = result['username'] 204 | 205 | if self.auth_token and self.username: 206 | self.logged_in = True 207 | 208 | return result 209 | 210 | def logout(self): 211 | """Logout of Snapchat.""" 212 | 213 | if not self.logged_in: 214 | return False 215 | 216 | timestamp = self._timestamp() 217 | 218 | data = { 219 | 'username': self.username, 220 | 'timestamp': timestamp 221 | } 222 | 223 | params = [ 224 | self.auth_token, 225 | timestamp 226 | ] 227 | 228 | result = self.post('/logout', data, params) 229 | if not result: 230 | self.logged_in = False 231 | return True 232 | 233 | return False 234 | 235 | def register(self, username, password, email, birthday): 236 | """Registers a new username for the Snapchat service. 237 | 238 | :param username: The username of the new user. 239 | :param password: The password of the new user. 240 | :param email: The email of the new user. 241 | :param birthday: The birthday of the new user (yyyy-mm-dd). 242 | """ 243 | 244 | timestamp = self._timestamp() 245 | 246 | data = { 247 | 'birthday': birthday, 248 | 'password': password, 249 | 'email': email, 250 | 'timestamp': timestamp 251 | } 252 | 253 | params = [ 254 | Snapchat.STATIC_TOKEN, 255 | timestamp 256 | ] 257 | 258 | # Perform email/password registration. 259 | result = self.post('/register', data, params) 260 | 261 | timestamp = self._timestamp() 262 | 263 | if 'token' not in result: 264 | return False 265 | 266 | data = { 267 | 'email': email, 268 | 'username': username, 269 | 'timestamp': timestamp 270 | } 271 | 272 | params = [ 273 | Snapchat.STATIC_TOKEN, 274 | timestamp 275 | ] 276 | 277 | # Perform username registration. 278 | result = self.post('/registeru', data, params) 279 | 280 | # Store the authentication token if the server sent one. 281 | if 'auth_token' in result: 282 | self.auth_token = result['auth_token'] 283 | 284 | # Store the username if the server sent it. 285 | if 'username' in result: 286 | self.username = result['username'] 287 | 288 | return result 289 | 290 | def upload(self, type, filename): 291 | """Upload a video or image to Snapchat. 292 | 293 | You must call send() after uploading the image for someone the receive it. 294 | 295 | :param type: The type of content being uploaded, i.e. Snapchat.MEDIA_VIDEO. 296 | :param filename: The filename of the content. 297 | :returns: The media_id of the file if successful. 298 | """ 299 | 300 | if not self.logged_in: 301 | return False 302 | 303 | timestamp = self._timestamp() 304 | 305 | # TODO: media_ids are GUIDs now. 306 | media_id = self.username.upper() + '~' + str(timestamp) 307 | 308 | data = { 309 | 'media_id': media_id, 310 | 'type': type, 311 | 'timestamp': timestamp, 312 | 'username': self.username 313 | } 314 | 315 | params = [ 316 | self.auth_token, 317 | timestamp 318 | ] 319 | 320 | # Read the file and encrypt it. 321 | with open(filename, 'rb') as fin: 322 | encrypted_data = self._encrypt(fin.read()) 323 | 324 | result = self.post('/upload', data, params, encrypted_data) 325 | 326 | if result: 327 | return False 328 | return media_id 329 | 330 | def send(self, media_id, recipients, time=10): 331 | """Send a Snapchat. 332 | 333 | You must have uploaded the video or image using upload() to get the media_id. 334 | 335 | :param media_id: The unique id for the media. 336 | :param recipients: A list of usernames to send the Snap to. 337 | :param time: Viewing time for the Snap (in seconds). 338 | """ 339 | 340 | if not self.logged_in: 341 | return False 342 | 343 | # If we only have one recipient, convert it to a list. 344 | if not isinstance(recipients, list): 345 | recipients = [recipients] 346 | 347 | timestamp = self._timestamp() 348 | 349 | data = { 350 | 'media_id': media_id, 351 | 'recipient': ','.join(recipients), 352 | 'time': time, 353 | 'timestamp': timestamp, 354 | 'username': self.username 355 | } 356 | 357 | params = [ 358 | self.auth_token, 359 | timestamp 360 | ] 361 | 362 | result = self.post('/send', data, params) 363 | return result != False 364 | 365 | def add_story(self, media_id, time=10): 366 | """Add a story to your stories. 367 | 368 | You must have uploaded the video or image using upload() to get the media_id. 369 | 370 | :param media_id: The unique id for the media. 371 | :param time: Viewing time for the Snap (in seconds). 372 | """ 373 | if not self.logged_in: 374 | return False 375 | 376 | timestamp = self._timestamp() 377 | print media_id 378 | data = { 379 | 'client_id': media_id, 380 | 'media_id': media_id, 381 | 'time': time, 382 | 'timestamp': timestamp, 383 | 'username': self.username, 384 | 'caption_text_display': '#YOLO', 385 | 'type': 0, 386 | } 387 | 388 | params = [ 389 | self.auth_token, 390 | timestamp 391 | ] 392 | 393 | result = self.post('/post_story', data, params) 394 | return result != False 395 | 396 | def get_updates(self): 397 | """Get all events pertaining to the user. (User, Snaps, Friends).""" 398 | 399 | if not self.logged_in: 400 | return False 401 | 402 | timestamp = self._timestamp() 403 | data = { 404 | 'timestamp': timestamp, 405 | 'username': self.username 406 | } 407 | 408 | params = [ 409 | self.auth_token, 410 | timestamp 411 | ] 412 | 413 | result = self.post('/all_updates', data, params) 414 | return result 415 | 416 | def get_snaps(self): 417 | """Get all snaps for the user.""" 418 | 419 | updates = self.get_updates() 420 | 421 | if not updates: 422 | return False 423 | 424 | snaps = updates['updates_response']['snaps'] 425 | result = [] 426 | 427 | print self._timestamp() 428 | for snap in snaps: 429 | # Make the fields more readable. 430 | snap_readable = { 431 | 'id': self._parse_field(snap, 'id'), 432 | 'media_id': self._parse_field(snap, 'c_id'), 433 | 'media_type': self._parse_field(snap, 'm'), 434 | 'time': self._parse_field(snap, 't'), 435 | 'sender': self._parse_field(snap, 'sn'), 436 | 'recipient': self._parse_field(snap, 'rp'), 437 | 'status': self._parse_field(snap, 'st'), 438 | 'screenshot_count': self._parse_field(snap, 'c'), 439 | 'sent': self._parse_datetime(snap['sts']), 440 | 'opened': self._parse_datetime(snap['ts']) 441 | } 442 | result.append(snap_readable) 443 | 444 | return result 445 | 446 | def get_stories(self): 447 | """Get all stories.""" 448 | 449 | if not self.logged_in: 450 | return False 451 | 452 | timestamp = self._timestamp() 453 | data = { 454 | 'timestamp': timestamp, 455 | 'username': self.username 456 | } 457 | 458 | params = [ 459 | self.auth_token, 460 | timestamp 461 | ] 462 | 463 | result = self.post('/stories', data, params) 464 | 465 | return result 466 | 467 | def get_media(self, id): 468 | """Download a snap. 469 | 470 | :param id: The unique id of the snap (NOT media_id). 471 | :returns: The media in a byte string. 472 | """ 473 | 474 | if not self.logged_in: 475 | return False 476 | 477 | timestamp = self._timestamp() 478 | data = { 479 | 'id': id, 480 | 'timestamp': timestamp, 481 | 'username': self.username 482 | } 483 | 484 | params = [ 485 | self.auth_token, 486 | timestamp 487 | ] 488 | 489 | result = self.post('/blob', data, params) 490 | 491 | if not result: 492 | return False 493 | 494 | if self.is_media(result): 495 | return result 496 | 497 | result = self._decrypt(result) 498 | 499 | if self.is_media(result): 500 | return result 501 | 502 | return False 503 | 504 | def find_friends(self, numbers, country='US'): 505 | """Finds friends based on phone numbers. 506 | 507 | :param numbers: A list of phone numbers. 508 | :param country: The country code (US is default). 509 | :returns: List of user objects found. 510 | """ 511 | 512 | if not self.logged_in: 513 | return False 514 | 515 | timestamp = self._timestamp() 516 | data = { 517 | 'countryCode': country, 518 | 'numbers': json.dumps(numbers), 519 | 'timestamp': timestamp, 520 | 'username': self.username 521 | } 522 | 523 | params = [ 524 | self.auth_token, 525 | timestamp 526 | ] 527 | 528 | result = self.post('/find_friends', data, params) 529 | 530 | print result 531 | 532 | if 'results' in result: 533 | return result['results'] 534 | 535 | return result 536 | 537 | def clear_feed(self): 538 | """Clear the user's feed.""" 539 | 540 | if not self.logged_in: 541 | return False 542 | 543 | timestamp = self._timestamp() 544 | data = { 545 | 'timestamp': timestamp, 546 | 'username': self.username 547 | } 548 | 549 | params = [ 550 | self.auth_token, 551 | timestamp 552 | ] 553 | 554 | result = self.post('/clear', data, params) 555 | 556 | if not result: 557 | return True 558 | 559 | return False 560 | 561 | def add_friend(self, friend): 562 | timestamp = self._timestamp() 563 | 564 | data = { 565 | 'action': 'add', 566 | 'friend': friend, 567 | 'timestamp': timestamp, 568 | 'username': self.username 569 | } 570 | 571 | params = [ 572 | self.auth_token, 573 | timestamp 574 | ] 575 | 576 | self.post('/friend', data, params) 577 | 578 | return True 579 | --------------------------------------------------------------------------------