Initial Plugin Commit
This commit is contained in:
151
.gitignore
vendored
Normal file
151
.gitignore
vendored
Normal 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/**
|
||||
3
__init__.py
Normal file
3
__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
async def twitch_setup(bot):
|
||||
from .tracker import setup
|
||||
await setup(bot)
|
||||
178
data/schema.sql
Normal file
178
data/schema.sql
Normal 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
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
twitchio
|
||||
7
tracker/__init__.py
Normal file
7
tracker/__init__.py
Normal 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
477
tracker/component.py
Normal 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
39
tracker/data.py
Normal 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')
|
||||
Reference in New Issue
Block a user