generated from HoloTech/holotech-plugin-template
Rough Campaign module skeleton.
This commit is contained in:
@@ -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;
|
|
||||||
@@ -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;
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
+40
-8
@@ -2,16 +2,48 @@ from data import Registry, RowModel, Table
|
|||||||
from data.columns import String, Timestamp, Integer, Bool
|
from data.columns import String, Timestamp, Integer, Bool
|
||||||
|
|
||||||
|
|
||||||
class AwesomeTable(RowModel):
|
class Campaign(RowModel):
|
||||||
_tablename_ = "awesome_table"
|
_tablename_ = "campaigns"
|
||||||
_cache_ = {}
|
# _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()
|
_timestamp = Timestamp()
|
||||||
|
|
||||||
|
|
||||||
class AwesomeData(Registry):
|
|
||||||
VERSION = ("AWESOME", 1)
|
|
||||||
|
|
||||||
AwesomeTable = AwesomeTable
|
class CampaignData(Registry):
|
||||||
awesome_table = AwesomeTable.table
|
VERSION = ("CAMPAIGN", 1)
|
||||||
|
|
||||||
|
Campaign = Campaign
|
||||||
|
campaigns = Campaign.table
|
||||||
|
|
||||||
|
EarnedReward = EarnedReward
|
||||||
|
campaign_rewards_earned = EarnedReward.table
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ from .. import logger
|
|||||||
|
|
||||||
|
|
||||||
async def setup(bot):
|
async def setup(bot):
|
||||||
from .cog import AwesomeCog
|
from .cog import CampaignCog
|
||||||
|
|
||||||
await bot.add_cog(AwesomeCog(bot))
|
await bot.add_cog(CampaignCog(bot))
|
||||||
|
|||||||
@@ -9,18 +9,18 @@ from meta import LionBot, LionCog, LionContext
|
|||||||
from meta.logger import log_wrap
|
from meta.logger import log_wrap
|
||||||
from utils.lib import utc_now
|
from utils.lib import utc_now
|
||||||
|
|
||||||
from ..data import AwesomeData
|
from ..data import CampaignData
|
||||||
from ..awesome import AwesomeRegistry
|
from ..campaigns import CampaignRegistry
|
||||||
|
|
||||||
|
|
||||||
class AwesomeCog(LionCog):
|
class CampaignCog(LionCog):
|
||||||
def __init__(self, bot: LionBot):
|
def __init__(self, bot: LionBot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
self.data = bot.db.load_registry(AwesomeData())
|
self.data = bot.db.load_registry(CampaignData())
|
||||||
self.profiles = AwesomeRegistry(self.data)
|
self.campaigns = CampaignRegistry(self.data)
|
||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
await self.bot.version_check(*self.data.VERSION)
|
await self.bot.version_check(*self.data.VERSION)
|
||||||
await self.profiles.init()
|
await self.campaigns.init()
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
async def setup(bot: "Bot"):
|
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))
|
||||||
|
|||||||
@@ -10,22 +10,22 @@ from utils.lib import utc_now
|
|||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
|
||||||
from ..data import AwesomeData
|
from ..data import CampaignData
|
||||||
from ..awesome import AwesomeRegistry
|
from ..campaign import CampaignRegistry
|
||||||
|
|
||||||
|
|
||||||
class AwesomeComponent(cmds.Component):
|
class CampaignComponent(cmds.Component):
|
||||||
def __init__(self, bot: Bot):
|
def __init__(self, bot: Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
self.data = bot.dbconn.load_registry(AwesomeData())
|
self.data = bot.dbconn.load_registry(CampaignData())
|
||||||
self.awesome = AwesomeRegistry(self.data)
|
self.campaigns = CampaignRegistry(self.data)
|
||||||
|
|
||||||
# ----- API -----
|
# ----- API -----
|
||||||
async def component_load(self):
|
async def component_load(self):
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
await self.bot.version_check(*self.data.VERSION)
|
await self.bot.version_check(*self.data.VERSION)
|
||||||
await self.profiles.init()
|
await self.campaigns.init()
|
||||||
|
|
||||||
async def component_teardown(self):
|
async def component_teardown(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user