Compare commits
52 Commits
c1e7c0eb38
...
main
Author | SHA1 | Date | |
---|---|---|---|
cfcc88f8d4 | |||
3d69f9b5d8 | |||
0ee2e3ac72 | |||
840fd90b75 | |||
5e514bcc20 | |||
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 | ||
![]() |
ec87c6751c | ||
724b17c4b7 | |||
![]() |
8b7a318e6b | ||
![]() |
a0e1aaa779 | ||
![]() |
94933c2da0 | ||
c9254a3e88 | |||
![]() |
3097594482 | ||
49df5a4495 | |||
![]() |
9eb7f5bb77 | ||
35dbbe4ece | |||
6b63276dd7 | |||
![]() |
d40afca1a4 | ||
0e2b714848 | |||
df55dbf6c7 |
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
|
50
.gitea/workflows/build_container.yaml
Normal file
50
.gitea/workflows/build_container.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
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: Fetch tags from main branch
|
||||
id: version
|
||||
run: |
|
||||
git fetch origin main --tags
|
||||
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
# remove the 'v' prefix from the tag name
|
||||
echo "BUILD_NAME=${LATEST_TAG//v}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Output the version that is being used
|
||||
run: |
|
||||
echo "Building for version ${{ steps.version.outputs.BUILD_NAME }}"
|
||||
|
||||
|
||||
- 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:${{ steps.version.outputs.BUILD_NAME }}-${{ gitea.run_number }}
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -2,28 +2,33 @@ import datetime
|
||||
import os
|
||||
from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, filters, CallbackQueryHandler
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
ACTION_CHOICE, DATE_ENTRY, ADD_CONTENT = range(3)
|
||||
from telegram.constants import ParseMode
|
||||
import models
|
||||
|
||||
ENTRY_OPTIONS, CONTENT_ENTRY, DAY_RATING = range(3)
|
||||
BUTTON_COUNT = 5
|
||||
|
||||
|
||||
from .basehandler import BaseHandler
|
||||
|
||||
class JournalHandler(BaseHandler):
|
||||
def __init__(self, entry_string, models):
|
||||
self.models = models
|
||||
def __init__(self, entry_string):
|
||||
self.entry_string = entry_string
|
||||
self.handler = ConversationHandler(
|
||||
entry_points=[CommandHandler(entry_string, self.entry_point)],
|
||||
states={
|
||||
ACTION_CHOICE: [
|
||||
CallbackQueryHandler(self.date_choice, pattern="today|yesterday"),
|
||||
CallbackQueryHandler(self.date_custom, pattern="custom"),
|
||||
CallbackQueryHandler(self.option_delete, pattern="delete")
|
||||
],
|
||||
DATE_ENTRY: [
|
||||
ENTRY_OPTIONS: [
|
||||
CallbackQueryHandler(self.date_button, pattern=r"^\d{8}$"), # a serialized date
|
||||
CallbackQueryHandler(self.date_custom, pattern=r"^\d{1,3}$"), # a ~ small delta, symbolizing a new range to show
|
||||
CallbackQueryHandler(self.option_delete, pattern="delete"),
|
||||
MessageHandler(filters.ALL, self.date_entry),
|
||||
],
|
||||
ADD_CONTENT: [
|
||||
CONTENT_ENTRY: [
|
||||
MessageHandler(filters.ALL, self.content_save),
|
||||
]
|
||||
],
|
||||
DAY_RATING: [
|
||||
CallbackQueryHandler(self.day_rating_save),
|
||||
],
|
||||
},
|
||||
fallbacks=[],
|
||||
)
|
||||
@@ -33,70 +38,91 @@ 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 = get_names(dates)
|
||||
callbacks = [d.strftime("%d%m%Y") for d in dates]
|
||||
|
||||
options = [[
|
||||
InlineKeyboardButton("Today", callback_data="today"),
|
||||
InlineKeyboardButton("Yesterday", callback_data="yesterday"),
|
||||
InlineKeyboardButton("Custom date", callback_data="custom"),
|
||||
options = [
|
||||
[InlineKeyboardButton(n, callback_data=c)] for n,c in zip(names[::-1], callbacks[::-1])
|
||||
] + [
|
||||
[
|
||||
InlineKeyboardButton("<<", callback_data=BUTTON_COUNT + 2)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Delete", callback_data="delete")
|
||||
]
|
||||
]
|
||||
keyboard = InlineKeyboardMarkup(options)
|
||||
await update.message.reply_text("Please choose an option for the entry:", reply_markup=keyboard)
|
||||
return ACTION_CHOICE
|
||||
await update.message.reply_text("Please choose a date \(or type it in the format _DDMMYYYY_\)", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN_V2)
|
||||
return ENTRY_OPTIONS
|
||||
|
||||
|
||||
async def date_choice(self, update, context):
|
||||
async def date_button(self, update, context):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
if query.data == "today":
|
||||
date = datetime.datetime.now().date()
|
||||
elif query.data == "yesterday":
|
||||
date = datetime.datetime.now().date() - datetime.timedelta(days=1)
|
||||
else:
|
||||
raise ValueError("Invalid date choice")
|
||||
date = datetime.datetime.strptime(query.data, "%d%m%Y").date()
|
||||
|
||||
with self.models.db:
|
||||
self.current_model, new = self.models.JournalEntry.get_or_create(
|
||||
with models.db:
|
||||
self.current_model, new = models.JournalEntry.get_or_create(
|
||||
date = date
|
||||
)
|
||||
if new:
|
||||
count = self.models.JournalEntry.select().count()
|
||||
count = models.JournalEntry.select().count()
|
||||
await query.edit_message_text(
|
||||
text=f"Journal entry no. {count}. What happened on {self.current_model.date_pretty}?"
|
||||
)
|
||||
else:
|
||||
await query.edit_message_text(text="An entry already exists for this date")
|
||||
return ConversationHandler.END
|
||||
|
||||
return ADD_CONTENT
|
||||
|
||||
return CONTENT_ENTRY
|
||||
|
||||
|
||||
async def date_custom(self, update, context):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
await query.edit_message_text(text="Please enter the date in the format DDMMYYYY")
|
||||
return DATE_ENTRY
|
||||
delta = int(query.data)
|
||||
|
||||
dates = [(datetime.datetime.now() - datetime.timedelta(days = i + delta)).date() for i in range(BUTTON_COUNT)][::-1]
|
||||
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[::-1], callbacks[::-1])
|
||||
] + [
|
||||
[
|
||||
InlineKeyboardButton("<<", callback_data=delta + BUTTON_COUNT)
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton("Delete", callback_data="delete")
|
||||
]
|
||||
]
|
||||
keyboard = InlineKeyboardMarkup(options)
|
||||
await query.edit_message_text("Please choose a date \(or type it in the format _DDMMYYYY_\)", parse_mode=ParseMode.MARKDOWN_V2, reply_markup=keyboard)
|
||||
|
||||
return ENTRY_OPTIONS
|
||||
|
||||
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")
|
||||
return DATE_ENTRY
|
||||
|
||||
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 self.models.db:
|
||||
self.current_model = self.models.JournalEntry.get_or_none(
|
||||
with models.db:
|
||||
self.current_model = models.JournalEntry.get_or_none(
|
||||
date = date
|
||||
)
|
||||
if self.current_model:
|
||||
@@ -106,23 +132,23 @@ class JournalHandler(BaseHandler):
|
||||
context.chat_data["delete"] = False
|
||||
return ConversationHandler.END
|
||||
else:
|
||||
with self.models.db:
|
||||
self.current_model, new = self.models.JournalEntry.get_or_create(
|
||||
with models.db:
|
||||
self.current_model, new = models.JournalEntry.get_or_create(
|
||||
date = date
|
||||
)
|
||||
if not new:
|
||||
await update.message.reply_text("An entry already exists for this date")
|
||||
return ConversationHandler.END
|
||||
else:
|
||||
count = self.models.JournalEntry.select().count()
|
||||
count = models.JournalEntry.select().count()
|
||||
await update.message.reply_text(
|
||||
text=f"Journal entry no. {count}. What happened on {self.current_model.date_pretty}?"
|
||||
)
|
||||
return ADD_CONTENT
|
||||
return CONTENT_ENTRY
|
||||
|
||||
|
||||
async def content_save(self, update, context):
|
||||
with self.models.db:
|
||||
with models.db:
|
||||
self.current_model.author_id = update.message.from_user.id
|
||||
|
||||
if update.message.text:
|
||||
@@ -132,29 +158,64 @@ 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)
|
||||
|
||||
self.current_model.text = update.message.caption
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
async def option_delete(self, update, context):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
await query.edit_message_text(text="Please enter the date in the format DDMMYYYY")
|
||||
await query.edit_message_text(text="Please enter the date in the format _DDMMYYYY_", parse_mode=ParseMode.MARKDOWN_V2)
|
||||
context.chat_data["delete"] = True
|
||||
return DATE_ENTRY
|
||||
return ENTRY_OPTIONS
|
||||
|
||||
|
||||
async def delete_entry(self, update, context):
|
||||
with self.models.db:
|
||||
with models.db:
|
||||
self.current_model.delete_instance()
|
||||
context.chat_data["delete"] = False
|
||||
await update.message.reply_text(text="Entry deleted ✅")
|
||||
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
|
||||
|
@@ -15,8 +15,7 @@ from ..basehandler import BaseHandler
|
||||
class ListHandler(BaseHandler):
|
||||
"""Create and edit lists"""
|
||||
|
||||
def __init__(self, entry_string, models):
|
||||
del models # not needed here, but part of the template
|
||||
def __init__(self, entry_string):
|
||||
self.entry_string = entry_string
|
||||
|
||||
set_db(PERSISTENCE_DIR / "lists.sqlite")
|
||||
|
@@ -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)
|
||||
|
@@ -1,15 +1,16 @@
|
||||
import os
|
||||
from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, filters, CallbackQueryHandler, CallbackContext
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, InputMediaPhoto
|
||||
# ACTION_CHOICE, DATE_ENTRY, ADD_CONTENT = range(3)
|
||||
import models
|
||||
from telegram.constants import ParseMode
|
||||
|
||||
MEMORY_CHOICE = range(1)
|
||||
|
||||
|
||||
from .basehandler import BaseHandler
|
||||
|
||||
class MemoryHandler(BaseHandler):
|
||||
def __init__(self, entry_string, models):
|
||||
self.models = models
|
||||
def __init__(self, entry_string):
|
||||
self.entry_string = entry_string
|
||||
self.handler = ConversationHandler(
|
||||
entry_points=[CommandHandler(entry_string, self.entry_point, )],
|
||||
@@ -27,20 +28,20 @@ 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
|
||||
|
||||
search_string = " ".join(context.args)
|
||||
|
||||
if search_string == '~photo':
|
||||
matching_models = self.models.JournalEntry.select().where(self.models.JournalEntry.media_path != "").order_by(self.models.JournalEntry.date)
|
||||
matching_models = models.JournalEntry.select().where(models.JournalEntry.media_path != "").order_by(models.JournalEntry.date)
|
||||
else: # searching for text
|
||||
matching_models = self.models.JournalEntry.select().where(
|
||||
self.models.JournalEntry.text.contains(
|
||||
matching_models = models.JournalEntry.select().where(
|
||||
models.JournalEntry.text.contains(
|
||||
search_string
|
||||
)
|
||||
).order_by(self.models.JournalEntry.date)
|
||||
).order_by(models.JournalEntry.date)
|
||||
|
||||
|
||||
# exit if no memory matches the string
|
||||
@@ -55,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):
|
||||
@@ -68,20 +69,23 @@ 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.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.text}"
|
||||
message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
|
||||
return ConversationHandler.END
|
||||
|
@@ -6,16 +6,18 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
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, models):
|
||||
|
||||
def __init__(self, entry_string):
|
||||
self.start_time = datetime.datetime.now()
|
||||
self.entry_string = entry_string
|
||||
self.models = models
|
||||
self.handler = ConversationHandler(
|
||||
entry_points=[CommandHandler(self.entry_string, self.entry_point)],
|
||||
states={
|
||||
@@ -37,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
|
||||
@@ -49,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
|
||||
|
||||
|
||||
|
@@ -4,17 +4,17 @@ from telegram.error import BadRequest
|
||||
import logging
|
||||
from datetime import time, timedelta, timezone, datetime, date
|
||||
from peewee import fn
|
||||
import models
|
||||
|
||||
CHAT_ID = os.getenv("CHAT_ID")
|
||||
|
||||
|
||||
class SetChatPhotoJob():
|
||||
def __init__(self, models, bot: ExtBot, job_queue):
|
||||
self.models = models
|
||||
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 self.models.db:
|
||||
possible_photos = self.models.JournalEntry.select().where(
|
||||
self.models.JournalEntry.media_path != 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:
|
||||
|
92
bot/cronjob/leaderboard.py
Normal file
92
bot/cronjob/leaderboard.py
Normal file
@@ -0,0 +1,92 @@
|
||||
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, time=sending_time, days=(0,))
|
||||
|
||||
|
||||
async def callback_leaderboard(self, context):
|
||||
"""Send a weakly leaderboard to the chat."""
|
||||
|
||||
# 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
|
||||
)
|
@@ -1,17 +1,19 @@
|
||||
from datetime import time, timedelta, timezone, datetime, date
|
||||
from telegram.constants import ParseMode
|
||||
import os
|
||||
from peewee import fn
|
||||
import logging
|
||||
import models
|
||||
from telegram.ext import ExtBot
|
||||
|
||||
class RandomMemoryJob():
|
||||
def __init__(self, models, bot, job_queue):
|
||||
self.models = models
|
||||
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
|
||||
@@ -21,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 self.models.db:
|
||||
possible_entries = self.models.JournalEntry.select().where(
|
||||
(self.models.JournalEntry.last_shown <= datetime.today().date() - timedelta(days=self.min_age)) | \
|
||||
(self.models.JournalEntry.last_shown == 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:
|
||||
@@ -41,21 +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.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.text}"
|
||||
text = message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
|
||||
|
18
bot/main.py
18
bot/main.py
@@ -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",
|
||||
@@ -23,14 +23,16 @@ def main() -> None:
|
||||
models.set_db(db_path)
|
||||
application = Application.builder().token(token).build()
|
||||
|
||||
application.add_handler(journal.JournalHandler("journal", models).handler)
|
||||
application.add_handler(list.ListHandler("list", models).handler)
|
||||
application.add_handler(status.StatusHandler("status", models).handler)
|
||||
application.add_handler(journal.JournalHandler("journal").handler)
|
||||
application.add_handler(list.ListHandler("list").handler)
|
||||
application.add_handler(status.StatusHandler("status").handler)
|
||||
application.add_handler(turtle.TurtleHandler().handler)
|
||||
application.add_handler(memory.MemoryHandler("memory", models).handler)
|
||||
application.add_handler(memory.MemoryHandler("memory").handler)
|
||||
application.add_handler(advent.AdventsHandler("advent").handler)
|
||||
|
||||
random_memory.RandomMemoryJob(models, application.bot, application.job_queue)
|
||||
chat_photo.SetChatPhotoJob(models, application.bot, application.job_queue)
|
||||
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()
|
||||
|
@@ -1,7 +1,8 @@
|
||||
from peewee import *
|
||||
from pathlib import Path
|
||||
import re
|
||||
import os
|
||||
import datetime
|
||||
import socket
|
||||
|
||||
ID_MAPPINGS = {
|
||||
"Lia": 5603036217,
|
||||
@@ -9,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):
|
||||
@@ -27,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}"
|
||||
@@ -60,6 +74,25 @@ class JournalEntry(BaseModel):
|
||||
except ValueError: #fck windows
|
||||
return self.date.strftime('%a, %d. %b %Y')
|
||||
|
||||
@property
|
||||
def spoiler_text(self) -> str:
|
||||
"""Returns the text with all the frisky details hidden away"""
|
||||
new_text = self.text.replace("<", "<").replace(">", ">").replace("&", "&")
|
||||
pattern = re.compile(
|
||||
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.53
|
||||
- name: journal
|
||||
newName: git.kluster.moll.re/remoll/journal-bot
|
||||
newTag: 1.1.0-17
|
||||
|
@@ -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",
|
||||
]
|
17
renovate.json5
Normal file
17
renovate.json5
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"dependencyDashboard": true,
|
||||
"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"
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
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