Skip to content

Instantly share code, notes, and snippets.

@werthog2994
Created October 30, 2012 01:10
Show Gist options
  • Select an option

  • Save werthog2994/3977725 to your computer and use it in GitHub Desktop.

Select an option

Save werthog2994/3977725 to your computer and use it in GitHub Desktop.
Naoko bot with ponysay
#!/usr/bin/env python
# Naoko - A prototype synchtube bot
# Based on Denshi written in 2011 by Falaina [email protected]
#
# This software is released under the 2-clause BSD License.
# A copy of this license should have been provided with this
# software. If not see
# <http://www.freebsd.org/copyright/freebsd-license.html>.
import hashlib
import itertools
import json
import logging
import random
import sched, time, math
import socket
import struct
import threading
import urllib, urlparse, httplib
import re
from urllib2 import Request, urlopen
from collections import namedtuple, deque
import ConfigParser
from datetime import datetime
import code
from lib.repl import Repl
from settings import *
from webserver import startServer
from lib.database import NaokoDB
from lib.sioclient import SocketIOClient
from lib.ircclient import IRCClient
from lib.apiclient import APIClient
# Cleverbot doesn't like publicly posting code to access it.
try:
from lib.cbclient import CleverbotClient
except ImportError:
class CleverbotClient(object):
pass
# Package arguments for later use.
# Due to the way python handles scopes this needs to be used to avoid race conditions.
def package(fn, *args, **kwargs):
def action():
fn(*args, **kwargs)
return action
eight_choices = [
"It is certain",
"It is decidedly so",
"Without a doubt",
"Yes - definitely",
"You may rely on it",
"As I see it, yes",
"Most likely",
"Outlook good",
"Signs point to yes",
"Yes",
"Reply hazy, try again",
"Ask again later",
"Better not tell you now",
"Cannot predict now",
"Concentrate and ask again",
"Don't count on it",
"My reply is no",
"My sources say no",
"Outlook not so good",
"Very doubtful"]
pony_choices = [
"Rarity",
"Twilight Sparkle",
"Pinkie Pie",
"Applejack",
"Fluttershy",
"Rainbow Dash"
]
# Simple Record Types for variable synchtube constructs
SynchtubeUser = namedtuple('SynchtubeUser',
['sid', 'nick', 'uid', 'auth', 'ava', 'lead', 'mod', 'karma', 'msgs', 'nickChanges'])
SynchtubeVidInfo = namedtuple('SynchtubeVidInfo',
['site', 'vid', 'title', 'thumb', 'dur'])
SynchtubeVideo = namedtuple('SynchtubeVideo',
['vidinfo', 'v_sid', 'uid', 'nick'])
IRCUser = namedtuple('IRCUser', ["nick", "mod", "uid"])
# Generic object that can be assigned attributes
class Object(object):
pass
# Synchtube "client" built on top of a socket.io socket
# Synchtube messages are generally of the form:
# ["TYPE", DATA]
# e.g., The self message (describes current client)
# ["self" ["bbc2c922",22262,true,"jpg",false,true,21]]
# Which describes a particular connection for the user Naoko
# (uid 22262). The first field is the session identifier,
# second is uid, third is whether or not client is authenticated
# fourth is avatar type, and so on.
class Naoko(object):
_HEADERS = {'User-Agent' : 'NaokoBot',
'Accept' : 'text/html,application/xhtml+xml,application/xml;',
'Host' : DOMAIN,
'Connection' : 'keep-alive',
'Origin' : 'http://' + DOMAIN,
'Referer' : 'http://' + DOMAIN}
# Bitmasks for hybrid mods.
MASKS = {
"LEAD" : (1, 'O'), # O - Both the steal, lead, and mod commands.
"BUMP" : ((1 << 1), 'B'), # B - Bumping videos.
"DELETE" : ((1 << 2), 'D'), # D - Deleting videos.
"KICK" : ((1 << 3), 'K'), # K - Kicking users.
"BAN" : ((1 << 4), 'Q'), # Q - Banning and unbanning users, as well as viewing the banlist.
"RESTART" : ((1 << 5), 'R'), # R - Restarting Naoko.
"CLEAN" : ((1 << 6), 'C'), # C - The clean command.
"SKIP" : ((1 << 7), 'S'), # S - Skipping videos.
"LOCK" : ((1 << 8), 'L'), # L - Lock and unlock the playlist.
"RANDOM" : ((1 << 9), 'A'), # A - Addrandom with more than 5 videos.
"SETSKIP" : ((1 << 11), 'E'), # E - Setskip.
"DUPLICATES" : ((1 << 12), 'T'), # T - Remove duplicate videos.
"MUTE" : ((1 << 13), 'M'), # M - Mute or unmute Naoko.
"PURGE" : ((1 << 14), 'G'), # G - Purge.
"AUTOLEAD" : ((1 << 15), 'U'), # U - Autolead.
"AUTOSKIP" : ((1 << 16), 'V'), # V - Autosetskip.
"POLL" : ((1 << 17), 'P'), # P - Start and end polls.
"SHUFFLE" : ((1 << 18), 'F'), # F - Shuffle.
"UNREGSPAMBAN" : ((1 << 19), 'I'), # I - Change whether unregistered users are banned for spamming or have multiple chances.
"ADD" : ((1 << 20), 'H')} # H - Add when the list is locked.
# Bitmasks for deferred tosses
DEFERRED_MASKS = {
"SKIP" : 1,
"UNBAN" : 1 << 1,
"SHUFFLE" : 1 << 2}
def __init__(self, pipe=None):
# Initialize all loggers
self.logger = logging.getLogger("stclient")
self.logger.setLevel(LOG_LEVEL)
self.chat_logger = logging.getLogger("stclient.chat")
self.chat_logger.setLevel(LOG_LEVEL)
self.irc_logger = logging.getLogger("stclient.irc")
self.irc_logger.setLevel(LOG_LEVEL)
# Seem to have some kind of role in os.terminate() from the watchdog
self.thread = threading.currentThread()
self.thread.st = self
self.thread.close = self.close
self._getConfig()
# If more than one thread attempts to close Naoko at the same time it will cause an error
self.closeLock = threading.Lock()
self.closing = threading.Event()
# Since the video list can be accessed by two threads synchronization is necessary
# This is currently only used to make nextVideo() thread safe
self.vidLock = threading.Lock()
self._initHandlers()
self._initCommandHandlers()
self._initIRCCommandHandlers()
self._initPersistentSettings()
self.modList = set()
self.room_info = {}
self.vidlist = []
self.skipLevel = None
self.skips = deque(maxlen=3)
self.muted = False
self.doneInit = False
# Workarounds for non-atomic operations
self.verboseBanlist = False
self.unbanTarget = None
self.shuffleBump = False
# Used to control taking and returning leader
self.leader_queue = deque()
self.leader_sid = None
# Stores the action that will be necessary to give back leader after Naoko is done with it
self.pendingToss = False
# Used to avoid handing back the leader when asLeader is called multiple times in quick succession
self.notGivingBack = False
self.tossing = False
self.deferredToss = 0
# Tracks whether she is leading
# Is not triggered when she is going to give the leader position back or turn tv mode back on
self.leading = threading.Event()
# Pending kicks/bans to prevent duplicates
self.pending = {}
# Used to implement a three-strikes policy
self.banTracker = {}
# By default USER_COUNT_THROTTLE is 0 so this will have no effect
self.userCountTime = time.time() - USER_COUNT_THROTTLE
# Used to avoid spamming chat or the playlist
self.last_random = time.time() - 5
self.last_quote = time.time() - 5
# All the information related to playback state
self.state = Object()
self.state.state = 0
self.state.current = None
self.state.time = 0
self.state.pauseTime = -1.0
self.state.dur = 0
self.state.reason = None
# Tracks when she needs to update her playback status
# This is used to interrupt her timer as she is waiting for the end of a video
self.playerAction = threading.Event()
if self.pw:
self.logger.info("Attempting to login")
login_url = "http://%s/user/login" % (DOMAIN)
login_body = {'email' : self.name.encode('utf-8'), 'password' : self.pw.encode('utf-8')};
login_data = urllib.urlencode(login_body)
login_req = Request(login_url, data=login_data, headers=self._HEADERS)
login_req.add_header('X-Requested-With', 'XMLHttpRequest')
login_req.add_header('Content', 'XMLHttpRequest')
login_res = urlopen(login_req)
login_res_headers = login_res.info()
if login_res_headers['Status'][:3] != '200':
raise Exception("Could not login")
if login_res_headers.has_key('Set-Cookie'):
self._HEADERS['Cookie'] = login_res_headers['Set-Cookie']
self.logger.info("Login successful")
room_url = "http://%s/r/%s" % (DOMAIN, self.room)
if self.room_pw:
self.logger.info("Attempting to join password protected room.")
room_body = {'personalpassword' : self.room_pw.encode('utf-8')};
room_data = urllib.urlencode(room_body)
room_req = Request(room_url, data=room_data, headers=self._HEADERS)
else:
room_req = Request(room_url, headers=self._HEADERS)
room_res = urlopen(room_req)
room_res_body = room_res.read()
def getHiddenValue(val):
m = re.search(r"<input.*?id=\"%s\".*?value=\"(\S+)\"" % (val), room_res_body)
return m.group(1)
# room_authkey is the only information needed to authenticate you, keep this
# secret!
self.authkey = getHiddenValue("room_authkey")
self.port = getHiddenValue("room_dest_port")
self.st_build = getHiddenValue("st_build")
self.userid = getHiddenValue("room_userid")
config_url = "http://%s/api/1/room/%s" % (DOMAIN, self.room)
config_info = urllib.urlopen(config_url).read()
config = json.loads(config_info)
try:
self.logger.info("Obtaining session ID")
if config['room'].has_key('port'):
self.port = config['room']['port']
self.port = int(self.port)
self.config_params = {'b' : self.st_build,
'r' : config['room']['id'],
'p' : self.port,
't' : int(round(time.time()*1000)),
'i' : socket.gethostbyname(socket.gethostname())}
if self.authkey and self.userid:
self.config_params['a'] = self.authkey
self.config_params['u'] = self.userid
if config.has_key("moderators"):
for m in config["moderators"]:
self.modList.add(m.lower())
except:
self.logger.debug("Config is %s" % (config))
if config.has_key('error'):
self.logger.error("Synchtube returned error: %s" %(config['error']))
raise
self.userlist = {}
self.logger.info("Starting SocketIO Client")
self.client = SocketIOClient(SOCKET_IP, self.port, "socket.io",
self.config_params)
# Various queues and events used to sychronize actions in separate threads
# Some are initialized with maxlen = 0 so they will silently discard actions meant for non-existent threads
self.st_queue = deque()
self.irc_queue = deque(maxlen=0)
self.sql_queue = deque(maxlen=0)
self.api_queue = deque()
self.st_action_queue = deque()
# Events are used to prevent busy-waiting
self.sqlAction = threading.Event()
self.stAction = threading.Event()
self.apiAction = threading.Event()
# Initialize the clients that are always used
self.apiclient = APIClient(self.apikeys)
self.cbclient = CleverbotClient()
self.client.connect()
# Start the threads that are required for all normal operation
self.chatthread = threading.Thread(target=Naoko._chatloop, args=[self])
self.chatthread.start()
self.stthread = threading.Thread(target=Naoko._stloop, args=[self])
self.stthread.start()
self.stlistenthread = threading.Thread(target=Naoko._stlistenloop, args=[self])
self.stlistenthread.start()
self.playthread = threading.Thread(target=Naoko._playloop, args=[self])
self.playthread.start()
self.apithread = threading.Thread(target=Naoko._apiloop, args=[self])
self.apithread.start()
# Start the optional threads
if self.irc_nick:
self.ircclient = False
self.ircthread = threading.Thread(target=Naoko._ircloop, args=[self])
self.ircthread.start()
if self.dbfile:
self.sql_queue = deque()
self.sqlthread = threading.Thread(target=Naoko._sqlloop, args=[self])
self.sqlthread.start()
# A database is required for Naoko's web server
if self.webserver_mode == "embedded":
self.webthread = threading.Thread(target=startServer, args=[self])
self.webthread.start()
# Start a REPL on the specified port. Only accept connections from localhost
# and expose ourself as 'naoko' in the REPL's local scope
# WARNING: THE REPL WILL REDIRECT STDOUT AND STDERR.
# the logger will still go to the the launching terminals
# stdout/stderr, however print statements will probably be rerouted
# to the socket.
# This is not checked by the healthcheck
if REPL_PORT > 0:
self.repl = Repl(port=REPL_PORT, host='localhost', locals={"naoko": self})
# Healthcheck loop, reports to the watchdog timer every 5 seconds
while not self.closing.wait(5):
# Sleeping first lets everything get initialized
# The parent process will wait
try:
status = self.stthread.isAlive() and self.stlistenthread.isAlive()
status = status and (not self.irc_nick or self.ircthread.isAlive())
status = status and self.chatthread.isAlive()
# Catch the case where the client is still connecting after 5 seconds
status = status and (not self.client.heartBeatEvent or self.client.hbthread.isAlive())
status = status and (not self.dbfile or self.sqlthread.isAlive())
status = status and (not self.dbfile or self.webserver_mode != "embedded" or self.webthread.isAlive())
status = status and self.playthread.isAlive()
status = status and self.apithread.isAlive()
except Exception as e:
self.logger.error(e)
status = False
if status and pipe:
pipe.send("HEALTHY")
if not status:
self.close()
else:
if pipe:
self.logger.warn("Restarting")
pipe.send("RESTART")
# Responsible for listening to communication from Synchtube
def _stlistenloop(self):
client = self.client
while not self.closing.isSet():
data = client.recvMessage()
try:
data = json.loads(data)
except ValueError as e:
self.logger.warn("Failed to parse" + data)
raise e;
if not data or len(data) == 0:
self.sendHeartBeat()
continue
st_type = data[0]
try:
if len(data) > 1:
arg = data[1]
else:
arg = ''
fn = self.handlers[st_type]
except KeyError:
self.logger.warn("No handler for %s [%s]", st_type, arg)
else:
self.st_action_queue.append(package(fn, st_type, arg))
self.stAction.set()
else:
self.logger.info("Synchtube Listening Loop Closed")
self.close()
# Responsible for handling messages from Synchtube
def _stloop(self):
client = self.client
while not self.closing.isSet() and self.stAction.wait():
self.stAction.clear()
while self.st_action_queue:
self.st_action_queue.popleft()()
else:
self.logger.info("Synchtube Loop Closed")
self.close()
# Responsible for communicating with IRC
def _ircloop(self):
time.sleep(5)
self.irc_logger.info("Starting IRC Client")
self.ircclient = client = IRCClient(self.server, self.channel, self.irc_nick, self.ircpw)
self.irc_queue = deque()
failCount = 0
while not self.closing.isSet():
frame = deque(client.recvMessage().split('\n'))
while len(frame) > 0:
data = self.filterString(frame.popleft().strip())[1]
if data.find("PING :") != -1:
client.ping()
elif data.find("PRIVMSG " + self.channel + " :") != -1:
name = data.split('!', 1)[0][1:]
msg = data[data.find("PRIVMSG " + self.channel + " :") + len("PRIVMSG " + self.channel + " :"):]
if not name == self.irc_nick:
self.st_queue.append(msg)
self.chatCommand(IRCUser(*(name, False, False)), msg, True)
self.irc_logger.info("IRC %r:%r", name, msg)
# Currently ignore messages sent directly to her
elif data.find("PRIVMSG " + self.irc_nick + " :") != -1:
continue
# Failed to send to the channel
elif data.find("404 " + self.irc_nick + " " + self.channel +" :Cannot send to channel") != -1:
failCount += 1
self.irc_logger.debug("Failed to send to channel %d times" % (failCount))
if failCount > 4:
self.irc_logger.info("Could not send to %s %d times, restarting" % (self.channel, failCount))
self.sendChat("Failed to send messages to the IRC channel %d times, check my configuration file." % (failCount))
self.close()
if self.ircpw and not client.loggedIn:
client.send("PRIVMSG nickserv :id " + self.ircpw + "\n")
if not client.inChannel:
client.send("JOIN " + self.channel + "\n")
elif data.find("NOTICE " + self.irc_nick + " :Password accepted - you are now recognized.") != -1:
client.loggedIn = True
self.irc_logger.debug("Authenticated")
elif data.find("JOIN :" + self.channel) != -1:
client.inChannel = True
self.irc_logger.debug("Joined Channel")
# Disable IRC support when an incorrect password is provided.
elif data.find("NOTICE " + self.irc_nick + " :Password incorrect.") != -1:
self.disableIRC("Incorrect IRC password provided. Disabling IRC support.")
return
# Nickname in use, attempt to ghost if a password is provided or use an alternate name
elif data.find("433 * " + self.irc_nick + " :Nickname is already in use.") != -1:
if self.ircpw:
self.irc_logger.info("Nickname in use, attempting to ghost.")
client.send("NICK " + self.irc_nick[:24] + "_naoko\n")
client.send("PRIVMSG nickserv :ghost " +self.irc_nick + " " + self.ircpw + "\n")
else:
failCount += 1
if failCount > 4:
self.disableIRC("Unable to find an unused IRC nickname. Disabling IRC support.")
return
self.irc_logger.info("Nickname in use and no password provided. Switching to alternate name")
self.irc_nick = self.irc_nick[:29] + str(failCount)
client.send("NICK " + self.irc_nick + "\n")
# Ghost Succeeded
elif data.find("NOTICE " + self.irc_nick[:24] + "_naoko :Ghost with your nick has been killed.") != -1:
self.irc_logger.info("Ghost successful, reverting name.")
client.send("NICK " + self.irc_nick + "\n")
client.send("PRIVMSG nickserv :id " + self.ircpw + "\n")
client.send("JOIN " + self.channel + "\n")
# Ghost failed. Since the nickname is in use and an incorrect password is provided disable IRC
# to avoid being stuck in a restart loop and bring attention to the incorrect password.
elif data.find("NOTICE " + self.irc_nick[:24] + "_naoko :Access denied.") != -1:
self.disableIRC("IRC nickname in use and incorrect password provided. Disabling IRC support.")
return
elif data[:5] == "ERROR":
err = "Unknown Error"
if data.find("(") != -1 and data.find(")") != -1:
err = data[data.find("(") + 1:data.find(")")]
self.disableIRC("IRC connection closed due to %s. Restarting in 2 minutes." % (err))
time.sleep(2*60)
self.close()
else:
self.logger.info("IRC Loop Closed")
self.close()
# Responsible for sending chat messages to IRC and Synchtube.
# Only the $status command and error messages should send a chat message to Synchtube or IRC outside this thread.
def _chatloop(self):
while not self.closing.isSet():
# Detect when far too many messages are being sent and clear the queue
if len(self.irc_queue) > 20 or len(self.st_queue) > 20:
time.sleep(5)
self.irc_queue.clear()
self.st_queue.clear()
continue
if self.muted:
self.irc_queue.clear()
self.st_queue.clear()
else:
if self.irc_queue:
self.ircclient.sendMsg(self.irc_queue.popleft())
if self.st_queue:
self.sendChat(self.st_queue.popleft())
time.sleep(self.spam_interval)
else:
self.logger.info("Chat Loop Closed")
# Responsible for handling playback
def _playloop(self):
while self.leading.wait():
if self.closing.isSet(): break
sleepTime = self.state.dur + (self.state.time / 1000) - time.time() + 1
if sleepTime < 0:
sleepTime = 0
if not self.state.current:
self.enqueueMsg("Unknown video playing, skipping.")
self.nextVideo()
self.state.state = -1
sleepTime = 60
elif self.state.reason:
self.enqueueMsg(self.state.reason)
self.state.reason = None
self.nextVideo()
self.state.state = -1
sleepTime = 60
# If the video is paused, unpause it automatically.
elif self.state.state == 2:
unpause = 0
if not self.state.pauseTime < 0:
unpause = self.state.pauseTime - (self.state.time / 1000)
self.pauseTime = -1.0
self.logger.info("Unpausing video %.3f seconds from the beginning." % (unpause))
self.send("s", [1, unpause])
sleepTime = 60
elif self.state.state == 0:
sleepTime = 60
self.logger.debug("Waiting %.3f seconds for the end of the video." % (sleepTime))
if not self.playerAction.wait(sleepTime):
if self.closing.isSet(): break
if not self.leading.isSet(): continue
self.nextVideo()
self.playerAction.clear()
self.logger.info("Playback Loop Closed")
def _sqlloop(self):
self.db_logger = logging.getLogger("stclient.db")
self.db_logger.setLevel(LOG_LEVEL)
self.db_logger.info("Starting Database Client")
self.dbclient = client = NaokoDB(self.dbfile)
while self.sqlAction.wait():
if self.closing.isSet(): break
self.sqlAction.clear()
while self.sql_queue:
self.sql_queue.popleft()()
self.logger.info("SQL Loop Closed")
# This loop is responsible for dealing with all external APIs
# This includes validating Youtube videos and any future functionality
def _apiloop(self):
while self.apiAction.wait():
if self.closing.isSet(): break
self.apiAction.clear()
while self.api_queue:
self.api_queue.popleft()()
self.logger.info("API Loop Closed")
# Initialize stored settings that can be changed within Synchtube.
# In the case of any error, default to everything being disabled.
def _initPersistentSettings(self):
self.logger.debug("Reading persistent settings.")
f = None
try:
f = open("persistentsettings", "rb")
line = f.readline()
while line and line[0] == '#':
line = f.readline()
if line == "ON\n" or line == "OFF\n":
version = 0
else:
version = int(line.strip())
line = f.readline()
self.autoLead = (line == "ON\n")
line = f.readline()
self.autoSkip = line[:-1]
line = f.readline()
self.unregSpamBan = (line == "ON\n")
line = f.readline()
self.commandLock = ""
if (version >= 1):
self.commandLock = line[:-1]
line = f.readline()
self.hybridModStatus = (line == "ON\n")
self.hybridModList = {}
line = f.readline()
while line:
line = line.strip().split(' ', 1)
self.hybridModList[line[0]] = int(line[1])
line = f.readline()
except Exception as e:
self.logger.debug("Reading persistent settings failed.")
self.logger.debug(e)
self.autoLead = False
self.autoSkip = "none"
self.hybridModStatus = False
self.hybridModList = {}
self.unregSpamBan = False
self.commandLock = ""
finally:
if f:
f.close()
def _initHandlers(self):
self.handlers = {"<" : self.chat,
"leader" : self.leader,
"users" : self.users,
"recording?" : self.roomSetting,
"tv?" : self.roomSetting,
"skip?" : self.roomSetting,
"lock?" : self.roomSetting,
"public?" : self.roomSetting,
"history" : self.roomSetting,
"vote_settings" : self.roomSetting,
"playlist_rules" : self.roomSetting,
"num_votes" : self.roomSetting,
"self" : self.selfInfo,
"add_user" : self.addUser,
"remove_user" : self.remUser,
"nick" : self.nick,
"pm" : self.play,
"am" : self.addMedia,
"cm" : self.changeMedia,
"rm" : self.removeMedia,
"mm" : self.moveMedia,
"s" : self.changeState,
"playlist" : self.playlist,
"shuffle" : self.shuffle,
"initdone" : self.initDone,
"clear" : self.clear,
"banlist" : self.banlist}
def _initCommandHandlers(self):
self.commandHandlers = {"restart" : self.restart,
"steal" : self.steal,
"lead" : self.lead,
"mod" : self.makeLeader,
"mute" : self.mute,
"unmute" : self.unmute,
"status" : self.status,
"lock" : self.lock,
"unlock" : self.lock,
"choose" : self.choose,
"permute" : self.permute,
"ask" : self.ask,
"8ball" : self.eightBall,
"ponyroll" : self.ponysay,
"kick" : self.kick,
"steak" : self.steak,
"ban" : self.ban,
"skip" : self.skip,
"d" : self.dice,
"dice" : self.dice,
"bump" : self.bump,
"clean" : self.cleanList,
"duplicates" : self.cleanDuplicates,
"delete" : self.delete,
"lastbans" : self.lastBans,
"lastban" : self.lastBans,
"addrandom" : self.addRandom,
"purge" : self.purge,
"cleverbot" : self.cleverbot,
"translate" : self.translate,
"wolfram" : self.wolfram,
"unban" : self.unban,
"banlist" : self.getBanlist,
"eval" : self.eval,
"setskip" : self.setSkip,
"help" : self.help,
"hybridmods" : self.hybridMods,
"permissions" : self.permissions,
"autolead" : self.autoLeader,
"autosetskip" : self.autoSetSkip,
"poll" : self.poll,
"endpoll" : self.endPoll,
"shuffle" : self.shuffleList,
"unregspamban" : self.setUnregSpamBan,
"commandlock" : self.setCommandLock,
"add" : self.add,
"quote" : self.quote,
"accident" : self.accident}
def _initIRCCommandHandlers(self):
self.ircCommandHandlers = {"status" : self.status,
"choose" : self.choose,
"permute" : self.permute,
"ask" : self.ask,
"8ball" : self.eightBall,
"steak" : self.steak,
"d" : self.dice,
"dice" : self.dice,
"cleverbot" : self.cleverbot,
"translate" : self.translate,
"wolfram" : self.wolfram,
"eval" : self.eval,
"help" : self.help,
"quote" : self.quote}
# Handle chat commands from both IRC and Synchtube
def chatCommand(self, user, msg, irc=False):
if not msg or msg[0] != '$': return
if self.commandLock == "Mods" and not user.mod:
return
elif self.commandLock == "Registered" and not user.uid:
return
elif self.commandLock == "Named" and user.nick == "unnamed":
return
commands = self.commandHandlers
if irc:
commands = self.ircCommandHandlers
line = msg[1:].split(' ', 1)
command = line[0].lower()
try:
if len(line) > 1:
arg = line[1].strip()
else:
arg = ''
fn = commands[command]
except KeyError:
# Dice is a special case
if re.match(r"^[0-9]+d[0-9]+$", command):
self.dice(command, user, " ".join(command.split('d')))
else:
self.logger.warn("No handler for %s [%s]", command, arg)
else:
fn(command, user, arg)
# Executes a function in the main Synchtube thread
def stExecute(self, action):
self.st_action_queue.append(action)
self.stAction.set()
def nextVideo(self):
self.vidLock.acquire()
try:
videoIndex = self.getVideoIndexById(self.state.current)
if videoIndex == None:
videoIndex = -1
if not self.vidlist or (len(self.vidlist) == 1 and videoIndex >= 0):
self.logger.debug("Empty list, playing default video.")
# Hardcoded video.
self.send("cm", ["yt", "hGqyJmlJ-MY", u"\u304a\u3061\u3083\u3081\u6a5f\u80fd\u3092\u9ed2\u5b50\u3063\u307d\u304f\u6b4c\u3063\u3066\u307f\u305f" ,"http://i.ytimg.com/vi/hGqyJmlJ-MY/default.jpg", 92])
self.sql_queue.append(package(self.addRandom, "addrandom", self.selfUser, ""))
self.sqlAction.set()
else:
videoIndex = (videoIndex + 1) % len(self.vidlist)
self.logger.debug("Advancing to next video [%s]", self.vidlist[videoIndex])
self.send("s", [2])
self.send("pm", self.vidlist[videoIndex].v_sid)
self.enqueueMsg("Playing: %s" % (self.filterString(self.vidlist[videoIndex].vidinfo.title)[1]))
self.state.time = int(round(time.time() * 1000))
finally:
self.vidLock.release()
def disableIRC(self, reason):
self.irc_logger.warning(reason)
self.sendChat(reason)
self.irc_nick = None
self.irc_queue = deque(maxlen=0)
self.ircclient.close()
# Enqueues a message for sending to both IRC and Synchtube
# This should not be used for bridging chat between IRC and Synchtube
def enqueueMsg(self, msg):
self.irc_queue.append(msg)
self.st_queue.append(msg)
def close(self):
self.closeLock.acquire()
if self.closing.isSet():
self.closeLock.release()
return
self.closing.set()
self.closeLock.release()
self.client.close()
self.repl.close()
self.leading.set()
self.playerAction.set()
self.apiAction.set()
self.sqlAction.set()
self.stAction.set()
if self.irc_nick and self.ircclient:
self.ircclient.close()
# Bans a user for changing to an invalid name
def nameBan(self, sid):
if self.pending.has_key(sid): return
self.pending[sid] = True
user = self.userlist[sid]
self.logger.info("Attempted ban of %s for invalid characters in name", user.nick)
reason = "Name [%s] contains illegal characters" % user.nick
self.asLeader(package(self._banUser, sid, reason))
def sendChat(self, msg):
self.logger.debug(repr(msg))
self.send("<", msg)
def send(self, tag='', data=''):
buf = []
if not tag == '':
buf.append(tag)
if not data == '':
buf.append(data)
try:
buf = json.dumps(buf, encoding="utf-8")
except UnicodeDecodeError:
buf = json.dumps(buf, encoding="iso-8859-15")
self.client.send(3, data=buf)
def _turnOnTV(self):
# Short sleep to give Synchtube some time to react
time.sleep(0.05)
self.tossing = True
self.pendingToss = False
self.notGivingBack = False
self.unToss = package(self.send, "turnoff_tv")
self.send("turnon_tv")
def checkVideo(self, vidinfo):
if not self.checkVideoId(vidinfo):
self.invalidVideo("Invalid video ID.")
return
# appendleft so it doesn't wait for the entire playlist to be checked
self.api_queue.appendleft(package(self._checkVideo, vidinfo))
self.apiAction.set()
# Skips the current invalid video if she is leading.
# Otherwise saves that information for if she does take lead.
def invalidVideo(self, reason):
if reason:
if self.leading.isSet():
self.enqueueMsg(reason)
self.nextVideo()
else:
self.state.reason = reason
# Kicks a user for something they did in chat
# Tracks kicks by username for a three strikes policy
def chatKick(self, user, reason):
if self.pending.has_key(user.sid):
return
else:
self.pending[user.sid] = True
if self.banTracker.has_key(user.nick):
self.banTracker[user.nick] = self.banTracker[user.nick] + 1
else:
self.banTracker[user.nick] = 1
reason = "[%d times] %s" % (self.banTracker[user.nick], reason)
if self.banTracker[user.nick] >= 3 or (not user.uid and self.unregSpamBan):
self.banTracker.pop(user.nick)
self.asLeader(package(self._banUser, user.sid, reason))
else:
self.asLeader(package(self._kickUser, user.sid, reason))
# Handlers for Synchtube message types
# All of them receive input in the form (tag, data)
def addMedia(self, tag, data):
self._addVideo(data)
def removeMedia(self, tag, data):
self._removeVideo(data)
def moveMedia(self, tag, data):
after = None
if "after" in data:
after = data["after"]
self._moveVideo(data["id"], after)
def playlist(self, tag, data):
self.clear(tag, None)
for v in data:
self._addVideo(v, False, False)
def clear(self, tag, data):
self.vidLock.acquire()
self.vidlist = []
self.vidLock.release()
def shuffle(self, tag, data):
self._shuffle(data)
if self.shuffleBump:
self._bump((self.shuffleBump, ))
self.shuffleBump = False
if self.deferredToss & self.DEFERRED_MASKS["SHUFFLE"]:
self.deferredToss &= ~self.DEFERRED_MASKS["SHUFFLE"]
if not self.deferredToss:
self.tossLeader()
def changeState(self, tag, data):
self.logger.debug("State is %s %s", tag, data)
if data == None:
# Just assume whatever is loaded is playing correctly.
if tag == "cm":
self.state.state = 1
else:
self.state.state = 0
self.state.time = int(round(time.time() * 1000))
else:
self.state.state = data[0]
if self.state.state == 2:
self.state.pauseTime = time.time()
self.logger.debug("Paused %.3f seconds from the beginning of the video." % (self.state.pauseTime - (self.state.time/1000)))
elif len(data) > 1:
self.state.time = data[1]
else:
self.state.time = int(round(time.time() * 1000))
self.playerAction.set()
def play(self, tag, data):
self._play()
self.state.current = data[1]
self.state.reason = None
index = self.getVideoIndexById(self.state.current)
if index == None:
self.sendChat("Unexpected video, restarting.")
self.close()
return
self.state.dur = self.vidlist[index].vidinfo.dur
self.checkVideo(self.vidlist[index].vidinfo)
self.changeState(tag, data[2])
self.logger.debug("Playing %s %s", tag, data)
def changeMedia(self, tag, data):
self._play()
self.logger.info("Change media: %s" % (data))
self.state.current = data[0]
self.state.reason = None
# Prevent her from skipping something she does not recognize, like a livestream.
# HOWEVER, this will require a mod to tell her to skip before DEFAULT_WAIT seconds.
self.state.dur = DEFAULT_WAIT
v = data[1]
if len(v) < len(SynchtubeVidInfo._fields):
v.extend([None] * (len(SynchtubeVidInfo._fields) - len(v))) # If an unregistered adds a video there is no name included
v = v[:len(SynchtubeVidInfo._fields)]
v[2] = self.filterString(v[2])[1]
vi = SynchtubeVidInfo(*v)
# Have to assume it's a valid video if it's not from one of these sites.
if vi.site in ["yt", "bt", "dm", "vm", "sc"]:
self.checkVideo(vi)
self.changeState(tag, data[2])
# Actions required when a video starts playing with Naoko as the leader.
def _play(self):
if self.leading.isSet() or self.deferredToss & self.DEFERRED_MASKS["SKIP"]:
if (not self.state.current == None) and (not self.getVideoIndexById(self.state.current) == None):
self.send("rm", self.state.current)
self.send("s", [1,0])
if self.deferredToss & self.DEFERRED_MASKS["SKIP"]:
self.deferredToss &= ~self.DEFERRED_MASKS["SKIP"]
if not self.deferredToss:
self.tossLeader()
def ignore(self, tag, data):
self.logger.debug("Ignoring %s, %s", tag, data)
def nick(self, tag, data):
sid = data[0]
valid, nick = self.filterString(data[1], True)
oldnick = self.userlist[sid].nick
self.logger.debug("%s nick: %s (was: %s)", sid, nick, oldnick)
self.userlist[sid]= self.userlist[sid]._replace(nick=nick)
if not valid:
self.nameBan(sid)
return
user = self.userlist[sid]
if sid == self.sid:
self.selfUser = user
if user.mod or user.sid == self.sid: return
if user.nickChanges > 5 or (user.nickChanges > 0 and not nick == oldnick):
if self.pending.has_key(sid):
return
else:
# Only a script/bot can change nicks to different nicks multiple times.
# It is possible for it to glitch and a user can change to the same nick several times.
# In order to reduce false positives while still catching nick flood attempts a threshhold of 5 was chosen.
self.pending[sid] = True
self.logger.info("Attempted ban of %s for %d nick changes", (user.nick, user.nickChanges))
reason = "%s changed names %d times" % (user.nick, user.nickChanges)
self.asLeader(package(self._banUser, sid, reason))
else:
self.userlist[sid] = user._replace(nickChanges=user.nickChanges+1)
def addUser(self, tag, data, isSelf=False):
# add_user and users data are similar aside from users having
# a name field at idx 1
userinfo = data[:]
userinfo.insert(1, 'unnamed')
self._addUser(userinfo, isSelf)
self.storeUserCount()
self.updateSkipLevel()
def remUser(self, tag, data):
try:
del self.userlist[data]
if self.pending.has_key(data):
del self.pending[data]
except KeyError:
self.logger.exception("Failure to delete user %s from %s", data, self.userlist)
self.storeUserCount()
self.updateSkipLevel()
def users(self, tag, data):
for u in data:
self._addUser(u)
def selfInfo(self, tag, data):
self.addUser(tag, data, isSelf=True)
self.sid = data[0]
if not self.pw:
self.send("nick", self.name)
def roomSetting(self, tag, data):
self.room_info[tag] = data
if tag == "tv?" and self.room_info["tv?"]:
self.tossing = False
self.leader_sid = None
self.leading.clear()
if tag == "skip?" or tag == "vote_settings":
self.updateSkipLevel()
if tag == "num_votes":
self.checkSkip()
def banlist(self, tag, data):
if not self.unbanTarget:
# If there is no pending unban simply display the list.
out = []
for ban in data:
if not self.verboseBanlist:
out.append(self.filterString(ban[0], True)[1])
else:
out.append("%s %s" % (self.filterString(ban[0], True)[1], ban[1]))
self.verboseBanlist = False
self.enqueueMsg("Banlist: %s" % (", ".join(out)))
else:
# If there is a pending unban perform it if a target can be found.
self.logger.info("Unbanning %s" % (self.unbanTarget))
target = None
for ban in data:
if self.unbanTarget.lower() == ban[0].lower():
self.unbanTarget = ban[1]
if self.unbanTarget == ban[1]:
target = ban[1]
break
if target and self.leader_sid == self.sid:
self.send("unban", {"id": target})
self.unbanTarget = None
if self.deferredToss & self.DEFERRED_MASKS["UNBAN"]:
self.deferredToss &= ~self.DEFERRED_MASKS["UNBAN"]
if not self.deferredToss:
self.tossLeader()
def chat(self, tag, data):
sid = data[0]
user = self.userlist[sid]
msg = data[1]
self.chat_logger.info("%s: %r" , user.nick, msg)
self.sql_queue.append(package(self.insertChat, msg=msg, username=user.nick,
userid=user.uid, timestamp=None, protocol='ST', channel=self.room, flags=None))
self.sqlAction.set()
if not user.sid == self.sid and self.irc_nick:
self.irc_queue.append("(" + user.nick + ") " + msg)
self.chatCommand(user, msg)
if user.mod or user.sid == self.sid: return
user.msgs.append(time.time())
span = user.msgs[-1] - user.msgs[0]
if span < self.spam_interval * user.msgs.maxlen and len(user.msgs) == user.msgs.maxlen:
self.logger.info("Attempted kick/ban of %s for spam", user.nick)
reason = "%s sent %d messages in %1.3f seconds" % (user.nick, len(user.msgs), span)
self.chatKick(user, reason)
else:
# Currently the only two blacklisted phrases are links to other Synchtube rooms.
# Links to the current room or the Synchtube homepage aren't blocked.
m = re.search(r"(synchtube\.com\/r\/|synchtu\.be\/|clickbank\.net)(%s)?" % (self.room), msg, re.IGNORECASE)
if m and not m.groups()[1]:
self.logger.info("Attempted kick/ban of %s for blacklisted phrase", user.nick)
reason = "%s sent a blacklisted message" % (user.nick)
self.chatKick(user, reason)
def leader(self, tag, data):
self.logger.debug("Leader is %s", self.userlist[data])
self.leader_sid = data
self.tossing = False
if self.leader_sid == self.sid:
toss = self.pendingToss
self._leaderActions()
if not toss:
self.leading.set()
else:
self.leading.clear()
# Automatically set the skip mode and take leader if applicable.
# Setskip("none") will simply fail silently, so it is safe to call.
def initDone(self, tag, data):
self.storeUserCount()
self.updateSkipLevel()
self.doneInit = True
if self.autoLead:
self.asLeader(package(self.setSkip, "", self.selfUser, self.autoSkip), False)
else:
if self.leader_queue:
def fn():
return
self.asLeader(fn)
self.setSkip("", self.selfUser, self.autoSkip)
# Command handlers for commands that users can type in Synchtube chat
# All of them receive input in the form (command, user, data)
# Where command is the typed command, user is the user who sent the message
# and data is everything following the command in the chat message
def skip(self, command, user, data):
if not (user.mod or self.hasPermission(user, "SKIP")): return
self.asLeader(self.nextVideo, deferred=self.DEFERRED_MASKS["SKIP"])
def accident(self, command, user, data):
if not user.mod: return
self.enqueueMsg("A terrible accident has befallen the currently playing video.")
self.asLeader(self.nextVideo, deferred=self.DEFERRED_MASKS["SKIP"])
# Set the skipping mode. Takes either on, off, x, or x%.
def setSkip(self, command, user, data):
if not (user.mod or self.hasPermission(user, "SKIP")): return
m = re.match("^((on)|(off)|([1-9][0-9]*)(%)?)( .*)?$", data, re.IGNORECASE)
if m:
g = m.groups()
if g[2]:
if self.room_info["skip?"]:
self.asLeader(package(self.send, "skip?", False))
settings = None
if g[1]:
if not self.room_info["skip?"]:
if "vote_settings" in self.room_info:
settings = self.room_info["vote_settings"]
else:
# If there is no known previous setting, default to 33%.
settings = {"settings" : "percent", "num" : 33}
if g[3]:
settings = {"num" : int(g[3]), "settings" : ("percent" if g[4] else "thres")}
if settings:
self.asLeader(package(self._setSkip, settings.copy()))
def _setSkip(self, settings):
self.send("skip?", True)
self.send("vote_settings", settings)
def autoSetSkip(self, command, user, data):
if not (user.mod or self.hasPermission(user, "AUTOSKIP")): return
m = re.match("^((none)|(on)|(off)|([1-9][0-9]*)(%)?)( .*)?$", data, re.IGNORECASE)
if m:
self.autoSkip = m.groups()[0].lower()
self.enqueueMsg("Automatic skip mode set to: %s" % (self.autoSkip))
self._writePersistentSettings()
else:
self.enqueueMsg("Invalid skip setting.")
def autoLeader(self, command, user, data):
if not (user.mod or self.hasPermission(user, "AUTOLEAD")): return
d = data.lower()
if d == "on":
self.autoLead = True
self.enqueueMsg("Automatic leading is enabled.")
elif d == "off":
self.autoLead = False
self.enqueueMsg("Automatic leading is disabled.")
else: return
self._writePersistentSettings()
def setUnregSpamBan(self, command, user, data):
if not (user.mod or self.hasPermission(user, "UNREGSPAMBAN")): return
d = data.lower()
if d == "on":
self.unregSpamBan = True
self.enqueueMsg("Unregistered spammers will be banned for the first offense.")
elif d == "off":
self.unregSpamBan = False
self.enqueueMsg("Unregistered spammers will have three chances.")
else: return
self._writePersistentSettings()
def setCommandLock(self, command, user, data):
if not user.mod: return
d = data.lower()
if d == "registered":
self.commandLock = "Registered"
self.enqueueMsg("Unregistered users are unable to use commands.")
elif d == "named":
self.commandLock = "Named"
self.enqueueMsg("Unnamed users are unable to use commands.")
elif d == "mods":
self.commandLock = "Mods"
self.enqueueMsg("Only mods may use commands.")
elif d == "off":
self.commandLock = ""
self.enqueueMsg("Unregistered users can use commands.")
else: return
self._writePersistentSettings()
def shuffleList(self, command, user, data):
if not (user.mod or self.hasPermission(user, "SHUFFLE")): return
self.shuffleBump = self.state.current
self.asLeader(package(self.send, "shuffle"), deferred=self.DEFERRED_MASKS["SHUFFLE"])
def help(self, command, user, data):
self.enqueueMsg("I only do this out of pity. https://raw.github.com/Suwako/Naoko/master/commands.txt")
#self.enqueueMsg("I refuse; you are beneath me.")
# Creates a poll given an asterisk separated list of strings containing the title and at least two choices.
def poll(self, command, user, data):
if not (user.mod or self.hasPermission(user, "POLL")): return
elements = data.split("*")
# Filter out any empty or whitespace strings
i = len(elements) - 1
while i >= 0:
if not elements[i].strip():
elements.pop(i)
i-=1
if len(elements) < 3: return
self.asLeader(package(self._poll, list(elements)))
def _poll(self, elements):
self.send("close_poll")
self.send("init_poll", [elements[0], elements[1:]])
def endPoll(self, command, user, data):
if not (user.mod or self.hasPermission(user, "POLL")): return
self.asLeader(package(self.send, "close_poll"))
def mute(self, command, user, data):
if user.mod or self.hasPermission(user, "MUTE"):
self.muted = True
def unmute(self, command, user, data):
if user.mod or self.hasPermission(user, "MUTE"):
self.muted = False
def steal(self, command, user, data):
if not (user.mod or self.hasPermission(user, "LEAD")): return
self.changeLeader(user.sid)
def lead(self, command, user, data):
if not (user.mod or user.sid == self.leader_sid or self.hasPermission(user, "LEAD")): return
self.changeLeader(self.sid)
def makeLeader(self, command, user, data):
if not (user.mod or user.sid == self.leader_sid or self.hasPermission(user, "LEAD")): return
args = data.split(' ', 1)
target = self.getUserByNick(args[0])
self.logger.info("Requested mod change to %s by %s", target, user)
if not target: return
self.changeLeader(target.sid)
def dice(self, command, user, data):
if not data: return
params = data.split()
if len(params) < 2: return
num = 0
size = 0
try:
num = int(params[0])
size = int(params[1])
if num < 1 or size < 1 or num > 1000 or size > 1000: return # Limits
sum = 0
i = 0
output = []
while i < num:
rand = random.randint(1, size)
if i < 5:
output.append(str(rand))
if i == 5:
output.append("...")
sum = sum + rand
i = i+1
self.enqueueMsg("%dd%d: %d [%s]" % (num, size, sum, ",".join(output)))
except (TypeError, ValueError) as e:
self.logger.debug(e)
# Bumps the last video added by the specificied user
# If no name is provided it bumps the last video by the user who sent the command
def bump(self, command, user, data):
if not (user.mod or self.hasPermission(user, "BUMP")): return
p = self.parseParameters(data, 0b10011)
if not p: return
name, num, title = p["base"], p["num"], p["title"]
if not name == None:
if name == "-unnamed":
name = ""
elif name == "-all":
name = None
else:
name = self.filterString(name, True)[1]
if not name: return
else:
if not title:
name = user.nick.lower()
if not num:
num = 1
else:
if num > 10 or num < 1: return
videoIndex = self.getVideoIndexById(self.state.current)
bumpList = []
i = len(self.vidlist)
while i > videoIndex + 2 and len(bumpList) < num:
i -= 1
v = self.vidlist[i]
# Match names
if not (None == name or v.nick.lower() == name): continue
# Titles
if title and v.vidinfo.title.lower().find(title) == -1: continue
bumpList.append(v.v_sid)
if bumpList:
after = None
if videoIndex >= 0:
after = self.vidlist[videoIndex].v_sid
self.asLeader(package(self._bump, list(bumpList), after))
def _bump(self, targets, after=None):
for t in targets:
output = {"id" : t}
if after:
output["after"] = after
self.send("mm", output)
self.moveMedia("mm", output)
# Cleans all the videos above the currently playing video
def cleanList(self, command, user, data):
if not (user.mod or self.hasPermission(user, "CLEAN")): return
videoIndex = self.getVideoIndexById(self.state.current)
if videoIndex > 0:
self.logger.debug("Cleaning %d Videos", videoIndex)
self.asLeader(package(self._cleanList, videoIndex))
def _cleanList(self, videoIndex):
i = 0
while i < videoIndex:
self.send("rm", self.vidlist[i].v_sid)
i+=1
# Clears any duplicate videos from the list
def cleanDuplicates(self, command, user, data):
if not (user.mod or self.hasPermission(user, "DUPLICATES")): return
kill = []
vids = set()
i = 0
while i < len(self.vidlist):
key = "%s:%s" % (self.vidlist[i].vidinfo.site, self.vidlist[i].vidinfo.vid)
if key in vids:
if not self.vidlist[i].v_sid == self.state.current:
kill.append(self.vidlist[i].v_sid)
else:
vids.add(key)
i += 1
if kill:
self.asLeader(package(self._cleanPlaylist, list(kill)))
# Deletes all the videos posted by the specified user,
# with a specific pattern in their title (min 4 characters), or longer than a certain duration (min 20 minutes).
# If multiple options are provided it will only remove videos that match all of the criteria.
# Combines the previous removelong and purge functions together, with more functionality.
# Mods can purge themselves, and Naoko can be purged only by a mod.
def purge(self, command, user, data):
if not (user.mod or self.hasPermission(user, "PURGE")): return
p = self.parseParameters(data, 0b111)
if not p: return
name, duration, title = p["base"], p["dur"], p["title"]
if not name == None:
if name == "-unnamed":
name = ""
else:
name = self.filterString(name, True)[1]
if not name: return
if name == None and not duration and not title: return
if duration and duration < 25 * 60: return
# Only mods can purge themselves or Naoko.
if name and (name in self.modList and not (name == user.nick.lower() or (user.mod and name == self.name.lower()))):
return
kill = []
for v in self.vidlist:
# Only purge videos that match all criteria
if not v.v_sid == self.state.current and (name == None or v.nick.lower() == name) and (not duration
or v.vidinfo.dur >= duration) and (not title or v.vidinfo.title.lower().find(title) != -1):
kill.append(v.v_sid)
if kill:
self.asLeader(package(self._cleanPlaylist, list(kill)))
def _cleanPlaylist(self, kill):
for x in kill:
self.send("rm", x)
# Deletes the last video matching the given criteria. Same parameters as purge, but if nothing is given it will default to the last video
# posted by the user who calls it.
def delete(self, command, user, data):
# Due to the way Synchtube handles videos added by unregistered users they are
# unable to delete their own videos. This prevents them abusing it to delete
# videos added by registered users.
if not user.uid: return
p = self.parseParameters(data, 0b10111)
if not p: return
name, duration, title, num = p["base"], p["dur"], p["title"], p["num"]
if not name == None:
if name == "-unnamed":
name = ""
elif name == "-all":
name = None
else:
name = self.filterString(name, True)[1]
if not name: return
else:
if not duration and not title:
name = user.nick.lower()
if not num:
num = 1
else:
if num > 10 or num < 1: return
# Non-mods and non-hybrid mods can only delete their own videos
# This does prevent unregistered users from deleting their own videos
if (not user.nick.lower() == name or title or duration) and not (user.mod or self.hasPermission(user, "DELETE")): return
videoIndex = self.getVideoIndexById(self.state.current)
kill = []
i = len(self.vidlist)
while i > videoIndex + 1 and len(kill) < num:
i -= 1
v = self.vidlist[i]
# Match names
if None != name and v.nick.lower() != name: continue
# Titles
if title and v.vidinfo.title.lower().find(title) == -1: continue
# Durations
if duration and v.vidinfo.dur < duration: continue
kill.append(v.v_sid)
if kill:
self.asLeader(package(self._cleanPlaylist, list(kill)))
# Adds random videos from the database
def addRandom(self, command, user, data):
# Limit to once every 5 seconds
if time.time() - self.last_random < 5: return
self.last_random = time.time()
if not (user.mod or len(self.vidlist) <= 10 or self.hasPermission(user, "RANDOM")): return
p = self.parseParameters(data, 0b1111)
if not p: return
num, duration, title, username = p["base"], p["dur"], p["title"], p["user"]
if duration or title or username:
if not (user.mod or self.hasPermission(user, "RANDOM")): return
if not duration:
duration = 600
try:
num = int(num)
if num > 20 or (not user.mod and not self.hasPermission(user, "RANDOM") and num > 5) or num < 1: return
except (TypeError, ValueError) as e:
if num: return
num = 5
self.sql_queue.append(package(self._addRandom, num, duration, title, username))
self.sqlAction.set()
# Retrieve the latest bans for the specified user
def lastBans(self, command, user, data):
params = data.split()
target = user.nick
num = 1
if params and user.mod:
target = params[0]
if len(params) > 1 and command == "lastbans":
try:
num = int(params[1])
except (TypeError, ValueError) as e:
self.logger.debug(e)
if num > 5 or num < 1: return
self.sql_queue.append(package(self._lastBans, target, num))
self.sqlAction.set()
def add(self, command, user, data, store=True):
if self.room_info["lock?"]:
if not (user.mod or self.hasPermission(user, "ADD")):
return
nick = user.nick
if not user.uid:
nick = ""
site = False
vid = False
if data.find("youtube") != -1:
x = data.find("v=")
if x != -1:
site = "yt"
vid = data[x + 2:x + 13]
elif data.find("youtu.be") != -1:
x = data.find("be/")
if x != -1:
site = "yt"
vid = data[x + 3:x + 14]
elif data.find("vimeo") != -1:
x = data.find(".com/")
if x != -1:
site = "vm"
vid = data[x+5:x+13]
elif data.find("dailymotion") != -1:
x = data.find("video/")
if x != -1:
site = "dm"
vid = data[x+6:x+12]
elif data.find("blip.tv") != -1:
site = "bt"
vid = data[-7:]
elif data.find("soundcloud") != -1:
# Soundcloud URLs do not contain ids so additional steps are required.
site = "sc"
vid = data
if site and (site == "sc" or self._checkVideoId(site, vid)):
self.api_queue.appendleft(package(self._add, site, vid, nick, store))
self.apiAction.set()
# Add an individual video after verifying it
def _add(self, site, vid, nick, store):
if site == "sc":
vid = self.apiclient.resolveSoundcloud(vid)
if not vid: return
data = self.apiclient.getVideoInfo(site, vid)
if not data or data == "Unknown":
return
title, dur, valid = data
if valid:
self.logger.debug("Adding video %s %s %s %s", title, site, vid, dur)
self.stExecute(package(self.asLeader, package(self.send, "am", [site, vid, self.filterString(title)[1], "http://i.ytimg.com/vi/%s/default.jpg" % (vid), dur])))
if store:
self.sql_queue.append(package(self.insertVideo, site, vid, title, dur, nick))
self.sqlAction.set()
def lock(self, command, user, data):
if not (user.mod or self.hasPermission(user, "LOCK")): return
if self.room_info["lock?"] == (command == "lock"): return
self.asLeader(package(self.send, "lock?", command == "lock"))
def status(self, command, user, data):
msg = "Status = ["
if not self.muted:
msg += "Not "
msg += "Muted, Hybrid Mods "
msg += "Enabled" if self.hybridModStatus else "Disabled"
msg += ", Automatic Leading "
msg += "Enabled" if self.autoLead else "Disabled"
msg += ", Automatic Skip Mode: %s" % (self.autoSkip)
msg += ", Unregistered Spammers: "
msg += "One Chance" if self.unregSpamBan else "3 Chances"
msg += ", Command Lock: "
msg += "%s]" % (self.commandLock if self.commandLock else "Disabled")
self.sendChat(msg)
if self.irc_nick and self.ircclient:
self.ircclient.sendMsg(msg)
def hybridMods(self, command, user, data):
if not user.mod: return
d = data.lower()
if d == "on":
self.hybridModStatus = True
self.enqueueMsg("Hybrid mods enabled.")
self._writePersistentSettings()
if d == "off":
self.hybridModStatus = False
self.enqueueMsg("Hybrid mods disabled.")
self._writePersistentSettings()
if not d:
output = []
for h, v in self.hybridModList.iteritems():
if v:
output.append(h)
self.enqueueMsg("Hybrid Mods: %s" % ",".join(output))
# Displays and possibly modifies the permissions of a hybrid mod.
def permissions(self, command, user, data):
m = re.match(r"^((\+|\-)((ALL)|(.*)) )?(.*)$", data.upper())
if not m: return
g = m.groups()
if g[5]:
if not user.mod: return
valid, name = self.filterString(g[5], True)
if not valid:
self.enqueueMsg("Invalid name.")
return
else:
if g[0]:
self.enqueueMsg("No name given.")
return
# Default to displaying the permissions for the current user.
name = user.nick
name = name.lower()
p = 0
if name in self.hybridModList:
p = self.hybridModList[name]
# Change permissions before displaying them.
# Only change permissions if the calling user is a mod, a valid name was given, and flags were specified.
# Also check whether a hybrid mod administrator is set.
if user.mod and g[0] and g[5] and ((not self.hmod_admin) or self.hmod_admin.lower() == user.nick.lower()):
mask = 0
if g[3]:
mask = ~0
if g[4]:
for ma, k in self.MASKS.itervalues():
if g[4].find(k) != -1:
mask |= ma
if g[1] == '+':
p |= mask
else:
p &= ~mask
self.hybridModList[name] = p
self._writePersistentSettings()
output = []
for ma, k in self.MASKS.itervalues():
if p & ma:
output.append(k)
self.enqueueMsg("Permissions for %s: %s" % (name, "".join(output)))
def restart(self, command, user, data):
if user.mod or self.hasPermission(user, "RESTART"):
self.close()
def choose(self, command, user, data):
if not data: return
self.enqueueMsg("[Choose: %s] %s" % (data, random.choice(data.split())))
def permute(self, command, user, data):
if not data: return
choices = data.split()
random.shuffle(choices)
self.enqueueMsg("[Permute] %s" % (" ".join(choices)))
def steak(self, command, user, data):
self.enqueueMsg("There is no steak.")
def ask(self, command, user, data):
if not data: return
self.enqueueMsg("[Ask: %s] %s" % (data, random.choice(["Yes", "No"])))
def eightBall(self, command, user, data):
if not data: return
self.enqueueMsg("[8ball: %s] %s" % (data, random.choice(eight_choices)))
def ponysay(self, command, user, data):
if not data: return
self.enqueueMsg("[ponyroll: %s] %s" % (user.nick, random.choice(pony_choices)))
def quote(self, command, user, data):
# Limit to once every 5 seconds
if time.time() - self.last_quote < 5: return
self.last_quote = time.time()
self.sql_queue.append(package(self._quote, data))
self.sqlAction.set()
def _quote(self, name):
row = self.dbclient.getQuote(name, self.name)
if row:
self.enqueueMsg("[%s %s] %s" % (row[0], datetime.fromtimestamp(row[2] / 1000).isoformat(' '), row[1]))
# Kick a single user by their name.
# Two special arguments -unnamed and -unregistered.
# Those commands kick all unnammed and unregistered users.
def kick(self, command, user, data):
if not data or not (user.mod or self.hasPermission(user, "KICK")): return
args = data.split(' ', 1)
if args[0].lower() == "-unnamed":
if not user.mod: return
kicks = []
for u in self.userlist:
if self.userlist[u].nick == "unnamed":
kicks.append(u)
self.logger.info("Kicking %d unnamed users requested by %s", len(kicks), user.nick)
self.asLeader(package(self._kickList, kicks))
return
if args[0].lower() == "-unregistered":
if not user.mod: return
kicks = []
for u in self.userlist:
# Synchtube doesn't properly set user.auth in some cases.
# A more reliable method without false positives is user.uid.
if self.userlist[u].uid == None:
kicks.append(u)
self.logger.info("Kicking %d unregistered users requested by %s", len(kicks), user.nick)
self.asLeader(package(self._kick, kicks))
return
target = self.getUserByNick(args[0])
if not target or target.mod: return
self.logger.info("Kick Target %s Requestor %s", target.nick, user.nick)
if len(args) > 1:
self.asLeader(package(self._kickUser, target.sid, args[1]))
else:
self.asLeader(package(self._kickUser, target.sid))
def _kickList(self, kicks):
for k in kicks:
self._kickUser(k, sendMessage=False)
def ban(self, command, user, data):
if not data or not (user.mod or self.hasPermission(user, "BAN")): return
args = data.split(' ', 1)
target = self.getUserByNick(args[0])
if not target or target.mod: return
self.logger.info("Ban Target %s Requestor %s", target, user)
if len(args) > 1:
self.asLeader(package(self._banUser, target.sid, args[1], modName=user.nick))
else:
self.asLeader(package(self._banUser, target.sid, modName=user.nick))
def unban(self, command, user, data):
if not (user.mod or self.hasPermission(user, "BAN")): return
target = data
if not target: return
self.unbanTarget = target
self.getBanlist(command, user, data)
def getBanlist(self, command, user, data):
if not (user.mod or self.hasPermission(user, "BAN")): return
if data.lower() == "-v":
self.verboseBanlist = True
# If she is trying to unban a user defer the current ban.
self.asLeader(package(self.send, "banlist"), deferred=(self.DEFERRED_MASKS["UNBAN"] if self.unbanTarget else 0))
def cleverbot(self, command, user, data):
if not hasattr(self.cbclient, "cleverbot"): return
text = data
if text:
self.api_queue.append(package(self._cleverbot, text))
self.apiAction.set()
def _cleverbot(self, text):
self.enqueueMsg(self.cbclient.cleverbot(text))
def eval(self, command, user, data):
self.enqueueMsg("You're not the boss of me.")
# Translate a given string.
# Defaults to translating to English and detecting the source language.
# If the string starts with [src->dst], [src>dst], or [dst] where src and dst
# are ISO two letter language code it will attempt to translate using those codes.
def translate(self, command, user, data):
m = re.match("^(\[(([a-zA-Z]{2})|([a-zA-Z]{2})-?>([a-zA-Z]{2}))\] ?)?(.+)$", data)
if not m: return
g = m.groups()
src = g[3] or None
dst = g[2] or g[4] or "en"
self.api_queue.append(package(self._translate, g[5], src, dst))
self.apiAction.set()
def _translate(self, text, src, dst):
out = self.apiclient.translate(text, src, dst)
if out:
if out != -1:
self.enqueueMsg("[%s] %s" % (dst.lower(), out))
else:
self.enqueueMsg("Translate query failed.")
# Queries the Wolfram Alpha API with the provided string.
def wolfram(self, command, user, data):
query = data
if not query: return
self.api_queue.append(package(self._wolfram, query))
self.apiAction.set()
def _wolfram(self, query):
out = self.apiclient.wolfram(query)
if out:
if out != -1:
self.enqueueMsg("[%s] %s" % (query, out))
else:
self.enqueueMsg("Wolfram Alpha query failed.")
# Telnet commands
# Only callable through telnet
# Kicks everyone in the channel except Naoko.
def clearRoom(self, kickSelf=False):
self.stExecute(package(self.asLeader, package(self._kickList, (u for u in self.userlist.iterkeys() if kickSelf or u != self.sid))))
# Imports all the videos in <filename>.lst
# An lst file is simply a plain text file containing a list of videos, one per line.
def importFile(self, filename, name=False):
if name:
name = self.filterString(name, True, False)[1]
f = False
try:
f = file("%s.lst" % (filename), "r")
user = SynchtubeUser(*self.selfUser)
user = user._replace(nick=name)
for line in f:
self.add("add", user, line, name!=False)
# Sleep between adds, otherwise the Youtube API could throttle her, resulting in unpredictable behaviour.
# This sleep results in the adds taking a very long time for long lists, which can be very annoying, use this function sparingly.
time.sleep(0.5)
except Exception as e:
print e
return
finally:
if f != False:
f.close()
# Parses the parameters common to several functions.
# All returned values are lower case
# Returns a lone unfiltered string, often a name or number, a title specified by -title and quotes, and
# a duration in seconds. The title must be at least 3 characters.
# A mask is passed to determine which options are looked for.
# base : 1
# -title : 1 << 1
# -dur : 1 << 2
# -user : 1 << 3
# -n : 1 << 4
def parseParameters(self, data, mask):
text = data.lower().split("\"")
params = re.split(" +", text[0])
if len(text) == 3:
if not text[1] or len(text[1]) < 3: return
params.append(-1)
params.extend(re.split(" +", text[2]))
elif len(text) != 1: return
params = deque(params)
base = None
duration = None
title = None
user = None
num = None
# Could be done with a huge regexp but this is probably cleaner and easier to maintain.
while params:
t = params.popleft()
if not t: continue
if t == -1: return
if t == "-title" and mask & (1 << 1):
if title or not params or params.popleft(): return
if not params or not params.popleft() == -1 or len(text) < 3: return
title = text[1]
elif t == "-dur" and mask & (1 << 2):
if duration or not params: return
duration = params.popleft()
if not duration or duration == -1: return
m = re.match("^(([0-9]*):)?([0-9]*)$", duration)
if not m: return
length = 0
g = m.groups()
if g[1]:
length = int(g[1]) * 60
if g[2]:
length += int(g[2])
duration = length * 60
elif t == "-user" and mask & (1 << 3):
if user or not params: return
user = params.popleft()
if not user or user == -1: return
elif t == "-n" and mask & (1 << 4):
if num or not params: return
try:
num = int(params.popleft())
except Exception:
return
else:
if not t: continue
if not base == None or not mask & 1: return
base = t
return {"base" : base,
"dur" : duration,
"title" : title,
"user" : user,
"num": num}
# Two functions that search the lists in an efficient manner
def getUserByNick(self, nick):
name = self.filterString(nick, True)[1].lower()
try: return (u for u in self.userlist.itervalues() if u.nick.lower() == name).next()
except StopIteration: return None
def getVideoIndexById(self, vid):
try: return (idx for idx, ele in enumerate(self.vidlist) if ele.v_sid == vid).next()
except StopIteration: return -1
# Updates the required skip level
def updateSkipLevel(self):
if not self.doneInit: return
if not self.room_info["skip?"] or not "vote_settings" in self.room_info:
self.skipLevel = False
return
if self.room_info["vote_settings"]["settings"] == "percent":
self.skipLevel = int(math.ceil(self.room_info["vote_settings"]["num"] * len(self.userlist) / 100.0))
else:
self.skipLevel = self.room_info["vote_settings"]["num"]
# logs the user count to the database
def storeUserCount(self):
count = len(self.userlist)
storeTime = time.time()
if storeTime - self.userCountTime > USER_COUNT_THROTTLE:
self.userCountTime = storeTime
self.sql_queue.append(package(self.insertUserCount, count, storeTime))
self.sqlAction.set()
# Returns whether a specified user has the permission specified by the mask.
def hasPermission(self, user, mask):
# If hybrid mods are disabled or the user isn't logged in return False.
if not self.hybridModStatus or not user.uid: return False
n = user.nick.lower()
if n in self.hybridModList and (self.hybridModList[n] & self.MASKS[mask][0]):
return True
return False
def checkSkip(self):
if "num_votes" in self.room_info and self.room_info["num_votes"]["votes"] >= self.skipLevel:
self.skips.append(time.time())
if len(self.skips) == self.skips.maxlen and self.skips[-1] - self.skips[0] <= self.skips.maxlen * self.skip_interval:
self.setSkip("", self.selfUser, "off")
# Returns whether or not a video id could possibly be valid
# Guards against possible attacks and annoyances
def checkVideoId(self, vi):
if not vi.vid or not vi.site: return False
vid = vi.vid
if type(vid) is not str and type(vid) is not unicode:
vid = str(vid)
return self._checkVideoId(vi.site, vid)
def _checkVideoId(self, site, vid):
if site == "yt":
return re.match("^[a-zA-Z0-9\-_]+$", vid)
elif site == "dm":
return re.match("^[a-zA-A0-9]+$", vid)
elif site == "vm" or site == "sc" or site == "bt":
return re.match("^[0-9]+$", vid)
else:
return False
def takeLeader(self):
if self.sid == self.leader_sid and not self.tossing:
self._leaderActions()
return
if self.tossing:
self.unToss()
elif self.room_info["tv?"]:
self.send("turnoff_tv")
else:
self.send("takeleader", self.sid)
def asLeader(self, action=None, giveBack=True, deferred=0):
self.leader_queue.append(action)
if not self.doneInit: return
if giveBack and not self.pendingToss and not self.notGivingBack:
if self.leader_sid and self.leader_sid != self.sid:
oldLeader = self.leader_sid
self.pendingToss = True
self.deferredToss |= deferred
self.tossLeader = package(self._tossLeader, oldLeader)
if self.room_info["tv?"]:
self.pendingToss = True
self.deferredToss |= deferred
self.tossLeader = self._turnOnTV
if self.tossing:
self.pendingToss = True
self.deferredToss |= deferred
if not giveBack:
self.pendingToss = False
self.notGivingBack = True
self.deferredToss = 0
self.takeLeader()
def changeLeader(self, sid):
if sid == self.leader_sid: return
if sid == self.sid:
self.takeLeader()
return
self.pendingToss = True
self.tossLeader = package(self._tossLeader, sid)
self.takeLeader()
# Checks the currently playing video against a provided API
# Filters a string, removing invalid characters
# Used to sanitize nicks or video titles for printing
# Returns a boolean describing whether invalid characters were found
# As well as the filtered string
def filterString(self, input, isNick=False, replace=True):
if input == None: return (False, "")
output = []
value = input
if type(value) is not str and type(value) is not unicode:
value = str(value)
if type(value) is not unicode:
try:
value = value.decode('utf-8')
except UnicodeDecodeError:
value = value.decode('iso-8859-15')
valid = True
for c in value:
o = ord(c)
# Locale independent ascii alphanumeric check
if isNick and ((o >= 48 and o <= 57) or (o >= 97 and o <= 122) or (o >= 65 and o <= 90)):
output.append(c)
continue
valid = o > 31 and o != 127 and not (o >= 0xd800 and o <= 0xdfff) and o <= 0xffff
if (not isNick) and valid:
output.append(c)
continue
valid = False
if replace:
output.append(unichr(0xfffd))
return (valid, "".join(output))
# The following private API methods are fairly low level and work with
# synchtube sid's (session ids) or raw data arrays. They will usually
# Fire off a synchtube message without any validation. Higher-level
# public API methods should be built on top of them.
# Add the user described by u_arr
# u_arr should be in the following format:
# [<sid>, <nick>, <uid>, <authenticated>, <avatar-type>, <leader>, <moderator>, <karma>]
# This is the format used by user arrays from the synchtube "users" message
def _addUser(self, u_arr, isSelf=False):
userinfo = itertools.izip_longest(SynchtubeUser._fields, u_arr)
userinfo = dict(userinfo)
userinfo['nick'] = self.filterString(userinfo['nick'], True)[1]
userinfo['msgs'] = deque(maxlen=3)
userinfo['nickChanges'] = 0
user = SynchtubeUser(**userinfo)
self.userlist[user.sid] = user
if isSelf:
self.selfUser = user
# Write the current status of the hybrid mods and a short warning about editing the resulting file.
def _writePersistentSettings(self):
f = None
self.logger.debug("Writing persistent settings to file.")
try:
f = open("persistentsettings", "wb")
f.write("# This is a file generated by Naoko.\n# Do not edit it manually unless you know what you are doing.\n")
f.write("1\n")
f.write("ON\n" if self.autoLead else "OFF\n")
f.write("%s\n" % (self.autoSkip))
f.write("ON\n" if self.unregSpamBan else "OFF\n")
f.write("%s\n" % (self.commandLock))
f.write("ON\n" if self.hybridModStatus else "OFF\n")
for h, v in self.hybridModList.iteritems():
if v:
f.write("%s %d\n" % (h, v))
except Exception as e:
self.logger.debug("Failed to write hybrid mods to file.")
self.logger.debug(e)
finally:
if f:
f.close()
def _shuffle(self, data):
if self.stthread != threading.currentThread():
raise Exception("_shuffle should not be called outside the Synchtube thread")
indices = {}
for i, v in enumerate(self.vidlist):
indices[v.v_sid] = i
newlist = []
for v in data:
newlist.append(self.vidlist[indices[v]])
self.vidLock.acquire()
self.vidlist = newlist
self.vidLock.release()
# Marks a video with the specified flags.
# 1 << 0 : Invalid video, may become valid in the future. Reset upon successful manual add.
# 1 << 1 : Manually blacklisted video.
def flagVideo(self, site, vid, flags):
self.sql_queue.append(package(self.dbclient.flagVideo, site, vid, flags))
self.sqlAction.set()
# Remove flags from a video.
def unflagVideo(self, site, vid, flags):
self.sql_queue.append(package(self.dbclient.unflagVideo, site, vid, flags))
self.sqlAction.set()
# Wrapper for dbclient.insertVideo
def insertVideo(self, *args, **kwargs):
self.dbclient.insertVideo(*args, **kwargs)
# Wrapper for dbclient.insertUserCount
def insertUserCount(self, *args, **kwargs):
self.dbclient.insertUserCount(*args, **kwargs)
# Wrapper for dbclient.insertChat
def insertChat(self, *args, **kwargs):
self.dbclient.insertChat(*args, **kwargs)
# Checks to see if the current video isn't invalid, blocked, or removed.
# Also updates the duration if necessary to prevent certain types of annoying attacks on the room.
def _checkVideo(self, vi):
data = self.apiclient.getVideoInfo(vi.site, vi.vid)
if data:
if data != "Unknown":
title, dur, embed = data
if not embed:
self.logger.debug("Embedding disabled.")
self.logger.debug(data)
self.invalidVideo("Embedding disabled.")
return
# When someone has manually added a video with an incorrect duration.
elif self.state.dur != dur:
self.logger.debug("Duration mismatch: %d expected, %.3f actual." % (self.state.dur, dur))
self.state.dur = dur
self.playerAction.set()
return
self.invalidVideo("Invalid video.")
# Validates a video before inserting it into the database.
# Will correct invalid durations and titles for Youtube videos.
# This makes SQL inserts dependent on the external API.
def _validateAddVideo(self, v, sql=True, echo=True):
vi = v.vidinfo
dur = vi.dur
title = vi.title
valid = self.checkVideoId(vi)
if valid:
data = self.apiclient.getVideoInfo(vi.site, vi.vid)
if data == "Unknown":
# Do not store the video if it is invalid or from an unknown website.
# Trust that it is a video that will play.
valid = "Unknown"
elif data:
title, dur, valid = data
else:
valid = False
# -- TODO -- See if people care about videos with incorrect titles.
if not valid: #or title != vi.title:
# The video is invalid don't insert it.
self.logger.debug("Invalid video, skipping SQL insert.")
self.logger.debug(data)
# Flag the video as invalid.
self.flagVideo(vi.site, vi.vid, 1)
# Go even further and remove it from the playlist completely
if echo:
self.enqueueMsg("Invalid video removed.")
self.stExecute(package(self.asLeader, package(self.send, "rm", v.v_sid)))
return
# Curl is missing or the duration is 0, don't insert it but leave it on the playlist
if valid == "Unknown" or dur == 0: return
# Don't insert videos added by Naoko.
if str(v.uid) == self.userid: return
if sql:
# The insert the video using the retrieved title and duration.
# Trust the external APIs over the Synchtube playlist.
self.sql_queue.append(package(self.insertVideo, vi.site, vi.vid, title, dur, v.nick))
self.sqlAction.set()
def _lastBans(self, nick, num):
rows = self.dbclient.getLastBans(nick, num)
if not nick == "-all":
if not rows:
self.enqueueMsg("No recorded bans for %s" % nick)
return
if num > 1:
self.enqueueMsg("Last %d bans for user %s:" % (num, nick))
else:
self.enqueueMsg("Last ban for user %s:" % (nick))
for r in rows:
self.enqueueMsg("%s by %s - %s" % (datetime.fromtimestamp(r[0] / 1000).isoformat(' '), r[2], r[1]))
else:
if not rows:
self.enqueueMsg("No recorded bans")
return
if num > 1:
self.enqueueMsg("Last %d bans:" % (num,))
else:
self.enqueueMsg("Last ban:")
for r in rows:
self.enqueueMsg("%s - %s by %s - %s" % (r[3], datetime.fromtimestamp(r[0] / 1000).isoformat(' '), r[2], r[1]))
def _addRandom(self, num, duration, title, user):
self.logger.debug("Adding %d randomly selected videos, with title like %s, and duration no more than %s seconds, posted by user %s", num, title, duration, user)
vids = self.dbclient.getVideos(num, ['type', 'id', 'title', 'duration_ms'], ('RANDOM()',), duration, title, user)
self.logger.debug("Retrieved %s", vids)
self.stExecute(package(self.asLeader, package(self._addVideosToList, list(vids))))
def _addVideosToList(self, vids):
for v in vids:
self.send("am", [v[0], v[1], self.filterString(v[2])[1],"http://i.ytimg.com/vi/%s/default.jpg" % (v[1]), v[3]/1000.0])
# Add the video described by v
def _addVideo(self, v, sql=True, echo=True):
if self.stthread != threading.currentThread():
raise Exception("_addVideo should not be called outside the Synchtube thread")
v[0] = v[0][:len(SynchtubeVidInfo._fields)]
v[0][2] = self.filterString(v[0][2])[1]
# Synchtube will sometimes send durations as strings.
try:
v[0][4] = int(v[0][4])
if v[0][4] < 0:
v[0][4] = 60
except (ValueError, TypeError) as e:
# Something invalid, set a default duration of one minute.
v[0][4] = 60
except IndexError as e:
# Malformed vidinfo, attempt to handle anyway
v[0].extend([60] * (len(SynchtubeVidInfo._fields) - len(v[0])))
v[0] = SynchtubeVidInfo(*v[0])
if len(v) < len(SynchtubeVideo._fields):
v.extend([None] * (len(SynchtubeVideo._fields) - len(v))) # If an unregistered adds a video there is no name included
v = v[:len(SynchtubeVideo._fields)]
v[3] = self.filterString(v[3], True)[1]
vid = SynchtubeVideo(*v)
self.vidLock.acquire()
self.vidlist.append(vid)
self.vidLock.release()
self.api_queue.append(package(self._validateAddVideo, vid, sql, echo and not v[3] == self.name))
self.apiAction.set()
def _removeVideo(self, v):
if self.stthread != threading.currentThread():
raise Exception("_removeVideo should not be called outside the Synchtube thread")
idx = self.getVideoIndexById(v)
if idx >= 0:
self.vidLock.acquire()
self.vidlist.pop(idx)
self.vidLock.release()
def _moveVideo(self, v, after=None):
if self.stthread != threading.currentThread():
raise Exception("_moveVideo should not be called outside the Synchtube thread")
self.vidLock.acquire()
idx = self.getVideoIndexById(v)
if idx >= 0:
video = self.vidlist.pop(self.getVideoIndexById(v))
pos = 0
if after:
pos = self.getVideoIndexById(after) + 1
self.vidlist.insert(pos, video)
self.logger.debug("Inserted %s after %s", video, self.vidlist[pos - 1])
self.vidLock.release()
# Kick user using their sid(session id)
def _kickUser(self, sid, reason="Requested", sendMessage=True):
if not sid in self.userlist: return
if sendMessage:
self.enqueueMsg("Kicked %s: (%s)" % (self.userlist[sid].nick, reason))
self.send("kick", [sid, reason])
# By default none of the functions use this.
# Don't come crying to me if the bot bans the entire channel
def _banUser(self, sid, reason="Requested", sendMessage=True, modName=None):
if not sid in self.userlist: return
if not modName:
modName = self.name
if sendMessage:
self.enqueueMsg("Banned %s: (%s)" % (self.userlist[sid].nick, reason))
self.send("ban", sid)
self.sql_queue.append(package(self.dbclient.insertBan, self.userlist[sid], reason, time.time(), modName))
self.sqlAction.set()
# Perform pending pending leader actions.
# This should _NOT_ be called outside the main SynchtubeClient's thread
def _leaderActions(self):
if self.stthread != threading.currentThread():
raise Exception("_leaderActions should not be called outside the Synchtube thread")
while len(self.leader_queue) > 0:
self.leader_queue.popleft()()
if self.pendingToss and not self.deferredToss:
self.tossLeader()
# Give leader to another user using their sid(session id)
# This command does not ensure the client is currently leader before executing
def _tossLeader(self, sid):
# Short sleep to give Synchtube some time to react
time.sleep(0.05)
self.pendingToss = False
self.notGivingBack = False
self.tossing = True
self.unToss = package(self.send, "takeleader", self.sid)
self.send("toss", sid)
def sendHeartBeat(self):
self.send()
def _getConfig(self):
config = ConfigParser.RawConfigParser()
config.read("naoko.conf")
self.room = config.get("naoko", "room")
self.room_pw = config.get("naoko", "room_pw")
self.name = config.get("naoko", "nick")
self.pw = config.get("naoko", "pass")
self.hmod_admin = config.get("naoko", "hmod_admin").lower()
self.spam_interval = float(config.get("naoko", "spam_interval"))
self.skip_interval = float(config.get("naoko", "skip_interval"))
self.server = config.get("naoko", "irc_server")
self.channel = config.get("naoko", "irc_channel")
self.irc_nick = config.get("naoko", "irc_nick")
self.ircpw = config.get("naoko", "irc_pass")
self.dbfile = config.get("naoko", "db_file")
self.apikeys = Object()
self.apikeys.mst_id = config.get("naoko", "mst_client_id")
self.apikeys.mst_secret = config.get("naoko", "mst_client_secret")
self.apikeys.sc_id = config.get("naoko", "sc_client_id")
self.apikeys.wf_id = config.get("naoko", "wolfram_id")
self.webserver_mode = config.get("naoko", "webserver_mode")
self.webserver_host = config.get("naoko", "webserver_host")
self.webserver_port = config.get("naoko", "webserver_port")
self.webserver_protocol = config.get("naoko", "webserver_protocol")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment