feat(premium): Impl guild upgrade path.

This commit is contained in:
2023-10-26 13:48:17 +03:00
parent c41374bbaa
commit d3bd233b3e
2 changed files with 299 additions and 5 deletions

View File

@@ -19,6 +19,7 @@ from constants import MAX_COINS
from . import logger, babel
from .data import PremiumData, GemTransactionType
from .ui.transactions import TransactionList
from .ui.premium import PremiumUI
from .errors import GemTransactionFailed, BalanceTooLow, BalanceTooHigh
_p = babel._p
@@ -44,7 +45,7 @@ class PremiumCog(LionCog):
# ----- API -----
def buy_gems_buttons(self) -> Button:
def buy_gems_button(self) -> Button:
t = self.bot.translator.t
button = Button(
@@ -89,7 +90,7 @@ class PremiumCog(LionCog):
row = await self.data.PremiumGuild.fetch(guildid)
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
@log_wrap(isolate=True)
@@ -216,6 +217,7 @@ class PremiumCog(LionCog):
try:
await self.gem_logger.send(embed=embed)
posted = True
except discord.HTTPException:
pass
@@ -482,7 +484,6 @@ class PremiumCog(LionCog):
await ctx.reply(embed=embed, ephemeral=True)
@cmds.hybrid_command(
name=_p('cmd:premium', "premium"),
description=_p(
@@ -490,9 +491,16 @@ class PremiumCog(LionCog):
"Upgrade your server with LionGems!"
)
)
@appcmds.guild_only
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 -----
@LionCog.placeholder_group

View File

@@ -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()