From 9203a795e32c97b8fed623b2fc76543f0ddc8398 Mon Sep 17 00:00:00 2001 From: Interitio Date: Wed, 29 Jul 2026 15:47:58 +0300 Subject: [PATCH] (campaign): Add reward webhook logging --- plugin/campaign.py | 124 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/plugin/campaign.py b/plugin/campaign.py index d74e914..28a3a90 100644 --- a/plugin/campaign.py +++ b/plugin/campaign.py @@ -6,22 +6,38 @@ from data import ORDER, Condition from data.conditions import NULL, condition from utils.lib import utc_now -from .lib import LOWER, asexpr +from .lib import LOWER, asexpr, ThreadedWebhook from .data import ( CampaignData, Campaign, EarnedReward, ) +from . import logger class RewardCampaign: def __init__(self, row: Campaign): self.row = row + self._webhook: ThreadedWebhook | None = None + self._cached_webhookurl: str | None = None + @property def is_active(self): return self.row.started_at is not None and self.row.completed_at is None + @property + def webhook(self): + if self._cached_webhookurl != self.row.logging_webhook_url: + url = self._cached_webhookurl = self.row.logging_webhook_url + if url is not None: + # TODO: I don't know if these needs a client + # Might be hard if so given we need to run this from Twitch client as well + self._webhook = ThreadedWebhook.from_url(url) + else: + self._webhook = None + return self._webhook + async def start(self): """Start the reward campaign.""" if self.row.started_at is not None: @@ -53,15 +69,103 @@ class RewardCampaign: twitch_user_name=twitch_user_name, earned_at=earned_at or utc_now(), ) + await self.try_to_log_reward(row) return row + async def update_reward(self, rewardid: int, **kwargs): + # In particular, log or update the logged message + if kwargs: + reward = await EarnedReward.fetch(rewardid) + if reward is None: + raise ValueError("Reward doesn't exist") + await reward.update(**kwargs) + await self.try_to_log_reward(reward) + async def get_rewards(self) -> list[EarnedReward]: rows = await EarnedReward.fetch_where( campaign_id=self.row.campaign_id ).order_by("earned_at", direction=ORDER.ASC) return rows + async def try_to_log_reward(self, reward: EarnedReward): + import discord + if self.webhook: + embed = await self._log_format_reward(reward) + if reward.log_messageid: + # Try and edit message + try: + await self.webhook.edit_message(reward.log_messageid, embed=embed) + except discord.HTTPException: + await reward.update(log_messageid=None) + if not reward.log_messageid: + try: + message = await self.webhook.send(embed=embed, wait=True) + await reward.update(log_messageid=message.id) + except discord.HTTPException: + # Couldn't send, give up. + logger.warning(f"Failed to log campaign reward {reward!r}", exc_info=True) + + async def _log_format_reward(self, reward: EarnedReward): + """ + Quick and ugly embed format for the webhook. + """ + import discord + + embed = discord.Embed( + title=f"Reward #{reward.earned_id} in {self.row.campaign_name}", + ) + # User Field + embed.add_field( + name="User Information", + value=( + f"Twitch user `{reward.twitch_user_name or 'Unknown'}` `(ID: {reward.twitch_user_id or 'Unknown'})`.\n" + f"Internal ID `{reward.profileid}`" + ), + inline=True + ) + + # Earning field + embed.add_field( + name="Reward Earned", + value=( + f"Earned campaign reward at {discord.utils.format_dt(reward.earned_at, 'F')}.\n" + f"Earned reason: '{reward.earned_from}'" + ), + inline=True + ) + + # Reference field + embed.add_field( + name="Reference", + value=reward.reference or "No Reference information saved.", + inline=False + ) + # Modnote field + embed.add_field( + name="Notes", + value=reward.modnote or "No notes added", + inline=False + ) + + # Fulfilled field + if reward.fulfilled_at is not None: + fluffed = f"Fluffed at {discord.utils.format_dt(reward.fulfilled_at, 'F')}" + if reward.fulfilled_note: + fluffed += '\n' + "Fluff Note: " + reward.fulfilled_note + else: + fluffed = "Not yet fluffed" + + embed.add_field( + name="Fulfilled", + value=fluffed + ) + + embed.set_footer(text="Last Updated") + embed.timestamp = utc_now() + + return embed + class CampaignRegistry: VERSION = CampaignData.VERSION @@ -132,7 +236,7 @@ class CampaignRegistry: self, cid: int, campaign_name: str, - target_rewards: Optional[int] = None, + **kwargs ) -> RewardCampaign: """ Create a new campaign. @@ -140,7 +244,21 @@ class CampaignRegistry: """ row = await Campaign.create( communityid=cid, - target_rewards=target_rewards, campaign_name=campaign_name, + **kwargs ) return RewardCampaign(row) + + async def update_campaign( + self, + campaign_id: int, + **kwargs + ) -> RewardCampaign: + """ + Update a campaign with the given data args. + + Registry handles this for caching and dispatch reasons. + """ + campaign = await Campaign.fetch(campaign_id) + await campaign.update(**kwargs) + return RewardCampaign(campaign)