mirror of
https://github.com/Steffo99/greed.git
synced 2024-11-21 13:34:18 +00:00
Completely rework the configuration system (#76)
* Do a lot of progress on the new config system * Completely rework the configuration system * Improve logging messages
This commit is contained in:
parent
378bbbad43
commit
6a49756394
11 changed files with 627 additions and 571 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -102,5 +102,6 @@ ENV/
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
config/config.ini
|
config/config.ini
|
||||||
|
config/config.toml
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite-journal
|
*.sqlite-journal
|
|
@ -1,110 +0,0 @@
|
||||||
# greed configuration file
|
|
||||||
|
|
||||||
# boolean parameters should be written in lowercase
|
|
||||||
|
|
||||||
# Config file parameters
|
|
||||||
[Config]
|
|
||||||
; Config file version. DO NOT EDIT THIS!
|
|
||||||
version = 19
|
|
||||||
; Set this to no when you are done editing the file
|
|
||||||
is_template = yes
|
|
||||||
|
|
||||||
# Language parameters
|
|
||||||
[Language]
|
|
||||||
; Available languages:
|
|
||||||
; it - Italian, by https://github.com/Steffo99
|
|
||||||
; en - English, by https://github.com/DarrenWestwood
|
|
||||||
; uk - Ukrainian, by https://github.com/pzhuk
|
|
||||||
; ru - Russian, by https://github.com/pzhuk
|
|
||||||
; zh_cn - Simplified Chinese, by https://github.com/zhihuiyuze
|
|
||||||
; he - Hebrew, by https://github.com/netanelkoli
|
|
||||||
; The lanugages that messages can be displayed in
|
|
||||||
enabled_languages = it | en | uk | ru | zh_cn | he
|
|
||||||
; The default language to be set for users whose language cannot be autodetected or whose language is not enabled
|
|
||||||
default_language = it
|
|
||||||
; The language to fallback to if a string is missing in a specific language
|
|
||||||
fallback_language = en
|
|
||||||
|
|
||||||
# Telegram bot parameters
|
|
||||||
[Telegram]
|
|
||||||
; Your bot token goes here. Get one from https://t.me/BotFather!
|
|
||||||
token = 123456789:YOUR_TOKEN_GOES_HERE_______________
|
|
||||||
; Time in seconds before a conversation (thread) with no new messages expires
|
|
||||||
; A lower value reduces memory usage, but can be inconvenient for the users
|
|
||||||
conversation_timeout = 7200
|
|
||||||
; Time to wait before sending another update request if there are no messages
|
|
||||||
long_polling_timeout = 30
|
|
||||||
; Time in seconds before retrying a request if it times out
|
|
||||||
timed_out_pause = 1
|
|
||||||
; Time in seconds before retrying a request that returned an error
|
|
||||||
error_pause = 5
|
|
||||||
|
|
||||||
# Database parameters
|
|
||||||
[Database]
|
|
||||||
; The database engine you want to use.
|
|
||||||
; Refer to http://docs.sqlalchemy.org/en/latest/core/engines.html for the possible settings.
|
|
||||||
engine = sqlite:///database.sqlite
|
|
||||||
|
|
||||||
# General payment settings
|
|
||||||
[Payments]
|
|
||||||
; ISO currency code
|
|
||||||
currency = EUR
|
|
||||||
; Currency exp parameter. You can find that on https://core.telegram.org/bots/payments/currencies.json.
|
|
||||||
; It has a value of 2 in most currencies (EUR, USD, GBP...)
|
|
||||||
currency_exp = 2
|
|
||||||
; Currency symbol which is show to the client users when displaying prices and transaction values
|
|
||||||
; If not defined here, default language specific currency symbol from strings would be used
|
|
||||||
currency_symbol = €
|
|
||||||
|
|
||||||
# Credit card payment settings
|
|
||||||
[Credit Card]
|
|
||||||
; Telegram Payments provider token obtainable at https://t.me/BotFather in the bot's Payments menu
|
|
||||||
; If empty, credit card payments are disabled.
|
|
||||||
# credit_card_token =
|
|
||||||
credit_card_token = 123456789:YOUR_TOKEN_HERE_
|
|
||||||
; Minimum wallet payment accepted (in miniumum currency units, $1.00 = 100 units)
|
|
||||||
min_amount = 1000
|
|
||||||
; Maximum wallet payment accepted (in miniumum currency units, $1.00 = 100 units)
|
|
||||||
max_amount = 10000
|
|
||||||
; The preset selections that can be made when adding credit to the wallet with a credit card
|
|
||||||
; Presets are pipe-separated |, and should never be outside the bounds provided by the min_amount and max_amount options
|
|
||||||
payment_presets = 10.00 | 25.00 | 50.00 | 100.00
|
|
||||||
; Make the user pay a extra fee when adding credit to the wallet with a credit card
|
|
||||||
; The formula for determining the total cost is:
|
|
||||||
; cost = added_funds + added_funds * fee_percentage / 100 + fee_fixed
|
|
||||||
; Set these values to 0 to disable the feature.
|
|
||||||
fee_percentage = 2.9
|
|
||||||
fee_fixed = 30
|
|
||||||
; "Shipping" information
|
|
||||||
; Telegram can ask for extra information when charging the user for a credit card transaction
|
|
||||||
; Set to yes the data you want to be required
|
|
||||||
; This data will be stored in the database
|
|
||||||
name_required = yes
|
|
||||||
email_required = yes
|
|
||||||
phone_required = yes
|
|
||||||
|
|
||||||
# Bot appearance settings
|
|
||||||
[Appearance]
|
|
||||||
; Display the full order information to the customers instead of the shortened version
|
|
||||||
; The full order information includes the order number and the timestamp of the order placement
|
|
||||||
full_order_info = no
|
|
||||||
; Allow balance refill during the order checkout in case of unsufficient balance
|
|
||||||
refill_on_checkout = yes
|
|
||||||
; Display welcome message (conversation_after_start) when the user sends /start
|
|
||||||
display_welcome_message = yes
|
|
||||||
|
|
||||||
|
|
||||||
# Exception reporting settings
|
|
||||||
[Error Reporting]
|
|
||||||
; Optional sentry token: get the token at https://sentry.io/ or ask @Steffo for one
|
|
||||||
; Needed to automatically report bugs found by the users in the code.
|
|
||||||
sentry_token = https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000
|
|
||||||
|
|
||||||
# Logging settings
|
|
||||||
[Logging]
|
|
||||||
; The output format for the messages printed to the console
|
|
||||||
; See https://docs.python.org/3/library/logging.html#logrecord-attributes for information about the {}-attributes
|
|
||||||
format = {asctime} | {threadName} | {name} | {message}
|
|
||||||
; Logging level: ignore all log entries with a level lower than the specified one
|
|
||||||
; Valid options are FATAL, ERROR, WARNING, INFO, and DEBUG
|
|
||||||
level = INFO
|
|
100
config/template_config.toml
Normal file
100
config/template_config.toml
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
# greed configuration file
|
||||||
|
|
||||||
|
# Language parameters
|
||||||
|
[Language]
|
||||||
|
# Available languages:
|
||||||
|
# it - Italian, by https://github.com/Steffo99
|
||||||
|
# en - English, by https://github.com/DarrenWestwood
|
||||||
|
# uk - Ukrainian, by https://github.com/pzhuk
|
||||||
|
# ru - Russian, by https://github.com/pzhuk
|
||||||
|
# zh_cn - Simplified Chinese, by https://github.com/zhihuiyuze
|
||||||
|
# he - Hebrew, by https://github.com/netanelkoli
|
||||||
|
# The lanugages that messages can be displayed in
|
||||||
|
enabled_languages = ["it", "en", "uk", "ru", "zh_cn", "he"]
|
||||||
|
# The default language to be set for users whose language cannot be autodetected or whose language is not enabled
|
||||||
|
default_language = "it"
|
||||||
|
# The language to fallback to if a string is missing in a specific language
|
||||||
|
fallback_language = "en"
|
||||||
|
|
||||||
|
|
||||||
|
# Database parameters
|
||||||
|
[Database]
|
||||||
|
# The database engine you want to use.
|
||||||
|
# Refer to http://docs.sqlalchemy.org/en/latest/core/engines.html for the possible settings.
|
||||||
|
engine = "sqlite:///database.sqlite"
|
||||||
|
|
||||||
|
|
||||||
|
# Telegram bot parameters
|
||||||
|
[Telegram]
|
||||||
|
# Your bot token goes here. Get one from https://t.me/BotFather!
|
||||||
|
token = "123456789:YOUR_TOKEN_GOES_HERE_______________"
|
||||||
|
# Time in seconds before a conversation (thread) with no new messages expires
|
||||||
|
# A lower value reduces memory usage, but can be inconvenient for the users
|
||||||
|
conversation_timeout = 7200
|
||||||
|
# Time to wait before sending another update request if there are no messages
|
||||||
|
long_polling_timeout = 30
|
||||||
|
# Time in seconds before retrying a request if it times out
|
||||||
|
timed_out_pause = 1
|
||||||
|
# Time in seconds before retrying a request that returned an error
|
||||||
|
error_pause = 5
|
||||||
|
|
||||||
|
|
||||||
|
# General payment settings
|
||||||
|
[Payments]
|
||||||
|
# ISO currency code
|
||||||
|
currency = "EUR"
|
||||||
|
# Currency exp parameter. You can find that on https://core.telegram.org/bots/payments/currencies.json.
|
||||||
|
# It has a value of 2 in most currencies (EUR, USD, GBP...)
|
||||||
|
currency_exp = 2
|
||||||
|
# Currency symbol which is show to the client users when displaying prices and transaction values
|
||||||
|
# If not defined here, default language specific currency symbol from strings would be used
|
||||||
|
currency_symbol = "€"
|
||||||
|
|
||||||
|
|
||||||
|
# Credit card payment settings
|
||||||
|
[Payments.CreditCard]
|
||||||
|
# Telegram Payments provider token obtainable at https://t.me/BotFather in the bot's Payments menu
|
||||||
|
# If empty, credit card payments are disabled.
|
||||||
|
# credit_card_token =
|
||||||
|
credit_card_token = "123456789:YOUR_TOKEN_HERE_"
|
||||||
|
# Minimum wallet payment accepted (in miniumum currency units, $1.00 = 100 units)
|
||||||
|
min_amount = 1000
|
||||||
|
# Maximum wallet payment accepted (in miniumum currency units, $1.00 = 100 units)
|
||||||
|
max_amount = 10000
|
||||||
|
# The preset selections that can be made when adding credit to the wallet with a credit card
|
||||||
|
# Presets are pipe-separated |, and should never be outside the bounds provided by the min_amount and max_amount options
|
||||||
|
payment_presets = [10.00, 25.00, 50.00, 100.00]
|
||||||
|
# Make the user pay a extra fee when adding credit to the wallet with a credit card
|
||||||
|
# The formula for determining the total cost is:
|
||||||
|
# cost = added_funds + added_funds * fee_percentage / 100 + fee_fixed
|
||||||
|
# Set these values to 0 to disable the feature.
|
||||||
|
fee_percentage = 2.9
|
||||||
|
fee_fixed = 30
|
||||||
|
# "Shipping" information
|
||||||
|
# Telegram can ask for extra information when charging the user for a credit card transaction
|
||||||
|
# Set to yes the data you want to be required
|
||||||
|
# This data will be stored in the database
|
||||||
|
name_required = true
|
||||||
|
email_required = true
|
||||||
|
phone_required = true
|
||||||
|
|
||||||
|
|
||||||
|
# Bot appearance settings
|
||||||
|
[Appearance]
|
||||||
|
# Display the full order information to the customers instead of the shortened version
|
||||||
|
# The full order information includes the order number and the timestamp of the order placement
|
||||||
|
full_order_info = false
|
||||||
|
# Allow balance refill during the order checkout in case of unsufficient balance
|
||||||
|
refill_on_checkout = true
|
||||||
|
# Display welcome message (conversation_after_start) when the user sends /start
|
||||||
|
display_welcome_message = true
|
||||||
|
|
||||||
|
|
||||||
|
# Logging settings
|
||||||
|
[Logging]
|
||||||
|
# The output format for the messages printed to the console
|
||||||
|
# See https://docs.python.org/3/library/logging.html#logrecord-attributes for information about the {}-attributes
|
||||||
|
format = "{asctime} | {threadName} | {name} | {message}"
|
||||||
|
# Logging level: ignore all log entries with a level lower than the specified one
|
||||||
|
# Valid options are FATAL, ERROR, WARNING, INFO, and DEBUG
|
||||||
|
level = "INFO"
|
|
@ -1,55 +0,0 @@
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import configparser
|
|
||||||
import logging
|
|
||||||
|
|
||||||
|
|
||||||
# Logs won't show up for this file as it is imported before logging is configured
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# Check if the config file exists, and create one if it doesn't
|
|
||||||
if not os.path.isfile("config/config.ini"):
|
|
||||||
log.debug("Creating config.ini from template_config.ini")
|
|
||||||
# Open the template file and create the config file
|
|
||||||
with open("config/template_config.ini", encoding="utf8") as template_file, \
|
|
||||||
open("config/config.ini", "w", encoding="utf8") as config_file:
|
|
||||||
# Copy the template file to the config file
|
|
||||||
config_file.write(template_file.read())
|
|
||||||
|
|
||||||
with open("config/template_config.ini", encoding="utf8") as template_file:
|
|
||||||
# Find the template version number
|
|
||||||
config = configparser.ConfigParser()
|
|
||||||
config.read_file(template_file)
|
|
||||||
template_version = int(config["Config"]["version"])
|
|
||||||
log.debug(f"Template is version {template_version}")
|
|
||||||
|
|
||||||
# Overwrite the template config with the values in the config
|
|
||||||
with open("config/config.ini", encoding="utf8") as config_file:
|
|
||||||
config.read_file(config_file)
|
|
||||||
config_version = int(config["Config"]["version"])
|
|
||||||
log.debug(f"Config is version {template_version}")
|
|
||||||
|
|
||||||
# Check if the file has been edited
|
|
||||||
if config["Config"]["is_template"] == "yes":
|
|
||||||
log.debug("Config is a template, aborting...")
|
|
||||||
log.fatal("A config file has been created in config/config.ini.\n"
|
|
||||||
"Edit it with your configuration, set the is_template flag to 'no' and restart this script.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Check if the version has changed from the template
|
|
||||||
if template_version > config_version:
|
|
||||||
log.debug("Config is older than Template, trying to merge...")
|
|
||||||
# Reset the is_template flag
|
|
||||||
config["Config"]["is_template"] = "yes"
|
|
||||||
# Update the config version
|
|
||||||
config["Config"]["version"] = str(template_version)
|
|
||||||
# Save the file
|
|
||||||
with open("config/config.ini", "w", encoding="utf8") as config_file:
|
|
||||||
log.debug("Writing merged config file...")
|
|
||||||
config.write(config_file)
|
|
||||||
# Notify the user and quit
|
|
||||||
log.debug("Config is now a template, aborting...")
|
|
||||||
log.fatal("The config file in config/config.ini has been updated.\n"
|
|
||||||
"Edit it with the new required data, set the is_template flag to true and restart this script.")
|
|
||||||
sys.exit(1)
|
|
73
core.py
73
core.py
|
@ -1,11 +1,16 @@
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import telegram
|
import telegram
|
||||||
import worker
|
import worker
|
||||||
import configloader
|
import nuconfig
|
||||||
import utils
|
|
||||||
import threading
|
import threading
|
||||||
import localization
|
import localization
|
||||||
import logging
|
import logging
|
||||||
|
import duckbot
|
||||||
|
import sqlalchemy
|
||||||
|
import sqlalchemy.orm
|
||||||
|
import sqlalchemy.ext.declarative as sed
|
||||||
|
import database
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import coloredlogs
|
import coloredlogs
|
||||||
|
@ -18,14 +23,47 @@ def main():
|
||||||
# Rename the main thread for presentation purposes
|
# Rename the main thread for presentation purposes
|
||||||
threading.current_thread().name = "Core"
|
threading.current_thread().name = "Core"
|
||||||
|
|
||||||
# Setup logging
|
# Start logging setup
|
||||||
log = logging.getLogger("core")
|
log = logging.getLogger("core")
|
||||||
logging.root.setLevel(configloader.config["Logging"]["level"])
|
logging.root.setLevel("INFO")
|
||||||
|
log.debug("Set logging level to INFO while the config is being loaded")
|
||||||
|
|
||||||
|
# Ensure the template config file exists
|
||||||
|
if not os.path.isfile("config/template_config.toml"):
|
||||||
|
log.fatal("config/template_config.toml does not exist!")
|
||||||
|
exit(254)
|
||||||
|
|
||||||
|
# If the config file does not exist, clone the template and exit
|
||||||
|
if not os.path.isfile("config/config.toml"):
|
||||||
|
log.debug("config/config.toml does not exist.")
|
||||||
|
|
||||||
|
with open("config/template_config.toml", encoding="utf8") as template_cfg_file, \
|
||||||
|
open("config/config.toml", "w", encoding="utf8") as user_cfg_file:
|
||||||
|
# Copy the template file to the config file
|
||||||
|
user_cfg_file.write(template_cfg_file.read())
|
||||||
|
|
||||||
|
log.fatal("A config file has been created in config/config.toml."
|
||||||
|
" Customize it, then restart greed!")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Compare the template config with the user-made one
|
||||||
|
with open("config/template_config.toml", encoding="utf8") as template_cfg_file, \
|
||||||
|
open("config/config.toml", encoding="utf8") as user_cfg_file:
|
||||||
|
template_cfg = nuconfig.NuConfig(template_cfg_file)
|
||||||
|
user_cfg = nuconfig.NuConfig(user_cfg_file)
|
||||||
|
if not template_cfg.cmplog(user_cfg):
|
||||||
|
log.fatal("There were errors while parsing the config.toml file. Please fix them and restart greed!")
|
||||||
|
exit(2)
|
||||||
|
else:
|
||||||
|
log.debug("Configuration parsed successfully!")
|
||||||
|
|
||||||
|
# Finish logging setup
|
||||||
|
logging.root.setLevel(user_cfg["Logging"]["level"])
|
||||||
stream_handler = logging.StreamHandler()
|
stream_handler = logging.StreamHandler()
|
||||||
if coloredlogs is not None:
|
if coloredlogs is not None:
|
||||||
stream_handler.formatter = coloredlogs.ColoredFormatter(configloader.config["Logging"]["format"], style="{")
|
stream_handler.formatter = coloredlogs.ColoredFormatter(user_cfg["Logging"]["format"], style="{")
|
||||||
else:
|
else:
|
||||||
stream_handler.formatter = logging.Formatter(configloader.config["Logging"]["format"], style="{")
|
stream_handler.formatter = logging.Formatter(user_cfg["Logging"]["format"], style="{")
|
||||||
logging.root.handlers.clear()
|
logging.root.handlers.clear()
|
||||||
logging.root.addHandler(stream_handler)
|
logging.root.addHandler(stream_handler)
|
||||||
log.debug("Logging setup successfully!")
|
log.debug("Logging setup successfully!")
|
||||||
|
@ -33,8 +71,18 @@ def main():
|
||||||
# Ignore most python-telegram-bot logs, as they are useless most of the time
|
# Ignore most python-telegram-bot logs, as they are useless most of the time
|
||||||
logging.getLogger("telegram").setLevel("ERROR")
|
logging.getLogger("telegram").setLevel("ERROR")
|
||||||
|
|
||||||
|
# Create the database engine
|
||||||
|
log.debug("Creating the sqlalchemy engine...")
|
||||||
|
engine = sqlalchemy.create_engine(user_cfg["Database"]["engine"])
|
||||||
|
log.debug("Preparing the tables through deferred reflection...")
|
||||||
|
sed.DeferredReflection.prepare(engine)
|
||||||
|
log.debug("Binding metadata to the engine...")
|
||||||
|
database.TableDeclarativeBase.metadata.bind = engine
|
||||||
|
log.debug("Creating all missing tables...")
|
||||||
|
database.TableDeclarativeBase.metadata.create_all()
|
||||||
|
|
||||||
# Create a bot instance
|
# Create a bot instance
|
||||||
bot = utils.DuckBot(configloader.config["Telegram"]["token"])
|
bot = duckbot.factory(user_cfg)()
|
||||||
|
|
||||||
# Test the specified token
|
# Test the specified token
|
||||||
log.debug("Testing bot token...")
|
log.debug("Testing bot token...")
|
||||||
|
@ -46,7 +94,7 @@ def main():
|
||||||
log.debug("Bot token is valid!")
|
log.debug("Bot token is valid!")
|
||||||
|
|
||||||
# Finding default language
|
# Finding default language
|
||||||
default_language = configloader.config["Language"]["default_language"]
|
default_language = user_cfg["Language"]["default_language"]
|
||||||
# Creating localization object
|
# Creating localization object
|
||||||
default_loc = localization.Localization(language=default_language, fallback=default_language)
|
default_loc = localization.Localization(language=default_language, fallback=default_language)
|
||||||
|
|
||||||
|
@ -63,9 +111,10 @@ def main():
|
||||||
# Main loop of the program
|
# Main loop of the program
|
||||||
while True:
|
while True:
|
||||||
# Get a new batch of 100 updates and mark the last 100 parsed as read
|
# Get a new batch of 100 updates and mark the last 100 parsed as read
|
||||||
log.debug("Getting updates from Telegram")
|
update_timeout = user_cfg["Telegram"]["long_polling_timeout"]
|
||||||
|
log.debug(f"Getting updates from Telegram with a timeout of {update_timeout} seconds")
|
||||||
updates = bot.get_updates(offset=next_update,
|
updates = bot.get_updates(offset=next_update,
|
||||||
timeout=int(configloader.config["Telegram"]["long_polling_timeout"]))
|
timeout=update_timeout)
|
||||||
# Parse all the updates
|
# Parse all the updates
|
||||||
for update in updates:
|
for update in updates:
|
||||||
# If the update is a message...
|
# If the update is a message...
|
||||||
|
@ -89,7 +138,9 @@ def main():
|
||||||
# Initialize a new worker for the chat
|
# Initialize a new worker for the chat
|
||||||
new_worker = worker.Worker(bot=bot,
|
new_worker = worker.Worker(bot=bot,
|
||||||
chat=update.message.chat,
|
chat=update.message.chat,
|
||||||
telegram_user=update.message.from_user)
|
telegram_user=update.message.from_user,
|
||||||
|
cfg=user_cfg,
|
||||||
|
engine=engine)
|
||||||
# Start the worker
|
# Start the worker
|
||||||
log.debug(f"Starting {new_worker.name}")
|
log.debug(f"Starting {new_worker.name}")
|
||||||
new_worker.start()
|
new_worker.start()
|
||||||
|
|
112
database.py
112
database.py
|
@ -1,29 +1,24 @@
|
||||||
import typing
|
import typing
|
||||||
from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint
|
from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint
|
||||||
from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary, DateTime, Boolean
|
from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary, DateTime, Boolean
|
||||||
from sqlalchemy.orm import sessionmaker, relationship, backref
|
from sqlalchemy.orm import relationship, backref
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base, DeferredReflection
|
||||||
import configloader
|
|
||||||
import telegram
|
import telegram
|
||||||
import requests
|
import requests
|
||||||
import utils
|
import utils
|
||||||
import localization
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
if typing.TYPE_CHECKING:
|
||||||
|
import worker
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Create a (lazy) database engine
|
|
||||||
engine = create_engine(configloader.config["Database"]["engine"])
|
|
||||||
|
|
||||||
# Create a base class to define all the database subclasses
|
# Create a base class to define all the database subclasses
|
||||||
TableDeclarativeBase = declarative_base(bind=engine)
|
TableDeclarativeBase = declarative_base()
|
||||||
|
|
||||||
# Create a Session class able to initialize database sessions
|
|
||||||
Session = sessionmaker()
|
|
||||||
|
|
||||||
|
|
||||||
# Define all the database tables using the sqlalchemy declarative base
|
# Define all the database tables using the sqlalchemy declarative base
|
||||||
class User(TableDeclarativeBase):
|
class User(DeferredReflection, TableDeclarativeBase):
|
||||||
"""A Telegram user who used the bot at least once."""
|
"""A Telegram user who used the bot at least once."""
|
||||||
|
|
||||||
# Telegram data
|
# Telegram data
|
||||||
|
@ -39,16 +34,18 @@ class User(TableDeclarativeBase):
|
||||||
# Extra table parameters
|
# Extra table parameters
|
||||||
__tablename__ = "users"
|
__tablename__ = "users"
|
||||||
|
|
||||||
def __init__(self, telegram_user: telegram.User, **kwargs):
|
def __init__(self, w: "worker.Worker", **kwargs):
|
||||||
# Initialize the super
|
# Initialize the super
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
# Get the data from telegram
|
# Get the data from telegram
|
||||||
self.user_id = telegram_user.id
|
self.user_id = w.telegram_user.id
|
||||||
self.first_name = telegram_user.first_name
|
self.first_name = w.telegram_user.first_name
|
||||||
self.last_name = telegram_user.last_name
|
self.last_name = w.telegram_user.last_name
|
||||||
self.username = telegram_user.username
|
self.username = w.telegram_user.username
|
||||||
self.language = telegram_user.language_code if telegram_user.language_code else configloader.config["Language"][
|
if w.telegram_user.language_code:
|
||||||
"default_language"]
|
self.language = w.telegram_user.language_code
|
||||||
|
else:
|
||||||
|
self.language = w.cfg["Language"]["default_language"]
|
||||||
# The starting wallet value is 0
|
# The starting wallet value is 0
|
||||||
self.credit = 0
|
self.credit = 0
|
||||||
|
|
||||||
|
@ -88,7 +85,7 @@ class User(TableDeclarativeBase):
|
||||||
return f"<User {self.mention()} having {self.credit} credit>"
|
return f"<User {self.mention()} having {self.credit} credit>"
|
||||||
|
|
||||||
|
|
||||||
class Product(TableDeclarativeBase):
|
class Product(DeferredReflection, TableDeclarativeBase):
|
||||||
"""A purchasable product."""
|
"""A purchasable product."""
|
||||||
|
|
||||||
# Product id
|
# Product id
|
||||||
|
@ -109,18 +106,18 @@ class Product(TableDeclarativeBase):
|
||||||
|
|
||||||
# No __init__ is needed, the default one is sufficient
|
# No __init__ is needed, the default one is sufficient
|
||||||
|
|
||||||
def text(self, *, loc: localization.Localization, style: str = "full", cart_qty: int = None):
|
def text(self, w: "worker.Worker", *, style: str = "full", cart_qty: int = None):
|
||||||
"""Return the product details formatted with Telegram HTML. The image is omitted."""
|
"""Return the product details formatted with Telegram HTML. The image is omitted."""
|
||||||
if style == "short":
|
if style == "short":
|
||||||
return f"{cart_qty}x {utils.telegram_html_escape(self.name)} - {str(utils.Price(self.price, loc) * cart_qty)}"
|
return f"{cart_qty}x {utils.telegram_html_escape(self.name)} - {str(w.Price(self.price) * cart_qty)}"
|
||||||
elif style == "full":
|
elif style == "full":
|
||||||
if cart_qty is not None:
|
if cart_qty is not None:
|
||||||
cart = loc.get("in_cart_format_string", quantity=cart_qty)
|
cart = w.loc.get("in_cart_format_string", quantity=cart_qty)
|
||||||
else:
|
else:
|
||||||
cart = ''
|
cart = ''
|
||||||
return loc.get("product_format_string", name=utils.telegram_html_escape(self.name),
|
return w.loc.get("product_format_string", name=utils.telegram_html_escape(self.name),
|
||||||
description=utils.telegram_html_escape(self.description),
|
description=utils.telegram_html_escape(self.description),
|
||||||
price=str(utils.Price(self.price, loc)),
|
price=str(w.Price(self.price)),
|
||||||
cart=cart)
|
cart=cart)
|
||||||
else:
|
else:
|
||||||
raise ValueError("style is not an accepted value")
|
raise ValueError("style is not an accepted value")
|
||||||
|
@ -128,18 +125,18 @@ class Product(TableDeclarativeBase):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Product {self.name}>"
|
return f"<Product {self.name}>"
|
||||||
|
|
||||||
def send_as_message(self, loc: localization.Localization, chat_id: int) -> dict:
|
def send_as_message(self, w: "worker.Worker", chat_id: int) -> dict:
|
||||||
"""Send a message containing the product data."""
|
"""Send a message containing the product data."""
|
||||||
if self.image is None:
|
if self.image is None:
|
||||||
r = requests.get(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendMessage",
|
r = requests.get(f"https://api.telegram.org/bot{w.cfg['Telegram']['token']}/sendMessage",
|
||||||
params={"chat_id": chat_id,
|
params={"chat_id": chat_id,
|
||||||
"text": self.text(loc=loc),
|
"text": self.text(w),
|
||||||
"parse_mode": "HTML"})
|
"parse_mode": "HTML"})
|
||||||
else:
|
else:
|
||||||
r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto",
|
r = requests.post(f"https://api.telegram.org/bot{w.cfg['Telegram']['token']}/sendPhoto",
|
||||||
files={"photo": self.image},
|
files={"photo": self.image},
|
||||||
params={"chat_id": chat_id,
|
params={"chat_id": chat_id,
|
||||||
"caption": self.text(loc=loc),
|
"caption": self.text(w),
|
||||||
"parse_mode": "HTML"})
|
"parse_mode": "HTML"})
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
@ -152,7 +149,7 @@ class Product(TableDeclarativeBase):
|
||||||
self.image = r.content
|
self.image = r.content
|
||||||
|
|
||||||
|
|
||||||
class Transaction(TableDeclarativeBase):
|
class Transaction(DeferredReflection, TableDeclarativeBase):
|
||||||
"""A greed wallet transaction.
|
"""A greed wallet transaction.
|
||||||
Wallet credit ISN'T calculated from these, but they can be used to recalculate it."""
|
Wallet credit ISN'T calculated from these, but they can be used to recalculate it."""
|
||||||
# TODO: split this into multiple tables
|
# TODO: split this into multiple tables
|
||||||
|
@ -188,10 +185,10 @@ class Transaction(TableDeclarativeBase):
|
||||||
__tablename__ = "transactions"
|
__tablename__ = "transactions"
|
||||||
__table_args__ = (UniqueConstraint("provider", "provider_charge_id"),)
|
__table_args__ = (UniqueConstraint("provider", "provider_charge_id"),)
|
||||||
|
|
||||||
def text(self, *, loc: localization.Localization):
|
def text(self, w: "worker.Worker"):
|
||||||
string = f"<b>T{self.transaction_id}</b> | {str(self.user)} | {utils.Price(self.value, loc)}"
|
string = f"<b>T{self.transaction_id}</b> | {str(self.user)} | {w.Price(self.value)}"
|
||||||
if self.refunded:
|
if self.refunded:
|
||||||
string += f" | {loc.get('emoji_refunded')}"
|
string += f" | {w.loc['emoji_refunded']}"
|
||||||
if self.provider:
|
if self.provider:
|
||||||
string += f" | {self.provider}"
|
string += f" | {self.provider}"
|
||||||
if self.notes:
|
if self.notes:
|
||||||
|
@ -202,7 +199,7 @@ class Transaction(TableDeclarativeBase):
|
||||||
return f"<Transaction {self.transaction_id} for User {self.user_id}>"
|
return f"<Transaction {self.transaction_id} for User {self.user_id}>"
|
||||||
|
|
||||||
|
|
||||||
class Admin(TableDeclarativeBase):
|
class Admin(DeferredReflection, TableDeclarativeBase):
|
||||||
"""A greed administrator with his permissions."""
|
"""A greed administrator with his permissions."""
|
||||||
|
|
||||||
# The telegram id
|
# The telegram id
|
||||||
|
@ -224,7 +221,7 @@ class Admin(TableDeclarativeBase):
|
||||||
return f"<Admin {self.user_id}>"
|
return f"<Admin {self.user_id}>"
|
||||||
|
|
||||||
|
|
||||||
class Order(TableDeclarativeBase):
|
class Order(DeferredReflection, TableDeclarativeBase):
|
||||||
"""An order which has been placed by an user.
|
"""An order which has been placed by an user.
|
||||||
It may include multiple products, available in the OrderItem table."""
|
It may include multiple products, available in the OrderItem table."""
|
||||||
|
|
||||||
|
@ -254,41 +251,41 @@ class Order(TableDeclarativeBase):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Order {self.order_id} placed by User {self.user_id}>"
|
return f"<Order {self.order_id} placed by User {self.user_id}>"
|
||||||
|
|
||||||
def text(self, *, loc: localization.Localization, session, user=False):
|
def text(self, w: "worker.Worker", session, user=False):
|
||||||
joined_self = session.query(Order).filter_by(order_id=self.order_id).join(Transaction).one()
|
joined_self = session.query(Order).filter_by(order_id=self.order_id).join(Transaction).one()
|
||||||
items = ""
|
items = ""
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
items += item.text(loc=loc) + "\n"
|
items += item.text(w) + "\n"
|
||||||
if self.delivery_date is not None:
|
if self.delivery_date is not None:
|
||||||
status_emoji = loc.get("emoji_completed")
|
status_emoji = w.loc.get("emoji_completed")
|
||||||
status_text = loc.get("text_completed")
|
status_text = w.loc.get("text_completed")
|
||||||
elif self.refund_date is not None:
|
elif self.refund_date is not None:
|
||||||
status_emoji = loc.get("emoji_refunded")
|
status_emoji = w.loc.get("emoji_refunded")
|
||||||
status_text = loc.get("text_refunded")
|
status_text = w.loc.get("text_refunded")
|
||||||
else:
|
else:
|
||||||
status_emoji = loc.get("emoji_not_processed")
|
status_emoji = w.loc.get("emoji_not_processed")
|
||||||
status_text = loc.get("text_not_processed")
|
status_text = w.loc.get("text_not_processed")
|
||||||
if user and configloader.config["Appearance"]["full_order_info"] == "no":
|
if user and w.cfg["Appearance"]["full_order_info"] == "no":
|
||||||
return loc.get("user_order_format_string",
|
return w.loc.get("user_order_format_string",
|
||||||
status_emoji=status_emoji,
|
status_emoji=status_emoji,
|
||||||
status_text=status_text,
|
status_text=status_text,
|
||||||
items=items,
|
items=items,
|
||||||
notes=self.notes,
|
notes=self.notes,
|
||||||
value=str(utils.Price(-joined_self.transaction.value, loc))) + \
|
value=str(w.Price(-joined_self.transaction.value))) + \
|
||||||
(loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "")
|
(w.loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "")
|
||||||
else:
|
else:
|
||||||
return status_emoji + " " + \
|
return status_emoji + " " + \
|
||||||
loc.get("order_number", id=self.order_id) + "\n" + \
|
w.loc.get("order_number", id=self.order_id) + "\n" + \
|
||||||
loc.get("order_format_string",
|
w.loc.get("order_format_string",
|
||||||
user=self.user.mention(),
|
user=self.user.mention(),
|
||||||
date=self.creation_date.isoformat(),
|
date=self.creation_date.isoformat(),
|
||||||
items=items,
|
items=items,
|
||||||
notes=self.notes if self.notes is not None else "",
|
notes=self.notes if self.notes is not None else "",
|
||||||
value=str(utils.Price(-joined_self.transaction.value, loc))) + \
|
value=str(w.Price(-joined_self.transaction.value))) + \
|
||||||
(loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "")
|
(w.loc.get("refund_reason", reason=self.refund_reason) if self.refund_date is not None else "")
|
||||||
|
|
||||||
|
|
||||||
class OrderItem(TableDeclarativeBase):
|
class OrderItem(DeferredReflection, TableDeclarativeBase):
|
||||||
"""A product that has been purchased as part of an order."""
|
"""A product that has been purchased as part of an order."""
|
||||||
|
|
||||||
# The unique item id
|
# The unique item id
|
||||||
|
@ -302,11 +299,8 @@ class OrderItem(TableDeclarativeBase):
|
||||||
# Extra table parameters
|
# Extra table parameters
|
||||||
__tablename__ = "orderitems"
|
__tablename__ = "orderitems"
|
||||||
|
|
||||||
def text(self, *, loc: localization.Localization):
|
def text(self, w: "worker.Worker"):
|
||||||
return f"{self.product.name} - {str(utils.Price(self.product.price, loc))}"
|
return f"{self.product.name} - {str(w.Price(self.product.price))}"
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<OrderItem {self.item_id}>"
|
return f"<OrderItem {self.item_id}>"
|
||||||
|
|
||||||
|
|
||||||
TableDeclarativeBase.metadata.create_all()
|
|
||||||
|
|
116
duckbot.py
Normal file
116
duckbot.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import nuconfig
|
||||||
|
import telegram.error
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import sys
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def factory(cfg: nuconfig.NuConfig):
|
||||||
|
"""Construct a DuckBot type based on the passed config."""
|
||||||
|
|
||||||
|
def catch_telegram_errors(func):
|
||||||
|
"""Decorator, can be applied to any function to retry in case of Telegram errors."""
|
||||||
|
|
||||||
|
def result_func(*args, **kwargs):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
# Bot was blocked by the user
|
||||||
|
except telegram.error.Unauthorized:
|
||||||
|
log.debug(f"Unauthorized to call {func.__name__}(), skipping.")
|
||||||
|
break
|
||||||
|
# Telegram API didn't answer in time
|
||||||
|
except telegram.error.TimedOut:
|
||||||
|
log.warning(f"Timed out while calling {func.__name__}(),"
|
||||||
|
f" retrying in {cfg['Telegram']['timed_out_pause']} secs...")
|
||||||
|
time.sleep(cfg["Telegram"]["timed_out_pause"])
|
||||||
|
# Telegram is not reachable
|
||||||
|
except telegram.error.NetworkError as error:
|
||||||
|
log.error(f"Network error while calling {func.__name__}(),"
|
||||||
|
f" retrying in {cfg['Telegram']['error_pause']} secs...\n"
|
||||||
|
f"Full error: {error.message}")
|
||||||
|
time.sleep(cfg["Telegram"]["error_pause"])
|
||||||
|
# Unknown error
|
||||||
|
except telegram.error.TelegramError as error:
|
||||||
|
if error.message.lower() in ["bad gateway", "invalid server response"]:
|
||||||
|
log.warning(f"Bad Gateway while calling {func.__name__}(),"
|
||||||
|
f" retrying in {cfg['Telegram']['error_pause']} secs...")
|
||||||
|
time.sleep(cfg["Telegram"]["error_pause"])
|
||||||
|
elif error.message.lower() == "timed out":
|
||||||
|
log.warning(f"Timed out while calling {func.__name__}(),"
|
||||||
|
f" retrying in {cfg['Telegram']['timed_out_pause']} secs...")
|
||||||
|
time.sleep(cfg["Telegram"]["timed_out_pause"])
|
||||||
|
else:
|
||||||
|
log.error(f"Telegram error while calling {func.__name__}(),"
|
||||||
|
f" retrying in {cfg['Telegram']['error_pause']} secs...\n"
|
||||||
|
f"Full error: {error.message}")
|
||||||
|
traceback.print_exception(*sys.exc_info())
|
||||||
|
time.sleep(cfg["Telegram"]["error_pause"])
|
||||||
|
|
||||||
|
return result_func
|
||||||
|
|
||||||
|
class DuckBot:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.bot = telegram.Bot(token=cfg["Telegram"]["token"], *args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def send_message(self, *args, **kwargs):
|
||||||
|
# All messages are sent in HTML parse mode
|
||||||
|
return self.bot.send_message(parse_mode="HTML", *args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def edit_message_text(self, *args, **kwargs):
|
||||||
|
# All messages are sent in HTML parse mode
|
||||||
|
return self.bot.edit_message_text(parse_mode="HTML", *args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def edit_message_caption(self, *args, **kwargs):
|
||||||
|
# All messages are sent in HTML parse mode
|
||||||
|
return self.bot.edit_message_caption(parse_mode="HTML", *args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def edit_message_reply_markup(self, *args, **kwargs):
|
||||||
|
return self.bot.edit_message_reply_markup(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def get_updates(self, *args, **kwargs):
|
||||||
|
return self.bot.get_updates(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def get_me(self, *args, **kwargs):
|
||||||
|
return self.bot.get_me(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def answer_callback_query(self, *args, **kwargs):
|
||||||
|
return self.bot.answer_callback_query(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def answer_pre_checkout_query(self, *args, **kwargs):
|
||||||
|
return self.bot.answer_pre_checkout_query(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def send_invoice(self, *args, **kwargs):
|
||||||
|
return self.bot.send_invoice(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def get_file(self, *args, **kwargs):
|
||||||
|
return self.bot.get_file(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def send_chat_action(self, *args, **kwargs):
|
||||||
|
return self.bot.send_chat_action(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def delete_message(self, *args, **kwargs):
|
||||||
|
return self.bot.delete_message(*args, **kwargs)
|
||||||
|
|
||||||
|
@catch_telegram_errors
|
||||||
|
def send_document(self, *args, **kwargs):
|
||||||
|
return self.bot.send_document(*args, **kwargs)
|
||||||
|
|
||||||
|
# More methods can be added here
|
||||||
|
|
||||||
|
return DuckBot
|
91
nuconfig.py
Normal file
91
nuconfig.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
from typing import *
|
||||||
|
import toml
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
CompareReport = Dict[str, Union[str, List[str], "Missing"]]
|
||||||
|
|
||||||
|
|
||||||
|
class NuConfig:
|
||||||
|
def __init__(self, file: "TextIO"):
|
||||||
|
self.data = toml.load(file)
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return self.data.__getitem__(item)
|
||||||
|
|
||||||
|
def cmplog(self, other) -> bool:
|
||||||
|
"""Compare two different NuConfig objects and log information about which keys are missing or invalid.
|
||||||
|
Returns a bool, which is false if there was something to report and true otherwise."""
|
||||||
|
compare_report: CompareReport = self.compare(other)
|
||||||
|
self.__cmplog_log(compare_report)
|
||||||
|
return compare_report == {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __cmplog_log(compare_report: CompareReport, root: str = "") -> None:
|
||||||
|
"""The recursive portion of :meth:`.cmplog`."""
|
||||||
|
for item in compare_report.get("__missing__", []):
|
||||||
|
log.error(f"Missing key: {root}{item}")
|
||||||
|
|
||||||
|
for item in compare_report.get("__invalid__", []):
|
||||||
|
log.error(f"Key has an invalid type: {root}{item}")
|
||||||
|
|
||||||
|
for key, value in compare_report.items():
|
||||||
|
if key == "__missing__" or key == "__invalid__":
|
||||||
|
continue
|
||||||
|
NuConfig.__cmplog_log(value, root=f"{root}{key}.")
|
||||||
|
|
||||||
|
def compare(self, other: "NuConfig") -> CompareReport:
|
||||||
|
"""Compare two different NuConfig objects and return a dictionary of the keys missing in the other."""
|
||||||
|
if not isinstance(other, NuConfig):
|
||||||
|
raise TypeError("You can only compare two NuConfig objects.")
|
||||||
|
return self.__compare_recurse(self.data, other.data)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __compare_miss(self: dict) -> CompareReport:
|
||||||
|
"""Mark all keys of a dict as missing."""
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for key, value in self.items():
|
||||||
|
missing.append(key)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
result[key] = NuConfig.__compare_miss(value)
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
result["__missing__"] = missing
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __compare_recurse(self: dict, other: dict) -> CompareReport:
|
||||||
|
"""The recursive portion of :meth:`.compare`."""
|
||||||
|
invalid = []
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for key, value in self.items():
|
||||||
|
try:
|
||||||
|
other_value = other[key]
|
||||||
|
except KeyError:
|
||||||
|
missing.append(key)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
result[key] = NuConfig.__compare_miss(value)
|
||||||
|
else:
|
||||||
|
if type(value) != type(other_value):
|
||||||
|
invalid.append(key)
|
||||||
|
if isinstance(value, dict):
|
||||||
|
result[key] = NuConfig.__compare_miss(value)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
recursive_result = NuConfig.__compare_recurse(value, other_value)
|
||||||
|
if recursive_result != {}:
|
||||||
|
result[key] = recursive_result
|
||||||
|
|
||||||
|
if invalid:
|
||||||
|
result["__invalid__"] = invalid
|
||||||
|
if missing:
|
||||||
|
result["__missing__"] = missing
|
||||||
|
|
||||||
|
return result
|
|
@ -1,4 +1,4 @@
|
||||||
python-telegram-bot
|
python-telegram-bot
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
requests
|
requests
|
||||||
raven
|
toml
|
||||||
|
|
228
utils.py
228
utils.py
|
@ -1,233 +1,5 @@
|
||||||
import telegram
|
|
||||||
import telegram.error
|
|
||||||
import time
|
|
||||||
from configloader import config
|
|
||||||
import typing
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import importlib
|
|
||||||
import logging
|
|
||||||
import traceback
|
|
||||||
import localization
|
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
if config["Error Reporting"]["sentry_token"] != \
|
|
||||||
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
|
|
||||||
import raven
|
|
||||||
import raven.exceptions
|
|
||||||
try:
|
|
||||||
release = raven.fetch_git_sha(os.path.dirname(__file__))
|
|
||||||
except raven.exceptions.InvalidGitRepository:
|
|
||||||
release = "Unknown"
|
|
||||||
sentry_client = raven.Client(config["Error Reporting"]["sentry_token"],
|
|
||||||
release=release,
|
|
||||||
environment="Dev" if __debug__ else "Prod")
|
|
||||||
else:
|
|
||||||
sentry_client = None
|
|
||||||
|
|
||||||
|
|
||||||
class Price:
|
|
||||||
"""The base class for the prices in greed.
|
|
||||||
Its int value is in minimum units, while its float and str values are in decimal format.int("""
|
|
||||||
|
|
||||||
def __init__(self, value: typing.Union[int, float, str, "Price"], loc: localization.Localization):
|
|
||||||
# Keep a reference to the localization file
|
|
||||||
self.loc = loc
|
|
||||||
if isinstance(value, int):
|
|
||||||
# Keep the value as it is
|
|
||||||
self.value = int(value)
|
|
||||||
elif isinstance(value, float):
|
|
||||||
# Convert the value to minimum units
|
|
||||||
self.value = int(value * (10 ** int(config["Payments"]["currency_exp"])))
|
|
||||||
elif isinstance(value, str):
|
|
||||||
# Remove decimal points, then cast to int
|
|
||||||
self.value = int(float(value.replace(",", ".")) * (10 ** int(config["Payments"]["currency_exp"])))
|
|
||||||
elif isinstance(value, Price):
|
|
||||||
# Copy self
|
|
||||||
self.value = value.value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<Price of value {self.value}>"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.loc.get("currency_format_string",
|
|
||||||
symbol=config["Payments"]["currency_symbol"],
|
|
||||||
value="{0:.2f}".format(self.value / (10 ** int(config["Payments"]["currency_exp"]))))
|
|
||||||
|
|
||||||
def __int__(self):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
def __float__(self):
|
|
||||||
return self.value / (10 ** int(config["Payments"]["currency_exp"]))
|
|
||||||
|
|
||||||
def __ge__(self, other):
|
|
||||||
return self.value >= Price(other, self.loc).value
|
|
||||||
|
|
||||||
def __le__(self, other):
|
|
||||||
return self.value <= Price(other, self.loc).value
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.value == Price(other, self.loc).value
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
return self.value > Price(other, self.loc).value
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
return self.value < Price(other, self.loc).value
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
return Price(self.value + Price(other, self.loc).value, self.loc)
|
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
return Price(self.value - Price(other, self.loc).value, self.loc)
|
|
||||||
|
|
||||||
def __mul__(self, other):
|
|
||||||
return Price(int(self.value * other), self.loc)
|
|
||||||
|
|
||||||
def __floordiv__(self, other):
|
|
||||||
return Price(int(self.value // other), self.loc)
|
|
||||||
|
|
||||||
def __radd__(self, other):
|
|
||||||
return self.__add__(other)
|
|
||||||
|
|
||||||
def __rsub__(self, other):
|
|
||||||
return Price(Price(other, self.loc).value - self.value, self.loc)
|
|
||||||
|
|
||||||
def __rmul__(self, other):
|
|
||||||
|
|
||||||
return self.__mul__(other)
|
|
||||||
|
|
||||||
def __iadd__(self, other):
|
|
||||||
self.value += Price(other, self.loc).value
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __isub__(self, other):
|
|
||||||
self.value -= Price(other, self.loc).value
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __imul__(self, other):
|
|
||||||
self.value *= other
|
|
||||||
self.value = int(self.value)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __ifloordiv__(self, other):
|
|
||||||
self.value //= other
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
def telegram_html_escape(string: str):
|
def telegram_html_escape(string: str):
|
||||||
return string.replace("<", "<") \
|
return string.replace("<", "<") \
|
||||||
.replace(">", ">") \
|
.replace(">", ">") \
|
||||||
.replace("&", "&") \
|
.replace("&", "&") \
|
||||||
.replace('"', """)
|
.replace('"', """)
|
||||||
|
|
||||||
|
|
||||||
def catch_telegram_errors(func):
|
|
||||||
"""Decorator, can be applied to any function to retry in case of Telegram errors."""
|
|
||||||
|
|
||||||
def result_func(*args, **kwargs):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
# Bot was blocked by the user
|
|
||||||
except telegram.error.Unauthorized:
|
|
||||||
log.debug(f"Unauthorized to call {func.__name__}(), skipping.")
|
|
||||||
break
|
|
||||||
# Telegram API didn't answer in time
|
|
||||||
except telegram.error.TimedOut:
|
|
||||||
log.warning(f"Timed out while calling {func.__name__}(),"
|
|
||||||
f" retrying in {config['Telegram']['timed_out_pause']} secs...")
|
|
||||||
time.sleep(int(config["Telegram"]["timed_out_pause"]))
|
|
||||||
# Telegram is not reachable
|
|
||||||
except telegram.error.NetworkError as error:
|
|
||||||
log.error(f"Network error while calling {func.__name__}(),"
|
|
||||||
f" retrying in {config['Telegram']['error_pause']} secs...\n"
|
|
||||||
f"Full error: {error.message}")
|
|
||||||
time.sleep(int(config["Telegram"]["error_pause"]))
|
|
||||||
# Unknown error
|
|
||||||
except telegram.error.TelegramError as error:
|
|
||||||
if error.message.lower() in ["bad gateway", "invalid server response"]:
|
|
||||||
log.warning(f"Bad Gateway while calling {func.__name__}(),"
|
|
||||||
f" retrying in {config['Telegram']['error_pause']} secs...")
|
|
||||||
time.sleep(int(config["Telegram"]["error_pause"]))
|
|
||||||
elif error.message.lower() == "timed out":
|
|
||||||
log.warning(f"Timed out while calling {func.__name__}(),"
|
|
||||||
f" retrying in {config['Telegram']['timed_out_pause']} secs...")
|
|
||||||
time.sleep(int(config["Telegram"]["timed_out_pause"]))
|
|
||||||
else:
|
|
||||||
log.error(f"Telegram error while calling {func.__name__}(),"
|
|
||||||
f" retrying in {config['Telegram']['error_pause']} secs...\n"
|
|
||||||
f"Full error: {error.message}")
|
|
||||||
# Send the error to the Sentry server
|
|
||||||
if sentry_client is not None:
|
|
||||||
sentry_client.captureException(exc_info=sys.exc_info())
|
|
||||||
else:
|
|
||||||
traceback.print_exception(*sys.exc_info())
|
|
||||||
time.sleep(int(config["Telegram"]["error_pause"]))
|
|
||||||
|
|
||||||
return result_func
|
|
||||||
|
|
||||||
|
|
||||||
class DuckBot:
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.bot = telegram.Bot(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def send_message(self, *args, **kwargs):
|
|
||||||
# All messages are sent in HTML parse mode
|
|
||||||
return self.bot.send_message(parse_mode="HTML", *args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def edit_message_text(self, *args, **kwargs):
|
|
||||||
# All messages are sent in HTML parse mode
|
|
||||||
return self.bot.edit_message_text(parse_mode="HTML", *args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def edit_message_caption(self, *args, **kwargs):
|
|
||||||
# All messages are sent in HTML parse mode
|
|
||||||
return self.bot.edit_message_caption(parse_mode="HTML", *args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def edit_message_reply_markup(self, *args, **kwargs):
|
|
||||||
return self.bot.edit_message_reply_markup(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def get_updates(self, *args, **kwargs):
|
|
||||||
return self.bot.get_updates(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def get_me(self, *args, **kwargs):
|
|
||||||
return self.bot.get_me(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def answer_callback_query(self, *args, **kwargs):
|
|
||||||
return self.bot.answer_callback_query(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def answer_pre_checkout_query(self, *args, **kwargs):
|
|
||||||
return self.bot.answer_pre_checkout_query(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def send_invoice(self, *args, **kwargs):
|
|
||||||
return self.bot.send_invoice(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def get_file(self, *args, **kwargs):
|
|
||||||
return self.bot.get_file(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def send_chat_action(self, *args, **kwargs):
|
|
||||||
return self.bot.send_chat_action(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def delete_message(self, *args, **kwargs):
|
|
||||||
return self.bot.delete_message(*args, **kwargs)
|
|
||||||
|
|
||||||
@catch_telegram_errors
|
|
||||||
def send_document(self, *args, **kwargs):
|
|
||||||
return self.bot.send_document(*args, **kwargs)
|
|
||||||
|
|
||||||
# More methods can be added here
|
|
||||||
|
|
288
worker.py
288
worker.py
|
@ -3,18 +3,18 @@ from typing import *
|
||||||
import uuid
|
import uuid
|
||||||
import datetime
|
import datetime
|
||||||
import telegram
|
import telegram
|
||||||
import configloader
|
import nuconfig
|
||||||
import sys
|
import sys
|
||||||
import queue as queuem
|
import queue as queuem
|
||||||
import database as db
|
import database as db
|
||||||
import re
|
import re
|
||||||
import utils
|
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
from html import escape
|
from html import escape
|
||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
import localization
|
import localization
|
||||||
|
import sqlalchemy.orm
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -33,16 +33,24 @@ class CancelSignal:
|
||||||
class Worker(threading.Thread):
|
class Worker(threading.Thread):
|
||||||
"""A worker for a single conversation. A new one is created every time the /start command is sent."""
|
"""A worker for a single conversation. A new one is created every time the /start command is sent."""
|
||||||
|
|
||||||
def __init__(self, bot: utils.DuckBot, chat: telegram.Chat, telegram_user: telegram.User, *args, **kwargs):
|
def __init__(self,
|
||||||
|
bot,
|
||||||
|
chat: telegram.Chat,
|
||||||
|
telegram_user: telegram.User,
|
||||||
|
cfg: nuconfig.NuConfig,
|
||||||
|
engine,
|
||||||
|
*args,
|
||||||
|
**kwargs):
|
||||||
# Initialize the thread
|
# Initialize the thread
|
||||||
super().__init__(name=f"Worker {chat.id}", *args, **kwargs)
|
super().__init__(name=f"Worker {chat.id}", *args, **kwargs)
|
||||||
# Store the bot and chat info inside the class
|
# Store the bot, chat info and config inside the class
|
||||||
self.bot: utils.DuckBot = bot
|
self.bot = bot
|
||||||
self.chat: telegram.Chat = chat
|
self.chat: telegram.Chat = chat
|
||||||
self.telegram_user: telegram.User = telegram_user
|
self.telegram_user: telegram.User = telegram_user
|
||||||
|
self.cfg = cfg
|
||||||
# Open a new database session
|
# Open a new database session
|
||||||
log.debug(f"Opening new database session for {self.name}")
|
log.debug(f"Opening new database session for {self.name}")
|
||||||
self.session = db.Session()
|
self.session = sqlalchemy.orm.sessionmaker(bind=engine)()
|
||||||
# Get the user db data from the users and admin tables
|
# Get the user db data from the users and admin tables
|
||||||
self.user: Optional[db.User] = None
|
self.user: Optional[db.User] = None
|
||||||
self.admin: Optional[db.Admin] = None
|
self.admin: Optional[db.Admin] = None
|
||||||
|
@ -50,23 +58,103 @@ class Worker(threading.Thread):
|
||||||
self.queue = queuem.Queue()
|
self.queue = queuem.Queue()
|
||||||
# The current active invoice payload; reject all invoices with a different payload
|
# The current active invoice payload; reject all invoices with a different payload
|
||||||
self.invoice_payload = None
|
self.invoice_payload = None
|
||||||
# The localization strings for this user
|
# The price class of this worker.
|
||||||
self.loc = None
|
self.Price = self.price_factory()
|
||||||
# The Sentry client for reporting errors encountered by the user
|
|
||||||
if configloader.config["Error Reporting"]["sentry_token"] != \
|
|
||||||
"https://00000000000000000000000000000000:00000000000000000000000000000000@sentry.io/0000000":
|
|
||||||
import raven
|
|
||||||
self.sentry_client = raven.Client(configloader.config["Error Reporting"]["sentry_token"],
|
|
||||||
release=raven.fetch_git_sha(os.path.dirname(__file__)),
|
|
||||||
environment="Dev" if __debug__ else "Prod")
|
|
||||||
log.debug("Sentry: enabled")
|
|
||||||
else:
|
|
||||||
self.sentry_client = None
|
|
||||||
log.debug("Sentry: disabled")
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__qualname__} {self.chat.id}>"
|
return f"<{self.__class__.__qualname__} {self.chat.id}>"
|
||||||
|
|
||||||
|
# noinspection PyMethodParameters
|
||||||
|
def price_factory(worker):
|
||||||
|
class Price:
|
||||||
|
"""The base class for the prices in greed.
|
||||||
|
Its int value is in minimum units, while its float and str values are in decimal format."""
|
||||||
|
|
||||||
|
def __init__(self, value: Union[int, float, str, "Price"]):
|
||||||
|
if isinstance(value, int):
|
||||||
|
# Keep the value as it is
|
||||||
|
self.value = int(value)
|
||||||
|
elif isinstance(value, float):
|
||||||
|
# Convert the value to minimum units
|
||||||
|
self.value = int(value * (10 ** worker.cfg["Payments"]["currency_exp"]))
|
||||||
|
elif isinstance(value, str):
|
||||||
|
# Remove decimal points, then cast to int
|
||||||
|
self.value = int(float(value.replace(",", ".")) * (10 ** worker.cfg["Payments"]["currency_exp"]))
|
||||||
|
elif isinstance(value, Price):
|
||||||
|
# Copy self
|
||||||
|
self.value = value.value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__qualname__} of value {self.value}>"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return worker.loc.get(
|
||||||
|
"currency_format_string",
|
||||||
|
symbol=worker.cfg["Payments"]["currency_symbol"],
|
||||||
|
value="{0:.2f}".format(self.value / (10 ** worker.cfg["Payments"]["currency_exp"]))
|
||||||
|
)
|
||||||
|
|
||||||
|
def __int__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def __float__(self):
|
||||||
|
return self.value / (10 ** worker.cfg["Payments"]["currency_exp"])
|
||||||
|
|
||||||
|
def __ge__(self, other):
|
||||||
|
return self.value >= Price(other).value
|
||||||
|
|
||||||
|
def __le__(self, other):
|
||||||
|
return self.value <= Price(other).value
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.value == Price(other).value
|
||||||
|
|
||||||
|
def __gt__(self, other):
|
||||||
|
return self.value > Price(other).value
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return self.value < Price(other).value
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
return Price(self.value + Price(other).value)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
return Price(self.value - Price(other).value)
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
return Price(int(self.value * other))
|
||||||
|
|
||||||
|
def __floordiv__(self, other):
|
||||||
|
return Price(int(self.value // other))
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self.__add__(other)
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
return Price(Price(other).value - self.value)
|
||||||
|
|
||||||
|
def __rmul__(self, other):
|
||||||
|
return self.__mul__(other)
|
||||||
|
|
||||||
|
def __iadd__(self, other):
|
||||||
|
self.value += Price(other).value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __isub__(self, other):
|
||||||
|
self.value -= Price(other).value
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __imul__(self, other):
|
||||||
|
self.value *= other
|
||||||
|
self.value = int(self.value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __ifloordiv__(self, other):
|
||||||
|
self.value //= other
|
||||||
|
return self
|
||||||
|
|
||||||
|
return Price
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""The conversation code."""
|
"""The conversation code."""
|
||||||
log.debug("Starting conversation")
|
log.debug("Starting conversation")
|
||||||
|
@ -78,7 +166,7 @@ class Worker(threading.Thread):
|
||||||
# Check if there are other registered users: if there aren't any, the first user will be owner of the bot
|
# Check if there are other registered users: if there aren't any, the first user will be owner of the bot
|
||||||
will_be_owner = (self.session.query(db.Admin).first() is None)
|
will_be_owner = (self.session.query(db.Admin).first() is None)
|
||||||
# Create the new record
|
# Create the new record
|
||||||
self.user = db.User(self.telegram_user)
|
self.user = db.User(w=self)
|
||||||
# Add the new record to the db
|
# Add the new record to the db
|
||||||
self.session.add(self.user)
|
self.session.add(self.user)
|
||||||
# Flush the session to get an userid
|
# Flush the session to get an userid
|
||||||
|
@ -106,7 +194,7 @@ class Worker(threading.Thread):
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
# Welcome the user to the bot
|
# Welcome the user to the bot
|
||||||
if configloader.config["Appearance"]["display_welcome_message"] == "yes":
|
if self.cfg["Appearance"]["display_welcome_message"] == "yes":
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("conversation_after_start"))
|
self.bot.send_message(self.chat.id, self.loc.get("conversation_after_start"))
|
||||||
# If the user is not an admin, send him to the user menu
|
# If the user is not an admin, send him to the user menu
|
||||||
if self.admin is None:
|
if self.admin is None:
|
||||||
|
@ -119,16 +207,14 @@ class Worker(threading.Thread):
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
# Open the admin menu
|
# Open the admin menu
|
||||||
self.__admin_menu()
|
self.__admin_menu()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
# Try to notify the user of the exception
|
# Try to notify the user of the exception
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("fatal_conversation_exception"))
|
self.bot.send_message(self.chat.id, self.loc.get("fatal_conversation_exception"))
|
||||||
except Exception:
|
except Exception as ne:
|
||||||
pass
|
log.error(f"Failed to notify the user of a conversation exception: {ne}")
|
||||||
# If the Sentry integration is enabled, log the exception
|
log.error(f"Exception in {self}: {e}")
|
||||||
if self.sentry_client is not None:
|
|
||||||
self.sentry_client.captureException()
|
|
||||||
traceback.print_exception(*sys.exc_info())
|
traceback.print_exception(*sys.exc_info())
|
||||||
|
|
||||||
def is_ready(self):
|
def is_ready(self):
|
||||||
|
@ -155,7 +241,7 @@ class Worker(threading.Thread):
|
||||||
If a stop signal is sent, try to gracefully stop the thread."""
|
If a stop signal is sent, try to gracefully stop the thread."""
|
||||||
# Pop data from the queue
|
# Pop data from the queue
|
||||||
try:
|
try:
|
||||||
data = self.queue.get(timeout=int(configloader.config["Telegram"]["conversation_timeout"]))
|
data = self.queue.get(timeout=self.cfg["Telegram"]["conversation_timeout"])
|
||||||
except queuem.Empty:
|
except queuem.Empty:
|
||||||
# If the conversation times out, gracefully stop the thread
|
# If the conversation times out, gracefully stop the thread
|
||||||
self.__graceful_stop(StopSignal("timeout"))
|
self.__graceful_stop(StopSignal("timeout"))
|
||||||
|
@ -365,7 +451,7 @@ class Worker(threading.Thread):
|
||||||
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
||||||
self.bot.send_message(self.chat.id,
|
self.bot.send_message(self.chat.id,
|
||||||
self.loc.get("conversation_open_user_menu",
|
self.loc.get("conversation_open_user_menu",
|
||||||
credit=utils.Price(self.user.credit, self.loc)),
|
credit=self.Price(self.user.credit)),
|
||||||
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
||||||
# Wait for a reply from the user
|
# Wait for a reply from the user
|
||||||
selection = self.__wait_for_specific_message([
|
selection = self.__wait_for_specific_message([
|
||||||
|
@ -417,7 +503,7 @@ class Worker(threading.Thread):
|
||||||
if product.price is None:
|
if product.price is None:
|
||||||
continue
|
continue
|
||||||
# Send the message without the keyboard to get the message id
|
# Send the message without the keyboard to get the message id
|
||||||
message = product.send_as_message(loc=self.loc, chat_id=self.chat.id)
|
message = product.send_as_message(w=self, chat_id=self.chat.id)
|
||||||
# Add the product to the cart
|
# Add the product to the cart
|
||||||
cart[message['result']['message_id']] = [product, 0]
|
cart[message['result']['message_id']] = [product, 0]
|
||||||
# Create the inline keyboard to add the product to the cart
|
# Create the inline keyboard to add the product to the cart
|
||||||
|
@ -428,12 +514,12 @@ class Worker(threading.Thread):
|
||||||
if product.image is None:
|
if product.image is None:
|
||||||
self.bot.edit_message_text(chat_id=self.chat.id,
|
self.bot.edit_message_text(chat_id=self.chat.id,
|
||||||
message_id=message['result']['message_id'],
|
message_id=message['result']['message_id'],
|
||||||
text=product.text(loc=self.loc),
|
text=product.text(w=self),
|
||||||
reply_markup=inline_keyboard)
|
reply_markup=inline_keyboard)
|
||||||
else:
|
else:
|
||||||
self.bot.edit_message_caption(chat_id=self.chat.id,
|
self.bot.edit_message_caption(chat_id=self.chat.id,
|
||||||
message_id=message['result']['message_id'],
|
message_id=message['result']['message_id'],
|
||||||
caption=product.text(loc=self.loc),
|
caption=product.text(w=self),
|
||||||
reply_markup=inline_keyboard)
|
reply_markup=inline_keyboard)
|
||||||
# Create the keyboard with the cancel button
|
# Create the keyboard with the cancel button
|
||||||
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"),
|
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"),
|
||||||
|
@ -477,13 +563,13 @@ class Worker(threading.Thread):
|
||||||
if product.image is None:
|
if product.image is None:
|
||||||
self.bot.edit_message_text(chat_id=self.chat.id,
|
self.bot.edit_message_text(chat_id=self.chat.id,
|
||||||
message_id=callback.message.message_id,
|
message_id=callback.message.message_id,
|
||||||
text=product.text(loc=self.loc,
|
text=product.text(w=self,
|
||||||
cart_qty=cart[callback.message.message_id][1]),
|
cart_qty=cart[callback.message.message_id][1]),
|
||||||
reply_markup=product_inline_keyboard)
|
reply_markup=product_inline_keyboard)
|
||||||
else:
|
else:
|
||||||
self.bot.edit_message_caption(chat_id=self.chat.id,
|
self.bot.edit_message_caption(chat_id=self.chat.id,
|
||||||
message_id=callback.message.message_id,
|
message_id=callback.message.message_id,
|
||||||
caption=product.text(loc=self.loc,
|
caption=product.text(w=self,
|
||||||
cart_qty=cart[callback.message.message_id][1]),
|
cart_qty=cart[callback.message.message_id][1]),
|
||||||
reply_markup=product_inline_keyboard)
|
reply_markup=product_inline_keyboard)
|
||||||
|
|
||||||
|
@ -525,13 +611,13 @@ class Worker(threading.Thread):
|
||||||
# Edit the product message
|
# Edit the product message
|
||||||
if product.image is None:
|
if product.image is None:
|
||||||
self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id,
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=callback.message.message_id,
|
||||||
text=product.text(loc=self.loc,
|
text=product.text(w=self,
|
||||||
cart_qty=cart[callback.message.message_id][1]),
|
cart_qty=cart[callback.message.message_id][1]),
|
||||||
reply_markup=product_inline_keyboard)
|
reply_markup=product_inline_keyboard)
|
||||||
else:
|
else:
|
||||||
self.bot.edit_message_caption(chat_id=self.chat.id,
|
self.bot.edit_message_caption(chat_id=self.chat.id,
|
||||||
message_id=callback.message.message_id,
|
message_id=callback.message.message_id,
|
||||||
caption=product.text(loc=self.loc,
|
caption=product.text(w=self,
|
||||||
cart_qty=cart[callback.message.message_id][1]),
|
cart_qty=cart[callback.message.message_id][1]),
|
||||||
reply_markup=product_inline_keyboard)
|
reply_markup=product_inline_keyboard)
|
||||||
|
|
||||||
|
@ -573,12 +659,12 @@ class Worker(threading.Thread):
|
||||||
if credit_required > 0:
|
if credit_required > 0:
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("error_not_enough_credit"))
|
self.bot.send_message(self.chat.id, self.loc.get("error_not_enough_credit"))
|
||||||
# Suggest payment for missing credit value if configuration allows refill
|
# Suggest payment for missing credit value if configuration allows refill
|
||||||
if configloader.config["Credit Card"]["credit_card_token"] != "" \
|
if self.cfg["Payments"]["CreditCard"]["credit_card_token"] != "" \
|
||||||
and configloader.config["Appearance"]["refill_on_checkout"] == 'yes' \
|
and self.cfg["Appearance"]["refill_on_checkout"] == 'yes' \
|
||||||
and utils.Price(int(configloader.config["Credit Card"]["min_amount"]), self.loc) <= \
|
and self.Price(self.cfg["Payments"]["CreditCard"]["min_amount"]) <= \
|
||||||
credit_required <= \
|
credit_required <= \
|
||||||
utils.Price(int(configloader.config["Credit Card"]["max_amount"]), self.loc):
|
self.Price(self.cfg["Payments"]["CreditCard"]["max_amount"]):
|
||||||
self.__make_payment(utils.Price(credit_required, self.loc))
|
self.__make_payment(self.Price(credit_required))
|
||||||
# If afer requested payment credit is still insufficient (either payment failure or cancel)
|
# If afer requested payment credit is still insufficient (either payment failure or cancel)
|
||||||
if self.user.credit < self.__get_cart_value(cart):
|
if self.user.credit < self.__get_cart_value(cart):
|
||||||
# Rollback all the changes
|
# Rollback all the changes
|
||||||
|
@ -589,7 +675,7 @@ class Worker(threading.Thread):
|
||||||
|
|
||||||
def __get_cart_value(self, cart):
|
def __get_cart_value(self, cart):
|
||||||
# Calculate total items value in cart
|
# Calculate total items value in cart
|
||||||
value = utils.Price(0, self.loc)
|
value = self.Price(0)
|
||||||
for product in cart:
|
for product in cart:
|
||||||
value += cart[product][0].price * cart[product][1]
|
value += cart[product][0].price * cart[product][1]
|
||||||
return value
|
return value
|
||||||
|
@ -599,7 +685,7 @@ class Worker(threading.Thread):
|
||||||
product_list = ""
|
product_list = ""
|
||||||
for product_id in cart:
|
for product_id in cart:
|
||||||
if cart[product_id][1] > 0:
|
if cart[product_id][1] > 0:
|
||||||
product_list += cart[product_id][0].text(loc=self.loc,
|
product_list += cart[product_id][0].text(w=self,
|
||||||
style="short",
|
style="short",
|
||||||
cart_qty=cart[product_id][1]) + "\n"
|
cart_qty=cart[product_id][1]) + "\n"
|
||||||
return product_list
|
return product_list
|
||||||
|
@ -621,7 +707,7 @@ class Worker(threading.Thread):
|
||||||
|
|
||||||
def __order_notify_admins(self, order):
|
def __order_notify_admins(self, order):
|
||||||
# Notify the user of the order result
|
# Notify the user of the order result
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("success_order_created", order=order.text(loc=self.loc,
|
self.bot.send_message(self.chat.id, self.loc.get("success_order_created", order=order.text(w=self,
|
||||||
session=self.session,
|
session=self.session,
|
||||||
user=True)))
|
user=True)))
|
||||||
# Notify the admins (in Live Orders mode) of the new order
|
# Notify the admins (in Live Orders mode) of the new order
|
||||||
|
@ -636,7 +722,7 @@ class Worker(threading.Thread):
|
||||||
for admin in admins:
|
for admin in admins:
|
||||||
self.bot.send_message(admin.user_id,
|
self.bot.send_message(admin.user_id,
|
||||||
self.loc.get('notification_order_placed',
|
self.loc.get('notification_order_placed',
|
||||||
order=order.text(loc=self.loc, session=self.session)),
|
order=order.text(w=self, session=self.session)),
|
||||||
reply_markup=order_keyboard)
|
reply_markup=order_keyboard)
|
||||||
|
|
||||||
def __order_status(self):
|
def __order_status(self):
|
||||||
|
@ -653,7 +739,7 @@ class Worker(threading.Thread):
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("error_no_orders"))
|
self.bot.send_message(self.chat.id, self.loc.get("error_no_orders"))
|
||||||
# Display the order status to the user
|
# Display the order status to the user
|
||||||
for order in orders:
|
for order in orders:
|
||||||
self.bot.send_message(self.chat.id, order.text(loc=self.loc, session=self.session, user=True))
|
self.bot.send_message(self.chat.id, order.text(w=self, session=self.session, user=True))
|
||||||
# TODO: maybe add a page displayer instead of showing the latest 5 orders
|
# TODO: maybe add a page displayer instead of showing the latest 5 orders
|
||||||
|
|
||||||
def __add_credit_menu(self):
|
def __add_credit_menu(self):
|
||||||
|
@ -665,7 +751,7 @@ class Worker(threading.Thread):
|
||||||
# Cash
|
# Cash
|
||||||
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cash"))])
|
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cash"))])
|
||||||
# Telegram Payments
|
# Telegram Payments
|
||||||
if configloader.config["Credit Card"]["credit_card_token"] != "":
|
if self.cfg["Payments"]["CreditCard"]["credit_card_token"] != "":
|
||||||
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_credit_card"))])
|
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_credit_card"))])
|
||||||
# Keyboard: go back to the previous menu
|
# Keyboard: go back to the previous menu
|
||||||
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cancel"))])
|
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cancel"))])
|
||||||
|
@ -693,8 +779,8 @@ class Worker(threading.Thread):
|
||||||
"""Add money to the wallet through a credit card payment."""
|
"""Add money to the wallet through a credit card payment."""
|
||||||
log.debug("Displaying __add_credit_cc")
|
log.debug("Displaying __add_credit_cc")
|
||||||
# Create a keyboard to be sent later
|
# Create a keyboard to be sent later
|
||||||
presets = list(map(lambda s: s.strip(" "), configloader.config["Credit Card"]["payment_presets"].split('|')))
|
presets = list(map(lambda s: s.strip(" "), self.cfg["Payments"]["CreditCard"]["payment_presets"].split('|')))
|
||||||
keyboard = [[telegram.KeyboardButton(str(utils.Price(preset, self.loc)))] for preset in presets]
|
keyboard = [[telegram.KeyboardButton(str(self.Price(preset)))] for preset in presets]
|
||||||
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cancel"))])
|
keyboard.append([telegram.KeyboardButton(self.loc.get("menu_cancel"))])
|
||||||
# Boolean variable to check if the user has cancelled the action
|
# Boolean variable to check if the user has cancelled the action
|
||||||
cancelled = False
|
cancelled = False
|
||||||
|
@ -711,15 +797,15 @@ class Worker(threading.Thread):
|
||||||
cancelled = True
|
cancelled = True
|
||||||
continue
|
continue
|
||||||
# Convert the amount to an integer
|
# Convert the amount to an integer
|
||||||
value = utils.Price(selection, self.loc)
|
value = self.Price(selection)
|
||||||
# Ensure the amount is within the range
|
# Ensure the amount is within the range
|
||||||
if value > utils.Price(int(configloader.config["Credit Card"]["max_amount"]), self.loc):
|
if value > self.Price(self.cfg["Payments"]["CreditCard"]["max_amount"]):
|
||||||
self.bot.send_message(self.chat.id,
|
self.bot.send_message(self.chat.id,
|
||||||
self.loc.get("error_payment_amount_over_max", max_amount=utils.Price(configloader.config["Credit Card"]["max_amount"], self.loc)))
|
self.loc.get("error_payment_amount_over_max", max_amount=self.Price(self.cfg["Credit Card"]["max_amount"])))
|
||||||
continue
|
continue
|
||||||
elif value < utils.Price(int(configloader.config["Credit Card"]["min_amount"]), self.loc):
|
elif value < self.Price(self.cfg["Payments"]["CreditCard"]["min_amount"]):
|
||||||
self.bot.send_message(self.chat.id,
|
self.bot.send_message(self.chat.id,
|
||||||
self.loc.get("error_payment_amount_under_min", min_amount=utils.Price(configloader.config["Credit Card"]["min_amount"], self.loc)))
|
self.loc.get("error_payment_amount_under_min", min_amount=self.Price(self.cfg["Credit Card"]["min_amount"])))
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
# If the user cancelled the action...
|
# If the user cancelled the action...
|
||||||
|
@ -740,7 +826,8 @@ class Worker(threading.Thread):
|
||||||
prices.append(telegram.LabeledPrice(label=self.loc.get("payment_invoice_fee_label"),
|
prices.append(telegram.LabeledPrice(label=self.loc.get("payment_invoice_fee_label"),
|
||||||
amount=fee))
|
amount=fee))
|
||||||
# Create the invoice keyboard
|
# Create the invoice keyboard
|
||||||
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_pay"), pay=True)],
|
inline_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(self.loc.get("menu_pay"),
|
||||||
|
pay=True)],
|
||||||
[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"),
|
[telegram.InlineKeyboardButton(self.loc.get("menu_cancel"),
|
||||||
callback_data="cmd_cancel")]])
|
callback_data="cmd_cancel")]])
|
||||||
# The amount is valid, send the invoice
|
# The amount is valid, send the invoice
|
||||||
|
@ -748,13 +835,13 @@ class Worker(threading.Thread):
|
||||||
title=self.loc.get("payment_invoice_title"),
|
title=self.loc.get("payment_invoice_title"),
|
||||||
description=self.loc.get("payment_invoice_description", amount=str(amount)),
|
description=self.loc.get("payment_invoice_description", amount=str(amount)),
|
||||||
payload=self.invoice_payload,
|
payload=self.invoice_payload,
|
||||||
provider_token=configloader.config["Credit Card"]["credit_card_token"],
|
provider_token=self.cfg["Payments"]["CreditCard"]["credit_card_token"],
|
||||||
start_parameter="tempdeeplink",
|
start_parameter="tempdeeplink",
|
||||||
currency=configloader.config["Payments"]["currency"],
|
currency=self.cfg["Payments"]["currency"],
|
||||||
prices=prices,
|
prices=prices,
|
||||||
need_name=configloader.config["Credit Card"]["name_required"] == "yes",
|
need_name=self.cfg["Payments"]["CreditCard"]["name_required"],
|
||||||
need_email=configloader.config["Credit Card"]["email_required"] == "yes",
|
need_email=self.cfg["Payments"]["CreditCard"]["email_required"],
|
||||||
need_phone_number=configloader.config["Credit Card"]["phone_required"] == "yes",
|
need_phone_number=self.cfg["Payments"]["CreditCard"]["phone_required"],
|
||||||
reply_markup=inline_keyboard)
|
reply_markup=inline_keyboard)
|
||||||
# Wait for the precheckout query
|
# Wait for the precheckout query
|
||||||
precheckoutquery = self.__wait_for_precheckoutquery(cancellable=True)
|
precheckoutquery = self.__wait_for_precheckoutquery(cancellable=True)
|
||||||
|
@ -782,11 +869,10 @@ class Worker(threading.Thread):
|
||||||
# Commit all the changes
|
# Commit all the changes
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
@staticmethod
|
def __get_total_fee(self, amount):
|
||||||
def __get_total_fee(amount):
|
|
||||||
# Calculate a fee for the required amount
|
# Calculate a fee for the required amount
|
||||||
fee_percentage = float(configloader.config["Credit Card"]["fee_percentage"]) / 100
|
fee_percentage = self.cfg["Payments"]["CreditCard"]["fee_percentage"] / 100
|
||||||
fee_fixed = int(configloader.config["Credit Card"]["fee_fixed"])
|
fee_fixed = self.cfg["Payments"]["CreditCard"]["fee_fixed"]
|
||||||
total_fee = amount * fee_percentage + fee_fixed
|
total_fee = amount * fee_percentage + fee_fixed
|
||||||
if total_fee > 0:
|
if total_fee > 0:
|
||||||
return total_fee
|
return total_fee
|
||||||
|
@ -818,8 +904,7 @@ class Worker(threading.Thread):
|
||||||
keyboard.append([self.loc.get("menu_user_mode")])
|
keyboard.append([self.loc.get("menu_user_mode")])
|
||||||
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
# Send the previously created keyboard to the user (ensuring it can be clicked only 1 time)
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("conversation_open_admin_menu"),
|
self.bot.send_message(self.chat.id, self.loc.get("conversation_open_admin_menu"),
|
||||||
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True),
|
reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True))
|
||||||
)
|
|
||||||
# Wait for a reply from the user
|
# Wait for a reply from the user
|
||||||
selection = self.__wait_for_specific_message([self.loc.get("menu_products"),
|
selection = self.__wait_for_specific_message([self.loc.get("menu_products"),
|
||||||
self.loc.get("menu_orders"),
|
self.loc.get("menu_orders"),
|
||||||
|
@ -934,7 +1019,7 @@ class Worker(threading.Thread):
|
||||||
if product:
|
if product:
|
||||||
self.bot.send_message(self.chat.id,
|
self.bot.send_message(self.chat.id,
|
||||||
self.loc.get("edit_current_value",
|
self.loc.get("edit_current_value",
|
||||||
value=(str(utils.Price(product.price, self.loc))
|
value=(str(self.Price(product.price))
|
||||||
if product.price is not None else 'Non in vendita')),
|
if product.price is not None else 'Non in vendita')),
|
||||||
reply_markup=cancel)
|
reply_markup=cancel)
|
||||||
# Wait for an answer
|
# Wait for an answer
|
||||||
|
@ -946,7 +1031,7 @@ class Worker(threading.Thread):
|
||||||
elif price.lower() == "x":
|
elif price.lower() == "x":
|
||||||
price = None
|
price = None
|
||||||
else:
|
else:
|
||||||
price = utils.Price(price, self.loc)
|
price = self.Price(price)
|
||||||
# Ask for the product image
|
# Ask for the product image
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("ask_product_image"), reply_markup=cancel)
|
self.bot.send_message(self.chat.id, self.loc.get("ask_product_image"), reply_markup=cancel)
|
||||||
# Wait for an answer
|
# Wait for an answer
|
||||||
|
@ -1044,7 +1129,7 @@ class Worker(threading.Thread):
|
||||||
# Create a message for every one of them
|
# Create a message for every one of them
|
||||||
for order in orders:
|
for order in orders:
|
||||||
# Send the created message
|
# Send the created message
|
||||||
self.bot.send_message(self.chat.id, order.text(loc=self.loc, session=self.session),
|
self.bot.send_message(self.chat.id, order.text(w=self, session=self.session),
|
||||||
reply_markup=order_keyboard)
|
reply_markup=order_keyboard)
|
||||||
# Set the Live mode flag to True
|
# Set the Live mode flag to True
|
||||||
self.admin.live_mode = True
|
self.admin.live_mode = True
|
||||||
|
@ -1073,11 +1158,11 @@ class Worker(threading.Thread):
|
||||||
# Commit the transaction
|
# Commit the transaction
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
# Update order message
|
# Update order message
|
||||||
self.bot.edit_message_text(order.text(loc=self.loc, session=self.session), chat_id=self.chat.id,
|
self.bot.edit_message_text(order.text(w=self, session=self.session), chat_id=self.chat.id,
|
||||||
message_id=update.message.message_id)
|
message_id=update.message.message_id)
|
||||||
# Notify the user of the completition
|
# Notify the user of the completition
|
||||||
self.bot.send_message(order.user_id,
|
self.bot.send_message(order.user_id,
|
||||||
self.loc.get("notification_order_completed", order=order.text(loc=self.loc, session=self.session, user=True)))
|
self.loc.get("notification_order_completed", order=order.text(w=self, session=self.session, user=True)))
|
||||||
# If the user pressed the refund order button, refund the order...
|
# If the user pressed the refund order button, refund the order...
|
||||||
elif update.data == "order_refund":
|
elif update.data == "order_refund":
|
||||||
# Ask for a refund reason
|
# Ask for a refund reason
|
||||||
|
@ -1101,12 +1186,12 @@ class Worker(threading.Thread):
|
||||||
# Commit the changes
|
# Commit the changes
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
# Update the order message
|
# Update the order message
|
||||||
self.bot.edit_message_text(order.text(loc=self.loc, session=self.session),
|
self.bot.edit_message_text(order.text(w=self, session=self.session),
|
||||||
chat_id=self.chat.id,
|
chat_id=self.chat.id,
|
||||||
message_id=update.message.message_id)
|
message_id=update.message.message_id)
|
||||||
# Notify the user of the refund
|
# Notify the user of the refund
|
||||||
self.bot.send_message(order.user_id,
|
self.bot.send_message(order.user_id,
|
||||||
self.loc.get("notification_order_refunded", order=order.text(loc=self.loc,
|
self.loc.get("notification_order_refunded", order=order.text(w=self,
|
||||||
session=self.session,
|
session=self.session,
|
||||||
user=True)))
|
user=True)))
|
||||||
# Notify the admin of the refund
|
# Notify the admin of the refund
|
||||||
|
@ -1131,7 +1216,7 @@ class Worker(threading.Thread):
|
||||||
if isinstance(reply, CancelSignal):
|
if isinstance(reply, CancelSignal):
|
||||||
return
|
return
|
||||||
# Convert the reply to a price object
|
# Convert the reply to a price object
|
||||||
price = utils.Price(reply, self.loc)
|
price = self.Price(reply)
|
||||||
# Ask the user for notes
|
# Ask the user for notes
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("ask_transaction_notes"), reply_markup=cancel)
|
self.bot.send_message(self.chat.id, self.loc.get("ask_transaction_notes"), reply_markup=cancel)
|
||||||
# Wait for an answer
|
# Wait for an answer
|
||||||
|
@ -1152,10 +1237,10 @@ class Worker(threading.Thread):
|
||||||
# Notify the user of the credit/debit
|
# Notify the user of the credit/debit
|
||||||
self.bot.send_message(user.user_id,
|
self.bot.send_message(user.user_id,
|
||||||
self.loc.get("notification_transaction_created",
|
self.loc.get("notification_transaction_created",
|
||||||
transaction=transaction.text(loc=self.loc)))
|
transaction=transaction.text(w=self)))
|
||||||
# Notify the admin of the success
|
# Notify the admin of the success
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("success_transaction_created",
|
self.bot.send_message(self.chat.id, self.loc.get("success_transaction_created",
|
||||||
transaction=transaction.text(loc=self.loc)))
|
transaction=transaction.text(w=self)))
|
||||||
|
|
||||||
def __help_menu(self):
|
def __help_menu(self):
|
||||||
"""Help menu. Allows the user to ask for assistance, get a guide or see some info about the bot."""
|
"""Help menu. Allows the user to ask for assistance, get a guide or see some info about the bot."""
|
||||||
|
@ -1221,7 +1306,7 @@ class Worker(threading.Thread):
|
||||||
# Create the inline keyboard markup
|
# Create the inline keyboard markup
|
||||||
inline_keyboard = telegram.InlineKeyboardMarkup(inline_keyboard_list)
|
inline_keyboard = telegram.InlineKeyboardMarkup(inline_keyboard_list)
|
||||||
# Create the message text
|
# Create the message text
|
||||||
transactions_string = "\n".join([transaction.text(loc=self.loc) for transaction in transactions])
|
transactions_string = "\n".join([transaction.text(w=self) for transaction in transactions])
|
||||||
text = self.loc.get("transactions_page", page=page + 1, transactions=transactions_string)
|
text = self.loc.get("transactions_page", page=page + 1, transactions=transactions_string)
|
||||||
# Update the previously sent message
|
# Update the previously sent message
|
||||||
self.bot.edit_message_text(chat_id=self.chat.id, message_id=message.message_id, text=text,
|
self.bot.edit_message_text(chat_id=self.chat.id, message_id=message.message_id, text=text,
|
||||||
|
@ -1280,7 +1365,7 @@ class Worker(threading.Thread):
|
||||||
# Reopen the file for reading
|
# Reopen the file for reading
|
||||||
with open(f"transactions_{self.chat.id}.csv") as file:
|
with open(f"transactions_{self.chat.id}.csv") as file:
|
||||||
# Send the file via a manual request to Telegram
|
# Send the file via a manual request to Telegram
|
||||||
requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendDocument",
|
requests.post(f"https://api.telegram.org/bot{self.cfg['Telegram']['token']}/sendDocument",
|
||||||
files={"document": file},
|
files={"document": file},
|
||||||
params={"chat_id": self.chat.id,
|
params={"chat_id": self.chat.id,
|
||||||
"parse_mode": "HTML"})
|
"parse_mode": "HTML"})
|
||||||
|
@ -1299,9 +1384,11 @@ class Worker(threading.Thread):
|
||||||
admin = self.session.query(db.Admin).filter_by(user_id=user.user_id).one_or_none()
|
admin = self.session.query(db.Admin).filter_by(user_id=user.user_id).one_or_none()
|
||||||
if admin is None:
|
if admin is None:
|
||||||
# Create the keyboard to be sent
|
# Create the keyboard to be sent
|
||||||
keyboard = telegram.ReplyKeyboardMarkup([[self.loc.get("emoji_yes"), self.loc.get("emoji_no")]], one_time_keyboard=True)
|
keyboard = telegram.ReplyKeyboardMarkup([[self.loc.get("emoji_yes"), self.loc.get("emoji_no")]],
|
||||||
|
one_time_keyboard=True)
|
||||||
# Ask for confirmation
|
# Ask for confirmation
|
||||||
self.bot.send_message(self.chat.id, self.loc.get("conversation_confirm_admin_promotion"), reply_markup=keyboard)
|
self.bot.send_message(self.chat.id, self.loc.get("conversation_confirm_admin_promotion"),
|
||||||
|
reply_markup=keyboard)
|
||||||
# Wait for an answer
|
# Wait for an answer
|
||||||
selection = self.__wait_for_specific_message([self.loc.get("emoji_yes"), self.loc.get("emoji_no")])
|
selection = self.__wait_for_specific_message([self.loc.get("emoji_yes"), self.loc.get("emoji_no")])
|
||||||
# Proceed only if the answer is yes
|
# Proceed only if the answer is yes
|
||||||
|
@ -1321,17 +1408,26 @@ class Worker(threading.Thread):
|
||||||
while True:
|
while True:
|
||||||
# Create the inline keyboard with the admin status
|
# Create the inline keyboard with the admin status
|
||||||
inline_keyboard = telegram.InlineKeyboardMarkup([
|
inline_keyboard = telegram.InlineKeyboardMarkup([
|
||||||
[telegram.InlineKeyboardButton(f"{self.loc.boolmoji(admin.edit_products)} {self.loc.get('prop_edit_products')}",
|
[telegram.InlineKeyboardButton(
|
||||||
callback_data="toggle_edit_products")],
|
f"{self.loc.boolmoji(admin.edit_products)} {self.loc.get('prop_edit_products')}",
|
||||||
[telegram.InlineKeyboardButton(f"{self.loc.boolmoji(admin.receive_orders)} {self.loc.get('prop_receive_orders')}",
|
callback_data="toggle_edit_products"
|
||||||
callback_data="toggle_receive_orders")],
|
)],
|
||||||
|
[telegram.InlineKeyboardButton(
|
||||||
|
f"{self.loc.boolmoji(admin.receive_orders)} {self.loc.get('prop_receive_orders')}",
|
||||||
|
callback_data="toggle_receive_orders"
|
||||||
|
)],
|
||||||
[telegram.InlineKeyboardButton(
|
[telegram.InlineKeyboardButton(
|
||||||
f"{self.loc.boolmoji(admin.create_transactions)} {self.loc.get('prop_create_transactions')}",
|
f"{self.loc.boolmoji(admin.create_transactions)} {self.loc.get('prop_create_transactions')}",
|
||||||
callback_data="toggle_create_transactions")],
|
callback_data="toggle_create_transactions"
|
||||||
|
)],
|
||||||
[telegram.InlineKeyboardButton(
|
[telegram.InlineKeyboardButton(
|
||||||
f"{self.loc.boolmoji(admin.display_on_help)} {self.loc.get('prop_display_on_help')}",
|
f"{self.loc.boolmoji(admin.display_on_help)} {self.loc.get('prop_display_on_help')}",
|
||||||
callback_data="toggle_display_on_help")],
|
callback_data="toggle_display_on_help"
|
||||||
[telegram.InlineKeyboardButton(self.loc.get('menu_done'), callback_data="cmd_done")]
|
)],
|
||||||
|
[telegram.InlineKeyboardButton(
|
||||||
|
self.loc.get('menu_done'),
|
||||||
|
callback_data="cmd_done"
|
||||||
|
)]
|
||||||
])
|
])
|
||||||
# Update the inline keyboard
|
# Update the inline keyboard
|
||||||
self.bot.edit_message_reply_markup(message_id=message.message_id,
|
self.bot.edit_message_reply_markup(message_id=message.message_id,
|
||||||
|
@ -1358,27 +1454,27 @@ class Worker(threading.Thread):
|
||||||
keyboard = []
|
keyboard = []
|
||||||
options: Dict[str, str] = {}
|
options: Dict[str, str] = {}
|
||||||
# https://en.wikipedia.org/wiki/List_of_language_names
|
# https://en.wikipedia.org/wiki/List_of_language_names
|
||||||
if "it" in configloader.config["Language"]["enabled_languages"]:
|
if "it" in self.cfg["Language"]["enabled_languages"]:
|
||||||
lang = "🇮🇹 Italiano"
|
lang = "🇮🇹 Italiano"
|
||||||
keyboard.append([telegram.KeyboardButton(lang)])
|
keyboard.append([telegram.KeyboardButton(lang)])
|
||||||
options[lang] = "it"
|
options[lang] = "it"
|
||||||
if "en" in configloader.config["Language"]["enabled_languages"]:
|
if "en" in self.cfg["Language"]["enabled_languages"]:
|
||||||
lang = "🇬🇧 English"
|
lang = "🇬🇧 English"
|
||||||
keyboard.append([telegram.KeyboardButton(lang)])
|
keyboard.append([telegram.KeyboardButton(lang)])
|
||||||
options[lang] = "en"
|
options[lang] = "en"
|
||||||
if "ru" in configloader.config["Language"]["enabled_languages"]:
|
if "ru" in self.cfg["Language"]["enabled_languages"]:
|
||||||
lang = "🇷🇺 Русский"
|
lang = "🇷🇺 Русский"
|
||||||
keyboard.append([telegram.KeyboardButton(lang)])
|
keyboard.append([telegram.KeyboardButton(lang)])
|
||||||
options[lang] = "ru"
|
options[lang] = "ru"
|
||||||
if "uk" in configloader.config["Language"]["enabled_languages"]:
|
if "uk" in self.cfg["Language"]["enabled_languages"]:
|
||||||
lang = "🇺🇦 Українська"
|
lang = "🇺🇦 Українська"
|
||||||
keyboard.append([telegram.KeyboardButton(lang)])
|
keyboard.append([telegram.KeyboardButton(lang)])
|
||||||
options[lang] = "uk"
|
options[lang] = "uk"
|
||||||
if "zh_cn" in configloader.config["Language"]["enabled_languages"]:
|
if "zh_cn" in self.cfg["Language"]["enabled_languages"]:
|
||||||
lang = "🇨🇳 简体中文"
|
lang = "🇨🇳 简体中文"
|
||||||
keyboard.append([telegram.KeyboardButton(lang)])
|
keyboard.append([telegram.KeyboardButton(lang)])
|
||||||
options[lang] = "zh_cn"
|
options[lang] = "zh_cn"
|
||||||
if "he" in configloader.config["Language"]["enabled_languages"]:
|
if "he" in self.cfg["Language"]["enabled_languages"]:
|
||||||
lang = "🇮🇱 עברית"
|
lang = "🇮🇱 עברית"
|
||||||
keyboard.append([telegram.KeyboardButton(lang)])
|
keyboard.append([telegram.KeyboardButton(lang)])
|
||||||
options[lang] = "he"
|
options[lang] = "he"
|
||||||
|
@ -1397,14 +1493,14 @@ class Worker(threading.Thread):
|
||||||
|
|
||||||
def __create_localization(self):
|
def __create_localization(self):
|
||||||
# Check if the user's language is enabled; if it isn't, change it to the default
|
# Check if the user's language is enabled; if it isn't, change it to the default
|
||||||
if self.user.language not in configloader.config["Language"]["enabled_languages"]:
|
if self.user.language not in self.cfg["Language"]["enabled_languages"]:
|
||||||
log.debug(f"User's language '{self.user.language}' is not enabled, changing it to the default")
|
log.debug(f"User's language '{self.user.language}' is not enabled, changing it to the default")
|
||||||
self.user.language = configloader.config["Language"]["default_language"]
|
self.user.language = self.cfg["Language"]["default_language"]
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
# Create a new Localization object
|
# Create a new Localization object
|
||||||
self.loc = localization.Localization(
|
self.loc = localization.Localization(
|
||||||
language=self.user.language,
|
language=self.user.language,
|
||||||
fallback=configloader.config["Language"]["fallback_language"],
|
fallback=self.cfg["Language"]["fallback_language"],
|
||||||
replacements={
|
replacements={
|
||||||
"user_string": str(self.user),
|
"user_string": str(self.user),
|
||||||
"user_mention": self.user.mention(),
|
"user_mention": self.user.mention(),
|
||||||
|
|
Loading…
Reference in a new issue