diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Dockerfile | 13 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | docker-compose.yml | 13 | ||||
-rw-r--r-- | githubPrinter.env.example | 14 | ||||
-rw-r--r-- | issue.html.j2 | 24 | ||||
-rw-r--r-- | printIssues.py | 174 | ||||
-rw-r--r-- | requirements.txt | 5 |
8 files changed, 249 insertions, 1 deletions
@@ -1,3 +1,6 @@ +githubPrinter.env +.last_checked_at + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6db0144 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM ubuntu:latest +MAINTAINER Eden Attenborough "eda@e.email" +ENV TZ=Europe/London +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone +RUN apt-get update -y +RUN apt-get install -y python3-pip python3-dev build-essential wkhtmltopdf cron +RUN mkdir githubPrinter +COPY . /githubPrinter +WORKDIR /githubPrinter +RUN pip3 install -r requirements.txt + +RUN echo "*/15 * * * * root python3 /githubPrinter/printIssues.py > /proc/1/fd/1 2>/proc/1/fd/2" > /etc/crontab +ENTRYPOINT ["cron", "-f"]
\ No newline at end of file @@ -1,2 +1,4 @@ # PrintGithubIssues -A system for printing github issues as soon as they arrive +A system for printing github issues as soon as they arrive. + +Renders github markdown to PDF then sends it to a CUPS printer server. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..720e73f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' + +services: + printer: + container_name: githubprinter + build: + context: . + dockerfile: Dockerfile + image: jwansek/githubprinter + volumes: + - ./github_printer.log:/githubPrinter/github_printer.log + env_file: + - githubPrinter.env
\ No newline at end of file diff --git a/githubPrinter.env.example b/githubPrinter.env.example new file mode 100644 index 0000000..b401a51 --- /dev/null +++ b/githubPrinter.env.example @@ -0,0 +1,14 @@ +CUPS_USER=eden # CUPS user +CUPS_PASSWD=************** # CUPS password +CUPS_HOST=192.168.1.4:631 # CUPS server host +CUPS_PRINTER=StarLC-10 # CUPS printer name +CUPS_OPTS={"media": "A4"} # Additional options given to the cups server when printing + +# github personal access token. unfortunately the entire 'repo' scope is required- see +# https://stackoverflow.com/questions/55420473/github-api-token-scope-to-list-issues-of-a-private-repo +GITHUB_TOKEN=************************************ +# associated user from which to get repos +GITHUB_USER=jwansek + +# Issues are rendered to HTML then converted to PDF using wkhtmltopdf. Options given to wkhtmltopdf +WKHTMLTOPDF_OPTS={"page-size": "A4", "margin-top": "0.75in", "margin-right": "0.75in", "margin-bottom": "0.75in", "margin-left": "0.75in", "encoding": "UTF-8", "custom-header": [["Accept-Encoding", "gzip"]], "no-outline": null}
\ No newline at end of file diff --git a/issue.html.j2 b/issue.html.j2 new file mode 100644 index 0000000..816a656 --- /dev/null +++ b/issue.html.j2 @@ -0,0 +1,24 @@ +<!doctype html> +<html lang="en"> + <body> + <header> + <h1>{{ "%s #%d" % (title, number) }}</h1> + <table> + <tr> + <th>URL:</th> + <th><a href="{{ html_url }}">{{ html_url }}<a></th> + </tr> + <tr> + <th>Created:</th> + <th>{{ created_at }}</th> + </tr> + <tr> + <th>Created by:</th> + <th><a href="https://github.com/{{ user['login'] }}">{{ user["login"] }}</a></th> + </tr> + </table> + <hr /> + </header> + {{ gfm_html|safe }} + </body> +</html>
\ No newline at end of file diff --git a/printIssues.py b/printIssues.py new file mode 100644 index 0000000..e849567 --- /dev/null +++ b/printIssues.py @@ -0,0 +1,174 @@ +from urllib.parse import urlparse +from dataclasses import dataclass +import datetime +import tempfile +import requests +import logging +import pickle +import pdfkit +import jinja2 +import json +import cups +import os + +logging.basicConfig( + format = "%(levelname)s\t[%(asctime)s]\t%(message)s", + level = logging.INFO, + handlers=[ + logging.FileHandler("github_printer.log"), + logging.StreamHandler() + ]) + +@dataclass +class RenderedIssue(tempfile.TemporaryDirectory): + + gh_api_key: str + issue: dict + + # this really should be done by @dataclass imo... + def __post_init__(self): + super().__init__() + + def __enter__(self): + self.pdf_path = os.path.join(self.name, "out.pdf") + + with open(os.path.join(os.path.split(__file__)[0], "issue.html.j2"), "r") as f: + jinja_template = jinja2.Template(f.read()) + + gfm_html = gfm_to_html( + self.gh_api_key, self.issue["body"], get_context_from_html_url(self.issue["html_url"]) + ) + html = jinja_template.render(**self.issue, gfm_html = gfm_html) + + pdfkit.from_string(html, self.pdf_path, options = json.loads(os.environ["WKHTMLTOPDF_OPTS"])) + + return self.pdf_path + + def __exit__(self, exc, value, tb): + self.cleanup() + +# we're not inside docker- add the environment variables from the file +if not os.path.exists("/.dockerenv") and os.path.exists("githubPrinter.env"): + from dotenv import load_dotenv + load_dotenv("githubPrinter.env") + logging.info("Not being run in docker. Adding environment variables...") + +cups.setUser(os.environ["CUPS_USER"]) +cups.setPasswordCB(lambda a: os.environ["CUPS_PASSWD"]) +cups.setServer(os.environ["CUPS_HOST"]) +conn = cups.Connection() +logging.info( + "Successfully connected to CUPS server. The avaliable printers are %s" + % ", ".join(conn.getPrinters().keys()) +) +logging.info( + "The printer selected to print, '%s', to has details %s" % ( + os.environ["CUPS_PRINTER"], + json.dumps(conn.getPrinters()[os.environ["CUPS_PRINTER"]], indent=4) + ) +) + +def get_user_repos(gh_api_key: str, gh_user: str): + req = requests.get( + "https://api.github.com/users/%s/repos" % gh_user, + headers = { + "Authorization": "token %s" % gh_api_key, + "Accept": "application/vnd.github+json" + } + ) + if req.status_code == 200: + repos = req.json() + + return [get_suffix_from_issues_irl(r["issues_url"]) for r in repos] + + else: + logging.error("Request 'get_user_repos' '%s' failed with status code %d. Request returned %s" % ( + gh_user, req.status_code, req.text + )) + +def get_issues_for(gh_api_key: str, url_suffix: str, since: datetime.datetime) -> [dict]: + # logging.info("Searching for issues in %s..." % url_suffix) + req = requests.get( + "https://api.github.com%s" % url_suffix, + headers = { + "Authorization": "token %s" % gh_api_key, + "Accept": "application/vnd.github+json" + }, + params = { + "since": since.replace(microsecond = 0).isoformat() + } + ) + if req.status_code == 200: + return req.json() + else: + logging.error("Request 'get_issues_for' '%s' failed with status code %d. Request returned %s" % ( + url_suffix, req.status_code, req.text + )) + +def get_context_from_html_url(html_url: str) -> str: + return "/".join(urlparse(html_url).path.split("/")[1:3]) + +def get_suffix_from_issues_irl(issues_url: str) -> str: + return urlparse(issues_url).path.replace("{/number}", "") + +def gfm_to_html(gh_api_key: str, md_text: str, context: str): + req = requests.post( + "https://api.github.com/markdown", + headers = { + "Authorization": gh_api_key, + "Accept": "application/vnd.github+json" + }, + json = { + "mode": "gfm", + "context": context, + "text": md_text + } + ) + if req.status_code == 200: + return req.text + else: + logging.error("Request 'gfm_to_html' failed with status code %d. Request returned %s" % ( + req.status_code, req.text + )) + +def print_file(file_path: str, actually_print: bool = True): + if actually_print: + conn.printFile( + os.environ["CUPS_PRINTER"], file_path, + "GitHub printer job", json.loads(os.environ["CUPS_OPTS"]) + ) + logging.info("Sent file %s to the printer..." % file_path) + logging.info("The printer queue is now %s" % json.dumps(conn.getJobs(), indent = 4)) + +def main(): + if not os.path.exists(".last_checked_at"): + since = datetime.datetime(1970, 1, 1) + else: + with open(".last_checked_at", "rb") as f: + since = pickle.load(f) + + logging.info("Last checked at %s" % since.replace(microsecond=0).isoformat()) + the_next_since = datetime.datetime.now() + + repos = get_user_repos(os.environ["GITHUB_TOKEN"], os.environ["GITHUB_USER"]) + logging.info("Found %i repositories to search for issues in..." % len(repos)) + issues = [] + for repo in repos: + issues += get_issues_for(os.environ["GITHUB_TOKEN"], repo, since) + + logging.info("Found %i issues..." % len(issues)) + + for issue in issues: + logging.info("Going to try and render and print issue '%s #%d' in repo '%s'..." % (issue["title"], issue["number"], issue["html_url"])) + + with RenderedIssue(os.environ["GITHUB_TOKEN"], issue) as rendered_pdf: + print_file(rendered_pdf, False) + + with open(".last_checked_at", "wb") as f: + pickle.dump(the_next_since, f) + + logging.info("Run finished. Marked the last time checked as '%s'." % the_next_since.isoformat()) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..295928a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# sudo apt-get install wkhtmltopdf +pdfkit +requests +jinja +pycups |