From f517bb2c7d9975edbeb4badb8de7465a5f69b279 Mon Sep 17 00:00:00 2001 From: Radoslav Gerganov Date: Sun, 24 Sep 2023 18:02:16 +0300 Subject: [PATCH] examples : make badge.py work without GH token If the user doesn't supply GH token, try to fetch and parse profile information from public HTML --- examples/README.md | 4 +- examples/badge.py | 127 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 104 insertions(+), 27 deletions(-) diff --git a/examples/README.md b/examples/README.md index 83346ed..89b8441 100644 --- a/examples/README.md +++ b/examples/README.md @@ -45,13 +45,15 @@ $ ./demo.py save # `badge.py` Creates a badge-like tag for a GitHub user. Some examples are [octocat](https://ggtag.io/?i=%5Cr10%2C25%2C110%2C110%5CI15%2C30%2C100%2C100%2C1%2Chttps%3A%2F%2Favatars.githubusercontent.com%2Fu%2F583231%3Fv%3D4%5Ct140%2C50%2C5%2CThe%20Octocat%5Ct140%2C80%2C2%2Cgithub.com%2Foctocat%5Ca13%2C156%2C16%2Cmap-marker-alt%5Ct33%2C158%2C2%2CSan%20Francisco%5Ca13%2C183%2C16%2Cbuilding%5Ct33%2C185%2C2%2C%40github%5Ca180%2C154%2C16%2Clink%5Ct202%2C158%2C2%2Chttps%3A%2F%2Fgithub.blog%5Ca180%2C185%2C16%2Cenvelope%5Ct202%2C185%2C2%2Coctocat%40github.com), [antirez](https://ggtag.io/?i=%5Cr10%2C25%2C110%2C110%5CI15%2C30%2C100%2C100%2C1%2Chttps%3A%2F%2Favatars.githubusercontent.com%2Fu%2F65632%3Fv%3D4%5Ct140%2C40%2C5%2CSalvatore%5Ct140%2C70%2C5%2CSanfilippo%5Ct140%2C110%2C2%2Cgithub.com%2Fantirez%5Ca13%2C156%2C16%2Cmap-marker-alt%5Ct33%2C158%2C2%2CCatania%2CSicily%2CItaly%5Ca13%2C183%2C16%2Cbuilding%5Ct33%2C185%2C2%2CRedis%20Labs%5Ca180%2C154%2C16%2Clink%5Ct202%2C158%2C2%2Chttp%3A%2F%2Finvece.org%5Ca180%2C185%2C16%2Cenvelope%5Ct202%2C185%2C2%2Cantirez%40gmail.com), [ggerganov](https://ggtag.io/?i=%5Cr10%2C25%2C110%2C110%5CI15%2C30%2C100%2C100%2C1%2Chttps%3A%2F%2Favatars.githubusercontent.com%2Fu%2F1991296%3Fv%3D4%5Ct140%2C40%2C5%2CGeorgi%5Ct140%2C70%2C5%2CGerganov%5Ct140%2C110%2C2%2Cgithub.com%2Fggerganov%5Ca13%2C156%2C16%2Cmap-marker-alt%5Ct33%2C158%2C2%2CSofia%2C%20Bulgaria%5Ca13%2C183%2C16%2Cbuilding%5Ct33%2C185%2C2%2C%40viewray-inc%20%5Ca180%2C154%2C16%2Clink%5Ct202%2C158%2C2%2Chttps%3A%2F%2Fggerganov.com%5Ca180%2C185%2C16%2Cenvelope%5Ct202%2C185%2C2%2Cggerganov%40gmail.com), [rgerganov](https://ggtag.io/?i=%5Cr10%2C25%2C110%2C110%5CI15%2C30%2C100%2C100%2C1%2Chttps%3A%2F%2Favatars.githubusercontent.com%2Fu%2F271616%3Fv%3D4%5Ct140%2C40%2C5%2CRadoslav%5Ct140%2C70%2C5%2CGerganov%5Ct140%2C110%2C2%2Cgithub.com%2Frgerganov%5Ca13%2C156%2C16%2Cmap-marker-alt%5Ct33%2C158%2C2%2CSofia%2C%20Bulgaria%5Ca13%2C183%2C16%2Cbuilding%5Ct33%2C185%2C2%2C%40vmware%5Ca180%2C154%2C16%2Clink%5Ct202%2C158%2C2%2Chttps%3A%2F%2Fxakcop.com%5Ca180%2C185%2C16%2Cenvelope%5Ct202%2C185%2C2%2Crgerganov%40gmail.com). -You need to create a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) on GitHub and export it as `GITHUB_TOKEN` environment variable. +For best results, we suggest to create a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) on GitHub and export it as `GITHUB_TOKEN` environment variable. ``` $ export GITHUB_TOKEN= $ ./badge.py # You can replace the profile picture with QR code, so you can program the tag with sound: $ ./badge.py --qrcode ``` +If you don't set `GITHUB_TOKEN`, the script will try to fetch and parse the profile information from GitHub website, which is less reliable. + # `img.py` Creates a tag with PNG image specified either as local file or URL: ``` diff --git a/examples/badge.py b/examples/badge.py index 0664baa..fe33a96 100755 --- a/examples/badge.py +++ b/examples/badge.py @@ -5,8 +5,67 @@ import ggtag import urllib.request import argparse +import html.parser -def fetch_profile(token, user): +# This is a hacky HTML parser to extract profile information from GitHub +class GHProfileParser(html.parser.HTMLParser): + + @staticmethod + def has_itemprop(attrs, value): + for attr in attrs: + if attr[0] == 'itemprop' and attr[1] == value: + return True + return False + + @staticmethod + def get_attr(attrs, key): + for attr in attrs: + if attr[0] == key: + return attr[1] + return None + + def __init__(self): + super().__init__() + self.profile = {} + self.in_works_for = False + self.in_homeloc = False + self.in_url = False + self.in_name = False + self.in_followers = False + + def set_once(self, key, value): + if key not in self.profile: + self.profile[key] = value + + def handle_starttag(self, tag, attrs) -> None: + if tag == 'li' and self.has_itemprop(attrs, 'worksFor'): + self.in_works_for = True + if tag == 'li' and self.has_itemprop(attrs, 'homeLocation'): + self.in_homeloc = True + if tag == 'li' and self.has_itemprop(attrs, 'url'): + self.in_url = True + if tag == 'span' and self.has_itemprop(attrs, 'name'): + self.in_name = True + if tag == 'span' and len(attrs) == 1 and attrs[0][0] == 'class' and attrs[0][1] == 'text-bold color-fg-default': + self.in_followers = True + if tag == 'span' and self.in_works_for: + self.set_once('company', self.get_attr(attrs, 'title')) + if tag == 'a' and self.in_url: + self.set_once('blog', self.get_attr(attrs, 'href')) + if tag == 'a' and self.has_itemprop(attrs, 'image'): + self.set_once('avatar_url', self.get_attr(attrs, 'href')) + return super().handle_starttag(tag, attrs) + + def handle_data(self, data: str) -> None: + if self.in_name: + self.set_once('name', data.strip()) + if self.in_homeloc and data.strip(): + self.set_once('location', data.strip()) + if self.in_followers: + self.set_once('followers', data.strip()) + return super().handle_data(data) + +def fetch_api_profile(token, user): url = "https://api.github.com/users/{}".format(user) headers = { 'Accept': 'application/vnd.github+json', @@ -15,29 +74,42 @@ def fetch_profile(token, user): } req = urllib.request.Request(url, None, headers) response = urllib.request.urlopen(req).read() - return json.loads(response) + profile = json.loads(response) + if profile['followers'] > 1000: + profile['followers'] = '{}k'.format(profile['followers'] // 1000) + else: + profile['followers'] = '{}'.format(profile['followers']) + return profile + +def fetch_html_profile(user): + url = "https://github.com/{}".format(user) + response = urllib.request.urlopen(url) + html_profile = response.read().decode('utf-8') + parser = GHProfileParser() + parser.feed(html_profile) + return parser.profile +def tweak_profile(profile): + if 'location' in profile: + profile['location'] = profile['location'].replace(', ', ',') + profile['followers'] = profile['followers'] + ' followers' + return profile if __name__ == '__main__': parser = argparse.ArgumentParser(description='Creates a GitHub badge') parser.add_argument('username', type=str, help='GitHub username') parser.add_argument('-q', '--qrcode', action='store_true', help='Use QR code instead of profile image') args = parser.parse_args() - token = os.environ['GITHUB_TOKEN'] - if not token: - print("Please set GITHUB_TOKEN environment variable") - sys.exit(1) username = args.username - profile = fetch_profile(token, username) + token = os.environ.get('GITHUB_TOKEN') + if not token: + print('GITHUB_TOKEN environment variable not set, using HTML parser') + profile = fetch_html_profile(username) + else: + profile = fetch_api_profile(token, username) + profile = tweak_profile(profile) profile_pic = profile['avatar_url'] name = profile['name'] - location = profile['location'] - company = profile['company'] - blog = profile['blog'] - email = profile['email'] - if location and len(location) > 15: - # try to make it shorter by removing some spaces - location = location.replace(', ', ',') tag = ggtag.GGTag() tag.rect(10, 25, 110, 110) if not args.qrcode: @@ -55,16 +127,19 @@ def fetch_profile(token, user): else: tag.text(140, 50, 5, name) tag.text(140, 80, 2, "github.com/{}".format(username)) - if location: - tag.icon(13, 156, 16, 'map-marker-alt') - tag.text(33, 158, 2, location) - if company: - tag.icon(13, 183, 16, 'building') - tag.text(33, 185, 2, company) - if blog: - tag.icon(180, 154, 16, 'link') - tag.text(202, 158, 2, blog) - if email: - tag.icon(180, 185, 16, 'envelope') - tag.text(202, 185, 2, email) + + placeholder_icons = [(13, 156), (13, 183), (180, 154), (180, 185)] + placeholder_texts = [(33, 158), (33, 185), (202, 158), (202, 185)] + icon_map = {'location': 'map-marker-alt', + 'company': 'building', + 'blog': 'link', + 'email': 'envelope', + 'followers': 'user-friends'} + props = ['location', 'company', 'blog', 'followers', 'email'] + index = 0 + for prop in props: + if profile.get(prop) and index < 4: + tag.icon(*placeholder_icons[index], 16, icon_map[prop]) + tag.text(*placeholder_texts[index], 2, profile[prop]) + index += 1 tag.browse()