├── .dockerignore ├── .env.template ├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── output └── .gitignore └── src └── main.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | .env 4 | *.md -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | GITHUB_USERNAME= 2 | GITHUB_ACCESS_TOKEN= -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code 2 | .vscode 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # GitHub Notifications RSS Feed Changelog 2 | 3 | All notable changes to this project are documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | Additionally, the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) format is respected and used to generate this [conventional changelog](https://github.com/conventional-changelog/conventional-changelog). 7 | 8 | # 0.2.0 (2020-02-23) 9 | 10 | ### Features 11 | 12 | * add message to indicate successful feed generation ([145a865](https://github.com/teddy-gustiaux/github-notifications-rss-feed/commit/145a865f58eeeeb9f36c839ee80c12e4324b62d3)) 13 | 14 | ### Bug Fixes 15 | 16 | * use current date as last build date ([c102d69](https://github.com/teddy-gustiaux/github-notifications-rss-feed/commit/c102d69433992ebc65dffeb5b8b8b6afe9730e11)) 17 | 18 | # 0.1.0 (2020-02-13) 19 | 20 | ### Features 21 | 22 | * generate feed from past week's notifications ([02cfd93](https://github.com/teddy-gustiaux/github-notifications-rss-feed/commit/02cfd93)) 23 | 24 | ### Bug Fixes 25 | 26 | * better handle OS and network exceptions ([bc5b00e](https://github.com/teddy-gustiaux/github-notifications-rss-feed/commit/bc5b00e)) 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # === BASE STAGE === 2 | 3 | FROM python:3.8 as base 4 | 5 | # Install system dependencies 6 | RUN apt-get update 7 | RUN apt-get install pipenv -y 8 | 9 | # Set environment variables 10 | ENV appDirectory /app 11 | WORKDIR ${appDirectory} 12 | 13 | # Add the dependency manager files 14 | ADD Pipfile ${appDirectory} 15 | ADD Pipfile.lock ${appDirectory} 16 | 17 | # Install application dependencies 18 | RUN pipenv install --dev 19 | 20 | # === ENTRYPOINT STAGE === 21 | 22 | FROM base as entrypoint 23 | 24 | # Command to run when the container start 25 | ENTRYPOINT [ "pipenv", "run", "python", "./src/main.py" ] 26 | 27 | # === PRODUCTION BUILD STAGE === 28 | 29 | FROM entrypoint as production 30 | 31 | # Add the application source files 32 | ADD ./src/* ${appDirectory}/src/ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Teddy Gustiaux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Variables 2 | DOCKER_ID := teddygustiaux 3 | REPOSITORY := github-notifications-rss-feed 4 | TAG_PROD := latest 5 | TAG_DEV := latest-dev 6 | IMAGE_PROD := $(DOCKER_ID)/$(REPOSITORY):$(TAG_PROD) 7 | IMAGE_DEV := $(DOCKER_ID)/$(REPOSITORY):$(TAG_DEV) 8 | 9 | # Parameters 10 | ifeq ($(ENV_DIR),) 11 | ENV_DIR := ${CURDIR} 12 | endif 13 | 14 | ifeq ($(OUTPUT_DIR),) 15 | OUTPUT_DIR := ${CURDIR} 16 | endif 17 | 18 | ifeq ($(APP_DIR),) 19 | APP_DIR := ${CURDIR} 20 | endif 21 | 22 | # Targets 23 | default: 24 | $(error Please provide a target) 25 | 26 | build_production: 27 | docker build --target production --tag $(IMAGE_PROD) . 28 | 29 | build_development: 30 | docker build --target entrypoint --tag $(IMAGE_DEV) . 31 | 32 | run_production: 33 | docker run \ 34 | --rm \ 35 | --name ghnrf \ 36 | --env-file "${ENV_DIR}/.env" \ 37 | --mount type=bind,source="${OUTPUT_DIR}/output",target=/app/output \ 38 | $(IMAGE_PROD) 39 | 40 | run_development: 41 | docker run \ 42 | --rm \ 43 | --name ghnrf-dev \ 44 | --env-file "${ENV_DIR}/.env" \ 45 | --mount type=bind,source="${APP_DIR}/output",target=/app/output \ 46 | --mount type=bind,source="${OUTPUT_DIR}/src",target=/app/src \ 47 | $(IMAGE_DEV) 48 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pylint = "*" 8 | autopep8 = "*" 9 | 10 | [packages] 11 | requests = "*" 12 | python-dotenv = "*" 13 | pyrss2gen = "*" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "881533828d1b8cc146bfb887f3a61a1f1140164c9e6f7302ad5fcd1cb506b292" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.8" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", 22 | "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" 23 | ], 24 | "version": "==2019.11.28" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "idna": { 34 | "hashes": [ 35 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 36 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 37 | ], 38 | "version": "==2.8" 39 | }, 40 | "pyrss2gen": { 41 | "hashes": [ 42 | "sha256:7960aed7e998d2482bf58716c316509786f596426f879b05f8d84e98b82c6ee7" 43 | ], 44 | "index": "pypi", 45 | "version": "==1.1" 46 | }, 47 | "python-dotenv": { 48 | "hashes": [ 49 | "sha256:440c7c23d53b7d352f9c94d6f70860242c2f071cf5c029dd661ccb22d64ae42b", 50 | "sha256:f254bfd0c970d64ccbb6c9ebef3667ab301a71473569c991253a481f1c98dddc" 51 | ], 52 | "index": "pypi", 53 | "version": "==0.10.5" 54 | }, 55 | "requests": { 56 | "hashes": [ 57 | "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", 58 | "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" 59 | ], 60 | "index": "pypi", 61 | "version": "==2.22.0" 62 | }, 63 | "urllib3": { 64 | "hashes": [ 65 | "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", 66 | "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" 67 | ], 68 | "version": "==1.25.8" 69 | } 70 | }, 71 | "develop": { 72 | "astroid": { 73 | "hashes": [ 74 | "sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", 75 | "sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" 76 | ], 77 | "version": "==2.3.3" 78 | }, 79 | "autopep8": { 80 | "hashes": [ 81 | "sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43" 82 | ], 83 | "index": "pypi", 84 | "version": "==1.5" 85 | }, 86 | "colorama": { 87 | "hashes": [ 88 | "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", 89 | "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" 90 | ], 91 | "markers": "sys_platform == 'win32'", 92 | "version": "==0.4.3" 93 | }, 94 | "isort": { 95 | "hashes": [ 96 | "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", 97 | "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" 98 | ], 99 | "version": "==4.3.21" 100 | }, 101 | "lazy-object-proxy": { 102 | "hashes": [ 103 | "sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", 104 | "sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", 105 | "sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", 106 | "sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", 107 | "sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", 108 | "sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", 109 | "sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", 110 | "sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", 111 | "sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", 112 | "sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", 113 | "sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", 114 | "sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", 115 | "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", 116 | "sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", 117 | "sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", 118 | "sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", 119 | "sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", 120 | "sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", 121 | "sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", 122 | "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", 123 | "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" 124 | ], 125 | "version": "==1.4.3" 126 | }, 127 | "mccabe": { 128 | "hashes": [ 129 | "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", 130 | "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" 131 | ], 132 | "version": "==0.6.1" 133 | }, 134 | "pycodestyle": { 135 | "hashes": [ 136 | "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", 137 | "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" 138 | ], 139 | "version": "==2.5.0" 140 | }, 141 | "pylint": { 142 | "hashes": [ 143 | "sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", 144 | "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" 145 | ], 146 | "index": "pypi", 147 | "version": "==2.4.4" 148 | }, 149 | "six": { 150 | "hashes": [ 151 | "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", 152 | "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" 153 | ], 154 | "version": "==1.14.0" 155 | }, 156 | "wrapt": { 157 | "hashes": [ 158 | "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" 159 | ], 160 | "version": "==1.11.2" 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Notifications RSS Feed 2 | 3 | ## Warning note 4 | 5 | This project is in an **alpha** release stage. 6 | It is not recommended for production usage. 7 | 8 | ## What does it do? 9 | 10 | This application generates an RSS feed from your GitHub notifications, that you can subscribe to in your favorite RSS reader application. 11 | 12 | You no longer have to sort through all the notification emails GitHub is sending you- leverage the power of RSS! 13 | 14 | ## Prerequisites 15 | 16 | Prerequisites: 17 | 18 | - You will need to install Docker. 19 | - You will need a [GitHub personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line#creating-a-token) to be able to use their API. 20 | The `notifications` scope is the only one required. 21 | 22 | ## Development 23 | 24 | Steps: 25 | 26 | - Clone this repository 27 | - Setup your `.env` file (as per the `.env.template` file) 28 | - You will have to build the Docker image for development once: 29 | 30 | ``` 31 | cd /path/to/github-notifications-rss-feed 32 | make build_development 33 | ``` 34 | 35 | - Then you can edit the source code files. 36 | Run the application with: 37 | 38 | ``` 39 | make run_development 40 | ``` 41 | 42 | ## Self-hosting installation 43 | 44 | In addition to the prerequisites of the previous, you will want to setup a cron job to run the application regularly. 45 | 46 | For instance, setting up `crontab` to run it every 20 minutes: 47 | 48 | ``` 49 | */20 * * * * /path/to/my/script.sh 50 | ``` 51 | 52 | For security reasons, use an user with minimum permissions. 53 | It is recommended to set up the command in a separate shell script: 54 | 55 | ``` 56 | #!/bin/sh 57 | 58 | docker run \ 59 | --rm \ 60 | --name ghnrf \ 61 | --env-file /path/to/.env \ 62 | --mount type=bind,source=/path/to/output,target=/app/output \ 63 | teddygustiaux/github-notifications-rss-feed:latest 64 | ``` 65 | 66 | You will want to serve your RSS file to the web (see below). 67 | It is recommended to store your `.env` file in a different location with appropriate permissions as it contains personal credentials which should not be shared. 68 | 69 | Finally, setup a custom sub-domain to access the generate RSS feed. 70 | I recommend [Let's Encrypt](https://letsencrypt.org/) to get a free TLS certificate. 71 | 72 | You may also likely want to protect your endpoint. 73 | Basic authentication is a quick and basic (pun intended) way to do this. 74 | The web has many resources explaining [how](https://httpd.apache.org/docs/2.4/programs/htpasswd.html) to do [this](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/). 75 | 76 | Your Nginx configuration could look like this: 77 | 78 | ``` 79 | server { 80 | 81 | listen 80; 82 | server_name ghnrf.example.com; 83 | 84 | location / { 85 | return 301 https://ghnrf.example.com$request_uri; 86 | } 87 | 88 | } 89 | 90 | server { 91 | 92 | listen 443 ssl; 93 | server_name ghnrf.example.com; 94 | server_tokens off; 95 | 96 | access_log /var/log/nginx/access.log; 97 | error_log /var/log/nginx/error.log; 98 | 99 | auth_basic "Administrator's Area"; 100 | auth_basic_user_file /path/to/my.htpasswd; 101 | 102 | ssl_certificate /etc/letsencrypt/live/ghnrf.example.com/fullchain.pem; 103 | ssl_certificate_key /etc/letsencrypt/live/ghnrf.example.com/privkey.pem; 104 | 105 | root /path/to/output; 106 | 107 | location = /feed.xml { 108 | try_files /feed.xml =404; 109 | } 110 | 111 | location ^~ / { 112 | return 404; 113 | } 114 | 115 | } 116 | ``` 117 | 118 | Your feed will be available at https://ghnrf.example.com/feed.xml once the application has run once. 119 | 120 | ## Notes for Docker Toolbox on Windows 121 | 122 | If you are using Docker Toolbox on Windows, you may want to have your application files in a different directory than the default shared location. 123 | In this case, once you have shared the appropriate folder in VirtualBox, the regular commands to run the application will fail. 124 | 125 | The `Makefile` allows to pass up to 3 different parameters indicating where to find the files and folders the application needs to run. 126 | All of them default to the current directory. 127 | 128 | - `ENV_DIR` must point to the folder containing your `.env` file. 129 | - `APP_DIR` must point to the folder that itself contains the application source file under a folder named `src`. 130 | - `OUTPUT_DIR` must point to the folder that itself will have a folder named `output` containing the generated RSS file. 131 | 132 | If all parameters are the same, which is very likely if you are in a development environment, then you can copy the value of the first parameter to the others! 133 | 134 | ``` 135 | make run_development ENV_DIR=/path/to/github-notifications-rss-feed APP_DIR=$(ENV_DIR) OUTPUT_DIR=$(ENV_DIR) 136 | ``` -------------------------------------------------------------------------------- /output/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated XML files 2 | *.xml -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import datetime 4 | import requests 5 | import PyRSS2Gen 6 | from datetime import timedelta, datetime as dt 7 | from dotenv import load_dotenv 8 | from requests.auth import HTTPBasicAuth 9 | 10 | load_dotenv() 11 | 12 | 13 | def generate_rss(notifications): 14 | items = [] 15 | for notification in notifications: 16 | title = "[" + notification["repository"]["name"] + "] " + \ 17 | notification["subject"]["title"] 18 | description = "[" + notification["reason"] + "] " + \ 19 | notification["subject"]["type"] 20 | items.append(PyRSS2Gen.RSSItem( 21 | title=title, 22 | link=notification["subject"]["url"], 23 | description=description, 24 | guid=PyRSS2Gen.Guid(notification["url"]), 25 | pubDate=dt.strptime(notification["updated_at"], "%Y-%m-%dT%H:%M:%S%z"))) 26 | rss = PyRSS2Gen.RSS2( 27 | title="GitHub Notifications Feed", 28 | link="https://github.com/notifications", 29 | description="The latest notifications from your GitHub account.", 30 | lastBuildDate=datetime.datetime.now(), 31 | items=items) 32 | rss.write_xml(open("./output/feed.xml", "w")) 33 | 34 | 35 | def get_notifications(username, token): 36 | date = dt.today() - datetime.timedelta(days=14) 37 | since_last_week = date.isoformat(timespec='seconds') 38 | headers = {'Accept': 'application/vnd.github.v3+json'} 39 | payload = {'all': 'true', 'since': since_last_week} 40 | return requests.get('https://api.github.com/notifications', 41 | auth=HTTPBasicAuth(username, token), 42 | headers=headers, 43 | params=payload) 44 | 45 | 46 | def main(): 47 | try: 48 | os.remove("./output/feed.xml") 49 | except OSError: 50 | pass 51 | try: 52 | GITHUB_USERNAME = os.getenv("GITHUB_USERNAME") 53 | GITHUB_ACCESS_TOKEN = os.getenv("GITHUB_ACCESS_TOKEN") 54 | response = get_notifications(GITHUB_USERNAME, GITHUB_ACCESS_TOKEN) 55 | response.raise_for_status() 56 | generate_rss(response.json()) 57 | print("RSS feed generated successfully!") 58 | except requests.exceptions.RequestException as e: 59 | print(e) 60 | sys.exit(1) 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | --------------------------------------------------------------------------------