Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ on:

permissions:
issues: read
pull_request: read
pull_requests: read

jobs:
notify:
Expand Down
23 changes: 10 additions & 13 deletions notifier/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,20 @@ def get_interactor(url: str) -> type[SendIssue] | type[SendPR]:


if __name__ == "__main__":
html_template = os.environ.get("HTML_TEMPLATE", "").strip()
bot_token = os.environ["TELEGRAM_BOT_TOKEN"]
attempt_count = os.environ["ATTEMPT_COUNT"]

telegram_gateway = TelegramGateway(
chat_id=os.environ["TELEGRAM_CHAT_ID"],
bot_token=os.environ["TELEGRAM_BOT_TOKEN"],
attempt_count=int(os.environ["ATTEMPT_COUNT"]),
message_thread_id=os.environ.get("TELEGRAM_MESSAGE_THREAD_ID"),
)
telegram_gateway = TelegramGateway(bot_token, int(attempt_count))

gh_token = os.environ["GITHUB_TOKEN"]
event_url = os.environ["EVENT_URL"]

github_gateway = GithubGateway(
token=(os.environ.get("GITHUB_TOKEN") or "").strip(),
event_url=event_url,
)
github_gateway = GithubGateway(gh_token, event_url)

html_template = os.environ.get("HTML_TEMPLATE", "").strip()
custom_labels = os.environ.get("CUSTOM_LABELS", "").split(",")
if custom_labels == [""]:
custom_labels = []

render_service = RenderService(
custom_labels=custom_labels,
join_input_with_list=os.environ.get("JOIN_INPUT_WITH_LIST") == "1",
Expand All @@ -56,8 +50,11 @@ def get_interactor(url: str) -> type[SendIssue] | type[SendPR]:
render_service=render_service,
)

chat_id = os.environ["TELEGRAM_CHAT_ID"]
message_thread_id=os.environ.get("TELEGRAM_MESSAGE_THREAD_ID")

try:
interactor.handler()
interactor.handler(chat_id, message_thread_id)
except Exception as e:
print(f"Error processing event: {e}", file=sys.stderr)
sys.exit(1)
32 changes: 27 additions & 5 deletions notifier/application/interactors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from notifier.application import interfaces
from notifier.application.services import RenderService
from notifier.domain.entities import Issue, PullRequest
from notifier.infrastructure.telegram_gateway import TgPayload

TG_MESSAGE_LIMIT: typing.Final = 4096


ISSUE_TEMPLATE: typing.Final = (
"🚀 <b>New issue to <a href=/{repository}>{repository}</a> by <a href=/{user}>@{user}</a> </b><br/>"
"📝 <b>{title}</b> (<a href='{url}'>#{id}</a>)<br/><br/>"
Expand Down Expand Up @@ -41,7 +41,7 @@ def __init__(
self._telegram = telegram
self._render_service = render_service

def handler(self) -> None:
def handler(self, chat_id: str, thread_id: str | None) -> None:
issue = self._github.get_issue()

labels = self._render_service.format_labels(issue.labels)
Expand All @@ -54,8 +54,19 @@ def handler(self) -> None:
base_url="https://github.com",
)

for e in render_result.entities:
e.pop("language", None)

tg_body=TgPayload(
text=render_result.text,
entities=render_result.entities,
disable_web_page_preview=True,
chat_id=chat_id,
message_thread_id=thread_id,
)

if len(render_result.text) <= TG_MESSAGE_LIMIT:
return self._telegram.send_message(render_result)
return self._telegram.send_message(tg_body)

message_without_description = self._create_message(issue, "<p></p>", labels)

Expand Down Expand Up @@ -90,7 +101,7 @@ def __init__(
self._telegram = telegram
self._render_service = render_service

def handler(self) -> None:
def handler(self, chat_id: str, thread_id: str | None) -> None:
pr = self._github.get_pull_request()

labels = self._render_service.format_labels(pr.labels)
Expand All @@ -103,8 +114,19 @@ def handler(self) -> None:
base_url="https://github.com",
)

for e in render_result.entities:
e.pop("language", None)

tg_body = TgPayload(
text=render_result.text,
entities=render_result.entities,
disable_web_page_preview=True,
chat_id=chat_id,
message_thread_id=thread_id,
)

if len(render_result.text) <= TG_MESSAGE_LIMIT:
return self._telegram.send_message(render_result)
return self._telegram.send_message(tg_body)

message_without_description = self._create_message(pr, "<p></p>", labels)

Expand Down
5 changes: 2 additions & 3 deletions notifier/application/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import abc
import typing

import sulguk

from notifier.domain.entities import PullRequest, Issue
from notifier.infrastructure.telegram_gateway import TgPayload


class Github(typing.Protocol):
Expand All @@ -16,4 +15,4 @@ def get_pull_request(self) -> PullRequest: ...

class Telegram(typing.Protocol):
@abc.abstractmethod
def send_message(self, render_result: sulguk.RenderResult) -> None: ...
def send_message(self, body: TgPayload) -> typing.Any: ...
130 changes: 81 additions & 49 deletions notifier/infrastructure/github_gateway.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,89 @@
import requests
import os
from typing import Any

from adaptix import P, Retort, loader, name_mapping, Chain
from descanso import RestBuilder
from descanso import request_transformers as rt
from descanso.http.requests import RequestsClient
from requests import Session

from notifier.application import interfaces
from notifier.domain.entities import Issue, PullRequest


class GithubGateway(interfaces.Github):
def __init__(self, token: str, event_url: str) -> None:
def body_pre_loader(data: dict[str, Any]) -> dict[str, Any]:
if "body_html" not in data:
data["body_html"] = ""
return data


issue_recipe = [
name_mapping(
Issue,
map={
"id": "number",
"url": "html_url",
"user": ["user", "login"],
"body": "body_html",
},
),
loader(P[Issue], body_pre_loader, Chain.FIRST),
loader(P[Issue].labels, lambda labels: [label["name"] for label in labels]),
]

pr_recipe = [
name_mapping(
PullRequest,
map={
"id": "number",
"url": "html_url",
"user": ["user", "login"],
"head_ref": ["head", "label"],
"base_ref": ["base", "ref"],
"repository": ["base", "repo", "full_name"],
"body": "body_html",
},
),
loader(P[PullRequest], body_pre_loader, Chain.FIRST),
loader(P[PullRequest].labels, lambda labels: [label["name"] for label in labels]),
]

gh_recipe = [
*issue_recipe,
*pr_recipe,

]

rest = RestBuilder(
request_body_dumper=Retort(),
response_body_loader=Retort(recipe=gh_recipe),
query_param_dumper=Retort(),
)


class GithubGateway(RequestsClient, interfaces.Github):
def __init__(
self,
token: str,
event_url: str,
session: Session | None = None
) -> None:
self._token = token
self._url = event_url

def get_issue(self) -> Issue:
headers = {
"Accept": "application/vnd.github.v3.html+json",
"X-GitHub-Api-Version": "2022-11-28",
"Authorization": f"Bearer {self._token}",
}

response = requests.get(self._url, headers=headers, timeout=30)
response.raise_for_status()

data = response.json()

return Issue(
id=data["number"],
title=data["title"],
labels=[label["name"] for label in data["labels"]],
url=(data["html_url"] or "").strip(),
user=data["user"]["login"],
body=(data.get("body_html", "") or "").strip(),
self._event_url = event_url
super().__init__(
"",
session or Session(),
(
rt.Header("Accept", "application/vnd.github.v3.html+json"),
rt.Header("X-GitHub-Api-Version", "2022-11-28"),
rt.Header("Authorization", f"Bearer {self._token}"),
),
)

def get_pull_request(self) -> PullRequest:
headers = {
"Accept": "application/vnd.github.v3.html+json",
"X-GitHub-Api-Version": "2022-11-28",
"Authorization": f"Bearer {self._token}",
}

response = requests.get(self._url, headers=headers, timeout=30)
response.raise_for_status()

data = response.json()

return PullRequest(
id=data["number"],
title=data["title"],
labels=[label["name"] for label in data["labels"]],
url=data["html_url"],
user=data["user"]["login"],
body=(data.get("body_html", "") or "").strip(),
additions=data["additions"],
deletions=data["deletions"],
head_ref=data["head"]["label"],
base_ref=data["base"]["ref"],
repository=data["base"]["repo"]["full_name"],
)
@rest.get("{self._event_url}")
def get_issue(self) -> Issue: # type: ignore[empty-body]
pass

@rest.get("{self._event_url}")
def get_pull_request(self) -> PullRequest: # type: ignore[empty-body]
pass
82 changes: 37 additions & 45 deletions notifier/infrastructure/telegram_gateway.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,45 @@
import sys
import time

import requests
from dataclasses import dataclass
from typing import Any

from adaptix import Retort
from requests import Session
from requests.adapters import HTTPAdapter
from descanso import RestBuilder
from descanso.http.requests import RequestsClient
import sulguk

from notifier.application import interfaces

rest = RestBuilder(
request_body_dumper=Retort(),
response_body_loader=Retort(),
query_param_dumper=Retort(),
)


@dataclass
class TgPayload:
text: str
entities: list[sulguk.data.MessageEntity]
disable_web_page_preview: bool
chat_id: str
message_thread_id: str | None


class TelegramGateway(RequestsClient, interfaces.Telegram):

class TelegramGateway(interfaces.Telegram):
def __init__(
self,
chat_id: str,
bot_token: str,
attempt_count: int,
message_thread_id: str | int | None,
token: str,
attemp_count: int,
base_url: str = "https://api.telegram.org",
session: Session | None = None,
) -> None:
self._chat_id = chat_id
self._bot_token = bot_token
self._attempt_count = attempt_count
self._message_thread_id = message_thread_id

def send_message(self, render_result: sulguk.RenderResult) -> None:
count = 0
payload = self._create_payload(render_result)
url = f"https://api.telegram.org/bot{self._bot_token}/sendMessage"
while count < self._attempt_count:
response = requests.post(url, json=payload, timeout=30)
try:
response.raise_for_status()
except requests.exceptions.HTTPError:
print(response.content, file=sys.stderr)
count += 1
time.sleep(count * 2)
else:
print(response.json(), file=sys.stdout)
return

def _create_payload(self, render_result: sulguk.RenderResult) -> dict:
for e in render_result.entities:
e.pop("language", None)

payload = {
"text": render_result.text,
"entities": render_result.entities,
"disable_web_page_preview": True,
}
payload["chat_id"] = self._chat_id

if self._message_thread_id is not None:
payload["message_thread_id"] = self._message_thread_id

return payload
self._token = token
tg_session = session or Session()
tg_session.mount("https", HTTPAdapter(max_retries=attemp_count))
super().__init__(base_url, tg_session)

@rest.post("/bot{self._token}/sendMessage")
def send_message(self, body: interfaces.TgPayload) -> Any: # type: ignore[empty-body]
pass
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ sulguk==0.10.1
requests==2.32.5
beautifulsoup4==4.14.2
lxml==6.0.2
descanso==0.7.1
adaptix==3.0.0b11