Razem Mattermost Bot
razem-mattermost-bot to mała biblioteka do szybkiego pisania botów Mattermost dla Partii Razem. Opiera się na mattermostdriver, ale usuwa powtarzalny boilerplate z naszych botów: konfigurację hosta i tokenu, patch SSL dla websocketów, logowanie, filtrowanie eventów, odpowiedzi w wątkach, reakcje, edycje postów, DM-y, wyszukiwanie użytkowników, paginację API, stan JSON i podstawowe operacje na członkostwie w kanałach.
Domyślny host to https://mattermost.example.org. Token nigdy nie jest zaszyty w kodzie: można przekazać go jawnie albo ustawić MATTERMOST_TOKEN / MATTERMOST_ACCESS_TOKEN.
Instalacja
Lokalnie, podczas pracy nad pakietem:
uv add ../razem-mattermost-bot
Z repozytorium Forgejo:
uv add git+https://github.com/example/razem-mattermost-bot.git
Konfiguracja
Najprostszy .env:
MATTERMOST_TOKEN=tu_wklej_token_bota
Opcjonalnie:
MATTERMOST_HOST=https://mattermost.example.org
W kodzie można też przekazać token bezpośrednio:
from razem_mattermost_bot import RazemMattermostBot
bot = RazemMattermostBot(token="token-z-bezpiecznego-sekretu")
Bardzo prosty bot
Bot odpowiada w wątku na !ping we wszystkich kanałach, do których ma dostęp.
from razem_mattermost_bot import Post, RazemMattermostBot
bot = RazemMattermostBot(name="ping-bot")
@bot.on_trigger("!ping")
def ping(post: Post) -> None:
bot.reply_in_thread(post, "pong")
bot.start()
Słuchanie tylko jednego kanału
Jeśli bot ma reagować tylko na konkretne kanały, podaj ich ID:
from razem_mattermost_bot import Post, RazemMattermostBot
DYZURY_CHANNEL_ID = "abc123kanal"
bot = RazemMattermostBot(name="dyzury-bot")
@bot.on_hashtag("#dyzur", channel_ids={DYZURY_CHANNEL_ID})
async def handle_shift(post: Post) -> None:
bot.reply_in_thread(post, "Zapisane. Dzięki!")
bot.start()
Brak channel_ids oznacza: reaguj na wszystkie kanały, w których Mattermost wyśle event do bota.
Warunek logiczny zamiast komendy
Możesz zarejestrować dowolny warunek:
from razem_mattermost_bot import Post, RazemMattermostBot
bot = RazemMattermostBot(name="uwaga-bot")
def asks_for_help(post: Post) -> bool:
text = post.message.lower()
return "kto pomoże" in text or "potrzebuję pomocy" in text
@bot.on_message(condition=asks_for_help, include_threads=False)
def help_reply(post: Post) -> None:
bot.reply_in_channel(post.channel_id, "Widzę prośbę o pomoc. Oznaczam dyżur.")
bot.start()
Powitanie nowych osób
user_added pozwala śledzić nowe osoby w kanałach i wysyłać im DM-y albo odpowiedź na kanale.
from razem_mattermost_bot import MattermostEvent, RazemMattermostBot
START_CHANNEL_ID = "kanal-startowy"
bot = RazemMattermostBot(name="welcome-bot")
@bot.on_user_added(channel_ids={START_CHANNEL_ID})
def welcome(event: MattermostEvent) -> None:
if event.user_id:
bot.send_dm(
event.user_id,
"Cześć! Tu kilka linków na start: ...",
)
bot.start()
Dodawanie i usuwanie osób z kanału
Użytkownika można znaleźć po ID, mailu albo nazwie:
from razem_mattermost_bot import Post, RazemMattermostBot
bot = RazemMattermostBot(name="channel-admin-bot")
@bot.on_trigger("!dodaj")
def add_person(post: Post) -> None:
parts = post.message.split(maxsplit=2)
if len(parts) < 3:
bot.reply_in_thread(post, "Użycie: `!dodaj @nick kanal_id`")
return
user = bot.find_user(parts[1])
channel_id = parts[2]
bot.add_user_to_channel(channel_id, user["id"])
bot.reply_in_thread(post, f"Dodano {parts[1]} do kanału.")
bot.start()
Analogicznie:
bot.remove_user_from_channel(channel_id, user["id"])
Coś bardziej kompletnego: prosty bot zapisów
Ten przykład reaguje na hashtag, zapisuje chętnych przez odpowiedź w wątku i potwierdza DM-em.
from razem_mattermost_bot import Post, RazemMattermostBot
bot = RazemMattermostBot(name="zapisy-bot")
events: dict[str, set[str]] = {}
@bot.on_hashtag("#zapisy", include_threads=False)
def create_signup(post: Post) -> None:
events[post.id] = set()
bot.reply_in_thread(
post,
"Utworzono listę zapisów. Odpowiedz w tym wątku, żeby się zapisać.",
)
@bot.on_message()
def signup_reply(post: Post) -> None:
if not post.root_id or post.root_id not in events:
return
events[post.root_id].add(post.user_id)
bot.reply_in_thread(post, "Zapisane.")
bot.send_dm(post.user_id, "Potwierdzam zapis.")
bot.start()
Reakcje: tablica zapisów emoji
Wzór z emojibot: jedna mapa emoji -> kanał, handler na reakcje i opcjonalny skan istniejących reakcji.
from razem_mattermost_bot import Reaction, RazemMattermostBot
BOARD_POST_ID = "post_z_tablica"
EMOJI_TO_CHANNEL = {
"art": "kanal-grafiki",
"newspaper": "kanal-mediow",
}
bot = RazemMattermostBot(name="emoji-board")
@bot.on_reaction_added(*EMOJI_TO_CHANNEL.keys(), post_ids={BOARD_POST_ID})
def join_from_reaction(reaction: Reaction) -> None:
channel_id = EMOJI_TO_CHANNEL[reaction.emoji_name]
bot.add_user_to_channel(channel_id, reaction.user_id)
bot.start()
Jeśli bot był wyłączony, można doskanować reakcje na starcie po bot.connect():
bot.connect()
for reaction in bot.get_reactions(BOARD_POST_ID):
emoji = reaction["emoji_name"]
if emoji in EMOJI_TO_CHANNEL:
bot.add_user_to_channel(EMOJI_TO_CHANNEL[emoji], reaction["user_id"])
bot.start()
Edycje postów i mirroring
Wzór z notifier / notifier-sm: bot kopiuje post do innego kanału, zapisuje powiązanie w props, a po edycji oryginału aktualizuje kopię.
from razem_mattermost_bot import Post, RazemMattermostBot
SOURCE = "kanal-zrodlowy"
TARGET = "kanal-docelowy"
bot = RazemMattermostBot(name="mirror-bot")
def format_copy(post: dict, author: dict) -> str:
username = author.get("username", post.get("user_id", "unknown"))
return f"**Zlecenie**: {post['message'].strip()}\n**Osoba**: {username}"
@bot.on_hashtag("#zlecenie", channel_ids={SOURCE}, include_threads=False)
def mirror(post: Post) -> None:
bot.mirror_post(post, target_channel_id=TARGET, message_builder=format_copy)
@bot.on_post_edited(channel_ids={SOURCE}, include_threads=False)
def update_copy(post: Post) -> None:
bot.update_mirror_for_post(post, target_channel_id=TARGET, message_builder=format_copy)
bot.start()
Pobieranie postów i członków kanałów
Paginacja, filtrowanie root-postów, lista członków.
from razem_mattermost_bot import RazemMattermostBot
bot = RazemMattermostBot()
bot.connect()
posts = bot.get_root_posts_for_channel("kanal", since_ms=1760000000000)
member_ids = bot.get_channel_member_user_ids("kanal")
users = bot.get_users_by_ids(member_ids)
get_root_posts_for_channel() odrzuca odpowiedzi w wątkach, usunięte posty i wiadomości systemowe, a potem sortuje od najstarszych.
Stan JSON
Wzór z bingo, emoji-reminder, onboarding-bot, czescbot, wulgarbot: atomowy zapis state.json bez kopiowania helperów.
from razem_mattermost_bot import JsonStateStore
store = JsonStateStore("state.json", default={"seen_user_ids": []})
state = store.load()
state["seen_user_ids"].append("user_id")
store.save(state)
Prosty łańcuch rozmowy
Do botów, które prowadzą użytkownika przez kilka kroków:
from razem_mattermost_bot import ConversationStore, Post, RazemMattermostBot
bot = RazemMattermostBot()
conversations = ConversationStore()
@bot.on_trigger("!start")
def start(post: Post) -> None:
conversations.start(post.user_id, "color")
bot.reply_in_thread(post, "Podaj kolor.")
@bot.on_message()
def continue_flow(post: Post) -> None:
state = conversations.get(post.user_id)
if not state:
return
if state.step == "color":
conversations.advance(post.user_id, "number", color=post.message)
bot.reply_in_thread(post, "Podaj liczbę.")
elif state.step == "number":
color = state.data["color"]
conversations.cancel(post.user_id)
bot.reply_in_thread(post, f"Gotowe: {color}, {post.message}")
bot.start()
Systemd
Pakiet nie uruchamia sudo i nie instaluje usługi samodzielnie, ale potrafi wyrenderować unit systemd dla wdrożenia przez operatora:
from razem_mattermost_bot import SystemdService
service = SystemdService(
name="zapisy-bot",
working_directory="/opt/zapisy-bot",
module="zapisy_bot.main",
env_file="/etc/zapisy-bot.env",
user="mattermost-bot",
)
print(service.render())
ExecStart używa uv run python -m ..., więc wdrożenie może korzystać z lockfile projektu bota.
Logowanie
Każdy RazemMattermostBot konfiguruje spójny format:
[2026-05-21 12:00:00,000] INFO zapisy-bot: event=mattermost_login_ok username='zapisy' user_id='...'
Do własnych zdarzeń:
bot.events.event("signup_created", channel_id=post.channel_id, post_id=post.id)
Dokumentacja API
Docstringi są po angielsku i w stylu zgodnym z mkdocstrings. Dokumentację można wygenerować lokalnie:
uv run --extra docs mkdocs serve
Albo zbudować statycznie:
uv run --extra docs mkdocs build
Pliki konfiguracyjne dokumentacji są w mkdocs.yml i docs/.
Testy
uv run --extra test pytest -q
Metadata
Release files for razem-mattermost-bot 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| razem_mattermost_bot-1.0.1.tar.gz | 63.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| razem_mattermost_bot-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 82.3 kB
Release files / razem_mattermost_bot-1.0.1.tar.gz
| Download URL | razem_mattermost_bot-1.0.1.tar.gz |
|---|---|
| Size | 63.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6e84ac82c431e668a95dcf1ae56617ca3ed9d08ceb85fdcac7c8fd99295f443c
|
|
BLAKE2b-256 checksum How to use checksums |
586124c3a182dd1b7cf9661cdebe315a22c33fa27f036b951c6172388fad268c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / razem_mattermost_bot-1.0.1-py3-none-any.whl
| Download URL | razem_mattermost_bot-1.0.1-py3-none-any.whl |
|---|---|
| Size | 19.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
086f4b880014dab8d39f6fc4c61a4293cf909e3fc1c1c8612dd23760095e21fb
|
|
BLAKE2b-256 checksum How to use checksums |
356b3857b69b4cff69fac6bff7b4744c9465fb95ffb6ca7009e001f36569ff4a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|