Compare commits

...

18 Commits

Author SHA1 Message Date
conatum 6122603115 fix: Deactivate testing event 2026-07-30 15:25:27 +03:00
conatum a69d45c1c2 tweak(campaign): Slight reformat to reward log 2026-07-30 14:43:13 +03:00
conatum e11fb8f071 fix(channel): Use correct channel name 2026-07-30 14:43:13 +03:00
conatum c54653322e Merge branch 'master' of thewisewolf.dev:CarmiCoven/pluscampaign-plugin 2026-07-30 14:41:29 +03:00
conatum 8189eeec05 feat(discord): Inline reward editing 2026-07-30 14:40:58 +03:00
conatum f2e8852d45 tweak(discord): Reform reward summary in list 2026-07-30 14:40:32 +03:00
conatum 5eb0c1a712 tweak: Add fluffed emoji to camp log 2026-07-30 14:39:22 +03:00
conatum 300f712bfd Merge branch 'master' of thewisewolf.dev:CarmiCoven/pluscampaign-plugin 2026-07-30 13:41:00 +03:00
conatum 05c428bcc9 fix(twitch): Converter on campaign reward cmd 2026-07-30 13:40:09 +03:00
conatum 87aec03e57 fix: Correct campaign condition 2026-07-30 13:39:06 +03:00
conatum f61d0519ee fix(discord): Actually send edit modal 2026-07-30 12:43:18 +03:00
conatum 99d1193be6 fix: Add edit support to ThreadedWebhook 2026-07-30 12:42:53 +03:00
conatum 1cc93d732e fix: Reward formatting and acmpl 2026-07-30 12:26:53 +03:00
conatum e700628403 tweak: Log formatting 2026-07-30 12:26:35 +03:00
conatum 299618ab1d fix: Rename logged_messageid column 2026-07-30 12:26:13 +03:00
conatum a681da058f fix: Add client to testing webhook 2026-07-30 11:59:50 +03:00
conatum 5873b0fe65 fix: Make 'earned_from' column name consistent 2026-07-30 11:58:35 +03:00
conatum d8817d4473 fix: Component string typos 2026-07-30 10:32:40 +03:00
7 changed files with 226 additions and 144 deletions
+2 -2
View File
@@ -38,9 +38,9 @@ CREATE TABLE campaign_rewards_earned(
fulfilled_note TEXT, fulfilled_note TEXT,
modnote TEXT, modnote TEXT,
reference TEXT, reference TEXT,
logged_messageid BIGINT, log_messageid BIGINT,
earned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), earned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
earned_reason TEXT NOT NULL, earned_from TEXT NOT NULL,
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() _timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
+31 -38
View File
@@ -57,7 +57,7 @@ class RewardCampaign:
async def add_reward( async def add_reward(
self, self,
profileid: int, profileid: int,
earned_reason: str, earned_from: str,
event_id: Optional[int] = None, event_id: Optional[int] = None,
twitch_user_id: Optional[str] = None, twitch_user_id: Optional[str] = None,
twitch_user_name: Optional[str] = None, twitch_user_name: Optional[str] = None,
@@ -66,7 +66,7 @@ class RewardCampaign:
row = await EarnedReward.create( row = await EarnedReward.create(
campaign_id=self.row.campaign_id, campaign_id=self.row.campaign_id,
profileid=profileid, profileid=profileid,
earned_reason=earned_reason, earned_from=earned_from,
event_id=event_id, event_id=event_id,
twitch_user_id=twitch_user_id, twitch_user_id=twitch_user_id,
twitch_user_name=twitch_user_name, twitch_user_name=twitch_user_name,
@@ -93,6 +93,7 @@ class RewardCampaign:
async def try_to_log_reward(self, reward: EarnedReward): async def try_to_log_reward(self, reward: EarnedReward):
import discord import discord
if self.webhook: if self.webhook:
embed = await self._log_format_reward(reward) embed = await self._log_format_reward(reward)
if reward.log_messageid: if reward.log_messageid:
@@ -107,7 +108,9 @@ class RewardCampaign:
await reward.update(log_messageid=message.id) await reward.update(log_messageid=message.id)
except discord.HTTPException: except discord.HTTPException:
# Couldn't send, give up. # Couldn't send, give up.
logger.warning(f"Failed to log campaign reward {reward!r}", exc_info=True) logger.warning(
f"Failed to log campaign reward {reward!r}", exc_info=True
)
async def _log_format_reward(self, reward: EarnedReward): async def _log_format_reward(self, reward: EarnedReward):
""" """
@@ -118,51 +121,46 @@ class RewardCampaign:
embed = discord.Embed( embed = discord.Embed(
title=f"Reward #{reward.earned_id} in {self.row.campaign_name}", title=f"Reward #{reward.earned_id} in {self.row.campaign_name}",
) )
embed.description = f"> {reward.earned_from}"
# User Field # User Field
embed.add_field( embed.add_field(
name="User Information", name="User Information",
value=( value=(
f"Twitch user `{reward.twitch_user_name or 'Unknown'}` `(ID: {reward.twitch_user_id or 'Unknown'})`.\n" f"`{reward.twitch_user_name or 'Unknown'}`\n"
f"`ID: {reward.twitch_user_id or 'Unknown'}`.\n"
f"Internal ID `{reward.profileid}`" f"Internal ID `{reward.profileid}`"
), ),
inline=True inline=True,
) )
# Earning field # Earning field
embed.add_field( embed.add_field(
name="Reward Earned", name="Reward Earned",
value=( value=(f"{discord.utils.format_dt(reward.earned_at, 'F')}."),
f"Earned campaign reward at {discord.utils.format_dt(reward.earned_at, 'F')}.\n" inline=True,
f"Earned reason: '{reward.earned_from}'"
),
inline=True
) )
# Reference field # Reference field
embed.add_field( embed.add_field(
name="Reference", name="Reference",
value=reward.reference or "No Reference information saved.", value=reward.reference or "No Reference information saved.",
inline=False inline=False,
) )
# Modnote field # Modnote field
embed.add_field( embed.add_field(
name="Notes", name="Notes", value=reward.modnote or "No notes added", inline=False
value=reward.modnote or "No notes added",
inline=False
) )
# Fulfilled field # Fulfilled field
fluffed_emoji = "" if reward.fulfilled_at else "🔳"
if reward.fulfilled_at is not None: if reward.fulfilled_at is not None:
fluffed = f"Fluffed at {discord.utils.format_dt(reward.fulfilled_at, 'F')}" fluffed = f"{fluffed_emoji} Fluffed at {discord.utils.format_dt(reward.fulfilled_at, 'F')}"
if reward.fulfilled_note: if reward.fulfilled_note:
fluffed += '\n' + "Fluff Note: " + reward.fulfilled_note fluffed += "\n" + "Fluff Note: " + reward.fulfilled_note
else: else:
fluffed = "Not yet fluffed" fluffed = f"{fluffed_emoji} Not yet fluffed"
embed.add_field( embed.add_field(name="Fulfilled", value=fluffed)
name="Fulfilled",
value=fluffed
)
embed.set_footer(text="Last Updated") embed.set_footer(text="Last Updated")
embed.timestamp = utc_now() embed.timestamp = utc_now()
@@ -173,7 +171,9 @@ class RewardCampaign:
class CampaignRegistry: class CampaignRegistry:
VERSION = CampaignData.VERSION VERSION = CampaignData.VERSION
def __init__(self, data: CampaignData, session: aiohttp.ClientSession | None = None): def __init__(
self, data: CampaignData, session: aiohttp.ClientSession | None = None
):
self.data = data self.data = data
# TODO: Actually pass in a session # TODO: Actually pass in a session
self._session = aiohttp.ClientSession() self._session = aiohttp.ClientSession()
@@ -204,14 +204,16 @@ class CampaignRegistry:
if active is not None: if active is not None:
active_condition = ( active_condition = (
Campaign.started_at != NULL and Campaign.completed_at == NULL (Campaign.started_at != NULL) & (Campaign.completed_at == NULL)
) )
if active: if active:
condition = condition and active_condition condition = condition & active_condition
else: else:
condition = condition and ~active_condition condition = condition & ~active_condition
rows = await Campaign.fetch_where(condition) rows = await Campaign.fetch_where(
condition,
)
campaigns = [RewardCampaign(row, session=self._session) for row in rows] campaigns = [RewardCampaign(row, session=self._session) for row in rows]
return campaigns return campaigns
@@ -238,27 +240,18 @@ class CampaignRegistry:
return camp return camp
async def create_campaign( async def create_campaign(
self, self, cid: int, campaign_name: str, **kwargs
cid: int,
campaign_name: str,
**kwargs
) -> RewardCampaign: ) -> RewardCampaign:
""" """
Create a new campaign. Create a new campaign.
The name must be unique (ignoring case) to facilitate easy lookup. The name must be unique (ignoring case) to facilitate easy lookup.
""" """
row = await Campaign.create( row = await Campaign.create(
communityid=cid, communityid=cid, campaign_name=campaign_name, **kwargs
campaign_name=campaign_name,
**kwargs
) )
return RewardCampaign(row, session=self._session) return RewardCampaign(row, session=self._session)
async def update_campaign( async def update_campaign(self, campaign_id: int, **kwargs) -> RewardCampaign:
self,
campaign_id: int,
**kwargs
) -> RewardCampaign:
""" """
Update a campaign with the given data args. Update a campaign with the given data args.
+51 -14
View File
@@ -123,7 +123,9 @@ class CampaignCog(LionCog):
return return
# Open campaign UI # Open campaign UI
widget = CampaignDashboard(bot=self.bot, campaign=campaign, callerid=ctx.author.id) widget = CampaignDashboard(
bot=self.bot, campaign=campaign, callerid=ctx.author.id
)
await widget.run(ctx.interaction) await widget.run(ctx.interaction)
await widget.wait() await widget.wait()
@@ -179,7 +181,7 @@ class CampaignCog(LionCog):
# If logging webhook is given, check that it works # If logging webhook is given, check that it works
if logging_webhook is not None: if logging_webhook is not None:
webhook = ThreadedWebhook.from_url(logging_webhook) webhook = ThreadedWebhook.from_url(logging_webhook, client=self.bot)
try: try:
await webhook.test_webhook() await webhook.test_webhook()
except (discord.HTTPException, discord.Forbidden): except (discord.HTTPException, discord.Forbidden):
@@ -202,7 +204,9 @@ class CampaignCog(LionCog):
f"Setup and started your reward campaign {campaign.row.campaign_name}! Good luck" f"Setup and started your reward campaign {campaign.row.campaign_name}! Good luck"
) )
# Open campaign UI # Open campaign UI
widget = CampaignDashboard(bot=self.bot, campaign=campaign, callerid=ctx.author.id) widget = CampaignDashboard(
bot=self.bot, campaign=campaign, callerid=ctx.author.id
)
await widget.run(ctx.interaction) await widget.run(ctx.interaction)
await widget.wait() await widget.wait()
@@ -278,10 +282,7 @@ class CampaignCog(LionCog):
) )
# Ack creation # Ack creation
await ctx.reply( await ctx.reply("Updated your campaign, good luck!", ephemeral=True)
"Updated your campaign, good luck!",
ephemeral=True
)
campaign_configure_cmd.autocomplete("campaign_name")(_campaign_acmpl) campaign_configure_cmd.autocomplete("campaign_name")(_campaign_acmpl)
@@ -322,9 +323,19 @@ class CampaignCog(LionCog):
) )
@appcmds.describe( @appcmds.describe(
rewardid="Earned reward to edit", rewardid="Earned reward to edit",
fulfilled="Whether this reward has been completed or not.",
reference="Reference information for this reward, e.g. image or message URL",
notes="Any additional notes",
) )
@appcmds.rename(rewardid="reward") @appcmds.rename(rewardid="reward")
async def campaign_editreward_cmd(self, ctx: LionContext, rewardid: str): async def campaign_editreward_cmd(
self,
ctx: LionContext,
rewardid: str,
fulfilled: Optional[bool] = None,
reference: Optional[str] = None,
notes: Optional[str] = None
):
# For this we'll just open the reward editor # For this we'll just open the reward editor
# Will need to identify the reward with autocomplete. Can use the reward id directly.. # Will need to identify the reward with autocomplete. Can use the reward id directly..
if not rewardid.isdigit(): if not rewardid.isdigit():
@@ -352,11 +363,31 @@ class CampaignCog(LionCog):
) )
return return
# Okay, this is our reward, and author has permission to modify it. Spin the modal. # Okay, this is our reward, and author has permission to modify it.
modal = RewardEditor.from_reward(reward)
currently_fluffed = reward.fulfilled_at is not None currently_fluffed = reward.fulfilled_at is not None
update_args = {}
if fulfilled is not None:
if fulfilled and not currently_fluffed:
# Reward has been fluffed
update_args["fulfilled_at"] = utc_now()
elif currently_fluffed and not fulfilled:
# Reward has been unfluffed
update_args["fulfilled_at"] = None
if reference is not None and reference != reward.reference:
update_args["reference"] = reference
if notes is not None and notes != reward.modnote:
update_args["modnote"] = notes
if update_args:
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
await campaign.update_reward(reward.earned_id, **update_args)
await ctx.interaction.followup.send(
content="Reward updated!"
)
else:
modal = RewardEditor.from_reward(reward)
@modal.submit_callback() @modal.submit_callback()
async def on_editor_submit(interaction: discord.Interaction): async def on_editor_submit(interaction: discord.Interaction):
update_args = {} update_args = {}
@@ -368,11 +399,11 @@ class CampaignCog(LionCog):
# Reward has been unfluffed # Reward has been unfluffed
update_args["fulfilled_at"] = None update_args["fulfilled_at"] = None
new_ref_value = modal.reference.value or None new_ref_value = modal.reference.component.value or None
if new_ref_value != reward.reference: if new_ref_value != reward.reference:
update_args["reference"] = new_ref_value update_args["reference"] = new_ref_value
new_notes_value = modal.notes.value or None new_notes_value = modal.notes.component.value or None
if new_notes_value != reward.modnote: if new_notes_value != reward.modnote:
update_args["modnote"] = new_notes_value update_args["modnote"] = new_notes_value
@@ -385,6 +416,8 @@ class CampaignCog(LionCog):
else: else:
await interaction.response.defer(thinking=False) await interaction.response.defer(thinking=False)
await ctx.interaction.response.send_modal(modal)
def _reward_acmpl_format( def _reward_acmpl_format(
self, campaign: RewardCampaign, reward: EarnedReward self, campaign: RewardCampaign, reward: EarnedReward
) -> str: ) -> str:
@@ -393,7 +426,11 @@ class CampaignCog(LionCog):
#100: username in campaign #100: username in campaign
""" """
... return "#{rewardid}: {username} in {cname}".format(
rewardid=reward.earned_id,
username=reward.twitch_user_name or reward.twitch_user_id or "Unknown",
cname=campaign.row.campaign_name,
)
@campaign_editreward_cmd.autocomplete("rewardid") @campaign_editreward_cmd.autocomplete("rewardid")
async def _reward_acmpl( async def _reward_acmpl(
+21 -9
View File
@@ -100,31 +100,43 @@ class CampaignDashboard(MessageUI):
table = { table = {
"Created at": discord.utils.format_dt(campaign.row.created_at, "F"), "Created at": discord.utils.format_dt(campaign.row.created_at, "F"),
"Started at": started_at, "Started at": started_at,
"Moderator Role": f"<&@{campaign.row.moderator_role_id}>" if campaign.row.moderator_role_id else "*Not Set*", "Moderator Role": f"<@&{campaign.row.moderator_role_id}>"
"Logging webhook": "*Set Up*" if campaign.row.logging_webhook_url else "*Not Set*", 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 Earned": str(rewards_earned),
"Rewards Cap": str(reward_cap), "Rewards Cap": str(reward_cap),
} }
if campaign.row.completed_at is not None: if campaign.row.completed_at is not None:
table['Finished at'] = discord.utils.format_dt(campaign.row.completed_at, "F") table["Finished at"] = discord.utils.format_dt(
prop_table = '\n'.join(tabulate(*table.items())) campaign.row.completed_at, "F"
)
prop_table = "\n".join(tabulate(*table.items()))
embed.description = f"{description}\n\n{prop_table}" embed.description = f"{description}\n\n{prop_table}"
# Brief summary of rewards in columns. 12 per column. Empty titles? # Brief summary of rewards in columns. 12 per column. Empty titles?
rewardrows = [] rewardrows = []
for reward in all_rewards: for reward in all_rewards:
name = reward.twitch_user_name or str(reward.twitch_user_id) or str(reward.profileid) name = (
reward.twitch_user_name
or str(reward.twitch_user_id)
or str(reward.profileid)
)
fluffed = reward.fulfilled_at is not None fluffed = reward.fulfilled_at is not None
fluffed_emoji = '' if fluffed else '🔳' fluffed_emoji = "" if fluffed else "🔳"
rewardrows.append(f"{fluffed_emoji} {name}") rewardrows.append(f"{fluffed_emoji} {name}")
blocks = ['\n'.join(rewardrows[i:i+12]) for i in range(0, len(rewardrows), 12)] blocks = [
"\n".join(rewardrows[i : i + 12]) for i in range(0, len(rewardrows), 12)
]
embed.add_field( embed.add_field(
name="Rewards Summary", name="Rewards Summary",
value=blocks[0] if blocks else 'No Rewards Earned', value=blocks[0] if blocks else "No Rewards Earned",
inline=True inline=True,
) )
for block in blocks[1:]: for block in blocks[1:]:
embed.add_field( embed.add_field(
+47 -46
View File
@@ -29,15 +29,13 @@ class RewardEditor(FastModal):
# Title is the reward we are editing # Title is the reward we are editing
# Block of text with dates and user info # Block of text with dates and user info
blurb = discord.ui.TextDisplay(content='placeholder') blurb = discord.ui.TextDisplay(content="placeholder")
# Fulfilled is a checkbox, and supports a fulfilled note # Fulfilled is a checkbox, and supports a fulfilled note
flufbox = discord.ui.Label( flufbox = discord.ui.Label(
text="Fulfilled", text="Fulfilled",
description="Whether this reward has been completed", description="Whether this reward has been completed",
component=discord.ui.Checkbox( component=discord.ui.Checkbox(default=False),
default=False
)
) )
# Reference and further notes need to be editable, # Reference and further notes need to be editable,
@@ -47,6 +45,7 @@ class RewardEditor(FastModal):
description="Reference URL or other information", description="Reference URL or other information",
component=discord.ui.TextInput( component=discord.ui.TextInput(
style=discord.TextStyle.long, style=discord.TextStyle.long,
required=False,
), ),
) )
notes = discord.ui.Label( notes = discord.ui.Label(
@@ -54,6 +53,7 @@ class RewardEditor(FastModal):
description="Further notes for this user/reward", description="Further notes for this user/reward",
component=discord.ui.TextInput( component=discord.ui.TextInput(
style=discord.TextStyle.long, style=discord.TextStyle.long,
required=False,
), ),
) )
@@ -64,19 +64,18 @@ class RewardEditor(FastModal):
# blurb # blurb
table = { table = {
'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,
} }
prop_table = '\n'.join(tabulate(*table.items())) prop_table = "\n".join(tabulate(*table.items()))
self.blurb.content = prop_table self.blurb.content = prop_table
# Fulfilled # Fulfilled
self.flufbox.component.default = (reward.fulfilled_at is not None) self.flufbox.component.default = reward.fulfilled_at is not None
# Notes # Notes
self.reference.component.default = reward.reference or '' self.reference.component.default = reward.reference or ""
self.notes.component.default = reward.modnote or '' self.notes.component.default = reward.modnote or ""
return self return self
@@ -134,7 +133,7 @@ class RewardList(MessageUI):
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) currently_fluffed = reward.fulfilled_at is not None
@modal.submit_callback() @modal.submit_callback()
async def on_editor_submit(interaction: discord.Interaction): async def on_editor_submit(interaction: discord.Interaction):
@@ -142,18 +141,18 @@ class RewardList(MessageUI):
if modal.flufbox.component.value and not currently_fluffed: if modal.flufbox.component.value and not currently_fluffed:
# Reward has been fluffed # Reward has been fluffed
update_args['fulfilled_at'] = utc_now() update_args["fulfilled_at"] = utc_now()
elif currently_fluffed and not modal.flufbox.component.value: elif currently_fluffed and not modal.flufbox.component.value:
# Reward has been unfluffed # Reward has been unfluffed
update_args['fulfilled_at'] = None update_args["fulfilled_at"] = None
new_ref_value = modal.reference.value or None new_ref_value = modal.reference.component.value or None
if new_ref_value != reward.reference: if new_ref_value != reward.reference:
update_args['reference'] = new_ref_value update_args["reference"] = new_ref_value
new_notes_value = modal.notes.value or None new_notes_value = modal.notes.component.value or None
if new_notes_value != reward.modnote: if new_notes_value != reward.modnote:
update_args['modnote'] = new_notes_value update_args["modnote"] = new_notes_value
if update_args: if update_args:
await interaction.response.defer(thinking=True, ephemeral=True) await interaction.response.defer(thinking=True, ephemeral=True)
@@ -162,7 +161,6 @@ class RewardList(MessageUI):
else: else:
await interaction.response.defer(thinking=False) await interaction.response.defer(thinking=False)
await selection.response.send_modal(modal) await selection.response.send_modal(modal)
await self.refresh() await self.refresh()
@@ -173,14 +171,10 @@ class RewardList(MessageUI):
menu = self.reward_menu menu = self.reward_menu
rewards = self.page rewards = self.page
if rewards: if rewards:
menu.options = [ menu.options = [self._format_reward_option(r) for r in rewards]
self._format_reward_option(r) for r in rewards
]
menu.disabled = False menu.disabled = False
else: else:
menu.options = [ menu.options = [SelectOption(label="DUMMY")]
SelectOption(label='DUMMY')
]
menu.disabled = True menu.disabled = True
# Meta buttons # Meta buttons
@@ -214,10 +208,9 @@ class RewardList(MessageUI):
""" """
# Title is Reward #n earned by twitch_username # Title is Reward #n earned by twitch_username
# Reward, Earned at, Earned by, Fulfilled at (not fulfilled/date), Ref, Added Notes # Reward, Earned at, Earned by, Fulfilled at (not fulfilled/date), Ref, Added Notes
name = f"Reward #{reward.earned_id} earned by {reward.twitch_user_name or reward.twitch_user_id}"
if reward.fulfilled_at is not None: if reward.fulfilled_at is not None:
fat = discord.utils.format_dt(reward.fulfilled_at, 'F') fat = discord.utils.format_dt(reward.fulfilled_at, "F")
if reward.fulfilled_note is not None: if reward.fulfilled_note is not None:
fluf = f"{fat} ({reward.fulfilled_note})" fluf = f"{fat} ({reward.fulfilled_note})"
else: else:
@@ -225,17 +218,25 @@ class RewardList(MessageUI):
else: else:
fluf = "*Not Fulfilled*" fluf = "*Not Fulfilled*"
table = { fluffed_emoji = "" if reward.fulfilled_at else "🔳"
'Reward': "Plus Campaign Sketch", earned = discord.utils.format_dt(reward.earned_at, "d")
'Earned At': discord.utils.format_dt(reward.earned_at, 'F'),
'Earned From': reward.earned_from,
'Fulfilled At': fluf,
'Reference': reward.reference or "*No reference set*",
'Further notes': reward.modnote or "*No notes*",
}
prop_table = '\n'.join(tabulate(*table.items()))
return (name, prop_table) name = f"{fluffed_emoji} #{reward.earned_id} earned by {reward.twitch_user_name or reward.twitch_user_id} at {earned}"
table = {
# "Reward": "Plus Campaign Sketch",
# "Earned From": reward.earned_from,
# "Fulfilled At": fluf,
"Reference": reward.reference or "*No reference set*",
"Further notes": reward.modnote or "*No notes*",
}
prop_table = "\n".join(tabulate(*table.items()))
value = '\n'.join((
"> {reason}",
"{table}",
)).format(reason=reward.earned_from, table=prop_table)
return (name, value)
def _format_reward_option(self, reward: EarnedReward) -> SelectOption: def _format_reward_option(self, reward: EarnedReward) -> SelectOption:
""" """
@@ -246,14 +247,10 @@ class RewardList(MessageUI):
name = f"Reward #{reward.earned_id} earned by {reward.twitch_user_name or reward.twitch_user_id}" name = f"Reward #{reward.earned_id} earned by {reward.twitch_user_name or reward.twitch_user_id}"
value = reward.earned_id value = reward.earned_id
return SelectOption( return SelectOption(label=name, value=str(value))
label=name, value=str(value)
)
async def refresh_layout(self): async def refresh_layout(self):
to_refresh = ( to_refresh = (self.reward_menu_refresh(),)
self.reward_menu_refresh(),
)
await asyncio.gather(*to_refresh) await asyncio.gather(*to_refresh)
if self.page_count <= 1: if self.page_count <= 1:
@@ -264,13 +261,18 @@ class RewardList(MessageUI):
self.next_page_button.disabled = False self.next_page_button.disabled = False
self.set_layout( self.set_layout(
(self.prev_page_button, self.refresh_button, self.next_page_button, self.quit_button), (
self.prev_page_button,
self.refresh_button,
self.next_page_button,
self.quit_button,
),
(self.reward_menu,), (self.reward_menu,),
) )
async def make_message(self) -> MessageArgs: async def make_message(self) -> MessageArgs:
embed = discord.Embed(title=f"{self.campaign.row.campaign_name} Reward List") embed = discord.Embed(title=f"{self.campaign.row.campaign_name} Reward List")
embed.set_footer(text='Last Update') embed.set_footer(text="Last Update")
embed.timestamp = utc_now() embed.timestamp = utc_now()
campaign = self.campaign campaign = self.campaign
@@ -284,7 +286,6 @@ class RewardList(MessageUI):
embed.description = description embed.description = description
for reward in self.page: for reward in self.page:
name, value = self._format_reward_section(reward) name, value = self._format_reward_section(reward)
embed.add_field(name=name, value=value, inline=False) embed.add_field(name=name, value=value, inline=False)
+9 -2
View File
@@ -17,12 +17,14 @@ def LOWER(expression: Expression) -> RawExpr:
return RawExpr(final_expr, final_values) return RawExpr(final_expr, final_values)
def asexpr(value: Any) -> RawExpr: def asexpr(value: Any) -> RawExpr:
""" """
Turn a value into an expression. Turn a value into an expression.
""" """
return RawExpr(sql.Placeholder(), (value,)) return RawExpr(sql.Placeholder(), (value,))
async def fire_and_forget(awaitable, do_in=1, ignorable=(discord.HTTPException)): async def fire_and_forget(awaitable, do_in=1, ignorable=(discord.HTTPException)):
await asyncio.sleep(do_in) await asyncio.sleep(do_in)
try: try:
@@ -33,6 +35,7 @@ async def fire_and_forget(awaitable, do_in=1, ignorable=(discord.HTTPException))
# TODO: Log unexpected exceptions # TODO: Log unexpected exceptions
pass pass
class ThreadedWebhook(discord.Webhook): class ThreadedWebhook(discord.Webhook):
__slots__ = ("thread_id",) __slots__ = ("thread_id",)
@@ -55,10 +58,14 @@ class ThreadedWebhook(discord.Webhook):
kwargs.setdefault("thread", discord.Object(self.thread_id)) kwargs.setdefault("thread", discord.Object(self.thread_id))
return await super().send(*args, **kwargs) return await super().send(*args, **kwargs)
async def edit_message(self, *args, **kwargs):
if self.thread_id is not None:
kwargs.setdefault("thread", discord.Object(self.thread_id))
return await super().edit_message(*args, **kwargs)
async def test_webhook(self): async def test_webhook(self):
embed = discord.Embed( embed = discord.Embed(
title="Testing", title="Testing", description="Testing logging webhook, feel free to delete."
description="Testing logging webhook, feel free to delete."
) )
result = await self.send(embed=embed, wait=True, silent=True) result = await self.send(embed=embed, wait=True, silent=True)
asyncio.create_task(fire_and_forget(result.delete())) asyncio.create_task(fire_and_forget(result.delete()))
+40 -8
View File
@@ -6,6 +6,7 @@ from twitchio.ext import commands as cmds
from meta import Bot from meta import Bot
from meta.logger import log_wrap from meta.logger import log_wrap
from meta.sockets import Channel, register_channel
from utils.lib import utc_now from utils.lib import utc_now
from . import logger from . import logger
@@ -23,7 +24,7 @@ class CampaignComponent(cmds.Component):
self.campaigns = CampaignRegistry(self.data) self.campaigns = CampaignRegistry(self.data)
self.channel = CampaignChannel(self.bot.profiles.profiles, self.campaigns) self.channel = CampaignChannel(self.bot.profiles.profiles, self.campaigns)
register_channel("Campaign", self.channel) register_channel(self.channel.name, self.channel)
# ----- API ----- # ----- API -----
async def component_load(self): async def component_load(self):
@@ -73,7 +74,7 @@ class CampaignComponent(cmds.Component):
# Add a reward to the database with the correct info. # Add a reward to the database with the correct info.
await campaign.add_reward( await campaign.add_reward(
profileid=event_row["profileid"], profileid=event_row["profileid"],
earned_reason=f"(SUB) User subscribed for {duration} months at tier {tier}", earned_from=f"(SUB) User subscribed for {duration} months at tier {tier}",
event_id=event_row["event_id"], event_id=event_row["event_id"],
twitch_user_id=data["chatter_user_id"], twitch_user_id=data["chatter_user_id"],
twitch_user_name=data["chatter_user_name"], twitch_user_name=data["chatter_user_name"],
@@ -82,6 +83,34 @@ class CampaignComponent(cmds.Component):
# TODO: Webhook logging maybe.. (do this in campaign) # TODO: Webhook logging maybe.. (do this in campaign)
# Or just general logging. (also do this in campaign, but we can do again here) # Or just general logging. (also do this in campaign, but we can do again here)
# @cmds.Component.listener()
async def event_message(self, payload):
if not payload.text.startswith("%reward%"):
return
community = await self.bot.profiles.fetch_community(payload.broadcaster)
cid = community.communityid
profile = await self.bot.profiles.fetch_profile(payload.chatter)
pid = profile.profileid
campaigns = await self.campaigns.fetch_campaigns(cid)
for campaign in campaigns:
reward_progress = len(await campaign.get_rewards())
if (
campaign.row.target_rewards is None
or reward_progress < campaign.row.target_rewards
):
# Add a reward to the database with the correct info.
await campaign.add_reward(
profileid=pid,
earned_from=f"(MSG) User messaged in chat: {payload.text}",
event_id=None,
twitch_user_id=payload.chatter.id,
twitch_user_name=payload.chatter.name,
)
await self.dispatch_update(campaign)
@cmds.Component.listener() @cmds.Component.listener()
async def event_safe_event_chat_notice_resub(self, payload): async def event_safe_event_chat_notice_resub(self, payload):
# Check that the end of the sub is past the threshold by adding duration to the previous sub # Check that the end of the sub is past the threshold by adding duration to the previous sub
@@ -136,9 +165,9 @@ class CampaignComponent(cmds.Component):
reward_cap = campaign.row.target_rewards reward_cap = campaign.row.target_rewards
if reward_cap is not None: if reward_cap is not None:
response = f"{name}: {given} rewards earned out of {cap}!" response = "{name}: {given} rewards earned out of {cap}!"
else: else:
response = f"{name}: {given} rewards earned so far!" response = "{name}: {given} rewards earned so far!"
formatted = response.format( formatted = response.format(
name=campaign.row.campaign_name, name=campaign.row.campaign_name,
given=rewards_earned, given=rewards_earned,
@@ -180,7 +209,9 @@ class CampaignComponent(cmds.Component):
return return
# Now can create # Now can create
campaign = await self.campaigns.create_campaign(cid, name, rewards) campaign = await self.campaigns.create_campaign(
cid, name, target_rewards=rewards
)
# Also start the campaign # Also start the campaign
await campaign.start() await campaign.start()
@@ -230,9 +261,9 @@ class CampaignComponent(cmds.Component):
reward_cap = campaign.row.target_rewards reward_cap = campaign.row.target_rewards
if reward_cap is not None: if reward_cap is not None:
response = f"Completed {name}: {given} rewards earned out of {cap}!" response = f"Completed {name}: {rewards_earned} rewards earned out of {reward_cap}!"
else: else:
response = f"Completed {name}: {given} rewards earned so far!" response = f"Completed {name}: {rewards_earned} rewards earned so far!"
formatted = response.format( formatted = response.format(
name=campaign.row.campaign_name, name=campaign.row.campaign_name,
given=rewards_earned, given=rewards_earned,
@@ -246,7 +277,8 @@ class CampaignComponent(cmds.Component):
async def cmd_campaign_reward( async def cmd_campaign_reward(
self, self,
ctx: cmds.Context, ctx: cmds.Context,
user: twitchio.PartialUser, user: twitchio.User,
*,
reward_reason: str, reward_reason: str,
): ):
"""Manually reward a target user, adjusting their points.""" """Manually reward a target user, adjusting their points."""