feat: generate and validate config.ini
All checks were successful
Build / Upload to production (push) Successful in 1m38s

Signed-off-by: mctaylors <cantsendmails@mctaylors.ru>
This commit is contained in:
Macintxsh 2025-06-09 01:19:06 +03:00
parent c64075f84d
commit d0357ff7c9
Signed by: mctaylors
GPG key ID: 4EEF4F949A266EE2
7 changed files with 113 additions and 77 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ build/
dist/
__pycache__/
*.spec
config.ini

View file

@ -8,8 +8,8 @@ from telegram.constants import ParseMode
from telegram.ext import ContextTypes
import html_parser
import config
from datetime import datetime, timedelta
from config import general, app
from extensions import get_json, format_rating, format_status, humanize_filesize
@ -43,8 +43,8 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
async def about_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
reply_markup = None
if general.sourceurl is not None:
keyboard = [[InlineKeyboardButton(f"source code", url=general.sourceurl)]]
if config.general.sourceurl is not None:
keyboard = [[InlineKeyboardButton(f"source code", url=config.general.sourceurl)]]
reply_markup = InlineKeyboardMarkup(keyboard)
await update.message.reply_text(
@ -54,7 +54,7 @@ async def about_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
f"{context.bot.first_name} is an inline image grabber written in python that grabs images from "
"Danbooru (or other similar services).\n",
html_parser.bold("currently configured instance:"),
f"{html_parser.italic(app.name)} ({app.hostname})",
f"{html_parser.italic(config.app.name)} ({config.app.hostname})",
]
),
parse_mode=ParseMode.HTML,
@ -85,8 +85,8 @@ async def info_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
keyboard = [
[
InlineKeyboardButton(
f"Open in {app.name}",
url=f"{app.protocol}://{app.hostname}/posts/{post_id}",
f"Open in {config.app.name}",
url=f"{config.app.protocol}://{config.app.hostname}/posts/{post_id}",
)
]
]
@ -99,11 +99,11 @@ async def info_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Uploader:",
html_parser.hyperlink(
uploader_data["name"],
f"{app.protocol}://{app.hostname}/users/{post_data['uploader_id']}",
f"{config.app.protocol}://{config.app.hostname}/users/{post_data['uploader_id']}",
),
html_parser.hyperlink(
"»",
f"{app.protocol}://{app.hostname}/posts?tags=user:{uploader_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/posts?tags=user:{uploader_data['name']}",
),
]
),
@ -115,7 +115,7 @@ async def info_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Date:",
html_parser.hyperlink(
created_at.strftime("%Y-%m-%d %X (%z)"),
f"{app.protocol}://{app.hostname}/posts?tags=date:{created_at.strftime("%Y-%m-%d")}",
f"{config.app.protocol}://{config.app.hostname}/posts?tags=date:{created_at.strftime("%Y-%m-%d")}",
),
]
)
@ -128,11 +128,11 @@ async def info_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Approver:",
html_parser.hyperlink(
approver_data["name"],
f"{app.protocol}://{app.hostname}/users/{post_data['approver_id']}",
f"{config.app.protocol}://{config.app.hostname}/users/{post_data['approver_id']}",
),
html_parser.hyperlink(
"»",
f"{app.protocol}://{app.hostname}/posts?tags=approver:{approver_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/posts?tags=approver:{approver_data['name']}",
),
]
)
@ -148,7 +148,7 @@ async def info_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
f"({post_data['media_asset']['image_width']}x{post_data['media_asset']['image_height']})",
html_parser.hyperlink(
"»",
f"{app.protocol}://{app.hostname}/media_assets/{post_data['media_asset']['id']}",
f"{config.app.protocol}://{config.app.hostname}/media_assets/{post_data['media_asset']['id']}",
),
]
)
@ -165,7 +165,7 @@ async def info_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Score:",
html_parser.hyperlink(
post_data["score"],
f"{app.protocol}://{app.hostname}/post_votes?search[post_id]={post_data['id']}&variant=compact",
f"{config.app.protocol}://{config.app.hostname}/post_votes?search[post_id]={post_data['id']}&variant=compact",
),
f"(+{post_data['up_score']} / -{post_data['down_score']})",
]
@ -177,7 +177,7 @@ async def info_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Favorites:",
html_parser.hyperlink(
post_data["fav_count"],
f"{app.protocol}://{app.hostname}/posts/{post_data['id']}/favorites",
f"{config.app.protocol}://{config.app.hostname}/posts/{post_data['id']}/favorites",
),
]
)
@ -228,8 +228,8 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
keyboard = [
[
InlineKeyboardButton(
f"Open in {app.name}",
url=f"{app.protocol}://{app.hostname}/users/{user_id}",
f"Open in {config.app.name}",
url=f"{config.app.protocol}://{config.app.hostname}/users/{user_id}",
)
]
]
@ -245,7 +245,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Uploads:",
html_parser.hyperlink(
user_data["post_upload_count"],
f"{app.protocol}://{app.hostname}/posts?tags=user:{user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/posts?tags=user:{user_data['name']}",
),
]
)
@ -270,7 +270,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Favorites:",
html_parser.hyperlink(
fav_post_count,
f"{app.protocol}://{app.hostname}/posts?tags=ordfav:{user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/posts?tags=ordfav:{user_data['name']}",
),
]
)
@ -288,16 +288,16 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Post Votes:",
html_parser.hyperlink(
vote_post_count,
f"{app.protocol}://{app.hostname}/post_votes?search[user_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/post_votes?search[user_name]={user_data['name']}",
),
f"({' '.join([
html_parser.hyperlink(
f"up:{upvote_post_count}",
f"{app.protocol}://{app.hostname}/post_votes?search[user_name]={user_data['name']}&search[score]=1",
f"{config.app.protocol}://{config.app.hostname}/post_votes?search[user_name]={user_data['name']}&search[score]=1",
),
html_parser.hyperlink(
f"down:{downvote_post_count}",
f"{app.protocol}://{app.hostname}/post_votes?search[user_name]={user_data['name']}&search[score]=-1",
f"{config.app.protocol}://{config.app.hostname}/post_votes?search[user_name]={user_data['name']}&search[score]=-1",
),
])})",
]
@ -309,7 +309,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Favorite Groups:",
html_parser.hyperlink(
user_data["favorite_group_count"],
f"{app.protocol}://{app.hostname}/favorite_groups?search[creator_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/favorite_groups?search[creator_name]={user_data['name']}",
),
]
)
@ -320,7 +320,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Post Changes:",
html_parser.hyperlink(
user_data["post_update_count"],
f"{app.protocol}://{app.hostname}/post_versions?search[updater_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/post_versions?search[updater_name]={user_data['name']}",
),
]
)
@ -334,12 +334,12 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Note Changes:",
html_parser.hyperlink(
user_data["note_update_count"],
f"{app.protocol}://{app.hostname}/note_versions?search[updater_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/note_versions?search[updater_name]={user_data['name']}",
),
"in",
html_parser.hyperlink(
noteupdater_post_count,
f"{app.protocol}://{app.hostname}/posts?tags=noteupdater:{user_data['name']}+order:note",
f"{config.app.protocol}://{config.app.hostname}/posts?tags=noteupdater:{user_data['name']}+order:note",
),
"posts",
]
@ -351,7 +351,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Wiki Page Changes:",
html_parser.hyperlink(
user_data["wiki_page_version_count"],
f"{app.protocol}://{app.hostname}/wiki_page_versions?search[updater_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/wiki_page_versions?search[updater_name]={user_data['name']}",
),
]
)
@ -362,7 +362,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Artist Changes:",
html_parser.hyperlink(
user_data["artist_version_count"],
f"{app.protocol}://{app.hostname}/artist_versions?search[updater_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/artist_versions?search[updater_name]={user_data['name']}",
),
]
)
@ -373,7 +373,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Commentary Changes:",
html_parser.hyperlink(
user_data["artist_commentary_version_count"],
f"{app.protocol}://{app.hostname}/artist_commentary_versions?search[updater_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/artist_commentary_versions?search[updater_name]={user_data['name']}",
),
]
)
@ -384,7 +384,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Forum Posts:",
html_parser.hyperlink(
user_data["forum_post_count"],
f"{app.protocol}://{app.hostname}/forum_posts?search[creator_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/forum_posts?search[creator_name]={user_data['name']}",
),
]
)
@ -398,12 +398,12 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Comments:",
html_parser.hyperlink(
user_data["comment_count"],
f"{app.protocol}://{app.hostname}/comments?group_by=comment&search[creator_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/comments?group_by=comment&search[creator_name]={user_data['name']}",
),
"in",
html_parser.hyperlink(
commenter_post_count,
f"{app.protocol}://{app.hostname}/posts?tags=commenter:{user_data['name']}+order:comment_bumped",
f"{config.app.protocol}://{config.app.hostname}/posts?tags=commenter:{user_data['name']}+order:comment_bumped",
),
"posts",
]
@ -415,7 +415,7 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Appeals:",
html_parser.hyperlink(
user_data["appeal_count"],
f"{app.protocol}://{app.hostname}/post_appeals?search[creator_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/post_appeals?search[creator_name]={user_data['name']}",
),
]
)
@ -431,20 +431,20 @@ async def user_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
"Feedback:",
html_parser.hyperlink(
total_feedback_count,
f"{app.protocol}://{app.hostname}/user_feedbacks?search[user_name]={user_data['name']}",
f"{config.app.protocol}://{config.app.hostname}/user_feedbacks?search[user_name]={user_data['name']}",
),
f"({' '.join([
html_parser.hyperlink(
f"positive:{user_data['positive_feedback_count']}",
f"{app.protocol}://{app.hostname}/user_feedbacks?search[user_name]={user_data['name']}&search[category]=positive",
f"{config.app.protocol}://{config.app.hostname}/user_feedbacks?search[user_name]={user_data['name']}&search[category]=positive",
),
html_parser.hyperlink(
f"neutral:{user_data['neutral_feedback_count']}",
f"{app.protocol}://{app.hostname}/user_feedbacks?search[user_name]={user_data['name']}&search[category]=neutral",
f"{config.app.protocol}://{config.app.hostname}/user_feedbacks?search[user_name]={user_data['name']}&search[category]=neutral",
),
html_parser.hyperlink(
f"negative:{user_data['negative_feedback_count']}",
f"{app.protocol}://{app.hostname}/user_feedbacks?search[user_name]={user_data['name']}&search[category]=negative",
f"{config.app.protocol}://{config.app.hostname}/user_feedbacks?search[user_name]={user_data['name']}&search[category]=negative",
),
])})"
]

View file

@ -1,21 +0,0 @@
[General]
; HTTP API token of Telegram bot.
; Can also be set with the BOT_TOKEN environment variable.
Token =
; (Optional) URL for "source code" button in /about.
; If not set, button will not appear in "about" menu.
SourceUrl = git.mctaylors.ru
[App]
; Application name (e.g. Danbooru).
Name = Danbooru
; Host (without http://) used to access the application API.
Host = danbooru.donmai.us
; (Optional) Public hostname (without http://) for example, the “Open in [App.Name]” button.
; Useful if the bot is hosted on the same server as the application.
; If not set, [App.Host] will be used instead.
HostName =
; Prefer the secure HTTPS protocol for generating links that use a public hostname.
; If True, bot will generate HTTPS links.
; If False, bot will generate HTTP links.
PreferSecureProtocol = True

View file

@ -1,10 +1,72 @@
import configparser
from os import path
from typing import Optional
import requests
config_file = "config.ini"
_c = configparser.ConfigParser()
_c.read("config.ini", "utf-8")
_c.optionxform = str
def init_config() -> None:
if _validate_config() is False:
print("Config is invalid.")
exit(1)
_c.read("config.ini", "utf-8")
global general, app
general = _General()
app = _App()
def _validate_config() -> bool:
default_config = {
"General": {"Token": "", "SourceUrl": ""},
"App": {
"Name": "Danbooru",
"Host": "danbooru.donmai.us",
"HostName": "",
"PreferSecureProtocol": True,
},
}
if not path.exists(config_file):
for section, options in default_config.items():
_c[section] = options
with open(config_file, "w") as cf:
_c.write(cf)
print(f"{config_file} generated successfully.")
else:
_c.read(config_file, "utf-8")
is_valid = True
for section, options in default_config.items():
if not _c.has_section(section):
print(f"Missing section: [{section}]")
is_valid = False
for option in options:
if not _c.has_option(section, option):
print(f"Missing option: '{option}' in section [{section}]")
is_valid = False
if not is_valid:
return False
if _c.get("App", "Name") == "":
print(f"Empty option: 'Name' in section [App]")
return False
try:
response = requests.get(
f"http://{_c.get("App", "Host")}/status.json", timeout=10
)
if response.status_code != 200:
raise requests.exceptions.InvalidURL
except requests.exceptions.RequestException:
print(f"Unable to validate: 'Host' in section [App]")
return False
return True
class _General:
@ -28,13 +90,6 @@ class _App:
def __init__(self):
self.name = _c.get("App", "Name")
self.host = _c.get("App", "Host")
try:
response = requests.get(f"http://{self.host}/status.json", timeout=10)
if response.status_code != 200:
raise requests.exceptions.InvalidURL
except requests.exceptions.RequestException:
print("Unable validate App.Host in config.ini.")
exit(1)
self.hostname = _c.get("App", "HostName")
if not self.hostname:
self.hostname = self.host
@ -43,5 +98,5 @@ class _App:
self.protocol = "https"
general = _General()
app = _App()
general = None
app = None

View file

@ -2,7 +2,7 @@ import re
import requests
from typing import Any, Optional
from config import app
import config
def humanize_tags_from_json(value: str, default: str) -> str:
@ -58,7 +58,7 @@ def format_status(data) -> str:
def get_json(pathname: str, query: Optional[list] = None) -> Any | None:
url = [f"http://{app.host}/{pathname}.json"]
url = [f"http://{config.app.host}/{pathname}.json"]
if query is not None:
url.append("?")
url.append("&".join(query))

View file

@ -13,7 +13,7 @@ from telegram.constants import ParseMode
from telegram.ext import ContextTypes
import html_parser
from config import app
import config
from extensions import humanize_tags_from_json, format_rating, get_json
@ -111,7 +111,7 @@ async def answer_query(
html_parser.bold(artists),
]
),
f"{app.protocol}://{app.hostname}/posts/{query}",
f"{config.app.protocol}://{config.app.hostname}/posts/{query}",
),
rating,
]

11
main.py
View file

@ -1,10 +1,9 @@
import logging
import os
import config
from telegram.ext import CommandHandler, InlineQueryHandler, ApplicationBuilder
from config import general
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
@ -15,6 +14,8 @@ logger = logging.getLogger(__name__)
def main() -> None:
config.init_config()
application = ApplicationBuilder().token(get_token()).build()
import commands
@ -39,11 +40,11 @@ def get_token() -> str:
if os.getenv("BOT_TOKEN") is not None:
return os.getenv("BOT_TOKEN")
if general.token != "":
return general.token
if config.general.token != "":
return config.general.token
print(
"Set BOT_TOKEN environment variable or use General.Token in config.ini to set bot token."
f"Set BOT_TOKEN environment variable or use 'Token' option in section [General] to set bot token."
)
exit(1)