diff --git a/data/awesome-v1.sql b/data/awesome-v1.sql deleted file mode 100644 index 6fb0d8e..0000000 --- a/data/awesome-v1.sql +++ /dev/null @@ -1,11 +0,0 @@ -BEGIN; - --- Version dependency checks -DO $$ - ASSERT current_module_version('PROFILES') = 1, 'Dependency version mismatch: PROFILES'; -$$ LANGUAGE plpgsql; - --- Plugin version history -INSERT INTO version_history (component, from_version, to_version, author) VALUES ('AWESOME_PLUGIN', 0, 1, 'Initial Creation'); - -COMMIT; diff --git a/data/pluscampaign-v1.sql b/data/pluscampaign-v1.sql new file mode 100644 index 0000000..ad0a4ce --- /dev/null +++ b/data/pluscampaign-v1.sql @@ -0,0 +1,41 @@ +BEGIN; + +-- Version dependency checks +DO $$ + ASSERT current_module_version('PROFILES') = 1, 'Dependency version mismatch: PROFILES'; + ASSERT current_module_version('EVENT_TRACKER') = 2, 'Dependency version mismatch: EVENT_TRACKER'; +$$ LANGUAGE plpgsql; + +-- Plugin version history +INSERT INTO version_history (component, from_version, to_version, author) VALUES ('PLUSCAMPAIGN', 0, 1, 'Initial Creation'); + + +CREATE TABLE campaigns( + campaign_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + communityid INTEGER NOT NULL REFERENCES communities(communityid) ON DELETE CASCADE ON UPDATE CASCADE, + target_rewards INTEGER NOT NULL, + campaign_name TEXT NOT NULL, + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + _timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE TABLE campaign_rewards_earned( + earned_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + campaign_id INTEGER NOT NULL REFERENCES campaigns ON DELETE CASCADE ON UPDATE CASCADE, + profileid INTEGER NOT NULL REFERENCES user_profiles(profileid) ON DELETE CASCADE ON UPDATE CASCADE, + event_id INTEGER REFERENCES events ON DELETE SET NULL ON UPDATE CASCADE, + twitch_user_id TEXT, + twitch_user_name TEXT, + fulfilled_at TIMESTAMPTZ, + fulfilled_note TEXT, + earned_at TIMESTAMPTZ NOT NULL, + modnote TEXT, + earned_from TEXT NOT NULL, + _timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- TODO: Possibly reference table for form destination? + +COMMIT; diff --git a/plugin/awesome.py b/plugin/awesome.py deleted file mode 100644 index 3784517..0000000 --- a/plugin/awesome.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Optional -from .data import ( - AwesomeData, - AwesomeTable, -) - - -class AwesomeRegistry: - VERSION = AwesomeData.VERSION - - def __init__(self, data: AwesomeData): - self.data = data - - async def init(self): - await self.data.init() diff --git a/plugin/campaign.py b/plugin/campaign.py new file mode 100644 index 0000000..f7f39e5 --- /dev/null +++ b/plugin/campaign.py @@ -0,0 +1,16 @@ +from typing import Optional +from .data import ( + CampaignData, + Campaign, + EarnedReward, +) + + +class CampaignRegistry: + VERSION = CampaignData.VERSION + + def __init__(self, data: CampaignData): + self.data = data + + async def init(self): + await self.data.init() diff --git a/plugin/channel.py b/plugin/channel.py new file mode 100644 index 0000000..171e555 --- /dev/null +++ b/plugin/channel.py @@ -0,0 +1,124 @@ +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 + + +# ISO formatted timestamp +ISOTimestamp: TypeAlias = str + + +async def prepare_campaign( + profiler: ProfilesRegistry, campaign: Campaign +): + return {} + + +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) + + # TODO: Prepare campaign for sending + if campaign: + payload = await prepare_campaign(self.profiler, campaign) + await self.send_campaign_update(cid, payload, websocket) + else: + await self.send_no_campaign(cid, websocket) + + async def send_sample(self, websocket): + import json + import random + with open("sample-payload.json") as f: + payload = json.load(f) + ending = utc_now() + timedelta(seconds=10) + payload['args']['end_at'] = ending.isoformat() + 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) diff --git a/plugin/data.py b/plugin/data.py index 9c3e7b6..62462e5 100644 --- a/plugin/data.py +++ b/plugin/data.py @@ -2,16 +2,48 @@ from data import Registry, RowModel, Table from data.columns import String, Timestamp, Integer, Bool -class AwesomeTable(RowModel): - _tablename_ = "awesome_table" - _cache_ = {} +class Campaign(RowModel): + _tablename_ = "campaigns" + # _cache_ = {} + + campaign_id = Integer(primary=True) + communityid = Integer() + target_rewards = Integer() + campaign_name = String() + started_at = Timestamp() + completed_at = Timestamp() + + created_at = Timestamp() + _timestamp = Timestamp() + +class EarnedReward(RowModel): + _tablename_ = "campaign_rewards_earned" + # _cache_ = {} + + earned_id = Integer(primary=True) + campaign_id = Integer() + profileid = Integer() + + event_id = Integer() + twitch_user_id = String() + twitch_user_name = String() + + fulfilled_at = Timestamp() + fulfilled_note = String() + + earned_at = Timestamp() + earned_from = String() + modnote = String() - userid = String(primary=True) _timestamp = Timestamp() -class AwesomeData(Registry): - VERSION = ("AWESOME", 1) - AwesomeTable = AwesomeTable - awesome_table = AwesomeTable.table +class CampaignData(Registry): + VERSION = ("CAMPAIGN", 1) + + Campaign = Campaign + campaigns = Campaign.table + + EarnedReward = EarnedReward + campaign_rewards_earned = EarnedReward.table diff --git a/plugin/discord/__init__.py b/plugin/discord/__init__.py index 0dd58d9..bd59eb9 100644 --- a/plugin/discord/__init__.py +++ b/plugin/discord/__init__.py @@ -2,6 +2,6 @@ from .. import logger async def setup(bot): - from .cog import AwesomeCog + from .cog import CampaignCog - await bot.add_cog(AwesomeCog(bot)) + await bot.add_cog(CampaignCog(bot)) diff --git a/plugin/discord/cog.py b/plugin/discord/cog.py index 0263343..dcc9c61 100644 --- a/plugin/discord/cog.py +++ b/plugin/discord/cog.py @@ -9,18 +9,18 @@ from meta import LionBot, LionCog, LionContext from meta.logger import log_wrap from utils.lib import utc_now -from ..data import AwesomeData -from ..awesome import AwesomeRegistry +from ..data import CampaignData +from ..campaigns import CampaignRegistry -class AwesomeCog(LionCog): +class CampaignCog(LionCog): def __init__(self, bot: LionBot): self.bot = bot - self.data = bot.db.load_registry(AwesomeData()) - self.profiles = AwesomeRegistry(self.data) + self.data = bot.db.load_registry(CampaignData()) + self.campaigns = CampaignRegistry(self.data) async def cog_load(self): await self.data.init() await self.bot.version_check(*self.data.VERSION) - await self.profiles.init() + await self.campaigns.init() diff --git a/plugin/twitch/__init__.py b/plugin/twitch/__init__.py index a3601a2..144216a 100644 --- a/plugin/twitch/__init__.py +++ b/plugin/twitch/__init__.py @@ -6,6 +6,6 @@ if TYPE_CHECKING: async def setup(bot: "Bot"): - from .component import AwesomeComponent + from .component import CampaignComponent - await bot.add_component(AwesomeComponent(bot)) + await bot.add_component(CampaignComponent(bot)) diff --git a/plugin/twitch/component.py b/plugin/twitch/component.py index 3e97334..32ce4ce 100644 --- a/plugin/twitch/component.py +++ b/plugin/twitch/component.py @@ -10,22 +10,22 @@ from utils.lib import utc_now from . import logger -from ..data import AwesomeData -from ..awesome import AwesomeRegistry +from ..data import CampaignData +from ..campaign import CampaignRegistry -class AwesomeComponent(cmds.Component): +class CampaignComponent(cmds.Component): def __init__(self, bot: Bot): self.bot = bot - self.data = bot.dbconn.load_registry(AwesomeData()) - self.awesome = AwesomeRegistry(self.data) + self.data = bot.dbconn.load_registry(CampaignData()) + self.campaigns = CampaignRegistry(self.data) # ----- API ----- async def component_load(self): await self.data.init() await self.bot.version_check(*self.data.VERSION) - await self.profiles.init() + await self.campaigns.init() async def component_teardown(self): pass