Skip to main content

Helpers for writing your organization Mattermost bots with mattermostdriver.

Project description

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

razem_mattermost_bot-1.0.1.tar.gz (63.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

razem_mattermost_bot-1.0.1-py3-none-any.whl (19.0 kB view details)

Uploaded Python 3

File details

Details for the file razem_mattermost_bot-1.0.1.tar.gz.

File metadata

  • Download URL: razem_mattermost_bot-1.0.1.tar.gz
  • Upload date:
  • Size: 63.3 kB
  • Tags: Source
  • Uploaded using 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}

File hashes

Hashes for razem_mattermost_bot-1.0.1.tar.gz
Algorithm Hash digest
SHA256 6e84ac82c431e668a95dcf1ae56617ca3ed9d08ceb85fdcac7c8fd99295f443c
MD5 5aff8449584d1f135018f6086e6a3211
BLAKE2b-256 586124c3a182dd1b7cf9661cdebe315a22c33fa27f036b951c6172388fad268c

See more details on using hashes here.

File details

Details for the file razem_mattermost_bot-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: razem_mattermost_bot-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 19.0 kB
  • Tags: Python 3
  • Uploaded using 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}

File hashes

Hashes for razem_mattermost_bot-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 086f4b880014dab8d39f6fc4c61a4293cf909e3fc1c1c8612dd23760095e21fb
MD5 1a39a896b6f18182eb6fecedb07e0665
BLAKE2b-256 356b3857b69b4cff69fac6bff7b4744c9465fb95ffb6ca7009e001f36569ff4a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page