Compare commits
35 Commits
v0.5
...
08c58f10c2
| Author | SHA1 | Date | |
|---|---|---|---|
| 08c58f10c2 | |||
| a380b9da82 | |||
| c8be6bffcc | |||
| dd4a037479 | |||
| a24fd875d8 | |||
| fb022f5a53 | |||
| 1d67cb2d92 | |||
| c3eff5ed7f | |||
| 22a3b14edd | |||
| 55ef585bdb | |||
| a9cf3af487 | |||
| 01e3138f29 | |||
| 46fc7d0d60 | |||
| a41a483041 | |||
| 917e90b48b | |||
| 467aebc3af | |||
| 818f4dfdae | |||
| 0db5d4d31c | |||
| 6967fa5358 | |||
| 70363033c1 | |||
| baf774f4b0 | |||
| 64215ffda5 | |||
| 13a9075376 | |||
| 05e72bf0dc | |||
| 654274f632 | |||
| 56d7a8155c | |||
| b82d6fa518 | |||
| c4cb477637 | |||
| 2f33d840f3 | |||
| f01b942818 | |||
| 8e1b4269d1 | |||
| e1455de14b | |||
| e6195d9a88 | |||
| 1221035369 | |||
| 796f43d629 |
@@ -1,27 +0,0 @@
|
|||||||
**/__pycache__
|
|
||||||
**/.venv
|
|
||||||
**/.classpath
|
|
||||||
**/.dockerignore
|
|
||||||
**/.env
|
|
||||||
**/.git
|
|
||||||
**/.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,13 @@
|
|||||||
|
###############################
|
||||||
|
# Git Large File System (LFS) #
|
||||||
|
###############################
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.gif filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ico filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.webp filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# Gradle wrapper
|
||||||
|
gradle/wrapper/gradle-wrapper.jar filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
name: Release Multi-Arch Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push-docker:
|
|
||||||
runs-on: self-hosted
|
|
||||||
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: Build and push multi-arch image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: |
|
|
||||||
ghcr.io/${{ github.repository }}:${{ github.event.release.tag_name }}
|
|
||||||
ghcr.io/${{ github.repository }}:latest
|
|
||||||
+61
-151
@@ -1,172 +1,82 @@
|
|||||||
# Custom additions
|
########
|
||||||
logs/*
|
# Java #
|
||||||
config/*.ini
|
########
|
||||||
pastebin.txt
|
|
||||||
.DS_Store
|
|
||||||
test*.py
|
|
||||||
docker-*
|
|
||||||
.vscode
|
|
||||||
|
|
||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Log file
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
*.log
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
# Package Files #
|
||||||
instance/
|
*.jar
|
||||||
.webassets-cache
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
# Scrapy stuff:
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
.scrapy
|
hs_err_pid*
|
||||||
|
replay_pid*
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
##########
|
||||||
.pybuilder/
|
# Gradle #
|
||||||
target/
|
##########
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Gradle files
|
||||||
.ipynb_checkpoints
|
.gradle
|
||||||
|
**/build/
|
||||||
|
!**/src/**/build/
|
||||||
|
|
||||||
# IPython
|
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||||
profile_default/
|
!gradle-wrapper.jar
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
# Avoid ignore Gradle wrappper properties
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
!gradle-wrapper.properties
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
# Cache of project
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
.gradletasknamecache
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
# Gradle properties
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
gradle.properties
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
#########
|
||||||
__pypackages__/
|
# MacOS #
|
||||||
|
#########
|
||||||
|
|
||||||
# Celery stuff
|
# General
|
||||||
celerybeat-schedule
|
.DS_Store
|
||||||
celerybeat.pid
|
__MACOSX/
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon[]
|
||||||
|
|
||||||
# SageMath parsed files
|
# Thumbnails
|
||||||
*.sage.py
|
._*
|
||||||
|
|
||||||
# Environments
|
# Files that might appear in the root of a volume
|
||||||
.env
|
.DocumentRevisions-V100
|
||||||
.venv
|
.fseventsd
|
||||||
env/
|
.Spotlight-V100
|
||||||
venv/
|
.TemporaryItems
|
||||||
ENV/
|
.Trashes
|
||||||
env.bak/
|
.VolumeIcon.icns
|
||||||
venv.bak/
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
# Spyder project settings
|
# Directories potentially created on remote AFP share
|
||||||
.spyderproject
|
.AppleDB
|
||||||
.spyproject
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
########
|
||||||
/site
|
# Jenv #
|
||||||
|
########
|
||||||
|
|
||||||
# mypy
|
.java-version
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
# 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"]
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* This file was generated by the Gradle 'init' task.
|
||||||
|
*
|
||||||
|
* This generated file contains a sample Java application project to get you started.
|
||||||
|
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/9.2.0/userguide/building_java_projects.html in the Gradle documentation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
// Apply the application plugin to add support for building a CLI application in Java.
|
||||||
|
id 'application'
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
// Use Maven Central for resolving dependencies.
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Use JUnit Jupiter for testing.
|
||||||
|
testImplementation libs.junit.jupiter
|
||||||
|
|
||||||
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
|
||||||
|
// This dependency is used by the application.
|
||||||
|
implementation libs.guava
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a specific Java toolchain to ease working on different environments.
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
// Define the main class for the application.
|
||||||
|
mainClass = 'postit.App'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('test') {
|
||||||
|
// Use JUnit Platform for unit tests.
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[DISCORD]
|
|
||||||
TOKEN = <PLACE YOUR DISCORD TOKEN FROM THE DEV PORTAL HERE>
|
|
||||||
OWNER_ID = <PLACE THE USER ID OF THE OWNER HERE>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
; Create an script on the reddit developer page (https://www.reddit.com/prefs/apps)
|
|
||||||
[REDDIT]
|
|
||||||
CLIENT_ID = <PASTE YOUR CLIENT ID HERE>
|
|
||||||
CLIENT_SECRET = <PASTE YOUR CLIENT SECRET HERE>
|
|
||||||
USER_AGENT = Small discord bot to embed posts (given by url) into an standardized format
|
|
||||||
EMBED_COLOR = 0xff4500
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# This file was generated by the Gradle 'init' task.
|
||||||
|
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
|
||||||
|
|
||||||
|
[versions]
|
||||||
|
guava = "33.4.6-jre"
|
||||||
|
junit-jupiter = "5.12.1"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
||||||
|
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
|
||||||
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+93
@@ -0,0 +1,93 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
discord.py==2.4.0
|
|
||||||
asyncpraw==7.8.1
|
|
||||||
colorama==0.4.6
|
|
||||||
aiohttp==3.11.11
|
|
||||||
pillow==11.1.0
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* This file was generated by the Gradle 'init' task.
|
||||||
|
*
|
||||||
|
* The settings file is used to specify which projects to include in your build.
|
||||||
|
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/9.2.0/userguide/multi_project_builds.html in the Gradle documentation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
// Apply the foojay-resolver plugin to allow automatic download of JDKs
|
||||||
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'PostIt'
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import discord
|
|
||||||
from discord import app_commands
|
|
||||||
from discord.ext import commands
|
|
||||||
import logging
|
|
||||||
|
|
||||||
class Base_Cog(commands.Cog):
|
|
||||||
def __init__(self, logger:logging.Logger):
|
|
||||||
self._logger = logger
|
|
||||||
|
|
||||||
async def cog_load(self):
|
|
||||||
self._logger.debug(f"Cog for the '{self.__class__.__name__}' command got loaded")
|
|
||||||
|
|
||||||
async def cog_unload(self):
|
|
||||||
self._logger.debug(f"Cog for the '{self.__class__.__name__}' command got unloaded")
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import discord
|
|
||||||
from discord import app_commands
|
|
||||||
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
|
|
||||||
|
|
||||||
class Debug_Command(Base_Cog):
|
|
||||||
def __init__(self, bot:commands.Bot):
|
|
||||||
self.__bot = bot
|
|
||||||
super().__init__(logging.getLogger("cmds.debug"))
|
|
||||||
|
|
||||||
@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}`",
|
|
||||||
inline=True)
|
|
||||||
embed.add_field(name="Uptime",
|
|
||||||
value=f"{get_elapsed_time_big(datetime.now().timestamp() - portal.STARTUP_TIMESTAMP)}",
|
|
||||||
inline=True)
|
|
||||||
embed.add_field(name="Bot Owner",
|
|
||||||
value=f"<@{portal.bot_config['DISCORD']['OWNER_ID']}>",
|
|
||||||
inline=True)
|
|
||||||
embed.add_field(name="Latency to Gateway",
|
|
||||||
value=f"{round(self.__bot.latency * 1000, 2)}ms",
|
|
||||||
inline=True)
|
|
||||||
embed.add_field(name="Application ID",
|
|
||||||
value=f"{self.__bot.application_id}",
|
|
||||||
inline=True)
|
|
||||||
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}")
|
|
||||||
|
|
||||||
await ctx.response.send_message(embed=embed)
|
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot:commands.Bot):
|
|
||||||
await bot.add_cog(Debug_Command(bot))
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import discord
|
|
||||||
from discord import app_commands
|
|
||||||
from discord.ext import commands
|
|
||||||
from cogs.base_cog import Base_Cog
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from asyncpraw.models import Submission
|
|
||||||
from utils.portal import Portal
|
|
||||||
import aiohttp
|
|
||||||
from PIL import Image
|
|
||||||
from io import BytesIO
|
|
||||||
from datetime import datetime
|
|
||||||
from utils.datetime_tools import get_elapsed_time_milliseconds
|
|
||||||
|
|
||||||
class NoMediaFound(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Post_Command(Base_Cog):
|
|
||||||
def __init__(self, bot:commands.Bot):
|
|
||||||
self.__bot = bot
|
|
||||||
super().__init__(logging.getLogger("cmds.post"))
|
|
||||||
|
|
||||||
@app_commands.command(name = "post", description = "Post an embed in the Current Channel with a link to the content")
|
|
||||||
@app_commands.describe(url = "URL to the post", custom_note = "Describe the post with your own note", use_title = "Display the title of the post", quality = "Specifies the quality of the converted image, closer to 100 is better")
|
|
||||||
@app_commands.choices(quality = [
|
|
||||||
app_commands.Choice(name = "Poor (60)", value = 60),
|
|
||||||
app_commands.Choice(name = "Fair (70)", value = 70),
|
|
||||||
app_commands.Choice(name = "Good (80)", value = 80),
|
|
||||||
app_commands.Choice(name = "Very Good (85)", value = 85),
|
|
||||||
app_commands.Choice(name = "Excellent (90)", value = 90),
|
|
||||||
app_commands.Choice(name = "Superior (95)", value = 95),
|
|
||||||
app_commands.Choice(name = "Perfect (100)", value = 100)
|
|
||||||
])
|
|
||||||
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)
|
|
||||||
image_urls = []
|
|
||||||
# Check if submission has a gallery
|
|
||||||
if hasattr(subm, "media_metadata"):
|
|
||||||
for media_id, media in subm.media_metadata.items():
|
|
||||||
file_extension = media["m"].split("/")[1]
|
|
||||||
if file_extension not in ("jpg", "jpeg", "png", "webp", "heic", "heif"):
|
|
||||||
continue
|
|
||||||
image_urls.append(f"https://i.redd.it/{media_id}.{file_extension}")
|
|
||||||
else:
|
|
||||||
image_urls.append(subm.url)
|
|
||||||
image_count = len(image_urls)
|
|
||||||
if image_count == 0:
|
|
||||||
raise NoMediaFound
|
|
||||||
self._logger.debug(f"Found {image_count} image urls for the post")
|
|
||||||
|
|
||||||
progress_title = f"`{image_count}` images are going to be converted, it may take a while."
|
|
||||||
progress_temp = progress_title + f"\n`0` of `{image_count}` have already been loaded"
|
|
||||||
await ctx.response.send_message(progress_temp, ephemeral = True)
|
|
||||||
|
|
||||||
if isinstance(quality, app_commands.Choice):
|
|
||||||
quality_value = quality.value
|
|
||||||
else:
|
|
||||||
quality_value = quality
|
|
||||||
|
|
||||||
# Download and convert each image
|
|
||||||
image_files:list[discord.File] = []
|
|
||||||
begin_conversion = datetime.now().timestamp()
|
|
||||||
async with aiohttp.ClientSession() as session:
|
|
||||||
index = 0
|
|
||||||
for image_url in image_urls:
|
|
||||||
async with session.get(image_url) as response:
|
|
||||||
response.raise_for_status()
|
|
||||||
image_data = await response.read()
|
|
||||||
|
|
||||||
original_image = Image.open(BytesIO(image_data))
|
|
||||||
webp_buffer = BytesIO()
|
|
||||||
original_image.save(webp_buffer, format = "WEBP", quality = quality_value)
|
|
||||||
webp_buffer.seek(0)
|
|
||||||
image_file = discord.File(webp_buffer, filename = f"image_{index}.webp")
|
|
||||||
image_files.append(image_file)
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
progress_temp = progress_title + f"\n`{index}` of `{image_count}` have already been loaded"
|
|
||||||
await ctx.edit_original_response(content = progress_temp)
|
|
||||||
|
|
||||||
self._logger.debug(f"Downloaded and converted {len(image_files)} images in {get_elapsed_time_milliseconds(datetime.now().timestamp() - begin_conversion)}")
|
|
||||||
|
|
||||||
author = subm.author.name if subm.author else "Author not found"
|
|
||||||
content = f":copyright: [{author}]({url})"
|
|
||||||
if use_title:
|
|
||||||
content += f"\n# {subm.title}"
|
|
||||||
|
|
||||||
if custom_note:
|
|
||||||
content += f"\n> {custom_note}"
|
|
||||||
|
|
||||||
await ctx.delete_original_response()
|
|
||||||
message = await ctx.followup.send(
|
|
||||||
content = content,
|
|
||||||
suppress_embeds = True,
|
|
||||||
files = image_files
|
|
||||||
)
|
|
||||||
|
|
||||||
self._logger.info(f"Successfully processed the command executed by {ctx.user.name} ({ctx.user.id}) after {get_elapsed_time_milliseconds(datetime.now().timestamp() - begin_process)} (ID of message: {message.id})")
|
|
||||||
|
|
||||||
# No domain for seperation found
|
|
||||||
case _:
|
|
||||||
if toplevel_domain == "":
|
|
||||||
toplevel_domain = "not_found"
|
|
||||||
|
|
||||||
embed = discord.Embed(
|
|
||||||
title = "Domain not found",
|
|
||||||
description = f"The requested domain `{toplevel_domain}` is currently not supported\nOpen [an issue](https://github.com/official-Cromatin/Post-It/issues/new?assignees=&labels=feature-request&projects=&template=feature_request.yml) to request support for it.\n\nCurrently supported plattforms:\n- Reddit",
|
|
||||||
color = 0xED4337)
|
|
||||||
|
|
||||||
await ctx.response.send_message(embed = embed, ephemeral = True)
|
|
||||||
|
|
||||||
except NoMediaFound:
|
|
||||||
self._logger.error(f"Aborted issued command by {ctx.user.name} ({ctx.user.id}). Post had no media attatched")
|
|
||||||
|
|
||||||
# Delete the original response, if existing
|
|
||||||
try:
|
|
||||||
await ctx.delete_original_response()
|
|
||||||
except discord.NotFound:
|
|
||||||
pass
|
|
||||||
embed = discord.Embed(
|
|
||||||
title = "No media found",
|
|
||||||
description = "The post had no media attatched, or was in an unsupported format\nVideos are not supported! (yet)",
|
|
||||||
color = 0xED4337
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Delete the original response, if existing
|
|
||||||
try:
|
|
||||||
await ctx.delete_original_response()
|
|
||||||
except discord.NotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Explain to the user what the error was
|
|
||||||
embed = discord.Embed(
|
|
||||||
title = "Error while processing",
|
|
||||||
description = f"While we processed your request, the following exception occured: `{error}`",
|
|
||||||
color = 0xED4337
|
|
||||||
)
|
|
||||||
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):
|
|
||||||
await bot.add_cog(Post_Command(bot))
|
|
||||||
-137
@@ -1,137 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
from utils.logger.custom_logging import Custom_Logger
|
|
||||||
import logging
|
|
||||||
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
|
|
||||||
from discord.ext import commands
|
|
||||||
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
|
|
||||||
|
|
||||||
print(" ____ ____ ___________ __________")
|
|
||||||
print(" / __ \/ __ \/ ___/_ __/ / _/_ __/")
|
|
||||||
print(" / /_/ / / / /\__ \ / /_____ / / / / ")
|
|
||||||
print(" / ____/ /_/ /___/ // /_____// / / / ")
|
|
||||||
print(" /_/ \____//____//_/ /___/ /_/ ")
|
|
||||||
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]
|
|
||||||
app_logger.info(f"Using the following path as entrypoint: '{base_path}'")
|
|
||||||
|
|
||||||
intents = discord.Intents.default()
|
|
||||||
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
|
|
||||||
|
|
||||||
async def setup_hook(self):
|
|
||||||
# Register cogs to handle commands
|
|
||||||
for cog_name in ["debug", "post"]:
|
|
||||||
await self.load_extension(f"cogs.{cog_name}")
|
|
||||||
await self.tree.sync()
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
async def on_interaction(self, interaction: discord.Interaction):
|
|
||||||
"""Called when an interaction happened"""
|
|
||||||
match interaction.type.name:
|
|
||||||
case discord.InteractionType.application_command.name:
|
|
||||||
self.__portal.no_executed_commands += 1
|
|
||||||
case discord.InteractionType.ping.name:
|
|
||||||
print("App got pinged by discord")
|
|
||||||
case discord.InteractionType.autocomplete.name:
|
|
||||||
print("Interaction with autocomplete")
|
|
||||||
case discord.InteractionType.modal_submit.name:
|
|
||||||
print("Modal interaction submitted")
|
|
||||||
case discord.InteractionType.component.name:
|
|
||||||
print("Component interaction")
|
|
||||||
|
|
||||||
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("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("Loading platforms config ...")
|
|
||||||
platforms_config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "platforms.ini"))
|
|
||||||
portal.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("Creating reddit adapter ...")
|
|
||||||
portal.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)
|
|
||||||
startup_logger.info(f"Startup routine finished after {get_elapsed_time_milliseconds(datetime.now().timestamp() - routine_begin)}")
|
|
||||||
self.__first_on_ready = True
|
|
||||||
else:
|
|
||||||
startup_logger.info("Startup routine allready executed, omitting this execution")
|
|
||||||
|
|
||||||
async def on_ready(self):
|
|
||||||
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"]):
|
|
||||||
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"):
|
|
||||||
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):
|
|
||||||
"""Executed when exception during command execution occurs"""
|
|
||||||
print('Ignoring exception in command {}:'.format(ctx.command), file=sys.stderr)
|
|
||||||
traceback.print_exception(type(error), error, error.__traceback__)
|
|
||||||
|
|
||||||
portal.no_failed_commands += 1
|
|
||||||
|
|
||||||
try:
|
|
||||||
bot.run(bot_config["DISCORD"]["TOKEN"], log_handler = None)
|
|
||||||
except discord.errors.LoginFailure:
|
|
||||||
app_logger.critical("Improper token has been passed. Aborting startup")
|
|
||||||
quit(1)
|
|
||||||
|
|
||||||
app_logger.info("Quitting application ...")
|
|
||||||
asyncio.run(bot.close())
|
|
||||||
app_logger.info(f"Exiting. Application ran for {get_elapsed_time_big(datetime.now().timestamp() - startup)}")
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* This source file was generated by the Gradle 'init' task
|
||||||
|
*/
|
||||||
|
package postit;
|
||||||
|
|
||||||
|
public class App {
|
||||||
|
public String getGreeting() {
|
||||||
|
return "Hello World!";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println(new App().getGreeting());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import asyncpraw
|
|
||||||
import asyncpraw.models
|
|
||||||
import logging
|
|
||||||
from utils.event_counter import Event_Counter
|
|
||||||
from utils.datetime_tools import get_elapsed_time_milliseconds
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
class Reddit_Adapter(asyncpraw.Reddit):
|
|
||||||
"""A class that extends and abstracts the functionality of the `asyncpraw.Reddit` class by adding
|
|
||||||
logging and request tracking capabilities."""
|
|
||||||
VERSION = "1.0"
|
|
||||||
number_of_instances = 0
|
|
||||||
|
|
||||||
def __init__(self, client_id:str, client_secret:str):
|
|
||||||
"""Initializes the Reddit Adapter, while stating credentials for the login to the reddit api"""
|
|
||||||
self.__instance_number = self.__class__.number_of_instances
|
|
||||||
self.__class__.number_of_instances += 1
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
client_id = client_id,
|
|
||||||
client_secret = client_secret,
|
|
||||||
user_agent="Small discord bot to embed posts (given by url) into an standardized format"
|
|
||||||
)
|
|
||||||
self.__events = Event_Counter(1000)
|
|
||||||
self.__logger = logging.getLogger(f"pltfm.reddit.{self.__instance_number}")
|
|
||||||
|
|
||||||
async def fetch(self, post_url:str) -> asyncpraw.models.Submission:
|
|
||||||
"""Fetches specified submission (post) and returns it"""
|
|
||||||
start_time = datetime.now().timestamp()
|
|
||||||
self.__events.increment()
|
|
||||||
subm = await self.submission(url = post_url)
|
|
||||||
self.__logger.debug(f"Submission for post (URL: {post_url}), successfully fetched after {get_elapsed_time_milliseconds(datetime.now().timestamp() - start_time)}")
|
|
||||||
return subm
|
|
||||||
|
|
||||||
def get_total_requests(self) -> int:
|
|
||||||
"""Returns the total number of requests made since the creation of the adapter"""
|
|
||||||
return self.__events.get_total_events()
|
|
||||||
|
|
||||||
def get_events_last_5m(self) -> int:
|
|
||||||
"""Returns the number of requests made in the last 5 minutes"""
|
|
||||||
return self.__events.get_count("5m")
|
|
||||||
|
|
||||||
def get_events_last_5m_10m_15m(self) -> tuple[int]:
|
|
||||||
"""Returns an tuple containing the number of requests made in the last 5, 10 and 15 minutes"""
|
|
||||||
return (self.__events.get_count("5m"), self.__events.get_count("10m"), self.__events.get_count("15m"))
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* This source file was generated by the Gradle 'init' task
|
||||||
|
*/
|
||||||
|
package postit;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class AppTest {
|
||||||
|
@Test void appHasAGreeting() {
|
||||||
|
App classUnderTest = new App();
|
||||||
|
assertNotNull(classUnderTest.getGreeting(), "app should have a greeting");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
import configparser
|
|
||||||
import logging
|
|
||||||
from os.path import isfile, split
|
|
||||||
from pathlib import Path
|
|
||||||
from shutil import copy
|
|
||||||
|
|
||||||
class Advanced_ConfigParser(configparser.ConfigParser):
|
|
||||||
"""Utility class to ease the use of the configparser libary"""
|
|
||||||
VERSION = "2.5"
|
|
||||||
number_of_instances = 0
|
|
||||||
|
|
||||||
def __init__(self, path:str, allow_template:bool = True, allow_update:bool = True) -> None:
|
|
||||||
"""
|
|
||||||
Initializes the Advanced_ConfigParser instance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path (str): The path to the configuration file.
|
|
||||||
allow_template (bool, optional): Determines if an existing template can be used when the config file is missing. Default is True.
|
|
||||||
allow_update (bool, optional): Specifies if the config file should be updated based on a template. Default is True.
|
|
||||||
|
|
||||||
This constructor loads the configuration file if it exists, optionally creates one from a template if allowed,
|
|
||||||
and can update the configuration based on the template. It also initializes logging and tracks pending changes.
|
|
||||||
"""
|
|
||||||
super().__init__()
|
|
||||||
self.__instance_number = self.__class__.number_of_instances
|
|
||||||
self.__class__.number_of_instances += 1
|
|
||||||
self.__from_template = False
|
|
||||||
|
|
||||||
self.__logger = logging.getLogger(f"utils.config.{self.__instance_number}")
|
|
||||||
self.__logger.debug(f"New instance {self.__instance_number} of class created")
|
|
||||||
|
|
||||||
# Check existence of provided file
|
|
||||||
self.__template_path = self.__get_template_path(path)
|
|
||||||
if isfile(path):
|
|
||||||
self.__logger.debug(f"Opening existing file at path: '{path}'")
|
|
||||||
else:
|
|
||||||
# Check if it is allowed to use an existing template
|
|
||||||
if allow_template:
|
|
||||||
if isfile(self.__template_path):
|
|
||||||
self.__logger.info(f"Using existing template file {self.__template_path}")
|
|
||||||
copy(self.__template_path, path)
|
|
||||||
self.__from_template = True
|
|
||||||
else:
|
|
||||||
self.__logger.warning("No potential template found, starting with empty file")
|
|
||||||
|
|
||||||
# Load the configfile
|
|
||||||
self.__file_path = path
|
|
||||||
self.__pending_changes = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.read_file(open(path))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.__logger.info(f"Successfully opened config file at path {path}")
|
|
||||||
|
|
||||||
# Check for update if allowed
|
|
||||||
if allow_update:
|
|
||||||
self.check_for_update()
|
|
||||||
|
|
||||||
def __get_template_path(self, path:str) -> str | None:
|
|
||||||
"""Looks for a existing template and returns the path to it
|
|
||||||
|
|
||||||
If no config file was found, None is returned"""
|
|
||||||
path_to_file, file_name = split(path)
|
|
||||||
file_name = file_name.split(".")[0]
|
|
||||||
path_to_temp = Path(path_to_file) / f".{file_name}.template"
|
|
||||||
|
|
||||||
return path_to_temp
|
|
||||||
|
|
||||||
def __has_all_template_options(self, template:configparser.ConfigParser):
|
|
||||||
"""Check if config contains all sections and options from the template"""
|
|
||||||
for section in template.sections():
|
|
||||||
if section not in self:
|
|
||||||
return False
|
|
||||||
template_options = set(template.options(section))
|
|
||||||
config_options = set(config.options(section))
|
|
||||||
if not template_options.issubset(config_options):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def set(self, section: str, option: str, value: str | None = None) -> None:
|
|
||||||
old_value = self.get(section, option, fallback=None)
|
|
||||||
if old_value:
|
|
||||||
self.__logger.debug(f"New value in section '{section}' at option '{option}' added: '{value}'")
|
|
||||||
self.__pending_changes += 1
|
|
||||||
else:
|
|
||||||
if self.has_option(section, option):
|
|
||||||
self.__logger.debug(f"Value in section '{section}' at option '{option}' changed: '{old_value}' -> '{value}'")
|
|
||||||
if old_value != value:
|
|
||||||
self.__pending_changes += 1
|
|
||||||
|
|
||||||
return super().set(section, option, value)
|
|
||||||
|
|
||||||
def remove_option(self, section, option):
|
|
||||||
if self.has_option(section, option):
|
|
||||||
value = self.get(section, option)
|
|
||||||
self.__logger.debug(f"Option removed in section '{section}' for '{option}': '{value}'")
|
|
||||||
self.__pending_changes += 1
|
|
||||||
|
|
||||||
return super().remove_option(section, option)
|
|
||||||
|
|
||||||
def remove_section(self, section):
|
|
||||||
if self.has_section(section):
|
|
||||||
self.__logger.info(f"Section removed: '{section}'")
|
|
||||||
self.__pending_changes += 1
|
|
||||||
|
|
||||||
return super().remove_section(section)
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
"""Saves changes to disk"""
|
|
||||||
with open(self.__file_path, 'w') as configfile:
|
|
||||||
self.write(configfile)
|
|
||||||
self.__logger.debug(f"Contents ({self.__pending_changes} changes) have been saved to disk")
|
|
||||||
|
|
||||||
def get_config_file_path(self) -> str:
|
|
||||||
"""Returns the path to the config file"""
|
|
||||||
return self.__file_path
|
|
||||||
|
|
||||||
def get_template_file_path(self) -> str:
|
|
||||||
"""Returns the path to the template file, a config could be created from
|
|
||||||
|
|
||||||
Returns None if the is no template file"""
|
|
||||||
return self.__template_path
|
|
||||||
|
|
||||||
def compare_to_template(self) -> str:
|
|
||||||
"""Returns one of multiple possible keywords depending on the relation of both files
|
|
||||||
|
|
||||||
+-----------------+--------------------------------------------------------------------------------------------------------------------------+
|
|
||||||
| Return Keyword | Description |
|
|
||||||
+-----------------+--------------------------------------------------------------------------------------------------------------------------+
|
|
||||||
| not_found | No template file found |
|
|
||||||
| equal | Both files contain the same sections and options |
|
|
||||||
| config_plus | Config contains additional sections and/or options compared to template |
|
|
||||||
| config_options | Config contains additional options but same sections |
|
|
||||||
| config_base | Same as "config_plus" but additionally config contains all sections and options from the template |
|
|
||||||
| config_minus | Config contains fewer sections and/or options as template |
|
|
||||||
+-----------------+--------------------------------------------------------------------------------------------------------------------------+"""
|
|
||||||
|
|
||||||
# Check existence of template
|
|
||||||
if self.__template_path == None:
|
|
||||||
return "not_found"
|
|
||||||
|
|
||||||
template_config = configparser.ConfigParser()
|
|
||||||
template_config.read_file(open(self.__template_path))
|
|
||||||
config_sections = set(self.sections())
|
|
||||||
template_sections = set(template_config.sections())
|
|
||||||
|
|
||||||
# Check if config contains all the same (or more) sections than template
|
|
||||||
same_or_more_sections = config_sections >= template_sections
|
|
||||||
|
|
||||||
# Check if both files have the same amount of sections
|
|
||||||
same_amount_of_sections = len(config_sections) == len(template_sections)
|
|
||||||
|
|
||||||
# Consolidate the options of both files
|
|
||||||
all_config_options = set()
|
|
||||||
all_template_options = set()
|
|
||||||
all_sections = config_sections.union(template_sections)
|
|
||||||
config_all_present = True
|
|
||||||
template_all_present = True
|
|
||||||
for section in all_sections:
|
|
||||||
try:
|
|
||||||
for option in self.options(section):
|
|
||||||
all_config_options.add(f"{section}.{option}")
|
|
||||||
except configparser.NoSectionError:
|
|
||||||
config_all_present = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
for option in template_config.options(section):
|
|
||||||
all_template_options.add(f"{section}.{option}")
|
|
||||||
except configparser.NoSectionError:
|
|
||||||
template_all_present = False
|
|
||||||
|
|
||||||
# Check if both have the same options
|
|
||||||
same_or_more_options = all_config_options >= all_template_options
|
|
||||||
|
|
||||||
# Check if both have the same amount of options
|
|
||||||
same_amount_of_options = len(all_config_options) == len(all_template_options)
|
|
||||||
|
|
||||||
# Check for different cases
|
|
||||||
# Since both files have the same (or more) options (and the options contain the section name) and contain the same amount, they must be equal
|
|
||||||
if same_or_more_options and same_amount_of_options:
|
|
||||||
return "equal"
|
|
||||||
|
|
||||||
# Since the config contains not even the same sections as present in the template, it doesn't contain the same base options
|
|
||||||
if not same_or_more_sections:
|
|
||||||
return "config_minus"
|
|
||||||
|
|
||||||
# If the config contains more sections or options than the template
|
|
||||||
if same_or_more_sections:
|
|
||||||
if same_or_more_options:
|
|
||||||
# If config contains all sections and options from template but also has extras
|
|
||||||
return "config_base"
|
|
||||||
else:
|
|
||||||
# Config has extra sections and/or options, but not necessarily all from template
|
|
||||||
return "config_plus"
|
|
||||||
|
|
||||||
# If the config contains the same sections but additional options within those sections
|
|
||||||
if same_amount_of_sections and not same_amount_of_options:
|
|
||||||
return "config_options"
|
|
||||||
|
|
||||||
return "config_plus"
|
|
||||||
|
|
||||||
def created_from_template(self) -> bool:
|
|
||||||
"""Will ONLY return `True` if the config file has been created from template at the first run"""
|
|
||||||
return self.__from_template
|
|
||||||
|
|
||||||
def check_for_update(self, omit_save:bool = False):
|
|
||||||
"""Updates the already created config to the same level as the template,
|
|
||||||
|
|
||||||
no options are deleted or their values changed."""
|
|
||||||
if self.__template_path == None:
|
|
||||||
raise FileNotFoundError
|
|
||||||
template_config = configparser.ConfigParser()
|
|
||||||
template_config.read_file(open(self.__template_path))
|
|
||||||
|
|
||||||
updated_options = 0
|
|
||||||
for section in template_config.sections():
|
|
||||||
options = template_config.options(section)
|
|
||||||
|
|
||||||
for option in options:
|
|
||||||
if not self.has_option(section, option):
|
|
||||||
self.set(section, option, template_config.get(section, option))
|
|
||||||
updated_options += 1
|
|
||||||
|
|
||||||
if updated_options:
|
|
||||||
_, file_name = split(self.__file_path)
|
|
||||||
self.__logger.error(f"The confg updated at {updated_options} locations, you might want check the {file_name} for unchanged placeholders")
|
|
||||||
|
|
||||||
if not omit_save:
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Dummy logger
|
|
||||||
console = logging.StreamHandler()
|
|
||||||
console.setLevel(logging.DEBUG)
|
|
||||||
logger = logging.getLogger("utils")
|
|
||||||
logger.addHandler(console)
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
file_path = Path(__file__).resolve()
|
|
||||||
|
|
||||||
base_path = file_path.parents[2]
|
|
||||||
print(f"Base path of the program: {base_path}")
|
|
||||||
|
|
||||||
config = Advanced_ConfigParser(Path.joinpath(base_path, "config", "bot.ini"))
|
|
||||||
print(config.compare_to_template())
|
|
||||||
# config["DEFAULT"]["Compression"] = "no"
|
|
||||||
# config.save()
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def get_elapsed_time(timestamp:float) -> str:
|
|
||||||
"""Convert an timestamp into an predefined elapsed time format (00min 00sec), without miliseconds"""
|
|
||||||
time_elapsed = datetime.fromtimestamp(timestamp)
|
|
||||||
return f"{time_elapsed.minute // 60}min {time_elapsed.second:02}sec"
|
|
||||||
|
|
||||||
def get_elapsed_time_ms(timestamp:float) -> str:
|
|
||||||
"""Convert an timestamp into an predefined elapsed time format (00min 00sec 000ms), with miliseconds"""
|
|
||||||
time_elapsed = datetime.fromtimestamp(timestamp)
|
|
||||||
return f"{time_elapsed.minute // 60}min {time_elapsed.second:02}sec {time_elapsed.microsecond // 1000}ms"
|
|
||||||
|
|
||||||
def get_elapsed_time_smal(timestamp:float) -> str:
|
|
||||||
"""Convert an timestamp into an predefined elapsed time format (00sec 000ms)"""
|
|
||||||
time_elapsed = datetime.fromtimestamp(timestamp)
|
|
||||||
return f"{time_elapsed.second:02}sec {time_elapsed.microsecond // 1000:03}ms"
|
|
||||||
|
|
||||||
def get_elapsed_time_big(timestamp:float) -> str:
|
|
||||||
"""Convert an timestamp into an predefined elapsed time format (00days 00hrs 00min)"""
|
|
||||||
time_elapsed = datetime.fromtimestamp(timestamp)
|
|
||||||
return f"{time_elapsed.day - 1}days {time_elapsed.hour - 1}hrs {1 if time_elapsed.minute == 0 else time_elapsed.minute}min"
|
|
||||||
|
|
||||||
def get_elapsed_time_milliseconds(timestamp:float) -> str:
|
|
||||||
"""Convert an timestamp into an predefined elapsed time format (0000ms)"""
|
|
||||||
time_elapsed = datetime.fromtimestamp(timestamp)
|
|
||||||
return f"{time_elapsed.second * 1000 + time_elapsed.microsecond // 1000}ms"
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
from datetime import datetime
|
|
||||||
import re
|
|
||||||
|
|
||||||
class Event_Counter:
|
|
||||||
"""A class to track and count events, storing the event timestamps and allowing queries
|
|
||||||
|
|
||||||
for the number of events in specific time windows (e.g., last 5 minutes, 10 minutes)."""
|
|
||||||
REGEX_PATTERN = r'(\d+)\s*(sec|second|seconds|s|min|minute|minutes|m|h|hour|hours|d|day|days)'
|
|
||||||
TIME_MULTIPLIERS = {
|
|
||||||
'sec': 1, 'second': 1, 'seconds': 1, 's': 1,
|
|
||||||
'min': 60, 'minute': 60, 'minutes': 60, 'm': 60,
|
|
||||||
'h': 3600, 'hour': 3600, 'hours': 3600,
|
|
||||||
'd': 86400, 'day': 86400, 'days': 86400
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, retention_duration:int = 900) -> None:
|
|
||||||
"""
|
|
||||||
Initializes the Event_Counter object with a specified retention duration.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
retention_duration (int, optional):
|
|
||||||
The time period (in seconds) for which event data should be retained. Defaults to 900 (15 minutes).
|
|
||||||
|
|
||||||
uwu
|
|
||||||
"""
|
|
||||||
self.__events:list[float] = []
|
|
||||||
self.__total_events = 0
|
|
||||||
self.__retention_duration = retention_duration
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""Cleans up events that are older than the retention duration.
|
|
||||||
|
|
||||||
Removes all events that occurred outside the defined retention window."""
|
|
||||||
offset_time = datetime.now().timestamp() - self.__retention_duration
|
|
||||||
while len(self.__events) > 0:
|
|
||||||
event_time = self.__events[0]
|
|
||||||
if event_time < offset_time:
|
|
||||||
self.__events.pop(0)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def duration_to_seconds(cls, duration_str:str) -> int:
|
|
||||||
"""Converts a human-readable duration string (e.g., '5min 2sec') into its equivalent in seconds.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
duration_str (str): The string representing the duration (e.g., "5min 2sec").
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: The total duration in seconds derived from the input string."""
|
|
||||||
matches = re.findall(cls.REGEX_PATTERN, duration_str, flags=re.IGNORECASE)
|
|
||||||
|
|
||||||
total_seconds = 0
|
|
||||||
for value, unit in matches:
|
|
||||||
unit = unit.lower()
|
|
||||||
total_seconds += int(value) * cls.TIME_MULTIPLIERS[unit]
|
|
||||||
|
|
||||||
return total_seconds
|
|
||||||
|
|
||||||
def increment(self, count:int = 1, skip_cleanup:bool = False):
|
|
||||||
"""Increments the event count by a specified number of events.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
count (int, optional): The number of events to add. Defaults to 1.
|
|
||||||
skip_cleanup (bool, optional): If True, skips the cleanup process. Defaults to False."""
|
|
||||||
timestamp = datetime.now().timestamp()
|
|
||||||
self.__total_events += count
|
|
||||||
self.__events.extend([timestamp] * count)
|
|
||||||
|
|
||||||
if skip_cleanup:
|
|
||||||
return
|
|
||||||
self.cleanup()
|
|
||||||
|
|
||||||
def get_count(self, time_window:str = "5M") -> int:
|
|
||||||
"""Returns the number of events that occurred within a specified time window.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
time_window (str, optional):
|
|
||||||
The time window to query, e.g., "5M" for 5 minutes. Defaults to "5M".
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: The count of events that occurred within the specified time window."""
|
|
||||||
self.cleanup()
|
|
||||||
|
|
||||||
offset_time = datetime.now().timestamp() - self.duration_to_seconds(time_window)
|
|
||||||
counter = 0
|
|
||||||
for event_time in self.__events:
|
|
||||||
if event_time > offset_time:
|
|
||||||
counter += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
return counter
|
|
||||||
|
|
||||||
def get_total_events(self) -> int:
|
|
||||||
"""Returns the total number of events tracked by the Event_Counter.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: The total number of events that have been recorded."""
|
|
||||||
return self.__total_events
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from time import sleep
|
|
||||||
counter = Event_Counter(1)
|
|
||||||
counter.increment()
|
|
||||||
sleep(1)
|
|
||||||
counter.increment()
|
|
||||||
print(counter.get_count("5sEc"))
|
|
||||||
sleep(0)
|
|
||||||
print(counter.get_count("5sEc"))
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import logging
|
|
||||||
from utils.logger.formatter import Colored_Formatter
|
|
||||||
|
|
||||||
class Custom_Logger:
|
|
||||||
console_handler: logging.StreamHandler
|
|
||||||
colored_formatter: Colored_Formatter
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def initialize(cls):
|
|
||||||
# Create the handler for logging to the console
|
|
||||||
cls.console_handler = logging.StreamHandler()
|
|
||||||
cls.console_handler.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
# Create the colorful formatter
|
|
||||||
cls.colored_formatter = Colored_Formatter()
|
|
||||||
cls.console_handler.setFormatter(cls.colored_formatter)
|
|
||||||
|
|
||||||
# Create the loggers for the different sections of the app
|
|
||||||
discordpy_logger = logging.getLogger("discord")
|
|
||||||
discordpy_logger.addHandler(cls.console_handler)
|
|
||||||
discordpy_logger.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
app_logger = logging.getLogger("app")
|
|
||||||
app_logger.addHandler(cls.console_handler)
|
|
||||||
app_logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
utils_logger = logging.getLogger("utils")
|
|
||||||
utils_logger.addHandler(cls.console_handler)
|
|
||||||
utils_logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
commands_logger = logging.getLogger("cmds")
|
|
||||||
commands_logger.addHandler(cls.console_handler)
|
|
||||||
commands_logger.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
logging.getLogger('discord.app_commands.tree').setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
app_logger.debug("Logging successfully initialized")
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
import discord
|
|
||||||
|
|
||||||
def log_command_execution(func):
|
|
||||||
"""Wraps an command function to print execution logs to the console"""
|
|
||||||
@wraps(func)
|
|
||||||
async def wrapper(self, interaction:discord.Interaction, *args, **kwargs):
|
|
||||||
self._logger.debug(f"User {interaction.user} in channel {interaction.channel} on guild {interaction.guild} executed the {func.__name__} command")
|
|
||||||
|
|
||||||
return await func(self, interaction, *args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
from logging import Formatter
|
|
||||||
from colorama import Style, Fore
|
|
||||||
|
|
||||||
class Colored_Formatter(Formatter):
|
|
||||||
def __init__(self, fmt=None, datefmt='%Y-%m-%d %H:%M:%S', style='%'):
|
|
||||||
super().__init__(fmt, datefmt, style)
|
|
||||||
|
|
||||||
def format(self, record):
|
|
||||||
# Formatieren des Zeitstempels fett
|
|
||||||
date = f"{Style.BRIGHT}{Fore.BLACK}{self.formatTime(record, self.datefmt)}{Style.RESET_ALL}"
|
|
||||||
|
|
||||||
# Färben des Log-Levels mit fester Breite
|
|
||||||
levelname_color = {
|
|
||||||
'DEBUG': Fore.CYAN,
|
|
||||||
'INFO': Fore.BLUE,
|
|
||||||
'WARNING': Fore.YELLOW,
|
|
||||||
'ERROR': Fore.RED,
|
|
||||||
'CRITICAL': f"{Style.BRIGHT}{Fore.RED}"
|
|
||||||
}
|
|
||||||
levelname = f"{levelname_color.get(record.levelname, Fore.WHITE)}{record.levelname:<8}{Fore.RESET}{Style.RESET_ALL}"
|
|
||||||
|
|
||||||
# Färben des Logger-Namens in Magenta mit fester Breite
|
|
||||||
name = f"{Fore.MAGENTA}{record.name:<20}{Fore.RESET}"
|
|
||||||
|
|
||||||
# Die Nachricht bleibt unverändert
|
|
||||||
message = record.getMessage()
|
|
||||||
|
|
||||||
# Zusammenfügen der gefärbten Teile
|
|
||||||
formatted_record = f"{date} {levelname} {name} {message}"
|
|
||||||
return formatted_record
|
|
||||||
@@ -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
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
class Singleton:
|
|
||||||
"""
|
|
||||||
A non-thread-safe helper class to ease implementing singletons.
|
|
||||||
This should be used as a decorator -- not a metaclass -- to the
|
|
||||||
class that should be a singleton.
|
|
||||||
|
|
||||||
The decorated class can define one `__init__` function that
|
|
||||||
takes only the `self` argument. Also, the decorated class cannot be
|
|
||||||
inherited from. Other than that, there are no restrictions that apply
|
|
||||||
to the decorated class.
|
|
||||||
|
|
||||||
To get the singleton instance, use the `instance` method. Trying
|
|
||||||
to use `__call__` will result in a `TypeError` being raised.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Source: https://stackoverflow.com/questions/31875/is-there-a-simple-elegant-way-to-define-singletons
|
|
||||||
|
|
||||||
def __init__(self, decorated):
|
|
||||||
self._decorated = decorated
|
|
||||||
|
|
||||||
def instance(self):
|
|
||||||
"""
|
|
||||||
Returns the singleton instance. Upon its first call, it creates a
|
|
||||||
new instance of the decorated class and calls its `__init__` method.
|
|
||||||
On all subsequent calls, the already created instance is returned.
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return self._instance
|
|
||||||
except AttributeError:
|
|
||||||
self._instance = self._decorated()
|
|
||||||
return self._instance
|
|
||||||
|
|
||||||
def __call__(self):
|
|
||||||
raise TypeError('Singletons must be accessed through `instance()`.')
|
|
||||||
|
|
||||||
def __instancecheck__(self, inst):
|
|
||||||
return isinstance(inst, self._decorated)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
def truncate_message_with_notice(output:str, max_length:int = 1000, suffix:str = "Output was truncated") -> str:
|
|
||||||
"""Trim a long output string to a specified length and append a suffix message if truncation occurs"""
|
|
||||||
|
|
||||||
total_max_length = max_length - len(suffix) # Adjust for the suffix length
|
|
||||||
|
|
||||||
# If the output is already shorter than the max length, return it as is
|
|
||||||
if len(output) <= max_length:
|
|
||||||
return output
|
|
||||||
|
|
||||||
# Split the output into lines and add lines until the length exceeds the allowed maximum
|
|
||||||
result = ""
|
|
||||||
lines = output.splitlines()
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
if len(result) + len(line) + 1 > total_max_length: # +1 accounts for the line break
|
|
||||||
break
|
|
||||||
result += line + "\n"
|
|
||||||
|
|
||||||
# Append the suffix message
|
|
||||||
result += suffix
|
|
||||||
return result
|
|
||||||
Reference in New Issue
Block a user