diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 001db43..0000000 --- a/.drone.yml +++ /dev/null @@ -1,29 +0,0 @@ -kind: pipeline -type: kubernetes -name: docker-build - -node_selector: - kubernetes.io/arch: amd64 - - -steps: -- name: docker - image: plugins/docker - settings: - username: - from_secret: docker_uname - password: - from_secret: docker_pw - - repo: mollre/journal-bot - tags: - - 1.0.${DRONE_BUILD_NUMBER} - - latest - build_args: "BOT_VERSION=1.0.${DRONE_BUILD_NUMBER}" - - -trigger: - branch: - - main - event: - - push diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1d953f4 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use nix diff --git a/.gitea/workflows/build_container.yaml b/.gitea/workflows/build_container.yaml new file mode 100644 index 0000000..47ba0ac --- /dev/null +++ b/.gitea/workflows/build_container.yaml @@ -0,0 +1,35 @@ +on: + pull_request: + branches: + - main + push: + branches: + - main + +name: Build container + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + + - uses: https://gitea.com/actions/checkout@v4 + + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: git.kluster.moll.re + username: ${{ gitea.repository_owner }} + password: ${{ secrets.PACKAGE_REGISTRY_ACCESS }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + # use the current commit SHA as the tag + tags: git.kluster.moll.re/remoll/journal-bot:${{ gitea.sha }} + push: true diff --git a/.gitignore b/.gitignore index 7655e78..b1c4306 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/.vscode/launch.json b/.vscode/launch.json index 8167722..743924e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "configurations": [ { "name": "Python: Current project", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${workspaceFolder}/bot/main.py", "console": "integratedTerminal", @@ -14,4 +14,4 @@ "envFile": "${workspaceFolder}/dev.env", } ] -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile index 516fb69..2833624 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,14 @@ -FROM python:3-slim -ENV DOCKERIZED=true -ARG BOT_VERSION -# set at build time -ENV BOT_VERSION=$BOT_VERSION +FROM docker.io/python:3.13-alpine +# use the latest version of uv, independently of the python version +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app -RUN pip install pipenv +# copy the requirements and install them +COPY pyproject.toml uv.lock . +RUN uv sync --frozen -COPY Pipfile Pipfile.lock ./ - -RUN pipenv install --system --deploy - +# copy the rest of the code COPY bot . -CMD ["python", "main.py"] \ No newline at end of file +CMD ["uv", "run", "main.py"] diff --git a/Makefile b/Makefile deleted file mode 100644 index f3536c5..0000000 --- a/Makefile +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Pipfile b/Pipfile deleted file mode 100644 index d89fd45..0000000 --- a/Pipfile +++ /dev/null @@ -1,14 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -peewee = "*" -python-telegram-bot = {extras = ["job-queue"], version = "*"} -anyio = "*" - -[dev-packages] - -[pipenv] -allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 33e4ad6..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,131 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "5458e81c4f85af776acc44f46af838644ef8c00ccf4223fbe06f9d76a4717fc6" - }, - "pipfile-spec": 6, - "requires": {}, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "anyio": { - "hashes": [ - "sha256:48d53f0b141f5757c38d648309e6fe254857fae092d67f938fa248d7c0f36804", - "sha256:596b09c520820e7eed961ddc889540972f92d5e8fcb081117fc054c409df34ae" - ], - "index": "pypi", - "version": "==4.0.0rc1" - }, - "apscheduler": { - "hashes": [ - "sha256:0293937d8f6051a0f493359440c1a1b93e882c57daf0197afeff0e727777b96e", - "sha256:e813ad5ada7aff36fb08cdda746b520531eaac7757832abc204868ba78e0c8f6" - ], - "version": "==3.10.1" - }, - "certifi": { - "hashes": [ - "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082", - "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9" - ], - "markers": "python_version >= '3.6'", - "version": "==2023.7.22" - }, - "h11": { - "hashes": [ - "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", - "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" - ], - "markers": "python_version >= '3.7'", - "version": "==0.14.0" - }, - "httpcore": { - "hashes": [ - "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", - "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87" - ], - "markers": "python_version >= '3.7'", - "version": "==0.17.3" - }, - "httpx": { - "hashes": [ - "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd", - "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd" - ], - "markers": "python_version >= '3.7'", - "version": "==0.24.1" - }, - "idna": { - "hashes": [ - "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", - "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" - ], - "markers": "python_version >= '3.5'", - "version": "==3.4" - }, - "peewee": { - "hashes": [ - "sha256:10769981198c7311f84a0ca8db892fa213303a8eb1305deb795a71e7bd606a91" - ], - "index": "pypi", - "version": "==3.16.2" - }, - "python-telegram-bot": { - "extras": [ - "job-queue" - ], - "hashes": [ - "sha256:a6ac3f9c9674aaf7d1c7e652d8b75cde969fb872f75e9521b8516eceaba82b1b", - "sha256:e426404b0006989a5bcc05e11a7ef3ffe0c086b684a4e963db5bda1d361a049a" - ], - "index": "pypi", - "version": "==20.4" - }, - "pytz": { - "hashes": [ - "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588", - "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb" - ], - "version": "==2023.3" - }, - "setuptools": { - "hashes": [ - "sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f", - "sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235" - ], - "markers": "python_version >= '3.7'", - "version": "==68.0.0" - }, - "six": { - "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" - }, - "sniffio": { - "hashes": [ - "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", - "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.0" - }, - "tzlocal": { - "hashes": [ - "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803", - "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0.1" - } - }, - "develop": {} -} diff --git a/bot/commands/journal.py b/bot/commands/journal.py index d47b00b..abccd99 100644 --- a/bot/commands/journal.py +++ b/bot/commands/journal.py @@ -38,7 +38,7 @@ 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 @@ -79,7 +79,7 @@ class JournalHandler(BaseHandler): else: await query.edit_message_text(text="An entry already exists for this date") return ConversationHandler.END - + return CONTENT_ENTRY @@ -113,13 +113,13 @@ class JournalHandler(BaseHandler): async def date_entry(self, update, context): date = update.message.text - + try: date = datetime.datetime.strptime(date, "%d%m%Y").date() except ValueError: await update.message.reply_text("Please enter the date in the format _DDMMYYYY_", parse_mode=ParseMode.MARKDOWN_V2) return ENTRY_OPTIONS - + if context.chat_data.get("delete", False): # if not set, delete was not chosen with models.db: self.current_model = models.JournalEntry.get_or_none( @@ -158,7 +158,7 @@ class JournalHandler(BaseHandler): file = await update.message.effective_attachment[-1].get_file() else: file = await update.message.effective_attachment.get_file() - + file_bytes = await file.download_as_bytearray() file_path = file.file_path self.current_model.save_media(file_bytes, file_path) @@ -211,7 +211,7 @@ def get_names(dates: list): 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): diff --git a/bot/commands/list/models.py b/bot/commands/list/models.py index 8d5379c..a1d3dc1 100644 --- a/bot/commands/list/models.py +++ b/bot/commands/list/models.py @@ -1,5 +1,5 @@ +from pathlib import Path from peewee import * - db = DatabaseProxy() class BaseModel(Model): @@ -13,7 +13,7 @@ class ListModel(BaseModel): @property def content(self) -> dict: return {e.id: e.entry for e in self.entries} - + @content.setter def content(self, new_content: dict): old_content = self.content @@ -29,7 +29,7 @@ class ListModel(BaseModel): @property def done_dict(self): return {e.id: e.done for e in self.entries} - + @done_dict.setter def done_dict(self, new_done: dict): old_done_dict = self.done_dict @@ -46,7 +46,8 @@ class ListEntryModel(BaseModel): done = BooleanField(default=None, null=True) -def set_db(db_path): +def set_db(db_path: Path): + db_path.parent.mkdir(parents=True, exist_ok=True) db.initialize(SqliteDatabase(db_path)) with db: db.create_tables([ListModel, ListEntryModel], safe=True) diff --git a/bot/commands/memory.py b/bot/commands/memory.py index 5a25664..35cf9ec 100644 --- a/bot/commands/memory.py +++ b/bot/commands/memory.py @@ -3,7 +3,7 @@ from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, fi from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, InputMediaPhoto import models from telegram.constants import ParseMode -# ACTION_CHOICE, DATE_ENTRY, ADD_CONTENT = range(3) + MEMORY_CHOICE = range(1) @@ -28,7 +28,7 @@ class MemoryHandler(BaseHandler): async def entry_point(self, update: Update, context: CallbackContext): await super().entry_point(update, context) - if os.getenv("DOCKERIZED", "false") == "true" and os.getenv("CHAT_ID") != str(update.message.chat_id): + if models.IS_PRODUCTION and os.getenv("CHAT_ID") != str(update.message.chat_id): await update.message.reply_text("You are not authorized to use this bot") return ConversationHandler.END @@ -56,9 +56,9 @@ class MemoryHandler(BaseHandler): await update.message.reply_text( f"Which moment would you like to remember?", reply_markup=keyboard ) - + context.chat_data["kept_matches"] = list(matching_models) - + return MEMORY_CHOICE async def choose_memory(self, update: Update, context: CallbackContext): diff --git a/bot/commands/status.py b/bot/commands/status.py index 82bde41..98b4791 100644 --- a/bot/commands/status.py +++ b/bot/commands/status.py @@ -7,10 +7,14 @@ from telegram.constants import ParseMode import os FIRST = 1 +import models from .basehandler import BaseHandler + + + class StatusHandler(BaseHandler): """Shows a short status of the program.""" - + def __init__(self, entry_string): self.start_time = datetime.datetime.now() self.entry_string = entry_string @@ -35,7 +39,6 @@ class StatusHandler(BaseHandler): reply_markup = InlineKeyboardMarkup(keyboard) delta = str(datetime.datetime.now() - self.start_time) - message = "BeebBop, this is Norbit\n" try: ip = httpx.get('https://api.ipify.org').text @@ -47,18 +50,21 @@ class StatusHandler(BaseHandler): ip = "not fetchable" local_ips = "not fetchable" - message += "Status: Running đŸŸĸ\n" - message += f"Version: `{os.getenv('BOT_VERSION', 'dev')}`\n" - message += f"Uptime: `{delta[:delta.rfind('.')]}`\n" - message += f"IP \(public\): `{ip}`\n" - message += f"IP \(private\): `{local_ips}`\n" - message += f"Chat ID: `{update.effective_chat.id}`\n" + message = f""" + BeebBop\! + Status: Running đŸŸĸ + Version: `{os.getenv('BOT_VERSION', 'dev')}` and`prod={models.IS_PRODUCTION}` + Uptime: `{delta[:delta.rfind('.')]}` + IP \(public\): `{ip}` + IP \(private\): `{local_ips}` + Chat ID: `{update.effective_chat.id}` + """.strip() # remove trailing whitespace if update.message: await update.message.reply_text(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2) else: await update._effective_chat.send_message(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2) - + return FIRST diff --git a/bot/cronjob/chat_photo.py b/bot/cronjob/chat_photo.py index ce41177..a05cdb2 100644 --- a/bot/cronjob/chat_photo.py +++ b/bot/cronjob/chat_photo.py @@ -13,8 +13,8 @@ class SetChatPhotoJob(): def __init__(self, bot: ExtBot, job_queue): self.bot = bot self.logger = logging.getLogger(self.__class__.__name__) - - if os.getenv("DOCKERIZED", "false") != "true": + + if not models.IS_PRODUCTION: # when running locally, annoy the programmer every 60 seconds <3 job_queue.run_repeating(self.callback_photo, interval=60) else: @@ -24,13 +24,13 @@ class SetChatPhotoJob(): async def callback_photo(self, context): - + # last_seen of memory must be older than 10 days in past or None with models.db: possible_photos = models.JournalEntry.select().where( models.JournalEntry.media_path != None ).order_by(fn.Random()) - + try: chosen_entry = possible_photos.get() except: diff --git a/bot/cronjob/leaderboard.py b/bot/cronjob/leaderboard.py new file mode 100644 index 0000000..6e85f5e --- /dev/null +++ b/bot/cronjob/leaderboard.py @@ -0,0 +1,95 @@ +import os +from telegram.ext import ExtBot +from telegram.constants import ParseMode +import logging +from datetime import time, timedelta, timezone, datetime, date +from peewee import fn +import models +from telegram.ext import JobQueue + + +RANKING_TEMPLATE = """ +Journal Leaderboard +This week: 📈{week_leader_name} - {week_leader_count} 📉{week_last_name} - {week_last_count} +This month: 📈{month_leader_name} - {month_leader_count} 📉{month_last_name} - {month_last_count} +This year: 📈{year_leader_name} - {year_leader_count} 📉{year_last_name} - {year_last_count} + +🏆 Leader: {leader_name} +""" + + + +def get_author_ranking(since_days): + """Returns the query for the top authors by counting their journal entries. An additional field for the count is added.""" + + cutoff_date = date.today() - timedelta(days=since_days) + with models.db: + return models.JournalEntry.select( + models.JournalEntry.author, + fn.Count(models.JournalEntry.id).alias('message_count') + ).where( + models.JournalEntry.date >= cutoff_date + ).group_by( + models.JournalEntry.author + ).order_by( + fn.Count(models.JournalEntry.id).desc() + ) + + + + +class SendLeaderboard(): + def __init__(self, bot: ExtBot, job_queue: JobQueue): + self.bot = bot + self.logger = logging.getLogger(self.__class__.__name__) + + if not models.IS_PRODUCTION: + # when running locally, just run once after 10 seconds + job_queue.run_once(self.callback_leaderboard, when=10) + else: + # set the message sending time; include UTC shift +2 + sending_time = time(hour=12, minute=0, second=0, tzinfo=timezone(timedelta(hours=2))) + job_queue.run_daily(self.callback_leaderboard, when=sending_time, day=-1) + + + async def callback_leaderboard(self, context): + """Send a weakly leaderboard to the chat.""" + if date.today().weekday() != 1: + self.logger.info("Today is not Monday, skipping leaderboard.") + return + + # get the top contributions of the past week, month and year: + ranking_week = get_author_ranking(7) + ranking_month = get_author_ranking(30) + ranking_year = get_author_ranking(365) + + week_leader, week_last = ranking_week.first(n=2) + month_leader, month_last = ranking_month.first(n=2) + year_leader, year_last = ranking_year.first(n=2) + + leader = year_leader + + message_text = RANKING_TEMPLATE.format( + week_leader_name=week_leader.author, + week_leader_count=week_leader.message_count, + week_last_name=week_last.author, + week_last_count=week_last.message_count, + month_leader_name=month_leader.author, + month_leader_count=month_leader.message_count, + month_last_name=month_last.author, + month_last_count=month_last.message_count, + year_leader_name=year_leader.author, + year_leader_count=year_leader.message_count, + year_last_name=year_last.author, + year_last_count=year_last.message_count, + leader_name=leader.author + ) + + print(message_text) + + chat_id = os.getenv("CHAT_ID") + await self.bot.send_message( + chat_id = chat_id, + text = message_text, + parse_mode=ParseMode.HTML + ) diff --git a/bot/cronjob/random_memory.py b/bot/cronjob/random_memory.py index 17589d3..e1c5d1e 100644 --- a/bot/cronjob/random_memory.py +++ b/bot/cronjob/random_memory.py @@ -4,13 +4,14 @@ import os from peewee import fn import logging import models +from telegram.ext import ExtBot class RandomMemoryJob(): - def __init__(self, bot, job_queue): + def __init__(self, bot: ExtBot, job_queue): self.bot = bot self.logger = logging.getLogger(self.__class__.__name__) - - if os.getenv("DOCKERIZED", "false") != "true": + + if not models.IS_PRODUCTION: # when running locally, annoy the programmer every 60 seconds <3 job_queue.run_repeating(self.callback_memory, interval=3600) self.min_age = 0 # do not filter messages: show them all @@ -22,14 +23,14 @@ class RandomMemoryJob(): async def callback_memory(self, context): - + # last_seen of memory must be older than 10 days in past or None with models.db: possible_entries = models.JournalEntry.select().where( (models.JournalEntry.last_shown <= datetime.today().date() - timedelta(days=self.min_age)) | \ (models.JournalEntry.last_shown == None) ).order_by(fn.Random()) - + try: chosen_entry = possible_entries.get() except: diff --git a/bot/main.py b/bot/main.py index bbc09c1..bb792b7 100644 --- a/bot/main.py +++ b/bot/main.py @@ -5,7 +5,7 @@ import logging import models from commands import journal, status, turtle, memory, advent from commands.list import list -from cronjob import chat_photo, random_memory +from cronjob import chat_photo, random_memory, leaderboard logging.basicConfig( format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", @@ -32,6 +32,7 @@ def main() -> None: random_memory.RandomMemoryJob(application.bot, application.job_queue) chat_photo.SetChatPhotoJob(application.bot, application.job_queue) + leaderboard.SendLeaderboard(application.bot, application.job_queue) # Run the bot until the user presses Ctrl-C application.run_polling() diff --git a/bot/models.py b/bot/models.py index 2cd1313..de23064 100644 --- a/bot/models.py +++ b/bot/models.py @@ -2,7 +2,7 @@ from peewee import * from pathlib import Path import re import os -import datetime +import socket ID_MAPPINGS = { "Lia": 5603036217, @@ -11,9 +11,9 @@ ID_MAPPINGS = { ID_MAPPINGS_REV = dict((v, k) for k, v in ID_MAPPINGS.items()) RATING_MAPPING = { - 1: "đŸ˜ĩ", - 2: "â˜šī¸", - 3: "😐", + 1: "🙁", + 2: "😐", + 3: "🙂", 4: "😃", 5: "đŸĨ°" } @@ -21,6 +21,11 @@ RATING_MAPPING = { 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): @@ -37,11 +42,11 @@ class JournalEntry(BaseModel): 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}" @@ -74,20 +79,20 @@ class JournalEntry(BaseModel): """Returns the text with all the frisky details hidden away""" new_text = self.text.replace("<", "<").replace(">", ">").replace("&", "&") pattern = re.compile( - "(" - "(((?<=(\.|\!|\?)\s)[A-Z])|(^[A-Z]))" # beginning of a sentence - "([^\.\!\?])+" # any character being part of a sentence - "((\:\))|😇|😈|[Ss]ex)" # the smiley - "([^\.\!\?])*" # continuation of sentence - "(\.|\!|\?|\,|$)" # end of the sentence - ")" + r"(" + r"(((?<=(\.|\!|\?)\s)[A-Z])|(^[A-Z]))" # beginning of a sentence + r"([^\.\!\?])+" # any character being part of a sentence + r"((\:\))|😇|😈|[Ss]ex)" # the smiley + r"([^\.\!\?])*" # continuation of sentence + r"(\.|\!|\?|\,|$)" # end of the sentence + r")" ) matches = pattern.findall(new_text) for match in matches: group_to_replace = match[0] new_text = new_text.replace(group_to_replace, f"{group_to_replace}") return new_text - + def set_db(db_path): db.initialize(SqliteDatabase(db_path)) diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..f4758b0 --- /dev/null +++ b/default.nix @@ -0,0 +1,29 @@ +{ pkgs ? import {} }: +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 + "; +} diff --git a/deployment/deployment.yaml b/deployment/deployment.yaml index 40ab0f6..04a64d1 100644 --- a/deployment/deployment.yaml +++ b/deployment/deployment.yaml @@ -17,7 +17,7 @@ spec: spec: containers: - name: journal - image: mollre/journal-bot:1.0.19 + image: journal envFrom: - secretRef: name: journal-secret-env @@ -33,31 +33,3 @@ spec: - name: journal-nfs persistentVolumeClaim: claimName: journal-data-nfs ---- -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 - diff --git a/deployment/kustomization.yaml b/deployment/kustomization.yaml index c82b522..51e5a25 100644 --- a/deployment/kustomization.yaml +++ b/deployment/kustomization.yaml @@ -1,9 +1,15 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization + resources: - ./namespace.yaml - ./deployment.yaml - ./sealedsecret.yaml +- ./pvc.yaml + +namespace: journal-bot + images: -- name: mollre/journal-bot - newTag: 1.0.68 +- name: journal + newName: git.kluster.moll.re/remoll/journal-bot + newTag: 29d951427d6f3377e43767916cefb07e03e9eab8 diff --git a/deployment/namespace.yaml b/deployment/namespace.yaml index a201ebc..0a074bd 100644 --- a/deployment/namespace.yaml +++ b/deployment/namespace.yaml @@ -1,6 +1,4 @@ apiVersion: v1 kind: Namespace metadata: - name: journal - labels: - name: journal \ No newline at end of file + name: placeholder diff --git a/deployment/pvc.yaml b/deployment/pvc.yaml new file mode 100644 index 0000000..0ba0728 --- /dev/null +++ b/deployment/pvc.yaml @@ -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 + diff --git a/dev.env b/dev.env new file mode 100644 index 0000000..d6331b7 --- /dev/null +++ b/dev.env @@ -0,0 +1,3 @@ +MEDIA_DIR="./.bot/media" +PERSISTENCE_DIR="./.bot/persistence" +DB_PATH="./.bot/db.sqlite" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a708f78 --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 7fbeff1..0000000 --- a/renovate.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "packageRules": [ - { - "matchUpdateTypes": ["minor", "patch"], - "matchCurrentVersion": "!/^0/", - "automerge": true, - "automergeType": "branch", - "ignoreTests": true - } - ], - "commitMessagePrefix" : "[CI SKIP]" -} diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000..5dc7d03 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,14 @@ +{ + "$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]" +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1ea0688 --- /dev/null +++ b/uv.lock @@ -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" }, +]