|
|
@ -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):
|
|
|
|