diff --git a/plugin/twitch/component.py b/plugin/twitch/component.py index 90dbbff..011524f 100644 --- a/plugin/twitch/component.py +++ b/plugin/twitch/component.py @@ -1,9 +1,13 @@ from typing import Optional import asyncio +import datetime as dt +from datetime import datetime, timedelta import twitchio from twitchio.ext import commands as cmds +from dateutil.relativedelta import relativedelta +from data.queries import JOINTYPE, ORDER from meta import Bot from meta.logger import log_wrap from meta.sockets import Channel, register_channel @@ -42,7 +46,7 @@ class CampaignComponent(cmds.Component): # ------ Event Handlers ----- @cmds.Component.listener() - async def event_safe_event_chat_notice_sub(self, payload): + async def event_safe_chat_notice_sub(self, payload): """ This is our most important notice for this event. @@ -61,9 +65,15 @@ class CampaignComponent(cmds.Component): if detail_row["tier"] == 3000 and detail_row["duration_months"] >= 3: # Check if there is an ongoing campaign campaigns = await self.campaigns.fetch_campaigns(event_row["communityid"]) + pid = event_row["profileid"] for campaign in campaigns: - reward_progress = len(await campaign.get_rewards()) + rewards = await campaign.get_rewards() + reward_progress = len(rewards) + + existing = any(reward.profileid == pid for reward in rewards) + if existing: + continue if ( campaign.row.target_rewards is None @@ -145,7 +155,7 @@ class CampaignComponent(cmds.Component): await self.dispatch_update(campaign) @cmds.Component.listener() - async def event_safe_event_chat_notice_resub(self, payload): + async def event_safe_chat_notice_resub(self, payload): # Check that the end of the sub is past the threshold by adding duration to the previous sub # Threshold being now + 3 months, by calendar date. # Or even if the sub ends on at least the month that is past the three month region. @@ -154,7 +164,72 @@ class CampaignComponent(cmds.Component): # # TODO: This logic should be done for completeness, but will postpone for now # This could be done in subscription_message as well, but the # notice has slightly more self-contained metadata. - ... + event_row, detail_row, data = payload + + # Check the tier and duration of the subscription. + # Continue if tier = 3000 and duration is at least 3 months + if detail_row["tier"] == 3000 and detail_row["duration_months"] >= 3: + # Check if there is an ongoing campaign + campaigns = await self.campaigns.fetch_campaigns(event_row["communityid"]) + pid = event_row["profileid"] + + # Get user's last sub date + last_sub_date = await self.get_sub_start( + channelid=event_row["channel_id"], + userid=event_row["user_id"], + tier=3000, + ) + if last_sub_date is None: + logger.error(f"T3 rsub with no history: {event_row!r}") + return + forecast_end = last_sub_date + relativedelta( + months=detail_row["duration_months"] + ) + if forecast_end < dt.datetime(2026, 11, 1, tzinfo=dt.UTC): + logger.warning(f"T3 sub with forecast end too short: {event_row!r}") + return + + for campaign in campaigns: + rewards = await campaign.get_rewards() + reward_progress = len(rewards) + + existing = any(reward.profileid == pid for reward in rewards) + if existing: + continue + + 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=event_row["profileid"], + earned_from=f"(RESUB NOTICE) {data.system_message}", + event_id=event_row["event_id"], + twitch_user_id=data.chatter.id, + twitch_user_name=data.chatter.name, + ) + await self.dispatch_update(campaign) + else: + logger.info(f"Campaigns ignoring sub notice event: {event_row}") + + async def get_sub_start(self, channelid, userid, tier=3000): + event_tracker = self.bot.get_component("TrackerComponent") + query = event_tracker.data.events.select_where( + channel_id=channelid, + user_id=userid, + tier=tier, + ) + query.join("subscribe_events", using=("event_id",), join_type=JOINTYPE.INNER) + query.order_by("created_at", direction=ORDER.DESC) + query.select("created_at") + query.limit(1) + query.with_no_adapter() + rows = await query + if rows: + return rows[0]["created_at"] + else: + return None @cmds.Component.listener() async def event_safe_subscription(self, payload): ... @@ -163,7 +238,7 @@ class CampaignComponent(cmds.Component): async def event_safe_subscription_message(self, payload): ... # ------ Commands ----- - @cmds.group(name="campaign", invoke_fallback=True) + @cmds.group(name="campaign", aliases=["ppp"], invoke_fallback=True) async def group_campaign(self, ctx: cmds.Context, name: Optional[str] = None): """Status of the current or named campaign.""" community = await self.bot.profiles.fetch_community(ctx.broadcaster) @@ -196,12 +271,19 @@ class CampaignComponent(cmds.Component): rewards_earned = len(all_rewards) reward_cap = campaign.row.target_rewards - if reward_cap is not None: - response = "{name}: {given} rewards earned out of {cap}!" - else: - response = "{name}: {given} rewards earned so far!" + response = ( + "event details can be found here: {event_link} " + "there are {remaining} sketch slots remaining! ♡♡ " + ) + + # if reward_cap is not None: + # response = "{name}: {given} rewards earned out of {cap}!" + # else: + # response = "{name}: {given} rewards earned so far!" formatted = response.format( name=campaign.row.campaign_name, + event_link="https://lilac.thewisewolf.dev/provides/partner_plus_poster2.jpg", + remaining=reward_cap - rewards_earned, given=rewards_earned, cap=reward_cap, ) @@ -344,3 +426,51 @@ class CampaignComponent(cmds.Component): f"Successfully added campaign reward to {user.mention}'s account." ) await self.dispatch_update(campaign) + + @group_campaign.command(name="test") + @cmds.is_moderator() + async def cmd_campaign_test( + self, + ctx: cmds.Context, + user: twitchio.User, + channel: twitchio.User, + tier: int = 3000, + ): + community = await self.bot.profiles.fetch_community(ctx.broadcaster) + cid = community.communityid + profile = await self.bot.profiles.fetch_profile(user) + name = user.display_name or profile.nickname or "Unknown" + pid = profile.profileid + campaigns = await self.campaigns.fetch_campaigns(cid) + campaign = campaigns[0] + + # Get user's last sub date + last_sub_date = await self.get_sub_start( + channelid=channel.id, + userid=user.id, + tier=tier, + ) + lines = [] + if last_sub_date is None: + subline = f"Last {tier} sub not found" + lines.append(subline) + else: + subline = f"Last {tier} sub at {last_sub_date}" + lines.append(subline) + forecast_end = last_sub_date + relativedelta(months=3) + lines.append(f"Three months after: {forecast_end}") + + if forecast_end > dt.datetime(2026, 11, 1, tzinfo=dt.UTC): + lines.append("Which is after the end of the event") + else: + lines.append("Which is not after the end of the event") + + rewards = await campaign.get_rewards() + + existing = any(reward.profileid == pid for reward in rewards) + if existing: + lines.append(f"This user has already been rewarded.") + else: + lines.append(f"This user might yet earn a reward") + + await ctx.reply("; ".join(lines))