Compare commits
	
		
			109 Commits
		
	
	
		
			secretbran
			...
			renovate/d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9588ed28be | |||
| 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@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", | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,12 +1,14 @@ | ||||
| FROM python:3.10-slim | ||||
| ENV DOCKERIZED=true | ||||
| FROM docker.io/python:3.14-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