├── scripts ├── __init__.py ├── right_hubot.py ├── hubot_script.py ├── jira_lookup.py ├── lastfm.py ├── teamcity.py └── license_plate.py ├── README.md ├── requirements.txt ├── .gitignore ├── package.json ├── index.coffee ├── python_debug.py └── python_dispatch.py /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hubot-pyscripts 2 | =============== 3 | 4 | Write Hubot scripts in Python. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==0.14.0 2 | # lastfm.py 3 | lastfmapi==0.1 4 | # jira.py 5 | jira-python==0.13 6 | -------------------------------------------------------------------------------- /scripts/right_hubot.py: -------------------------------------------------------------------------------- 1 | # Description: 2 | # None 3 | # 4 | # Dependencies: 5 | # None 6 | # 7 | # Configuration: 8 | # None 9 | # 10 | # Commands: 11 | # right, hubot 12 | # 13 | # Author: 14 | # maxgoedjen 15 | 16 | import os 17 | 18 | from scripts.hubot_script import * 19 | 20 | class RightHubot(HubotScript): 21 | 22 | @hear('right(,)? hubot') 23 | def right(self, message, matches): 24 | return 'Yep.' 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /scripts/hubot_script.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import sys 4 | import time 5 | 6 | _hear_regexes = {} 7 | _resp_regexes = {} 8 | 9 | class HubotScript: 10 | 11 | def __init__(self): 12 | pass 13 | 14 | # Decorators 15 | 16 | def hear(regex): 17 | def decorator(handler): 18 | _hear_regexes[regex] = handler 19 | return decorator 20 | 21 | def respond(regex): 22 | def decorator(handler): 23 | _resp_regexes['^%s$' % regex] = handler 24 | return decorator 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-python-scripts", 3 | "version": "0.0.2", 4 | "author": "maxgoedjen", 5 | "keywords": ["hubot", "python", "scripts", "campfire", "bot", "robot"], 6 | "description": "Write Hubot scripts in Python", 7 | "main": "./index", 8 | "repository" : { 9 | "type" : "git", 10 | "url" : "https://github.com/maxgoedjen/hubot-python-scripts.git" 11 | }, 12 | "enabled_scripts" : [ 13 | "lastfm.py", 14 | "right_hubot.py", 15 | "teamcity.py", 16 | "jira_lookup.py", 17 | "license_plate.py" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # None 3 | # 4 | # Dependencies: 5 | # None 6 | # 7 | # Configuration: 8 | # None 9 | # 10 | # Author: 11 | # maxgoedjen 12 | 13 | 14 | class PythonScript 15 | 16 | pyScriptPath = __dirname + '/python_dispatch.py' 17 | python_script = require('child_process').spawn('python', [pyScriptPath]) 18 | python_script.stdout.on 'data', (data) => 19 | receive_from_python(data.toString()) 20 | 21 | module.exports = (robot) -> 22 | @robot = robot 23 | robot.respond /(.*)/i, (msg) -> 24 | newRegex = new RegExp("^[@]?#{robot.name}[:,]? ?(.*)", 'i') 25 | match = newRegex.exec msg.message.text 26 | send_to_python(match[1], msg.message.room, 'respond') 27 | robot.hear /(.*)/i, (msg) -> 28 | send_to_python(msg.message.text, msg.message.room, 'hear') 29 | 30 | send_to_python = (message, room, method) -> 31 | dict = 32 | type : method, 33 | message : message, 34 | room : room 35 | python_script.stdin.write(JSON.stringify(dict) + '\n') 36 | 37 | receive_from_python = (json) -> 38 | data = JSON.parse(json) 39 | @robot.messageRoom data.room, data.message 40 | -------------------------------------------------------------------------------- /scripts/jira_lookup.py: -------------------------------------------------------------------------------- 1 | # Description: 2 | # None 3 | # 4 | # Dependencies: 5 | # None 6 | # 7 | # Configuration: 8 | # None 9 | # 10 | # Commands: 11 | # HU-201 12 | # 13 | # Author: 14 | # maxgoedjen 15 | 16 | import os 17 | from os import environ 18 | 19 | from jira.client import JIRA 20 | 21 | from scripts.hubot_script import * 22 | 23 | HOST = environ.get('HUBOT_JIRA_HOSTNAME', '') 24 | USERNAME = environ.get('HUBOT_JIRA_USERNAME', '') 25 | PASSWORD = environ.get('HUBOT_JIRA_PASSWORD', '') 26 | 27 | 28 | class JIRALookup(HubotScript): 29 | 30 | @hear('([a-z]{2,100}-[0-9]+)') 31 | def lookup_jira(self, message, matches): 32 | issue_id = matches[0] 33 | jira = JIRA(options={'server': HOST}, basic_auth=(USERNAME, PASSWORD)) 34 | try: 35 | issue = jira.issue(issue_id) 36 | url = '{base}/browse/{id}'.format(base=HOST, id=issue_id) 37 | desc = '' 38 | env = '' 39 | assignee = 'unassigned' 40 | if issue.fields.description: 41 | desc = issue.fields.description 42 | if issue.fields.environment: 43 | env = '\n{0}'.format(issue.fields.environment) 44 | if issue.fields.assignee: 45 | assignee = 'assigned to {assignee}'.format( 46 | assignee=issue.fields.assignee.displayName) 47 | return '{id}: {status}, {assignee} ({tags})\n{title}: {url}\n{desc}{env}'.format(id=issue_id.upper(), status=issue.fields.status.name, title=issue.fields.summary, desc=desc, url=url, tags=', '.join(issue.fields.labels), assignee=assignee, env=env) 48 | except Exception as e: 49 | pass 50 | -------------------------------------------------------------------------------- /scripts/lastfm.py: -------------------------------------------------------------------------------- 1 | # Description: 2 | # None 3 | # 4 | # Dependencies: 5 | # None 6 | # 7 | # Configuration: 8 | # None 9 | # 10 | # Commands: 11 | # stuff 12 | # 13 | # Author: 14 | # maxgoedjen 15 | 16 | import os 17 | 18 | from scripts.hubot_script import * 19 | 20 | import lastfmapi 21 | 22 | class LastFM(HubotScript): 23 | 24 | def __init__(self): 25 | self.api = lastfmapi.LastFmApi(os.environ.get('HUBOT_LASTFM_API_KEY')) 26 | HubotScript.__init__(self) 27 | 28 | def recent_tracks(self): 29 | recent = self.api.user_getRecentTracks(user=os.environ.get('HUBOT_LASTFM_USERNAME')) 30 | if 'track' in recent['recenttracks']: 31 | tracks = [Track(x) for x in recent['recenttracks']['track']] 32 | return tracks 33 | return [] 34 | 35 | @hear('(?:last )?([0-9]* )?(?:song(?:s)? )played') 36 | def recently_played(self, message, matches): 37 | try: 38 | last_x = '' 39 | played = [track for track in self.recent_tracks() if not track.playing] 40 | lim = 1 41 | if len(matches) > 0 and matches[0]: 42 | lim = min(int(matches[0]), len(played)) 43 | else: 44 | lim = min(1, len(played)) 45 | 46 | for i in range(0, lim): 47 | last_x += '%s\n' % played[i] 48 | return last_x 49 | except Exception as e: 50 | pass 51 | 52 | class Track: 53 | def __init__(self, props): 54 | self.name = props['name'] 55 | self.artist = props['artist']['#text'] 56 | self.playing = props.get('@attr', {'nowplaying':'false'})['nowplaying'] == 'true' 57 | 58 | def __str__(self): 59 | return '%s by %s' % (self.name, self.artist) 60 | -------------------------------------------------------------------------------- /scripts/teamcity.py: -------------------------------------------------------------------------------- 1 | import json 2 | import difflib 3 | from os import environ 4 | 5 | import requests 6 | from requests.auth import HTTPBasicAuth 7 | from scripts.hubot_script import * 8 | 9 | HOST = environ.get('HUBOT_TEAMCITY_HOSTNAME', '') 10 | USERNAME = environ.get('HUBOT_TEAMCITY_USERNAME', '') 11 | PASSWORD = environ.get('HUBOT_TEAMCITY_PASSWORD', '') 12 | PROJECTS = [x.strip() for x in environ.get('HUBOT_TEAMCITY_PROJECTS', '').split(',')] 13 | 14 | HEADERS = {'Accept': 'application/json'} 15 | AUTH = HTTPBasicAuth(USERNAME, PASSWORD) 16 | 17 | 18 | class TeamCity(HubotScript): 19 | 20 | @respond('list (?:teamcity )?projects') 21 | def hubot_list_projects(self, message, matches): 22 | projects = '' 23 | for x in self.get_buildtypes(): 24 | projects += '{0}\n'.format(x) 25 | return projects 26 | 27 | @respond('(?:teamcity )?(?:build|deploy) ([^ ]*)(?: (.*))?') 28 | def hubot_build(self, message, matches): 29 | return self.build(name=matches[0], branch=matches[1]) 30 | 31 | def build(self, name='', branch=''): 32 | closest = self.get_closest_buildtype(name) 33 | if not closest: 34 | return 'No projects found for {name}'.format(name=name) 35 | closest_name, closest_id = closest 36 | branchstr = '' 37 | if branch: 38 | branchstr = '&branchName={branch}'.format(branch=branch) 39 | url = '/httpAuth/action.html?add2Queue={buildtype}&moveToTop=true{branchstr}'.format(buildtype=closest_id, branchstr=branchstr) 40 | r = self.request(url) 41 | if r.status_code == 200: 42 | return 'Building {name}'.format(name=closest_name) 43 | else: 44 | return 'Error building {name}'.format(name=closest_name) 45 | 46 | def get_buildtypes(self): 47 | all_buildtypes = {} 48 | for project in PROJECTS: 49 | url = '/httpAuth/app/rest/projects/name:{project}/buildTypes'.format(project=project) 50 | r = self.request(url) 51 | data = json.loads(r.text) 52 | for x in data['buildType']: 53 | all_buildtypes[x['name']] = x['id'] 54 | return all_buildtypes 55 | 56 | def get_closest_buildtype(self, name=''): 57 | buildtypes = self.get_buildtypes() 58 | matches = difflib.get_close_matches(name, buildtypes.keys(), 1, .3) 59 | if matches: 60 | return (matches[0], buildtypes[matches[0]]) 61 | 62 | def request(self, url): 63 | return requests.get('http://{host}{url}'.format(host=HOST, url=url), headers=HEADERS, auth=AUTH) 64 | -------------------------------------------------------------------------------- /python_debug.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | import inspect 5 | import json 6 | import re 7 | 8 | from scripts import hubot_script 9 | 10 | 11 | class HubotDispatch(object): 12 | 13 | def __init__(self): 14 | self.hear_regexes = {} 15 | self.resp_regexes = {} 16 | self.instance_map = {} 17 | self.load_scripts() 18 | self.start_listening() 19 | 20 | def start_listening(self): 21 | while True: 22 | line = raw_input('>') 23 | if line: 24 | self.receive(line) 25 | else: 26 | return 27 | 28 | def receive(self, line): 29 | self.dispatch(line) 30 | 31 | def send(self, message): 32 | if message: 33 | print message 34 | 35 | def dispatch(self, msg): 36 | if msg.startswith('hubot '): 37 | trimmed = msg[len('hubot '):] 38 | self.dispatch_generic(trimmed, self.resp_regexes) 39 | else: 40 | self.dispatch_generic(msg, self.hear_regexes) 41 | 42 | def dispatch_generic(self, message, regexes): 43 | for regex in regexes: 44 | search = re.search(regex, message, re.IGNORECASE) 45 | if search: 46 | handler = regexes[regex] 47 | response = message 48 | instance = self.instance_map[handler] 49 | try: 50 | response_text = handler(instance, message, search.groups()) 51 | if response_text: 52 | self.send(response_text) 53 | except Exception as e: 54 | self.send('Python exception: {0}'.format(str(e))) 55 | self.send(response) 56 | 57 | def no_handler(self, message): 58 | pass 59 | 60 | def load_scripts(self): 61 | prefix = '{root}{sep}'.format( 62 | root=os.path.dirname(os.path.realpath(__file__)), sep=os.sep) 63 | sys.path.append('{0}scripts'.format(prefix)) 64 | package = json.load(open('{0}package.json'.format(prefix))) 65 | self.scripts = [] 66 | for filename in package['enabled_scripts']: 67 | modname = filename.replace('.py', '') 68 | modf = imp.find_module(modname) 69 | hubot_script._hear_regexes.clear() 70 | hubot_script._resp_regexes.clear() 71 | try: 72 | mod = imp.load_module(modname, *modf) 73 | regexes = {} 74 | regexes.update(hubot_script._hear_regexes) 75 | regexes.update(hubot_script._resp_regexes) 76 | self.hear_regexes.update(hubot_script._hear_regexes) 77 | self.resp_regexes.update(hubot_script._resp_regexes) 78 | 79 | for name, member in inspect.getmembers(mod): 80 | if inspect.isclass(member): 81 | if issubclass(member, hubot_script.HubotScript) and member != hubot_script.HubotScript: 82 | instance = member() 83 | for key in regexes: 84 | self.instance_map[regexes[key]] = instance 85 | self.scripts += [instance] 86 | except Exception as e: 87 | pass 88 | 89 | if __name__ == '__main__': 90 | dispatch = HubotDispatch() 91 | -------------------------------------------------------------------------------- /scripts/license_plate.py: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Looks up owner of a car from license plate by looking at a google doc 3 | # 4 | # Dependencies: 5 | # None 6 | # 7 | # Configuration: 8 | # LICENSE_PLATE_DOC 9 | # 10 | # Commands: 11 | # plate ASDF123 12 | # 13 | # Author: 14 | # maxgoedjen 15 | 16 | import os 17 | from os import environ 18 | import csv 19 | 20 | import requests 21 | 22 | from scripts.hubot_script import * 23 | 24 | URL = environ.get('HUBOT_LICENSE_PLATE_DOC', '') 25 | MAKE = 'What is the make of your vehicle?' 26 | MODEL = 'What is the model of your vehicle?' 27 | COLOR = 'What color is your vehicle?' 28 | PLATE = 'What is your license plate #?' 29 | NAME = 'What is your name?' 30 | SURVEY_URL = environ.get('HUBOT_LICENSE_PLATE_SURVEY_URL', '') 31 | 32 | class LicensePlate(HubotScript): 33 | 34 | @hear('\\bplate #? ?([a-z0-9]+)') 35 | def lookup_plate(self, message, matches): 36 | lookup_plate = matches[0].replace(' ', '').lower() 37 | csvrows = self.get_csv_rows(URL) 38 | mapping = self.get_mapping(csvrows[0]) 39 | partials = [] 40 | for row in csvrows: 41 | plate = row[mapping[PLATE]].replace( 42 | ' ', '').replace('#', '').lower() 43 | make = row[mapping[MAKE]] 44 | model = row[mapping[MODEL]] 45 | color = row[mapping[COLOR]] 46 | name = row[mapping[NAME]] 47 | description = 'Plate {plate} is a {color} {make} {model} owned by {name}'.format( 48 | plate=plate.upper(), color=color, make=make, model=model, name=name) 49 | if plate == lookup_plate: 50 | return description 51 | if lookup_plate in plate and len(lookup_plate) > 2: 52 | partials += [description] 53 | if partials: 54 | return 'No exact matches, the following plates partially matched:\n{0}'.format('\n'.join(partials)) 55 | return "I don't know who the car with plate {0} belongs to".format(lookup_plate) 56 | 57 | @hear('who (?:owns|drives) (?:an?|the) ([^\?]+)') 58 | def lookup_car(self, message, matches): 59 | search = matches[0].lower() 60 | csvrows = self.get_csv_rows(URL) 61 | mapping = self.get_mapping(csvrows[0]) 62 | matches = [] 63 | for row in csvrows: 64 | make = row[mapping[MAKE]] 65 | model = row[mapping[MODEL]] 66 | color = row[mapping[COLOR]] 67 | name = row[mapping[NAME]] 68 | search_description = '{color} {make} {model} {color} {model}'.format( 69 | color=color, make=make, model=model).lower() 70 | if search in search_description: 71 | description = '{name} owns a {color} {make} {model}'.format( 72 | color=color.title(), make=make.title(), model=model.title(), name=name) 73 | matches += [description] 74 | if matches: 75 | return '\n'.join(matches) 76 | return "I don't know of anyone owning a {search}".format( 77 | search=search.title()) 78 | 79 | @respond('new car') 80 | def link_survey_url(self, message, matches): 81 | if SURVEY_URL: 82 | return SURVEY_URL 83 | 84 | def get_csv_rows(self, url): 85 | r = requests.get(url) 86 | text = unicode(r.text) 87 | csv_reader = csv.reader(text.splitlines()) 88 | rows = list(csv_reader) 89 | return rows 90 | 91 | def get_mapping(self, row): 92 | mapping = {} 93 | fields = [MAKE, MODEL, COLOR, PLATE, NAME] 94 | for field in fields: 95 | mapping[field] = row.index(field) 96 | return mapping 97 | -------------------------------------------------------------------------------- /python_dispatch.py: -------------------------------------------------------------------------------- 1 | import imp 2 | import os 3 | import sys 4 | import inspect 5 | import json 6 | import re 7 | 8 | from scripts import hubot_script 9 | 10 | class HubotDispatch(object): 11 | 12 | def __init__(self): 13 | self.hear_regexes = {} 14 | self.resp_regexes = {} 15 | self.instance_map = {} 16 | self.load_scripts() 17 | self.start_listening() 18 | 19 | def start_listening(self): 20 | while True: 21 | line = sys.stdin.readline() 22 | if line: 23 | self.receive(line) 24 | else: 25 | return 26 | 27 | def receive(self, json_str): 28 | try: 29 | json_dict = json.loads(json_str) 30 | self.dispatch(json_dict) 31 | except Exception as e: 32 | pass 33 | 34 | def send(self, message): 35 | if message: 36 | sys.stdout.write(json.dumps(message) + '\n') 37 | sys.stdout.flush() 38 | 39 | def dispatch(self, json_dict): 40 | msg_type = json_dict['type'] 41 | if msg_type == 'hear': 42 | json_dict['message'] = json_dict['message'] 43 | self.dispatch_generic(json_dict, self.hear_regexes) 44 | elif msg_type == 'respond': 45 | self.dispatch_generic(json_dict, self.resp_regexes) 46 | 47 | def dispatch_generic(self, message, regexes): 48 | for regex in regexes: 49 | search = re.search(regex, message['message'], re.IGNORECASE) 50 | if search: 51 | handler = regexes[regex] 52 | response = message 53 | instance = self.instance_map[handler] 54 | try: 55 | response_text = handler(instance, message, search.groups()) 56 | if response_text: 57 | response['message'] = response_text 58 | self.send(response) 59 | except Exception as e: 60 | response['message'] = 'Python exception: {0}'.format(str(e)) 61 | self.send(response) 62 | 63 | def no_handler(self, message): 64 | pass 65 | 66 | def load_scripts(self): 67 | prefix = '{root}{sep}'.format(root=os.path.dirname(os.path.realpath(__file__)), sep=os.sep) 68 | sys.path.append('{0}scripts'.format(prefix)) 69 | package = json.load(open('{0}package.json'.format(prefix))) 70 | self.scripts = [] 71 | for filename in package['enabled_scripts']: 72 | modname = filename.replace('.py', '') 73 | modf = imp.find_module(modname) 74 | hubot_script._hear_regexes.clear() 75 | hubot_script._resp_regexes.clear() 76 | try: 77 | mod = imp.load_module(modname, *modf) 78 | regexes = {} 79 | regexes.update(hubot_script._hear_regexes) 80 | regexes.update(hubot_script._resp_regexes) 81 | self.hear_regexes.update(hubot_script._hear_regexes) 82 | self.resp_regexes.update(hubot_script._resp_regexes) 83 | 84 | for name, member in inspect.getmembers(mod): 85 | if inspect.isclass(member): 86 | if issubclass(member, hubot_script.HubotScript) and member != hubot_script.HubotScript: 87 | instance = member() 88 | for key in regexes: 89 | self.instance_map[regexes[key]] = instance 90 | self.scripts += [instance] 91 | except Exception as e: 92 | pass 93 | 94 | if __name__ == '__main__': 95 | dispatch = HubotDispatch() 96 | --------------------------------------------------------------------------------