├── .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 | 
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 |
--------------------------------------------------------------------------------