Compare commits
109 Commits
secretbran
...
renovate/h
Author | SHA1 | Date | |
---|---|---|---|
9f562d3430 | |||
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 | ||
![]() |
ec87c6751c | ||
724b17c4b7 | |||
![]() |
8b7a318e6b | ||
![]() |
a0e1aaa779 | ||
c9254a3e88 | |||
![]() |
3097594482 | ||
49df5a4495 | |||
![]() |
9eb7f5bb77 | ||
35dbbe4ece | |||
6b63276dd7 | |||
![]() |
d40afca1a4 | ||
0e2b714848 | |||
df55dbf6c7 | |||
![]() |
c1e7c0eb38 | ||
c58e256194 | |||
![]() |
cbaedb04cb | ||
008cf08163 | |||
b1a9e5fa46 | |||
![]() |
7e3cf46765 | ||
d6447c54d1 | |||
![]() |
db44a38eb7 | ||
![]() |
a77ab50f82 | ||
![]() |
63daf6845c | ||
![]() |
7d793c571c | ||
![]() |
a805775707 | ||
![]() |
9ee7302f65 | ||
![]() |
58fb79820f | ||
![]() |
9518b813f6 | ||
![]() |
122b090475 | ||
![]() |
6a100e7364 | ||
![]() |
0a14e8be4a | ||
![]() |
bdb58c8a5f | ||
![]() |
c526c23e3a | ||
![]() |
8ff3f05034 | ||
![]() |
1ce47df5f4 | ||
![]() |
fa5427ec09 | ||
![]() |
378795d3ef | ||
![]() |
8ea0c2f517 | ||
![]() |
3622e76639 | ||
![]() |
d5349433d4 | ||
![]() |
282a3d305d | ||
![]() |
e5d24b0171 | ||
![]() |
0fb841fce4 | ||
![]() |
3507d16394 | ||
![]() |
6952e7f12f | ||
![]() |
674a88f6ab | ||
![]() |
11aefc0322 | ||
![]() |
7273dd5121 | ||
![]() |
350bd19b08 | ||
e3f643e805 | |||
970d6931db | |||
b39aa6ecb1 | |||
9b54f05d75 | |||
![]() |
cd7594350f | ||
c202ad8035 | |||
![]() |
50e7226709 | ||
![]() |
ae330eb389 | ||
ebc89b48e2 | |||
0f49cbf4fb | |||
cd3bbe9b00 | |||
1bc829f53d | |||
3975ca5997 | |||
2afde219a9 | |||
a666b8e9ae | |||
e77c106813 | |||
86a9762f39 | |||
3455946996 | |||
3f58eab8c7 | |||
![]() |
1613e05b61 | ||
![]() |
0860196a53 | ||
49339ebcb9 |
26
.drone.yml
26
.drone.yml
@@ -1,26 +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: latest
|
||||
|
||||
|
||||
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@v5
|
||||
|
||||
- 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",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
14
Dockerfile
14
Dockerfile
@@ -1,12 +1,14 @@
|
||||
FROM python:3.10-slim
|
||||
ENV DOCKERIZED=true
|
||||
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
|
||||
|
||||
COPY Pipfile Pipfile.lock ./
|
||||
|
||||
RUN pip install pipenv && pipenv install --system --deploy
|
||||
# copy the requirements and install them
|
||||
COPY pyproject.toml uv.lock .
|
||||
RUN uv sync --frozen
|
||||
|
||||
# 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
|
10
Pipfile
10
Pipfile
@@ -1,10 +0,0 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
python-telegram-bot = "*"
|
||||
peewee = "*"
|
||||
|
||||
[dev-packages]
|
100
Pipfile.lock
generated
100
Pipfile.lock
generated
@@ -1,100 +0,0 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "e95b9deab62bd0c661f20a178b8701fc84420db5f663fa4416666e1d05f6ce76"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421",
|
||||
"sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.2'",
|
||||
"version": "==3.6.2"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3",
|
||||
"sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2022.12.7"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
|
||||
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb",
|
||||
"sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.16.3"
|
||||
},
|
||||
"httpx": {
|
||||
"hashes": [
|
||||
"sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9",
|
||||
"sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.23.3"
|
||||
},
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:4d1d4b643ce158aa17a0987b84005eaf25fe0ce8b38fd234099594985611c198",
|
||||
"sha256:d0aa53e1f06d7cb7919cc0e2d6c81a02d968fc29921aeaa962edd1efb816a9bd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.2"
|
||||
},
|
||||
"rfc3986": {
|
||||
"extras": [
|
||||
"idna2008"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
|
||||
"sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
|
||||
],
|
||||
"version": "==1.5.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
|
||||
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.0"
|
||||
}
|
||||
},
|
||||
"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;
|
||||
```
|
||||
|
@@ -2,12 +2,21 @@ 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
|
||||
@@ -66,12 +75,11 @@ class AdventsHandler(BaseHandler):
|
||||
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=open(".bot_storage\stickers\stickerwhat.tgs", "rb"))
|
||||
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()
|
||||
@@ -79,7 +87,7 @@ class AdventsHandler(BaseHandler):
|
||||
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=open(".bot_storage\stickers\stickerkiss.tgs", "rb"))
|
||||
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
|
||||
@@ -101,7 +109,8 @@ class AdventsHandler(BaseHandler):
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
picture_number = random.randint(1,31)
|
||||
await update.effective_message.reply_photo(photo=open(".bot_storage\pretty_pictures\photo_"+f"{picture_number}"+"_2023-11-25_14-25-53.jpg", "rb"), caption="So ein cutes Foto!", reply_markup=reply_markup)
|
||||
#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
|
||||
|
||||
@@ -116,11 +125,11 @@ class AdventsHandler(BaseHandler):
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
|
||||
#if tuer_nummer <= int(date.today().strftime("%d")):
|
||||
if (tuer_nummer <= int(date.today().strftime("%d"))) and (int(date.today().strftime("%m"))==12):
|
||||
await update.message.reply_document(document=open(".bot_storage\gifs\gif"+f"{tuer_nummer}"+".mp4", "rb"),caption=f"Türchen für den {tuer_nummer}. Dezember: \n" + self.tuerchen_texte[tuerchen], reply_markup=reply_markup)
|
||||
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=open(".bot_storage\stickers\stickerangry.tgs", "rb"))
|
||||
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
|
||||
@@ -129,7 +138,7 @@ class AdventsHandler(BaseHandler):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
await update.effective_message.reply_sticker(sticker=open(".bot_storage\stickers\stickerbye.tgs", "rb"))
|
||||
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,4 +5,4 @@ class BaseHandler:
|
||||
entry_string: str
|
||||
|
||||
async def entry_point(self, update, context) -> None:
|
||||
self.logger.info(f"Chat said: {self.entry_string}")
|
||||
self.logger.info(f"Chat ({update.message.chat_id}) said: {self.entry_string}")
|
@@ -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,69 +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 = models.JournalEntry.select().count()
|
||||
await query.edit_message_text(
|
||||
text=f"What is your entry for {self.current_model.date_pretty}?"
|
||||
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:
|
||||
@@ -105,20 +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:
|
||||
await update.message.reply_text(f"What is your entry for {self.current_model.date_pretty}?")
|
||||
return ADD_CONTENT
|
||||
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 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:
|
||||
@@ -128,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
|
||||
|
@@ -5,10 +5,7 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from .models import ListModel, set_db, db
|
||||
|
||||
MEDIA_DIR = Path(os.getenv("MEDIA_DIR"))
|
||||
DB_DIR = MEDIA_DIR / "lists_db"
|
||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
PERSISTENCE_DIR = Path(os.getenv("PERSISTENCE_DIR"))
|
||||
|
||||
NAME, NEW, ACTION, ITEMADD, ITEMREMOVE, ITEMTOGGLE = range(6)
|
||||
|
||||
@@ -18,16 +15,17 @@ from ..basehandler import BaseHandler
|
||||
class ListHandler(BaseHandler):
|
||||
"""Create and edit lists"""
|
||||
|
||||
def __init__(self, entry_string, models):
|
||||
self.journal_models = models # not needed here
|
||||
def __init__(self, entry_string):
|
||||
self.entry_string = entry_string
|
||||
|
||||
set_db(PERSISTENCE_DIR / "lists.sqlite")
|
||||
|
||||
self.list_overview_keyboard = [
|
||||
[InlineKeyboardButton("Print list", callback_data="print")],
|
||||
[InlineKeyboardButton("Add item", callback_data="add")],
|
||||
[InlineKeyboardButton("Toggle item", callback_data="toggle")],
|
||||
[InlineKeyboardButton("Remove item", callback_data="remove")],
|
||||
[InlineKeyboardButton("Clear list", callback_data="clear")],
|
||||
[InlineKeyboardButton("Print list", callback_data="print")],
|
||||
[InlineKeyboardButton("Delete list", callback_data="delete")],
|
||||
|
||||
]
|
||||
@@ -40,13 +38,13 @@ class ListHandler(BaseHandler):
|
||||
],
|
||||
NEW : [MessageHandler(filters.TEXT, callback=self.new_listname)],
|
||||
ACTION: [
|
||||
CallbackQueryHandler(self.list_print, pattern="^print$"),
|
||||
CallbackQueryHandler(self.list_add, pattern="^add$"),
|
||||
CallbackQueryHandler(self.list_toggle, pattern="^toggle$"),
|
||||
CallbackQueryHandler(self.list_menu, pattern="^overview$"),
|
||||
CallbackQueryHandler(self.list_remove, pattern="^remove$"),
|
||||
CallbackQueryHandler(self.list_clear, pattern="^clear$"),
|
||||
CallbackQueryHandler(self.list_delete, pattern="^delete$"),
|
||||
CallbackQueryHandler(self.list_print, pattern="^print$"),
|
||||
CallbackQueryHandler(self.list_menu, pattern="^overview$"),
|
||||
],
|
||||
ITEMADD : [MessageHandler(filters.TEXT, callback=self.list_add_item)],
|
||||
ITEMTOGGLE: [CallbackQueryHandler(self.list_toggle_index)],
|
||||
@@ -58,10 +56,9 @@ class ListHandler(BaseHandler):
|
||||
|
||||
async def entry_point(self, update, context) -> None:
|
||||
await super().entry_point(update, context)
|
||||
set_db(DB_DIR / f"chat_{update.message.chat_id}.db")
|
||||
with db:
|
||||
lists = ListModel.select()
|
||||
keyboard = [[InlineKeyboardButton(k.name, callback_data=f"list-{k.name}")] for k in lists] + \
|
||||
lists = ListModel.select().where(ListModel.chat_id == update.effective_chat.id)
|
||||
keyboard = [[InlineKeyboardButton(k.name, callback_data=f"list-{k.id}")] for k in lists] + \
|
||||
[[InlineKeyboardButton("New list", callback_data="new")]]
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
@@ -72,13 +69,13 @@ class ListHandler(BaseHandler):
|
||||
async def choose_list(self, update, context: CallbackContext) -> None:
|
||||
query = update.callback_query
|
||||
data = query.data
|
||||
name = data.replace("list-","")
|
||||
id = data.replace("list-","")
|
||||
await query.answer()
|
||||
context.user_data["current_list"] = ListModel.get(name = name)
|
||||
context.user_data["current_list"] = ListModel.get(id = id)
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(self.list_overview_keyboard)
|
||||
|
||||
await query.edit_message_text("Very well. For " + name + " the following actions are available:", reply_markup=reply_markup)
|
||||
await query.edit_message_text(f"Using {context.user_data['current_list'].name}. Available actions:", reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
@@ -88,7 +85,7 @@ class ListHandler(BaseHandler):
|
||||
|
||||
reply_markup = InlineKeyboardMarkup(self.list_overview_keyboard)
|
||||
|
||||
await query.edit_message_text(f"Very well. For {context.user_data['current_list'].name} the following actions are available:", reply_markup=reply_markup)
|
||||
await query.edit_message_text(f"Using {context.user_data['current_list'].name}. Available actions:", reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
@@ -103,7 +100,7 @@ class ListHandler(BaseHandler):
|
||||
name = update.message.text
|
||||
try:
|
||||
with db:
|
||||
context.user_data["current_list"] = ListModel.create(name = name)
|
||||
context.user_data["current_list"] = ListModel.create(name = name, chat_id=update.effective_chat.id)
|
||||
keyboard = [[InlineKeyboardButton("Add an item", callback_data="add"), InlineKeyboardButton("To the menu!", callback_data="overview")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
await update.message.reply_text("Thanks. List " + name + " was successfully created.", reply_markup=reply_markup)
|
||||
@@ -125,10 +122,17 @@ class ListHandler(BaseHandler):
|
||||
await query.answer()
|
||||
|
||||
list_object = context.user_data["current_list"]
|
||||
keyboard = [[InlineKeyboardButton(v, callback_data=k)] for k,v in list_object.content.items()]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
readable_it = printable_list(list_object)
|
||||
|
||||
if readable_it:
|
||||
msg_content = "Which item would you like to toggle?"
|
||||
keyboard = [[InlineKeyboardButton(v, callback_data=k)] for k,v in zip(list_object.content.keys(), readable_it)]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
else:
|
||||
msg_content = "List empty"
|
||||
reply_markup = None
|
||||
|
||||
await query.edit_message_text("Which item would you like to toggle?", reply_markup = reply_markup)
|
||||
await query.edit_message_text(msg_content, reply_markup = reply_markup)
|
||||
return ITEMTOGGLE
|
||||
|
||||
|
||||
@@ -170,14 +174,10 @@ class ListHandler(BaseHandler):
|
||||
await query.answer()
|
||||
list_object = context.user_data["current_list"]
|
||||
|
||||
content_it = list_object.content.values()
|
||||
done_it = [
|
||||
"· " if e is None \
|
||||
else "✅ " if e \
|
||||
else "❌ " \
|
||||
for e in list_object.done_dict.values()]
|
||||
if content_it:
|
||||
msg_content = "\n".join([f"{d} {c}" for d, c in zip(done_it, content_it)])
|
||||
readable_it = printable_list(list_object)
|
||||
|
||||
if readable_it:
|
||||
msg_content = "\n".join(readable_it)
|
||||
else:
|
||||
msg_content = "List empty"
|
||||
|
||||
@@ -193,7 +193,6 @@ class ListHandler(BaseHandler):
|
||||
new = list_object.content
|
||||
new.update({"random_key": item})
|
||||
list_object.content = new
|
||||
# TODO test me!
|
||||
keyboard = [[InlineKeyboardButton("Add some more", callback_data="add"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
await update.message.reply_text(f"Added {item}", reply_markup=reply_markup)
|
||||
@@ -206,11 +205,10 @@ class ListHandler(BaseHandler):
|
||||
await query.answer()
|
||||
|
||||
list_object = context.user_data["current_list"]
|
||||
old = list_object.done_dict[toggle_key]
|
||||
# if all None or all False (first toggle or all false) then set all dones to False
|
||||
if not any(list_object.done_dict.values()):
|
||||
new_done_dict = dict.fromkeys(list_object.done_dict, False)
|
||||
else: new_done_dict = list_object.done_dict
|
||||
old = list_object.done_dict[toggle_key] or False
|
||||
# if it was previously unset (None), we can later on set it to not old = True
|
||||
|
||||
new_done_dict = list_object.done_dict
|
||||
new_done_dict[toggle_key] = not old
|
||||
list_object.done_dict = new_done_dict
|
||||
|
||||
@@ -236,3 +234,22 @@ class ListHandler(BaseHandler):
|
||||
|
||||
await query.edit_message_text(f"Removed {name}", reply_markup=reply_markup)
|
||||
return ACTION
|
||||
|
||||
|
||||
|
||||
def printable_list(list_object: ListModel):
|
||||
content_it = list_object.content.values()
|
||||
done_bool_it = list_object.done_dict.values()
|
||||
# distinguish the enumeration:
|
||||
# either all done_dict values are None -> the list is not toggleable
|
||||
# or at least one value is of type bool -> the list is toggleable and None === False
|
||||
if any([type(e) == bool for e in done_bool_it]):
|
||||
done_it = [
|
||||
"✅" if e else "❌" \
|
||||
for e in list_object.done_dict.values()
|
||||
]
|
||||
else:
|
||||
done_it = ["-" for e in done_bool_it]
|
||||
|
||||
readable_it = [f"{d} {c}" for d, c in zip(done_it, content_it)]
|
||||
return readable_it
|
||||
|
@@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from peewee import *
|
||||
|
||||
db = DatabaseProxy()
|
||||
|
||||
class BaseModel(Model):
|
||||
@@ -8,11 +8,12 @@ class BaseModel(Model):
|
||||
|
||||
class ListModel(BaseModel):
|
||||
name = CharField(default="")
|
||||
chat_id = IntegerField()
|
||||
|
||||
@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
|
||||
@@ -28,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
|
||||
@@ -45,23 +46,8 @@ class ListEntryModel(BaseModel):
|
||||
done = BooleanField(default=None, null=True)
|
||||
|
||||
|
||||
# class ListModel(BaseModel):
|
||||
# name = CharField(unique=True)
|
||||
# content = TextField(default="") # unlimited length, use to serialise list into
|
||||
|
||||
# @property
|
||||
# def content_list(self):
|
||||
# return json.loads(self.content or '[]')
|
||||
|
||||
# @content_list.setter
|
||||
# def content_list(self, list_content):
|
||||
# self.content = json.dumps(list_content)
|
||||
# with db:
|
||||
# self.save()
|
||||
|
||||
|
||||
|
||||
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 datetime
|
||||
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,27 @@ class MemoryHandler(BaseHandler):
|
||||
|
||||
async def entry_point(self, update: Update, context: CallbackContext):
|
||||
await super().entry_point(update, context)
|
||||
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
|
||||
if len(matching_models) == 0:
|
||||
await update.message.reply_text(f"There is no matching memory yet.")
|
||||
return ConversationHandler.END
|
||||
|
||||
|
||||
options = [[InlineKeyboardButton(m.date_pretty, callback_data=i)] for i,m in enumerate(matching_models)]
|
||||
|
||||
keyboard = InlineKeyboardMarkup(options)
|
||||
@@ -48,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):
|
||||
@@ -61,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
|
||||
|
@@ -4,18 +4,20 @@ import socket
|
||||
from telegram.ext import ConversationHandler, CommandHandler, CallbackQueryHandler
|
||||
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,16 +50,21 @@ class StatusHandler(BaseHandler):
|
||||
ip = "not fetchable"
|
||||
local_ips = "not fetchable"
|
||||
|
||||
message += "Status: Running 🟢\n"
|
||||
message += f"Uptime: `{delta[:delta.rfind('.')]}`\n"
|
||||
message += f"IP \(public\): `{ip}`\n"
|
||||
message += f"IP \(private\): `{local_ips}`\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
|
||||
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, filters, CallbackQueryHandler
|
||||
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from telegram.ext import MessageHandler, filters
|
||||
from telegram import Update
|
||||
import re
|
||||
import random
|
||||
@@ -18,37 +17,47 @@ TURTLE_VIDEOS = list(TURTLE_VIDEO_LOCATION.glob("*.mp4"))
|
||||
class TurtleHandler(BaseHandler):
|
||||
def __init__(self):
|
||||
self.entry_string = "Variation of hallo"
|
||||
self.handler = MessageHandler(filters.Regex(r"[hH]([aA]+|[eE]+)[lL]{2,}[oOöÖ]+(le)?|(chen)") | # react to hello strings
|
||||
filters.Regex(b"\xF0\x9F\x90\xA2".decode("utf8")) | # react to turtle emoji
|
||||
filters.Regex(r"[sS](childkröte)|[tT](urtle)"), # react to turtle string
|
||||
self.entry_point)
|
||||
pass
|
||||
self.handler = MessageHandler(
|
||||
filters.Regex(r"[hH]([aA]+|[eE]+)[lL]{2,}[oOöÖ]+(le|chen)?") |
|
||||
# react to hello strings
|
||||
filters.Regex(b"\xF0\x9F\x90\xA2".decode("utf8")) |
|
||||
# react to turtle emoji
|
||||
filters.Regex(r"[sS](childkröte)|[tT](urtle)"),
|
||||
# react to turtle string
|
||||
self.entry_point
|
||||
)
|
||||
|
||||
|
||||
async def entry_point(self, update: Update, context):
|
||||
await super().entry_point(update, context)
|
||||
msgtxt = update.message.text
|
||||
turtle_emoji = b"\xF0\x9F\x90\xA2".decode("utf8")
|
||||
|
||||
if "hallo" in msgtxt: # react to hallo
|
||||
if "hallo" in msgtxt:
|
||||
# react to hallo
|
||||
vid = TURTLE_VIDEOS[0]
|
||||
answertxt = "Hallo!"
|
||||
elif re.search("[eE][lL]{2,}[oO]", msgtxt): # react to hello
|
||||
elif re.search("[eE][lL]{2,}[oO]", msgtxt):
|
||||
# react to hello
|
||||
vid = TURTLE_VIDEOS[2]
|
||||
answertxt = "Hello!"
|
||||
elif re.search("([aA]{4,}|[lL]{4,}|[oO]{4,}|[öÖ]{4,})", msgtxt): # react to stretched hello
|
||||
elif re.search("([aA]{4,}|[lL]{4,}|[oO]{4,}|[öÖ]{4,})", msgtxt):
|
||||
# react to stretched hello
|
||||
vid = TURTLE_VIDEOS[5]
|
||||
answertxt = "That's a lot of letters!"
|
||||
elif re.search(turtle_emoji, msgtxt): # react to turtle emoji
|
||||
vid=TURTLE_VIDEOS[0] # TODO: choose video for smiley reaction
|
||||
elif re.search(turtle_emoji, msgtxt):
|
||||
# react to turtle emoji
|
||||
vid=TURTLE_VIDEOS[0]
|
||||
answertxt="Turtle detected! Self-destruction mode activated..."
|
||||
elif re.search("[sS](childkröte)|[tT](urtle)", msgtxt): # react to turtle string
|
||||
elif re.search("[sS](childkröte)|[tT](urtle)", msgtxt):
|
||||
# react to turtle string
|
||||
vid=None
|
||||
answertxt=turtle_emoji
|
||||
else:
|
||||
vid = random.choice(TURTLE_VIDEOS[1:2]+TURTLE_VIDEOS[3:5]+TURTLE_VIDEOS[6:])
|
||||
answertxt = ""
|
||||
|
||||
if vid!=None:
|
||||
if vid != None:
|
||||
if re.search(turtle_emoji, msgtxt):
|
||||
await update.message.reply_text(text=answertxt)
|
||||
time.sleep(1)
|
||||
|
@@ -1,22 +1,45 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from telegram.ext import ExtBot
|
||||
import random
|
||||
from telegram.error import BadRequest
|
||||
import logging
|
||||
from datetime import time, timedelta, timezone, datetime, date
|
||||
from peewee import fn
|
||||
import models
|
||||
|
||||
MEDIA_DIR = Path(os.getenv("MEDIA_DIR"))
|
||||
CHAT_ID = os.getenv("CHAT_ID")
|
||||
|
||||
|
||||
async def set_random(bot: ExtBot) -> None:
|
||||
"""Set a random chat photo."""
|
||||
if os.getenv("DOCKERIZED", "false") == "false":
|
||||
# only change image on prod
|
||||
return
|
||||
|
||||
photos = list(MEDIA_DIR.glob("*.jpg")) + list(MEDIA_DIR.glob("*.png")) + list(MEDIA_DIR.glob("*.jpeg"))
|
||||
|
||||
if len(photos) == 0:
|
||||
return
|
||||
class SetChatPhotoJob():
|
||||
def __init__(self, bot: ExtBot, job_queue):
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
photo = random.choice(photos)
|
||||
await bot.set_chat_photo(CHAT_ID, photo)
|
||||
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:
|
||||
# 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_monthly(self.callback_photo, when=sending_time, day=-1)
|
||||
|
||||
|
||||
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:
|
||||
self.logger.warning("No photos available.")
|
||||
return
|
||||
|
||||
chat_id = os.getenv("CHAT_ID")
|
||||
try:
|
||||
await self.bot.set_chat_photo(chat_id, chosen_entry.media_path)
|
||||
except BadRequest:
|
||||
self.logger.error("This is a private chat!")
|
||||
return
|
||||
|
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
|
||||
)
|
66
bot/cronjob/random_memory.py
Normal file
66
bot/cronjob/random_memory.py
Normal file
@@ -0,0 +1,66 @@
|
||||
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, bot: ExtBot, job_queue):
|
||||
self.bot = bot
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
if not models.IS_PRODUCTION:
|
||||
# when running locally, annoy the programmer every 60 seconds <3
|
||||
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
|
||||
sending_time = time(hour=12, minute=0, second=0, tzinfo=timezone(timedelta(hours=2)))
|
||||
job_queue.run_daily(self.callback_memory, sending_time)
|
||||
self.min_age = 30 # days
|
||||
|
||||
|
||||
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:
|
||||
self.logger.warning("Come back later for another memory.")
|
||||
return
|
||||
|
||||
# update the last_shown of the chosen entry
|
||||
chosen_entry.last_shown = datetime.today().date()
|
||||
chosen_entry.save()
|
||||
|
||||
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 = message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
else:
|
||||
await self.bot.send_message(
|
||||
chat_id = chat_id,
|
||||
text = message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
)
|
||||
|
24
bot/main.py
24
bot/main.py
@@ -5,14 +5,13 @@ import logging
|
||||
import models
|
||||
from commands import journal, status, turtle, memory, advent
|
||||
from commands.list import list
|
||||
from cronjob import chat_photo
|
||||
from cronjob import chat_photo, random_memory, leaderboard
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
level=logging.INFO
|
||||
)
|
||||
import asyncio
|
||||
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -24,22 +23,17 @@ 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)
|
||||
|
||||
# application.add_handler(CommandHandler("help", help_command))
|
||||
# on non command i.e message - echo the message on Telegram
|
||||
# application.add_handler(InlineQueryHandler(inline_query))
|
||||
random_memory.RandomMemoryJob(application.bot, application.job_queue)
|
||||
chat_photo.SetChatPhotoJob(application.bot, application.job_queue)
|
||||
leaderboard.SendLeaderboard(application.bot, application.job_queue)
|
||||
|
||||
|
||||
# on every start set a new chat photo
|
||||
# loop = asyncio.get_event_loop()
|
||||
asyncio.ensure_future(chat_photo.set_random(application.bot))
|
||||
# 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
|
||||
";
|
||||
}
|
@@ -1,12 +1,11 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
namespace: journal
|
||||
name: journal-bot
|
||||
labels:
|
||||
app: journal-bot
|
||||
spec:
|
||||
# deployment running a single container
|
||||
# deployment running a single container
|
||||
selector:
|
||||
matchLabels:
|
||||
app: journal-bot
|
||||
@@ -18,8 +17,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: journal
|
||||
image: mollre/journal-bot:latest
|
||||
imagePullPolicy: Always
|
||||
image: journal
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: journal-secret-env
|
||||
@@ -35,38 +33,3 @@ spec:
|
||||
- name: journal-nfs
|
||||
persistentVolumeClaim:
|
||||
claimName: journal-data-nfs
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
namespace: journal
|
||||
name: "journal-data-nfs"
|
||||
# labels:
|
||||
# directory: "journal-data"
|
||||
spec:
|
||||
storageClassName: fast
|
||||
capacity:
|
||||
storage: "5Gi"
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
nfs:
|
||||
path: /export/kluster/journal-bot
|
||||
server: 192.168.1.157
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
namespace: journal
|
||||
name: "journal-data-nfs"
|
||||
spec:
|
||||
storageClassName: "fast"
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: "5Gi"
|
||||
# selector:
|
||||
# matchLabels:
|
||||
# directory: "journal-data"
|
||||
|
||||
|
15
deployment/kustomization.yaml
Normal file
15
deployment/kustomization.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ./namespace.yaml
|
||||
- ./deployment.yaml
|
||||
- ./sealedsecret.yaml
|
||||
- ./pvc.yaml
|
||||
|
||||
namespace: journal
|
||||
|
||||
images:
|
||||
- name: journal
|
||||
newName: git.kluster.moll.re/remoll/journal-bot
|
||||
newTag: 1.1.0-17
|
4
deployment/namespace.yaml
Normal file
4
deployment/namespace.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
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
|
||||
|
25
deployment/sealedsecret.yaml
Normal file
25
deployment/sealedsecret.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"kind": "SealedSecret",
|
||||
"apiVersion": "bitnami.com/v1alpha1",
|
||||
"metadata": {
|
||||
"name": "journal-secret-env",
|
||||
"namespace": "journal",
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"spec": {
|
||||
"template": {
|
||||
"metadata": {
|
||||
"name": "journal-secret-env",
|
||||
"namespace": "journal",
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"type": "Opaque"
|
||||
},
|
||||
"encryptedData": {
|
||||
"BOT_TOKEN": "AgBcFTg4wRI37/sXlk7bNO3IB9dC2CaVi/Oh9TQeQ/wN+rRkTTqp/dBpzX3Y1Zcp16d59AqT7y3DafGGZ/V87zcxG1bngdCZUsZfDmZMP0+z+10caMxjbSY4xfBW1/MEL3rW6ONOibhTAI9DDS3p3YCu1V92xRLvOUkwc+mCkV7fneWWGU/wgeci+C75PKTyGIilo5NZXROOyytN7BQwvOiY70j9m/NC9L0Ulbcppho9iuVsYVfkkWHOU6/OcOeL7vjWcvYFTleyI0oclLgtBUSJzBTqe5eJeZPGoVWMGwyMw2BqR8DgeGpDIuSnMEgssh9wUlhVvqkoI7CEUrJy5Rb2YnWcriIbfUfUMwbfc4EpBKt1VVlQcEQmN9jJdoOJ81ywRXl6CJTMMM4apB3iHsBWdzVXCG4I5c6Mv8+xg0V+AYdDt4pGwuRX9s66LShnjFJKnn68chNLnGfNlU68YdwFio0GJkV0/FnXIgTrOwdOovtBz1Gl3ORIWTmSkY7yBRyYBzvzEBlXBAuj51yhdykrmuW6B5CvwqXsm3ia1fvtWwNfvmKySjzbHZQHmbbYR9hLvWm+rPS7TFXw52W6jUyvHh6U2mSHwpwI2byIE+uhXjVQgYbmDgJW2gif3Aam+2VSaGSqWUz7ECSGy4mVC755CyoZl0HDP4PxuHq5kcGm34qmjTRNYM6Y2QvaVb7cBplsZfs+cH+gcfVu2gg3KvAEaFJk85Hz4pirRhcPsLNce6Iw5OegCZ5IQBrzOJXC",
|
||||
"CHAT_ID": "AgDIgpsygMIcsTDy8a49isS4Hfkmqa0oav+q7Mu7VtcPyrZ4o7hR1u/IlH8Qt4Cg/9QxOw7rJ4DfbK4GDmiOO1oOf0uaR6btLl+/GoKT3mbSHusWfHPrJDGX0SBFw7rOopC+LyFgDHPJEhbKviwnyrBkUuI6gnf1sic4jJ9arb/B97y89dMKFlVCbEzRCrTCK6WDBQ3Lpk+5MI+ugAPSKC8CqsjNc6jmWymdGMk/9n3sAdalfYBCucxHKeVgkrv4sPr9jEUEzIziKansavTs8qVbZgSUMEAAob2KBIAXLcRmo5ISwKvppuA6DMbbXEYEMHVJH9B4gI2eAxClOPVEOBElL+BtsJaSJnJbVEclMzOqwxXQRFPOq4BKxhguA+Uj8Vl8/2diwXEJoUiCZ5emGVvCFQd+Dr2LUPj4AO1AL4zAg+VahuqNV2gI/Dkxgt7Hj+i6jY/jmbk3MIJYjeZh0irmfsWQmUMcmizhxutQdV3kXhHSlomVDHuIdFHFIbjQrI8vSgeysQARSxrJZvt/qeNUNnD1InKa/EQ8I5XDX4o4qIV/pqY8XLVoTcciYDOPZEy3OleHK+26SJDkJOiDOAHbfIBeinaLvYIEW3BwgrakBD16HaNzYBPLPW2ikDCSBTyFRayvfkWHHUGawhdrauxvZzp5UsJViZogypBJuDT3SPvAOlAR4X8Yfr9SmwQYKv8rALH+wW7QtON6R9D7UA==",
|
||||
"DB_PATH": "AgACucJGoBiO/ymyf6FvEeuQ2MDo+c+VgNk30xY2EZSQZpAbNE6VbaMO0lZj++T82OOBnhfmXID+TWJTIwIHaD2nkPq/ISppBIobVmtUmsR+sm76tao02HJCtPGycyCu1zhEpKtwy3k5nsd0jclq4bQFHccnBaBdZ6xcvmevvJ+YddysHhjSb6ESOoah/5lGiAa3sHe1Hwg57FPZeVuZOCx+MbbmhAXCYbu2bdZzSWA/mFAf1F6qkxSAuVFMJtijPrM13UIVONBPg5E02NO1VEV+LkI5NTUNx9YHVlGNPxogabDq+lhNwulWtLPbKkAXn/CTgcB2vb03geeVN20yz1UzbrdV5CKpIGGZ1At8ehuNypFa595XBVFSmL8RWNmfHCAvypAypjxOMWa89qC0diJW4tY+BUrl2jrIXpARNlD2GTqp7InLtwFsFb3AUbpg+0mnqqiqmtqYKQWJj0WDvfs7ol6k6Qx7P0FdAfRq3ojzArRQ7MruI24CS1hGTQ/hBJaHrcMzEIn/ZkKqZ2fEdNkJt8tTfxVsZ/paE97ERfYeKsuK2uHqUgvSQ+0w7FK7m6PYNpY9gmqZMaqRA7VIRblNauEuLDAGw8Jgyb3YUiV3xLjBuvaCaAEbXOsNsIF35+ZCw2bsYmgSxqNO2b1LvW/uFSUonjVJ9yT9+Qv6V0YJhMztzDLpNKRIEBKgyUN/BE7hV9vtygaan/AkUVmHVA9BwrU=",
|
||||
"PERSISTENCE_DIR": "AgDF8D5PfJ4DF7wNd8hpEPdph3r/MvK8R/sREX83/b7jFJgSGb/Dku82vwtkDTzsk9A+gGgU//EILYt5Irlo5ObHlcKfimR4fTvdcO9lpBHJnbwXipUcg20Xz/awIGbD3yJmy0LJqgc5MVPtWicF/ZsxQkkcv3+DN4+BgXczncEL+3g55eEbCAwbmFSvdHWIZGbi04VmtmAlSduPVzi7nqH05Nslqtu4p5mZaxPHjGvna0DcIVQNFQC/Wgobox8pezeJ6tANUPFAUpJUp+E5N3q8DofZXiHBQTpNXo3tyM2JYT/IwovSIdPSgJeIhbrfP6hVKOeZjKqaC8/SV67R/1LUpdO7KeKig5d7LJa3vjeeDmrM/3+1vb0C96Xrgv39j4MyPx7HrZxFdOQif4PZlpLEfarEtuSFUAUXx4N2uhLbTyAXyl4dfGkqdbQ5O/UT6xxXw44JsK6DzOz5OkT6cB0uUvJa5TrHk+cVoxlUu3Ex/5o6KXnMFaxfzyizPXeiIs/mTT9Bq6nAtvGad84U6Wvua3ZPofOlH0gFyN8/uMJxwqXbKuO1iScjkxuCNX8YRFzcoWH7VXzbbNPIQLORTu9/PhAIRZXOSxZw2iPZVg3LKuyyW3MgTtnVvgrKYnynw1Yrvc7gu35MhwjvnRrPlVQ+yFrZuB3l2Cf0OWZwonlMr36TsPHIJq+wD4ZEja+ciOsRAWzWuxBEDQ=="
|
||||
}
|
||||
}
|
||||
}
|
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