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/.gitea/workflows/build_container.yaml b/.gitea/workflows/build_container.yaml index 7314df9..47ba0ac 100644 --- a/.gitea/workflows/build_container.yaml +++ b/.gitea/workflows/build_container.yaml @@ -2,6 +2,9 @@ on: pull_request: branches: - main + push: + branches: + - main name: Build container 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/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 1d85190..a05cdb2 100644 --- a/bot/cronjob/chat_photo.py +++ b/bot/cronjob/chat_photo.py @@ -14,7 +14,7 @@ class SetChatPhotoJob(): self.bot = bot self.logger = logging.getLogger(self.__class__.__name__) - if models.IS_PRODUCTION: + 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: 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 94008f0..e1c5d1e 100644 --- a/bot/cronjob/random_memory.py +++ b/bot/cronjob/random_memory.py @@ -11,7 +11,7 @@ class RandomMemoryJob(): self.bot = bot self.logger = logging.getLogger(self.__class__.__name__) - if models.IS_PRODUCTION: + 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 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 e258535..de23064 100644 --- a/bot/models.py +++ b/bot/models.py @@ -79,13 +79,13 @@ 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: 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/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]" +}