generated from HoloTech/holotech-plugin-template
(discord): Fill in discord interfaces
This commit is contained in:
+445
-3
@@ -3,14 +3,17 @@ import asyncio
|
|||||||
|
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands as cmds
|
from discord.ext import commands as cmds
|
||||||
from discord import app_commands as appcmds
|
from discord import Forbidden, User, app_commands as appcmds
|
||||||
|
|
||||||
from meta import LionBot, LionCog, LionContext
|
from meta import LionBot, LionCog, LionContext
|
||||||
|
from meta.errors import SafeCancellation, UserInputError
|
||||||
from meta.logger import log_wrap
|
from meta.logger import log_wrap
|
||||||
from utils.lib import utc_now
|
from utils.lib import utc_now
|
||||||
|
|
||||||
from ..data import CampaignData
|
from ..data import CampaignData, EarnedReward
|
||||||
from ..campaigns import CampaignRegistry
|
from ..campaign import CampaignRegistry, RewardCampaign
|
||||||
|
from ..lib import ThreadedWebhook
|
||||||
|
from .ui import RewardList, RewardEditor, CampaignDashboard
|
||||||
|
|
||||||
|
|
||||||
class CampaignCog(LionCog):
|
class CampaignCog(LionCog):
|
||||||
@@ -24,3 +27,442 @@ class CampaignCog(LionCog):
|
|||||||
await self.data.init()
|
await self.data.init()
|
||||||
await self.bot.version_check(*self.data.VERSION)
|
await self.bot.version_check(*self.data.VERSION)
|
||||||
await self.campaigns.init()
|
await self.campaigns.init()
|
||||||
|
|
||||||
|
async def resolve_campaign(
|
||||||
|
self, cid: int, campaign_name: str | None
|
||||||
|
) -> RewardCampaign:
|
||||||
|
campaign = None
|
||||||
|
if campaign_name is not None:
|
||||||
|
campaign = await self.campaigns.fetch_campaign_by_name(cid, campaign_name)
|
||||||
|
if campaign is None:
|
||||||
|
raise UserInputError(
|
||||||
|
f"Sorry, no campaign found named '{campaign_name}'"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Find active campaign
|
||||||
|
active = await self.campaigns.fetch_campaigns(cid, active=True)
|
||||||
|
if len(active) > 1:
|
||||||
|
names = ", ".join(camp.row.campaign_name for camp in active)
|
||||||
|
raise UserInputError(f"Multiple active campaigns running: {names}")
|
||||||
|
elif not active:
|
||||||
|
raise UserInputError("No active campaigns running")
|
||||||
|
else:
|
||||||
|
campaign = active[0]
|
||||||
|
return campaign
|
||||||
|
|
||||||
|
async def campaign_modcheck(self, campaign: RewardCampaign, member: discord.Member):
|
||||||
|
if member.guild_permissions.administrator:
|
||||||
|
return True
|
||||||
|
if campaign.row.moderator_role_id and campaign.row.moderator_role_id in [
|
||||||
|
r.id for r in member.roles
|
||||||
|
]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _campaign_acmpl(
|
||||||
|
self, interaction: discord.Interaction, partial: str
|
||||||
|
) -> list[appcmds.Choice]:
|
||||||
|
"""
|
||||||
|
Generate a list of campaigns, with active campaigns listed first.
|
||||||
|
|
||||||
|
Campaign values are their name.
|
||||||
|
"""
|
||||||
|
if not interaction.guild:
|
||||||
|
return []
|
||||||
|
community = await self.bot.profiles.fetch_community(
|
||||||
|
interaction.guild, interaction=interaction
|
||||||
|
)
|
||||||
|
cid = community.communityid
|
||||||
|
|
||||||
|
campaigns = await self.campaigns.fetch_campaigns(cid, active=None)
|
||||||
|
sorted_campaigns = sorted(
|
||||||
|
campaigns,
|
||||||
|
key=lambda camp: (camp.is_active, camp.row.started_at, camp.row.created_at),
|
||||||
|
)
|
||||||
|
names = [
|
||||||
|
camp.row.campaign_name
|
||||||
|
for camp in sorted_campaigns
|
||||||
|
if partial.lower() in camp.row.campaign_name.lower()
|
||||||
|
]
|
||||||
|
|
||||||
|
choices = [appcmds.Choice(name=name[:100], value=name) for name in names]
|
||||||
|
return choices
|
||||||
|
|
||||||
|
@cmds.hybrid_group(
|
||||||
|
name="campaign", description="Command group for administering reward campaigns"
|
||||||
|
)
|
||||||
|
@appcmds.guild_only()
|
||||||
|
@appcmds.default_permissions(manage_guild=True)
|
||||||
|
async def campaign_group(self, ctx: LionContext):
|
||||||
|
"""
|
||||||
|
As a base command group this never gets executed.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@campaign_group.command(
|
||||||
|
name="dashboard",
|
||||||
|
description="Show summary dashboard for active or selected campaign.",
|
||||||
|
)
|
||||||
|
@appcmds.describe(campaign_name="Name of the campaign to display")
|
||||||
|
@appcmds.rename(campaign_name="campaign")
|
||||||
|
async def campaign_dashboard_cmd(
|
||||||
|
self, ctx: LionContext, campaign_name: Optional[str] = None
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
campaign = await self.resolve_campaign(ctx.community.communityid, campaign_name)
|
||||||
|
if not await self.campaign_modcheck(campaign, ctx.author):
|
||||||
|
await ctx.interaction.response.send_message(
|
||||||
|
"You need to be an administrator or have the configured modrole to use that!",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Open campaign UI
|
||||||
|
widget = CampaignDashboard(bot=self.bot, campaign=campaign, callerid=ctx.author.id)
|
||||||
|
await widget.run(ctx.interaction)
|
||||||
|
await widget.wait()
|
||||||
|
|
||||||
|
campaign_dashboard_cmd.autocomplete("campaign_name")(_campaign_acmpl)
|
||||||
|
|
||||||
|
@campaign_group.command(name="start", description="Setup and start a campaign")
|
||||||
|
@appcmds.describe(
|
||||||
|
campaign_name="Name of the campaign to create. Must be unique.",
|
||||||
|
rewards_cap="Optional maximum number of rewards to give.",
|
||||||
|
moderator_role="Optional discord role to allow to edit and moderate the campaign.",
|
||||||
|
logging_webhook="Optional discord webhook URL to log earned rewards to.",
|
||||||
|
)
|
||||||
|
async def campaign_start_cmd(
|
||||||
|
self,
|
||||||
|
ctx: LionContext,
|
||||||
|
campaign_name: str,
|
||||||
|
rewards_cap: Optional[int] = None,
|
||||||
|
moderator_role: Optional[discord.Role] = None,
|
||||||
|
logging_webhook: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Permission check
|
||||||
|
if not ctx.author.guild_permissions.administrator:
|
||||||
|
await ctx.interaction.response.send_message(
|
||||||
|
"You need to be an administrator to start a campaign!", ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check name clash
|
||||||
|
existing = await self.campaigns.fetch_campaign_by_name(
|
||||||
|
ctx.community.communityid, campaign_name
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
await ctx.reply("A campaign with that name already exists!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check active campaign
|
||||||
|
campaigns = await self.campaigns.fetch_campaigns(
|
||||||
|
ctx.community.communityid, active=True
|
||||||
|
)
|
||||||
|
if campaigns:
|
||||||
|
existing_name = campaigns[0].row.campaign_name
|
||||||
|
await ctx.reply(
|
||||||
|
f"Your campaign '{existing_name}' is already active! "
|
||||||
|
"Sorry, we don't yet support multiple active campaigns, "
|
||||||
|
"please use '/campaign finish' to end your current campaign before starting a new one."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If logging webhook is given, check that it works
|
||||||
|
if logging_webhook is not None:
|
||||||
|
webhook = ThreadedWebhook.from_url(logging_webhook)
|
||||||
|
try:
|
||||||
|
await webhook.test_webhook()
|
||||||
|
except (discord.HTTPException, discord.Forbidden):
|
||||||
|
await ctx.reply("Couldn't post to the logging webhook provided!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Passed all the checks, now we can create
|
||||||
|
campaign = await self.campaigns.create_campaign(
|
||||||
|
cid=ctx.community.communityid,
|
||||||
|
campaign_name=campaign_name,
|
||||||
|
target_rewards=rewards_cap,
|
||||||
|
moderator_role_id=moderator_role.id if moderator_role else None,
|
||||||
|
logging_webhook_url=logging_webhook,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ack creation
|
||||||
|
# TODO: Can also show dashboard
|
||||||
|
await ctx.reply(
|
||||||
|
f"Setup and started your reward campaign {campaign.row.campaign_name}! Good luck"
|
||||||
|
)
|
||||||
|
# Open campaign UI
|
||||||
|
widget = CampaignDashboard(bot=self.bot, campaign=campaign, callerid=ctx.author.id)
|
||||||
|
await widget.run(ctx.interaction)
|
||||||
|
await widget.wait()
|
||||||
|
|
||||||
|
@campaign_group.command(name="configure", description="Update campaign settings")
|
||||||
|
@appcmds.describe(
|
||||||
|
campaign_name="Name of the campaign you want to update.",
|
||||||
|
new_name="New name for the campaign. Must be unique.",
|
||||||
|
new_cap="New cap for the number of rewards. Use 0 or negative for no cap.",
|
||||||
|
moderator_role="Optional discord role to allow to edit and moderate the campaign.",
|
||||||
|
logging_webhook="Optional discord webhook URL to log earned rewards to.",
|
||||||
|
)
|
||||||
|
@appcmds.rename(campaign_name="campaign")
|
||||||
|
async def campaign_configure_cmd(
|
||||||
|
self,
|
||||||
|
ctx: LionContext,
|
||||||
|
campaign_name: Optional[str] = None,
|
||||||
|
new_name: Optional[str] = None,
|
||||||
|
new_cap: Optional[int] = None,
|
||||||
|
moderator_role: Optional[discord.Role] = None,
|
||||||
|
logging_webhook: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
campaign = await self.resolve_campaign(ctx.community.communityid, campaign_name)
|
||||||
|
if not await self.campaign_modcheck(campaign, ctx.author):
|
||||||
|
await ctx.interaction.response.send_message(
|
||||||
|
"You need to be an administrator or have the configured modrole to use that!",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Verify each of the given settings to update
|
||||||
|
update_args = {}
|
||||||
|
|
||||||
|
# Check name clash
|
||||||
|
if (
|
||||||
|
new_name is not None
|
||||||
|
and new_name.lower() != campaign.row.campaign_name.lower()
|
||||||
|
):
|
||||||
|
existing = await self.campaigns.fetch_campaign_by_name(
|
||||||
|
ctx.community.communityid, campaign_name
|
||||||
|
)
|
||||||
|
if existing:
|
||||||
|
await ctx.reply("A campaign with that name already exists!")
|
||||||
|
return
|
||||||
|
update_args["campaign_name"] = new_name
|
||||||
|
|
||||||
|
if new_cap is not None:
|
||||||
|
update_args["total_rewards"] = new_cap if new_cap > 0 else None
|
||||||
|
|
||||||
|
# If logging webhook is given, check that it works
|
||||||
|
if logging_webhook is not None:
|
||||||
|
webhook = ThreadedWebhook.from_url(logging_webhook)
|
||||||
|
try:
|
||||||
|
await webhook.test_webhook()
|
||||||
|
except (discord.HTTPException, discord.Forbidden):
|
||||||
|
await ctx.reply("Couldn't post to the logging webhook provided!")
|
||||||
|
return
|
||||||
|
update_args["logging_webhook_url"] = logging_webhook
|
||||||
|
|
||||||
|
if moderator_role is not None:
|
||||||
|
# TODO: Currently no way to actually unset this
|
||||||
|
update_args["moderator_role_id"] = moderator_role.id
|
||||||
|
|
||||||
|
# Passed all the checks, now we can update
|
||||||
|
|
||||||
|
campaign = await self.campaigns.update_campaign(
|
||||||
|
campaign.row.campaign_id,
|
||||||
|
**update_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ack creation
|
||||||
|
# TODO: Can also show dashboard
|
||||||
|
await ctx.reply(
|
||||||
|
f"Setup and started your reward campaign {campaign.row.campaign_name}! Good luck"
|
||||||
|
)
|
||||||
|
|
||||||
|
campaign_configure_cmd.autocomplete("campaign_name")(_campaign_acmpl)
|
||||||
|
|
||||||
|
@campaign_group.command(
|
||||||
|
name="rewards",
|
||||||
|
description="View and edit the rewards earned so far this campaign",
|
||||||
|
)
|
||||||
|
@appcmds.describe(campaign_name="Name of the campaign to display rewards for")
|
||||||
|
@appcmds.rename(campaign_name="campaign")
|
||||||
|
async def campaign_rewards_cmd(
|
||||||
|
self,
|
||||||
|
ctx: LionContext,
|
||||||
|
campaign_name: Optional[str] = None,
|
||||||
|
):
|
||||||
|
if not ctx.guild:
|
||||||
|
return
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
campaign = await self.resolve_campaign(ctx.community.communityid, campaign_name)
|
||||||
|
if not await self.campaign_modcheck(campaign, ctx.author):
|
||||||
|
await ctx.interaction.response.send_message(
|
||||||
|
"You need to be an administrator or have the configured modrole to use that!",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show rewardlist widget
|
||||||
|
widget = RewardList(bot=self.bot, campaign=campaign, callerid=ctx.author.id)
|
||||||
|
await widget.run(ctx.interaction)
|
||||||
|
await widget.wait()
|
||||||
|
|
||||||
|
campaign_rewards_cmd.autocomplete("campaign_name")(_campaign_acmpl)
|
||||||
|
|
||||||
|
@campaign_group.command(
|
||||||
|
name="editreward",
|
||||||
|
description="Add or edit details for a given reward in a campaign (see also /campaign rewards)",
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
rewardid="Earned reward to edit",
|
||||||
|
)
|
||||||
|
@appcmds.rename(rewardid="reward")
|
||||||
|
async def campaign_editreward_cmd(self, ctx: LionContext, rewardid: str):
|
||||||
|
# For this we'll just open the reward editor
|
||||||
|
# Will need to identify the reward with autocomplete. Can use the reward id directly..
|
||||||
|
if not rewardid.isdigit():
|
||||||
|
raise UserInputError(
|
||||||
|
"Please enter the reward number or select a reward from the argument menu"
|
||||||
|
)
|
||||||
|
|
||||||
|
reward = await EarnedReward.fetch(int(rewardid))
|
||||||
|
if not reward:
|
||||||
|
raise UserInputError(
|
||||||
|
"Reward not found. Please enter the reward number or select a reward from the argument menu"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch campaign and then do a perm check
|
||||||
|
campaign = await self.campaigns.fetch_campaign(reward.campaign_id)
|
||||||
|
if campaign is None:
|
||||||
|
raise SafeCancellation("Something went wrong.. please try again soon.")
|
||||||
|
if campaign.row.communityid != ctx.community.communityid:
|
||||||
|
raise UserInputError("This reward doesn't belong to this community!")
|
||||||
|
|
||||||
|
if not await self.campaign_modcheck(campaign, ctx.author):
|
||||||
|
await ctx.interaction.response.send_message(
|
||||||
|
"You need to be an administrator or have the configured modrole to use that!",
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Okay, this is our reward, and author has permission to modify it. Spin the modal.
|
||||||
|
modal = RewardEditor.from_reward(reward)
|
||||||
|
|
||||||
|
currently_fluffed = reward.fulfilled_at is not None
|
||||||
|
|
||||||
|
@modal.submit_callback()
|
||||||
|
async def on_editor_submit(interaction: discord.Interaction):
|
||||||
|
update_args = {}
|
||||||
|
|
||||||
|
if modal.flufbox.component.value and not currently_fluffed:
|
||||||
|
# Reward has been fluffed
|
||||||
|
update_args["fulfilled_at"] = utc_now()
|
||||||
|
elif currently_fluffed and not modal.flufbox.component.value:
|
||||||
|
# Reward has been unfluffed
|
||||||
|
update_args["fulfilled_at"] = None
|
||||||
|
|
||||||
|
new_ref_value = modal.reference.value or None
|
||||||
|
if new_ref_value != reward.reference:
|
||||||
|
update_args["reference"] = new_ref_value
|
||||||
|
|
||||||
|
new_notes_value = modal.notes.value or None
|
||||||
|
if new_notes_value != reward.modnote:
|
||||||
|
update_args["modnote"] = new_notes_value
|
||||||
|
|
||||||
|
if update_args:
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
await campaign.update_reward(reward.earned_id, **update_args)
|
||||||
|
await interaction.followup.send(
|
||||||
|
content="Reward updated!", ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.defer(thinking=False)
|
||||||
|
|
||||||
|
def _reward_acmpl_format(
|
||||||
|
self, campaign: RewardCampaign, reward: EarnedReward
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Format an EarnedReward for completion.
|
||||||
|
|
||||||
|
#100: username in campaign
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@campaign_editreward_cmd.autocomplete("rewardid")
|
||||||
|
async def _reward_acmpl(
|
||||||
|
self, interaction: discord.Interaction, partial: str
|
||||||
|
) -> list[appcmds.Choice]:
|
||||||
|
"""
|
||||||
|
Display list of rewards.
|
||||||
|
|
||||||
|
If possible, use the active campaign.
|
||||||
|
If no matching choices, search all campaigns of the community.
|
||||||
|
|
||||||
|
Values are the earned_id as a string
|
||||||
|
"""
|
||||||
|
if not interaction.guild:
|
||||||
|
return []
|
||||||
|
community = await self.bot.profiles.fetch_community(
|
||||||
|
interaction.guild, interaction=interaction
|
||||||
|
)
|
||||||
|
cid = community.communityid
|
||||||
|
|
||||||
|
choices = []
|
||||||
|
|
||||||
|
campaigns = await self.campaigns.fetch_campaigns(cid, active=None)
|
||||||
|
sorted_campaigns = sorted(
|
||||||
|
campaigns,
|
||||||
|
key=lambda camp: (camp.is_active, camp.row.started_at, camp.row.created_at),
|
||||||
|
)
|
||||||
|
for campaign in sorted_campaigns:
|
||||||
|
rewards = await campaign.get_rewards()
|
||||||
|
for reward in reversed(rewards):
|
||||||
|
formatted = self._reward_acmpl_format(campaign, reward)
|
||||||
|
if partial in formatted:
|
||||||
|
choices.append(
|
||||||
|
appcmds.Choice(name=formatted, value=str(reward.earned_id))
|
||||||
|
)
|
||||||
|
if len(choices) >= 20:
|
||||||
|
break
|
||||||
|
if len(choices) >= 20:
|
||||||
|
break
|
||||||
|
|
||||||
|
return choices[:25]
|
||||||
|
|
||||||
|
# @campaign_group.command(
|
||||||
|
# name="reward",
|
||||||
|
# description="Add a reward to a given user"
|
||||||
|
# )
|
||||||
|
# async def campaign_reward_cmd(self, ctx: LionContext, twitch_username: str, reward_reason: str, campaign_name: Optional[str] = None):
|
||||||
|
# # TODO: This does
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# @campaign_group.command(
|
||||||
|
# name="start", description="Add or edit details for a given reward in a campaign"
|
||||||
|
# )
|
||||||
|
# async def campaign_rewardnote_cmd(self, ctx: LionContext): ...
|
||||||
|
#
|
||||||
|
# @campaign_group.command(name="finish", description="Close a campaign")
|
||||||
|
# @appcmds.describe(campaign_name="Name of the campaign to mark as complete")
|
||||||
|
# @appcmds.rename(campaign_name='campaign')
|
||||||
|
# async def campaign_finish_cmd(
|
||||||
|
# self, ctx: LionContext, campaign_name: Optional[str] = None
|
||||||
|
# ):
|
||||||
|
# if not ctx.guild:
|
||||||
|
# return
|
||||||
|
# if not ctx.interaction:
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# campaign = await self.resolve_campaign(ctx.community.communityid, campaign_name)
|
||||||
|
# if not await self.campaign_modcheck(campaign, ctx.author):
|
||||||
|
# await ctx.interaction.response.send_message(
|
||||||
|
# "You need to be an administrator or have the configured modrole to use that!",
|
||||||
|
# ephemeral=True
|
||||||
|
# )
|
||||||
|
# return
|
||||||
|
# campaign_finish_cmd.autocomplete('campaign_name')(_campaign_acmpl)
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
from .rewards import RewardList, RewardEditor
|
from .rewards import RewardList, RewardEditor
|
||||||
|
from .campaign import CampaignDashboard
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
Defines widgets for displaying a campaign
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
import asyncio
|
||||||
|
import datetime as dt
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ui import Modal
|
||||||
|
from discord.ui.select import select, Select, SelectOption
|
||||||
|
from discord.ui.button import button, Button, ButtonStyle
|
||||||
|
from discord.ui.text_input import TextInput, TextStyle
|
||||||
|
|
||||||
|
from meta import LionBot
|
||||||
|
from meta.errors import UserInputError
|
||||||
|
from meta.config import conf
|
||||||
|
from utils.lib import tabulate, utc_now, MessageArgs, parse_duration
|
||||||
|
from utils.ui import MessageUI, AButton, AsComponents, ConfigEditor
|
||||||
|
from utils.ui.micros import FastModal
|
||||||
|
from utils.ui.pagers import BasePager, Pager
|
||||||
|
|
||||||
|
from ...campaign import RewardCampaign
|
||||||
|
from ...data import EarnedReward
|
||||||
|
from .. import logger
|
||||||
|
|
||||||
|
from .rewards import RewardList
|
||||||
|
|
||||||
|
|
||||||
|
class CampaignDashboard(MessageUI):
|
||||||
|
def __init__(self, bot: LionBot, campaign: RewardCampaign, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.bot = bot
|
||||||
|
self.campaign = campaign
|
||||||
|
|
||||||
|
# UI state
|
||||||
|
|
||||||
|
# ------ UI API -----
|
||||||
|
|
||||||
|
# ------ UI Components -----
|
||||||
|
# Button to open rewards list
|
||||||
|
|
||||||
|
@button(label="Rewards Earned")
|
||||||
|
async def rewards_list_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
widget = RewardList(
|
||||||
|
bot=self.bot,
|
||||||
|
campaign=self.campaign,
|
||||||
|
callerid=self._callerid,
|
||||||
|
)
|
||||||
|
self._slaves.append(widget)
|
||||||
|
await widget.run(press)
|
||||||
|
await widget.wait()
|
||||||
|
self._slaves.remove(widget)
|
||||||
|
|
||||||
|
@button(emoji=conf.emojis.cancel, style=ButtonStyle.red)
|
||||||
|
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
"""Close the UI and all children."""
|
||||||
|
await press.response.defer(thinking=False)
|
||||||
|
await self.quit()
|
||||||
|
|
||||||
|
@button(emoji=conf.emojis.refresh)
|
||||||
|
async def refresh_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer()
|
||||||
|
await self.refresh()
|
||||||
|
|
||||||
|
# ------ UI Flow -----
|
||||||
|
|
||||||
|
async def refresh_layout(self):
|
||||||
|
# Nothing to refresh
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{self.campaign.row.campaign_name} Campaign Dashboard"
|
||||||
|
)
|
||||||
|
# embed.set_footer with last update
|
||||||
|
|
||||||
|
# Don't show the webhook url since it contains secrets, just show whether it is set
|
||||||
|
# Hard-coded channel/timer URL for now
|
||||||
|
campaign = self.campaign
|
||||||
|
started_at = (
|
||||||
|
discord.utils.format_dt(campaign.row.started_at, "F")
|
||||||
|
if campaign.row.started_at
|
||||||
|
else "*Not Started*"
|
||||||
|
)
|
||||||
|
|
||||||
|
all_rewards = await campaign.get_rewards()
|
||||||
|
rewards_earned = len(all_rewards)
|
||||||
|
reward_cap = campaign.row.target_rewards
|
||||||
|
if reward_cap is not None:
|
||||||
|
rewards = f"{rewards_earned} out of {reward_cap}"
|
||||||
|
else:
|
||||||
|
rewards = f"{rewards_earned}"
|
||||||
|
|
||||||
|
description = (
|
||||||
|
f"Campaign running since {started_at} with {rewards} rewards given."
|
||||||
|
)
|
||||||
|
|
||||||
|
table = {
|
||||||
|
"Created at": discord.utils.format_dt(campaign.row.created_at, "F"),
|
||||||
|
"Started at": started_at,
|
||||||
|
"Moderator Role": f"<&@{campaign.row.moderator_role_id}>" if campaign.row.moderator_role_id else "*Not Set*",
|
||||||
|
"Logging webhook": "*Set Up*" if campaign.row.logging_webhook_url else "*Not Set*",
|
||||||
|
"Rewards Earned": str(rewards_earned),
|
||||||
|
"Rewards Cap": str(reward_cap),
|
||||||
|
}
|
||||||
|
if campaign.row.completed_at is not None:
|
||||||
|
table['Finished at'] = discord.utils.format_dt(campaign.row.completed_at, "F")
|
||||||
|
prop_table = '\n'.join(tabulate(*table.items()))
|
||||||
|
|
||||||
|
embed.description = f"{description}\n\n{prop_table}"
|
||||||
|
|
||||||
|
# Brief summary of rewards in columns. 12 per column. Empty titles?
|
||||||
|
rewardrows = []
|
||||||
|
for reward in all_rewards:
|
||||||
|
name = reward.twitch_user_name or str(reward.twitch_user_id) or str(reward.profileid)
|
||||||
|
fluffed = reward.fulfilled_at is not None
|
||||||
|
fluffed_emoji = '✅' if fluffed else '🔳'
|
||||||
|
rewardrows.append(f"{fluffed_emoji} {name}")
|
||||||
|
|
||||||
|
blocks = ['\n'.join(rewardrows[i:i+12]) for i in range(0, len(rewardrows), 12)]
|
||||||
|
|
||||||
|
embed.add_field(
|
||||||
|
name="Rewards Summary",
|
||||||
|
value=blocks[0] or 'No Rewards Earned',
|
||||||
|
inline=True
|
||||||
|
)
|
||||||
|
for block in blocks[1:]:
|
||||||
|
embed.add_field(
|
||||||
|
name="--",
|
||||||
|
value=block,
|
||||||
|
)
|
||||||
|
|
||||||
|
return MessageArgs(embed=embed)
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
await self.campaign.row.refresh()
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ from meta import LionBot
|
|||||||
from meta.errors import UserInputError
|
from meta.errors import UserInputError
|
||||||
from meta.config import conf
|
from meta.config import conf
|
||||||
from utils.lib import tabulate, utc_now, MessageArgs, parse_duration
|
from utils.lib import tabulate, utc_now, MessageArgs, parse_duration
|
||||||
from utils.ui import MessageUI, AButton, AsComponents, ConfigEditor
|
from utils.ui import MessageUI
|
||||||
from utils.ui.micros import FastModal
|
from utils.ui.micros import FastModal
|
||||||
from utils.ui.pagers import BasePager, Pager
|
|
||||||
|
|
||||||
from ...campaign import RewardCampaign
|
from ...campaign import RewardCampaign
|
||||||
from ...data import EarnedReward
|
from ...data import EarnedReward
|
||||||
@@ -134,7 +133,38 @@ class RewardList(MessageUI):
|
|||||||
value = int(selected.values[0])
|
value = int(selected.values[0])
|
||||||
reward = next(r for r in self._rewards if r.earned_id == value)
|
reward = next(r for r in self._rewards if r.earned_id == value)
|
||||||
modal = RewardEditor.from_reward(reward)
|
modal = RewardEditor.from_reward(reward)
|
||||||
|
|
||||||
|
currently_fluffed = (reward.fulfilled_at is not None)
|
||||||
|
|
||||||
|
@modal.submit_callback()
|
||||||
|
async def on_editor_submit(interaction: discord.Interaction):
|
||||||
|
update_args = {}
|
||||||
|
|
||||||
|
if modal.flufbox.component.value and not currently_fluffed:
|
||||||
|
# Reward has been fluffed
|
||||||
|
update_args['fulfilled_at'] = utc_now()
|
||||||
|
elif currently_fluffed and not modal.flufbox.component.value:
|
||||||
|
# Reward has been unfluffed
|
||||||
|
update_args['fulfilled_at'] = None
|
||||||
|
|
||||||
|
new_ref_value = modal.reference.value or None
|
||||||
|
if new_ref_value != reward.reference:
|
||||||
|
update_args['reference'] = new_ref_value
|
||||||
|
|
||||||
|
new_notes_value = modal.notes.value or None
|
||||||
|
if new_notes_value != reward.modnote:
|
||||||
|
update_args['modnote'] = new_notes_value
|
||||||
|
|
||||||
|
if update_args:
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
await self.campaign.update_reward(reward.earned_id, **update_args)
|
||||||
|
await self.refresh(thinking=interaction)
|
||||||
|
else:
|
||||||
|
await interaction.response.defer(thinking=False)
|
||||||
|
|
||||||
|
|
||||||
await selection.response.send_modal(modal)
|
await selection.response.send_modal(modal)
|
||||||
|
|
||||||
await self.refresh()
|
await self.refresh()
|
||||||
else:
|
else:
|
||||||
await selection.response.defer()
|
await selection.response.defer()
|
||||||
@@ -200,7 +230,7 @@ class RewardList(MessageUI):
|
|||||||
'Earned At': discord.utils.format_dt(reward.earned_at, 'F'),
|
'Earned At': discord.utils.format_dt(reward.earned_at, 'F'),
|
||||||
'Earned From': reward.earned_from,
|
'Earned From': reward.earned_from,
|
||||||
'Fulfilled At': fluf,
|
'Fulfilled At': fluf,
|
||||||
'Reference': "*No reference set*",
|
'Reference': reward.reference or "*No reference set*",
|
||||||
'Further notes': reward.modnote or "*No notes*",
|
'Further notes': reward.modnote or "*No notes*",
|
||||||
}
|
}
|
||||||
prop_table = '\n'.join(tabulate(*table.items()))
|
prop_table = '\n'.join(tabulate(*table.items()))
|
||||||
|
|||||||
Reference in New Issue
Block a user