You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

166 lines
6.8 KiB

6 years ago
import configparser
import datetime
import functools
import logging
import os
import re
import unicodedata
import dateutil.parser
6 years ago
import irc.bot
import requests
import tempora.schedule
6 years ago
class ReplayBot(irc.bot.SingleServerIRCBot):
def __init__(self):
self.logger = logging.getLogger('irc.client')
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
self.logger.addHandler(handler)
level = logging.DEBUG if bool(int(os.getenv('DEBUG', 0))) else logging.INFO
self.logger.setLevel(level)
self.config = configparser.ConfigParser()
self.config.read('settings.cfg')
self.supported_commands = [
(re.compile(r'^!load\s+(?P<id>\d+)$'), self._do_load),
(re.compile(r'^!start(\s+(?P<at>.+))?$'), self._do_start),
(re.compile(r'^!resync\s+(?P<offset>.+)$'), self._do_resync),
6 years ago
(re.compile(r'^!stop$'), self._do_stop),
]
self.messages = []
self.output_channel = '#{0}'.format(self.config['IRC'].get('output_channel'))
control_channel = self.config['IRC'].get('control_channel')
self.control_channel = '#{0}'.format(control_channel) if control_channel else None
self.control_users = self.config['IRC'].get('control_users').split(',')
6 years ago
server = self.config['IRC'].get('server')
port = self.config['IRC'].getint('port')
nickname = self.config['IRC'].get('nickname')
token = self.config['Twitch'].get('token')
self.logger.info('Connecting to %s:%d', server, port)
super(ReplayBot, self).__init__([(server, port, token)], nickname, nickname)
def on_welcome(self, connection, event):
connection.cap('REQ', ':twitch.tv/membership')
connection.cap('REQ', ':twitch.tv/tags')
connection.cap('REQ', ':twitch.tv/commands')
channels = set([self.output_channel])
if self.control_channel:
channels.add(self.control_channel)
for channel in channels:
self.logger.info('Joining %s', channel)
connection.join(channel)
def on_join(self, connection, event):
self.logger.info('Joined %s', event.target)
def on_pubmsg(self, connection, event):
self._process_message(connection, event)
def on_whisper(self, connection, event):
self._process_message(connection, event)
def _send_response(self, connection, event, msg):
if event.target.startswith('#'):
connection.privmsg(event.target, msg)
else:
connection.privmsg('#jtv', '/w {0} {1}'.format(event.source.nick, msg))
def _process_message(self, connection, event):
tags = {t['key']: t['value'] for t in event.tags}
message = ''.join([c for c in event.arguments[0] if not unicodedata.category(c).startswith('C')])
message = message.rstrip()
send_response = functools.partial(self._send_response, connection, event)
for pattern, action in self.supported_commands:
m = pattern.match(message)
if m:
action(tags, send_response, **m.groupdict())
def _do_load(self, tags, send_response, id, **kwargs):
if tags['user-id'] not in self.control_users:
send_response('Sorry @{0}, you are not allowed to do this'.format(tags['display-name']))
return
6 years ago
client_id = self.config['Twitch'].get('client_id')
user_blacklist = self.config['Rerun'].get('user_blacklist').split(',')
6 years ago
def get_comments(cursor):
url = 'https://api.twitch.tv/v5/videos/{0}/comments'.format(id)
params = dict(client_id=client_id, cursor=cursor)
r = requests.get(url, params=params)
r.raise_for_status()
return r.json()
self.messages = []
cursor = ''
while True:
data = get_comments(cursor)
for comment in data.get('comments', []):
if comment['commenter']['_id'] in user_blacklist:
continue
6 years ago
self.messages.append((
datetime.timedelta(seconds=float(comment['content_offset_seconds'])),
comment['commenter']['display_name'],
comment['message']['body']))
cursor = data.get('_next')
if not cursor:
break
if self.messages:
send_response('@{0}: loaded {1} messages, first message at {2}'.format(
tags['display-name'], len(self.messages), str(self.messages[0][0])))
else:
send_response('@{0}: failed to load messages'.format(tags['display-name']))
def _do_start(self, tags, send_response, at=None, **kwargs):
if tags['user-id'] not in self.control_users:
send_response('Sorry @{0}, you are not allowed to do this'.format(tags['display-name']))
return
if at is None:
t = datetime.datetime.now().astimezone(tz=None)
else:
t = dateutil.parser.parse(at).astimezone(tz=None)
6 years ago
for offset, user, msg in self.messages:
def cb(user=user, msg=msg):
return self._post_message(user, msg)
self.reactor.scheduler.execute_at(t + offset, cb)
with self.reactor.mutex:
while self.reactor.scheduler.queue:
if self.reactor.scheduler.queue[0].due():
self.reactor.scheduler.queue.pop(0)
else:
break
6 years ago
def _do_resync(self, tags, send_response, offset, **kwargs):
if tags['user-id'] not in self.control_users:
send_response('Sorry @{0}, you are not allowed to do this'.format(tags['display-name']))
return
offset = datetime.timedelta(seconds=float(offset))
with self.reactor.mutex:
for i, dc in enumerate(self.reactor.scheduler.queue):
ndc = tempora.schedule.DelayedCommand.from_datetime(dc + offset)
ndc.delay = dc.delay + offset
ndc.target = dc.target
self.reactor.scheduler.queue[i] = ndc
while self.reactor.scheduler.queue:
if self.reactor.scheduler.queue[0].due():
self.reactor.scheduler.queue.pop(0)
else:
break
6 years ago
def _do_stop(self, tags, send_response, **kwargs):
if tags['user-id'] not in self.control_users:
send_response('Sorry @{0}, you are not allowed to do this'.format(tags['display-name']))
return
with self.reactor.mutex:
self.reactor.scheduler.queue.clear()
6 years ago
def _post_message(self, user, msg):
text = self.config['Rerun'].get('message_template').format(username=user, message=msg)
self.connection.privmsg(self.output_channel, text)
6 years ago
def main():
bot = ReplayBot()
bot.start()
if __name__ == "__main__":
main()