Initial Plugin Commit

This commit is contained in:
2025-08-01 00:41:24 +10:00
commit 2ea0658c73
8 changed files with 856 additions and 0 deletions

151
.gitignore vendored Normal file
View File

@@ -0,0 +1,151 @@
src/modules/test/*
pending-rewrite/
logs/*
notes/*
tmp/*
output/*
locales/domains
.idea/*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
config/**

0
README.md Normal file
View File

3
__init__.py Normal file
View File

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

178
data/schema.sql Normal file
View File

@@ -0,0 +1,178 @@
INSERT INTO version_history (component, from_version, to_version, author) VALUES ('EVENT_TRACKER', 0, 1, 'Initial Creation');
-- TODO: Assert profiles data version
-- Twitch tracked event data {{{
CREATE TABLE tracking_channels(
userid TEXT PRIMARY KEY REFERENCES bot_channels(userid) ON DELETE CASCADE,
joined BOOLEAN,
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TRIGGER tracking_channels_timestamp BEFORE UPDATE ON tracking_channels
FOR EACH ROW EXECUTE FUNCTION update_timestamp_column();
CREATE TABLE events(
event_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
event_type TEXT NOT NULL,
communityid INTEGER NOT NULL REFERENCES communities (communityid),
channel_id TEXT NOT NULL,
profileid INTEGER REFERENCES user_profiles (profileid),
user_id TEXT,
occurred_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (event_id, event_type)
);
CREATE TABLE follow_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'follow' CHECK (event_type = 'follow'),
follower_count INTEGER NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE bits_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'bits' CHECK (event_type = 'bits'),
bits INTEGER NOT NULL,
bits_type TEXT NOT NULL,
message TEXT,
powerup_type TEXT,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE subscribe_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'subscribe' CHECK (event_type = 'subscribe'),
tier INTEGER NOT NULL,
gifted BOOLEAN NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE gift_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'gift' CHECK (event_type = 'gift'),
tier INTEGER NOT NULL,
gifted_count INTEGER NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE subscribe_message_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'subscribe_message' CHECK (event_type = 'subscribe_message'),
tier INTEGER NOT NULL,
duration_months INTEGER NOT NULL,
cumulative_months INTEGER NOT NULL,
streak_months INTEGER,
message TEXT,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE cheer_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'cheer' CHECK (event_type = 'cheer'),
amount INTEGER NOT NULL,
message TEXT,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE redemption_add_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'redemption_add' CHECK (event_type = 'redemption_add'),
redeem_id TEXT NOT NULL,
redeem_title TEXT NOT NULL,
redeem_cost INTEGER NOT NULL,
redemption_id TEXT NOT NULL,
redemption_status TEXT NOT NULL,
message TEXT,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE redemption_update_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'redemption_update' CHECK (event_type = 'redemption_update'),
redeem_id TEXT NOT NULL,
redeem_title TEXT NOT NULL,
redeem_cost INTEGER NOT NULL,
redemption_id TEXT NOT NULL,
redemption_status TEXT NOT NULL,
redeemed_at TIMESTAMPTZ NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE poll_end_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'poll_end' CHECK (event_type = 'poll_end'),
poll_id TEXT NOT NULL,
poll_title TEXT NOT NULL,
poll_choices TEXT NOT NULL,
poll_started TIMESTAMPTZ NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE stream_online_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'stream_online' CHECK (event_type = 'stream_online'),
stream_id TEXT NOT NULL,
stream_type TEXT NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE stream_offline_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'stream_offline' CHECK (event_type = 'stream_offline'),
stream_id TEXT,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE channel_update_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'channel_update' CHECK (event_type = 'channel_update'),
title TEXT,
language TEXT,
category_id TEXT,
category_name TEXT,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE vip_add_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'vip_add' CHECK (event_type = 'vip_add'),
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE vip_remove_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'vip_remove' CHECK (event_type = 'vip_remove'),
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE message_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'message' CHECK (event_type = 'message'),
message_id TEXT NOT NULL,
message_type TEXT NOT NULL,
content TEXT,
source_channel_id TEXT,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE raid_out_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'raidout' CHECK (event_type = 'raidout'),
target_id TEXT NOT NULL,
target_name TEXT,
viewer_count INTEGER NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
CREATE TABLE raid_in_events(
event_id INTEGER PRIMARY KEY REFERENCES events (event_id),
event_type TEXT NOT NULL DEFAULT 'raidin' CHECK (event_type = 'raidin'),
source_id TEXT NOT NULL,
source_name TEXT,
viewer_count INTEGER NOT NULL,
FOREIGN KEY (event_id, event_type) REFERENCES events (event_id, event_type)
);
-- }}}

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
twitchio

7
tracker/__init__.py Normal file
View File

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

477
tracker/component.py Normal file
View File

@@ -0,0 +1,477 @@
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
from . import logger
from .data import EventData, TrackingChannel
class TrackerComponent(cmds.Component):
def __init__(self, bot: CrocBot):
self.bot = bot
self.data = bot.dbconn.load_registry(EventData())
print(self.__all_listeners__)
# One command, which sends join link to start tracking
# Utility for fetch_or_create community and profiles and setting names
# Then just a stack of event listeners.
# ----- API -----
async def component_load(self):
await self.data.init()
async def component_teardown(self):
pass
# ----- Methods -----
async def start_tracking(self, channel: TrackingChannel):
# TODO: Make sure that we aren't trying to make duplicate subscriptions here
logger.debug(
"Initialising event tracking for %s",
channel.userid
)
# Get associated auth scopes
rows = await self.bot.data.user_auth_scopes.select_where(userid=channel.userid)
scopes = Scopes([row['scope'] for row in rows])
# Build subscription payloads based on available scopes
subs = []
usersubs = []
subcls = []
if Scopes.channel_read_redemptions in scopes or Scopes.channel_manage_redemptions in scopes:
subcls.append(eventsub.ChannelPointsRedeemAddSubscription)
subcls.append(eventsub.ChannelPointsRedeemUpdateSubscription)
if Scopes.bits_read in scopes:
subcls.append(eventsub.ChannelBitsUseSubscription)
subcls.append(eventsub.ChannelCheerSubscription)
if Scopes.channel_read_subscriptions in scopes:
subcls.extend((
eventsub.ChannelSubscribeSubscription,
eventsub.ChannelSubscribeMessageSubscription,
eventsub.ChannelSubscriptionGiftSubscription,
))
if Scopes.channel_read_polls in scopes or Scopes.channel_manage_polls in scopes:
subcls.append(eventsub.ChannelPollEndSubscription)
if Scopes.channel_read_vips in scopes or Scopes.channel_manage_vips in scopes:
subcls.extend((
eventsub.ChannelVIPAddSubscription,
eventsub.ChannelVIPRemoveSubscription,
))
subcls.extend((
eventsub.StreamOnlineSubscription,
eventsub.StreamOfflineSubscription,
eventsub.ChannelUpdateSubscription,
))
for subbr in subcls:
subs.append(subbr(broadcaster_user_id=channel.userid))
# This isn't needed because joining the channel means we have these
# Including them for completeness though
# if Scopes.chat_read in scopes or Scopes.channel_bot in scopes:
# subs.append(
# eventsub.ChatMessageSubscription(
# broadcaster_user_id=channel.userid,
# user_id=self.bot.bot_id,
# )
# )
if Scopes.moderator_read_followers in scopes:
usersubs.append(
eventsub.ChannelFollowSubscription(
broadcaster_user_id=channel.userid,
moderator_user_id=channel.userid,
)
)
subs.extend((
eventsub.ChannelRaidSubscription(to_broadcaster_user_id=channel.userid),
eventsub.ChannelRaidSubscription(from_broadcaster_user_id=channel.userid),
))
responses = []
for sub in subs:
try:
if self.bot.using_webhooks:
resp = await self.bot.subscribe_webhook(sub)
else:
resp = await self.bot.subscribe_websocket(sub)
responses.append(resp)
except Exception:
logger.exception("Failed to subscribe to %s", str(sub))
for sub in usersubs:
try:
if self.bot.using_webhooks:
resp = await self.bot.subscribe_webhook(sub)
else:
resp = await self.bot.subscribe_websocket(sub, token_for=channel.userid, as_bot=False)
responses.append(resp)
except Exception:
logger.exception("Failed to subscribe to %s", str(sub))
logger.info("Finished tracker subscription to %s: %s", channel.userid, ', '.join(map(str, responses)))
# ----- Events -----
@cmds.Component.listener()
async def event_safe_channel_joined(self, payload: BotChannel):
# Check if the channel is tracked
# If it is, call start_tracking
tracked = await TrackingChannel.fetch(payload.userid)
if tracked and tracked.joined:
await self.start_tracking(tracked)
@cmds.Component.listener()
async def event_custom_redemption_add(self, payload: twitchio.ChannelPointsRedemptionAdd):
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
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
event_row = await self.data.events.insert(
event_type='redemption_add',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.user.id,
occurred_at=payload.redeemed_at,
)
detail_row = await self.data.redemption_add_events.insert(
event_id=event_row['event_id'],
redeem_id=payload.reward.id,
redeem_title=payload.reward.title,
redeem_cost=payload.reward.cost,
redemption_id=payload.id,
redemption_status=payload.status,
message=payload.user_input,
)
@cmds.Component.listener()
async def event_custom_redemption_update(self, payload: twitchio.ChannelPointsRedemptionUpdate):
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
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
event_row = await self.data.events.insert(
event_type='redemption_update',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.user.id,
)
detail_row = await self.data.redemption_update_events.insert(
event_id=event_row['event_id'],
redeem_id=payload.reward.id,
redeem_title=payload.reward.title,
redeem_cost=payload.reward.cost,
redemption_id=payload.id,
redemption_status=payload.status,
redeemed_at=utc_now()
)
@cmds.Component.listener()
async def event_follow(self, payload: twitchio.ChannelFollow):
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
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
# Computer follower count
followers = await payload.broadcaster.fetch_followers()
event_row = await self.data.events.insert(
event_type='follow',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.user.id,
)
detail_row = await self.data.follow_events.insert(
event_id=event_row['event_id'],
follower_count=followers.total
)
@cmds.Component.listener()
async def event_bits_use(self, payload: twitchio.ChannelBitsUse):
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
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
event_row = await self.data.events.insert(
event_type='bits',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.user.id,
)
detail_row = await self.data.bits_events.insert(
event_id=event_row['event_id'],
bits=payload.bits,
bits_type=payload.type,
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 self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
event_row = await self.data.events.insert(
event_type='subscribe',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.user.id,
)
detail_row = await self.data.subscribe_events.insert(
event_id=event_row['event_id'],
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 self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
if payload.user is not None:
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
else:
pid = None
event_row = await self.data.events.insert(
event_type='gift',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.user.id if payload.user else None,
)
detail_row = await self.data.gift_events.insert(
event_id=event_row['event_id'],
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 self.bot.community_fetch(twitchid=payload.broadcaster.id, name=payload.broadcaster.name)
cid = community.communityid
profile = await self.bot.profile_fetch(
twitchid=payload.user.id, name=payload.user.name,
)
pid = profile.profileid
event_row = await self.data.events.insert(
event_type='subscribe_message',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.user.id,
)
detail_row = await self.data.subscribe_message_events.insert(
event_id=event_row['event_id'],
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 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_online',
communityid=cid,
channel_id=payload.broadcaster.id,
occurred_at=payload.started_at,
)
detail_row = await self.data.stream_online_events.insert(
event_id=event_row['event_id'],
stream_id=payload.id,
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(
payload.from_broadcaster,
payload.to_broadcaster,
payload.viewer_count,
)
await self._event_raid_in(
payload.to_broadcaster,
payload.from_broadcaster,
payload.viewer_count,
)
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 self.bot.community_fetch(twitchid=broadcaster.id, name=broadcaster.name)
cid = community.communityid
event_row = await self.data.events.insert(
event_type='raidout',
communityid=cid,
channel_id=broadcaster.id,
)
detail_row = await self.data.raid_out_events.insert(
event_id=event_row['event_id'],
target_id=to_broadcaster.id,
target_name=to_broadcaster.name,
viewer_count=viewer_count
)
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 self.bot.community_fetch(twitchid=broadcaster.id, name=broadcaster.name)
cid = community.communityid
event_row = await self.data.events.insert(
event_type='raidin',
communityid=cid,
channel_id=broadcaster.id,
)
detail_row = await self.data.raid_in_events.insert(
event_id=event_row['event_id'],
source_id=from_broadcaster.id,
source_name=from_broadcaster.name,
viewer_count=viewer_count
)
@cmds.Component.listener()
async def event_message(self, payload: twitchio.ChatMessage):
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
profile = await self.bot.profile_fetch(
twitchid=payload.chatter.id, name=payload.chatter.name,
)
pid = profile.profileid
event_row = await self.data.events.insert(
event_type='message',
communityid=cid,
channel_id=payload.broadcaster.id,
profileid=pid,
user_id=payload.chatter.id,
)
detail_row = await self.data.message_events.insert(
event_id=event_row['event_id'],
message_id=payload.id,
message_type=payload.type,
content=payload.text,
source_channel_id=payload.source_id
)
# ----- Commands -----
@cmds.command(name='starttracking')
async def cmd_starttracking(self, ctx: cmds.Context):
if ctx.broadcaster:
tracking = await TrackingChannel.fetch_or_create(ctx.channel.id, joined=True)
if not tracking.joined:
await tracking.update(joined=True)
rows = await self.bot.data.user_auth_scopes.select_where(userid=ctx.channel.id)
scopes = Scopes([row['scope'] for row in rows])
url = self.bot.get_auth_url(
Scopes({
Scopes.channel_read_subscriptions,
Scopes.channel_read_redemptions,
Scopes.bits_read,
Scopes.channel_read_polls,
Scopes.channel_read_vips,
Scopes.moderator_read_followers,
*scopes
})
)
await ctx.reply(f"Tracking enabled! Please authorise me to track events in this channel: {url}")
else:
await ctx.reply("Only the broadcaster can enable tracking.")
@cmds.command(name='stoptracking')
async def cmd_stoptracking(self, ctx: cmds.Context):
if ctx.broadcaster:
tracking = await TrackingChannel.fetch(ctx.channel.id)
if tracking and tracking.joined:
await tracking.update(joined=False)
# TODO: Actually disable the subscriptions instead of just on the next restart
# This is tricky because some of the subscriptions may have been requested by other modules
# Requires keeping track of the source of subscriptions, and having a central manager disable them when no-one is listening anymore.
pass
await ctx.reply("Event tracking has been disabled.")
else:
await ctx.reply("Event tracking is not enabled!")
else:
await ctx.reply("Only the broadcaster can enable tracking.")
@cmds.command(name='join')
async def cmd_join(self, ctx: cmds.Context):
url = self.bot.get_auth_url()
await ctx.reply(f"Invite me to your channel with: {url}")

39
tracker/data.py Normal file
View File

@@ -0,0 +1,39 @@
from data import Registry, RowModel, Table
from data.columns import String, Timestamp, Integer, Bool
class TrackingChannel(RowModel):
_tablename_ = 'tracking_channels'
_cache_ = {}
userid = String(primary=True)
joined = Bool
joined_at = Timestamp()
_timestamp = Timestamp()
class EventData(Registry):
tracking_channels = TrackingChannel.table
events = Table('events')
follow_events = Table('follow_events')
bits_events = Table('bits_events')
subscribe_events = Table('subscribe_events')
gift_events = Table('gift_events')
subscribe_message_events = Table('subscribe_message_events')
cheer_events = Table('cheer_events')
redemption_add_events = Table('redemption_add_events')
redemption_update_events = Table('redemption_update_events')
poll_end_events = Table('poll_end_events')
stream_online_events = Table('stream_online_events')
stream_offline_events = Table('stream_offline_events')
channel_update_events = Table('channel_update_events')
vip_add_events = Table('vip_add_events')
vip_remove_events = Table('vip_remove_events')
raid_out_events = Table('raid_out_events')
raid_in_events = Table('raid_in_events')
message_events = Table('message_events')