├── slackbot ├── __init__.py ├── helpers.py ├── core.py ├── context.py ├── commands.py └── commandlinefu.py ├── requirements.txt ├── Dockerfile ├── run.py ├── README.md └── .gitignore /slackbot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.11.1 2 | six==1.10.0 3 | slackclient==1.0.1 4 | websocket-client==0.37.0 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7-alpine 2 | MAINTAINER Nick Wood 3 | 4 | RUN mkdir -p /usr/src/app/slackbot 5 | 6 | COPY ./slackbot /usr/src/app/slackbot 7 | COPY ./requirements.txt /usr/src/app 8 | COPY ./run.py /usr/src/app 9 | WORKDIR /usr/src/app 10 | 11 | RUN pip install -r ./requirements.txt 12 | 13 | CMD [ "python", "./run.py" ] 14 | -------------------------------------------------------------------------------- /slackbot/helpers.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def msgenc(string): 5 | # these 3 characters must be replaced for posting messages on slack 6 | return ( 7 | string.replace('&', '&').replace('<', '<').replace('>', '>')) 8 | 9 | 10 | def get_time(): 11 | return "[%s]" % ( 12 | str(time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()))) 13 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import getopt 4 | import sys 5 | 6 | from slackbot import core 7 | 8 | 9 | def main(): 10 | try: 11 | opts, args = getopt.getopt(sys.argv[1:], "", ["websocket-delay="]) 12 | except getopt.GetoptError as err: 13 | print(str(err)) 14 | sys.exit(2) 15 | 16 | websocket_delay = 1 17 | 18 | for o, a in opts: 19 | if o == "--websocket-delay": 20 | websocket_delay = int(a) 21 | else: 22 | print("unhandled argument") 23 | sys.exit(2) 24 | 25 | core.start(websocket_delay) 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /slackbot/core.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from slackclient import SlackClient 4 | from . import context 5 | from . import commands 6 | from . import helpers 7 | 8 | 9 | def start(websocket_delay=1): 10 | ctx = context.get_context() 11 | slack_client = SlackClient(ctx["bot_token"]) 12 | 13 | if slack_client.rtm_connect(): 14 | print("%s Bot successfully connected to slack, fool." % ( 15 | helpers.get_time())) 16 | 17 | while True: 18 | output = slack_client.rtm_read() 19 | command, channel = commands.parse_slack_output(output, ctx) 20 | 21 | if command and channel: 22 | commands.handle_command(command, channel, slack_client, ctx) 23 | 24 | time.sleep(websocket_delay) 25 | else: 26 | print("Connection failed. Invalid Slack token?") 27 | -------------------------------------------------------------------------------- /slackbot/context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | no_apik_msg = """ 5 | No Slack bot API token found. There are 2 ways to accomplish this: 6 | 1) Add it as the environment variable CLFU_SLACKBOT_TOKEN (better) 7 | 2) Paste it directly into the script. (less ideal, but easier) 8 | """ 9 | 10 | 11 | def get_context(): 12 | # You can manually configure the bot here, or you can configure it from 13 | # environment variables. We recommend the latter. 14 | context = { 15 | "bot_token": "", 16 | "bot_trigger": "", 17 | "max_results": 5, 18 | "worst_possible_rating": 0 19 | } 20 | 21 | # set context from environment variables 22 | if context["bot_token"] == "": 23 | token = os.getenv("CLFU_SLACKBOT_TOKEN") 24 | 25 | if token is None: 26 | print(no_apik_msg) 27 | sys.exit(1) 28 | 29 | context["bot_token"] = token 30 | 31 | if context["bot_trigger"] == "": 32 | context["bot_trigger"] = "clfu" 33 | 34 | return context 35 | -------------------------------------------------------------------------------- /slackbot/commands.py: -------------------------------------------------------------------------------- 1 | import time 2 | import base64 3 | 4 | from . import commandlinefu 5 | from . import helpers 6 | 7 | 8 | def parse_slack_output(output=[], context={}): 9 | if "bot_trigger" not in context: 10 | return None, None 11 | 12 | trigger = context["bot_trigger"] 13 | 14 | if len(output) > 0: 15 | 16 | for msg in output: 17 | 18 | if "text" not in msg: 19 | continue 20 | 21 | if msg["text"].startswith(trigger) is False: 22 | continue 23 | 24 | tmp_output = str(msg["text"].strip().lower()) 25 | olen = len(tmp_output) 26 | tlen = len(trigger) 27 | 28 | return tmp_output[tlen+1:olen], msg["channel"] 29 | 30 | return None, None 31 | 32 | 33 | def handle_command(command, channel, slack_client, ctx): 34 | data = commandlinefu.download(command) 35 | parsed_data = commandlinefu.parse_response(data, command, ctx) 36 | 37 | slack_client.api_call( 38 | "chat.postMessage", channel=channel, text=parsed_data, as_user=True) 39 | 40 | print helpers.get_time() + " Someone searched: " + command 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CommandlineFu Slackbot 2 | This is a simple slackbot based that fetches search results from commandlinefu.com and displays them in slack. 3 | It is based on the instructions [given here](https://www.fullstackpython.com/blog/build-first-slack-bot-python.html). 4 | 5 | ## Setup 6 | 7 | Navigate to the source directory and install the dependencies with pip: 8 | 9 | ```bash 10 | pip install -r requirements.txt 11 | ``` 12 | 13 | The slackbot requires a valid slack API token. [This can be accomplished here.](https://my.slack.com/services/new/bot) 14 | 15 | You can supply this token either by directly editing `slackbot/core.py`, 16 | or by setting the environment variable: `CLFU_SLACKBOT_TOKEN` (this is recommended). 17 | 18 | ### Running the bot 19 | 20 | All you need to do to run the bot is execute `run.py` from the server of your choice. 21 | 22 | ### Interacting with the thing 23 | Once the script says it's successfully connected to Slack, check the user list to see if the 24 | bot is online. If so, invite it to whichever channels you want the bot to work in. Or 25 | you can just DM the bot directly. Now let's try it, type: 26 | 27 | * clfu convert video 28 | 29 | **clfu** is the trigger the bot is listening for, and anything that comes after 30 | it is your search string. You should get back some results for command line 31 | one-liners related to converting video. Try searching for other stuff too. CLFU is a 32 | really good repository for cool complicated things you can do from the command line. 33 | This bot will come in handy for nerds. 34 | -------------------------------------------------------------------------------- /slackbot/commandlinefu.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import base64 3 | import requests 4 | import json 5 | 6 | 7 | def endpoint(command): 8 | return "http://commandlinefu.com/commands/matching/%s/%s/json" % ( 9 | urllib.quote(command), 10 | base64.b64encode(command)) 11 | 12 | 13 | def download(command): 14 | res = requests.get(endpoint(command)) 15 | 16 | if res.status_code != 200: 17 | return "Error contacting CLFU API. (HTTP-%d)" % res.status_code 18 | 19 | return res.text 20 | 21 | 22 | def parse_response(textdata, command, ctx): 23 | maxresults = ctx["max_results"] 24 | min_rating = ctx["worst_possible_rating"] 25 | 26 | postscounter = 0 # number of items we've added to the buffer string 27 | output = "<%s|*CommandlineFu*> results for *%s*:\n" % ( 28 | "http://www.commandlinefu.com", command) 29 | 30 | try: 31 | js = json.loads(textdata) 32 | except ValueError: 33 | return "There was a problem parsing the json returned from the server." 34 | 35 | if len(js) < 1: 36 | return "No results. Try again using different search terms." 37 | 38 | # let's find the highest number of votes in the results 39 | qual = 0 40 | for i in js: 41 | if int(i['votes']) > qual: 42 | qual = int(i['votes']) 43 | 44 | # this loops over the results grabbing the highest vote count first, 45 | # then drops a quality level, loops again, drops a level, etc 46 | # which should effectively return best search results at the top 47 | while qual > 0: 48 | for i in js: 49 | if int(i["votes"]) == qual: 50 | output += "<%s|%s> _(Upvotes: %s)_\n```%s```\n\n" % ( 51 | i["url"], 52 | i["summary"], 53 | i["command"], 54 | i["votes"]) 55 | 56 | postscounter += 1 57 | 58 | # got our max, we're good 59 | if postscounter == maxresults: 60 | return output 61 | qual -= 1 62 | 63 | # if we get this far and still have nothing to show for it... 64 | if len(output) < 100: 65 | tmp_output = "There were results, but none with an upvote rating of " 66 | tmp_output += "at least " + str(min_rating) 67 | 68 | return output 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | .venv/ 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 93 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 94 | 95 | # User-specific stuff: 96 | .idea 97 | .idea/workspace.xml 98 | .idea/tasks.xml 99 | .idea/dictionaries 100 | .idea/vcs.xml 101 | .idea/jsLibraryMappings.xml 102 | 103 | # Sensitive or high-churn files: 104 | .idea/dataSources.ids 105 | .idea/dataSources.xml 106 | .idea/dataSources.local.xml 107 | .idea/sqlDataSources.xml 108 | .idea/dynamic.xml 109 | .idea/uiDesigner.xml 110 | 111 | # Gradle: 112 | .idea/gradle.xml 113 | .idea/libraries 114 | 115 | # Mongo Explorer plugin: 116 | .idea/mongoSettings.xml 117 | 118 | ## File-based project format: 119 | *.iws 120 | 121 | ## Plugin-specific files: 122 | 123 | # IntelliJ 124 | /out/ 125 | 126 | # mpeltonen/sbt-idea plugin 127 | .idea_modules/ 128 | 129 | # JIRA plugin 130 | atlassian-ide-plugin.xml 131 | 132 | # Crashlytics plugin (for Android Studio and IntelliJ) 133 | com_crashlytics_export_strings.xml 134 | crashlytics.properties 135 | crashlytics-build.properties 136 | fabric.properties --------------------------------------------------------------------------------