more refinements for the deployment
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Build container / Build (pull_request) Successful in 1m10s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Build container / Build (pull_request) Successful in 1m10s
				
			This commit is contained in:
		
							
								
								
									
										29
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										29
									
								
								.drone.yml
									
									
									
									
									
								
							| @@ -1,29 +0,0 @@ | |||||||
| kind: pipeline |  | ||||||
| type: kubernetes |  | ||||||
| name: docker-build |  | ||||||
|  |  | ||||||
| node_selector: |  | ||||||
|   kubernetes.io/arch: amd64 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| steps: |  | ||||||
| - name: docker   |  | ||||||
|   image: plugins/docker |  | ||||||
|   settings: |  | ||||||
|     username:  |  | ||||||
|       from_secret: docker_uname |  | ||||||
|     password: |  | ||||||
|       from_secret: docker_pw |  | ||||||
|  |  | ||||||
|     repo: mollre/journal-bot |  | ||||||
|     tags:  |  | ||||||
|       - 1.0.${DRONE_BUILD_NUMBER} |  | ||||||
|       - latest |  | ||||||
|     build_args: "BOT_VERSION=1.0.${DRONE_BUILD_NUMBER}" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| trigger: |  | ||||||
|   branch: |  | ||||||
|   - main |  | ||||||
|   event: |  | ||||||
|   - push |  | ||||||
| @@ -2,6 +2,9 @@ on: | |||||||
|   pull_request: |   pull_request: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - main | ||||||
|  |  | ||||||
| name: Build container | name: Build container | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
|  | from pathlib import Path | ||||||
| from peewee import * | from peewee import * | ||||||
|  |  | ||||||
| db = DatabaseProxy() | db = DatabaseProxy() | ||||||
|  |  | ||||||
| class BaseModel(Model): | class BaseModel(Model): | ||||||
| @@ -13,7 +13,7 @@ class ListModel(BaseModel): | |||||||
|     @property |     @property | ||||||
|     def content(self) -> dict: |     def content(self) -> dict: | ||||||
|         return {e.id: e.entry for e in self.entries} |         return {e.id: e.entry for e in self.entries} | ||||||
|      |  | ||||||
|     @content.setter |     @content.setter | ||||||
|     def content(self, new_content: dict): |     def content(self, new_content: dict): | ||||||
|         old_content = self.content |         old_content = self.content | ||||||
| @@ -29,7 +29,7 @@ class ListModel(BaseModel): | |||||||
|     @property |     @property | ||||||
|     def done_dict(self): |     def done_dict(self): | ||||||
|         return {e.id: e.done for e in self.entries} |         return {e.id: e.done for e in self.entries} | ||||||
|      |  | ||||||
|     @done_dict.setter |     @done_dict.setter | ||||||
|     def done_dict(self, new_done: dict): |     def done_dict(self, new_done: dict): | ||||||
|         old_done_dict = self.done_dict |         old_done_dict = self.done_dict | ||||||
| @@ -46,7 +46,8 @@ class ListEntryModel(BaseModel): | |||||||
|     done = BooleanField(default=None, null=True) |     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)) |     db.initialize(SqliteDatabase(db_path)) | ||||||
|     with db: |     with db: | ||||||
|         db.create_tables([ListModel, ListEntryModel], safe=True) |         db.create_tables([ListModel, ListEntryModel], safe=True) | ||||||
|   | |||||||
| @@ -7,10 +7,14 @@ from telegram.constants import ParseMode | |||||||
| import os | import os | ||||||
|  |  | ||||||
| FIRST = 1 | FIRST = 1 | ||||||
|  | import models | ||||||
| from .basehandler import BaseHandler | from .basehandler import BaseHandler | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class StatusHandler(BaseHandler): | class StatusHandler(BaseHandler): | ||||||
|     """Shows a short status of the program.""" |     """Shows a short status of the program.""" | ||||||
|      |  | ||||||
|     def __init__(self, entry_string): |     def __init__(self, entry_string): | ||||||
|         self.start_time = datetime.datetime.now() |         self.start_time = datetime.datetime.now() | ||||||
|         self.entry_string = entry_string |         self.entry_string = entry_string | ||||||
| @@ -35,7 +39,6 @@ class StatusHandler(BaseHandler): | |||||||
|         reply_markup = InlineKeyboardMarkup(keyboard) |         reply_markup = InlineKeyboardMarkup(keyboard) | ||||||
|  |  | ||||||
|         delta = str(datetime.datetime.now() - self.start_time) |         delta = str(datetime.datetime.now() - self.start_time) | ||||||
|         message = "BeebBop, this is Norbit\n" |  | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             ip = httpx.get('https://api.ipify.org').text |             ip = httpx.get('https://api.ipify.org').text | ||||||
| @@ -47,18 +50,21 @@ class StatusHandler(BaseHandler): | |||||||
|             ip = "not fetchable" |             ip = "not fetchable" | ||||||
|             local_ips = "not fetchable" |             local_ips = "not fetchable" | ||||||
|  |  | ||||||
|         message += "Status: Running 🟢\n" |         message = f""" | ||||||
|         message += f"Version: `{os.getenv('BOT_VERSION', 'dev')}`\n" |             BeebBop\! | ||||||
|         message += f"Uptime: `{delta[:delta.rfind('.')]}`\n" |             Status: Running 🟢 | ||||||
|         message += f"IP \(public\): `{ip}`\n" |             Version: `{os.getenv('BOT_VERSION', 'dev')}` and`prod={models.IS_PRODUCTION}` | ||||||
|         message += f"IP \(private\): `{local_ips}`\n" |             Uptime: `{delta[:delta.rfind('.')]}` | ||||||
|         message += f"Chat ID: `{update.effective_chat.id}`\n" |             IP \(public\): `{ip}` | ||||||
|  |             IP \(private\): `{local_ips}` | ||||||
|  |             Chat ID: `{update.effective_chat.id}` | ||||||
|  |         """.strip() # remove trailing whitespace | ||||||
|  |  | ||||||
|         if update.message: |         if update.message: | ||||||
|             await update.message.reply_text(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2) |             await update.message.reply_text(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2) | ||||||
|         else: |         else: | ||||||
|             await update._effective_chat.send_message(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2) |             await update._effective_chat.send_message(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN_V2) | ||||||
|          |  | ||||||
|         return FIRST |         return FIRST | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ class SetChatPhotoJob(): | |||||||
|         self.bot = bot |         self.bot = bot | ||||||
|         self.logger = logging.getLogger(self.__class__.__name__) |         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 |             # when running locally, annoy the programmer every 60 seconds <3 | ||||||
|             job_queue.run_repeating(self.callback_photo, interval=60) |             job_queue.run_repeating(self.callback_photo, interval=60) | ||||||
|         else: |         else: | ||||||
|   | |||||||
							
								
								
									
										95
									
								
								bot/cronjob/leaderboard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								bot/cronjob/leaderboard.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 = """ | ||||||
|  | <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, 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 | ||||||
|  |         ) | ||||||
| @@ -11,7 +11,7 @@ class RandomMemoryJob(): | |||||||
|         self.bot = bot |         self.bot = bot | ||||||
|         self.logger = logging.getLogger(self.__class__.__name__) |         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 |             # when running locally, annoy the programmer every 60 seconds <3 | ||||||
|             job_queue.run_repeating(self.callback_memory, interval=3600) |             job_queue.run_repeating(self.callback_memory, interval=3600) | ||||||
|             self.min_age = 0 # do not filter messages: show them all |             self.min_age = 0 # do not filter messages: show them all | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import logging | |||||||
| import models | import models | ||||||
| from commands import journal, status, turtle, memory, advent | from commands import journal, status, turtle, memory, advent | ||||||
| from commands.list import list | from commands.list import list | ||||||
| from cronjob import chat_photo, random_memory | from cronjob import chat_photo, random_memory, leaderboard | ||||||
|  |  | ||||||
| logging.basicConfig( | logging.basicConfig( | ||||||
|     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", |     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | ||||||
| @@ -32,6 +32,7 @@ def main() -> None: | |||||||
|  |  | ||||||
|     random_memory.RandomMemoryJob(application.bot, application.job_queue) |     random_memory.RandomMemoryJob(application.bot, application.job_queue) | ||||||
|     chat_photo.SetChatPhotoJob(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 |     # Run the bot until the user presses Ctrl-C | ||||||
|     application.run_polling() |     application.run_polling() | ||||||
|   | |||||||
| @@ -79,13 +79,13 @@ class JournalEntry(BaseModel): | |||||||
|         """Returns the text with all the frisky details hidden away""" |         """Returns the text with all the frisky details hidden away""" | ||||||
|         new_text = self.text.replace("<", "<").replace(">", ">").replace("&", "&") |         new_text = self.text.replace("<", "<").replace(">", ">").replace("&", "&") | ||||||
|         pattern = re.compile( |         pattern = re.compile( | ||||||
|             "(" |             r"(" | ||||||
|             "(((?<=(\.|\!|\?)\s)[A-Z])|(^[A-Z]))" # beginning of a sentence |             r"(((?<=(\.|\!|\?)\s)[A-Z])|(^[A-Z]))" # beginning of a sentence | ||||||
|             "([^\.\!\?])+" # any character being part of a sentence |             r"([^\.\!\?])+" # any character being part of a sentence | ||||||
|             "((\:\))|😇|😈|[Ss]ex)" # the smiley |             r"((\:\))|😇|😈|[Ss]ex)" # the smiley | ||||||
|             "([^\.\!\?])*" # continuation of sentence |             r"([^\.\!\?])*" # continuation of sentence | ||||||
|             "(\.|\!|\?|\,|$)" # end of the sentence |             r"(\.|\!|\?|\,|$)" # end of the sentence | ||||||
|             ")" |             r")" | ||||||
|         ) |         ) | ||||||
|         matches = pattern.findall(new_text) |         matches = pattern.findall(new_text) | ||||||
|         for match in matches: |         for match in matches: | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ spec: | |||||||
|     spec: |     spec: | ||||||
|       containers: |       containers: | ||||||
|         - name: journal |         - name: journal | ||||||
|           image: mollre/journal-bot:1.0.19 |           image: journal | ||||||
|           envFrom: |           envFrom: | ||||||
|             - secretRef: |             - secretRef: | ||||||
|                 name: journal-secret-env |                 name: journal-secret-env | ||||||
| @@ -33,31 +33,3 @@ spec: | |||||||
|         - name: journal-nfs |         - name: journal-nfs | ||||||
|           persistentVolumeClaim: |           persistentVolumeClaim: | ||||||
|             claimName: journal-data-nfs |             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 |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,15 @@ | |||||||
| apiVersion: kustomize.config.k8s.io/v1beta1 | apiVersion: kustomize.config.k8s.io/v1beta1 | ||||||
| kind: Kustomization | kind: Kustomization | ||||||
|  |  | ||||||
| resources: | resources: | ||||||
| - ./namespace.yaml | - ./namespace.yaml | ||||||
| - ./deployment.yaml | - ./deployment.yaml | ||||||
| - ./sealedsecret.yaml | - ./sealedsecret.yaml | ||||||
|  | - ./pvc.yaml | ||||||
|  |  | ||||||
|  | namespace: journal-bot | ||||||
|  |  | ||||||
| images: | images: | ||||||
| - name: mollre/journal-bot | - name: journal | ||||||
|   newTag: 1.0.68 |   newName: git.kluster.moll.re/remoll/journal-bot | ||||||
|  |   newTag: 29d951427d6f3377e43767916cefb07e03e9eab8 | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| apiVersion: v1 | apiVersion: v1 | ||||||
| kind: Namespace | kind: Namespace | ||||||
| metadata: | metadata: | ||||||
|   name: journal |   name: placeholder | ||||||
|   labels: |  | ||||||
|     name: journal |  | ||||||
|   | |||||||
							
								
								
									
										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 | ||||||
|  |  | ||||||
| @@ -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]" |  | ||||||
| } |  | ||||||
							
								
								
									
										14
									
								
								renovate.json5
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								renovate.json5
									
									
									
									
									
										Normal file
									
								
							| @@ -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]" | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user