mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-31 05:43:41 +00:00 
			
		
		
		
	add telegram integraion
This commit is contained in:
		| @ -114,6 +114,17 @@ Section name must be either `html` (plus optional architecture name, e.g. `html: | ||||
| * `link_path` - prefix for HTML links, string, required. | ||||
| * `template_path` - path to Jinja2 template, string, required. | ||||
|  | ||||
| ### `telegram` type | ||||
|  | ||||
| Section name must be either `telegram` (plus optional architecture name, e.g. `telegram:x86_64`) or random name with `type` set. | ||||
|  | ||||
| * `type` - type of the report, string, optional, must be set to `telegram` if exists. | ||||
| * `api_key` - telegram bot API key, string, required. Please refer FAQ about how to create chat and bot | ||||
| * `chat_id` - telegram chat id, either string with `@` or integer value, required. | ||||
| * `homepage` - link to homepage, string, optional. | ||||
| * `link_path` - prefix for HTML links, string, required. | ||||
| * `template_path` - path to Jinja2 template, string, required. | ||||
|  | ||||
| ## `upload` group | ||||
|  | ||||
| Remote synchronization settings. | ||||
|  | ||||
							
								
								
									
										40
									
								
								docs/faq.md
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								docs/faq.md
									
									
									
									
									
								
							| @ -402,6 +402,46 @@ There are several choices: | ||||
|     | ||||
| After these steps `index.html` file will be automatically synced to S3 | ||||
|  | ||||
| ### I would like to get messages to my telegram account/channel | ||||
|  | ||||
| 1. It still requires additional dependencies: | ||||
|  | ||||
|    ```shell | ||||
|    yay -S python-jinja | ||||
|    ``` | ||||
|  | ||||
| 2. Register bot in telegram. You can do it by using by talking with [@BotFather](https://t.me/botfather). For more details please refer to [official documentation](https://core.telegram.org/bots). | ||||
|  | ||||
| 3. Optionally (if you want to post message in chat): | ||||
|  | ||||
|    1. Create telegram channel.  | ||||
|    2. Invite your bot into the channel. | ||||
|    3. Make your channel public | ||||
|  | ||||
| 4. Get chat id if you want to use by numerical id or just use id prefixed with `@` (e.g. `@ahriman`). If you are not using chat the chat id is your user id. If you don't want to make channel public you can use [this guide](https://stackoverflow.com/a/33862907). | ||||
|  | ||||
| 5. Configure the service: | ||||
|  | ||||
|    ```ini | ||||
|    [report] | ||||
|    target = telegram | ||||
|     | ||||
|    [telegram] | ||||
|    api_key = aaAAbbBBccCC | ||||
|    chat_id = @ahriman | ||||
|    link_path = http://example.com/x86_64 | ||||
|    ``` | ||||
|     | ||||
|    `api_key` is the one sent by [@BotFather](https://t.me/botfather), `chat_id` is the value retrieved from previous step. | ||||
|  | ||||
| If you did everything fine you should receive the message with the next update. Quick credentials check can be done by using the following command: | ||||
|  | ||||
| ```shell | ||||
| curl 'https://api.telegram.org/bot${CHAT_ID}/sendMessage?chat_id=${API_KEY}&text=hello' | ||||
| ``` | ||||
|  | ||||
| (replace `${CHAT_ID}` and `${API_KEY}` with the values from configuration). | ||||
|  | ||||
| ## Web service | ||||
|  | ||||
| ### Readme mentions web interface, how do I use it? | ||||
|  | ||||
| @ -45,6 +45,9 @@ ssl = disabled | ||||
| [html] | ||||
| template_path = /usr/share/ahriman/templates/repo-index.jinja2 | ||||
|  | ||||
| [telegram] | ||||
| template_path = /usr/share/ahriman/templates/telegram.jinja2 | ||||
|  | ||||
| [upload] | ||||
| target = | ||||
|  | ||||
|  | ||||
							
								
								
									
										10
									
								
								package/share/ahriman/templates/telegram-index.jinja2
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								package/share/ahriman/templates/telegram-index.jinja2
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| {#simplified version of full report, now in markdown#} | ||||
| ## {{ repository }} update | ||||
|  | ||||
| {% for package in packages %} | ||||
|     * [{{ package.name }}]({{ link_path }}/{{ package.filename }}) {{ package.version }} at {{ package.build_date }} | ||||
| {% endfor %} | ||||
|  | ||||
| {% if homepage is not none %} | ||||
| [Repository]({{ homepage }}) | ||||
| {% endif %} | ||||
| @ -68,6 +68,9 @@ class Report: | ||||
|         if provider == ReportSettings.Console: | ||||
|             from ahriman.core.report.console import Console | ||||
|             return Console(architecture, configuration, section) | ||||
|         if provider == ReportSettings.Telegram: | ||||
|             from ahriman.core.report.telegram import Telegram | ||||
|             return Telegram(architecture, configuration, section) | ||||
|         return cls(architecture, configuration)  # should never happen | ||||
|  | ||||
|     def generate(self, packages: Iterable[Package], result: Result) -> None: | ||||
|  | ||||
							
								
								
									
										93
									
								
								src/ahriman/core/report/telegram.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/ahriman/core/report/telegram.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | ||||
| # | ||||
| # Copyright (c) 2021-2022 ahriman team. | ||||
| # | ||||
| # This file is part of ahriman | ||||
| # (see https://github.com/arcan1s/ahriman). | ||||
| # | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| # technically we could use python-telegram-bot, but it is just a single request, cmon | ||||
| import requests | ||||
|  | ||||
| from typing import Iterable | ||||
|  | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.report.jinja_template import JinjaTemplate | ||||
| from ahriman.core.report.report import Report | ||||
| from ahriman.core.util import exception_response_text | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.result import Result | ||||
|  | ||||
|  | ||||
| class Telegram(Report, JinjaTemplate): | ||||
|     """ | ||||
|     telegram report generator | ||||
|     :cvar TELEGRAM_API_URL: telegram api base url | ||||
|     :cvar TELEGRAM_MAX_CONTENT_LENGTH: max content length of the message | ||||
|     :ivar api_key: bot api key | ||||
|     :ivar chat_id: chat id to post message, either string with @ or integer | ||||
|     :ivar template_path: path to template for built packages | ||||
|     """ | ||||
|  | ||||
|     TELEGRAM_API_URL = "https://api.telegram.org" | ||||
|     TELEGRAM_MAX_CONTENT_LENGTH = 4096 | ||||
|  | ||||
|     def __init__(self, architecture: str, configuration: Configuration, section: str) -> None: | ||||
|         """ | ||||
|         default constructor | ||||
|         :param architecture: repository architecture | ||||
|         :param configuration: configuration instance | ||||
|         :param section: settings section name | ||||
|         """ | ||||
|         Report.__init__(self, architecture, configuration) | ||||
|         JinjaTemplate.__init__(self, section, configuration) | ||||
|  | ||||
|         self.api_key = configuration.get(section, "api_key") | ||||
|         self.chat_id = configuration.get(section, "chat_id") | ||||
|         self.template_path = configuration.getpath(section, "template_path") | ||||
|  | ||||
|     def _send(self, text: str) -> None: | ||||
|         """ | ||||
|         send message to telegram channel | ||||
|         :param text: message body text | ||||
|         """ | ||||
|         try: | ||||
|             response = requests.post( | ||||
|                 f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage", | ||||
|                 data={"chat_id": self.chat_id, "text": text}) | ||||
|             response.raise_for_status() | ||||
|         except requests.HTTPError as e: | ||||
|             self.logger.exception("could not perform request: %s", exception_response_text(e)) | ||||
|             raise | ||||
|         except Exception: | ||||
|             self.logger.exception("could not perform request") | ||||
|             raise | ||||
|  | ||||
|     def generate(self, packages: Iterable[Package], result: Result) -> None: | ||||
|         """ | ||||
|         generate report for the specified packages | ||||
|         :param packages: list of packages to generate report | ||||
|         :param result: build result | ||||
|         """ | ||||
|         if not result.success: | ||||
|             return | ||||
|         text = self.make_html(result, self.template_path) | ||||
|         # telegram content is limited by 4096 symbols, so we are going to split the message by new lines | ||||
|         # to fit into this restriction | ||||
|         if len(text) > self.TELEGRAM_MAX_CONTENT_LENGTH: | ||||
|             position = text.rfind("\n", 0, self.TELEGRAM_MAX_CONTENT_LENGTH) | ||||
|             portion, text = text[:position], text[position + 1:]  # +1 to exclude newline we split | ||||
|             self._send(portion) | ||||
|         # send remaining (or full in case if size is less than max length) text | ||||
|         self._send(text) | ||||
| @ -32,12 +32,14 @@ class ReportSettings(Enum): | ||||
|     :cvar HTML: html report generation | ||||
|     :cvar Email: email report generation | ||||
|     :cvar Console: print result to console | ||||
|     :cvar Telegram: markdown report to telegram channel | ||||
|     """ | ||||
|  | ||||
|     Disabled = "disabled"  # for testing purpose | ||||
|     HTML = "html" | ||||
|     Email = "email" | ||||
|     Console = "console" | ||||
|     Telegram = "telegram" | ||||
|  | ||||
|     @classmethod | ||||
|     def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings: | ||||
| @ -52,4 +54,6 @@ class ReportSettings(Enum): | ||||
|             return cls.Email | ||||
|         if value.lower() in ("console",): | ||||
|             return cls.Console | ||||
|         if value.lower() in ("telegram",): | ||||
|             return cls.Telegram | ||||
|         raise InvalidOption(value) | ||||
|  | ||||
| @ -53,3 +53,12 @@ def test_report_html(configuration: Configuration, result: Result, mocker: Mocke | ||||
|     report_mock = mocker.patch("ahriman.core.report.html.HTML.generate") | ||||
|     Report.load("x86_64", configuration, "html").run([], result) | ||||
|     report_mock.assert_called_once_with([], result) | ||||
|  | ||||
|  | ||||
| def test_report_telegram(configuration: Configuration, result: Result, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must generate telegram report | ||||
|     """ | ||||
|     report_mock = mocker.patch("ahriman.core.report.telegram.Telegram.generate") | ||||
|     Report.load("x86_64", configuration, "telegram").run([], result) | ||||
|     report_mock.assert_called_once_with([], result) | ||||
|  | ||||
							
								
								
									
										83
									
								
								tests/ahriman/core/report/test_telegram.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								tests/ahriman/core/report/test_telegram.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | ||||
| import pytest | ||||
| import requests | ||||
|  | ||||
| from pytest_mock import MockerFixture | ||||
| from unittest import mock | ||||
|  | ||||
| from ahriman.core.configuration import Configuration | ||||
| from ahriman.core.report.telegram import Telegram | ||||
| from ahriman.models.package import Package | ||||
| from ahriman.models.result import Result | ||||
|  | ||||
|  | ||||
| def test_send(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must send a message | ||||
|     """ | ||||
|     request_mock = mocker.patch("requests.post") | ||||
|     report = Telegram("x86_64", configuration, "telegram") | ||||
|  | ||||
|     report._send("a text") | ||||
|     request_mock.assert_called_once_with( | ||||
|         pytest.helpers.anyvar(str, strict=True), | ||||
|         data={"chat_id": pytest.helpers.anyvar(str, strict=True), "text": "a text"}) | ||||
|  | ||||
|  | ||||
| def test_send_failed(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must reraise generic exception | ||||
|     """ | ||||
|     mocker.patch("requests.post", side_effect=Exception()) | ||||
|     report = Telegram("x86_64", configuration, "telegram") | ||||
|  | ||||
|     with pytest.raises(Exception): | ||||
|         report._send("a text") | ||||
|  | ||||
|  | ||||
| def test_make_request_failed_http_error(configuration: Configuration, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must reraise http exception | ||||
|     """ | ||||
|     mocker.patch("requests.post", side_effect=requests.exceptions.HTTPError()) | ||||
|     report = Telegram("x86_64", configuration, "telegram") | ||||
|  | ||||
|     with pytest.raises(requests.exceptions.HTTPError): | ||||
|         report._send("a text") | ||||
|  | ||||
|  | ||||
| def test_generate(configuration: Configuration, package_ahriman: Package, result: Result, | ||||
|                   mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must generate report | ||||
|     """ | ||||
|     send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") | ||||
|  | ||||
|     report = Telegram("x86_64", configuration, "telegram") | ||||
|     report.generate([package_ahriman], result) | ||||
|     send_mock.assert_called_once_with(pytest.helpers.anyvar(int)) | ||||
|  | ||||
|  | ||||
| def test_generate_big_text(configuration: Configuration, package_ahriman: Package, result: Result, | ||||
|                            mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must generate report with big text | ||||
|     """ | ||||
|     mocker.patch("ahriman.core.report.jinja_template.JinjaTemplate.make_html", return_value="a\n" * 4096) | ||||
|     send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") | ||||
|  | ||||
|     report = Telegram("x86_64", configuration, "telegram") | ||||
|     report.generate([package_ahriman], result) | ||||
|     send_mock.assert_has_calls([ | ||||
|         mock.call(pytest.helpers.anyvar(str, strict=True)), mock.call(pytest.helpers.anyvar(str, strict=True)) | ||||
|     ]) | ||||
|  | ||||
|  | ||||
| def test_generate_no_empty(configuration: Configuration, package_ahriman: Package, mocker: MockerFixture) -> None: | ||||
|     """ | ||||
|     must generate report | ||||
|     """ | ||||
|     send_mock = mocker.patch("ahriman.core.report.telegram.Telegram._send") | ||||
|  | ||||
|     report = Telegram("x86_64", configuration, "telegram") | ||||
|     report.generate([package_ahriman], Result()) | ||||
|     send_mock.assert_not_called() | ||||
| @ -329,6 +329,7 @@ def test_walk(resource_path_root: Path) -> None: | ||||
|         resource_path_root / "web" / "templates" / "build-status.jinja2", | ||||
|         resource_path_root / "web" / "templates" / "email-index.jinja2", | ||||
|         resource_path_root / "web" / "templates" / "repo-index.jinja2", | ||||
|         resource_path_root / "web" / "templates" / "telegram-index.jinja2", | ||||
|     ]) | ||||
|     local_files = list(sorted(walk(resource_path_root))) | ||||
|     assert local_files == expected | ||||
|  | ||||
| @ -24,3 +24,6 @@ def test_from_option_valid() -> None: | ||||
|  | ||||
|     assert ReportSettings.from_option("console") == ReportSettings.Console | ||||
|     assert ReportSettings.from_option("conSOle") == ReportSettings.Console | ||||
|  | ||||
|     assert ReportSettings.from_option("telegram") == ReportSettings.Telegram | ||||
|     assert ReportSettings.from_option("TElegraM") == ReportSettings.Telegram | ||||
|  | ||||
| @ -52,6 +52,13 @@ homepage = | ||||
| link_path = | ||||
| template_path = ../web/templates/repo-index.jinja2 | ||||
|  | ||||
| [telegram] | ||||
| api_key = apikey | ||||
| chat_id = @ahrimantestchat | ||||
| homepage = | ||||
| link_path = | ||||
| template_path = ../web/templates/telegram-index.jinja2 | ||||
|  | ||||
| [upload] | ||||
| target = | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user