diff --git a/.gitignore b/.gitignore index 325ad527a..8b730eca8 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ env 0/ tests/.cache .coverage -.cache docs/_build docs/_static -docs/_templates \ No newline at end of file +docs/_templates +/api-token.txt diff --git a/.travis.yml b/.travis.yml index f8b358de7..84de22f21 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,14 @@ sudo: false +os: + - linux language: python python: - "3.5" env: - matrix: - - TOX_ENV=py27 - - TOX_ENV=py34 - - TOX_ENV=py35 - - TOX_ENV=flake8 + matrix: + - TOX_ENV=py27 + - TOX_ENV=py34 + - TOX_ENV=py35 cache: pip install: - "travis_retry pip install setuptools --upgrade" diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..91803f8d6 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +heroku ps:scale worker=1 +worker: python experibot.py \ No newline at end of file diff --git a/README.md b/README.md index 820a4270f..eb3daaa5b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,29 @@ -python-slackclient +BOTIN ================ +[![Build Status](https://travis-ci.org/taserian/python-slackclient.svg?branch=master)](https://travis-ci.org/slackhq/python-slackclient) + +Botin (Spanish for "little bot") + +This is a small bot used by a slack community of friends that have been communicating with each other via computers since the days of BBSes. + +Derived from slackhq/python-slackclient (Their README.md is below) + + + +Commands implemented: +- help: To learn its commands. +- imdb: Search for a movie +- imdbs: Search for similarly-titled movies +- imdbtt: Search for a movie given an IMDB ID (ttcode) + +Uses omdb.py and TMDB. + + + +python-slackclient +----------------- + [![Build Status](https://travis-ci.org/slackhq/python-slackclient.svg?branch=master)](https://travis-ci.org/slackhq/python-slackclient) [![Coverage Status](https://coveralls.io/repos/github/slackhq/python-slackclient/badge.svg?branch=master)](https://coveralls.io/github/slackhq/python-slackclient?branch=master) [![Documentation Status](https://readthedocs.org/projects/python-slackclient/badge/?version=latest)](http://python-slackclient.readthedocs.io/en/latest/?badge=latest) diff --git a/bot_commands.py b/bot_commands.py new file mode 100644 index 000000000..2bfe9cc37 --- /dev/null +++ b/bot_commands.py @@ -0,0 +1,132 @@ +import os + +import omdb +import requests + + +def imdb_info(input_text): + message_list = [] + if len(input_text) == 0: + text = "Command format: imdb [ ## <year> ]" + message_list.append((text, [])) + else: + text_l = input_text.split("##") + if len(text_l) == 1: + # title only + om = omdb.title(text_l[0], tomatoes=True) + else: + # title and year + om = omdb.title(text_l[0], year=text_l[1], tomatoes=True) + + if "title" in om.keys(): + message_list = output_movie(input_text, om) + else: + text = "Sorry, I can't seem to find anything for " + input_text + message_list.append((text, [])) + return message_list + + +def imdb_by_code(input_text): + message_list = [] + if len(input_text) == 0: + text = "Command format: imdbtt <imdb_id>" + message_list.append((text, [])) + else: + om = omdb.imdbid(input_text, tomatoes=True) + if "title" in om.keys(): + message_list = output_movie(input_text, om) + else: + text = "Sorry, " + input_text + " doesn't seem to be valid." + message_list.append((text, [])) + return message_list + + +def output_movie(input_text, om): + OMDB_API = os.environ.get("OMDB_API") + output = [] + text = 'This is what I found for "' + input_text + '":' + attach = list([dict(title=om.title, + title_link="http://www.imdb.com/title/" + om.imdb_id, + thumb_url="http://img.omdbapi.com/?apikey=" + OMDB_API + "&i=" + om.imdb_id, + text=om.plot, + fields=list([dict(title="Released", value=om.released, short=True), + dict(title="Runtime", value=om.runtime, short=True), + dict(title="Actors", value=om.actors, short=True), + dict(title="Rating", value=format_rating(om), + short=True) + ]) + ) + ]) + output.append((text, attach)) + text = get_trailer(om.imdb_id) + output.append((text, [])) + return output + + +def get_trailer(imdb_id): + trailer_info = "No trailer found." + TMDB_API = os.environ.get("TMDB_API") + find_movie_url = 'https://api.themoviedb.org/3/find/{id}?api_key={api}&language=en-US&external_source=imdb_id'. \ + format(id=imdb_id, api=TMDB_API) + t = _GET(find_movie_url) + if t["movie_results"]: + tmdb_movie_id = t['movie_results'][0]['id'] + get_trailer_url = 'https://api.themoviedb.org/3/movie/{id}/videos?api_key={api}&language=en-US'.format( + id=tmdb_movie_id, api=TMDB_API) + t = _GET(get_trailer_url) + if t['results']: + latest_trailer_key = t['results'][0]['key'] + trailer_info = "".join(["<", + "http://www.youtube.com/watch?v={yt_key}".format(yt_key=latest_trailer_key), + ">"]) + return trailer_info + + +def _GET(path): + return _request('GET', path) + + +def _request(method, path): + response = requests.request(method, path) + response.raise_for_status() + response.encoding = 'utf-8' + return response.json() + + +def format_rating(item): + if item.imdb_rating == "N/A": + imdb_portion = "N/A " + else: + imdb_portion = "IMDB: " + item.imdb_rating + "/10 (" + item.imdb_votes + ") " + + if "tomato_meter" in item.keys(): + tomato_portion = "\nTomato: " + item.tomato_meter + "% (" + item.tomato_reviews + ")" + else: + tomato_portion = "" + + return imdb_portion + tomato_portion + + +def imdb_search(input_text): + OMDB_API = os.environ.get("OMDB_API") + message_list = [] + if len(input_text) == 0: + text = "Command format: imdbs <title> [ ## <page> ]" + message_list.append((text, [])) + else: + options_list = input_text.split("##") + # print options_list + if len(options_list) > 1: + om = omdb.search(options_list[0], page=options_list[1].strip()) + else: + om = omdb.search(input_text) + mn = min(len(om), 10) + text = "Here's what I found: " + attach = [] + d = dict() + for i in range(mn): + item = om[i] + d["title"] = (d["title"] if "title" in d.keys() else "") + item.title + " (" + item.year + ") \n" + attach.append(d) + message_list.append((text, attach)) + return message_list diff --git a/bot_commands_test.py b/bot_commands_test.py new file mode 100644 index 000000000..2a7efdb61 --- /dev/null +++ b/bot_commands_test.py @@ -0,0 +1,8 @@ +def imdbInfo_test(): + # return c.imdbInfo("Mighty Ducks") + return "" + + +if __name__ == "__main__": + # print imdbInfo_test() + p = None diff --git a/command_caller.py b/command_caller.py new file mode 100644 index 000000000..283a2732c --- /dev/null +++ b/command_caller.py @@ -0,0 +1,20 @@ +import bot_commands as c + + +def call_imdb_info(i): + print(c.imdb_info(i)) + + +def call_imdb_by_code(c): + print(c.imdb_by_code(c)) + + +def call_imdb_search(s): + print(c.imdb_search(s)) + + +def test_each_output(): + print("imdb Fantastic Four ## 1994") + call_imdb_info("Fantastic Four ## 1994") + print("imdb white comanche") + call_imdb_info("white comanche") diff --git a/experibot.py b/experibot.py new file mode 100644 index 000000000..57217f7df --- /dev/null +++ b/experibot.py @@ -0,0 +1,111 @@ +import argparse +import os +import time +import traceback +from collections import namedtuple + +import bot_commands as c +import command_caller as a +from slackclient import SlackClient + +command_struct = namedtuple("command_struct", ["func", "description"]) + +# starterbot's ID as an environment variable +BOT_ID = os.environ.get("BOT_ID") + + +def bothelp(text): + message_list = [] + text = "Here's everything I can do:" + attach = [] + fields = [] + for key, available_command in COMMANDS.items(): + fields.append(dict(title=key, value=available_command.description)) + attach.append(dict(fields=fields)) + message_list.append((text, attach)) + return message_list + + +# constants +AT_BOT = "<@" + BOT_ID + "> " +COMMANDS = {"help": command_struct(bothelp, "This stuff, yeah."), + "imdb": command_struct(c.imdb_info, "Get information on a specific movie."), + "imdbs": command_struct(c.imdb_search, "Search information on movies."), + "imdbtt": command_struct(c.imdb_by_code, "Get information on a movie by providing an IMDB ID.") + } + +UNABLE_TO_UNDERSTAND = "help" + +# instantiate Slack & Twilio clients +slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN')) + + +def handle_command(comm, chan): + """ + Receives commands directed at the bot and determines if they + are valid commands. If so, then acts on the commands. If not, + returns back what it needs for clarification. + """ + response = dict() + t = "Not sure what you mean. Use the *" + UNABLE_TO_UNDERSTAND + \ + "* command for details on what I can do." + attach = [] + received_command = comm.split(" ")[0] + if received_command in COMMANDS.keys(): + try: + # Implementing message list, to allow bot to send multiple messages at once + message_list = COMMANDS[received_command].func(" ".join(comm.split(" ")[1:])) + for (t, attach) in message_list: + # print t, attach + slack_client.api_call("chat.postMessage", channel=chan, + text=t, + attachments=attach, + as_user=True) + except: + t = "Command text `" + comm + "` resulted in the following exception: " + t += traceback.format_exc() + slack_client.api_call("chat.postMessage", channel="#botin_test", + text=t, + attachments=None, + as_user=True) + tc = "Sorry, error encountered with command " + comm + tc += ". Diagnostics printed to #botin_test." + slack_client.api_call("chat.postMessage", channel="#botin_test", + text=tc, + attachments=None, + as_user=True) + + +def parse_slack_output(slack_rtm_output): + """ + The Slack Real Time Messaging API is an events firehose. + this parsing function returns None unless a message is + directed at the Bot, based on its ID. + """ + output_list = slack_rtm_output + if output_list and len(output_list) > 0: + for output in output_list: + if output and 'text' in output and AT_BOT in output['text']: + # return text after the @ mention, whitespace removed + return output['text'].split(AT_BOT)[1].strip(), \ + output['channel'] + return None, None + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="experimental imdb slackbot with local abilities") + parser.add_argument("--loc", choices=["local", "slack"], default="slack") + args = vars(parser.parse_args()) + if "loc" in args.keys() and args["loc"] == "local": + a.test_each_output() + else: + READ_WEBSOCKET_DELAY = 1 # 1 second delay between reading from firehose + if slack_client.rtm_connect(): + # print("StarterBot connected and running!") + while True: + command, channel = parse_slack_output(slack_client.rtm_read()) + if command and channel: + handle_command(command, channel) + time.sleep(READ_WEBSOCKET_DELAY) + else: + print("Connection failed. Invalid Slack token or bot ID?") diff --git a/print_bot_id.py b/print_bot_id.py new file mode 100644 index 000000000..b7ae916d0 --- /dev/null +++ b/print_bot_id.py @@ -0,0 +1,22 @@ +import os + +from slackclient import SlackClient + +BOT_NAME = 'botin' + +slack_client = SlackClient(os.environ.get('SLACK_BOT_TOKEN')) +print(os.environ.get('SLACK_BOT_TOKEN')) + +if __name__ == "__main__": + print(slack_client.api_call("api.test")) + api_call = slack_client.api_call("users.list") + print(api_call.get('ok')) + if api_call.get('ok'): + # retrieve all users so we can find our bot + users = api_call.get('members') + for user in users: + if 'name' in user and user.get('name') == BOT_NAME: + print("Bot ID for '" + user['name'] + "' is " + user.get('id')) + else: + print(api_call.get('error')) + print("could not find bot user with the name " + BOT_NAME) diff --git a/requirements-dev.txt b/requirements-dev.txt index 87864e5f5..656f53bec 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ coveralls==1.1 ipdb==0.9.3 -ipython==4.1.2 +ipython==5.1.0 pdbpp==0.8.3 pytest>=2.8.2 pytest-mock>=1.1 diff --git a/requirements.txt b/requirements.txt index 0b8229fe3..4b402e97e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,44 @@ -future==0.15.2 +backports-abc>=0.4 +backports.shutil-get-terminal-size==1.0.0 +backports.ssl-match-hostname==3.4.0.2 +colorama==0.3.7 +configparser==3.5.0 +decorator==4.0.10 +functools32==3.2.3.post2; python_version == "2.7" +gunicorn==19.6.0 +ipykernel>=4.4.1 +ipython==5.1.0 +ipython-genutils==0.1.0 +ipywidgets>=4.1.1 +Jinja2==2.8 +jsonschema==2.5.1 +jupyter==1.0.0 +jupyter-client>=4.3.0 +jupyter-console==5.0.0 +jupyter-core>=4.1.1 +MarkupSafe==0.23 +mistune>=0.7.2 +nbconvert>=4.2.0 +nbformat>=4.0.1 +notebook>=4.2.2 +omdb==0.7.0 +path.py>=8.2 +pathlib2==2.1.0 +pickleshare>=0.7.3 +prompt-toolkit>=1.0.3 +Pygments==2.1.3 +# Python-contrib-nbextensions>==alpha +pyzmq>=15.4.0 +qtconsole==4.2.1 +requests>=2.11.0 +simplegeneric==0.8.1 +singledispatch==3.4.0.3 six==1.10.0 -websocket-client==0.35.0 +slackclient==1.0.1 +tornado>=4.4.1 +tmdbsimple==1.4.0 +traitlets>=4.2.2 +wcwidth==0.1.7 +websocket-client==0.37.0 +win-unicode-console==0.5 +WTForms>=2.0.2 diff --git a/setup.py b/setup.py index 5d6429e9b..254ab53d0 100644 --- a/setup.py +++ b/setup.py @@ -13,4 +13,9 @@ 'requests', 'six', ], + extras_require={ + ':python_version == "2.7"': [ + 'functools32', + ] + }, zip_safe=False) diff --git a/tox.ini b/tox.ini index 822e858c7..92d9c5f22 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,8 @@ [tox] envlist= - py{27,34,35}, - flake8 + py{27,34,35} skipsdist=true -[flake8] -max-line-length= 100 -exclude= tests/* - [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH commands = @@ -21,10 +16,3 @@ basepython = py27: python2.7 py34: python3.4 py35: python3.5 - -[testenv:flake8] -basepython=python -deps=flake8 -commands= - flake8 \ - {toxinidir}/slackclient \ No newline at end of file