├── .gitignore ├── LICENSE ├── README.md ├── examples ├── autowelcomebot.py ├── capturebot.py ├── connectorbot.py ├── gifbot.py ├── googlerbot.py ├── randobot.py ├── reflectorbot.py ├── reporterbot.py └── storifierbot.py ├── resources ├── Arial.ttf ├── auto_welcome.png ├── connector_fail.png ├── rando_addme.png └── rando_welcome.png ├── scripts └── gif_to_mp4.sh ├── setup.py └── snapchat_bots ├── __init__.py ├── bot.py ├── constants.py ├── exceptions.py ├── snap.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | *html 4 | build/ 5 | dist/ 6 | .DS_Store 7 | agents.yml 8 | temp_dir 9 | run_bots.py 10 | py* 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anastasis Germanidis 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

This repo is deprecated due to changes in Snapchat's unofficial API.

2 | 3 | ![SnapchatBot](http://i.imgur.com/s8XADUn.png?1) 4 | 5 | # SnapchatBot: A library for making bots that live on Snapchat 6 | 7 | Introducing SnapchatBot, an easy way to program Snapchat accounts to do anything you want. 8 | SnapchatBot can be used to create image-based notification services, chatbots, search interfaces, 9 | and any kind of intelligent agent that uses picture messages as its interaction mode. 10 | 11 | ## Bots Included 12 | 13 | #### The Reflector Bot 14 | *(source at examples/reflectorbot.py)* 15 | 16 | Sends back everything you send it. 17 | 18 | #### The Storifier Bot 19 | *(source at examples/storifierbot.py)* 20 | 21 | Takes all the snaps sent to it and adds them to its story. It can be used to collect responses 22 | from multiple people around a single theme, much like a Twitter 23 | hashtag. 24 | 25 | #### The Capture Bot (by [EthanBlackburn](https://github.com/EthanBlackburn)) 26 | *(source at examples/capturebot.py)* 27 | 28 | Saves all snaps received to the current working directory. 29 | 30 | #### The Auto-Welcomer Bot 31 | *(source at examples/autowelcomebot.py)* 32 | 33 | Sends you an auto-welcome message when you add it to your friends. 34 | 35 | #### The Reporter Bot 36 | *(source at examples/reporterbot.py)* 37 | 38 | Sends you a snap when breaking news happen. Follows the [BBC Breaking News twitter account](https://twitter.com/bbcbreaking). 39 | 40 | #### The Googler Bot 41 | *(source at examples/googlerbot.py)* 42 | 43 | When sent an image, sends back the most similar image to that picture on the web. Uses Google Image Search. 44 | 45 | #### The GIF Bot 46 | *(source at examples/gifbot.py)* 47 | 48 | Posts popular GIFs taken from the [Giphy](http://giphy.com) home page to its story. 49 | 50 | #### The Connector Bot 51 | *(source at examples/connectorbot.py)* 52 | 53 | When you add the Connector to your friends, it links you with a stranger who's also added it. Every snap sent to the Connector will then arrive at the stranger's inbox, and all snaps sent from the stranger to the Connector will come to you. It's like ChatRoulette on Snapchat. 54 | 55 | #### The Rando Bot (by [PhlexPlexico](https://github.com/Phlexplexico)) 56 | *(source at examples/randobot.py)* 57 | 58 | When you add RandoBot to your friends, it throws you in a list of people who've also added it. When you send it a snap, it will send your snap to a random person in the list. Similar to the [Rando](http://techcrunch.com/2014/03/22/rip-rando/) app. 59 | 60 | ## Installation 61 | 62 | $ python setup.py install 63 | 64 | You also need to have [ffmpeg](https://www.ffmpeg.org/), [ImageMagick](http://www.imagemagick.org/), and [libjpeg](http://libjpeg.sourceforge.net/) installed. 65 | 66 | ## How to build your own bots 67 | 68 | `SnapchatBot` currently supports the following methods: 69 | 70 | * `SnapchatBot#send_snap(recipients, snap)` -- sends snap `snap` to the list of usernames `recipients` 71 | * `SnapchatBot#add_friend(username)` -- adds user with username `username` to the bot's friends 72 | * `SnapchatBot#delete_friend(username)` -- deletes user with username `username` from the bot's friends 73 | * `SnapchatBot#block(username)` -- blocks user with username `username` 74 | * `SnapchatBot#get_snaps(mark_viewed = True)` -- gets snaps in the bot's inbox that haven't been viewed yet (use `mark_viewed = False` as a keyword argument if you don't want the bot to mark every snap received as viewed) 75 | * `SnapchatBot#get_my_stories()` -- gets all snaps in the bot's story 76 | * `SnapchatBot#get_friend_stories()` -- gets all the stories of the bot's friends 77 | * `SnapchatBot#clear_stories()` -- deletes all the bot's stories 78 | * `SnapchatBot#mark_viewed(snap)` -- marks `snap` as viewed 79 | * `SnapchatBot#get_friends()` -- gets the bot's friends 80 | * `SnapchatBot#get_added_me()` -- gets all users that have added the bot to their friends 81 | * `SnapchatBot#listen()` -- listens to events (and triggers `on_snap`, `on_friend_add`, or `on_friend_delete`, if they are defined) 82 | 83 | To create a snap to send with your bot, either use `Snap.from_file(path_to_file)` with a path 84 | to an image or a video, or create an image with PIL and then use `Snap.from_image(img)`. 85 | 86 | To define behaviors for your bot in response to various events (right now only 87 | incoming snaps, friend additions, and friend deletions are supported) subclass `SnapchatBot` 88 | and define any subset of the following methods: 89 | 90 | * `initialize` -- run when the bot is created 91 | * `on_snap` -- run when the bot bot receives a snap 92 | * `on_friend_add` -- run when a Snapchat user adds the bot 93 | * `on_friend_delete` -- run when a Snapchat user deletes the bot 94 | 95 | To begin listening to events, use the `SnapchatBot#listen` method. 96 | 97 | For example, here is the code for the `ReflectorBot`, which simply responds to a snap by sending it 98 | back to the user who sent it: 99 | 100 | ```python 101 | class ReflectorBot(SnapchatBot): 102 | # when receiving a snap, sends the same snap back to the sender 103 | def on_snap(self, sender, snap): 104 | self.send_snap([sender], snap) 105 | 106 | # when someone adds the bot, the bot adds them back 107 | def on_friend_add(self, friend): 108 | self.add_friend(self, friend) 109 | 110 | # when someone deletes the bot, the bot deletes them too 111 | def on_friend_delete(self, friend): 112 | self.delete_friend(self, friend) 113 | ``` 114 | 115 | Then to run the bot: 116 | 117 | ```python 118 | bot = ReflectorBot(, ) 119 | bot.listen() 120 | ``` 121 | -------------------------------------------------------------------------------- /examples/autowelcomebot.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from snapchat_bots import SnapchatBot, Snap 3 | 4 | 5 | class AutoWelcomerBot(SnapchatBot): 6 | def on_friend_add(self, friend): 7 | self.send_snap(friend, Snap.from_file("resources/auto_welcome.png")) 8 | 9 | def on_friend_delete(self, friend): 10 | self.delete_friend(friend) 11 | 12 | if __name__ == '__main__': 13 | parser = ArgumentParser("Auto-Welcomer Bot") 14 | parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") 15 | parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") 16 | 17 | args = parser.parse_args() 18 | 19 | bot = AutoWelcomerBot(args.username, args.password) 20 | bot.listen() -------------------------------------------------------------------------------- /examples/capturebot.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from snapchat_bots import SnapchatBot 3 | 4 | class CaptureBot(SnapchatBot): 5 | def on_snap(self, sender, snap): 6 | snap.save() 7 | 8 | if __name__ == '__main__': 9 | parser = ArgumentParser("Capture Bot") 10 | parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") 11 | parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") 12 | 13 | args = parser.parse_args() 14 | 15 | bot = CaptureBot(args.username, args.password) 16 | bot.listen(timeout=60) 17 | -------------------------------------------------------------------------------- /examples/connectorbot.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from snapchat_bots import SnapchatBot, Snap 3 | 4 | 5 | class ConnectorBot(SnapchatBot): 6 | def initialize(self): 7 | self.connections = [] 8 | self.unconnected = None 9 | 10 | def connect(self, u1, u2): 11 | self.log("New Connection: %s <-> %s" % (u1, u2)) 12 | self.connections.append((u1, u2)) 13 | 14 | def on_friend_add(self, friend): 15 | if self.unconnected is not None: 16 | self.connect(friend, self.unconnected) 17 | self.unconnected = None 18 | else: 19 | self.unconnected = friend 20 | 21 | def on_friend_delete(self, friend): 22 | other = self.find_connection(friend) 23 | 24 | if self.unconnected is not None: 25 | self.connect(other, self.unconnected) 26 | self.unconnected = None 27 | 28 | else: 29 | self.unconnected = other 30 | 31 | def find_connection(self, username): 32 | for (u1, u2) in self.connections: 33 | if u1 == username: 34 | return u2 35 | elif u2 == username: 36 | return u1 37 | 38 | def on_snap(self, sender, snap): 39 | connection = self.find_connection(sender) 40 | 41 | if connection: 42 | self.send_snap([connection], snap) 43 | else: 44 | self.send_snap([sender], Snap.from_file("resources/connector_fail.png")) 45 | 46 | if __name__ == '__main__': 47 | parser = ArgumentParser("ConnectorBot Bot") 48 | parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") 49 | parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") 50 | 51 | args = parser.parse_args() 52 | 53 | bot = ConnectorBot(args.username, args.password) 54 | bot.listen(timeout=5) 55 | -------------------------------------------------------------------------------- /examples/gifbot.py: -------------------------------------------------------------------------------- 1 | import time, tempfile, subprocess, re, schedule 2 | from argparse import ArgumentParser 3 | from snapchat_bots import SnapchatBot, Snap 4 | from lxml.html import parse 5 | 6 | 7 | def grab_trending_gif_urls(): 8 | doc = parse("http://giphy.com").getroot() 9 | els = doc.cssselect(".gif-link img")[:10] 10 | ret = [] 11 | for el in els: 12 | ret.append("http:" +re.sub(r"\/([^./])*\.gif", "/giphy.gif", el.attrib['src'])) 13 | return ret 14 | 15 | 16 | def gif_to_video(url): 17 | f = tempfile.NamedTemporaryFile(suffix=".mp4", delete = False) 18 | code = subprocess.Popen(["/bin/sh", "scripts/gif_to_mp4.sh", url, f.name]).wait() 19 | print(code) 20 | return f.name 21 | 22 | 23 | def is_valid_video(filename): 24 | return subprocess.Popen(["ffprobe", filename]).wait() == 0 25 | 26 | 27 | class GIFBot(SnapchatBot): 28 | def on_friend_add(self, friend): 29 | self.add_friend(friend) 30 | 31 | def on_friend_delete(self, friend): 32 | self.delete_friend(friend) 33 | 34 | def run(self): 35 | def post_gifs(): 36 | urls = grab_trending_gif_urls() 37 | filenames = map(gif_to_video, urls) 38 | 39 | for filename in filter(is_valid_video, filenames): 40 | if not is_valid_video(filename): continue 41 | self.post_story(Snap.from_file(filename)) 42 | 43 | post_gifs() 44 | schedule.every().day.at("10:30").do(post_gifs) 45 | 46 | while True: 47 | schedule.run_pending() 48 | time.sleep(60) 49 | 50 | if __name__ == '__main__': 51 | parser = ArgumentParser("GIF Maniac Bot") 52 | parser.add_argument('-u', '--username', required = True, type=str, help = "Username of the account to run the bot on") 53 | parser.add_argument('-p', '--password', required = True, type=str, help = "Password of the account to run the bot on") 54 | 55 | args = parser.parse_args() 56 | 57 | bot = GIFBot(args.username, args.password) 58 | bot.run() 59 | -------------------------------------------------------------------------------- /examples/googlerbot.py: -------------------------------------------------------------------------------- 1 | import time, requests, tempfile, urlparse, os, boto, uuid, re 2 | from argparse import ArgumentParser 3 | from boto.s3.key import Key 4 | from lxml.html import document_fromstring 5 | 6 | from snapchat_bots import SnapchatBot, Snap 7 | 8 | def public_url_for(key): 9 | return ('http://%s.s3.amazonaws.com/' % key.bucket.name) + key.key 10 | 11 | 12 | def get_bucket(conn, name, public = False): 13 | b = conn.get_bucket(name) 14 | if public: 15 | b.make_public() 16 | return b 17 | 18 | def upload_file(bucket, filename): 19 | k = Key(bucket) 20 | k.key = uuid.uuid4().hex + get_file_extension(filename) 21 | k.set_contents_from_filename(filename) 22 | k.make_public() 23 | return public_url_for(k) 24 | 25 | def get_file_extension(filename): 26 | return os.path.splitext(filename)[1] 27 | 28 | def get_url_extension(url): 29 | path = urlparse.urlparse(url).path 30 | return os.path.splitext(path)[1] 31 | 32 | def download_file(url): 33 | resp = requests.get(url) 34 | local_file = tempfile.NamedTemporaryFile(suffix = get_url_extension(url), delete=False) 35 | local_file.write(resp.content) 36 | return local_file.name 37 | 38 | def reverse_image_search(url): 39 | value = "Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.27 Safari/537.17" 40 | headers = {'User-Bot': value} 41 | search_url = 'https://www.google.com/searchbyimage?image_url=%s' % url 42 | resp = requests.get(search_url, headers=headers) 43 | root = document_fromstring(resp.content) 44 | href = root.cssselect(".bia")[0].attrib['href'] 45 | print(search_url) 46 | new_url = "https://www.google.com" + href 47 | resp = requests.get(new_url, headers=headers) 48 | return re.search("imgurl=([^&]*)", resp.content).group(1) 49 | 50 | 51 | class GooglerBot(SnapchatBot): 52 | def initialize(self, aws_key=None, aws_secret=None, bucket=None): 53 | self.conn = boto.connect_s3(aws_key, aws_secret) 54 | self.bucket = get_bucket(self.conn, bucket) 55 | 56 | def on_snap(self, sender, snap): 57 | remote_url = upload_file(self.bucket, snap.file.name) 58 | 59 | try: 60 | similar_url = reverse_image_search(remote_url) 61 | local_filename = download_file(similar_url) 62 | snap = Snap.from_file(local_filename) 63 | self.send_snap([sender], snap) 64 | 65 | except: 66 | pass 67 | 68 | def on_friend_add(self, friend): 69 | self.add_friend(friend) 70 | 71 | def on_friend_delete(self, friend): 72 | self.delete_friend(friend) 73 | 74 | if __name__ == '__main__': 75 | parser = ArgumentParser("Googler Bot") 76 | 77 | parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") 78 | parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") 79 | 80 | parser.add_argument('--aws-key', required=True, type=str, help="AWS Key") 81 | parser.add_argument('--aws-secret', required=True, type=str, help="AWS Secret Key") 82 | parser.add_argument('--bucket', required=True, type=str, help="S3 bucket") 83 | 84 | args = parser.parse_args() 85 | 86 | bot = GooglerBot(args.username, args.password, aws_key=args.aws_key, aws_secret=args.aws_secret, bucket=args.bucket) 87 | bot.listen(timeout=3) 88 | -------------------------------------------------------------------------------- /examples/randobot.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from snapchat_bots import SnapchatBot, Snap 3 | import random 4 | 5 | class RandoBot(SnapchatBot): 6 | def initialize(self): 7 | self.connections = self.get_friends() 8 | #If your bot ever gets blocked, uncomment these lines. 9 | #Of course, make sure you have your old users backed up 10 | #to the users.txt file! So you must uncomment the first 11 | #three lines, while logged into the blocked bot, then 12 | #uncomment the rest to re-add all users from the old bot. 13 | #with open('users.txt', 'w') as file: 14 | # for item in self.connections: 15 | # print>>file, item 16 | #f = open('users.txt', 'r') 17 | #for line in f: 18 | # self.add_friend(line) 19 | # print(line) 20 | print(self.connections) 21 | 22 | def connect(self,user): 23 | self.log("Added user: %s to the array!" % (user)) 24 | self.connections.append(user) 25 | 26 | def on_friend_add(self,friend): 27 | self.add_friend(friend) 28 | self.connect(friend) 29 | 30 | def on_friend_delete(self,friend): 31 | self.delete_friend(friend) 32 | self.connections.remove(friend) 33 | 34 | def find_random_user(self,username): 35 | if len(self.connections) <= 1: 36 | return None 37 | newuser = random.choice(self.connections) 38 | while(newuser == username): 39 | newuser = random.choice(self.connections) 40 | return newuser 41 | 42 | def on_snap(self,sender,snap): 43 | connection = self.find_random_user(sender) 44 | if sender not in self.connections: 45 | self.send_snap([sender], Snap.from_file("../resources/rando_addme.png")) 46 | if connection: 47 | self.send_snap([connection],snap) 48 | print("%s sent snap to %s" % (sender,[connection])) 49 | else: 50 | self.send_snap([sender], Snap.from_file("../resources/rando_welcome.png")) 51 | 52 | if __name__ == '__main__': 53 | parser = ArgumentParser("RandoBot Bot") 54 | parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") 55 | parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") 56 | 57 | args = parser.parse_args() 58 | 59 | bot = RandoBot(args.username, args.password) 60 | bot.listen(timeout=33) 61 | -------------------------------------------------------------------------------- /examples/reflectorbot.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from snapchat_bots import SnapchatBot 3 | 4 | 5 | class ReflectorBot(SnapchatBot): 6 | def on_snap(self, sender, snap): 7 | self.send_snap([sender], snap) 8 | 9 | def on_friend_add(self, friend): 10 | self.add_friend(friend) 11 | 12 | def on_friend_delete(self, friend): 13 | self.delete_friend(friend) 14 | 15 | if __name__ == '__main__': 16 | parser = ArgumentParser("Reflector Bot") 17 | parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") 18 | parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") 19 | 20 | args = parser.parse_args() 21 | 22 | bot = ReflectorBot(args.username, args.password) 23 | bot.listen(timeout=5) 24 | -------------------------------------------------------------------------------- /examples/reporterbot.py: -------------------------------------------------------------------------------- 1 | import time, urllib2, textwrap, re, HTMLParser, tempfile 2 | from PIL import Image, ImageDraw, ImageFont 3 | from argparse import ArgumentParser 4 | from snapchat_bots import SnapchatBot, Snap 5 | from snapchat_bots.utils import resize_image 6 | 7 | h = HTMLParser.HTMLParser() 8 | 9 | 10 | def get_article_info(url): 11 | content = urllib2.urlopen(url).read() 12 | description = re.search("&1 /dev/null | grep "Video:"); 8 | 9 | # get GIF Frames per second 10 | fps=$(echo "$video" | sed -n "s/.* \([0-9.]*\) fps.*/\1/p"); 11 | 12 | # a convinience variable so we can easily set FPS on the video 13 | fps1000=$(echo "scale=0;$fps*100000/100" | bc); 14 | 15 | mkdir temp_dir; 16 | 17 | # extract the frames in sequental PNGs 18 | convert -coalesce "$1" temp_dir/temp%d.png; 19 | 20 | # sequence the PNGs into an MP4 container (libx264 encoded by default) 21 | ffmpeg -y -f image2 -r "$fps1000/1000" -i temp_dir/temp%d.png -v:b 1500 -an -vf scale="290:600" "$2" 22 | 23 | # remove the temporary PNGs 24 | rm -rf temp_dir; 25 | 26 | # notes: 27 | # fps detection is not thoroughly tested, thus not reliable; one could read the GIF and calculate each frame's delay (would allow variable frame rate GIF detection as well) 28 | # "convert -coalesce" makes disposable GIFs to export as full frames 29 | # "ffmpeg ... -vf scale=..." ensures MP4 output dimensions is divisable by 2 (a rather unoptimized calculation) 30 | -------------------------------------------------------------------------------- /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='snapchat_bots', 10 | version='0.1', 11 | description='Library for building Snapchat bots', 12 | long_description=open('README.md').read(), 13 | author='Anastasis Germanidis', 14 | author_email='agermanidis@gmail.com', 15 | url='https://github.com/agermanidis/SnapchatBot', 16 | packages=['snapchat_bots'], 17 | install_requires=[ 18 | 'schedule>=0.3.1', 19 | 'requests>=2.5.3', 20 | 'Pillow>=2.7.0', 21 | 'pysnap>=0.1.1' 22 | ], 23 | dependency_links = ['https://github.com/martinp/pysnap/tarball/master#egg=pysnap-0.1.1'], 24 | license=open('LICENSE').read() 25 | ) 26 | -------------------------------------------------------------------------------- /snapchat_bots/__init__.py: -------------------------------------------------------------------------------- 1 | from snap import Snap 2 | from bot import SnapchatBot 3 | -------------------------------------------------------------------------------- /snapchat_bots/bot.py: -------------------------------------------------------------------------------- 1 | import logging, time, uuid, requests, base64 2 | from pysnap import Snapchat 3 | from pysnap.utils import make_request_token, timestamp 4 | from snap import Snap 5 | from constants import DEFAULT_TIMEOUT, STATIC_TOKEN, BASE_URL 6 | 7 | FORMAT = '[%(asctime)-15s] %(message)s' 8 | logging.basicConfig(format=FORMAT) 9 | logger = logging.getLogger() 10 | logger.level = logging.DEBUG 11 | 12 | class SnapchatBot(object): 13 | def __init__(self, username, password, **kwargs): 14 | self.bot_id = uuid.uuid4().hex[0:4] 15 | 16 | self.auth_token = STATIC_TOKEN 17 | 18 | self.username = username 19 | self.password = password 20 | 21 | r = self._make_request("/loq/login", { 22 | 'username': self.username, 23 | 'password': self.password 24 | }) 25 | 26 | result = r.json() 27 | self.auth_token = result['updates_response']['auth_token'] 28 | 29 | self.client = Snapchat() 30 | self.client.username = username 31 | self.client.auth_token = self.auth_token 32 | 33 | self.current_friends = self.get_friends() 34 | self.added_me = self.get_added_me() 35 | 36 | if hasattr(self, "initialize"): 37 | self.initialize(**kwargs) 38 | 39 | def log(self, message, level=logging.DEBUG): 40 | logger.log(level, "[%s-%s] %s" % (self.__class__.__name__, self.bot_id, message)) 41 | 42 | @staticmethod 43 | def process_snap(snap_obj, data, is_story = False): 44 | media_type = snap_obj["media_type"] 45 | sender = snap_obj["sender"] 46 | snap_id = snap_obj['id'] 47 | duration = snap_obj['time'] 48 | snap = Snap(data=data, 49 | snap_id=snap_id, 50 | media_type=media_type, 51 | duration=duration, 52 | sender=sender, 53 | is_story=is_story) 54 | return snap 55 | 56 | def mark_viewed(self, snap): 57 | self.client.mark_viewed(snap.snap_id) 58 | 59 | def listen(self, timeout=DEFAULT_TIMEOUT): 60 | while True: 61 | self.log("Querying for new snaps...") 62 | snaps = self.get_snaps() 63 | 64 | if hasattr(self, "on_snap"): 65 | for snap in snaps: 66 | self.on_snap(snap.sender, snap) 67 | 68 | added_me = self.get_added_me() 69 | 70 | newly_added = set(added_me).difference(self.added_me) 71 | newly_deleted = set(self.added_me).difference(added_me) 72 | 73 | self.added_me = added_me 74 | 75 | if hasattr(self, "on_friend_add"): 76 | for friend in newly_added: 77 | self.log("User %s added me" % friend) 78 | self.on_friend_add(friend) 79 | 80 | if hasattr(self, "on_friend_delete"): 81 | for friend in newly_deleted: 82 | self.log("User %s deleted me" % friend) 83 | self.on_friend_delete(friend) 84 | 85 | time.sleep(timeout) 86 | 87 | def get_friends(self): 88 | return map(lambda fr: fr['name'], self.client.get_friends()) 89 | 90 | def get_added_me(self): 91 | updates = self.client.get_updates() 92 | return map(lambda fr: fr['name'], updates["added_friends"]) 93 | 94 | def send_snap(self, recipients, snap): 95 | media_id = self._upload_snap(snap) 96 | 97 | if type(recipients) is not list: 98 | recipients = [recipients] 99 | 100 | recipients_str = ','.join(recipients) 101 | 102 | self.log("Sending snap %s to %s" % (snap.snap_id, recipients_str)) 103 | 104 | self.client.send(media_id, recipients_str, snap.duration) 105 | 106 | def post_story(self, snap): 107 | media_id = self._upload_snap(snap) 108 | response = self.client.send_to_story(media_id, snap.duration, snap.media_type) 109 | 110 | try: 111 | snap.story_id = response['json']['story']['id'] 112 | except: 113 | pass 114 | 115 | def delete_story(self, snap): 116 | print snap.story_id 117 | if snap.story_id is None: 118 | return 119 | 120 | self.client._request('delete_story', { 121 | 'username': self.username, 122 | 'story_id': snap.story_id 123 | }) 124 | 125 | def add_friend(self, username): 126 | self.client.add_friend(username) 127 | 128 | def delete_friend(self, username): 129 | self.client.delete_friend(username) 130 | 131 | def block(self, username): 132 | self.client.block(username) 133 | 134 | def process_snaps(self, snaps, mark_viewed = True): 135 | ret = [] 136 | 137 | for snap_obj in snaps: 138 | if snap_obj['status'] == 2: 139 | continue 140 | 141 | data = self.client.get_blob(snap_obj["id"]) 142 | 143 | if data is None: 144 | continue 145 | 146 | snap = self.process_snap(snap_obj, data) 147 | 148 | if mark_viewed: 149 | self.mark_viewed(snap) 150 | 151 | ret.append(snap) 152 | 153 | return ret 154 | 155 | def process_stories(self, stories): 156 | ret = [] 157 | for snap_obj in stories: 158 | media_key = base64.b64decode(snap_obj['media_key']) 159 | media_iv = base64.b64decode(snap_obj['media_iv']) 160 | data = self.client.get_story_blob(snap_obj['media_id'], 161 | media_key, 162 | media_iv) 163 | if data is None: 164 | continue 165 | snap_obj['sender'] = self.username 166 | snap = self.process_snap(snap_obj, data, is_story = True) 167 | ret.append(snap) 168 | return ret 169 | 170 | def get_snaps(self, mark_viewed=True): 171 | snaps = self.client.get_snaps() 172 | return self.process_snaps(snaps) 173 | 174 | def get_my_stories(self): 175 | response = self.client._request('stories', { 176 | 'username': self.username 177 | }) 178 | stories = map(lambda s: s['story'], response.json()['my_stories']) 179 | return self.process_stories(stories) 180 | 181 | def get_friend_stories(self): 182 | response = self.client._request('stories', { 183 | 'username': self.username 184 | }) 185 | ret = [] 186 | stories_per_friend = map(lambda s: s['stories'], response.json()['friend_stories']) 187 | for stories_obj in stories_per_friend: 188 | stories = map(lambda so: so['story'], stories_obj) 189 | ret.extend(self.process_stories(stories)) 190 | return ret 191 | 192 | def clear_stories(self): 193 | for story in self.get_my_stories(): 194 | self.delete_story(story) 195 | 196 | def _upload_snap(self, snap): 197 | if not snap.uploaded: 198 | snap.media_id = self.client.upload(snap.file.name) 199 | snap.uploaded = True 200 | 201 | return snap.media_id 202 | 203 | def _make_request(self, path, data = None, method = 'POST', files = None): 204 | if data is None: 205 | data = {} 206 | 207 | headers = { 208 | 'User-Agent': 'Snapchat/8.1.1 (iPhone5,1; iOS 8.1.3; gzip)', 209 | 'Accept-Language': 'en-US;q=1, en;q=0.9', 210 | 'Accept-Locale': 'en' 211 | } 212 | 213 | now = timestamp() 214 | 215 | if method == 'POST': 216 | data['timestamp'] = now 217 | data['req_token'] = make_request_token(self.auth_token, str(now)) 218 | resp = requests.post(BASE_URL + path, data = data, files = files, headers = headers) 219 | else: 220 | resp = requests.get(BASE_URL + path, params = data, headers = headers) 221 | 222 | return resp 223 | -------------------------------------------------------------------------------- /snapchat_bots/constants.py: -------------------------------------------------------------------------------- 1 | BASE_URL = 'https://feelinsonice-hrd.appspot.com' 2 | STATIC_TOKEN = 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9' 3 | 4 | DEFAULT_TIMEOUT = 15 5 | DEFAULT_DURATION = 5 6 | 7 | SNAP_IMAGE_DIMENSIONS = (1334, 750) 8 | 9 | MEDIA_TYPE_UNKNOWN = -1 10 | MEDIA_TYPE_IMAGE = 0 11 | MEDIA_TYPE_VIDEO = 1 12 | MEDIA_TYPE_VIDEO_WITHOUT_AUDIO = 2 13 | -------------------------------------------------------------------------------- /snapchat_bots/exceptions.py: -------------------------------------------------------------------------------- 1 | class UnknownMediaType(Exception): 2 | pass 3 | 4 | class CannotOpenFile(Exception): 5 | pass 6 | -------------------------------------------------------------------------------- /snapchat_bots/snap.py: -------------------------------------------------------------------------------- 1 | import subprocess, uuid, os 2 | from PIL import Image 3 | from StringIO import StringIO 4 | 5 | from utils import guess_type, create_temporary_file, get_video_duration, resize_image, file_extension_for_type, default_filename_for_snap, cmd_exists, extract_zip_components 6 | from constants import MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO_WITHOUT_AUDIO, DEFAULT_DURATION, SNAP_IMAGE_DIMENSIONS 7 | from exceptions import UnknownMediaType, CannotOpenFile 8 | 9 | class Snap(object): 10 | @staticmethod 11 | def from_file(path, duration = None): 12 | media_type = guess_type(path) 13 | 14 | if media_type is MEDIA_TYPE_VIDEO or media_type is MEDIA_TYPE_VIDEO_WITHOUT_AUDIO: 15 | if duration is None: duration = get_video_duration(path) 16 | tmp = create_temporary_file(".snap.mp4") 17 | output_path = tmp.name 18 | subprocess.Popen(["ffmpeg", "-y", "-i", path, output_path]).wait() 19 | 20 | elif media_type is MEDIA_TYPE_IMAGE: 21 | image = Image.open(path) 22 | tmp = create_temporary_file(".jpg") 23 | output_path = tmp.name 24 | resize_image(image, output_path) 25 | if not duration: 26 | duration = DEFAULT_DURATION 27 | 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) 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 is_image(self): 40 | return media_type is MEDIA_TYPE_IMAGE 41 | 42 | def is_video(self): 43 | return media_type is MEDIA_TYPE_VIDEO or media_type is MEDIA_TYPE_VIDEO_WITHOUT_AUDIO 44 | 45 | def open(self): 46 | if not cmd_exists("open"): 47 | raise CannotOpenFile("Cannot open file") 48 | 49 | subprocess.Popen(["open", self.file.name]) 50 | 51 | def save(self, output_filename = None, dir_name = "."): 52 | if output_filename is None: 53 | output_filename = default_filename_for_snap(self) 54 | 55 | if not os.path.exists(dir_name): 56 | os.makedirs(dir_name) 57 | 58 | with open(os.path.join(dir_name, output_filename), 'wb') as f: 59 | data = self.file.file.read(8192) 60 | while data: 61 | f.write(data) 62 | data = self.file.file.read(8192) 63 | 64 | def __init__(self, **opts): 65 | self.uploaded = False 66 | self.duration = opts['duration'] 67 | self.media_type = opts['media_type'] 68 | 69 | if opts.get("is_story", False): 70 | self.story_id = opts['snap_id'] 71 | 72 | if 'sender' in opts: 73 | self.sender = opts['sender'] 74 | self.snap_id = opts['snap_id'] 75 | self.from_me = False 76 | 77 | else: 78 | self.snap_id = uuid.uuid4().hex 79 | self.from_me = True 80 | 81 | if 'data' in opts: 82 | data = opts['data'] 83 | 84 | if data[0:2] == 'PK': 85 | video_filename, _ = extract_zip_components(data) 86 | self.file = open(video_filename, 'rb+') 87 | 88 | else: 89 | suffix = file_extension_for_type(opts['media_type']) 90 | self.file = create_temporary_file(suffix) 91 | 92 | if self.media_type is MEDIA_TYPE_VIDEO or self.media_type is MEDIA_TYPE_VIDEO_WITHOUT_AUDIO: 93 | self.file.write(data) 94 | self.file.flush() 95 | 96 | else: 97 | image = Image.open(StringIO(data)) 98 | resize_image(image, self.file.name) 99 | 100 | else: 101 | path = opts['path'] 102 | self.file = open(path) 103 | -------------------------------------------------------------------------------- /snapchat_bots/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, MEDIA_TYPE_VIDEO_WITHOUT_AUDIO, SNAP_IMAGE_DIMENSIONS 4 | from zipfile import ZipFile 5 | 6 | def cmd_exists(cmd): 7 | return subprocess.call("type " + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 8 | 9 | def file_extension_for_type(media_type): 10 | if media_type is MEDIA_TYPE_IMAGE: 11 | return ".jpg" 12 | else: 13 | return ".mp4" 14 | 15 | def create_temporary_file(suffix): 16 | return tempfile.NamedTemporaryFile(suffix=suffix, delete=False) 17 | 18 | def default_filename_for_snap(snap): 19 | now = datetime.datetime.now() 20 | filename = '%s-%s-%s_%s-%s-%s_%s%s' % (now.year, now.month, now.day, now.hour, now.minute, now.second, snap.sender, snap.file.name[-4:]) 21 | return filename 22 | 23 | def is_video_file(path): 24 | return mimetypes.guess_type(path)[0].startswith("video") 25 | 26 | def is_image_file(path): 27 | return mimetypes.guess_type(path)[0].startswith("image") 28 | 29 | def guess_type(path): 30 | if is_video_file(path): return MEDIA_TYPE_VIDEO 31 | if is_image_file(path): return MEDIA_TYPE_IMAGE 32 | return MEDIA_TYPE_UNKNOWN 33 | 34 | def resize_image(im, output_path): 35 | im.thumbnail(SNAP_IMAGE_DIMENSIONS, Image.ANTIALIAS) 36 | im.save(output_path, quality = 100) 37 | 38 | def extract_zip_components(data): 39 | tmp = create_temporary_file(".zip") 40 | tmp.write(data) 41 | tmp.flush() 42 | zipped_snap = ZipFile(tmp.name) 43 | unzip_dir = os.path.join(tmp.name.split(".")[0]) 44 | os.mkdir(unzip_dir) 45 | zipped_snap.extractall(unzip_dir) 46 | filenames = os.listdir(unzip_dir) 47 | for filename in filenames: 48 | if filename.startswith("media"): 49 | old_video_path = os.path.join(unzip_dir, filename) 50 | new_video_path = os.path.join(unzip_dir, "video.mp4") 51 | os.rename(old_video_path, new_video_path) 52 | 53 | elif filename.startswith("overlay"): 54 | overlay_component = os.path.join(unzip_dir, filename) 55 | 56 | return new_video_path, overlay_component 57 | 58 | def duration_string_to_timedelta(s): 59 | [hours, minutes, seconds] = map(int, s.split(':')) 60 | seconds = seconds + minutes * 60 + hours * 3600 61 | return datetime.timedelta(seconds=seconds) 62 | 63 | def get_video_duration(path): 64 | result = subprocess.Popen(["ffprobe", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 65 | matches = [x for x in result.stdout.readlines() if "Duration" in x] 66 | duration_string = re.findall(r'Duration: ([0-9:]*)', matches[0])[0] 67 | return math.ceil(duration_string_to_timedelta(duration_string).seconds) 68 | 69 | --------------------------------------------------------------------------------