and Keith Cirkel
and John Flesch
GitHub (and GitHub:Enterprise) notifications in your menu bar!
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# <xbar.title>GitHub Notifications</xbar.title>
# <xbar.version>v3.0.3</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>python3</xbar.dependencies>
# <xbar.var>string(GITHUB_TOKEN): Your Personal GitHub token</xbar.var>
# <xbar.var>string(GITHUB_ENTERPRISE_TOKEN): Your Enterprise GitHub token (optional)</xbar.var>
# <xbar.var>string(GITHUB_ENTERPRISE_API): Your Enterprise GitHub URL (optional)</xbar.var>
import json
from urllib.request import urlopen, Request
import os
import sys
import re
from itertools import groupby
import textwrap
# GitHub.com
github_api_key = os.getenv("GITHUB_TOKEN", "")
# GitHub:Enterprise (optional)
enterprise_api_key = os.getenv("GITHUB_ENTERPRISE_TOKEN", "")
enterprise_api_url = os.getenv("GITHUB_ENTERPRISE_API", "")
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):
args = " ".join(["{}={}".format(k, v) for k, v in kwargs.items()])
print(title + " | " + args)
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 = Request(url, headers=headers)
request.get_method = lambda: method
response = urlopen(request, data)
return (
json.load(response) if int(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="\"%s\"" % __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="\"%s\"" % __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": textwrap.shorten(notification["subject"]["title"], width=75, placeholder="...").encode("utf-8"),
"href": notification["subject"]["url"],
"image": "iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAYAAAAmlE46AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAAA",
}
formatted["title"] = formatted["title"].decode().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", r"\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")
elif type == "Discussion":
formatted[
"image"
] += ""
formatted["templateImage"] = formatted.pop("image")
elif type == "CheckSuite":
formatted[
"image"
] = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAMpJREFUOI2Vkn0RwjAMxX+bASqlEiYBB1QCEnAADpiTbQo2B0wCKCh/NOPK2oby7nrXJi/py0dDGRY4yP0FLAr3Cw54AB4Y5XixnbRAAwzyky0oWoRjcglGoK9Q2EuSRHZ1jcJ1sWEllX0BukICKzGfh8+QtuZdydfsAduKc1LkngkTOO7sE2BaJTBGozl/lXBDKWF7/NvEjqiJEEYyazJ3SMYIQfK9IrgXbgIjjpnyKs/Cya7yBkeozxNWdpD7mpOtjcdGPz0prPobqq80AtYLw6wAAAAASUVORK5CYII="
formatted["templateImage"] = formatted.pop("image")
formatted["href"] = "https://github.com/{}/actions".format(
notification["repository"]["full_name"]
)
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", 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))
),
color=active,
href="https://github.com/notifications",
)
print_notifications(github_notifications)
else:
print_bitbar_line(
title=u"GitHub \u2014 No new notifications",
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))
),
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,
)