Compare commits

15 Commits

Author SHA1 Message Date
df94877068 fix: Handle no timecap. 2025-07-31 06:59:39 +10:00
a52dcade56 Add timecap. 2025-07-31 06:55:21 +10:00
2c57d135c9 Add subscription messages to sub tracker. 2025-07-28 22:16:44 +10:00
295ab69fa3 Add fallback for load_tokens. 2025-07-28 14:53:11 +10:00
bf835e2529 Add subathon timer websocket. 2025-07-28 14:49:17 +10:00
03d50fbe92 Fix subathon string. 2025-07-28 14:11:02 +10:00
3cd243f3eb Fix goalcheck. 2025-07-28 14:10:14 +10:00
11b214b553 Add goals to status text. 2025-07-28 14:07:25 +10:00
122dcfca3e Add status text. 2025-07-28 14:05:51 +10:00
34d1af1144 Fix event name typo. 2025-07-28 13:58:25 +10:00
a598e56075 Fix event name typo. 2025-07-28 13:57:33 +10:00
aae305bd43 Fix sub tier rec. 2025-07-28 13:54:07 +10:00
af95977577 Fix profile fetching. 2025-07-28 13:51:34 +10:00
239b778f27 Add eventsub secret 2025-07-28 13:32:11 +10:00
6879e53fbf Add basic subathon impl. 2025-07-28 13:29:21 +10:00
8 changed files with 679 additions and 34 deletions

View File

@@ -271,4 +271,46 @@ CREATE TABLE raid_in_events(
);
-- }}}
-- Subathon timer data {{{
CREATE TABLE subathons(
subathon_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
communityid INTEGER NOT NULL REFERENCES communities(communityid),
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
initial_time INTEGER NOT NULL,
sub1_score NUMERIC NOT NULL DEFAULT 1,
sub2_score NUMERIC NOT NULL DEFAULT 2,
sub3_score NUMERIC NOT NULL DEFAULT 6,
bit_score NUMERIC NOT NULL,
score_time NUMERIC NOT NULL,
duration INTEGER NOT NULL DEFAULT 0,
ended_at TIMESTAMPTZ
);
CREATE TABLE running_subathons(
subathon_id INTEGER PRIMARY KEY REFERENCES subathons(subathon_id),
last_started TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE subathon_contributions(
contribution_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
subathon_id INTEGER NOT NULL REFERENCES subathons(subathon_id),
profileid INTEGER REFERENCES user_profiles(profileid),
score NUMERIC NOT NULL,
event_id INTEGER REFERENCES events(event_id)
);
CREATE TABLE subathon_goals(
goal_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
subathon_id INTEGER NOT NULL REFERENCES subathons(subathon_id),
required_score NUMERIC NOT NULL,
description TEXT NOT NULL,
notified BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- }}}
-- vim: set fdm=marker:

View File

@@ -1,11 +1,13 @@
import asyncio
import logging
import websockets
from twitchio.web import AiohttpAdapter
from meta import CrocBot, conf, setup_main_logger, args
from data import Database
from constants import DATA_VERSION
import sockets
from modules import setup
@@ -26,19 +28,21 @@ async def main():
host=conf.bot.get('wshost', None),
port=conf.bot.getint('wsport', None),
domain=conf.bot.get('wsdomain', None),
eventsub_secret=conf.bot.get('eventsub_secret', None)
)
bot = CrocBot(
config=conf,
dbconn=db,
adapter=adapter,
setup=setup,
)
async with websockets.serve(sockets.root_handler, '', conf.wserver.getint('port')):
bot = CrocBot(
config=conf,
dbconn=db,
adapter=adapter,
setup=setup,
)
try:
await bot.start()
finally:
await bot.close()
try:
await bot.start()
finally:
await bot.close()
def _main():

View File

@@ -6,7 +6,7 @@ from twitchio.ext import commands
from twitchio import Scopes, eventsub
from data import Database
from datamodels import BotData, UserAuth, BotChannel
from datamodels import BotData, Communities, UserAuth, BotChannel, UserProfile
from .config import Conf
@@ -39,6 +39,22 @@ class CrocBot(commands.Bot):
# logger.info(f"Logged in as {self.nick}. User id is {self.user_id}")
logger.info("Logged in as %s", self.bot_id)
async def profile_fetch(self, twitchid, name):
profiles = await UserProfile.fetch_where(twitchid=twitchid)
if not profiles:
profile = await UserProfile.create(twitchid=twitchid, name=name)
else:
profile = profiles[0]
return profile
async def community_fetch(self, twitchid, name):
profiles = await Communities.fetch_where(twitchid=twitchid)
if not profiles:
profile = await Communities.create(twitchid=twitchid, name=name)
else:
profile = profiles[0]
return profile
async def setup_hook(self):
await self.data.init()
@@ -159,4 +175,7 @@ class CrocBot(commands.Bot):
async def load_tokens(self, path: str | None = None):
for row in await UserAuth.fetch_where():
await self.add_token(row.token, row.refresh_token)
try:
await self.add_token(row.token, row.refresh_token)
except Exception:
logger.exception(f"Failed to add token for {row}")

View File

@@ -1,3 +1,5 @@
async def setup(bot):
from . import tracker
from . import subathons
await tracker.setup(bot)
await subathons.setup(bot)

View File

@@ -0,0 +1,7 @@
import logging
logger = logging.getLogger()
async def setup(bot):
from .component import SubathonComponent
await bot.add_component(SubathonComponent(bot))

View File

@@ -0,0 +1,493 @@
from datetime import datetime, timedelta
from typing import Optional
import random
import twitchio
from twitchio import PartialUser, Scopes, eventsub
from twitchio.ext import commands as cmds
from datamodels import BotChannel, Communities, UserProfile
from meta import CrocBot
from utils.lib import utc_now, strfdelta
from sockets import Channel, register_channel
from . import logger
from .data import SubathonData, Subathon, RunningSubathon, SubathonContribution, SubathonGoal
class TimerChannel(Channel):
name = 'Timer'
def __init__(self, cog: 'SubathonComponent', **kwargs):
super().__init__(**kwargs)
self.cog = cog
self.communityid = 1
async def on_connection(self, websocket, event):
await super().on_connection(websocket, event)
await self.send_set(
**await self.get_args_for(self.communityid),
websocket=websocket,
)
async def send_updates(self):
await self.send_set(
**await self.get_args_for(self.communityid),
)
async def get_args_for(self, channelid):
active = await self.cog.get_active_subathon(channelid)
if active is not None:
ending = utc_now() + timedelta(seconds=await active.get_remaining())
return {
'end_at': ending,
'running': active.running
}
else:
return {
'end_at': utc_now,
'running': False,
}
async def send_set(self, end_at, running, websocket=None):
await self.send_event({
'type': "DO",
'method': 'setTimer',
'args': {
'end_at': end_at.isoformat(),
'running': running,
}
}, websocket=websocket)
class ActiveSubathon:
def __init__(self, subathondata: Subathon, runningdata: RunningSubathon | None):
self.subathondata = subathondata
self.runningdata = runningdata
@property
def running(self):
return self.runningdata is not None
async def check_cap(self):
if not (cap := self.subathondata.timecap):
return False
else:
score = await self.get_score()
time_earned = self.get_score_time(score)
total_time = self.subathondata.initial_time + time_earned
return total_time >= cap
async def pause(self):
if not self.running:
raise ValueError("This subathon is not running!")
assert self.runningdata is not None
new_duration = self.get_duration()
await self.subathondata.update(duration=new_duration)
await self.runningdata.delete()
async def resume(self):
if self.running:
raise ValueError("This subathon is already running!")
self.runningdata = await RunningSubathon.create(subathon_id=self.subathondata.subathon_id)
async def get_score(self) -> float:
rows = await SubathonContribution.fetch_where(subathon_id=self.subathondata.subathon_id)
return sum(row.score for row in rows)
def get_score_time(self, score: float) -> int:
# Get time contributed by this score
return int(score * self.subathondata.score_time)
def get_duration(self) -> int:
# Get the duration of this subathon so far
duration = self.subathondata.duration
if self.runningdata:
now = utc_now()
added = int( (now - self.runningdata.last_started).total_seconds() )
duration += added
return duration
async def get_remaining(self) -> int:
# Get the remaining time
score = await self.get_score()
time_earned = self.get_score_time(score)
total_time = self.subathondata.initial_time + time_earned
if cap := self.subathondata.timecap:
total_time = min(total_time, cap)
return total_time - self.get_duration()
async def add_contribution(self, profileid: int | None, score: float, event_id: int | None) -> SubathonContribution:
return await SubathonContribution.create(
subathon_id=self.subathondata.subathon_id,
profileid=profileid, score=score, event_id=event_id
)
async def get_goals(self) -> list[SubathonGoal]:
goals = await SubathonGoal.fetch_where(subathon_id=self.subathondata.subathon_id).order_by('required_score')
return goals
class SubathonComponent(cmds.Component):
def __init__(self, bot: CrocBot):
self.bot = bot
self.data = bot.dbconn.load_registry(SubathonData())
self.channel = TimerChannel(self)
register_channel('SubTimer', self.channel)
# ----- API -----
async def component_load(self):
# TODO: Setup the websocket
await self.data.init()
async def component_teardown(self):
pass
# ----- Methods -----
async def get_community(self, twitchid: str, name: str | None) -> Communities:
return await self.bot.community_fetch(twitchid=twitchid, name=name)
async def get_profile(self, twitchid: str, name: str | None) -> UserProfile:
return await self.bot.profile_fetch(twitchid=twitchid, name=name)
async def get_active_subathon(self, communityid: int) -> ActiveSubathon | None:
rows = await Subathon.fetch_where(communityid=communityid, ended_at=None)
if rows:
subathondata = rows[0]
running = await RunningSubathon.fetch(subathondata.subathon_id)
subba = ActiveSubathon(subathondata, running)
return subba
async def goalcheck(self, active: ActiveSubathon, channel: PartialUser):
goals = await active.get_goals()
score = await active.get_score()
for i, goal in enumerate(goals):
if not goal.notified and goal.required_score <= score:
# Goal completed, notify channel
await channel.send_message(
f"We have reached Goal #{i+1}: {goal.description} !! Thank you everyone for your support <3",
sender=self.bot.bot_id,
)
await goal.update(notified=True)
# ----- Event Handlers -----
@cmds.Component.listener()
async def event_safe_bits_use(self, payload):
event_row, detail_row, bits_payload = payload
if (active := await self.get_active_subathon(event_row['communityid'])) is not None:
# In an active subathon
pid = event_row['profileid']
uid = event_row['user_id']
score = detail_row['bits'] * active.subathondata.bit_score
await active.add_contribution(pid, score, event_row['event_id'])
# Send message to channel
sec = active.get_score_time(score)
added_min = int(sec // 60)
if added_min > 0:
added = f"{added_min} minutes"
else:
added = f"{sec} seconds"
name = bits_payload.user.name
pl = 's' if bits_payload.bits != 1 else ''
contrib_str = f"{name} contributed {score} bit{pl}"
if not await active.check_cap():
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
else:
contrib_str += " towards our studython! Thank you holono1Heart"
await bits_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
)
await self.channel.send_updates()
await self.goalcheck(active, bits_payload.broadcaster)
# Check goals
@cmds.Component.listener()
async def event_safe_subscription(self, payload):
event_row, detail_row, sub_payload = payload
if sub_payload.gift:
# Ignore gifted here
return
if (active := await self.get_active_subathon(event_row['communityid'])) is not None:
data = active.subathondata
# In an active subathon
pid = event_row['profileid']
tier = int(sub_payload.tier)
if tier == 1000:
mult = data.sub1_score
elif tier == 2000:
mult = data.sub2_score
elif tier == 3000:
mult = data.sub3_score
else:
raise ValueError(f"Unknown sub tier {sub_payload.tier}")
score = mult * 1
await active.add_contribution(pid, score, event_row['event_id'])
# Send message to channel
added_min = int(active.get_score_time(score) // 60)
added = f"{added_min} minutes"
name = sub_payload.user.name
pl = 's' if score > 1 else ''
contrib_str = f"{name} contributed {score} sub{pl}"
if not await active.check_cap():
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
else:
contrib_str += " towards our studython! Thank you holono1Heart"
await sub_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
)
await self.channel.send_updates()
# Check goals
await self.goalcheck(active, sub_payload.broadcaster)
@cmds.Component.listener()
async def event_safe_subscription_gift(self, payload):
event_row, detail_row, gift_payload = payload
if (active := await self.get_active_subathon(event_row['communityid'])) is not None:
data = active.subathondata
# In an active subathon
pid = event_row['profileid']
tier = int(gift_payload.tier)
if tier == 1000:
mult = data.sub1_score
elif tier == 2000:
mult = data.sub2_score
elif tier == 3000:
mult = data.sub3_score
else:
raise ValueError(f"Unknown sub tier {gift_payload.tier}")
score = mult * gift_payload.total
await active.add_contribution(pid, score, event_row['event_id'])
# Send message to channel
added_min = int(active.get_score_time(score) // 60)
added = f"{added_min} minutes"
name = gift_payload.user.name if gift_payload.user else 'Anonymous'
contrib_str = f"{name} contributed {score} subs"
if not await active.check_cap():
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
else:
contrib_str += " towards our studython! Thank you holono1Heart"
await gift_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
)
await self.channel.send_updates()
# Check goals
await self.goalcheck(active, gift_payload.broadcaster)
@cmds.Component.listener()
async def event_safe_subscription_message(self, payload):
event_row, detail_row, sub_payload = payload
if (active := await self.get_active_subathon(event_row['communityid'])) is not None:
data = active.subathondata
# In an active subathon
pid = event_row['profileid']
tier = int(sub_payload.tier)
if tier == 1000:
mult = data.sub1_score
elif tier == 2000:
mult = data.sub2_score
elif tier == 3000:
mult = data.sub3_score
else:
raise ValueError(f"Unknown sub tier {sub_payload.tier}")
score = mult * 1
await active.add_contribution(pid, score, event_row['event_id'])
# Send message to channel
added_min = int(active.get_score_time(score) // 60)
added = f"{added_min} minutes"
name = sub_payload.user.name
pl = 's' if score > 1 else ''
contrib_str = f"{name} contributed {score} sub{pl}"
if not await active.check_cap():
contrib_str += f" and added {added} to the timer! Thank you holono1Heart"
else:
contrib_str += " towards our studython! Thank you holono1Heart"
await sub_payload.broadcaster.send_message(
contrib_str,
sender=self.bot.bot_id
)
await self.channel.send_updates()
# Check goals
await self.goalcheck(active, sub_payload.broadcaster)
# end stream => Automatically pause the timer
@cmds.Component.listener()
async def event_stream_offline(self, payload: twitchio.StreamOffline):
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
if active.running:
await active.pause()
await payload.broadcaster.send_message(
"Paused the subathon timer because the stream went offline!",
sender=self.bot.bot_id
)
await self.channel.send_updates()
# ----- Commands -----
@cmds.group(name='subathon', aliases=['studython'], invoke_fallback=True)
async def group_subathon(self, ctx: cmds.Context):
# TODO: Status
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
score = await active.get_score()
goals = await active.get_goals()
total_goals = len(goals)
donegoals = len([goal for goal in goals if score >= goal.required_score])
goalstr = f"{donegoals}/{total_goals} goals achieved"
secs = await active.get_remaining()
remaining = strfdelta(timedelta(seconds=secs))
secs = active.get_duration()
duration = strfdelta(timedelta(seconds=secs))
text = (
f"Subathon running for {duration}! {score} (equivalent) subscriptions recieved, {goalstr}, and {remaining} left on the timer"
)
await ctx.reply(text)
else:
await ctx.reply("No active subathon running!")
# subathon start
@group_subathon.command(name='setup')
async def cmd_setup(self, ctx: cmds.Context, initial_hours: float, sub1: float, sub2: float, sub3: float, bit: float, timescore: int, timecap: Optional[int]=None):
if ctx.broadcaster:
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
await ctx.reply("There is already an active subathon running! Use !subathon stop to stop it!")
return
initial_time = initial_hours * 60 * 60
active = await Subathon.create(
communityid=cid,
initial_time=initial_time,
sub1_score=sub1,
sub2_score=sub2,
sub3_score=sub3,
bit_score=bit,
score_time=timescore,
timecap=timecap
)
await ctx.reply("Setup a new subathon! Use !subathon resume to get the timer running.")
await self.channel.send_updates()
# subathon stop
@group_subathon.command(name='stop')
async def cmd_stop(self, ctx: cmds.Context):
if ctx.broadcaster:
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
if active.running:
await active.pause()
await self.channel.send_updates()
await active.subathondata.update(ended_at=utc_now())
total = await active.get_score()
dursecs = active.get_duration()
dur = strfdelta(timedelta(seconds=dursecs))
await ctx.reply(
f"Subathon complete after {dur} with a total of {total} subs, congratulations!"
)
else:
await ctx.reply("No active subathon to stop.")
# subathon pause
@group_subathon.command(name='pause')
async def cmd_pause(self, ctx: cmds.Context):
if ctx.broadcaster or ctx.author.moderator:
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
if active.running:
await active.pause()
await ctx.reply("Subathon timer paused!")
await self.channel.send_updates()
else:
await ctx.reply("Subathon timer already paused!")
else:
await ctx.reply("No active subathon to pause")
# subathon resume
@group_subathon.command(name='resume')
async def cmd_resume(self, ctx: cmds.Context):
if ctx.broadcaster or ctx.author.moderator:
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
if not active.running:
await active.resume()
await ctx.reply("Subathon timer resumed!")
await self.channel.send_updates()
else:
await ctx.reply("Subathon timer already running!")
else:
await ctx.reply("No active subathon to resume")
@cmds.group(name='goals', invoke_fallback=True)
async def group_goals(self, ctx: cmds.Context):
# List the goals
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
goals = await active.get_goals()
goalstrs = []
for i, goal in enumerate(goals, start=1):
line = f"{goal.required_score} subs: {goal.description}"
goalstrs.append(line)
if goals:
text = ', '.join(goalstrs)
await ctx.reply(f"Subathon Goals! -- {text}")
else:
await ctx.reply("No goals have been configured!")
else:
await ctx.reply("No active subathon running!")
@group_goals.command(name='add')
async def cmd_add(self, ctx: cmds.Context, required: int, *, description: str):
if ctx.broadcaster or ctx.author.moderator:
community = await self.bot.community_fetch(twitchid=ctx.broadcaster.id, name=ctx.broadcaster.name)
cid = community.communityid
if (active := await self.get_active_subathon(cid)) is not None:
await SubathonGoal.create(
subathon_id=active.subathondata.subathon_id,
required_score=required,
description=description,
)
await ctx.reply("Goal added!")
else:
await ctx.reply("No active subathon to goal!")

View File

@@ -0,0 +1,58 @@
from data import Registry, RowModel, Table
from data.columns import String, Timestamp, Integer, Bool
# User contributed {} subs and added {}:{} to the timer! Thank you :love:
# We have reached goal #5: Karaoke !! Thank you everyone for your support <3
class Subathon(RowModel):
_tablename_ = 'subathons'
_cache_ = {}
subathon_id = Integer(primary=True)
communityid = Integer()
started_at = Timestamp()
initial_time = Integer() # Initial number of seconds
sub1_score = Integer()
sub2_score = Integer()
sub3_score = Integer()
bit_score = Integer()
score_time = Integer() # Conversion factor score to seconds
duration = Integer()
ended_at = Timestamp()
timecap = Integer() # Maximum subathon duration in seconds
class RunningSubathon(RowModel):
_tablename_ = 'running_subathons'
_cache_ = {}
subathon_id = Integer(primary=True)
last_started = Timestamp()
class SubathonContribution(RowModel):
_tablename_ = 'subathon_contributions'
_cache_ = {}
contribution_id = Integer(primary=True)
subathon_id = Integer()
profileid = Integer()
score = Integer()
event_id = Integer()
# TODO: Should add a created timestamp here, since not all contributions have event ids
class SubathonGoal(RowModel):
_tablename_ = 'subathon_goals'
_cache_ = {}
goal_id = Integer(primary=True)
subathon_id = Integer()
required_score = Integer()
description = String()
notified = Bool()
created_at = Timestamp()
class SubathonData(Registry):
subathons = Subathon.table
running_subathons = RunningSubathon.table
subathon_contributions = SubathonContribution.table
subathon_goals = SubathonGoal.table

View File

@@ -131,9 +131,9 @@ class TrackerComponent(cmds.Component):
async def event_custom_redemption_add(self, payload: twitchio.ChannelPointsRedemptionAdd):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
@@ -160,9 +160,9 @@ class TrackerComponent(cmds.Component):
async def event_custom_redemption_update(self, payload: twitchio.ChannelPointsRedemptionUpdate):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
@@ -188,9 +188,9 @@ class TrackerComponent(cmds.Component):
async def event_follow(self, payload: twitchio.ChannelFollow):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
@@ -214,9 +214,9 @@ class TrackerComponent(cmds.Component):
async def event_bits_use(self, payload: twitchio.ChannelBitsUse):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
@@ -235,14 +235,15 @@ class TrackerComponent(cmds.Component):
message=payload.text,
powerup_type=payload.power_up.type if payload.power_up else None
)
self.bot.safe_dispatch('bits_use', payload=(event_row, detail_row, payload))
@cmds.Component.listener()
async def event_subscription(self, payload: twitchio.ChannelSubscribe):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
@@ -256,18 +257,19 @@ class TrackerComponent(cmds.Component):
)
detail_row = await self.data.subscribe_events.insert(
event_id=event_row['event_id'],
tier=payload.tier,
tier=int(payload.tier),
gifted=payload.gift,
)
self.bot.safe_dispatch('subscription', payload=(event_row, detail_row, payload))
@cmds.Component.listener()
async def event_subscription_gift(self, payload: twitchio.ChannelSubscriptionGift):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
if payload.user is not None:
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
@@ -283,17 +285,18 @@ class TrackerComponent(cmds.Component):
)
detail_row = await self.data.gift_events.insert(
event_id=event_row['event_id'],
tier=payload.tier,
tier=int(payload.tier),
gifted_count=payload.total,
)
self.bot.safe_dispatch('subscription_gift', payload=(event_row, detail_row, payload))
@cmds.Component.listener()
async def event_subscription_message(self, payload: twitchio.ChannelSubscriptionMessage):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
@@ -307,18 +310,19 @@ class TrackerComponent(cmds.Component):
)
detail_row = await self.data.subscribe_message_events.insert(
event_id=event_row['event_id'],
tier=payload.tier,
tier=int(payload.tier),
duration_months=payload.months,
cumulative_months=payload.cumulative_months,
streak_months=payload.streak_months,
message=payload.text,
)
self.bot.safe_dispatch('subscription_message', payload=(event_row, detail_row, payload))
@cmds.Component.listener()
async def event_stream_online(self, payload: twitchio.StreamOnline):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
event_row = await self.data.events.insert(
@@ -333,6 +337,22 @@ class TrackerComponent(cmds.Component):
stream_type=payload.type,
)
@cmds.Component.listener()
async def event_stream_offline(self, payload: twitchio.StreamOffline):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
event_row = await self.data.events.insert(
event_type='stream_offline',
communityid=cid,
channel_id=payload.broadcaster.id,
)
detail_row = await self.data.stream_offline_events.insert(
event_id=event_row['event_id'],
)
@cmds.Component.listener()
async def event_raid(self, payload: twitchio.ChannelRaid):
await self._event_raid_out(
@@ -349,7 +369,7 @@ class TrackerComponent(cmds.Component):
async def _event_raid_out(self, broadcaster: PartialUser, to_broadcaster: PartialUser, viewer_count: int):
tracked = await TrackingChannel.fetch(broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=broadcaster.id, name=broadcaster.name)
community = await self.bot.community_fetch(twitchid=broadcaster.id, name=broadcaster.name)
cid = community.communityid
event_row = await self.data.events.insert(
@@ -367,7 +387,7 @@ class TrackerComponent(cmds.Component):
async def _event_raid_in(self, broadcaster: PartialUser, from_broadcaster: PartialUser, viewer_count: int):
tracked = await TrackingChannel.fetch(broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=broadcaster.id, name=broadcaster.name)
community = await self.bot.community_fetch(twitchid=broadcaster.id, name=broadcaster.name)
cid = community.communityid
event_row = await self.data.events.insert(
@@ -386,9 +406,9 @@ class TrackerComponent(cmds.Component):
async def event_message(self, payload: twitchio.ChatMessage):
tracked = await TrackingChannel.fetch(payload.broadcaster.id)
if tracked and tracked.joined:
community = await Communities.fetch_or_create(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
community = await self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await UserProfile.fetch_or_create(
profile = await self.bot.profile_fetch(
twitchid=payload.chatter.id, name=payload.chatter.name,
)
pid = profile.profileid