Move configuration from config.py to config.json and add support for GitHub organization webhooks

remove _format_watch() from utils.github_webhook as the value of
watchers_count is always identical with stargazers_count
https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#watch
https://developer.github.com/changes/2012-09-05-watcher-api/
This commit is contained in:
Dash Eclipse
2021-04-21 23:57:51 +00:00
parent 08c985a805
commit d3dc14977b
8 changed files with 76 additions and 49 deletions
+1 -1
View File
@@ -9,4 +9,4 @@ __pycache__/
/.idea/ /.idea/
# Configuration # Configuration
/config.py /config.json
+3 -18
View File
@@ -25,24 +25,9 @@ You need a Telegram bot token, create a Telegram bot with
1. Go to your GitHub project `Settings - Webhooks - Add webhook`, fill "Payload 1. Go to your GitHub project `Settings - Webhooks - Add webhook`, fill "Payload
URL", "Content Type" (must be `application/json`) and "Secret". You can also URL", "Content Type" (must be `application/json`) and "Secret". You can also
do this after start running the project. do this after start running the project.
2. Create a new `config.py` file based on this template. `chat_id` can be 2. Copy `config_sample.json` to `config.json` to configure it. `chat_id` can be
user/group/channel id (integer) or username (string), just make sure the bot user/group/channel id (integer) or username (string), make sure the bot is
is started or member of the chat with permission to send messages `/start`ed or member of the chat with permission to send messages
```
BOT_TOKEN = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
PORT = 12345
GH_WEBHOOKS = {
"Codertocat/Hello-World": {
"chat_id": -1001234567890,
"secret": "FPAh9pwRHCLpRL7j"
},
"Octocoders/Hello-World": {
"chat_id": "@username",
"secret": "H4xvfPNCnUhPTERq"
}
}
```
3. Configure reverse proxy for this app, corresponding configuration for Nginx 3. Configure reverse proxy for this app, corresponding configuration for Nginx
looks like this looks like this
``` ```
+12
View File
@@ -0,0 +1,12 @@
import json
from os import environ
if environ.get('DYNO'):
data = json.loads(environ.get("HOOK_CONFIG"))
else:
with open("config.json") as f:
data = json.load(f)
BOT_TOKEN = data['bot_token']
PORT = data.get('port')
GH_WEBHOOKS = data['gh_webhooks']
+18
View File
@@ -0,0 +1,18 @@
{
"bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"port": 12345,
"gh_webhooks": {
"Codertocat/Hello-World": {
"chat_id": -1001234567890,
"secret": "FPAh9pwRHCLpRL7j"
},
"Octocoders/Hello-World": {
"chat_id": "@username",
"secret": "H4xvfPNCnUhPTERq"
},
"octo-org": {
"chat_id": "@username",
"secret": "KLrYeiA3vNLPVbAv"
}
}
}
+4 -3
View File
@@ -18,6 +18,7 @@
""" """
import asyncio import asyncio
from typing import Union
from aiohttp import web, ClientSession from aiohttp import web, ClientSession
from aiohttp.web_request import Request from aiohttp.web_request import Request
@@ -39,10 +40,10 @@ async def main(_):
@routes.post("/") @routes.post("/")
async def github_webhook_post_handler(request: Request) -> Response: async def github_webhook_post_handler(request: Request) -> Response:
valid_github_webhook = await validate_github_webhook(request) tg_chat_id: Union[str, int, bool] = await validate_github_webhook(request)
if not valid_github_webhook: if not tg_chat_id:
return web.Response(status=403, text="403: Forbidden") return web.Response(status=403, text="403: Forbidden")
tg_status = await send_to_telegram(session, request) tg_status = await send_to_telegram(session, tg_chat_id, request)
return web.Response(text=f"Send to Telegram: {tg_status}") return web.Response(text=f"Send to Telegram: {tg_status}")
+33 -18
View File
@@ -22,6 +22,7 @@ import logging
from hashlib import sha256 from hashlib import sha256
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from typing import Optional from typing import Optional
from typing import Union
from aiohttp.web_request import Request from aiohttp.web_request import Request
from multidict import CIMultiDictProxy from multidict import CIMultiDictProxy
@@ -29,7 +30,7 @@ from multidict import CIMultiDictProxy
from config import GH_WEBHOOKS from config import GH_WEBHOOKS
async def validate_github_webhook(request: Request) -> bool: async def validate_github_webhook(request: Request) -> Union[str, int, bool]:
try: try:
headers = request.headers headers = request.headers
if not headers.get('User-Agent').startswith('GitHub-Hookshot'): if not headers.get('User-Agent').startswith('GitHub-Hookshot'):
@@ -39,20 +40,35 @@ async def validate_github_webhook(request: Request) -> bool:
logging.warning("Content type: not json") logging.warning("Content type: not json")
return False return False
payload = await request.json() payload = await request.json()
repo_name = payload['repository']['full_name'] hook_target: Optional[dict] = await _get_hook_target(payload)
if repo_name not in GH_WEBHOOKS.keys(): if not hook_target:
logging.warning("Repository: not in configuration")
return False return False
return await _verify_signature( valid_signature = await _verify_signature(
bytes(GH_WEBHOOKS[repo_name]['secret'], 'UTF-8'), bytes(hook_target['secret'], 'UTF-8'),
headers.get('X-Hub-Signature-256').split('=')[1], headers.get('X-Hub-Signature-256').split('=')[1],
await request.read() await request.read()
) )
if valid_signature:
return hook_target['chat_id']
else:
return False
except (JSONDecodeError, AttributeError) as error: except (JSONDecodeError, AttributeError) as error:
logging.warning("Invalid: %s", error) logging.warning("Invalid: %s", error)
return False return False
async def _get_hook_target(payload: dict) -> Optional[dict]:
name = (payload.get('organization', {}).get('login')
or payload.get('repository', {}).get('full_name'))
if not name:
logging.warning("no repo or organization found")
return None
target = GH_WEBHOOKS.get(name, None)
if not target:
logging.warning("unknown repo or organization")
return target
async def _verify_signature(secret: bytes, sig: str, msg: bytes) -> bool: async def _verify_signature(secret: bytes, sig: str, msg: bytes) -> bool:
mac = hmac.new(secret, msg=msg, digestmod=sha256) mac = hmac.new(secret, msg=msg, digestmod=sha256)
valid_signature = hmac.compare_digest(mac.hexdigest(), sig) valid_signature = hmac.compare_digest(mac.hexdigest(), sig)
@@ -96,10 +112,11 @@ async def _format_discussion(payload: dict) -> str:
async def _format_fork(payload: dict) -> str: async def _format_fork(payload: dict) -> str:
forkee = payload['forkee'] forkee = payload['forkee']
return "\u2192 <a href=\"{url}\">{name}</a>".format( text = ["\u2192 <a href=\"{url}\">{name}</a>".format(
url=forkee['html_url'], url=forkee['html_url'],
name=forkee['full_name'] name=forkee['full_name']
) ), await _get_repo_star_and_fork(payload['repository'])]
return "\n".join(text)
async def _format_issues(payload: dict) -> str: async def _format_issues(payload: dict) -> str:
@@ -153,26 +170,24 @@ async def _format_push(payload: dict) -> str:
async def _format_star(payload: dict) -> str: async def _format_star(payload: dict) -> str:
time = payload['starred_at'] time = payload['starred_at']
text = [f"\u2192 starred at <code>{time}</code>"] if time else [] text = [f"\u2192 starred at <code>{time}</code>"] if time else []
text.append(await _get_repo_watch_star_fork(payload['repository'])) text.append(await _get_repo_star_and_fork(payload['repository']))
return "\n".join(text) return "\n".join(text)
async def _format_watch(payload: dict) -> str:
return await _get_repo_watch_star_fork(payload['repository'])
async def _get_event_title(event: str, payload: dict) -> str: async def _get_event_title(event: str, payload: dict) -> str:
sender = payload['sender']['login'] summary = [payload['sender']['login']]
action = payload.get('action') or '' # if action := payload.get('action'): summary.append(action)
action = payload.get('action')
summary.append(action) if action else []
summary.append(event)
return "<b>{name}</b> | <i>{summary}</i>".format( return "<b>{name}</b> | <i>{summary}</i>".format(
name=payload['repository']['full_name'], name=payload['repository']['full_name'],
summary=" ".join([sender, action, event]) summary=" ".join(summary)
) )
async def _get_repo_watch_star_fork(repo: dict) -> str: async def _get_repo_star_and_fork(repo: dict) -> str:
return '\u2192 ' + ", ".join([ return '\u2192 ' + ", ".join([
f"<b>{repo['watchers_count']}</b> watchers",
f"<b>{repo['stargazers_count']}</b> stargazers", f"<b>{repo['stargazers_count']}</b> stargazers",
f"<b>{repo['forks_count']}</b> forks" f"<b>{repo['forks_count']}</b> forks"
]) ])
+4 -8
View File
@@ -24,13 +24,14 @@ from typing import Union
from aiohttp import ClientSession, ClientResponse from aiohttp import ClientSession, ClientResponse
from aiohttp.web_request import Request from aiohttp.web_request import Request
from config import BOT_TOKEN, GH_WEBHOOKS from config import BOT_TOKEN
from utils.github_webhook import format_github_webhook from utils.github_webhook import format_github_webhook
async def send_to_telegram(session: ClientSession, request: Request) -> str: async def send_to_telegram(session: ClientSession,
chat_id: Union[str, int],
request: Request) -> str:
message_text: Optional[str] = await format_github_webhook(request) message_text: Optional[str] = await format_github_webhook(request)
chat_id: int = await get_corresponding_chat_id(await request.json())
if not message_text: if not message_text:
tg_status = "nothing to send" tg_status = "nothing to send"
else: else:
@@ -39,11 +40,6 @@ async def send_to_telegram(session: ClientSession, request: Request) -> str:
return tg_status return tg_status
async def get_corresponding_chat_id(payload: dict) -> int:
repo_name = payload['repository']['full_name']
return GH_WEBHOOKS[repo_name]['chat_id']
async def send_message(session: ClientSession, async def send_message(session: ClientSession,
chat_id: Union[int, str], chat_id: Union[int, str],
text: str) -> bool: text: str) -> bool: