from typing import Optional, TypeAlias, TypedDict import json from collections import defaultdict from datetime import datetime, timedelta from dataclasses import dataclass from data.queries import JOINTYPE, ORDER from meta.sockets import Channel from utils.lib import utc_now from modules.profiles.profiles.profiles import ProfilesRegistry from . import logger from .data import ( Campaign, CampaignData, EarnedReward, ) from .campaign import CampaignRegistry, RewardCampaign # ISO formatted timestamp ISOTimestamp: TypeAlias = str class CampaignPayload(TypedDict): communityid: int campaign_id: int campaign_name: str started_at: ISOTimestamp | None completed_at: ISOTimestamp | None reward_progress: int reward_cap: int | None async def prepare_campaign(campaign: RewardCampaign) -> CampaignPayload: reward_progress = len(await campaign.get_rewards()) return CampaignPayload( communityid=campaign.row.communityid, campaign_id=campaign.row.campaign_id, campaign_name=campaign.row.campaign_name, started_at=campaign.row.started_at.isoformat() if campaign.row.started_at else None, completed_at=campaign.row.completed_at.isoformat() if campaign.row.completed_at else None, reward_progress=reward_progress, reward_cap=campaign.row.target_rewards, ) class CampaignChannel(Channel): name = "PlusCampaign" def __init__( self, profiler: ProfilesRegistry, campaigns: CampaignRegistry, **kwargs ): super().__init__(**kwargs) self.profiler: ProfilesRegistry = profiler self.campaigns: CampaignRegistry = campaigns # Map of communities to webhooks listening for this community self.communities = defaultdict( set ) # Map of communityid -> listening websockets async def on_connection(self, websocket, event): if not (cidstr := event.get("community")): logger.warning("Campaign connection missing communityid") await super().on_connection(websocket, event) await self.send_sample(websocket=websocket) return elif not cidstr.isdigit(): raise ValueError("Community id provided is not an integer") cid = int(cidstr) community = await self.profiler.get_community(cid) if community is None: raise ValueError("Unknown community provided.") await super().on_connection(websocket, event) self.communities[cid].add(websocket) # Fetch active campaign for this cid and send if it exists active = await self.campaigns.fetch_campaigns(cid) if active: # TODO: Presumes only one active campaign campaign = active[0] payload = await prepare_campaign(campaign) await self.send_campaign_update(cid, payload, websocket) else: await self.send_no_campaign(cid, websocket) async def send_sample(self, websocket): import random payload = CampaignPayload( communityid=1, campaign_id=1, campaign_name="PartnerPlus2026", started_at=utc_now().isoformat(), completed_at=None, reward_progress=random.randint(0, 105), reward_cap=150, ) await self.send_event(payload, websocket=websocket) async def del_connection(self, websocket): for wss in self.communities.values(): wss.discard(websocket) await super().del_connection(websocket) async def send_campaign_update(self, communityid: int, payload, websocket=None): for ws in (websocket,) if websocket else self.communities[communityid]: await self.send_event( { "type": "DO", "method": "setTimer", "args": payload, }, websocket=ws, ) async def send_campaign_ended(self, communityid: int, payload, websocket=None): for ws in (websocket,) if websocket else self.communities[communityid]: await self.send_event( { "type": "DO", "method": "endTimer", "args": payload, }, websocket=ws, ) async def send_no_campaign(self, communityid: int, websocket=None): for ws in (websocket,) if websocket else self.communities[communityid]: await self.send_event( { "type": "DO", "method": "noTimer", "args": {}, }, websocket=ws, ) async def send_event(self, event, **kwargs): logger.info(f"Sending websocket event: {json.dumps(event, indent=1)}") await super().send_event(event, **kwargs)