diff --git a/action.yml b/action.yml index e866699..b811bc7 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,9 @@ inputs: tg-message-thread-id: description: "Telegram Message Thread ID" required: false + tg-message-limit: + description: "Telegram Message limit" + required: false discord-webhook-url: description: "Discord Webhook URL" required: false @@ -63,6 +66,7 @@ runs: TELEGRAM_BOT_TOKEN: ${{ inputs.tg-bot-token }} TELEGRAM_CHAT_ID: ${{ inputs.tg-chat-id }} TELEGRAM_MESSAGE_THREAD_ID: ${{ inputs.tg-message-thread-id }} + TELEGRAM_MESSAGE_LIMIT: ${{ inputs.tg-message-limit }} DISCORD_WEBHOOK_URL: ${{ inputs.discord-webhook-url }} DISCORD_THREAD_ID: ${{ inputs.discord-thread-id }} GITHUB_TOKEN: ${{ inputs.github-token }} @@ -78,4 +82,4 @@ runs: branding: icon: "message-circle" - color: "blue" + color: "blue" \ No newline at end of file diff --git a/notifier/__main__.py b/notifier/__main__.py index a24ddbc..7fdb73c 100644 --- a/notifier/__main__.py +++ b/notifier/__main__.py @@ -8,7 +8,7 @@ from notifier.application.services import RenderService from notifier.infrastructure.discord_gateway import DiscordGateway from notifier.infrastructure.github_gateway import GithubGateway -from notifier.infrastructure.telegram_gateway import TelegramGateway +from notifier.infrastructure.telegram_gateway import TelegramGateway, TG_MESSAGE_LIMIT_DEFAULT def get_interactor(url: str) -> type[SendIssue] | type[SendPR]: @@ -55,6 +55,7 @@ def get_interactor(url: str) -> type[SendIssue] | type[SendPR]: attempt_count=int(os.environ.get("ATTEMPT_COUNT", "2")), message_thread_id=os.environ.get("TELEGRAM_MESSAGE_THREAD_ID"), custom_template=html_template, + tg_message_limit=os.environ.get('TELEGRAM_MESSAGE_LIMIT') or TG_MESSAGE_LIMIT_DEFAULT ) notifiers.append(telegram_gateway) @@ -85,4 +86,4 @@ def get_interactor(url: str) -> type[SendIssue] | type[SendPR]: except Exception as e: traceback.print_exc(file=sys.stderr) print(f"Error processing event: {e}", file=sys.stderr) - sys.exit(1) + sys.exit(1) \ No newline at end of file diff --git a/notifier/infrastructure/telegram_gateway.py b/notifier/infrastructure/telegram_gateway.py index f6696d8..cc250a9 100644 --- a/notifier/infrastructure/telegram_gateway.py +++ b/notifier/infrastructure/telegram_gateway.py @@ -5,8 +5,9 @@ from notifier.application import interfaces from notifier.domain.entities import Issue, PullRequest from notifier.infrastructure.send_weebhook import send_webhook +from notifier.infrastructure.truncate_html import TruncateHTML -TG_MESSAGE_LIMIT: typing.Final = 4096 +TG_MESSAGE_LIMIT_DEFAULT: typing.Final = 4096 ISSUE_TEMPLATE: typing.Final = ( "🚀 New issue to {repository} by @{user}
" @@ -33,6 +34,7 @@ def __init__( chat_id: str, bot_token: str, attempt_count: int, + tg_message_limit: int, message_thread_id: str | int | None = None, custom_template: str = "", ) -> None: @@ -41,6 +43,8 @@ def __init__( self._attempt_count = attempt_count self._message_thread_id = message_thread_id self._custom_template = custom_template + self._tg_message_limit = tg_message_limit + def send_issue( self, @@ -48,14 +52,17 @@ def send_issue( formatted_body: str, formatted_labels: str, ) -> None: - message = self._create_issue_message(issue, formatted_body, formatted_labels) - render_result = sulguk.transform_html(message, base_url="https://github.com") - if len(render_result.text) > TG_MESSAGE_LIMIT: - message = self._create_issue_message(issue, "

", formatted_labels) - render_result = sulguk.transform_html( - message, base_url="https://github.com" - ) + message = self._create_issue_message( + issue=issue, + body=formatted_body, + labels=formatted_labels + ) + render_result = sulguk.transform_html( + message, base_url="https://github.com" + ) + + send_webhook( payload=self._create_payload(render_result), url=f"https://api.telegram.org/bot{self._bot_token}/sendMessage", @@ -73,12 +80,6 @@ def send_pull_request( ) render_result = sulguk.transform_html(message, base_url="https://github.com") - if len(render_result.text) > TG_MESSAGE_LIMIT: - message = self._create_pr_message(pull_request, "

", formatted_labels) - render_result = sulguk.transform_html( - message, base_url="https://github.com" - ) - send_webhook( payload=self._create_payload(render_result), url=f"https://api.telegram.org/bot{self._bot_token}/sendMessage", @@ -94,6 +95,7 @@ def _create_payload(self, render_result: sulguk.RenderResult) -> dict: "entities": render_result.entities, "disable_web_page_preview": True, } + payload["chat_id"] = self._chat_id if self._message_thread_id is not None: @@ -101,9 +103,32 @@ def _create_payload(self, render_result: sulguk.RenderResult) -> dict: return payload + + def _create_message_with_limit( + self, + template: str, + payload: dict, + ): + + message = template.format(**payload) + message_length = len(message) + body = payload["body"] + truncate_html = TruncateHTML() + + if message_length > self._tg_message_limit: + max_length_body = len(body) - (message_length - self._tg_message_limit) + payload['body'] = truncate_html.render( + raw_html=body, + max_length=max_length_body, + ) + return template.format(**payload) + + return message + + def _create_issue_message(self, issue: Issue, body: str, labels: str) -> str: template = self._custom_template or ISSUE_TEMPLATE - return template.format( + payload = dict( id=issue.id, user=issue.user, title=issue.title, @@ -111,13 +136,19 @@ def _create_issue_message(self, issue: Issue, body: str, labels: str) -> str: url=issue.url, body=body, repository=issue.repository, - promo="sent via relator", + promo="sent via relator" + ) + + return self._create_message_with_limit( + template=template, + payload=payload, ) def _create_pr_message(self, pr: PullRequest, body: str, labels: str) -> str: """Create HTML message for pull request""" template = self._custom_template or PR_TEMPLATE - return template.format( + + payload = dict( id=pr.id, user=pr.user, title=pr.title, @@ -131,3 +162,8 @@ def _create_pr_message(self, pr: PullRequest, body: str, labels: str) -> str: base_ref=pr.base_ref, promo="sent via relator", ) + + return self._create_message_with_limit( + template=template, + payload=payload + ) \ No newline at end of file diff --git a/notifier/infrastructure/truncate_html.py b/notifier/infrastructure/truncate_html.py new file mode 100644 index 0000000..c621436 --- /dev/null +++ b/notifier/infrastructure/truncate_html.py @@ -0,0 +1,69 @@ +from dataclasses import field, dataclass + +from bs4 import BeautifulSoup, NavigableString, Tag + + +@dataclass +class TruncateHtmlState: + count: int = field(default=0) + done: bool = field(default=False) + + +class TruncateHTML: + def __init__(self): + self._ellipsis = "..." + + def _walk( + self, + node: Tag | NavigableString, + state: TruncateHtmlState, + max_length: int + ) -> bool: + if isinstance(node, NavigableString): + text = node.text + if state.count + len(text) <= max_length: + state.count += len(text) + return + + remaining = max_length - state.count + if remaining <= 0: + node.extract() + state.done = True + return + + cut_text = text[:remaining].rstrip() + self._ellipsis + node.replace_with(cut_text) + state.done = True + return + + if isinstance(node, Tag): + children = list(node.contents) + for i, child in enumerate(children): + self._walk(child, state, max_length) + if state.done: + tags_deleted = children[i + 1:] + for tag_deleted in tags_deleted: + tag_deleted.extract() + break + + def render( + self, + raw_html: str, + max_length: int + ) -> str: + + soup = BeautifulSoup(raw_html, "html.parser") + state = TruncateHtmlState( + count=0 + ) + + for node in soup.contents: + self._walk( + node=node, + state=state, + max_length=max_length + ) + if state.done: + break + + return str(soup)