feat(premium): Impl guild upgrade path.
This commit is contained in:
@@ -19,6 +19,7 @@ from constants import MAX_COINS
|
|||||||
from . import logger, babel
|
from . import logger, babel
|
||||||
from .data import PremiumData, GemTransactionType
|
from .data import PremiumData, GemTransactionType
|
||||||
from .ui.transactions import TransactionList
|
from .ui.transactions import TransactionList
|
||||||
|
from .ui.premium import PremiumUI
|
||||||
from .errors import GemTransactionFailed, BalanceTooLow, BalanceTooHigh
|
from .errors import GemTransactionFailed, BalanceTooLow, BalanceTooHigh
|
||||||
|
|
||||||
_p = babel._p
|
_p = babel._p
|
||||||
@@ -44,7 +45,7 @@ class PremiumCog(LionCog):
|
|||||||
|
|
||||||
|
|
||||||
# ----- API -----
|
# ----- API -----
|
||||||
def buy_gems_buttons(self) -> Button:
|
def buy_gems_button(self) -> Button:
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
|
|
||||||
button = Button(
|
button = Button(
|
||||||
@@ -89,7 +90,7 @@ class PremiumCog(LionCog):
|
|||||||
row = await self.data.PremiumGuild.fetch(guildid)
|
row = await self.data.PremiumGuild.fetch(guildid)
|
||||||
now = utc_now()
|
now = utc_now()
|
||||||
|
|
||||||
premium = (row is not None) and (row.premium_until > now)
|
premium = (row is not None) and row.premium_until and (row.premium_until > now)
|
||||||
return premium
|
return premium
|
||||||
|
|
||||||
@log_wrap(isolate=True)
|
@log_wrap(isolate=True)
|
||||||
@@ -216,6 +217,7 @@ class PremiumCog(LionCog):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
await self.gem_logger.send(embed=embed)
|
await self.gem_logger.send(embed=embed)
|
||||||
|
posted = True
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -482,7 +484,6 @@ class PremiumCog(LionCog):
|
|||||||
|
|
||||||
await ctx.reply(embed=embed, ephemeral=True)
|
await ctx.reply(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
@cmds.hybrid_command(
|
@cmds.hybrid_command(
|
||||||
name=_p('cmd:premium', "premium"),
|
name=_p('cmd:premium', "premium"),
|
||||||
description=_p(
|
description=_p(
|
||||||
@@ -490,9 +491,16 @@ class PremiumCog(LionCog):
|
|||||||
"Upgrade your server with LionGems!"
|
"Upgrade your server with LionGems!"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@appcmds.guild_only
|
||||||
async def cmd_premium(self, ctx: LionContext):
|
async def cmd_premium(self, ctx: LionContext):
|
||||||
# TODO
|
if not ctx.guild:
|
||||||
...
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
ui = PremiumUI(self.bot, ctx.guild, ctx.luser, callerid=ctx.author.id)
|
||||||
|
await ui.run(ctx.interaction)
|
||||||
|
await ui.wait()
|
||||||
|
|
||||||
# ----- Owner Commands -----
|
# ----- Owner Commands -----
|
||||||
@LionCog.placeholder_group
|
@LionCog.placeholder_group
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
from typing import Optional, TYPE_CHECKING, NamedTuple
|
||||||
|
import asyncio
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ui.button import button, Button, ButtonStyle
|
||||||
|
from psycopg import sql
|
||||||
|
|
||||||
|
from meta import LionBot, conf
|
||||||
|
from meta.logger import log_wrap
|
||||||
|
from core.lion_user import LionUser
|
||||||
|
from babel.translator import LazyStr
|
||||||
|
from meta.errors import ResponseTimedOut, UserInputError
|
||||||
|
from data import RawExpr
|
||||||
|
from modules.premium.errors import BalanceTooLow
|
||||||
|
|
||||||
|
from utils.ui import MessageUI, Confirm, AButton
|
||||||
|
from utils.lib import MessageArgs, utc_now
|
||||||
|
|
||||||
|
from .. import babel, logger
|
||||||
|
from ..data import GemTransactionType, PremiumData
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..cog import PremiumCog
|
||||||
|
|
||||||
|
_p = babel._p
|
||||||
|
|
||||||
|
|
||||||
|
class PremiumPlan(NamedTuple):
|
||||||
|
text: LazyStr
|
||||||
|
label: LazyStr
|
||||||
|
emoji: Optional[discord.PartialEmoji | discord.Emoji | str]
|
||||||
|
duration: int
|
||||||
|
price: int
|
||||||
|
|
||||||
|
|
||||||
|
plans = [
|
||||||
|
PremiumPlan(
|
||||||
|
_p('plan:three_months|text', "three months"),
|
||||||
|
_p('plan:three_months|label', "Three Months"),
|
||||||
|
None,
|
||||||
|
90,
|
||||||
|
4000
|
||||||
|
),
|
||||||
|
PremiumPlan(
|
||||||
|
_p('plan:one_year|text', "one year"),
|
||||||
|
_p('plan:one_year|label', "One Year"),
|
||||||
|
None,
|
||||||
|
365,
|
||||||
|
12000
|
||||||
|
),
|
||||||
|
PremiumPlan(
|
||||||
|
_p('plan:one_month|text', "one month"),
|
||||||
|
_p('plan:one_month|label', "One Month"),
|
||||||
|
None,
|
||||||
|
30,
|
||||||
|
1500
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class PremiumUI(MessageUI):
|
||||||
|
def __init__(self, bot: LionBot, guild: discord.Guild, luser: LionUser, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.bot = bot
|
||||||
|
self.guild = guild
|
||||||
|
self.luser = luser
|
||||||
|
|
||||||
|
self.cog: 'PremiumCog' = bot.get_cog('PremiumCog') # type: ignore
|
||||||
|
|
||||||
|
# UI State
|
||||||
|
self.premium_status: Optional[PremiumData.PremiumGuild] = None
|
||||||
|
|
||||||
|
self.plan_buttons = self._plan_buttons()
|
||||||
|
self.link_button = self.cog.buy_gems_button()
|
||||||
|
|
||||||
|
# ----- API -----
|
||||||
|
# ----- UI Components -----
|
||||||
|
|
||||||
|
async def plan_button(self, press: discord.Interaction, pressed: Button, plan: PremiumPlan):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
# Check Balance
|
||||||
|
if self.luser.data.gems < plan.price:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'ui:premium|button:plan|error:insufficient_gems',
|
||||||
|
"You do not have enough LionGems to purchase this plan!"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Confirm Purchase
|
||||||
|
confirm_msg = t(_p(
|
||||||
|
'ui:premium|button:plan|confirm|desc',
|
||||||
|
"Contributing **{plan_text}** of premium subscription for this server"
|
||||||
|
" will cost you {gem}**{plan_price}**.\n"
|
||||||
|
"Are you sure you want to proceed?"
|
||||||
|
)).format(
|
||||||
|
plan_text=t(plan.text),
|
||||||
|
gem=self.bot.config.emojis.gem,
|
||||||
|
plan_price=plan.price,
|
||||||
|
)
|
||||||
|
confirm = Confirm(confirm_msg, press.user.id)
|
||||||
|
confirm.embed.title = t(_p(
|
||||||
|
'ui:premium|button:plan|confirm|title',
|
||||||
|
"Confirm Server Upgrade"
|
||||||
|
))
|
||||||
|
confirm.embed.set_footer(
|
||||||
|
text=t(_p(
|
||||||
|
'ui:premium|button:plan|confirm|footer',
|
||||||
|
"Your current balance is {balance} LionGems"
|
||||||
|
)).format(balance=self.luser.data.gems)
|
||||||
|
)
|
||||||
|
confirm.embed.colour = 0x41f097
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await confirm.ask(press, ephemeral=True)
|
||||||
|
except ResponseTimedOut:
|
||||||
|
result = False
|
||||||
|
if not result:
|
||||||
|
await press.followup.send(
|
||||||
|
t(_p(
|
||||||
|
'ui:premium|button:plan|confirm|cancelled',
|
||||||
|
"Purchase cancelled! No LionGems were deducted from your account."
|
||||||
|
)),
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Write transaction, plan contribution, and new plan status, with potential rollback
|
||||||
|
try:
|
||||||
|
await self._do_premium_upgrade(plan)
|
||||||
|
except BalanceTooLow:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'ui:premium|button:plan|error:insufficient_gems_post_confirm',
|
||||||
|
"Insufficient LionGems to purchase this plan!"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Acknowledge premium
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
title=t(_p(
|
||||||
|
'ui:premium|button:plan|embed:success|title',
|
||||||
|
"Server Upgraded!"
|
||||||
|
)),
|
||||||
|
description=t(_p(
|
||||||
|
'ui:premium|button:plan|embed:success|desc',
|
||||||
|
"You have contributed **{plan_text}** of premium subscription to this server!"
|
||||||
|
)).format(plan_text=plan.text)
|
||||||
|
)
|
||||||
|
await press.followup.send(
|
||||||
|
embed=embed
|
||||||
|
)
|
||||||
|
await self.refresh()
|
||||||
|
|
||||||
|
@log_wrap(action='premium upgrade')
|
||||||
|
async def _do_premium_upgrade(self, plan: PremiumPlan):
|
||||||
|
async with self.bot.db.connection() as conn:
|
||||||
|
self.bot.db.conn = conn
|
||||||
|
async with conn.transaction():
|
||||||
|
# Perform the gem transaction
|
||||||
|
transaction = await self.cog.gem_transaction(
|
||||||
|
GemTransactionType.PURCHASE,
|
||||||
|
actorid=self.luser.userid,
|
||||||
|
from_account=self.luser.userid,
|
||||||
|
to_account=None,
|
||||||
|
amount=plan.price,
|
||||||
|
description=(
|
||||||
|
f"User purchased {plan.duration} days of premium"
|
||||||
|
f" for guild {self.guild.id} using the `PremiumUI`."
|
||||||
|
),
|
||||||
|
note=None,
|
||||||
|
reference=f"iid: {self._original.id if self._original else 'None'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
model = self.cog.data.PremiumGuild
|
||||||
|
# Ensure the premium guild row exists
|
||||||
|
premium_guild = await model.fetch_or_create(self.guild.id)
|
||||||
|
|
||||||
|
# Extend the subscription
|
||||||
|
await model.table.update_where(guildid=self.guild.id).set(
|
||||||
|
premium_until=RawExpr(
|
||||||
|
sql.SQL("GREATEST(premium_until, now()) + {}").format(
|
||||||
|
sql.Placeholder()
|
||||||
|
),
|
||||||
|
(dt.timedelta(days=plan.duration),)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finally, record the user's contribution
|
||||||
|
await self.cog.data.premium_guild_contributions.insert(
|
||||||
|
userid=self.luser.userid, guildid=self.guild.id,
|
||||||
|
transactionid=transaction.transactionid, duration=plan.duration
|
||||||
|
)
|
||||||
|
|
||||||
|
def _plan_buttons(self) -> list[Button]:
|
||||||
|
"""
|
||||||
|
Generate the Plan buttons.
|
||||||
|
|
||||||
|
Intended to be used once, upon initialisation.
|
||||||
|
"""
|
||||||
|
t = self.bot.translator.t
|
||||||
|
buttons = []
|
||||||
|
for plan in plans:
|
||||||
|
butt = AButton(
|
||||||
|
label=t(plan.label),
|
||||||
|
emoji=plan.emoji,
|
||||||
|
style=ButtonStyle.blurple,
|
||||||
|
pass_kwargs={'plan': plan}
|
||||||
|
)
|
||||||
|
butt(self.plan_button)
|
||||||
|
self.add_item(butt)
|
||||||
|
buttons.append(butt)
|
||||||
|
return buttons
|
||||||
|
|
||||||
|
# ----- UI Flow -----
|
||||||
|
def _current_status(self) -> str:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
if self.premium_status is None or self.premium_status.premium_until is None:
|
||||||
|
status = t(_p(
|
||||||
|
'ui:premium|current_status:none',
|
||||||
|
"__**Current Server Status:**__ Awaiting Upgrade."
|
||||||
|
))
|
||||||
|
elif self.premium_status.premium_until > utc_now():
|
||||||
|
status = t(_p(
|
||||||
|
'ui:premium|current_status:premium',
|
||||||
|
"__**Current Server Status:**__ Upgraded! Premium until {expiry}"
|
||||||
|
)).format(expiry=discord.utils.format_dt(self.premium_status.premium_until, 'd'))
|
||||||
|
else:
|
||||||
|
status = t(_p(
|
||||||
|
'ui:premium|current_status:none',
|
||||||
|
"__**Current Server Status:**__ Awaiting Upgrade. Premium status expired on {expiry}"
|
||||||
|
)).format(expiry=discord.utils.format_dt(self.premium_status.premium_until, 'd'))
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
t = self.bot.translator.t
|
||||||
|
|
||||||
|
blurb = t(_p(
|
||||||
|
'ui:premium|embed|description',
|
||||||
|
"By supporting our project, you will get access to countless customisation features!\n\n"
|
||||||
|
"- **Rebranding:** Customizable HEX colours and"
|
||||||
|
" **beautiful premium skins** for all of your community members!\n"
|
||||||
|
"- **Remove the vote and sponsor prompt!**\n"
|
||||||
|
"- Access to all of the [future premium features](https://staging.lionbot.org/donate)\n\n"
|
||||||
|
"Both server owners and **regular users** can"
|
||||||
|
" **buy and gift a subscription for this server** using this command!\n"
|
||||||
|
"To support both Leo and your server, **use the buttons below**!"
|
||||||
|
)) + '\n\n' + self._current_status()
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=0x41f097,
|
||||||
|
title=t(_p(
|
||||||
|
'ui:premium|embed|title',
|
||||||
|
"Support Leo and Upgrade your Server!"
|
||||||
|
)),
|
||||||
|
description=blurb,
|
||||||
|
)
|
||||||
|
embed.set_thumbnail(
|
||||||
|
url="https://i.imgur.com/v1mZolL.png"
|
||||||
|
)
|
||||||
|
embed.set_image(
|
||||||
|
url="https://cdn.discordapp.com/attachments/824196406482305034/972405513570615326/premium_test.png"
|
||||||
|
)
|
||||||
|
embed.set_footer(
|
||||||
|
text=t(_p(
|
||||||
|
'ui:premium|embed|footer',
|
||||||
|
"Your current balance is {balance} LionGems."
|
||||||
|
)).format(balance=self.luser.data.gems)
|
||||||
|
)
|
||||||
|
|
||||||
|
return MessageArgs(embed=embed)
|
||||||
|
|
||||||
|
async def refresh_layout(self):
|
||||||
|
self.set_layout(
|
||||||
|
(*self.plan_buttons, self.link_button),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
self.premium_status = await self.cog.data.PremiumGuild.fetch(self.guild.id, cached=False)
|
||||||
|
await self.luser.data.refresh()
|
||||||
|
|||||||
Reference in New Issue
Block a user