Compare commits
34 Commits
ec87c6751c
...
v1.1.0
Author | SHA1 | Date | |
---|---|---|---|
7b91ddbcb8 | |||
8583ccd249 | |||
c43bd2b7ec | |||
ea8b6fc8f7 | |||
f7478fb1e3 | |||
29d951427d | |||
b12eb62b41 | |||
f5c3d767c0 | |||
d8407bac65 | |||
2f9d94406c | |||
1ca13a9451 | |||
0cdc359463 | |||
2d923df965 | |||
421c3a7e1f | |||
![]() |
9eafa55dd8 | ||
2a344817f7 | |||
27656c21ae | |||
![]() |
b733d1040c | ||
6d9c60b0d7 | |||
84fb43e836 | |||
![]() |
cfcea80a64 | ||
7daf30f851 | |||
![]() |
a030d06641 | ||
15304d565e | |||
![]() |
6094112f48 | ||
![]() |
c75fb0b7a3 | ||
![]() |
b2820ce902 | ||
![]() |
b605ce315b | ||
![]() |
dc14eb0aec | ||
![]() |
393ac72191 | ||
![]() |
f235b27916 | ||
![]() |
c4fbd089cc | ||
![]() |
18af074b6d | ||
![]() |
94933c2da0 |
29
.drone.yml
29
.drone.yml
@@ -1,29 +0,0 @@
|
||||
kind: pipeline
|
||||
type: kubernetes
|
||||
name: docker-build
|
||||
|
||||
node_selector:
|
||||
kubernetes.io/arch: amd64
|
||||
|
||||
|
||||
steps:
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_uname
|
||||
password:
|
||||
from_secret: docker_pw
|
||||
|
||||
repo: mollre/journal-bot
|
||||
tags:
|
||||
- 1.0.${DRONE_BUILD_NUMBER}
|
||||
- latest
|
||||
build_args: "BOT_VERSION=1.0.${DRONE_BUILD_NUMBER}"
|
||||
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
event:
|
||||
- push
|
35
.gitea/workflows/build_container.yaml
Normal file
35
.gitea/workflows/build_container.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
name: Build container
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.kluster.moll.re
|
||||
username: ${{ gitea.repository_owner }}
|
||||
password: ${{ secrets.PACKAGE_REGISTRY_ACCESS }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
# use the current commit SHA as the tag
|
||||
tags: git.kluster.moll.re/remoll/journal-bot:${{ gitea.sha }}
|
||||
push: true
|
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,9 +1,13 @@
|
||||
# Nix shell files
|
||||
.direnv/
|
||||
|
||||
|
||||
|
||||
# Secrets
|
||||
dev.env
|
||||
secret.yaml
|
||||
*.secret.yaml
|
||||
|
||||
# Static data
|
||||
.bot_storage/
|
||||
.bot/
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
13
.vscode/launch.json
vendored
13
.vscode/launch.json
vendored
@@ -4,18 +4,9 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Python: Current File",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
},
|
||||
{
|
||||
"name": "Python: Current project",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/bot/main.py",
|
||||
"console": "integratedTerminal",
|
||||
@@ -23,4 +14,4 @@
|
||||
"envFile": "${workspaceFolder}/dev.env",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
19
Dockerfile
19
Dockerfile
@@ -1,17 +1,14 @@
|
||||
FROM python:3-slim
|
||||
ENV DOCKERIZED=true
|
||||
ARG BOT_VERSION
|
||||
# set at build time
|
||||
ENV BOT_VERSION=$BOT_VERSION
|
||||
FROM docker.io/python:3.13-alpine
|
||||
# use the latest version of uv, independently of the python version
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
RUN pip install pipenv
|
||||
|
||||
# copy the requirements and install them
|
||||
COPY pyproject.toml uv.lock .
|
||||
RUN uv sync --frozen
|
||||
|
||||
COPY Pipfile Pipfile.lock ./
|
||||
|
||||
RUN pipenv install --system --deploy
|
||||
|
||||
# copy the rest of the code
|
||||
COPY bot .
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
CMD ["uv", "run", "main.py"]
|
||||
|
7
Makefile
7
Makefile
@@ -1,7 +0,0 @@
|
||||
CURRENT_DIR := $(shell pwd)
|
||||
DOTENV := ${CURRENT_DIR}/dev.env
|
||||
PIPENV_CMD_PREFIX := PIPENV_DOTENV_LOCATION=${DOTENV} pipenv run
|
||||
|
||||
|
||||
run:
|
||||
${PIPENV_CMD_PREFIX} python bot/main.py
|
14
Pipfile
14
Pipfile
@@ -1,14 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
peewee = "*"
|
||||
python-telegram-bot = {extras = ["job-queue"], version = "*"}
|
||||
anyio = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
131
Pipfile.lock
generated
131
Pipfile.lock
generated
@@ -1,131 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "5458e81c4f85af776acc44f46af838644ef8c00ccf4223fbe06f9d76a4717fc6"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:48d53f0b141f5757c38d648309e6fe254857fae092d67f938fa248d7c0f36804",
|
||||
"sha256:596b09c520820e7eed961ddc889540972f92d5e8fcb081117fc054c409df34ae"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.0.0rc1"
|
||||
},
|
||||
"apscheduler": {
|
||||
"hashes": [
|
||||
"sha256:0293937d8f6051a0f493359440c1a1b93e882c57daf0197afeff0e727777b96e",
|
||||
"sha256:e813ad5ada7aff36fb08cdda746b520531eaac7757832abc204868ba78e0c8f6"
|
||||
],
|
||||
"version": "==3.10.1"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.7.22"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
|
||||
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888",
|
||||
"sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"httpx": {
|
||||
"hashes": [
|
||||
"sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd",
|
||||
"sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.24.1"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.4"
|
||||
},
|
||||
"peewee": {
|
||||
"hashes": [
|
||||
"sha256:10769981198c7311f84a0ca8db892fa213303a8eb1305deb795a71e7bd606a91"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.16.2"
|
||||
},
|
||||
"python-telegram-bot": {
|
||||
"extras": [
|
||||
"job-queue"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:a6ac3f9c9674aaf7d1c7e652d8b75cde969fb872f75e9521b8516eceaba82b1b",
|
||||
"sha256:e426404b0006989a5bcc05e11a7ef3ffe0c086b684a4e963db5bda1d361a049a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.4"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588",
|
||||
"sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"
|
||||
],
|
||||
"version": "==2023.3"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f",
|
||||
"sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==68.0.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
|
||||
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"tzlocal": {
|
||||
"hashes": [
|
||||
"sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803",
|
||||
"sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==5.0.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
@@ -1,3 +1,9 @@
|
||||
# journal-bot
|
||||
|
||||
Sharing memories, the digital way...
|
||||
Sharing memories, the digital way...
|
||||
|
||||
|
||||
## Migration 10.03.24
|
||||
```
|
||||
ALTER TABLE journalentry ADD COLUMN rating INTEGER;
|
||||
```
|
||||
|
149
bot/commands/advent.py
Normal file
149
bot/commands/advent.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from datetime import date
|
||||
from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, filters, CallbackQueryHandler, CallbackContext
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, InputMediaPhoto
|
||||
import random
|
||||
import os
|
||||
from pathlib import Path
|
||||
# ACTION_CHOICE, DATE_ENTRY, ADD_CONTENT = range(3)
|
||||
ACTION, TUERCHEN_CHOICE = range(2)
|
||||
|
||||
from .basehandler import BaseHandler
|
||||
|
||||
MEDIA_DIR = Path(os.getenv("MEDIA_DIR"))
|
||||
GIF_LOCATION = MEDIA_DIR / "advent" / "gifs"
|
||||
# GIFS = list(GIF_LOCATION.glob("*.mp4"))
|
||||
STICKER_LOCATION = MEDIA_DIR / "advent" / "stickers"
|
||||
# STICKERS = list(STICKER_LOCATION.glob("*.tgs"))
|
||||
PICTURE_LOCATION = MEDIA_DIR / "advent" / "pretty_pictures"
|
||||
PICTURES = list(PICTURE_LOCATION.glob("*.jpg"))
|
||||
|
||||
class AdventsHandler(BaseHandler):
|
||||
def __init__(self, entry_string):
|
||||
self.entry_string = entry_string
|
||||
self. tuerchen_texte = {
|
||||
"1": f"Mein kleiner, süßer Schatz, ich geb dir einen Schmatz! \n - Gutschein für ein gaaanz dickes Küsschen! 😘",
|
||||
"2": f"Die zwei ist eine tolle Zahl, \ndrum schenk ich dir nach deiner Wahl \nein Frühstück so wie's dir gefällt,\n dem cutesten Schuhu der Welt!\n - Gutschein für ein Wunschfrühstück von deinem Bubo 🥞",
|
||||
"3": f"Die Nummer heute ist die drei mit einer Prise Glück dabei! \n - Der Tag für das Glückskleeblattspritzgebäck ist gekommen! 🍀",
|
||||
"4": f"Hand in Hand, ganz fest verhakt, geht wir zwei auf den Weihnachtsmarkt!\n - Gutschein für einen heißen Punsch auf dem Zürcher Weihnachtsmarkt 🍹",
|
||||
"5": f"Fühlt der Schuhu sich mal matt,\nso höre gut auf meinen Rat:\nMassageöl auf das Gefieder,\nDann glänzt und strahlt der Schuhu wieder!\n - Gutschein für eine Massage 👐🏻",
|
||||
"6": f"Das ist das Haus vom Nikolaus\nund nebendrein der Bubos Heim!\n - Ich habe eine kleine Nascherei für dich! 🍫",
|
||||
"7": f"Für etwas Spaß braucht es nicht viel,\nden Bubo und ein gutes Spiel! - Gutschein für einen Zockabend mit einem Spiel deiner Wahl 🎮",
|
||||
"8": f"Zu abend da erzähl ich dir,\nso wundersam Geschichte fein\ndrum spitz dir ohren, lausche mir\ndoch schlaf mir dabei bloß nicht ein!\n - Gutschein für eine Vorlesesession 📖",
|
||||
"9": f"Es duftet fein aus naher Ferne, denn naschen tuen Kätzchen gerne!\n - Gutschein für ein Überraschungsdessert von deinem Naschkätzchen 😽",
|
||||
"10": f"Zu spät für Frühstück, zu spät für Lunch, dann machen wir daraus 'nen Brunch!\n - Gutschein für einen Brunch im Café des Amis 🥐",
|
||||
"11": f"Mein Bubo macht so gerne Quatsch und ist so süß wie Brownie Fudge!\n - Gutschein für ein Ben'n'Jerry's Brownie Fudge Ice Cream 🍫",
|
||||
"12": f"Halb ist geschafft die lange Reise,\ndrum wähle deine Toppings weise\nmit Apfel, Zimt und andren Sachen, wenn wir zusammen Milchreis machen!\n - Gutschein für eine Milchreismahlzeit, damit dir auf halber Strecke nicht die Kraft ausgeht 🍚",
|
||||
"13": f"Was hat hier so verbrannt gerochen?\n Das sind die Schuhus, die hier kochen!\n - Gutschein für eine Kochsession für ein ausgefallenes Gericht deiner Wahl 👩🍳👨🍳",
|
||||
"14": f"Auf leckres Essen darfst du hoffen, warm und dampfend aus dem Ofen!\n - Gutschein für einen Abend zum Bratapfel-Backen 🍎",
|
||||
"15": f"Die Weihnacht langsam Einzug hält,\nder erste Schnee vom Himmel fällt,\nGedanken tief in ihm versinken,\nlass uns gehn' einen Kaffee trinken!\n - Gutschein für einen Nachmittagskaffee in der Altstadt ☕️",
|
||||
"16": f"Ein Glück, der See noch nicht erstarrt,\nPerfekt fpr eine Bötchenfahrt!\n - Gutschein für eine Bootsfahrt auf dem Zürisee 🛳",
|
||||
"17": f"Was schaut denn da so frech ums Eck,\nnanu, das ist dein Öhrchen-Schleck! \n - Du bekommst gratis einen tollen Öhrchenschleck für beide Schuhu-Spitzöhrchen! 👂🏻",
|
||||
"18": f"Was ist im Winter glatt ein Muss?\nNa klar, der Spekulatius!\n - Gutschein für ein kleines Spekulatiusdessert 🍰",
|
||||
"19": f"Wenn Bubos sind mal ganz allein, da können sie ganz unbrav sein...\n - Gutschein für eine ganz zärtliche, persönliche Verführung von deiner Bubodame 😈",
|
||||
"20": f"Bubo, pack dich ganz warm ein\ndenn bald wird es noch kälter sein!\n - Gutschein für einen Eiskaffe bei Sprüngli und dabei gaaanz warm kuscheln! 🍨",
|
||||
"21": f"Die Kufen werden angezogen,\nwir kommen übers Eis geflogen!\n - Gutschein für Schlittschuhlaufen, Flügel in Flügel ⛸",
|
||||
"22": f"Oh bald es wird schon wieder wärmer,\nauf geht's in die Gelateria di Berna!\n - Gutschein für eine dicke Portion Eis in der Gelateria di Berna 🍦",
|
||||
"23": f"Kleiner Bubo, moki moki,\nich koch dir eine heiße Schoki,\ndamit der Hunger auch nicht weint\nmit einem Kekse sie vereint!\n - Gutschein für eine heiße Spezial-Schoki mit Keksi dazu! 🍪",
|
||||
"24": f"Das lange Warten ist geschafft,\ngekostet hat es ganz viel Kraft,\nnun höre mich, wenn ich dir sage:\nIch wünsch dir tolle Weihnachtstage,\nmit Family und Freunden schön\nund Kekse auf dem Tische stehend\nund in der Ferne, doch so nah\nist immer jemand für dich da,\nder an dich denkt und dich so liebt\n Mein Bubo ich hab dich so gern! ❤️🎄"
|
||||
}
|
||||
|
||||
|
||||
self.handler = ConversationHandler(
|
||||
entry_points=[CommandHandler(entry_string, self.entry_point)],
|
||||
states={
|
||||
ACTION: [
|
||||
CallbackQueryHandler(self.kuesschen, pattern = "^kuss$"),
|
||||
CallbackQueryHandler(self.tuerchen, pattern = "^tuer$"),
|
||||
CallbackQueryHandler(self.pretty_picture, pattern="^picture$"),
|
||||
CallbackQueryHandler(self.byebye, pattern ="^bye$")
|
||||
# CallbackQueryHandler(self.new_list, pattern="^new$")
|
||||
],
|
||||
TUERCHEN_CHOICE: [
|
||||
MessageHandler(filters.ALL, self.choose_tuerchen)
|
||||
]
|
||||
|
||||
},
|
||||
fallbacks=[],
|
||||
)
|
||||
|
||||
self.current_model = None
|
||||
|
||||
|
||||
async def entry_point(self, update: Update, context: CallbackContext):
|
||||
await super().entry_point(update, context)
|
||||
|
||||
keyboard = [[InlineKeyboardButton("Bubo Küsschen", callback_data="kuss")], [InlineKeyboardButton("Türchen öffnen", callback_data="tuer")], [InlineKeyboardButton("Pretty Bubo Picture", callback_data="picture")]]
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
await update.message.reply_sticker(sticker=STICKER_LOCATION/"stickerwhat.tgs")
|
||||
await update.message.reply_text(text="Hallo, mein süßer Weihnachts-Bubo! Ich bin dein Adventskalender ^^. Was möchtest du tun?", reply_markup=reply_markup)
|
||||
|
||||
return ACTION
|
||||
|
||||
async def kuesschen(self, update: Update, context: CallbackContext):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
keyboard = [[InlineKeyboardButton("Kalendertürchen öffnen", callback_data="tuer")], [InlineKeyboardButton("Noch ein Küsschen!", callback_data="kuss")], [InlineKeyboardButton("Pretty Bubo Picture", callback_data="picture")], [InlineKeyboardButton("Bis zum nächsten Mal!", callback_data="bye")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
await update.effective_message.reply_sticker(sticker=STICKER_LOCATION/"stickerkiss.tgs")
|
||||
await update.effective_message.reply_text(text="Mua!", reply_markup=reply_markup)
|
||||
|
||||
return ACTION
|
||||
|
||||
async def tuerchen(self, update: Update, context: CallbackContext):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
await update.effective_message.reply_text(text="Welches Türchen möchtest du öffnen? Bitte gib die Zahl des Türchens ein:")
|
||||
|
||||
return TUERCHEN_CHOICE
|
||||
|
||||
|
||||
async def pretty_picture(self, update: Update, context: CallbackContext):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
keyboard = [[InlineKeyboardButton("Kalendertürchen öffnen", callback_data="tuer")], [InlineKeyboardButton("Bubo Küsschen", callback_data="kuss")], [InlineKeyboardButton("Noch ein Bild!", callback_data="picture")], [InlineKeyboardButton("Bis zum nächsten Mal!", callback_data="bye")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
picture_number = random.randint(1,31)
|
||||
#print(picture_number-1)
|
||||
await update.effective_message.reply_photo(photo=PICTURES[picture_number-1], caption="So ein cutes Foto!", reply_markup=reply_markup)
|
||||
|
||||
return ACTION
|
||||
|
||||
|
||||
async def choose_tuerchen(self, update: Update, context: CallbackContext):
|
||||
tuerchen = update.message.text
|
||||
tuer_nummer = int(tuerchen)
|
||||
|
||||
#print(date.today().strftime("%d"+"%m"))
|
||||
|
||||
keyboard = [[InlineKeyboardButton("Anderes Türchen öffnen", callback_data="tuer")], [InlineKeyboardButton("Bubo Küsschen", callback_data="kuss")], [InlineKeyboardButton("Pretty Bubo Picture", callback_data="picture")], [InlineKeyboardButton("Bis zum nächsten Mal!", callback_data="bye")]]
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
datum_tuer_heute = date(2023, 12, tuer_nummer)
|
||||
if (date.today() - datum_tuer_heute).days >= 0:
|
||||
await update.message.reply_document(document=GIF_LOCATION/ f"gif{tuer_nummer}.mp4", caption=f"Türchen für den {tuer_nummer}. Dezember: \n" + self.tuerchen_texte[tuerchen], reply_markup=reply_markup)
|
||||
else:
|
||||
await update.message.reply_sticker(sticker=STICKER_LOCATION/"stickerangry.tgs")
|
||||
await update.message.reply_text(text="Hey, nicht schummeln! Dieses Türchen darfst du noch nicht sehen.", reply_markup=reply_markup)
|
||||
|
||||
return ACTION
|
||||
|
||||
async def byebye(self, update: Update, context: CallbackContext):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
await update.effective_message.reply_sticker(sticker=STICKER_LOCATION/"stickerbye.tgs")
|
||||
await update.effective_message.reply_text(text="Bye bye, Bubo! Hab dich ganz doll lieb! 😘")
|
||||
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -5,7 +5,7 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.constants import ParseMode
|
||||
import models
|
||||
|
||||
ENTRY_OPTIONS, CONTENT_ENTRY = range(2)
|
||||
ENTRY_OPTIONS, CONTENT_ENTRY, DAY_RATING = range(3)
|
||||
BUTTON_COUNT = 5
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ class JournalHandler(BaseHandler):
|
||||
CONTENT_ENTRY: [
|
||||
MessageHandler(filters.ALL, self.content_save),
|
||||
],
|
||||
DAY_RATING: [
|
||||
CallbackQueryHandler(self.day_rating_save),
|
||||
],
|
||||
},
|
||||
fallbacks=[],
|
||||
)
|
||||
@@ -35,27 +38,18 @@ class JournalHandler(BaseHandler):
|
||||
|
||||
async def entry_point(self, update, context):
|
||||
await super().entry_point(update, context)
|
||||
if os.getenv("DOCKERIZED", "false") == "true" and os.getenv("CHAT_ID") != str(update.message.chat_id):
|
||||
if models.IS_PRODUCTION and os.getenv("CHAT_ID") != str(update.message.chat_id):
|
||||
await update.message.reply_text("You are not authorized to use this bot")
|
||||
return ConversationHandler.END
|
||||
|
||||
dates = [(datetime.datetime.now() - datetime.timedelta(days = i)).date() for i in range(BUTTON_COUNT + 2)][::-1]
|
||||
# since there are two buttons additional buttons, we need to have two more days
|
||||
names = [d.strftime("%d.%m.") for d in dates]
|
||||
names = get_names(dates)
|
||||
callbacks = [d.strftime("%d%m%Y") for d in dates]
|
||||
names[-1] = "Today"
|
||||
names[-2] = "Yesterday"
|
||||
|
||||
options = [
|
||||
[
|
||||
InlineKeyboardButton(names[-1], callback_data=callbacks[-1])
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(names[-2], callback_data=callbacks[-2])
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(n, callback_data=c) for n,c in zip(names[:-2], callbacks[:-2])
|
||||
],
|
||||
[InlineKeyboardButton(n, callback_data=c)] for n,c in zip(names[::-1], callbacks[::-1])
|
||||
] + [
|
||||
[
|
||||
InlineKeyboardButton("<<", callback_data=BUTTON_COUNT + 2)
|
||||
],
|
||||
@@ -85,7 +79,7 @@ class JournalHandler(BaseHandler):
|
||||
else:
|
||||
await query.edit_message_text(text="An entry already exists for this date")
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
return CONTENT_ENTRY
|
||||
|
||||
|
||||
@@ -95,16 +89,16 @@ class JournalHandler(BaseHandler):
|
||||
delta = int(query.data)
|
||||
|
||||
dates = [(datetime.datetime.now() - datetime.timedelta(days = i + delta)).date() for i in range(BUTTON_COUNT)][::-1]
|
||||
names = [d.strftime("%d.%m.") for d in dates]
|
||||
names = get_names(dates)
|
||||
callbacks = [d.strftime("%d%m%Y") for d in dates]
|
||||
|
||||
options = [
|
||||
[
|
||||
InlineKeyboardButton(">>", callback_data=delta - BUTTON_COUNT)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(n, callback_data=c) for n,c in zip(names, callbacks)
|
||||
],
|
||||
]
|
||||
] + [
|
||||
[InlineKeyboardButton(n, callback_data=c)] for n,c in zip(names[::-1], callbacks[::-1])
|
||||
] + [
|
||||
[
|
||||
InlineKeyboardButton("<<", callback_data=delta + BUTTON_COUNT)
|
||||
],
|
||||
@@ -119,13 +113,13 @@ class JournalHandler(BaseHandler):
|
||||
|
||||
async def date_entry(self, update, context):
|
||||
date = update.message.text
|
||||
|
||||
|
||||
try:
|
||||
date = datetime.datetime.strptime(date, "%d%m%Y").date()
|
||||
except ValueError:
|
||||
await update.message.reply_text("Please enter the date in the format _DDMMYYYY_", parse_mode=ParseMode.MARKDOWN_V2)
|
||||
return ENTRY_OPTIONS
|
||||
|
||||
|
||||
if context.chat_data.get("delete", False): # if not set, delete was not chosen
|
||||
with models.db:
|
||||
self.current_model = models.JournalEntry.get_or_none(
|
||||
@@ -164,7 +158,7 @@ class JournalHandler(BaseHandler):
|
||||
file = await update.message.effective_attachment[-1].get_file()
|
||||
else:
|
||||
file = await update.message.effective_attachment.get_file()
|
||||
|
||||
|
||||
file_bytes = await file.download_as_bytearray()
|
||||
file_path = file.file_path
|
||||
self.current_model.save_media(file_bytes, file_path)
|
||||
@@ -173,7 +167,23 @@ class JournalHandler(BaseHandler):
|
||||
|
||||
self.current_model.save()
|
||||
|
||||
await update.message.reply_text(f"Saved entry ✅")
|
||||
options = [
|
||||
[InlineKeyboardButton(models.RATING_MAPPING[idx], callback_data=idx) for idx in [1,2,3,4,5]]
|
||||
]
|
||||
|
||||
await update.message.reply_text(f"Saved entry ✅. How was the day?", reply_markup=InlineKeyboardMarkup(options))
|
||||
|
||||
return DAY_RATING
|
||||
|
||||
|
||||
async def day_rating_save(self, update, context):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
rating = int(query.data)
|
||||
with models.db:
|
||||
self.current_model.rating = rating
|
||||
self.current_model.save()
|
||||
await query.edit_message_text(text="Rating saved ✅")
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
@@ -190,3 +200,22 @@ class JournalHandler(BaseHandler):
|
||||
self.current_model.delete_instance()
|
||||
context.chat_data["delete"] = False
|
||||
await update.message.reply_text(text="Entry deleted ✅")
|
||||
|
||||
|
||||
|
||||
### HELPERS
|
||||
|
||||
def get_names(dates: list):
|
||||
names = []
|
||||
for d in dates:
|
||||
suffix = ""
|
||||
if models.JournalEntry.get_or_none(date = d):
|
||||
suffix = " ✅"
|
||||
|
||||
if d == datetime.datetime.now().date():
|
||||
names.append("Today" + suffix)
|
||||
elif d == datetime.datetime.now().date() - datetime.timedelta(days = 1):
|
||||
names.append("Yesterday" + suffix)
|
||||
else:
|
||||
names.append(d.strftime("%d.%m.") + suffix)
|
||||
return names
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from peewee import *
|
||||
|
||||
db = DatabaseProxy()
|
||||
|
||||
class BaseModel(Model):
|
||||
@@ -13,7 +13,7 @@ class ListModel(BaseModel):
|
||||
@property
|
||||
def content(self) -> dict:
|
||||
return {e.id: e.entry for e in self.entries}
|
||||
|
||||
|
||||
@content.setter
|
||||
def content(self, new_content: dict):
|
||||
old_content = self.content
|
||||
@@ -29,7 +29,7 @@ class ListModel(BaseModel):
|
||||
@property
|
||||
def done_dict(self):
|
||||
return {e.id: e.done for e in self.entries}
|
||||
|
||||
|
||||
@done_dict.setter
|
||||
def done_dict(self, new_done: dict):
|
||||
old_done_dict = self.done_dict
|
||||
@@ -46,7 +46,8 @@ class ListEntryModel(BaseModel):
|
||||
done = BooleanField(default=None, null=True)
|
||||
|
||||
|
||||
def set_db(db_path):
|
||||
def set_db(db_path: Path):
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
db.initialize(SqliteDatabase(db_path))
|
||||
with db:
|
||||
db.create_tables([ListModel, ListEntryModel], safe=True)
|
||||
|
@@ -3,7 +3,7 @@ from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, fi
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, InputMediaPhoto
|
||||
import models
|
||||
from telegram.constants import ParseMode
|
||||
# ACTION_CHOICE, DATE_ENTRY, ADD_CONTENT = range(3)
|
||||
|
||||
MEMORY_CHOICE = range(1)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class MemoryHandler(BaseHandler):
|
||||
|
||||
async def entry_point(self, update: Update, context: CallbackContext):
|
||||
await super().entry_point(update, context)
|
||||
if os.getenv("DOCKERIZED", "false") == "true" and os.getenv("CHAT_ID") != str(update.message.chat_id):
|
||||
if models.IS_PRODUCTION and os.getenv("CHAT_ID") != str(update.message.chat_id):
|
||||
await update.message.reply_text("You are not authorized to use this bot")
|
||||
return ConversationHandler.END
|
||||
|
||||
@@ -56,9 +56,9 @@ class MemoryHandler(BaseHandler):
|
||||
await update.message.reply_text(
|
||||
f"Which moment would you like to remember?", reply_markup=keyboard
|
||||
)
|
||||
|
||||
|
||||
context.chat_data["kept_matches"] = list(matching_models)
|
||||
|
||||
|
||||
return MEMORY_CHOICE
|
||||
|
||||
async def choose_memory(self, update: Update, context: CallbackContext):
|
||||
@@ -69,21 +69,22 @@ class MemoryHandler(BaseHandler):
|
||||
matching_models = context.chat_data["kept_matches"]
|
||||
chosen_match = matching_models[ind]
|
||||
|
||||
rating_string = f" ({models.RATING_MAPPING[chosen_match.rating]})" if chosen_match.rating else ""
|
||||
|
||||
message_text = f"On {chosen_match.date_pretty}{rating_string}, " \
|
||||
f"{chosen_match.author} wrote: \n" \
|
||||
f"{chosen_match.spoiler_text}"
|
||||
|
||||
if chosen_match.media_path:
|
||||
# context.bot.sendPhoto()
|
||||
await update.effective_message.reply_photo(
|
||||
photo = chosen_match.media_path,
|
||||
caption=
|
||||
f"On {chosen_match.date_pretty}, "
|
||||
f"{chosen_match.author} wrote: \n"
|
||||
f"{chosen_match.spoiler_text}",
|
||||
caption = message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
else:
|
||||
await query.edit_message_text(
|
||||
f"On {chosen_match.date_pretty}, "
|
||||
f"{chosen_match.author} wrote: \n"
|
||||
f"{chosen_match.spoiler_text}",
|
||||
message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
|
||||
|
@@ -7,10 +7,14 @@ from telegram.constants import ParseMode
|
||||
import os
|
||||
|
||||
FIRST = 1
|
||||
import models
|
||||
from .basehandler import BaseHandler
|
||||
|
||||
|
||||
|
||||
class StatusHandler(BaseHandler):
|
||||
"""Shows a short status of the program."""
|
||||
|
||||
|
||||
def __init__(self, entry_string):
|
||||
self.start_time = datetime.datetime.now()
|
||||
self.entry_string = entry_string
|
||||
@@ -35,7 +39,6 @@ class StatusHandler(BaseHandler):
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
delta = str(datetime.datetime.now() - self.start_time)
|
||||
message = "BeebBop, this is Norbit\n"
|
||||
|
||||
try:
|
||||
ip = httpx.get('https://api.ipify.org').text
|
||||
@@ -47,18 +50,21 @@ class StatusHandler(BaseHandler):
|
||||
ip = "not fetchable"
|
||||
local_ips = "not fetchable"
|
||||
|
||||
message += "Status: Running 🟢\n"
|
||||
message += f"Version: `{os.getenv('BOT_VERSION', 'dev')}`\n"
|
||||
message += f"Uptime: `{delta[:delta.rfind('.')]}`\n"
|
||||
message += f"IP \(public\): `{ip}`\n"
|
||||
message += f"IP \(private\): `{local_ips}`\n"
|
||||
message += f"Chat ID: `{update.effective_chat.id}`\n"
|
||||
message = f"""
|
||||
BeebBop\!
|
||||
Status: Running 🟢
|
||||
Version: `{os.getenv('BOT_VERSION', 'dev')}` and`prod={models.IS_PRODUCTION}`
|
||||
Uptime: `{delta[:delta.rfind('.')]}`
|
||||
IP \(public\): `{ip}`
|
||||
IP \(private\): `{local_ips}`
|
||||
Chat ID: `{update.effective_chat.id}`
|
||||
""".strip() # remove trailing whitespace
|
||||
|
||||
if update.message:
|
||||
await update.message.reply_text(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
else:
|
||||
await update._effective_chat.send_message(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
|
||||
|
||||
return FIRST
|
||||
|
||||
|
||||
|
@@ -13,8 +13,8 @@ class SetChatPhotoJob():
|
||||
def __init__(self, bot: ExtBot, job_queue):
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
if os.getenv("DOCKERIZED", "false") != "true":
|
||||
|
||||
if not models.IS_PRODUCTION:
|
||||
# when running locally, annoy the programmer every 60 seconds <3
|
||||
job_queue.run_repeating(self.callback_photo, interval=60)
|
||||
else:
|
||||
@@ -24,13 +24,13 @@ class SetChatPhotoJob():
|
||||
|
||||
|
||||
async def callback_photo(self, context):
|
||||
|
||||
|
||||
# last_seen of memory must be older than 10 days in past or None
|
||||
with models.db:
|
||||
possible_photos = models.JournalEntry.select().where(
|
||||
models.JournalEntry.media_path != None
|
||||
).order_by(fn.Random())
|
||||
|
||||
|
||||
try:
|
||||
chosen_entry = possible_photos.get()
|
||||
except:
|
||||
|
95
bot/cronjob/leaderboard.py
Normal file
95
bot/cronjob/leaderboard.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
from telegram.ext import ExtBot
|
||||
from telegram.constants import ParseMode
|
||||
import logging
|
||||
from datetime import time, timedelta, timezone, datetime, date
|
||||
from peewee import fn
|
||||
import models
|
||||
from telegram.ext import JobQueue
|
||||
|
||||
|
||||
RANKING_TEMPLATE = """
|
||||
<b>Journal Leaderboard</b>
|
||||
This week: 📈{week_leader_name} - {week_leader_count} 📉{week_last_name} - {week_last_count}
|
||||
This month: 📈{month_leader_name} - {month_leader_count} 📉{month_last_name} - {month_last_count}
|
||||
This year: 📈{year_leader_name} - {year_leader_count} 📉{year_last_name} - {year_last_count}
|
||||
|
||||
🏆 Leader: {leader_name}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def get_author_ranking(since_days):
|
||||
"""Returns the query for the top authors by counting their journal entries. An additional field for the count is added."""
|
||||
|
||||
cutoff_date = date.today() - timedelta(days=since_days)
|
||||
with models.db:
|
||||
return models.JournalEntry.select(
|
||||
models.JournalEntry.author,
|
||||
fn.Count(models.JournalEntry.id).alias('message_count')
|
||||
).where(
|
||||
models.JournalEntry.date >= cutoff_date
|
||||
).group_by(
|
||||
models.JournalEntry.author
|
||||
).order_by(
|
||||
fn.Count(models.JournalEntry.id).desc()
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class SendLeaderboard():
|
||||
def __init__(self, bot: ExtBot, job_queue: JobQueue):
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
if not models.IS_PRODUCTION:
|
||||
# when running locally, just run once after 10 seconds
|
||||
job_queue.run_once(self.callback_leaderboard, when=10)
|
||||
else:
|
||||
# set the message sending time; include UTC shift +2
|
||||
sending_time = time(hour=12, minute=0, second=0, tzinfo=timezone(timedelta(hours=2)))
|
||||
job_queue.run_daily(self.callback_leaderboard, when=sending_time, day=-1)
|
||||
|
||||
|
||||
async def callback_leaderboard(self, context):
|
||||
"""Send a weakly leaderboard to the chat."""
|
||||
if date.today().weekday() != 1:
|
||||
self.logger.info("Today is not Monday, skipping leaderboard.")
|
||||
return
|
||||
|
||||
# get the top contributions of the past week, month and year:
|
||||
ranking_week = get_author_ranking(7)
|
||||
ranking_month = get_author_ranking(30)
|
||||
ranking_year = get_author_ranking(365)
|
||||
|
||||
week_leader, week_last = ranking_week.first(n=2)
|
||||
month_leader, month_last = ranking_month.first(n=2)
|
||||
year_leader, year_last = ranking_year.first(n=2)
|
||||
|
||||
leader = year_leader
|
||||
|
||||
message_text = RANKING_TEMPLATE.format(
|
||||
week_leader_name=week_leader.author,
|
||||
week_leader_count=week_leader.message_count,
|
||||
week_last_name=week_last.author,
|
||||
week_last_count=week_last.message_count,
|
||||
month_leader_name=month_leader.author,
|
||||
month_leader_count=month_leader.message_count,
|
||||
month_last_name=month_last.author,
|
||||
month_last_count=month_last.message_count,
|
||||
year_leader_name=year_leader.author,
|
||||
year_leader_count=year_leader.message_count,
|
||||
year_last_name=year_last.author,
|
||||
year_last_count=year_last.message_count,
|
||||
leader_name=leader.author
|
||||
)
|
||||
|
||||
print(message_text)
|
||||
|
||||
chat_id = os.getenv("CHAT_ID")
|
||||
await self.bot.send_message(
|
||||
chat_id = chat_id,
|
||||
text = message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
@@ -4,15 +4,16 @@ import os
|
||||
from peewee import fn
|
||||
import logging
|
||||
import models
|
||||
from telegram.ext import ExtBot
|
||||
|
||||
class RandomMemoryJob():
|
||||
def __init__(self, bot, job_queue):
|
||||
def __init__(self, bot: ExtBot, job_queue):
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
if os.getenv("DOCKERIZED", "false") != "true":
|
||||
|
||||
if not models.IS_PRODUCTION:
|
||||
# when running locally, annoy the programmer every 60 seconds <3
|
||||
job_queue.run_repeating(self.callback_memory, interval=60)
|
||||
job_queue.run_repeating(self.callback_memory, interval=3600)
|
||||
self.min_age = 0 # do not filter messages: show them all
|
||||
else:
|
||||
# set the message sending time; include UTC shift +2
|
||||
@@ -22,14 +23,14 @@ class RandomMemoryJob():
|
||||
|
||||
|
||||
async def callback_memory(self, context):
|
||||
|
||||
|
||||
# last_seen of memory must be older than 10 days in past or None
|
||||
with models.db:
|
||||
possible_entries = models.JournalEntry.select().where(
|
||||
(models.JournalEntry.last_shown <= datetime.today().date() - timedelta(days=self.min_age)) | \
|
||||
(models.JournalEntry.last_shown == None)
|
||||
).order_by(fn.Random())
|
||||
|
||||
|
||||
try:
|
||||
chosen_entry = possible_entries.get()
|
||||
except:
|
||||
@@ -42,23 +43,24 @@ class RandomMemoryJob():
|
||||
|
||||
chat_id = os.getenv("CHAT_ID")
|
||||
|
||||
rating_string = f" ({models.RATING_MAPPING[chosen_entry.rating]})" if chosen_entry.rating else ""
|
||||
|
||||
message_text = f"On {chosen_entry.date_pretty}{rating_string}, " \
|
||||
f"{chosen_entry.author} wrote: \n" \
|
||||
f"{chosen_entry.spoiler_text}"
|
||||
|
||||
|
||||
if chosen_entry.media_path:
|
||||
await self.bot.send_photo(
|
||||
chat_id = chat_id,
|
||||
photo = chosen_entry.media_path,
|
||||
caption =
|
||||
f"On {chosen_entry.date_pretty}, "
|
||||
f"{chosen_entry.author} wrote: \n"
|
||||
f"{chosen_entry.spoiler_text}",
|
||||
caption = message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
else:
|
||||
await self.bot.send_message(
|
||||
chat_id = chat_id,
|
||||
text =
|
||||
f"On {chosen_entry.date_pretty}, "
|
||||
f"{chosen_entry.author} wrote: \n"
|
||||
f"{chosen_entry.spoiler_text}",
|
||||
text = message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
|
||||
|
@@ -3,9 +3,9 @@ from telegram.ext import Application
|
||||
import logging
|
||||
|
||||
import models
|
||||
from commands import journal, status, turtle, memory
|
||||
from commands import journal, status, turtle, memory, advent
|
||||
from commands.list import list
|
||||
from cronjob import chat_photo, random_memory
|
||||
from cronjob import chat_photo, random_memory, leaderboard
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
@@ -28,9 +28,11 @@ def main() -> None:
|
||||
application.add_handler(status.StatusHandler("status").handler)
|
||||
application.add_handler(turtle.TurtleHandler().handler)
|
||||
application.add_handler(memory.MemoryHandler("memory").handler)
|
||||
application.add_handler(advent.AdventsHandler("advent").handler)
|
||||
|
||||
random_memory.RandomMemoryJob(application.bot, application.job_queue)
|
||||
chat_photo.SetChatPhotoJob(application.bot, application.job_queue)
|
||||
leaderboard.SendLeaderboard(application.bot, application.job_queue)
|
||||
|
||||
# Run the bot until the user presses Ctrl-C
|
||||
application.run_polling()
|
||||
|
@@ -2,7 +2,7 @@ from peewee import *
|
||||
from pathlib import Path
|
||||
import re
|
||||
import os
|
||||
import datetime
|
||||
import socket
|
||||
|
||||
ID_MAPPINGS = {
|
||||
"Lia": 5603036217,
|
||||
@@ -10,9 +10,22 @@ ID_MAPPINGS = {
|
||||
}
|
||||
ID_MAPPINGS_REV = dict((v, k) for k, v in ID_MAPPINGS.items())
|
||||
|
||||
RATING_MAPPING = {
|
||||
1: "🙁",
|
||||
2: "😐",
|
||||
3: "🙂",
|
||||
4: "😃",
|
||||
5: "🥰"
|
||||
}
|
||||
|
||||
MEDIA_DIR = Path(os.getenv("MEDIA_DIR"))
|
||||
MEDIA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# check if we are running on a cluster
|
||||
IS_PRODUCTION = os.getenv('KUBERNETES_SERVICE_HOST') is not None
|
||||
|
||||
|
||||
|
||||
db = DatabaseProxy()
|
||||
|
||||
class BaseModel(Model):
|
||||
@@ -28,12 +41,12 @@ class JournalEntry(BaseModel):
|
||||
text = TextField(null=True)
|
||||
media_path = TextField(null=True)
|
||||
last_shown = DateField(null=True)
|
||||
rating = IntegerField(null=True) # mapped by RATING_MAPPING
|
||||
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
return Path(self.media_path).open('rb')
|
||||
|
||||
|
||||
def save_media(self, media: bytearray, file_name: str):
|
||||
ext = Path(file_name).suffix
|
||||
file_name = f"{self.date.isoformat()}-media{ext}"
|
||||
@@ -66,20 +79,20 @@ class JournalEntry(BaseModel):
|
||||
"""Returns the text with all the frisky details hidden away"""
|
||||
new_text = self.text.replace("<", "<").replace(">", ">").replace("&", "&")
|
||||
pattern = re.compile(
|
||||
"("
|
||||
"(((?<=(\.|\!|\?)\s)[A-Z])|(^[A-Z]))" # beginning of a sentence
|
||||
"([^\.\!\?])+" # any character being part of a sentence
|
||||
"((\:\))|😇|😈)" # the smiley
|
||||
"([^\.\!\?])*" # continuation of sentence
|
||||
"(\.|\!|\?|\,|$)" # end of the sentence
|
||||
")"
|
||||
r"("
|
||||
r"(((?<=(\.|\!|\?)\s)[A-Z])|(^[A-Z]))" # beginning of a sentence
|
||||
r"([^\.\!\?])+" # any character being part of a sentence
|
||||
r"((\:\))|😇|😈|[Ss]ex)" # the smiley
|
||||
r"([^\.\!\?])*" # continuation of sentence
|
||||
r"(\.|\!|\?|\,|$)" # end of the sentence
|
||||
r")"
|
||||
)
|
||||
matches = pattern.findall(new_text)
|
||||
for match in matches:
|
||||
group_to_replace = match[0]
|
||||
new_text = new_text.replace(group_to_replace, f"<tg-spoiler>{group_to_replace}</tg-spoiler>")
|
||||
return new_text
|
||||
|
||||
|
||||
|
||||
def set_db(db_path):
|
||||
db.initialize(SqliteDatabase(db_path))
|
||||
|
29
default.nix
Normal file
29
default.nix
Normal file
@@ -0,0 +1,29 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
name = "journal-bot-shell";
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
# python
|
||||
python313
|
||||
# setuptools as downloaded by uv seems to be broken on nixos
|
||||
python313Packages.setuptools
|
||||
uv
|
||||
];
|
||||
|
||||
|
||||
# fix library dependencies:
|
||||
env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
|
||||
# required by numpy and the likes
|
||||
pkgs.stdenv.cc.cc.lib
|
||||
pkgs.libz
|
||||
];
|
||||
|
||||
# tell UV where to put the virtualenv:
|
||||
# env.UV_PROJECT_ENVIRONMENT = ".cache/uv-venvs/thesis";
|
||||
|
||||
# install the python packages through uv:
|
||||
shellHook = "
|
||||
uv sync
|
||||
source .venv/bin/activate
|
||||
";
|
||||
}
|
@@ -17,7 +17,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: journal
|
||||
image: mollre/journal-bot:1.0.19
|
||||
image: journal
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: journal-secret-env
|
||||
@@ -33,29 +33,3 @@ spec:
|
||||
- name: journal-nfs
|
||||
persistentVolumeClaim:
|
||||
claimName: journal-data-nfs
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: "journal-data-nfs"
|
||||
spec:
|
||||
storageClassName: fast
|
||||
capacity:
|
||||
storage: "5Gi"
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
nfs:
|
||||
path: /export/kluster/journal-bot
|
||||
server: 192.168.1.157
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: "journal-data-nfs"
|
||||
spec:
|
||||
storageClassName: "fast"
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: "5Gi"
|
||||
|
@@ -1,9 +1,15 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ./namespace.yaml
|
||||
- ./deployment.yaml
|
||||
- ./sealedsecret.yaml
|
||||
- ./pvc.yaml
|
||||
|
||||
namespace: journal
|
||||
|
||||
images:
|
||||
- name: mollre/journal-bot
|
||||
newTag: 1.0.58
|
||||
- name: journal
|
||||
newName: git.kluster.moll.re/remoll/journal-bot
|
||||
newTag: 29d951427d6f3377e43767916cefb07e03e9eab8
|
||||
|
@@ -1,6 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: journal
|
||||
labels:
|
||||
name: journal
|
||||
name: placeholder
|
||||
|
27
deployment/pvc.yaml
Normal file
27
deployment/pvc.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: "journal-data-nfs"
|
||||
spec:
|
||||
storageClassName: ""
|
||||
capacity:
|
||||
storage: "5Gi"
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
nfs:
|
||||
path: /export/kluster/journal-bot
|
||||
server: 192.168.1.157
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: "journal-data-nfs"
|
||||
spec:
|
||||
storageClassName: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: "5Gi"
|
||||
volumeName: journal-data-nfs
|
||||
|
3
dev.env
Normal file
3
dev.env
Normal file
@@ -0,0 +1,3 @@
|
||||
MEDIA_DIR="./.bot/media"
|
||||
PERSISTENCE_DIR="./.bot/persistence"
|
||||
DB_PATH="./.bot/db.sqlite"
|
11
pyproject.toml
Normal file
11
pyproject.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "journal-bot"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"anyio>=4.9.0",
|
||||
"peewee>=3.18.2",
|
||||
"python-telegram-bot[job-queue]>=22.3",
|
||||
]
|
31
renovate.json5
Normal file
31
renovate.json5
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"dependencyDashboard": true,
|
||||
"regexManagers": [
|
||||
{
|
||||
"fileMatch": ["kustomization\\.yaml"],
|
||||
"matchStrings": [
|
||||
"- name:\\s*(?<depName>\\S+)\\s*\\n\\s*newName:\\s*(?<lookupName>\\S+)\\s*\\n\\s*newTag:\\s*(?<currentValue>[a-f0-9]{7,40})"
|
||||
],
|
||||
"datasourceTemplate": "docker",
|
||||
"versioningTemplate": "loose",
|
||||
"extractVersionTemplate": "{{ currentValue }}",
|
||||
"lookupNameTemplate": "{{ lookupName }}"
|
||||
},
|
||||
],
|
||||
"packageRules": [
|
||||
// Fully automatically update the container version referenced in the deployment
|
||||
{
|
||||
"matchPackageNames": ["@kubernetes-sigs/kustomize"],
|
||||
"automerge": true,
|
||||
"automergeType": "branch",
|
||||
"ignoreTests": true,
|
||||
"commitMessagePrefix": "[skip ci]",
|
||||
"registryUrls": [
|
||||
"https://git.kluster.moll.re"
|
||||
],
|
||||
// Since the images use the commit sha as their tag, simply match any tag
|
||||
"versioning": "regex:(.*)"
|
||||
}
|
||||
],
|
||||
}
|
153
uv.lock
generated
Normal file
153
uv.lock
generated
Normal file
@@ -0,0 +1,153 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "apscheduler"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzlocal" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/00/6d6814ddc19be2df62c8c898c4df6b5b1914f3bd024b780028caa392d186/apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133", size = 107347, upload-time = "2024-11-24T19:39:26.463Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/ae/9a053dd9229c0fde6b1f1f33f609ccff1ee79ddda364c756a924c6d8563b/APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da", size = 64004, upload-time = "2024-11-24T19:39:24.442Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.7.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "journal-bot"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "peewee" },
|
||||
{ name = "python-telegram-bot", extra = ["job-queue"] },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anyio", specifier = ">=4.9.0" },
|
||||
{ name = "peewee", specifier = ">=3.18.2" },
|
||||
{ name = "python-telegram-bot", extras = ["job-queue"], specifier = ">=22.3" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "peewee"
|
||||
version = "3.18.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/89/76f6f1b744c8608e0d416b588b9d63c2a500ff800065ae610f7c80f532d6/peewee-3.18.2.tar.gz", hash = "sha256:77a54263eb61aff2ea72f63d2eeb91b140c25c1884148e28e4c0f7c4f64996a0", size = 949220, upload-time = "2025-07-08T12:52:03.941Z" }
|
||||
|
||||
[[package]]
|
||||
name = "python-telegram-bot"
|
||||
version = "22.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/db/fc/0196e0d7ad247011a560788db204e0a28d76ab75b3d7c7131878f8fb5a06/python_telegram_bot-22.3.tar.gz", hash = "sha256:513d5ab9db96dcf25272dad0a726555e80edf60d09246a7d0d425b77115f5440", size = 1464513, upload-time = "2025-07-20T20:03:09.805Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/54/0955bd46a1e046169500e129c7883664b6675d580074d68823485e4d5de1/python_telegram_bot-22.3-py3-none-any.whl", hash = "sha256:88fab2d1652dbfd5379552e8b904d86173c524fdb9270d3a8685f599ffe0299f", size = 717115, upload-time = "2025-07-20T20:03:07.261Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
job-queue = [
|
||||
{ name = "apscheduler" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||
]
|
Reference in New Issue
Block a user