Implement monitoring Twitch events and periodic commands

master
Nikola Forró 5 years ago
parent f8914e6b9e
commit cc2d2c2992

@ -4,7 +4,7 @@ WORKDIR /bot
COPY . .
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/*
RUN pip install --no-cache-dir --requirement requirements.txt

@ -1,8 +1,11 @@
import asyncio
import configparser
import json
import logging
import os
import websockets
from config import Config
from commands import Commands
from extracommands import ExtraCommands
from clients.discord import DiscordClient
@ -13,9 +16,6 @@ TIMEOUT = 0.2
def main():
config = configparser.ConfigParser()
config.read('settings.cfg')
level = logging.DEBUG if bool(int(os.getenv('DEBUG', 0))) else logging.INFO
handler = logging.StreamHandler()
@ -36,12 +36,23 @@ def main():
twitch_logger.addHandler(handler)
twitch_logger.setLevel(level)
config = Config()
config.reload()
commands = Commands(config, commands_logger)
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:
try:
config.reload_if_needed()
except Exception as e:
commands_logger.info('Exception', exc_info=e)
try:
extra_commands.reload_if_needed()
except Exception as e:
@ -49,7 +60,6 @@ def main():
await asyncio.sleep(TIMEOUT)
async def run_twitch_client():
twitch_client = TwitchClient(config, twitch_logger, commands, extra_commands)
twitch_client.connect_()
while True:
try:
@ -60,18 +70,33 @@ def main():
async def run_discord_client():
while True:
discord_client = DiscordClient(config, discord_logger, commands, extra_commands)
try:
await discord_client.start_()
except Exception as e:
discord_logger.info('Exception', exc_info=e)
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()
try:
asyncio.ensure_future(monitor_extra_commands())
asyncio.ensure_future(monitor_config_files())
asyncio.ensure_future(run_twitch_client())
asyncio.ensure_future(run_discord_client())
asyncio.ensure_future(run_periodic_commands())
asyncio.ensure_future(monitor_twitch_events())
loop.run_forever()
finally:
loop.close()

@ -1,15 +1,21 @@
import collections
import datetime
import functools
import random
import re
import time
import unicodedata
import dateutil.parser
import irc.bot
import nostril
import requests
from commands import CommandError
MAX_EVENTS = 100
# $username --> Sweet! Thanks for the quote! #$id: $response
QUOTE_ADDED_PATTERN = re.compile(r'''^(.\s)?
(?P<user>.+)\s+-->\s+
@ -83,6 +89,9 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
self.logger = logger
self.commands = commands
self.extra_commands = extra_commands
self.last_periodic_command = None
self.streaming = False
self.events = collections.deque([], MAX_EVENTS)
self.giveaway = None
self.patterns = [
(QUOTE_ADDED_PATTERN, self._add_quote),
@ -126,6 +135,56 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
def process_data(self):
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):
connection.cap('REQ', ':twitch.tv/membership')
connection.cap('REQ', ':twitch.tv/tags')
@ -137,6 +196,12 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
def on_join(self, connection, event):
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):
self._process_message(connection, event)
@ -166,7 +231,9 @@ class TwitchClient(irc.bot.SingleServerIRCBot):
if m:
action(tags, send_response, **m.groupdict())
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']))
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-twitter
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]
cache_api_url = https://ladylilia.com/twitch-cache/api
token = oauth:__TWITCH_OAUTH_TOKEN__
client_id = __TWITCH_CLIENT_ID__
channel_id = 92737529
[Youtube]
api_key = __GOOGLE_API_KEY__
@ -51,3 +53,14 @@ act_as_proxy = true
[TwitchSubs]
api_url = https://ladylilia.com/twitch-subs/api
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