diff --git a/README.md b/README.md index 831ca2d..90192ed 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ -# SnapchatBot: Python library for building bots that live on Snapchat +

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

+ +![SnapchatBot](http://i.imgur.com/s8XADUn.png?1) + +# SnapchatBot: A library for making bots that live on Snapchat Introducing SnapchatBot, an easy way to program Snapchat accounts to do anything you want. -SnapchatBot can be used to easily create image-based notification services, chatbots, search interfaces, +SnapchatBot can be used to create image-based notification services, chatbots, search interfaces, and any kind of intelligent agent that uses picture messages as its interaction mode. ## Bots Included @@ -15,7 +19,13 @@ Sends back everything you send it. *(source at examples/storifierbot.py)* Takes all the snaps sent to it and adds them to its story. It can be used to collect responses -from multiple people around a single theme, much like a Twitter hashtag. +from multiple people around a single theme, much like a Twitter +hashtag. + +#### The Capture Bot (by [EthanBlackburn](https://github.com/EthanBlackburn)) +*(source at examples/capturebot.py)* + +Saves all snaps received to the current working directory. #### The Auto-Welcomer Bot *(source at examples/autowelcomebot.py)* @@ -42,11 +52,16 @@ Posts popular GIFs taken from the [Giphy](http://giphy.com) home page to its sto 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. +#### The Rando Bot (by [PhlexPlexico](https://github.com/Phlexplexico)) +*(source at examples/randobot.py)* + +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. + ## Installation $ python setup.py install -You also need to have [ffmpeg](https://www.ffmpeg.org/) and [ImageMagick](http://www.imagemagick.org/) installed. +You also need to have [ffmpeg](https://www.ffmpeg.org/), [ImageMagick](http://www.imagemagick.org/), and [libjpeg](http://libjpeg.sourceforge.net/) installed. ## How to build your own bots @@ -57,6 +72,9 @@ You also need to have [ffmpeg](https://www.ffmpeg.org/) and [ImageMagick](http:/ * `SnapchatBot#delete_friend(username)` -- deletes user with username `username` from the bot's friends * `SnapchatBot#block(username)` -- blocks user with username `username` * `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) +* `SnapchatBot#get_my_stories()` -- gets all snaps in the bot's story +* `SnapchatBot#get_friend_stories()` -- gets all the stories of the bot's friends +* `SnapchatBot#clear_stories()` -- deletes all the bot's stories * `SnapchatBot#mark_viewed(snap)` -- marks `snap` as viewed * `SnapchatBot#get_friends()` -- gets the bot's friends * `SnapchatBot#get_added_me()` -- gets all users that have added the bot to their friends diff --git a/examples/capturebot.py b/examples/capturebot.py new file mode 100644 index 0000000..69d2c43 --- /dev/null +++ b/examples/capturebot.py @@ -0,0 +1,16 @@ +from argparse import ArgumentParser +from snapchat_bots import SnapchatBot + +class CaptureBot(SnapchatBot): + def on_snap(self, sender, snap): + snap.save() + +if __name__ == '__main__': + parser = ArgumentParser("Capture Bot") + parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") + parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") + + args = parser.parse_args() + + bot = CaptureBot(args.username, args.password) + bot.listen(timeout=60) diff --git a/examples/randobot.py b/examples/randobot.py new file mode 100644 index 0000000..b5a8e2c --- /dev/null +++ b/examples/randobot.py @@ -0,0 +1,60 @@ +from argparse import ArgumentParser +from snapchat_bots import SnapchatBot, Snap +import random + +class RandoBot(SnapchatBot): + def initialize(self): + self.connections = self.get_friends() + #If your bot ever gets blocked, uncomment these lines. + #Of course, make sure you have your old users backed up + #to the users.txt file! So you must uncomment the first + #three lines, while logged into the blocked bot, then + #uncomment the rest to re-add all users from the old bot. + #with open('users.txt', 'w') as file: + # for item in self.connections: + # print>>file, item + #f = open('users.txt', 'r') + #for line in f: + # self.add_friend(line) + # print(line) + print(self.connections) + + def connect(self,user): + self.log("Added user: %s to the array!" % (user)) + self.connections.append(user) + + def on_friend_add(self,friend): + self.add_friend(friend) + self.connect(friend) + + def on_friend_delete(self,friend): + self.delete_friend(friend) + self.connections.remove(friend) + + def find_random_user(self,username): + if len(self.connections) <= 1: + return None + newuser = random.choice(self.connections) + while(newuser == username): + newuser = random.choice(self.connections) + return newuser + + def on_snap(self,sender,snap): + connection = self.find_random_user(sender) + if sender not in self.connections: + self.send_snap([sender], Snap.from_file("../resources/rando_addme.png")) + if connection: + self.send_snap([connection],snap) + print("%s sent snap to %s" % (sender,[connection])) + else: + self.send_snap([sender], Snap.from_file("../resources/rando_welcome.png")) + +if __name__ == '__main__': + parser = ArgumentParser("RandoBot Bot") + parser.add_argument('-u', '--username', required=True, type=str, help="Username of the account to run the bot on") + parser.add_argument('-p', '--password', required=True, type=str, help="Password of the account to run the bot on") + + args = parser.parse_args() + + bot = RandoBot(args.username, args.password) + bot.listen(timeout=33) diff --git a/resources/rando_addme.png b/resources/rando_addme.png new file mode 100644 index 0000000..7128bb8 Binary files /dev/null and b/resources/rando_addme.png differ diff --git a/resources/rando_welcome.png b/resources/rando_welcome.png new file mode 100644 index 0000000..ae176d2 Binary files /dev/null and b/resources/rando_welcome.png differ diff --git a/setup.py b/setup.py index 3abc406..06780bb 100644 --- a/setup.py +++ b/setup.py @@ -16,8 +16,8 @@ packages=['snapchat_bots'], install_requires=[ 'schedule>=0.3.1', - 'requests>=2.5.1', - 'PIL>=1.1.7', + 'requests>=2.5.3', + 'Pillow>=2.7.0', 'pysnap>=0.1.1' ], dependency_links = ['https://github.com/martinp/pysnap/tarball/master#egg=pysnap-0.1.1'], diff --git a/snapchat_bots/__init__.py b/snapchat_bots/__init__.py old mode 100644 new mode 100755 diff --git a/snapchat_bots/bot.py b/snapchat_bots/bot.py old mode 100644 new mode 100755 index fe16e34..3fdb1f7 --- a/snapchat_bots/bot.py +++ b/snapchat_bots/bot.py @@ -1,7 +1,8 @@ -import logging, time, uuid +import logging, time, uuid, requests, base64 from pysnap import Snapchat +from pysnap.utils import make_request_token, timestamp from snap import Snap -from constants import DEFAULT_TIMEOUT +from constants import DEFAULT_TIMEOUT, STATIC_TOKEN, BASE_URL FORMAT = '[%(asctime)-15s] %(message)s' logging.basicConfig(format=FORMAT) @@ -12,11 +13,22 @@ class SnapchatBot(object): def __init__(self, username, password, **kwargs): self.bot_id = uuid.uuid4().hex[0:4] + self.auth_token = STATIC_TOKEN + self.username = username self.password = password + r = self._make_request("/loq/login", { + 'username': self.username, + 'password': self.password + }) + + result = r.json() + self.auth_token = result['updates_response']['auth_token'] + self.client = Snapchat() - self.client.login(username, password) + self.client.username = username + self.client.auth_token = self.auth_token self.current_friends = self.get_friends() self.added_me = self.get_added_me() @@ -28,7 +40,7 @@ def log(self, message, level=logging.DEBUG): logger.log(level, "[%s-%s] %s" % (self.__class__.__name__, self.bot_id, message)) @staticmethod - def process_snap(snap_obj, data): + def process_snap(snap_obj, data, is_story = False): media_type = snap_obj["media_type"] sender = snap_obj["sender"] snap_id = snap_obj['id'] @@ -37,7 +49,8 @@ def process_snap(snap_obj, data): snap_id=snap_id, media_type=media_type, duration=duration, - sender=sender) + sender=sender, + is_story=is_story) return snap def mark_viewed(self, snap): @@ -79,11 +92,7 @@ def get_added_me(self): return map(lambda fr: fr['name'], updates["added_friends"]) def send_snap(self, recipients, snap): - self.log("Preparing to send snap %s" % snap.snap_id) - - if not snap.uploaded: - self.log("Uploading snap %s" % snap.snap_id) - snap.upload(self) + media_id = self._upload_snap(snap) if type(recipients) is not list: recipients = [recipients] @@ -92,15 +101,26 @@ def send_snap(self, recipients, snap): self.log("Sending snap %s to %s" % (snap.snap_id, recipients_str)) - self.client.send(snap.media_id, recipients_str) + self.client.send(media_id, recipients_str, snap.duration) def post_story(self, snap): - if not snap.uploaded: - self.log("Uploading snap") - snap.upload(self) + media_id = self._upload_snap(snap) + response = self.client.send_to_story(media_id, snap.duration, snap.media_type) + + try: + snap.story_id = response['json']['story']['id'] + except: + pass + + def delete_story(self, snap): + print snap.story_id + if snap.story_id is None: + return - self.log("Posting snap as story") - self.client.send_to_story(snap.media_id, media_type=snap.media_type) + self.client._request('delete_story', { + 'username': self.username, + 'story_id': snap.story_id + }) def add_friend(self, username): self.client.add_friend(username) @@ -111,8 +131,7 @@ def delete_friend(self, username): def block(self, username): self.client.block(username) - def get_snaps(self, mark_viewed=True): - snaps = self.client.get_snaps() + def process_snaps(self, snaps, mark_viewed = True): ret = [] for snap_obj in snaps: @@ -132,3 +151,72 @@ def get_snaps(self, mark_viewed=True): ret.append(snap) return ret + + def process_stories(self, stories): + ret = [] + for snap_obj in stories: + media_key = base64.b64decode(snap_obj['media_key']) + media_iv = base64.b64decode(snap_obj['media_iv']) + data = self.client.get_story_blob(snap_obj['media_id'], + media_key, + media_iv) + if data is None: + continue + snap_obj['sender'] = self.username + snap = self.process_snap(snap_obj, data, is_story = True) + ret.append(snap) + return ret + + def get_snaps(self, mark_viewed=True): + snaps = self.client.get_snaps() + return self.process_snaps(snaps) + + def get_my_stories(self): + response = self.client._request('stories', { + 'username': self.username + }) + stories = map(lambda s: s['story'], response.json()['my_stories']) + return self.process_stories(stories) + + def get_friend_stories(self): + response = self.client._request('stories', { + 'username': self.username + }) + ret = [] + stories_per_friend = map(lambda s: s['stories'], response.json()['friend_stories']) + for stories_obj in stories_per_friend: + stories = map(lambda so: so['story'], stories_obj) + ret.extend(self.process_stories(stories)) + return ret + + def clear_stories(self): + for story in self.get_my_stories(): + self.delete_story(story) + + def _upload_snap(self, snap): + if not snap.uploaded: + snap.media_id = self.client.upload(snap.file.name) + snap.uploaded = True + + return snap.media_id + + def _make_request(self, path, data = None, method = 'POST', files = None): + if data is None: + data = {} + + headers = { + 'User-Agent': 'Snapchat/8.1.1 (iPhone5,1; iOS 8.1.3; gzip)', + 'Accept-Language': 'en-US;q=1, en;q=0.9', + 'Accept-Locale': 'en' + } + + now = timestamp() + + if method == 'POST': + data['timestamp'] = now + data['req_token'] = make_request_token(self.auth_token, str(now)) + resp = requests.post(BASE_URL + path, data = data, files = files, headers = headers) + else: + resp = requests.get(BASE_URL + path, params = data, headers = headers) + + return resp diff --git a/snapchat_bots/constants.py b/snapchat_bots/constants.py old mode 100644 new mode 100755 index 75d48fa..7727dba --- a/snapchat_bots/constants.py +++ b/snapchat_bots/constants.py @@ -1,7 +1,10 @@ +BASE_URL = 'https://feelinsonice-hrd.appspot.com' +STATIC_TOKEN = 'm198sOkJEn37DjqZ32lpRu76xmw288xSQ9' + DEFAULT_TIMEOUT = 15 DEFAULT_DURATION = 5 -SNAP_IMAGE_DIMENSIONS = (290, 600) +SNAP_IMAGE_DIMENSIONS = (1334, 750) MEDIA_TYPE_UNKNOWN = -1 MEDIA_TYPE_IMAGE = 0 diff --git a/snapchat_bots/exceptions.py b/snapchat_bots/exceptions.py old mode 100644 new mode 100755 index f5acc7f..5f38aae --- a/snapchat_bots/exceptions.py +++ b/snapchat_bots/exceptions.py @@ -1,2 +1,5 @@ class UnknownMediaType(Exception): pass + +class CannotOpenFile(Exception): + pass diff --git a/snapchat_bots/snap.py b/snapchat_bots/snap.py old mode 100644 new mode 100755 index 651bb5f..03f58b3 --- a/snapchat_bots/snap.py +++ b/snapchat_bots/snap.py @@ -1,17 +1,17 @@ -import subprocess, uuid +import subprocess, uuid, os from PIL import Image from StringIO import StringIO -from utils import guess_type, create_temporary_file, get_video_duration, resize_image, file_extension_for_type -from constants import MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, DEFAULT_DURATION, SNAP_IMAGE_DIMENSIONS -from exceptions import UnknownMediaType +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 +from constants import MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO_WITHOUT_AUDIO, DEFAULT_DURATION, SNAP_IMAGE_DIMENSIONS +from exceptions import UnknownMediaType, CannotOpenFile class Snap(object): @staticmethod def from_file(path, duration = None): media_type = guess_type(path) - if media_type is MEDIA_TYPE_VIDEO: + if media_type is MEDIA_TYPE_VIDEO or media_type is MEDIA_TYPE_VIDEO_WITHOUT_AUDIO: if duration is None: duration = get_video_duration(path) tmp = create_temporary_file(".snap.mp4") output_path = tmp.name @@ -36,15 +36,39 @@ def from_image(img, duration=DEFAULT_DURATION): resize_image(img, f.name) return Snap(path=f.name, media_type=MEDIA_TYPE_IMAGE, duration=duration) - def upload(self, bot): - self.media_id = bot.client.upload(self.file.name) - self.uploaded = True + def is_image(self): + return media_type is MEDIA_TYPE_IMAGE + + def is_video(self): + return media_type is MEDIA_TYPE_VIDEO or media_type is MEDIA_TYPE_VIDEO_WITHOUT_AUDIO + + def open(self): + if not cmd_exists("open"): + raise CannotOpenFile("Cannot open file") + + subprocess.Popen(["open", self.file.name]) + + def save(self, output_filename = None, dir_name = "."): + if output_filename is None: + output_filename = default_filename_for_snap(self) + + if not os.path.exists(dir_name): + os.makedirs(dir_name) + + with open(os.path.join(dir_name, output_filename), 'wb') as f: + data = self.file.file.read(8192) + while data: + f.write(data) + data = self.file.file.read(8192) def __init__(self, **opts): self.uploaded = False self.duration = opts['duration'] self.media_type = opts['media_type'] + if opts.get("is_story", False): + self.story_id = opts['snap_id'] + if 'sender' in opts: self.sender = opts['sender'] self.snap_id = opts['snap_id'] @@ -55,18 +79,22 @@ def __init__(self, **opts): self.from_me = True if 'data' in opts: - self.media_type = opts['media_type'] + data = opts['data'] - suffix = "." + file_extension_for_type(opts['media_type']) + if data[0:2] == 'PK': + video_filename, _ = extract_zip_components(data) + self.file = open(video_filename, 'rb+') - self.file = create_temporary_file(suffix) + else: + suffix = file_extension_for_type(opts['media_type']) + self.file = create_temporary_file(suffix) - if self.media_type is MEDIA_TYPE_VIDEO: - self.file.write(opts['data']) + if self.media_type is MEDIA_TYPE_VIDEO or self.media_type is MEDIA_TYPE_VIDEO_WITHOUT_AUDIO: + self.file.write(data) self.file.flush() else: - image = Image.open(StringIO(opts['data'])) + image = Image.open(StringIO(data)) resize_image(image, self.file.name) else: diff --git a/snapchat_bots/utils.py b/snapchat_bots/utils.py old mode 100644 new mode 100755 index b7f8c5a..5efb764 --- a/snapchat_bots/utils.py +++ b/snapchat_bots/utils.py @@ -1,6 +1,10 @@ -import tempfile, mimetypes, datetime, subprocess, re, math +import tempfile, mimetypes, datetime, subprocess, re, math, os from PIL import Image from constants import MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO, MEDIA_TYPE_VIDEO_WITHOUT_AUDIO, SNAP_IMAGE_DIMENSIONS +from zipfile import ZipFile + +def cmd_exists(cmd): + return subprocess.call("type " + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 def file_extension_for_type(media_type): if media_type is MEDIA_TYPE_IMAGE: @@ -11,6 +15,11 @@ def file_extension_for_type(media_type): def create_temporary_file(suffix): return tempfile.NamedTemporaryFile(suffix=suffix, delete=False) +def default_filename_for_snap(snap): + now = datetime.datetime.now() + 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:]) + return filename + def is_video_file(path): return mimetypes.guess_type(path)[0].startswith("video") @@ -24,7 +33,27 @@ def guess_type(path): def resize_image(im, output_path): im.thumbnail(SNAP_IMAGE_DIMENSIONS, Image.ANTIALIAS) - im.save(output_path) + im.save(output_path, quality = 100) + +def extract_zip_components(data): + tmp = create_temporary_file(".zip") + tmp.write(data) + tmp.flush() + zipped_snap = ZipFile(tmp.name) + unzip_dir = os.path.join(tmp.name.split(".")[0]) + os.mkdir(unzip_dir) + zipped_snap.extractall(unzip_dir) + filenames = os.listdir(unzip_dir) + for filename in filenames: + if filename.startswith("media"): + old_video_path = os.path.join(unzip_dir, filename) + new_video_path = os.path.join(unzip_dir, "video.mp4") + os.rename(old_video_path, new_video_path) + + elif filename.startswith("overlay"): + overlay_component = os.path.join(unzip_dir, filename) + + return new_video_path, overlay_component def duration_string_to_timedelta(s): [hours, minutes, seconds] = map(int, s.split(':')) @@ -36,3 +65,4 @@ def get_video_duration(path): matches = [x for x in result.stdout.readlines() if "Duration" in x] duration_string = re.findall(r'Duration: ([0-9:]*)', matches[0])[0] return math.ceil(duration_string_to_timedelta(duration_string).seconds) +