2018-02-05 11:49:21 +00:00
|
|
|
from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint
|
2018-02-05 12:32:18 +00:00
|
|
|
from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary, DateTime, Boolean
|
2018-01-10 10:29:02 +00:00
|
|
|
from sqlalchemy.orm import sessionmaker, relationship
|
2017-12-18 09:46:48 +00:00
|
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
|
|
import configloader
|
|
|
|
import telegram
|
|
|
|
import strings
|
2018-02-01 09:06:40 +00:00
|
|
|
import requests
|
2017-12-18 09:46:48 +00:00
|
|
|
from html import escape
|
2018-02-28 19:07:40 +00:00
|
|
|
from utils import Price
|
2017-12-11 09:43:37 +00:00
|
|
|
|
2017-12-18 09:46:48 +00:00
|
|
|
# Create a (lazy) database engine
|
|
|
|
engine = create_engine(configloader.config["Database"]["engine"])
|
|
|
|
|
|
|
|
# Create a base class to define all the database subclasses
|
|
|
|
TableDeclarativeBase = declarative_base(bind=engine)
|
|
|
|
|
2017-12-20 11:06:12 +00:00
|
|
|
# Create a Session class able to initialize database sessions
|
|
|
|
Session = sessionmaker()
|
|
|
|
|
2017-12-18 09:46:48 +00:00
|
|
|
# Define all the database tables using the sqlalchemy declarative base
|
|
|
|
class User(TableDeclarativeBase):
|
|
|
|
"""A Telegram user who used the bot at least once."""
|
|
|
|
|
|
|
|
# Telegram data
|
|
|
|
user_id = Column(BigInteger, primary_key=True)
|
|
|
|
first_name = Column(String, nullable=False)
|
|
|
|
last_name = Column(String)
|
|
|
|
username = Column(String)
|
|
|
|
|
|
|
|
# Current wallet credit
|
2018-01-10 10:29:02 +00:00
|
|
|
credit = Column(Integer, nullable=False)
|
2017-12-18 09:46:48 +00:00
|
|
|
|
|
|
|
# Extra table parameters
|
|
|
|
__tablename__ = "users"
|
|
|
|
|
2017-12-21 09:42:23 +00:00
|
|
|
def __init__(self, telegram_chat: telegram.Chat, **kwargs):
|
2017-12-20 10:18:06 +00:00
|
|
|
# Initialize the super
|
|
|
|
super().__init__(**kwargs)
|
2017-12-18 09:46:48 +00:00
|
|
|
# Get the data from telegram
|
2017-12-21 09:42:23 +00:00
|
|
|
self.user_id = telegram_chat.id
|
|
|
|
self.first_name = telegram_chat.first_name
|
|
|
|
self.last_name = telegram_chat.last_name
|
|
|
|
self.username = telegram_chat.username
|
2017-12-18 09:46:48 +00:00
|
|
|
# The starting wallet value is 0
|
2018-01-15 09:16:04 +00:00
|
|
|
self.credit = 0
|
2017-12-18 09:46:48 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
"""Describe the user in the best way possible given the available data."""
|
|
|
|
if self.username is not None:
|
|
|
|
return f"@{self.username}"
|
|
|
|
elif self.last_name is not None:
|
|
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
else:
|
|
|
|
return self.first_name
|
|
|
|
|
2018-03-07 09:48:30 +00:00
|
|
|
def mention(self):
|
|
|
|
"""Mention the user in the best way possible given the available data."""
|
|
|
|
if self.username is not None:
|
|
|
|
return f"@{self.username}"
|
|
|
|
else:
|
|
|
|
return f"[{self.first_name}](tg://user?id={self.user_id})"
|
|
|
|
|
2017-12-18 09:46:48 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return f"<User {self} having {self.credit} credit>"
|
|
|
|
|
|
|
|
|
|
|
|
class Product(TableDeclarativeBase):
|
|
|
|
"""A purchasable product."""
|
|
|
|
|
2018-01-29 12:26:23 +00:00
|
|
|
# Product id
|
|
|
|
id = Column(Integer, primary_key=True)
|
2017-12-18 09:46:48 +00:00
|
|
|
# Product name
|
2018-01-29 12:26:23 +00:00
|
|
|
name = Column(String)
|
2017-12-18 09:46:48 +00:00
|
|
|
# Product description
|
|
|
|
description = Column(Text)
|
|
|
|
# Product price, if null product is not for sale
|
2018-01-10 10:29:02 +00:00
|
|
|
price = Column(Integer)
|
2018-02-01 09:06:40 +00:00
|
|
|
# Image data
|
|
|
|
image = Column(LargeBinary)
|
2018-03-06 16:39:02 +00:00
|
|
|
# Product has been deleted
|
|
|
|
deleted = Column(Boolean, nullable=False)
|
2017-12-18 09:46:48 +00:00
|
|
|
# Stock quantity, if null product has infinite stock
|
|
|
|
stock = Column(Integer)
|
|
|
|
|
|
|
|
# Extra table parameters
|
2018-02-05 11:49:21 +00:00
|
|
|
__tablename__ = "products"
|
2017-12-18 09:46:48 +00:00
|
|
|
|
|
|
|
# No __init__ is needed, the default one is sufficient
|
|
|
|
|
2018-02-15 09:37:51 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.text()
|
|
|
|
|
2018-02-28 19:07:40 +00:00
|
|
|
def text(self, style:str="full", cart_qty:int=None):
|
2017-12-18 09:46:48 +00:00
|
|
|
"""Return the product details formatted with Telegram HTML. The image is omitted."""
|
2018-02-28 19:07:40 +00:00
|
|
|
if style == "short":
|
|
|
|
return f"{cart_qty}x {escape(self.name)} - {str(Price(self.price) * cart_qty)}"
|
|
|
|
elif style == "full":
|
|
|
|
return f"<b>{escape(self.name)}</b>\n" \
|
|
|
|
f"{escape(self.description)}\n" \
|
|
|
|
f"<i>{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}</i>\n" \
|
|
|
|
f"{str(Price(self.price))}\n" \
|
|
|
|
f"<b>{strings.in_cart_format_string.format(quantity=cart_qty) if cart_qty is not None else ''}</b>"
|
|
|
|
elif style == "image":
|
|
|
|
return f"{escape(self.name)}\n" \
|
|
|
|
f"{escape(self.description)}\n" \
|
|
|
|
f"{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}\n" \
|
2018-03-06 16:39:02 +00:00
|
|
|
f"{str(Price(self.price))}\n" \
|
2018-02-28 19:07:40 +00:00
|
|
|
f"{strings.in_cart_format_string.format(quantity=cart_qty) if cart_qty is not None else ''}"
|
|
|
|
else:
|
|
|
|
raise ValueError("style is not an accepted value")
|
2017-12-18 09:46:48 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f"<Product {self.name}>"
|
|
|
|
|
2018-02-15 09:37:51 +00:00
|
|
|
def send_as_message(self, chat_id: int) -> dict:
|
2018-02-01 09:06:40 +00:00
|
|
|
"""Send a message containing the product data."""
|
2018-02-15 09:37:51 +00:00
|
|
|
if self.image is None:
|
|
|
|
r = requests.get(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendMessage",
|
|
|
|
params={"chat_id": chat_id,
|
2018-02-28 19:07:40 +00:00
|
|
|
"text": self.text(),
|
2018-02-15 09:37:51 +00:00
|
|
|
"parse_mode": "HTML"})
|
|
|
|
else:
|
|
|
|
r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto",
|
|
|
|
files={"photo": self.image},
|
|
|
|
params={"chat_id": chat_id,
|
2018-02-28 19:07:40 +00:00
|
|
|
"caption": self.text(style="image"),
|
2018-02-15 09:37:51 +00:00
|
|
|
"parse_mode": "HTML"})
|
|
|
|
return r.json()
|
2018-02-01 09:06:40 +00:00
|
|
|
|
|
|
|
def set_image(self, file: telegram.File):
|
|
|
|
"""Download an image from Telegram and store it in the image column.
|
|
|
|
This is a slow blocking function. Try to avoid calling it directly, use a thread if possible."""
|
|
|
|
# Download the photo through a get request
|
|
|
|
r = requests.get(file.file_path)
|
|
|
|
# Store the photo in the database record
|
|
|
|
self.image = r.content
|
2017-12-18 09:46:48 +00:00
|
|
|
|
|
|
|
|
2017-12-20 10:18:06 +00:00
|
|
|
class Transaction(TableDeclarativeBase):
|
2017-12-18 09:46:48 +00:00
|
|
|
"""A greed wallet transaction.
|
|
|
|
Wallet credit ISN'T calculated from these, but they can be used to recalculate it."""
|
2018-04-01 16:10:14 +00:00
|
|
|
# TODO: split this into multiple tables
|
2017-12-18 09:46:48 +00:00
|
|
|
|
|
|
|
# The internal transaction ID
|
2018-01-10 10:29:02 +00:00
|
|
|
transaction_id = Column(Integer, primary_key=True)
|
2017-12-18 09:46:48 +00:00
|
|
|
# The user whose credit is affected by this transaction
|
2017-12-20 11:06:12 +00:00
|
|
|
user_id = Column(BigInteger, ForeignKey("users.user_id"), nullable=False)
|
2018-01-10 10:29:02 +00:00
|
|
|
user = relationship("User")
|
2017-12-18 09:46:48 +00:00
|
|
|
# The value of this transaction. Can be both negative and positive.
|
2018-01-10 10:29:02 +00:00
|
|
|
value = Column(Integer, nullable=False)
|
2018-03-22 08:54:45 +00:00
|
|
|
# Refunded status: if True, ignore the value of this transaction when recalculating
|
|
|
|
refunded = Column(Boolean, default=False)
|
2017-12-18 09:46:48 +00:00
|
|
|
# Extra notes on the transaction
|
|
|
|
notes = Column(Text)
|
2018-01-10 10:29:02 +00:00
|
|
|
|
2017-12-20 10:18:06 +00:00
|
|
|
# Payment provider
|
|
|
|
provider = Column(String)
|
2018-01-10 10:29:02 +00:00
|
|
|
# Transaction ID supplied by Telegram
|
|
|
|
telegram_charge_id = Column(String)
|
2017-12-20 10:18:06 +00:00
|
|
|
# Transaction ID supplied by the payment provider
|
2018-01-10 10:29:02 +00:00
|
|
|
provider_charge_id = Column(String)
|
2017-12-20 10:18:06 +00:00
|
|
|
# Extra transaction data, may be required by the payment provider in case of a dispute
|
|
|
|
payment_name = Column(String)
|
2018-01-10 10:29:02 +00:00
|
|
|
payment_phone = Column(String)
|
2017-12-20 10:18:06 +00:00
|
|
|
payment_email = Column(String)
|
|
|
|
|
2018-01-10 10:29:02 +00:00
|
|
|
# Order ID
|
2018-03-12 09:19:01 +00:00
|
|
|
order_id = Column(Integer, ForeignKey("orders.order_id"))
|
|
|
|
order = relationship("Order")
|
2018-01-10 10:29:02 +00:00
|
|
|
|
2017-12-20 10:18:06 +00:00
|
|
|
# Extra table parameters
|
|
|
|
__tablename__ = "transactions"
|
2018-01-10 10:29:02 +00:00
|
|
|
__table_args__ = (UniqueConstraint("provider", "provider_charge_id"),)
|
2017-12-20 10:18:06 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
"""Return the correctly formatted transaction value"""
|
|
|
|
# Add a plus symbol if the value is positive
|
|
|
|
string = "+" if self.value > 0 else ""
|
|
|
|
# Add the correctly formatted value
|
|
|
|
string += strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.value)
|
|
|
|
# Return the created string
|
|
|
|
return string
|
|
|
|
|
|
|
|
def __repr__(self):
|
2018-02-05 11:49:21 +00:00
|
|
|
return f"<Transaction {self.transaction_id} for User {self.user_id} {str(self)}>"
|
2017-12-20 10:18:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Admin(TableDeclarativeBase):
|
|
|
|
"""A greed administrator with his permissions."""
|
|
|
|
|
|
|
|
# The telegram id
|
|
|
|
user_id = Column(BigInteger, ForeignKey("users.user_id"), primary_key=True)
|
2018-01-10 10:29:02 +00:00
|
|
|
user = relationship("User")
|
2017-12-20 10:18:06 +00:00
|
|
|
# Permissions
|
2018-03-22 08:54:45 +00:00
|
|
|
edit_products = Column(Boolean, default=True)
|
2018-03-07 09:48:30 +00:00
|
|
|
receive_orders = Column(Boolean, default=True)
|
2018-03-22 08:54:45 +00:00
|
|
|
view_transactions = Column(Boolean, default=True)
|
2018-04-01 16:10:14 +00:00
|
|
|
# Live mode enabled
|
|
|
|
live_mode = Column(Boolean, default=False)
|
2017-12-20 10:18:06 +00:00
|
|
|
|
|
|
|
# Extra table parameters
|
|
|
|
__tablename__ = "admins"
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f"<Admin {self.user_id}>"
|
2017-12-20 11:06:12 +00:00
|
|
|
|
|
|
|
|
2018-02-05 11:49:21 +00:00
|
|
|
class Order(TableDeclarativeBase):
|
|
|
|
"""An order which has been placed by an user.
|
|
|
|
It may include multiple products, available in the OrderItem table."""
|
|
|
|
|
|
|
|
# The unique order id
|
|
|
|
order_id = Column(Integer, primary_key=True)
|
|
|
|
# The user who placed the order
|
|
|
|
user_id = Column(BigInteger, ForeignKey("users.user_id"))
|
|
|
|
user = relationship("User")
|
|
|
|
# Date of creation
|
|
|
|
creation_date = Column(DateTime, nullable=False)
|
2018-03-22 08:54:45 +00:00
|
|
|
# Date of delivery
|
2018-02-05 11:49:21 +00:00
|
|
|
delivery_date = Column(DateTime)
|
2018-03-22 08:54:45 +00:00
|
|
|
# Date of refund: if null, product hasn't been refunded
|
|
|
|
refund_date = Column(DateTime)
|
|
|
|
# Refund reason: if null, product hasn't been refunded
|
|
|
|
refund_reason = Column(Text)
|
2018-02-05 11:49:21 +00:00
|
|
|
# List of items in the order
|
|
|
|
items = relationship("OrderItem")
|
|
|
|
# Extra details specified by the purchasing user
|
|
|
|
notes = Column(Text)
|
2018-03-12 09:19:01 +00:00
|
|
|
# Linked transaction
|
|
|
|
transaction = relationship("Transaction", uselist=False)
|
2018-02-05 11:49:21 +00:00
|
|
|
|
|
|
|
# Extra table parameters
|
|
|
|
__tablename__ = "orders"
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return f"<Order {self.order_id} placed by User {self.user_id}>"
|
|
|
|
|
2018-03-12 09:19:01 +00:00
|
|
|
def get_text(self, session):
|
|
|
|
joined_self = session.query(Order).filter_by(order_id=self.order_id).join(Transaction).one()
|
2018-03-07 09:48:30 +00:00
|
|
|
items = ""
|
|
|
|
for item in self.items:
|
|
|
|
items += str(item) + "\n"
|
2018-04-01 16:10:14 +00:00
|
|
|
if self.delivery_date is not None:
|
|
|
|
status_emoji = strings.emoji_completed
|
|
|
|
elif self.refund_date is not None:
|
|
|
|
status_emoji = strings.emoji_refunded
|
|
|
|
else:
|
|
|
|
status_emoji = strings.emoji_not_processed
|
|
|
|
return status_emoji + " " + \
|
|
|
|
strings.order_number.format(id=self.order_id) + "\n" + \
|
|
|
|
strings.order_format_string.format(user=self.user.mention(),
|
|
|
|
date=self.creation_date.isoformat(),
|
|
|
|
items=items,
|
|
|
|
notes=self.notes if self.notes is not None else "",
|
|
|
|
value=str(Price(-joined_self.transaction.value)))
|
2018-03-07 09:48:30 +00:00
|
|
|
|
2018-02-05 11:49:21 +00:00
|
|
|
|
|
|
|
class OrderItem(TableDeclarativeBase):
|
|
|
|
"""A product that has been purchased as part of an order."""
|
|
|
|
|
|
|
|
# The unique item id
|
|
|
|
item_id = Column(Integer, primary_key=True)
|
|
|
|
# The product that is being ordered
|
|
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
|
|
|
product = relationship("Product")
|
|
|
|
# The order in which this item is being purchased
|
|
|
|
order_id = Column(Integer, ForeignKey("orders.order_id"), nullable=False)
|
|
|
|
|
|
|
|
# Extra table parameters
|
|
|
|
__tablename__ = "orderitems"
|
|
|
|
|
2018-03-07 09:48:30 +00:00
|
|
|
def __str__(self):
|
2018-03-12 09:19:01 +00:00
|
|
|
return f"{self.product.name} - {str(Price(self.product.price))}"
|
2018-03-07 09:48:30 +00:00
|
|
|
|
2018-02-05 11:49:21 +00:00
|
|
|
def __repr__(self):
|
|
|
|
return f"<OrderItem {self.item_id}>"
|
|
|
|
|
|
|
|
|
2017-12-20 11:06:12 +00:00
|
|
|
# If this script is ran as main, try to create all the tables in the database
|
|
|
|
if __name__ == "__main__":
|
|
|
|
TableDeclarativeBase.metadata.create_all()
|