mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
add telegram integraion
This commit is contained in:
parent
b8e17c4879
commit
1a83e55d64
@ -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.
|
* `link_path` - prefix for HTML links, string, required.
|
||||||
* `template_path` - path to Jinja2 template, 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
|
## `upload` group
|
||||||
|
|
||||||
Remote synchronization settings.
|
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
|
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
|
## Web service
|
||||||
|
|
||||||
### Readme mentions web interface, how do I use it?
|
### Readme mentions web interface, how do I use it?
|
||||||
|
@ -45,6 +45,9 @@ ssl = disabled
|
|||||||
[html]
|
[html]
|
||||||
template_path = /usr/share/ahriman/templates/repo-index.jinja2
|
template_path = /usr/share/ahriman/templates/repo-index.jinja2
|
||||||
|
|
||||||
|
[telegram]
|
||||||
|
template_path = /usr/share/ahriman/templates/telegram.jinja2
|
||||||
|
|
||||||
[upload]
|
[upload]
|
||||||
target =
|
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:
|
if provider == ReportSettings.Console:
|
||||||
from ahriman.core.report.console import Console
|
from ahriman.core.report.console import Console
|
||||||
return Console(architecture, configuration, section)
|
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
|
return cls(architecture, configuration) # should never happen
|
||||||
|
|
||||||
def generate(self, packages: Iterable[Package], result: Result) -> None:
|
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 HTML: html report generation
|
||||||
:cvar Email: email report generation
|
:cvar Email: email report generation
|
||||||
:cvar Console: print result to console
|
:cvar Console: print result to console
|
||||||
|
:cvar Telegram: markdown report to telegram channel
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Disabled = "disabled" # for testing purpose
|
Disabled = "disabled" # for testing purpose
|
||||||
HTML = "html"
|
HTML = "html"
|
||||||
Email = "email"
|
Email = "email"
|
||||||
Console = "console"
|
Console = "console"
|
||||||
|
Telegram = "telegram"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
|
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
|
||||||
@ -52,4 +54,6 @@ class ReportSettings(Enum):
|
|||||||
return cls.Email
|
return cls.Email
|
||||||
if value.lower() in ("console",):
|
if value.lower() in ("console",):
|
||||||
return cls.Console
|
return cls.Console
|
||||||
|
if value.lower() in ("telegram",):
|
||||||
|
return cls.Telegram
|
||||||
raise InvalidOption(value)
|
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_mock = mocker.patch("ahriman.core.report.html.HTML.generate")
|
||||||
Report.load("x86_64", configuration, "html").run([], result)
|
Report.load("x86_64", configuration, "html").run([], result)
|
||||||
report_mock.assert_called_once_with([], 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" / "build-status.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
resource_path_root / "web" / "templates" / "email-index.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "repo-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)))
|
local_files = list(sorted(walk(resource_path_root)))
|
||||||
assert local_files == expected
|
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("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 =
|
link_path =
|
||||||
template_path = ../web/templates/repo-index.jinja2
|
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]
|
[upload]
|
||||||
target =
|
target =
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user