Implement monitoring Twitch events and periodic commands

master
Nikola Forró 5 years ago
parent f8914e6b9e
commit cc2d2c2992

@ -4,7 +4,7 @@ WORKDIR /bot
COPY . . COPY . .
RUN touch settings.cfg RUN touch settings.cfg
RUN apk add --no-cache gcc libc-dev libxml2-dev libxslt-dev && \ RUN apk add --no-cache gcc git libc-dev libxml2-dev libxslt-dev && \
rm -rf /var/cache/apk/* rm -rf /var/cache/apk/*
RUN pip install --no-cache-dir --requirement requirements.txt RUN pip install --no-cache-dir --requirement requirements.txt

@ -1,8 +1,11 @@
import asyncio import asyncio
import configparser import json
import logging import logging
import os import os
import websockets
from config import Config
from commands import Commands from commands import Commands
from extracommands import ExtraCommands from extracommands import ExtraCommands
from clients.discord import DiscordClient from clients.discord import DiscordClient
@ -13,9 +16,6 @@ TIMEOUT = 0.2
def main(): def main():
config = configparser.ConfigParser()
config.read('settings.cfg')
level = logging.DEBUG if bool(int(os.getenv('DEBUG', 0))) else logging.INFO level = logging.DEBUG if bool(int(os.getenv('DEBUG', 0))) else logging.INFO
handler = logging.StreamHandler() handler = logging.StreamHandler()
@ -36,12 +36,23 @@ def main():
twitch_logger.addHandler(handler) twitch_logger.addHandler(handler)
twitch_logger.setLevel(level) twitch_logger.setLevel(level)
config = Config()
config.reload()
commands = Commands(config, commands_logger) commands = Commands(config, commands_logger)
extra_commands = ExtraCommands() extra_commands = ExtraCommands()
extra_commands.reload()
async def monitor_extra_commands(): twitch_client = TwitchClient(config, twitch_logger, commands, extra_commands)
discord_client = DiscordClient(config, discord_logger, commands, extra_commands)
async def monitor_config_files():
while True: while True:
try:
config.reload_if_needed()
except Exception as e:
commands_logger.info('Exception', exc_info=e)
try: try:
extra_commands.reload_if_needed() extra_commands.reload_if_needed()
except Exception as e: except Exception as e:
@ -49,7 +60,6 @@ def main():
await asyncio.sleep(TIMEOUT) await asyncio.sleep(TIMEOUT)
async def run_twitch_client(): async def run_twitch_client():
twitch_client = TwitchClient(config, twitch_logger, commands, extra_commands)
twitch_client.connect_() twitch_client.connect_()
while True: while True:
try: try:
@ -60,18 +70,33 @@ def main():
async def run_discord_client(): async def run_discord_client():
while True: while True:
discord_client = DiscordClient(config, discord_logger, commands, extra_commands)
try: try:
await discord_client.start_() await discord_client.start_()
except Exception as e: except Exception as e:
discord_logger.info('Exception', exc_info=e) discord_logger.info('Exception', exc_info=e)
await discord_client.logout() await discord_client.logout()
async def run_periodic_commands():
interval = config['PeriodicCommands'].getint('interval')
while True:
await asyncio.sleep(interval)
twitch_client.run_periodic_command()
async def monitor_twitch_events():
url = config['TwitchEvents'].get('wss_url')
key = config['TwitchEvents'].get('wss_key')
async with websockets.connect(url, extra_headers={'X-Events-Key': key}) as ws:
while True:
event = json.loads(await ws.recv())
twitch_client.process_twitch_event(event)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
try: try:
asyncio.ensure_future(monitor_extra_commands()) asyncio.ensure_future(monitor_config_files())
asyncio.ensure_future(run_twitch_client()) asyncio.ensure_future(run_twitch_client())
asyncio.ensure_future(run_discord_client()) asyncio.ensure_future(run_discord_client())
asyncio.ensure_future(run_periodic_commands())
asyncio.ensure_future(monitor_twitch_events())
loop.run_forever() loop.run_forever()
finally: finally:
loop.close() loop.close()

@ -1,15 +1,21 @@
import collections
import datetime import datetime
import functools import functools
import random
import re import re
import time import time
import unicodedata import unicodedata
import dateutil.parser import dateutil.parser
import irc.bot import irc.bot
import nostril
import requests
from commands import CommandError from commands import CommandError
MAX_EVENTS = 100
# $username --> Sweet! Thanks for the quote! #$id: $response # $username --> Sweet! Thanks for the quote! #$id: $response
QUOTE_ADDED_PATTERN = re.compile(r'''^(.\s)? QUOTE_ADDED_PATTERN = re.compile(r'''^(.\s)?
(?P<user>.+)\s+-->\s+ (?P<user>.+)\s+-->\s+
@ -83,6 +89,9 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
self.logger = logger self.logger = logger
self.commands = commands self.commands = commands
self.extra_commands = extra_commands self.extra_commands = extra_commands
self.last_periodic_command = None
self.streaming = False
self.events = collections.deque([], MAX_EVENTS)
self.giveaway = None self.giveaway = None
self.patterns = [ self.patterns = [
(QUOTE_ADDED_PATTERN, self._add_quote), (QUOTE_ADDED_PATTERN, self._add_quote),
@ -126,6 +135,56 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
def process_data(self): def process_data(self):
self.reactor.process_once() self.reactor.process_once()
def process_twitch_event(self, event):
def is_nonsensical(e):
try:
nonsense = nostril.nonsense(e['data'][0]['from_name'])
except (KeyError, IndexError, ValueError):
return False
else:
return nonsense
def get_time(e):
try:
time = dateutil.parser.parse(e['data'][0]['followed_at'])
time = time.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
except (KeyError, IndexError, ValueError):
return datetime.datetime(1, 1, 1)
else:
return time
if event.get('topic') == 'streams':
self.streaming = bool(event.get('data'))
self.events.appendleft(event)
follows = [e for e in self.events if e.get('topic') == 'followers']
if not follows:
return
nonsensical = [e for e in follows if is_nonsensical(e)]
if not nonsensical:
return
self.logger.info('Nonsensical followers detected: %s', str(nonsensical)) # FIXME: remove this
enabled = self.config['TwitchEvents'].getboolean('antispam_enabled')
interval = self.config['TwitchEvents'].getint('antispam_interval')
count = self.config['TwitchEvents'].getint('antispam_count')
command = self.config['TwitchEvents'].get('antispam_command')
times = sorted([get_time(e) for e in nonsensical], reverse=True)
if len([t for t in times if (times[0] - t).total_seconds() <= interval]) >= count:
if enabled:
for channel in self.config['IRC'].get('channels').split(','):
self.connection.privmsg('#{0}'.format(channel), command)
else:
self.logger.info('Potential SPAM detected!')
def run_periodic_command(self):
if not self.streaming:
return
commands = {k: v for k, v in self.extra_commands.items() if k.endswith('@')}
if not commands:
return
if self.last_periodic_command in commands and len(commands) > 1:
del commands[self.last_periodic_command]
self.last_periodic_command, resp = random.choice(list(commands.items()))
for channel in self.config['IRC'].get('channels').split(','):
self.connection.privmsg('#{0}'.format(channel), resp.format(user='?'))
def on_welcome(self, connection, event): def on_welcome(self, connection, event):
connection.cap('REQ', ':twitch.tv/membership') connection.cap('REQ', ':twitch.tv/membership')
connection.cap('REQ', ':twitch.tv/tags') connection.cap('REQ', ':twitch.tv/tags')
@ -137,6 +196,12 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
def on_join(self, connection, event): def on_join(self, connection, event):
self.logger.info('Joined %s', event.target) self.logger.info('Joined %s', event.target)
client_id = self.config['Twitch'].get('client_id')
channel_id = self.config['Twitch'].get('channel_id')
params = dict(client_id=client_id)
r = requests.get('https://api.twitch.tv/v5/streams/{0}'.format(channel_id), params=params)
if r.ok:
self.streaming = bool(r.json().get('stream'))
def on_pubmsg(self, connection, event): def on_pubmsg(self, connection, event):
self._process_message(connection, event) self._process_message(connection, event)
@ -166,7 +231,9 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
if m: if m:
action(tags, send_response, **m.groupdict()) action(tags, send_response, **m.groupdict())
for cmd, resp in self.extra_commands.items(): for cmd, resp in self.extra_commands.items():
if cmd == message: if cmd.rstrip('@') == message:
if cmd.endswith('@'):
self.last_periodic_command = message
send_response(resp.format(user=tags['display-name'])) send_response(resp.format(user=tags['display-name']))
def _format_quote(self, quote): def _format_quote(self, quote):

@ -0,0 +1,32 @@
import configparser
import os
FILENAME = 'settings.cfg'
class Config(object):
def __init__(self):
self.last_mtime = 0
self.config = None
def __contains__(self, item):
return item in self.config
def __getitem__(self, key):
return self.config[key]
def __getattr__(self, name):
return getattr(self.config, name)
def reload(self):
config = configparser.ConfigParser()
config.read(FILENAME)
self.config = config
def reload_if_needed(self):
mtime = os.stat(FILENAME).st_mtime
if mtime > self.last_mtime:
self.last_mtime = mtime
self.reload()

@ -8,3 +8,5 @@ pyquery
python-dateutil python-dateutil
python-twitter python-twitter
requests requests
websockets
git+https://github.com/casics/nostril.git@1.1.1#egg=nostril

@ -33,6 +33,8 @@ announcement_pattern = Live now! Playing {game} - Bring Cheese! {url}
[Twitch] [Twitch]
cache_api_url = https://ladylilia.com/twitch-cache/api cache_api_url = https://ladylilia.com/twitch-cache/api
token = oauth:__TWITCH_OAUTH_TOKEN__ token = oauth:__TWITCH_OAUTH_TOKEN__
client_id = __TWITCH_CLIENT_ID__
channel_id = 92737529
[Youtube] [Youtube]
api_key = __GOOGLE_API_KEY__ api_key = __GOOGLE_API_KEY__
@ -51,3 +53,14 @@ act_as_proxy = true
[TwitchSubs] [TwitchSubs]
api_url = https://ladylilia.com/twitch-subs/api api_url = https://ladylilia.com/twitch-subs/api
api_key = __TWITCH_SUBS_API_KEY__ api_key = __TWITCH_SUBS_API_KEY__
[TwitchEvents]
wss_url = wss://ladylilia.com/twitch-events/
wss_key = __TWITCH_EVENTS_KEY__
antispam_enabled = False
antispam_interval = 30
antispam_count = 4
antispam_command = /followers 60m
[PeriodicCommands]
Interval = 1800

Loading…
Cancel
Save