Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 01e3138f29 | |||
| a41a483041 | |||
| 917e90b48b | |||
| 467aebc3af | |||
| 818f4dfdae | |||
| 0db5d4d31c | |||
| 6967fa5358 | |||
| 70363033c1 | |||
| baf774f4b0 | |||
| 64215ffda5 | |||
| 13a9075376 | |||
| 05e72bf0dc | |||
| 654274f632 | |||
| 56d7a8155c | |||
| b82d6fa518 | |||
| c4cb477637 | |||
| 2f33d840f3 | |||
| f01b942818 | |||
| 8e1b4269d1 | |||
| e1455de14b | |||
| e6195d9a88 | |||
| 1221035369 | |||
| 796f43d629 | |||
| 0b031d20f9 | |||
| bfd497898e | |||
| 077f845b94 | |||
| 26e3a4bb8e | |||
| f56636c1ff | |||
| f427d06e61 | |||
| 42bd226a60 | |||
| 4dd578c9a8 | |||
| 01d90e6625 |
@@ -0,0 +1,28 @@
|
||||
**/__pycache__
|
||||
**/.venv
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.github
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
@@ -0,0 +1,15 @@
|
||||
name: Code Analysis (Bandit)
|
||||
|
||||
on: push
|
||||
jobs:
|
||||
bandit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mdegis/bandit-action@v1.0
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: "src/"
|
||||
level: high
|
||||
confidence: high
|
||||
exit_zero: true
|
||||
@@ -0,0 +1,44 @@
|
||||
name: Release Multi-Arch Docker Image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-and-push-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Transform 'github.repository' to lowercase
|
||||
id: lowercase_repo
|
||||
shell: bash
|
||||
run: echo "repository=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push multi-arch image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: |
|
||||
ghcr.io/${{ steps.lowercase_repo.outputs.repository }}:${{ github.event.release.tag_name }}
|
||||
ghcr.io/${{ steps.lowercase_repo.outputs.repository }}:latest
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Linting (Ruff)
|
||||
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
+1
-2
@@ -4,9 +4,8 @@ config/*.ini
|
||||
pastebin.txt
|
||||
.DS_Store
|
||||
test*.py
|
||||
.dockerignore
|
||||
docker-*
|
||||
.vscode
|
||||
.vscode/settings.json
|
||||
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Docker: Build Image",
|
||||
"type": "docker-build",
|
||||
"platform": "python",
|
||||
"dockerBuild": {
|
||||
"context": "${workspaceFolder}",
|
||||
"dockerfile": "${workspaceFolder}/Dockerfile",
|
||||
"tag": "post-it:dev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run post-it (interactive, show logs)",
|
||||
"type": "shell",
|
||||
"dependsOn": ["Docker: Build Image"],
|
||||
"command": "docker",
|
||||
"args": [
|
||||
"run",
|
||||
"--rm",
|
||||
"-it",
|
||||
"--name",
|
||||
"post-it-dev",
|
||||
"-v",
|
||||
"${workspaceFolder}/config:/app/config",
|
||||
"post-it:dev"
|
||||
],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "shared"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Base image (multi-arch via buildx)
|
||||
FROM python:3.10-slim
|
||||
|
||||
# Prevents pyc files + enables unbuffered logging
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Install dependencies
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy only necessary files (via .dockerignore)
|
||||
COPY . .
|
||||
|
||||
# Create non-root user
|
||||
RUN adduser --disabled-password --no-create-home --gecos "" appuser \
|
||||
&& chown -R appuser /app
|
||||
USER appuser
|
||||
|
||||
# Default command
|
||||
ENTRYPOINT ["python", "src/main.py"]
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Lars Winzer
|
||||
Copyright (c) 2024-2025 Lars Winzer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0.5.2
|
||||
+4
-6
@@ -4,7 +4,6 @@ from discord.ext import commands
|
||||
from cogs.base_cog import Base_Cog
|
||||
|
||||
import logging
|
||||
from utils.portal import Portal
|
||||
from datetime import datetime
|
||||
from utils.datetime_tools import get_elapsed_time_big
|
||||
from utils.logger.decorator import log_command_execution
|
||||
@@ -17,17 +16,16 @@ class Debug_Command(Base_Cog):
|
||||
@app_commands.command(name = "debug", description = "Provides debug informations about the bot, useful for troubleshooting")
|
||||
@log_command_execution
|
||||
async def debug(self, ctx:discord.Interaction):
|
||||
portal:Portal = Portal.instance()
|
||||
embed = discord.Embed(title="Debug Information")
|
||||
|
||||
embed.add_field(name="Version",
|
||||
value=f"`{portal.PROGRAM_VERSION}`",
|
||||
value=ctx.client.VERSION,
|
||||
inline=True)
|
||||
embed.add_field(name="Uptime",
|
||||
value=f"{get_elapsed_time_big(datetime.now().timestamp() - portal.STARTUP_TIMESTAMP)}",
|
||||
value=f"{get_elapsed_time_big(datetime.now().timestamp() - ctx.client.STARTUP_TIMESTAMP)}",
|
||||
inline=True)
|
||||
embed.add_field(name="Bot Owner",
|
||||
value=f"<@{portal.bot_config['DISCORD']['OWNER_ID']}>",
|
||||
value=f"<@{ctx.client.bot_config['DISCORD']['OWNER_ID']}>",
|
||||
inline=True)
|
||||
embed.add_field(name="Latency to Gateway",
|
||||
value=f"{round(self.__bot.latency * 1000, 2)}ms",
|
||||
@@ -38,7 +36,7 @@ class Debug_Command(Base_Cog):
|
||||
embed.add_field(name="Number of Guilds",
|
||||
value=f"{len(self.__bot.guilds)}",
|
||||
inline=True)
|
||||
embed.add_field(name = "Number of executed commands", value=f"Total: {portal.no_executed_commands}\nSucceeded: {portal.no_succeeded_commands}\nFailed: {portal.no_failed_commands}")
|
||||
embed.add_field(name = "Number of executed commands", value=f"Total: {ctx.client.no_executed_commands}\nSucceeded: {ctx.client.no_succeeded_commands}\nFailed: {ctx.client.no_failed_commands}")
|
||||
|
||||
await ctx.response.send_message(embed=embed)
|
||||
|
||||
|
||||
+26
-7
@@ -5,8 +5,7 @@ from cogs.base_cog import Base_Cog
|
||||
|
||||
import logging
|
||||
from urllib.parse import urlparse
|
||||
from praw.models import Submission, Subreddit
|
||||
from utils.portal import Portal
|
||||
from asyncpraw.models import Submission
|
||||
import aiohttp
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
@@ -35,14 +34,12 @@ class Post_Command(Base_Cog):
|
||||
async def post(self, ctx:discord.Interaction, url:str, custom_note:str = None, use_title:bool = True, quality:app_commands.Choice[int] = 95):
|
||||
try:
|
||||
domain_info = urlparse(url)
|
||||
portal = Portal.instance()
|
||||
toplevel_domain = '.'.join(domain_info.netloc.split('.')[-2:])
|
||||
begin_process = datetime.now().timestamp()
|
||||
match toplevel_domain:
|
||||
case "reddit.com":
|
||||
self._logger.debug(f"Recieved command by {ctx.user} ({ctx.user.id}) for reddit ({url})")
|
||||
|
||||
subm:Submission = await portal.reddit_adapter.fetch(url)
|
||||
subm:Submission = await ctx.client.reddit_adapter.fetch(url)
|
||||
image_urls = []
|
||||
# Check if submission has a gallery
|
||||
if hasattr(subm, "media_metadata"):
|
||||
@@ -93,7 +90,7 @@ class Post_Command(Base_Cog):
|
||||
author = subm.author.name if subm.author else "Author not found"
|
||||
content = f":copyright: [{author}]({url})"
|
||||
if use_title:
|
||||
content += f"\n## {subm.title}"
|
||||
content += f"\n# {subm.title}"
|
||||
|
||||
if custom_note:
|
||||
content += f"\n> {custom_note}"
|
||||
@@ -134,6 +131,25 @@ class Post_Command(Base_Cog):
|
||||
)
|
||||
embed.set_footer(text = "Supported image formats: jpg, jpeg, png, webp, heic, heif")
|
||||
|
||||
except discord.errors.HTTPException as error:
|
||||
self._logger.error(f"Could not complete command by {ctx.user.name} ({ctx.user.id})")
|
||||
self._logger.exception(error, stack_info = True)
|
||||
|
||||
match error.code:
|
||||
case 40005:
|
||||
self._logger.error("Failed to upload images, payload too large")
|
||||
|
||||
embed = discord.Embed(
|
||||
title = "Error while processing",
|
||||
description = "The attachments exceed the upload limit of Discord,\nchoose a different quality level via the argument `quality`",
|
||||
color = 0xED4337
|
||||
)
|
||||
|
||||
if ctx.response.is_done():
|
||||
await ctx.followup.send(embed = embed, ephemeral = True)
|
||||
else:
|
||||
await ctx.response.send_message(embed = embed)
|
||||
|
||||
except Exception as error:
|
||||
self._logger.error(f"Could not complete command by {ctx.user.name} ({ctx.user.id})")
|
||||
self._logger.exception(error, stack_info = True)
|
||||
@@ -150,7 +166,10 @@ class Post_Command(Base_Cog):
|
||||
description = f"While we processed your request, the following exception occured: `{error}`",
|
||||
color = 0xED4337
|
||||
)
|
||||
await ctx.followup.send(embed = embed, ephemeral = True)
|
||||
if ctx.response.is_done():
|
||||
await ctx.followup.send(embed = embed, ephemeral = True)
|
||||
else:
|
||||
await ctx.response.send_message(embed = embed)
|
||||
|
||||
|
||||
async def setup(bot:commands.Bot):
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from pathlib import Path
|
||||
|
||||
# Read VERSION file
|
||||
with open(Path(__file__).resolve().parent / "VERSION") as f:
|
||||
VERSION = f.read().strip()
|
||||
|
||||
|
||||
__all__ = VERSION
|
||||
+40
-43
@@ -1,25 +1,6 @@
|
||||
print(" ____ ____ ___________ __________")
|
||||
print(" / __ \/ __ \/ ___/_ __/ / _/_ __/")
|
||||
print(" / /_/ / / / /\__ \ / /_____ / / / / ")
|
||||
print(" / ____/ /_/ /___/ // /_____// / / / ")
|
||||
print(" /_/ \____//____//_/ /___/ /_/ ")
|
||||
print(" Copyright (c) 2024 Lars Winzer")
|
||||
print()
|
||||
print(" Source: https://github.com/official-Cromatin/Post-It")
|
||||
print(" Report an Issue: https://github.com/official-Cromatin/Post-It/issues/new?assignees=&labels=bug&projects=&template=issue_report.yml")
|
||||
print("\n")
|
||||
|
||||
from datetime import datetime
|
||||
startup = datetime.now().timestamp()
|
||||
|
||||
# Initialize the logger
|
||||
from utils.logger.custom_logging import Custom_Logger
|
||||
Custom_Logger.initialize()
|
||||
|
||||
import logging
|
||||
app_logger = logging.getLogger("app")
|
||||
startup_logger = logging.getLogger("app.startup")
|
||||
|
||||
from utils.adv_configparser import Advanced_ConfigParser
|
||||
from utils.datetime_tools import get_elapsed_time_smal, get_elapsed_time_big, get_elapsed_time_milliseconds
|
||||
import discord
|
||||
@@ -28,10 +9,27 @@ from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from utils.portal import Portal
|
||||
import asyncio
|
||||
from typing import Union
|
||||
from platforms.reddit import Reddit_Adapter
|
||||
from const import VERSION
|
||||
|
||||
print(" ____ ____ ___________ __________")
|
||||
print(" / __ \/ __ \/ ___/_ __/ / _/_ __/")
|
||||
print(" / /_/ / / / /\__ \ / /_____ / / / / ")
|
||||
print(" / ____/ /_/ /___/ // /_____// / / / ")
|
||||
print(f" /_/ \____//____//_/ /___/ /_/ v{VERSION}")
|
||||
print(" Copyright (c) 2024-2025 Lars Winzer")
|
||||
print()
|
||||
print(" Source: https://github.com/official-Cromatin/Post-It")
|
||||
print(" Report an Issue: https://github.com/official-Cromatin/Post-It/issues/new?assignees=&labels=bug&projects=&template=issue_report.yml")
|
||||
print("\n")
|
||||
startup = datetime.now().timestamp()
|
||||
|
||||
# Initialize the logger
|
||||
Custom_Logger.initialize()
|
||||
app_logger = logging.getLogger("app")
|
||||
startup_logger = logging.getLogger("app.startup")
|
||||
|
||||
source_path = Path(__file__).resolve()
|
||||
base_path = source_path.parents[1]
|
||||
@@ -43,11 +41,17 @@ intents.messages = True
|
||||
class MyBot(commands.Bot):
|
||||
def __init__(self):
|
||||
super().__init__(command_prefix=None, help_command=None, intents=intents)
|
||||
self.__portal:Portal
|
||||
self.__first_on_ready = False
|
||||
|
||||
def set_portal(self, portal:Portal):
|
||||
self.__portal = portal
|
||||
self.VERSION = VERSION
|
||||
self.STARTUP_TIMESTAMP: float = None
|
||||
self.platforms_config: Advanced_ConfigParser = None
|
||||
self.bot_config: Advanced_ConfigParser = None
|
||||
self.reddit_adapter: Reddit_Adapter = None
|
||||
|
||||
self.no_executed_commands:int = 0
|
||||
self.no_succeeded_commands:int = 0
|
||||
self.no_failed_commands:int = 0
|
||||
|
||||
async def setup_hook(self):
|
||||
# Register cogs to handle commands
|
||||
@@ -57,15 +61,13 @@ class MyBot(commands.Bot):
|
||||
|
||||
async def on_app_command_completion(self, interaction: discord.Interaction, command: Union[discord.app_commands.Command, discord.app_commands.ContextMenu]):
|
||||
"""Called when a `app_commands.Command` or `app_commands.ContextMenu` has successfully completed without error"""
|
||||
self.__portal.no_succeeded_commands += 1
|
||||
print("Command succeeded")
|
||||
self.no_succeeded_commands += 1
|
||||
|
||||
async def on_interaction(self, interaction: discord.Interaction):
|
||||
"""Called when an interaction happened"""
|
||||
match interaction.type.name:
|
||||
case discord.InteractionType.application_command.name:
|
||||
print("Interaction with bot", interaction.command.name)
|
||||
self.__portal.no_executed_commands += 1
|
||||
self.no_executed_commands += 1
|
||||
case discord.InteractionType.ping.name:
|
||||
print("App got pinged by discord")
|
||||
case discord.InteractionType.autocomplete.name:
|
||||
@@ -78,21 +80,21 @@ class MyBot(commands.Bot):
|
||||
async def on_connect(self):
|
||||
"""A coroutine to be called to setup the bot, after the bot is logged in but before it has connected to the Websocket"""
|
||||
if not self.__first_on_ready:
|
||||
startup_logger.info(f"Beginning startup routine ...")
|
||||
startup_logger.info("Beginning startup routine ...")
|
||||
routine_begin = datetime.now().timestamp()
|
||||
await self.change_presence(status = discord.Status.dnd, activity = discord.CustomActivity("Executing pre startup routine"))
|
||||
|
||||
# Create the adapters for the platforms
|
||||
task_start = datetime.now().timestamp()
|
||||
startup_logger.debug(f"Loading platforms config ...")
|
||||
startup_logger.debug("Loading platforms config ...")
|
||||
platforms_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "platforms.ini"))
|
||||
portal.platforms_config = platforms_config
|
||||
self.platforms_config = platforms_config
|
||||
startup_logger.info(f"Loaded platforms config after {get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}")
|
||||
|
||||
# Create platforms adapter
|
||||
task_start = datetime.now().timestamp()
|
||||
startup_logger.debug(f"Creating reddit adapter ...")
|
||||
portal.reddit_adapter = Reddit_Adapter(platforms_config["REDDIT"]["CLIENT_ID"], platforms_config["REDDIT"]["CLIENT_SECRET"])
|
||||
startup_logger.debug("Creating reddit adapter ...")
|
||||
self.reddit_adapter = Reddit_Adapter(platforms_config["REDDIT"]["CLIENT_ID"], platforms_config["REDDIT"]["CLIENT_SECRET"])
|
||||
startup_logger.info(f"Created reddit adapter after {get_elapsed_time_milliseconds(datetime.now().timestamp() - task_start)}")
|
||||
|
||||
await self.change_presence(status = discord.Status.online, activity = None)
|
||||
@@ -105,22 +107,17 @@ class MyBot(commands.Bot):
|
||||
app_logger.info(f"Successfully logged in (after {get_elapsed_time_smal(datetime.now().timestamp() - startup)}) as {self.user}")
|
||||
|
||||
bot = MyBot()
|
||||
bot_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "bot.ini"))
|
||||
if re.match(r'[A-Za-z\d]{24}\.[\w-]{6}\.[\w-]{27}', bot_config["DISCORD"]["TOKEN"]):
|
||||
bot.STARTUP_TIMESTAMP = startup
|
||||
bot.bot_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "bot.ini"))
|
||||
if re.match(r'[A-Za-z\d]{24}\.[\w-]{6}\.[\w-]{27}', bot.bot_config["DISCORD"]["TOKEN"]):
|
||||
app_logger.critical("Bot (config/bot.ini) configuration invalid, please set a valid token")
|
||||
quit(1)
|
||||
elif bot_config.compare_to_template() not in ("equal", "config_minus"):
|
||||
elif bot.bot_config.compare_to_template() not in ("equal", "config_minus"):
|
||||
app_logger.critical("Bot (config/bot.ini) configuration is missing some parts. Make sure it at least has all the same keys as the template")
|
||||
quit(1)
|
||||
else:
|
||||
app_logger.info("Bot configuration valid, continuing with startup")
|
||||
|
||||
# Execute some housekeeping actions
|
||||
portal = Portal.instance()
|
||||
portal.bot_config = bot_config
|
||||
portal.STARTUP_TIMESTAMP = startup
|
||||
bot.set_portal(portal)
|
||||
|
||||
# Setup handlers to handle states of command execution
|
||||
@bot.tree.error
|
||||
async def on_app_command_error(ctx:discord.Interaction, error):
|
||||
@@ -128,10 +125,10 @@ async def on_app_command_error(ctx:discord.Interaction, error):
|
||||
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
|
||||
traceback.print_exception(type(error), error, error.__traceback__)
|
||||
|
||||
portal.no_failed_commands += 1
|
||||
bot.no_failed_commands += 1
|
||||
|
||||
try:
|
||||
bot.run(bot_config["DISCORD"]["TOKEN"], log_handler = None)
|
||||
bot.run(bot.bot_config["DISCORD"]["TOKEN"], log_handler = None)
|
||||
except discord.errors.LoginFailure:
|
||||
app_logger.critical("Improper token has been passed. Aborting startup")
|
||||
quit(1)
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from utils.singleton import Singleton
|
||||
from utils.adv_configparser import Advanced_ConfigParser
|
||||
from platforms.reddit import Reddit_Adapter
|
||||
|
||||
@Singleton
|
||||
class Portal:
|
||||
PROGRAM_VERSION = "0.3"
|
||||
bot_config:Advanced_ConfigParser = None
|
||||
platforms_config:Advanced_ConfigParser = None
|
||||
STARTUP_TIMESTAMP:float = None
|
||||
no_executed_commands:int = 0
|
||||
no_succeeded_commands:int = 0
|
||||
no_failed_commands:int = 0
|
||||
reddit_adapter: Reddit_Adapter = None
|
||||
Reference in New Issue
Block a user