and Keith Cirkel
and John Flesch
GitHub (and GitHub:Enterprise) notifications in your menu bar!
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# <xbar.title>GitHub Notifications</xbar.title>
# <xbar.version>v3.0.2</xbar.version>
# <xbar.author>Matt Sephton, Keith Cirkel, John Flesch</xbar.author>
# <xbar.author.github>flesch</xbar.author.github>
# <xbar.desc>GitHub (and GitHub:Enterprise) notifications in your menu bar!</xbar.desc>
# <xbar.image>https://i.imgur.com/hW7dw9E.png</xbar.image>
# <xbar.dependencies>python</xbar.dependencies>
import json
import urllib2
import os
import sys
import re
from itertools import groupby
# GitHub.com
github_api_key = os.getenv( 'GITHUB_TOKEN', 'Enter your GitHub.com Personal Access Token here...' )
# GitHub:Enterprise (optional)
enterprise_api_key = os.getenv( 'GITHUB_ENTERPRISE_TOKEN', 'Enter your GitHub:Enterprise Personal Access Token here...' )
enterprise_api_url = os.getenv( 'GITHUB_ENTERPRISE_API', 'https://github.example.com/api/v3' )
active = '#4078C0'
inactive = '#7d7d7d'
# Utility Functions
def plural( word, n ):
return str(n) + ' ' + (word + 's' if n > 1 else word)
def get_dict_subset( thedict, *keys ):
return dict([ (key, thedict[key]) for key in keys if key in thedict ])
def print_bitbar_line( title, **kwargs ):
print title + ' | ' + ( ' '.join( [ '{}={}'.format( k, v ) for k, v in kwargs.items() ] ) )
def make_github_request( url, method='GET', data=None, enterprise = False ):
try:
api_key = enterprise_api_key if enterprise else github_api_key
headers = {
'Authorization': 'token ' + api_key,
'Accept': 'application/json',
}
if data is not None:
data = json.dumps(data)
headers['Content-Type'] = 'application/json'
headers['Content-Length'] = len(data)
request = urllib2.Request( url, headers=headers )
request.get_method = lambda: method
response = urllib2.urlopen( request, data )
return json.load( response ) if response.headers.get('content-length', 0) > 0 else {}
except Exception:
return None
def get_notifications( enterprise ):
url = '%s/notifications' % (enterprise_api_url if enterprise else 'https://api.github.com')
return make_github_request( url, enterprise=enterprise ) or []
def print_notifications( notifications, enterprise=False ):
notifications = sorted( notifications, key=lambda notification: notification['repository']['full_name'] )
for repo, repo_notifications in groupby( notifications, key=lambda notification: notification['repository']['full_name'] ):
if repo:
repo_notifications = list( repo_notifications )
print_bitbar_line( title=repo )
print_bitbar_line(
title='{title} - Mark {count} As Read'.format( title=repo, count=len( repo_notifications ) ),
alternate='true',
refresh='true',
bash=__file__,
terminal='false',
param1='readrepo',
param2=repo,
param3='--enterprise' if enterprise else None
)
for notification in repo_notifications:
formatted_notification = format_notification( notification )
print_bitbar_line( refresh='true', **get_dict_subset( formatted_notification, 'title', 'href', 'image', 'templateImage' ) )
print_bitbar_line(
refresh='true',
title='%s - Mark As Read' % formatted_notification['title'],
alternate='true',
bash=__file__,
terminal='false',
param1='readthread',
param2=formatted_notification['thread'],
param3='--enterprise' if enterprise else None,
**get_dict_subset( formatted_notification, 'image', 'templateImage' )
)
def format_notification( notification ):
type = notification['subject']['type']
formatted = {
'thread': notification['url'],
'title': notification['subject']['title'].encode('utf-8'),
'href': notification['subject']['url'],
'image': 'iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAYAAAAmlE46AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAA',
}
if len(formatted['title']) > 90:
formatted['title'] = formatted['title'][:79] + '…'
formatted['title'] = formatted['title'].replace('|','-')
latest_comment_url = notification.get( 'subject', {} ).get( 'latest_comment_url', None )
typejson = make_github_request( formatted['href'] )
if latest_comment_url:
formatted['href'] = ( make_github_request( latest_comment_url ) or {} ).get( 'html_url', formatted['href'] )
# Try to hack a web-viewable URL if the last check failed
if formatted['href']:
formatted['href'] = re.sub( 'api\.|api/v3/|repos/', '', re.sub( '(pull|commit)s', ur'\1', formatted['href'] ) )
if (type == 'PullRequest'):
if typejson and typejson['merged']:
formatted['image'] += 'SpJREFUKJG9kkFOwmAQhb+ZQiVx5xm4hIlncEF7jLZuWSjSeAJsvQUY4xkMHsCtcU9MXBmwJsy4EWgFEt34VpP55mX+eflhj9KoGO5jAK00LmOwoZiaYIPRbXaXRsVQRC6BvWZJ4uLJRI6DcKlUMsVl/G0CwIw3UR8V4+QKxFd9BbfDqiP6buo1sB5QjgTJ07i8aPTFgvNFa/7i7fYzaL+YpEN3zwGux4mY2QmAm6db783i0rO4bGyrh7OL66a0Bigm6d5gGkYz3brvV8a/SjeF/dPGJLrpmTMDXs/i4vTnQNYrInNm5szqvIVYbiJdCV1Z6ANwXze6em4i3SBcqi+CNVeAIFxq9dkR0+07HfHVz6rzlsLAK5keUCEu/R0hDD7C+SME6A7+Z30BqF2G+GPLjSUAAAAASUVORK5CYII='
elif typejson and typejson['state'] == 'closed':
formatted['image'] += 'Q9JREFUKJG9kjFOw0AQRd9sbAiiQOIMqWJfAInOPULANehTQIjFCbgHFS1eKhR6cDpEbyHRmljyDpWVTWJbSsOvRvvm72i+BjpkI2ZdDIBszNVzTG7HvGcx543JxmifT7KIj4FyUlaYcMjcKI8Id17Pj8JDknMvrD4zoriwRg4OMaKtU44FUhtz6z8aFW7KkC9X82mESbJghpICJDkiyimAwvV2EDG6uZMfThs3TeFYB8miP1XjFb0pdhp31cro/muijbjAUTjDtx1zttnwEnGJo8BR+DxQSAcwWv5i9vd4BZ58Yy2kgTIqq3VuAMoKEx4htCQrijaX5fMAYRoOmbMEJ0y2lhGmZcgbNdDGd9Uf3M1iNlKZZGMAAAAASUVORK5CYII='
else:
formatted['image'] += 'TJJREFUKJG9krFOAlEQRe/MLpqHnd/ATyBWsLWF+hcLPYXibvwCE/gLC2MNVrD7AbbGnpjYboBlro2QXWATabzVy5x338ydPKBC4awTVTEA8Huz9i1FI5gZBINh6+0lnHUiEXkAUGmWMGm/c7Fues4pLU9IPv+aAABm9i2qT6Pm+BECbuoqpnZWW4lmmRLG3ZdV9VyAOEyD+1JdVO4yqX+ua7UPofRHrUlEMgaA4cVYALkEACF6e/N2k4DdJCh1Ky7nENftyVACo9akcjElox3I9yfjsdoaVfFPHbtpcG2GOUy/wjS42r0QTjs3ZpibYV7kPmkxVuuGOKfIl1MAr0WjqMRcrBqec8oCVwDwnNOFnwugezkJ4+ZnFbkPeANanpwsAQr7+2m8Qab1FKcAeYgfqR/3P4pMOYR15QAAAABJRU5ErkJggg=='
if typejson and typejson.get( 'user', {} ).get( 'login', None ):
formatted['title'] += ' (by @{})'.format(typejson['user']['login'])
elif (type == 'RepositoryInvitation'):
formatted['image'] = 'iVBORw0KGgoAAAANSUhEUgAAAA4AAAAKCAYAAACE2W/HAAAAAXNSR0IArs4c6QAAAM1JREFUKBWVkD0OQUEUhcdv/ASJn55SyxLoVBJq8tZjBRQsgkZiAQoqOiQ2oFc935nMvLxEXsRJPufOufe+kTHGmBeEf3Jg3i4t8IwOP6QZzeoi+3PFt1BRkKAq+Q4uEKbdUA+/wxm6LoubshPcoO8b9lp3GOMPGPomPoInqOcV/VUfyKfwhsChegJxfS3O6R5hALpFqFY2A69osUyygT3UQOpAWwVSpt4aShAt6lWXkIUk5WiswL5qiqIODdBz6+ZirM67cwFvulwfaH0AC7M1lHL62U4AAAAASUVORK5CYII=';
formatted['templateImage'] = formatted.pop('image');
formatted['href'] = 'https://github.com/{}/invitations'.format(notification['repository']['full_name'])
elif (type == 'Issue'):
if typejson and typejson['state'] == 'closed':
formatted['image'] += 'YpJREFUKJGdkj9I23EQxT93SZrJQYNQ6Bo65BtQcHRpSeyUuQqNWx2ti7qJRpqhUxGX0lGhlXQ0uJhfU7cOLm3+QKCzoIjoJpp8z0HT/BpJBd90HO/dvTsePBLS36iM8Uw6vANyQNJ7RJU/wG7nmo1XLY7uCStp8uL5BJwgbJtRU8E8jImR98qoGHPZBjsSFhlsCRQjCQovD2iHhx5OEDu/ZF2E5Y4wLn/tXdMy5WO2zkqXHKQxgEy95+y7Y1KMXwogbeaBk0iCwqBn7DteAJym+NmBnSiAF3IK2/32wiIVqoGjYE1GRMlGARSSZtQGbZtq8CNwFBBWDUxgJjqI3I9Mg7XAgRoXmSYlACqOWuAG39ePimNB7+oyxuzhBLEwIUhj3c92sZckLsaSAvg2m14ZPb98eGs8ThFlqBcAx4wJX8T4cGasvm5yFRaUUjwZUd4bLKow/W/kbsWfgTMxvorxmwhtjHEPbxSGEd5many7F/JqiqdemceT88rzu3ZLoWywma1z/NA5/8UNNkSJCdaYQF4AAAAASUVORK5CYII='
else:
formatted['image'] += 'ZxJREFUKJGdkjFoU2EUhb97k9jNRzEFoWvJVHXoZCqIaQaH7JaUbtpi2zc4ORWJYKGTYJLBroFaiGPo0hBwyAOhU51Cd8EOOid53uugL4SnkOK3/dx7OPccfvhPZPqxHZUWc0gIUjFs6c/CJdDBtN580P36lzCM1jZ+mr9HuFKkJfgXABO9i/mmqOTd7VlztXcyEYbR2oabtFx5kx+Oa7VHn+LpS7bOV3K5UfBaTF+6WLW52juR7ai0mDEGovq2WezuJ8t7UdkBGsXu5KqdfvlQhOeeGRc0h4QIV/nhuDarkIVgft+xHxJnQwWpKNJKn/cvasvtkcCxuFTUsKWkiOsgrhcoBb2uIE1W4NKRO8DH6cF0KSnLewYDBTqGb26dr+Sm53tR2ZNmE8LTx3O4V9XpKKZ1FV24MZyf2aoH8YE5garXBWC3X1oX12NXO8zfvPWqttwepZ08iA9wXqDypFE8a09y7PZL64geCXx37IO4XiSZcK/+dso8bRTP2pD65DufH96WOBuKSwWlAGD4QJ2Oqtff3e99mxVnJr8AXSGi02ni0+YAAAAASUVORK5CYII='
elif (type == 'Commit'):
formatted['image'] += 'HhJREFUKJHl0LEKwkAQBNCH3yIaf05S+VUqmh8ykFoUYn8WbnEc8a7XgYVlmNkdhv/EDgNmvHDFtmXq8ETCiFvsD2xqxksI9xnXB3cqxamYceHgVOpWC6JUi/QNQxj7jDsEd6wZ83KmLOId69bXzqekOeas0eiv4g3q4SY7NY1R2gAAAABJRU5ErkJggg=='
formatted['templateImage'] = formatted.pop('image')
elif (type == 'Release'):
formatted['image'] += 'JdJREFUKJGl0DsKwkAUBdDTRgvFHbgmNyLY+QWzKxM/kK2kSKc70MIIQ0ziqBceA/dxinn8mSkKVMGUmH+CBWaNboQjdn2wqt97Pa8kNd5+C0O86YNdSZC34RLjCJxhHZYLXDCIxKuwTHGOwBNcm2WKUw9OcMCybZl6XjHpQOs30cB5gKNQiDPPP0WjV/a4aVwxNsNfUGce7P8k4XgVPSYAAAAASUVORK5CYII='
formatted['templateImage'] = formatted.pop('image')
return formatted
if len(sys.argv) > 1:
command = sys.argv[1]
args = sys.argv[2:]
enterprise=False
if ('--enterprise' in args):
enterprise=True
args.remove( '--enterprise' )
if command == 'readrepo':
url = '%s/repos/%s/notifications' % (enterprise_api_url if enterprise else 'https://api.github.com', args[0])
print 'Marking %s as read' % url
make_github_request( url=url, method='PUT', data={}, enterprise=enterprise )
elif command == 'readthread':
url = args[0]
print 'Marking %s as read' % url
make_github_request( url=url, method='PATCH', data={}, enterprise=enterprise )
else:
is_github_defined = len( github_api_key ) == 40
is_github_enterprise_defined = len( enterprise_api_key ) == 40
github_notifications = get_notifications( enterprise=False ) if is_github_defined else []
enterprise_notifications = get_notifications( enterprise=True ) if is_github_enterprise_defined else []
has_notifications = len( github_notifications ) + len( enterprise_notifications )
color = active if has_notifications else inactive
if (has_notifications):
print_bitbar_line(
title=u'\u25CF'.encode( 'utf-8' ),
color=color
)
print '---'
else:
print ''
exit(0)
print_bitbar_line( title='Refresh', refresh='true' )
if is_github_defined:
if len( github_notifications ):
print_bitbar_line(
title=( u'GitHub \u2014 %s' % plural( 'notification', len( github_notifications ) ) ).encode( 'utf-8' ),
color=active,
href='https://github.com/notifications',
)
print_notifications( github_notifications )
else:
print_bitbar_line(
title=u'GitHub \u2014 No new notifications'.encode( 'utf-8' ),
color=inactive,
href='https://github.com',
)
if is_github_enterprise_defined:
if len( enterprise_notifications ):
if is_github_defined:
print '---'
print_bitbar_line(
title=( u'GitHub:Enterprise \u2014 %s' % plural( 'notification', len( enterprise_notifications ) ) ).encode( 'utf-8' ),
color=active,
href='%s/notifications' % re.sub( '/api/v3', '', enterprise_api_url ),
)
print_notifications( enterprise_notifications, enterprise=True )
else:
print '---'
print_bitbar_line(
title=u'GitHub:Enterprise \u2014 No new notifications',
color=inactive,
)