Resolve merge conflict

This commit is contained in:
calzoneman 2015-07-06 11:28:18 -07:00
commit 70be8a6713
21 changed files with 752 additions and 432 deletions

View file

@ -120,10 +120,14 @@ mail:
# YouTube v3 API key
# See https://developers.google.com/youtube/registering_an_application
# Google is closing the v2 API (which allowed anonymous requests) on
# April 20, 2015 so you must register a v3 API key now.
# NOTE: You must generate a Server key under Public API access, NOT a
# browser key.
# YouTube links will not work without this!
# Instructions:
# 1. Go to https://console.developers.google.com/project
# 2. Create a new API project
# 3. On the left sidebar, click "Credentials" under "APIs & auth"
# 4. Click "Create new Key" under "Public API access"
# 5. Click "Server key"
# 6. Under "APIs & auth" click "YouTube Data API" and then click "Enable API"
youtube-v3-key: ''
# Minutes between saving channel state to disk
channel-save-interval: 5
@ -202,6 +206,9 @@ channel-blacklist: []
# * ffmpeg must be installed on the server
ffmpeg:
enabled: false
# Executable name for ffprobe if it is not "ffprobe". On Debian and Ubuntu (on which
# libav is used rather than ffmpeg proper), this is "avprobe"
ffprobe-exec: 'ffprobe'
link-domain-blacklist: []

View file

@ -22,6 +22,12 @@ const TYPE_PM = {
meta: "object,optional"
};
// Limit to 10 messages/sec
const MIN_ANTIFLOOD = {
burst: 20,
sustained: 10
};
function ChatModule(channel) {
ChannelModule.apply(this, arguments);
this.buffer = [];
@ -192,7 +198,13 @@ ChatModule.prototype.handlePm = function (user, data) {
return;
}
var msg = data.msg.substring(0, 240);
if (user.chatLimiter.throttle(MIN_ANTIFLOOD)) {
user.socket.emit("cooldown", 1000 / MIN_ANTIFLOOD.sustained);
return;
}
data.msg = data.msg.substring(0, 240);
var to = null;
for (var i = 0; i < this.channel.users.length; i++) {
if (this.channel.users[i].getLowerName() === data.to) {
@ -216,7 +228,7 @@ ChatModule.prototype.handlePm = function (user, data) {
}
}
if (msg.indexOf(">") === 0) {
if (data.msg.indexOf(">") === 0) {
meta.addClass = "greentext";
}
@ -243,13 +255,34 @@ ChatModule.prototype.processChatMsg = function (user, data) {
}
var msgobj = this.formatMessage(user.getName(), data);
var antiflood = MIN_ANTIFLOOD;
if (this.channel.modules.options &&
this.channel.modules.options.get("chat_antiflood") &&
user.account.effectiveRank < 2) {
var antiflood = this.channel.modules.options.get("chat_antiflood_params");
if (user.chatLimiter.throttle(antiflood)) {
user.socket.emit("cooldown", 1000 / antiflood.sustained);
antiflood = this.channel.modules.options.get("chat_antiflood_params");
}
if (user.chatLimiter.throttle(antiflood)) {
user.socket.emit("cooldown", 1000 / antiflood.sustained);
return;
}
if (data.msg.indexOf(">") === 0) {
msgobj.meta.addClass = "greentext";
}
if (data.msg.indexOf("/") === 0) {
var space = data.msg.indexOf(" ");
var cmd;
if (space < 0) {
cmd = data.msg.substring(1);
} else {
cmd = data.msg.substring(1, space);
}
if (cmd in this.commandHandlers) {
this.commandHandlers[cmd](user, data.msg, data.meta);
return;
}
}
@ -270,27 +303,7 @@ ChatModule.prototype.processChatMsg = function (user, data) {
});
return;
}
if (data.msg.indexOf("/") === 0) {
var space = data.msg.indexOf(" ");
var cmd;
if (space < 0) {
cmd = data.msg.substring(1);
} else {
cmd = data.msg.substring(1, space);
}
if (cmd in this.commandHandlers) {
this.commandHandlers[cmd](user, data.msg, data.meta);
} else {
this.sendMessage(msgobj);
}
} else {
if (data.msg.indexOf(">") === 0) {
msgobj.meta.addClass = "greentext";
}
this.sendMessage(msgobj);
}
this.sendMessage(msgobj);
};
ChatModule.prototype.formatMessage = function (username, data) {

View file

@ -39,6 +39,11 @@ OptionsModule.prototype.load = function (data) {
}
}
}
this.opts.chat_antiflood_params.burst = Math.min(20,
this.opts.chat_antiflood_params.burst);
this.opts.chat_antiflood_params.sustained = Math.min(10,
this.opts.chat_antiflood_params.sustained);
};
OptionsModule.prototype.save = function (data) {
@ -216,11 +221,15 @@ OptionsModule.prototype.handleSetOptions = function (user, data) {
b = 1;
}
b = Math.min(20, b);
var s = parseFloat(data.chat_antiflood_params.sustained);
if (isNaN(s) || s <= 0) {
s = 1;
}
s = Math.min(10, s);
var c = b / s;
this.opts.chat_antiflood_params = {
burst: b,

View file

@ -100,7 +100,8 @@ var defaults = {
},
"channel-blacklist": [],
ffmpeg: {
enabled: false
enabled: false,
"ffprobe-exec": "ffprobe"
},
"link-domain-blacklist": [],
setuid: {
@ -351,9 +352,8 @@ function preprocessConfig(cfg) {
require("cytube-mediaquery/lib/provider/youtube").setApiKey(
cfg["youtube-v3-key"]);
} else {
Logger.errlog.log("Warning: No YouTube v3 API key set. YouTube lookups will " +
"fall back to the v2 API, which is scheduled for closure soon after " +
"April 20, 2015. See " +
Logger.errlog.log("Warning: No YouTube v3 API key set. YouTube links will " +
"not work. See youtube-v3-key in config.template.yaml and " +
"https://developers.google.com/youtube/registering_an_application for " +
"information on registering an API key.");
}

View file

@ -7,6 +7,15 @@ var Logger = require("../logger");
var registrationLock = {};
var blackHole = function () { };
/**
* Replaces look-alike characters with "_" (single character wildcard) for
* use in LIKE queries. This prevents guests from taking names that look
* visually identical to existing names in certain fonts.
*/
function wildcardSimilarChars(name) {
return name.replace(/[Il1oO0]/g, "_");
}
module.exports = {
init: function () {
},
@ -15,7 +24,7 @@ module.exports = {
* Check if a username is taken
*/
isUsernameTaken: function (name, callback) {
db.query("SELECT name FROM `users` WHERE name=?", [name],
db.query("SELECT name FROM `users` WHERE name LIKE ?", [wildcardSimilarChars(name)],
function (err, rows) {
if (err) {
callback(err, true);

View file

@ -1,6 +1,12 @@
var Logger = require("./logger");
var Config = require("./config");
var spawn = require("child_process").spawn;
var https = require("https");
var http = require("http");
var urlparse = require("url");
var statusMessages = require("./status-messages");
var USE_JSON = true;
var acceptedCodecs = {
"mov/h264": true,
@ -19,6 +25,169 @@ var audioOnlyContainers = {
"mp3": true
};
function testUrl(url, cb, redirCount) {
if (!redirCount) redirCount = 0;
var data = urlparse.parse(url);
if (!/https?:/.test(data.protocol)) {
return cb("Video links must start with http:// or https://");
}
if (!data.hostname) {
return cb("Invalid link");
}
var transport = (data.protocol === "https:") ? https : http;
data.method = "HEAD";
var req = transport.request(data, function (res) {
req.abort();
if (res.statusCode === 301 || res.statusCode === 302) {
if (redirCount > 2) {
return cb("Too many redirects. Please provide a direct link to the " +
"file");
}
return testUrl(res.headers["location"], cb, redirCount + 1);
}
if (res.statusCode !== 200) {
var message = statusMessages[res.statusCode];
if (!message) message = "";
return cb("HTTP " + res.statusCode + " " + message);
}
if (!/^audio|^video/.test(res.headers["content-type"])) {
return cb("Server did not return an audio or video file, or sent the " +
"wrong Content-Type");
}
cb();
});
req.on("error", function (err) {
cb(err);
});
req.end();
}
function readOldFormat(buf) {
var lines = buf.split("\n");
var tmp = { tags: {} };
var data = {
streams: []
};
lines.forEach(function (line) {
if (line.match(/\[stream\]|\[format\]/i)) {
return;
} else if (line.match(/\[\/stream\]/i)) {
data.streams.push(tmp);
tmp = { tags: {} };
} else if (line.match(/\[\/format\]/i)) {
data.format = tmp;
tmp = { tags: {} };
} else {
var kv = line.split("=");
var key = kv[0].toLowerCase();
if (key.indexOf("tag:") === 0) {
tmp.tags[key.split(":")[1]] = kv[1];
} else {
tmp[key] = kv[1];
}
}
});
return data;
}
function reformatData(data) {
var reformatted = {};
var duration = parseInt(data.format.duration, 10);
if (isNaN(duration)) duration = "--:--";
reformatted.duration = Math.ceil(duration);
var bitrate = parseInt(data.format.bit_rate, 10) / 1000;
if (isNaN(bitrate)) bitrate = 0;
reformatted.bitrate = bitrate;
reformatted.title = data.format.tags ? data.format.tags.title : null;
var container = data.format.format_name.split(",")[0];
data.streams.forEach(function (stream) {
if (stream.codec_type === "video") {
reformatted.vcodec = stream.codec_name;
if (!reformatted.title && stream.tags) {
reformatted.title = stream.tags.title;
}
} else if (stream.codec_type === "audio") {
reformatted.acodec = stream.codec_name;
}
});
if (reformatted.vcodec && !(audioOnlyContainers.hasOwnProperty(container))) {
reformatted.type = [container, reformatted.vcodec].join("/");
reformatted.medium = "video";
} else if (reformatted.acodec) {
reformatted.type = [container, reformatted.acodec].join("/");
reformatted.medium = "audio";
}
return reformatted;
}
exports.ffprobe = function ffprobe(filename, cb) {
var childErr;
var args = ["-show_streams", "-show_format", filename];
if (USE_JSON) args = ["-of", "json"].concat(args);
var child = spawn(Config.get("ffmpeg.ffprobe-exec"), args);
var stdout = "";
var stderr = "";
child.on("error", function (err) {
childErr = err;
});
child.stdout.on("data", function (data) {
stdout += data;
});
child.stderr.on("data", function (data) {
stderr += data;
});
child.on("close", function (code) {
if (code !== 0) {
if (stderr.match(/unrecognized option|json/i) && USE_JSON) {
Logger.errlog.log("Warning: ffprobe does not support -of json. " +
"Assuming it will have old output format.");
USE_JSON = false;
return ffprobe(filename, cb);
}
if (!childErr) childErr = new Error(stderr);
return cb(childErr);
}
var result;
if (USE_JSON) {
try {
result = JSON.parse(stdout);
} catch (e) {
return cb(new Error("Unable to parse ffprobe output: " + e.message));
}
} else {
try {
result = readOldFormat(stdout);
} catch (e) {
return cb(new Error("Unable to parse ffprobe output: " + e.message));
}
}
return cb(null, result);
});
}
exports.query = function (filename, cb) {
if (!Config.get("ffmpeg.enabled")) {
return cb("Raw file playback is not enabled on this server");
@ -29,135 +198,73 @@ exports.query = function (filename, cb) {
"or HTTPS");
}
ffprobe(filename, function (err, meta) {
testUrl(filename, function (err) {
if (err) {
if (meta.stderr && meta.stderr.match(/Protocol not found/)) {
return cb("Link uses a protocol unsupported by this server's ffmpeg");
} else {
return cb(err);
}
exports.ffprobe(filename, function (err, data) {
if (err) {
if (err.code && err.code === "ENOENT") {
return cb("Failed to execute `ffprobe`. Set ffmpeg.ffprobe-exec " +
"to the correct name of the executable in config.yaml. " +
"If you are using Debian or Ubuntu, it is probably " +
"avprobe.");
} else if (err.message) {
if (err.message.match(/protocol not found/i))
return cb("Link uses a protocol unsupported by this server's " +
"version of ffmpeg");
// Ignore ffprobe error messages, they are common and most often
// indicate a problem with the remote file, not with this code.
if (!/(av|ff)probe/.test(String(err)))
Logger.errlog.log(err.stack || err);
return cb("Unable to query file data with ffmpeg");
} else {
if (!/(av|ff)probe/.test(String(err)))
Logger.errlog.log(err.stack || err);
return cb("Unable to query file data with ffmpeg");
}
}
try {
data = reformatData(data);
} catch (e) {
Logger.errlog.log(e.stack || e);
return cb("Unable to query file data with ffmpeg");
}
}
meta = parse(meta);
if (meta == null) {
return cb("Unknown error");
}
if (data.medium === "video") {
if (!acceptedCodecs.hasOwnProperty(data.type)) {
return cb("Unsupported video codec " + data.type);
}
if (isVideo(meta)) {
var codec = meta.container + "/" + meta.vcodec;
data = {
title: data.title || "Raw Video",
duration: data.duration,
bitrate: data.bitrate,
codec: data.type
};
if (!(codec in acceptedCodecs)) {
return cb("Unsupported video codec " + codec);
cb(null, data);
} else if (data.medium === "audio") {
if (!acceptedAudioCodecs.hasOwnProperty(data.acodec)) {
return cb("Unsupported audio codec " + data.acodec);
}
data = {
title: data.title || "Raw Audio",
duration: data.duration,
bitrate: data.bitrate,
codec: data.acodec
};
cb(null, data);
} else {
return cb("Parsed metadata did not contain a valid video or audio " +
"stream. Either the file is invalid or it has a format " +
"unsupported by this server's version of ffmpeg.");
}
var data = {
title: meta.title || "Raw Video",
duration: Math.ceil(meta.seconds) || "--:--",
bitrate: meta.bitrate,
codec: codec
};
cb(null, data);
} else if (isAudio(meta)) {
var codec = meta.acodec;
if (!(codec in acceptedAudioCodecs)) {
return cb("Unsupported audio codec " + codec);
}
var data = {
title: meta.title || "Raw Audio",
duration: Math.ceil(meta.seconds) || "--:--",
bitrate: meta.bitrate,
codec: codec
};
cb(null, data);
} else if (data.ffmpegErr.match(/Protocol not found/)) {
return cb("This server is unable to load videos over the " +
filename.split(":")[0] + " protocol.");
} else {
return cb("Parsed metadata did not contain a valid video or audio stream. " +
"Either the file is invalid or it has a format unsupported by " +
"this server's version of ffmpeg.");
}
});
});
};
function isVideo(meta) {
return meta.vcodec && !(meta.container in audioOnlyContainers);
}
function isAudio(meta) {
return meta.acodec;
}
function parse(meta) {
if (meta == null) {
return null;
}
if (!meta.format) {
return null;
}
var data = {};
meta.streams.forEach(function (s) {
if (s.codec_type === "video") {
data.vcodec = s.codec_name;
} else if (s.codec_type === "audio") {
data.acodec = s.codec_name;
}
});
data.container = meta.format.format_name.split(",")[0];
data.bitrate = parseInt(meta.format.bit_rate) / 1000;
if (meta.format.tags) {
data.title = meta.format.tags.title;
}
data.seconds = Math.ceil(parseFloat(meta.format.duration));
return data;
}
function ffprobe(filename, cb) {
var ff = spawn("ffprobe", ["-show_streams", "-show_format", filename]);
var outbuf = "";
var errbuf = "";
ff.stdout.on("data", function (data) {
outbuf += data;
});
ff.stderr.on("data", function (data) {
errbuf += data;
});
ff.on("close", function (code) {
if (code !== 0) {
return cb("ffprobe exited with nonzero exit code", { stderr: errbuf });
}
var lines = outbuf.split("\n");
var streams = [];
var format = {};
var data = {};
lines.forEach(function (line) {
if (line.match(/\[stream\]|\[format\]/i)) {
return;
} else if (line.match(/\[\/stream\]/i)) {
streams.push(data);
data = {};
} else if (line.match(/\[\/format\]/i)) {
format = data;
data = {};
} else {
var kv = line.split("=");
data[kv[0]] = kv[1];
}
});
cb(null, {
streams: streams,
format: format
});
});
}

View file

@ -77,7 +77,8 @@ var Getters = {
/* youtube.com */
yt: function (id, callback) {
if (!Config.get("youtube-v3-key")) {
return Getters.yt2(id, callback);
return callback("The YouTube API now requires an API key. Please see the " +
"documentation for youtube-v3-key in config.template.yaml");
}
@ -97,7 +98,8 @@ var Getters = {
/* youtube.com playlists */
yp: function (id, callback) {
if (!Config.get("youtube-v3-key")) {
return Getters.yp2(id, callback);
return callback("The YouTube API now requires an API key. Please see the " +
"documentation for youtube-v3-key in config.template.yaml");
}
YouTube.lookupPlaylist(id).then(function (videos) {
@ -119,7 +121,8 @@ var Getters = {
/* youtube.com search */
ytSearch: function (query, callback) {
if (!Config.get("youtube-v3-key")) {
return Getters.ytSearch2(query.split(" "), callback);
return callback("The YouTube API now requires an API key. Please see the " +
"documentation for youtube-v3-key in config.template.yaml");
}
YouTube.search(query).then(function (res) {
@ -582,254 +585,6 @@ var Getters = {
var media = new Media(id, title, "--:--", "hb");
callback(false, media);
},
/* youtube.com - old v2 API */
yt2: function (id, callback) {
var sv = Server.getServer();
var m = id.match(/([\w-]{11})/);
if (m) {
id = m[1];
} else {
callback("Invalid ID", null);
return;
}
var options = {
host: "gdata.youtube.com",
port: 443,
path: "/feeds/api/videos/" + id + "?v=2&alt=json",
method: "GET",
dataType: "jsonp",
timeout: 1000
};
if (Config.get("youtube-v2-key")) {
options.headers = {
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
};
}
urlRetrieve(https, options, function (status, data) {
switch (status) {
case 200:
break; /* Request is OK, skip to handling data */
case 400:
return callback("Invalid request", null);
case 403:
return callback("Private video", null);
case 404:
return callback("Video not found", null);
case 500:
case 503:
return callback("Service unavailable", null);
default:
return callback("HTTP " + status, null);
}
var buffer = data;
try {
data = JSON.parse(data);
/* Check for embedding restrictions */
if (data.entry.yt$accessControl) {
var ac = data.entry.yt$accessControl;
for (var i = 0; i < ac.length; i++) {
if (ac[i].action === "embed") {
if (ac[i].permission === "denied") {
callback("Embedding disabled", null);
return;
}
break;
}
}
}
var seconds = data.entry.media$group.yt$duration.seconds;
var title = data.entry.title.$t;
var meta = {};
/* Check for country restrictions */
if (data.entry.media$group.media$restriction) {
var rest = data.entry.media$group.media$restriction;
if (rest.length > 0) {
if (rest[0].relationship === "deny") {
meta.restricted = rest[0].$t;
}
}
}
var media = new Media(id, title, seconds, "yt", meta);
callback(false, media);
} catch (e) {
// Gdata version 2 has the rather silly habit of
// returning error codes in XML when I explicitly asked
// for JSON
var m = buffer.match(/<internalReason>([^<]+)<\/internalReason>/);
if (m === null)
m = buffer.match(/<code>([^<]+)<\/code>/);
var err = e;
if (m) {
if(m[1] === "too_many_recent_calls") {
err = "YouTube is throttling the server right "+
"now for making too many requests. "+
"Please try again in a moment.";
} else {
err = m[1];
}
}
callback(err, null);
}
});
},
/* youtube.com playlists - old v2 api */
yp2: function (id, callback, url) {
/**
* NOTE: callback may be called multiple times, once for each <= 25 video
* batch of videos in the list. It will be called in order.
*/
var m = id.match(/([\w-]+)/);
if (m) {
id = m[1];
} else {
callback("Invalid ID", null);
return;
}
var path = "/feeds/api/playlists/" + id + "?v=2&alt=json";
/**
* NOTE: the third parameter, url, is used to chain this retriever
* multiple times to get all the videos from a playlist, as each
* request only returns 25 videos.
*/
if (url !== undefined) {
path = "/" + url.split("gdata.youtube.com")[1];
}
var options = {
host: "gdata.youtube.com",
port: 443,
path: path,
method: "GET",
dataType: "jsonp",
timeout: 1000
};
if (Config.get("youtube-v2-key")) {
options.headers = {
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
};
}
urlRetrieve(https, options, function (status, data) {
switch (status) {
case 200:
break; /* Request is OK, skip to handling data */
case 400:
return callback("Invalid request", null);
case 403:
return callback("Private playlist", null);
case 404:
return callback("Playlist not found", null);
case 500:
case 503:
return callback("Service unavailable", null);
default:
return callback("HTTP " + status, null);
}
try {
data = JSON.parse(data);
var vids = [];
for(var i in data.feed.entry) {
try {
/**
* FIXME: This should probably check for embed restrictions
* and country restrictions on each video in the list
*/
var item = data.feed.entry[i];
var id = item.media$group.yt$videoid.$t;
var title = item.title.$t;
var seconds = item.media$group.yt$duration.seconds;
var media = new Media(id, title, seconds, "yt");
vids.push(media);
} catch(e) {
}
}
callback(false, vids);
var links = data.feed.link;
for (var i in links) {
if (links[i].rel === "next") {
/* Look up the next batch of videos from the list */
Getters["yp2"](id, callback, links[i].href);
}
}
} catch (e) {
callback(e, null);
}
});
},
/* youtube.com search - old v2 api */
ytSearch2: function (terms, callback) {
/**
* terms is a list of words from the search query. Each word must be
* encoded properly for use in the request URI
*/
for (var i in terms) {
terms[i] = encodeURIComponent(terms[i]);
}
var query = terms.join("+");
var options = {
host: "gdata.youtube.com",
port: 443,
path: "/feeds/api/videos/?q=" + query + "&v=2&alt=json",
method: "GET",
dataType: "jsonp",
timeout: 1000
};
if (Config.get("youtube-v2-key")) {
options.headers = {
"X-Gdata-Key": "key=" + Config.get("youtube-v2-key")
};
}
urlRetrieve(https, options, function (status, data) {
if (status !== 200) {
callback("YouTube search: HTTP " + status, null);
return;
}
try {
data = JSON.parse(data);
var vids = [];
for(var i in data.feed.entry) {
try {
/**
* FIXME: This should probably check for embed restrictions
* and country restrictions on each video in the list
*/
var item = data.feed.entry[i];
var id = item.media$group.yt$videoid.$t;
var title = item.title.$t;
var seconds = item.media$group.yt$duration.seconds;
var media = new Media(id, title, seconds, "yt");
media.thumb = item.media$group.media$thumbnail[0];
vids.push(media);
} catch(e) {
}
}
callback(false, vids);
} catch(e) {
callback(e, null);
}
});
},
};
module.exports = {

83
lib/status-messages.js Normal file
View file

@ -0,0 +1,83 @@
// This status message map is taken from the node.js source code. The original
// copyright notice for lib/_http_server.js is reproduced below.
//
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
module.exports = {
100: "Continue",
101: "Switching Protocols",
102: "Processing",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
207: "Multi-Status",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
308: "Permanent Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Time-out",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Large",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
418: "I'm a teapot",
422: "Unprocessable Entity",
423: "Locked",
424: "Failed Dependency",
425: "Unordered Collection",
426: "Upgrade Required",
428: "Precondition Required",
429: "Too Many Requests",
431: "Request Header Fields Too Large",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "HTTP Version Not Supported",
506: "Variant Also Negotiates",
507: "Insufficient Storage",
509: "Bandwidth Limit Exceeded",
510: "Not Extended",
511: "Network Authentication Required"
};

View file

@ -14,7 +14,7 @@
"cookie-parser": "^1.3.3",
"csrf": "^2.0.6",
"cytube-mediaquery": "git://github.com/CyTube/mediaquery",
"cytubefilters": "git://github.com/calzoneman/cytubefilters#7663b3a9",
"cytubefilters": "git://github.com/calzoneman/cytubefilters#65e685ad",
"express": "^4.11.1",
"express-minify": "^0.1.3",
"graceful-fs": "^3.0.5",

View file

@ -21,7 +21,7 @@ html(lang="en")
b.caret
ul.dropdown-menu
li: a(href="#" onclick="javascript:chatOnly()") Chat Only
li: a(href="#" onclick="javascript:removeVideo()") Remove Video
li: a(href="#" onclick="javascript:removeVideo(event)") Remove Video
mixin navloginlogout(cname)
section#mainpage
.container
@ -56,6 +56,7 @@ html(lang="en")
#controlsrow.row
#leftcontrols.col-lg-5.col-md-5
button#newpollbtn.btn.btn-sm.btn-default New Poll
button#emotelistbtn.btn.btn-sm.btn-default Emote List
#rightcontrols.col-lg-7.col-md-7
#plcontrol.btn-group.pull-left
button#showsearch.btn.btn-sm.btn-default(title="Search for a video", data-toggle="collapse", data-target="#searchcontrol")
@ -125,7 +126,7 @@ html(lang="en")
input.add-temp(type="checkbox")
| Add as temporary
| Paste the embed code below and click Next or At End.
| Acceptable embed codes are <code>&lt;iframe&gt;</code> and <code>&lt;object&gt;</code> tags.
| Acceptable embed codes are <code>&lt;iframe&gt;</code> and <code>&lt;object&gt;</code> tags. <strong>CUSTOM EMBEDS CANNOT BE SYNCHRONIZED.</strong>
textarea#customembed-content.input-block-level.form-control(rows="3")
#playlistmanager.collapse.plcontrol-collapse.col-lg-12.col-md-12
.vertical-spacer
@ -173,6 +174,24 @@ html(lang="en")
.modal-footer
button.btn.btn-primary(type="button", data-dismiss="modal", onclick="javascript:saveUserOptions()") Save
button.btn.btn-default(type="button", data-dismiss="modal") Close
#emotelist.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
.modal-dialog.modal-dialog-nonfluid
.modal-content
.modal-header
button.close(data-dismiss="modal", aria-hidden="true") &times;
h4 Emote List
.modal-body
.pull-left
input#emotelist-search.form-control(type="text", placeholder="Search")
.pull-right
.checkbox
label
input#emotelist-alphabetical(type="checkbox")
| Sort alphabetically
#emotelist-paginator-container
table
tbody
.modal-footer
#channeloptions.modal.fade(tabindex="-1", role="dialog", aria-hidden="true")
.modal-dialog
.modal-content

View file

@ -557,15 +557,15 @@ body.chatOnly .pm-panel, body.chatOnly .pm-panel-placeholder {
}
@media screen and (min-width: 768px) {
.modal {
padding: 30px;
}
.modal-dialog {
min-width: 600px!important;
max-width: 1200px!important;
width: auto!important;
}
.modal-dialog-nonfluid.modal-dialog {
max-width: 600px!important;
}
}
table td {
@ -592,3 +592,36 @@ table td {
border: 1px solid;
border-top-width: 0;
}
#emotelist table {
margin: auto;
}
.emote-preview-container {
width: 100px;
height: 100px;
float: left;
text-align: center;
white-space: nowrap;
margin: 5px;
}
.emote-preview-hax {
display: inline-block;
vertical-align: middle;
height: 100%;
}
.emote-preview {
max-width: 100px;
max-height: 100px;
cursor: pointer;
}
#emotelist-paginator-container {
text-align: center;
}
#leftcontrols .btn {
margin-right: 5px;
}

View file

@ -32,6 +32,10 @@ footer {
background-color: #ffffff;
}
#emotelist td {
background-color: #f0f0f0;
}
.chat-shadow {
color: #aaaaaa;
}

View file

@ -39,7 +39,7 @@ input.form-control[type="email"], textarea.form-control {
color: #c8c8c8;
}
.profile-box, .user-dropdown {
.profile-box, .user-dropdown, #emotelist td {
color: #c8c8c8;
background-color: #2d2d2d;
}

View file

@ -26,6 +26,10 @@ footer {
background-color: #ffffff;
}
#emotelist td {
background-color: #f0f0f0;
}
.chat-shadow {
color: #aaaaaa;
}

View file

@ -46,6 +46,15 @@ input.form-control[type="email"], textarea.form-control {
border-radius: 0px !important;
}
#emotelist table {
background-color: #2a2d30;
}
#emotelist td {
background-color: rgba(28, 30, 34, 0.95);
border: none;
}
.profile-image {
border-radius: 0px;
border: solid 1px #000000 !important;
@ -176,4 +185,4 @@ input.form-control[type="email"], textarea.form-control {
margin-bottom: 9px;
min-height: 20px;
padding: 10px 19px !important;
}
}

View file

@ -51,7 +51,7 @@ input.form-control[type="email"], textarea.form-control {
background-image: linear-gradient(#484e55, #3a3f44 60%, #313539) !important;
}
.profile-box, .user-dropdown {
.profile-box, .user-dropdown, #emotelist td {
color: #c8c8c8;
background-color: #161a20;
}

View file

@ -124,6 +124,7 @@ Callbacks = {
cooldown: function (time) {
time = time + 200;
$("#chatline").css("color", "#ff0000");
$(".pm-input").css("color", "#ff0000");
if (CHATTHROTTLE && $("#chatline").data("throttle_timer")) {
clearTimeout($("#chatline").data("throttle_timer"));
}
@ -131,6 +132,7 @@ Callbacks = {
$("#chatline").data("throttle_timer", setTimeout(function () {
CHATTHROTTLE = false;
$("#chatline").css("color", "");
$(".pm-input").css("color", "");
}, time));
},
@ -279,7 +281,7 @@ Callbacks = {
channelCSSJS: function(data) {
$("#chancss").remove();
CHANNEL.css = data.css;
$("#csstext").val(data.css);
$("#cs-csstext").val(data.css);
if(data.css && !USEROPTS.ignore_channelcss) {
$("<style/>").attr("type", "text/css")
.attr("id", "chancss")
@ -289,7 +291,7 @@ Callbacks = {
$("#chanjs").remove();
CHANNEL.js = data.js;
$("#jstext").val(data.js);
$("#cs-jstext").val(data.js);
if(data.js && !USEROPTS.ignore_channeljs) {
var src = data.js
@ -1015,6 +1017,7 @@ Callbacks = {
var tbl = $("#cs-emotes table");
tbl.data("entries", data);
formatCSEmoteList();
EMOTELIST.emoteListChanged = true;
},
updateEmote: function (data) {

View file

@ -44,6 +44,7 @@ var IGNORED = [];
var CHATHIST = [];
var CHATHISTIDX = 0;
var CHATTHROTTLE = false;
var CHATMAXSIZE = 100;
var SCROLLCHAT = true;
var LASTCHAT = {
name: ""
@ -115,7 +116,9 @@ var USEROPTS = {
default_quality : getOrDefault("default_quality", "auto"),
boop : getOrDefault("boop", "never"),
secure_connection : getOrDefault("secure_connection", false),
show_shadowchat : getOrDefault("show_shadowchat", false)
show_shadowchat : getOrDefault("show_shadowchat", false),
emotelist_sort : getOrDefault("emotelist_sort", true),
no_emotes : getOrDefault("no_emotes", false)
};
/* Backwards compatibility check */

View file

@ -103,3 +103,108 @@
return p;
};
})();
function NewPaginator(numItems, itemsPerPage, pageLoader) {
this.numItems = numItems;
this.itemsPerPage = itemsPerPage;
this.elem = document.createElement("ul");
this.elem.className = "pagination";
this.btnBefore = 3;
this.btnAfter = 3;
this.pageLoader = pageLoader;
}
NewPaginator.prototype.makeButton = function (target, text) {
var li = document.createElement("li");
var btn = document.createElement("a");
btn.href = "javascript:void(0)";
btn.innerHTML = text;
var _this = this;
if (target !== null) {
btn.onclick = function (event) {
if (this.parentNode.className === "disabled") {
event.preventDefault();
return false;
}
_this.loadPage(target);
};
}
li.appendChild(btn);
return li;
};
NewPaginator.prototype.makeBreak = function () {
var btn = this.makeButton(null, "&hellip;");
btn.className = "disabled";
return btn;
};
NewPaginator.prototype.loadButtons = function (page) {
this.elem.innerHTML = "";
var first = this.makeButton(0, "First");
this.elem.appendChild(first);
if (page === 0) {
first.className = "disabled";
}
var prev = this.makeButton(page - 1, "&laquo;");
this.elem.appendChild(prev);
if (page === 0) {
prev.className = "disabled";
}
if (page > this.btnBefore) {
var sep = this.makeBreak();
this.elem.appendChild(sep);
}
var numPages = Math.ceil(this.numItems / this.itemsPerPage);
numPages = Math.max(numPages, 1);
var numBtns = Math.min(this.btnBefore + this.btnAfter + 1, numPages);
var start;
if (page < this.btnBefore) {
start = 0;
} else if (page > numPages - this.btnAfter - 1) {
start = numPages - numBtns;
} else {
start = page - this.btnBefore;
}
var end = start + numBtns;
var _this = this;
for (var i = start; i < end; i++) {
(function (i) {
var btn = _this.makeButton(i, String(i + 1));
_this.elem.appendChild(btn);
if (i === page) {
btn.className = "disabled";
}
})(i);
}
if (page < numPages - this.btnAfter - 1) {
var sep = this.makeBreak();
this.elem.appendChild(sep);
}
var next = this.makeButton(page + 1, "&raquo;");
this.elem.appendChild(next);
if (page === numPages - 1) {
next.className = "disabled";
}
var last = this.makeButton(numPages - 1, "Last");
this.elem.appendChild(last);
if (page === numPages - 1) {
last.className = "disabled";
}
};
NewPaginator.prototype.loadPage = function (page) {
this.loadButtons(page);
if (this.pageLoader) {
this.pageLoader(page);
}
};

View file

@ -740,6 +740,11 @@ $("#channeloptions li > a[data-toggle='tab']").on("shown.bs.tab", function () {
applyOpts();
(function () {
var embed = document.querySelector("#videowrap .embed-responsive");
if (!embed) {
return;
}
if (typeof window.MutationObserver === "function") {
var mr = new MutationObserver(function (records) {
records.forEach(function (record) {
@ -751,14 +756,39 @@ applyOpts();
});
});
mr.observe($("#videowrap").find(".embed-responsive")[0], { childList: true });
mr.observe(embed, { childList: true });
} else {
/*
* DOMNodeInserted is deprecated. This code is here only as a fallback
* for browsers that do not support MutationObserver
*/
$("#videowrap").find(".embed-responsive")[0].addEventListener("DOMNodeInserted", function (ev) {
embed.addEventListener("DOMNodeInserted", function (ev) {
if (ev.target.id === "ytapiplayer") handleVideoResize();
});
}
})();
$("#emotelistbtn").click(function () {
EMOTELIST.show();
});
$("#emotelist-search").keyup(function () {
var value = this.value.toLowerCase();
if (value) {
EMOTELIST.filter = function (emote) {
return emote.name.toLowerCase().indexOf(value) >= 0;
};
} else {
EMOTELIST.filter = null;
}
EMOTELIST.handleChange();
EMOTELIST.loadPage(0);
});
$("#emotelist-alphabetical").prop("checked", USEROPTS.emotelist_sort);
$("#emotelist-alphabetical").change(function () {
USEROPTS.emotelist_sort = this.checked;
setOpt("emotelist_sort", USEROPTS.emotelist_sort);
EMOTELIST.handleChange();
EMOTELIST.loadPage(0);
});

View file

@ -733,9 +733,7 @@ function applyOpts() {
}
if(USEROPTS.hidevid) {
$("#qualitywrap").html("");
removeVideo();
$("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-lg-12 col-md-12");
}
$("#chatbtn").remove();
@ -1492,10 +1490,7 @@ function addChatMessage(data) {
div.mouseleave(function() {
$(".nick-hover").removeClass("nick-hover");
});
// Cap chatbox at most recent 100 messages
if($("#messagebuffer").children().length > 100) {
$($("#messagebuffer").children()[0]).remove();
}
trimChatBuffer();
if(SCROLLCHAT)
scrollChat();
@ -1511,6 +1506,18 @@ function addChatMessage(data) {
}
function trimChatBuffer() {
var maxSize = window.CHATMAXSIZE;
if (!maxSize || typeof maxSize !== "number")
maxSize = parseInt(maxSize || 100, 10) || 100;
var buffer = document.getElementById("messagebuffer");
var count = buffer.childNodes.length - maxSize;
for (var i = 0; i < count; i++) {
buffer.firstChild.remove();
}
}
function pingMessage(isHighlight) {
if (!FOCUSED) {
if (!TITLE_BLINK && (USEROPTS.blink_title === "always" ||
@ -1688,7 +1695,14 @@ function chatOnly() {
.click(function () {
$("#channeloptions").modal();
});
$("<span/>").addClass("label label-default pull-right pointer")
.text("Emote List")
.appendTo($("#chatheader"))
.click(function () {
EMOTELIST.show();
});
setVisible("#showchansettings", CLIENT.rank >= 2);
$("body").addClass("chatOnly");
handleWindowResize();
}
@ -1711,7 +1725,7 @@ function handleVideoResize() {
var intv, ticks = 0;
var resize = function () {
if (++ticks > 10) clearInterval(intv);
if ($("#ytapiplayer").parent().height() === 0) return;
if ($("#ytapiplayer").parent().outerHeight() <= 0) return;
clearInterval(intv);
var responsiveFrame = $("#ytapiplayer").parent();
@ -1730,7 +1744,7 @@ function handleVideoResize() {
$(window).resize(handleWindowResize);
handleWindowResize();
function removeVideo() {
function removeVideo(event) {
try {
PLAYER.setVolume(0);
if (PLAYER.type === "rv") {
@ -1741,6 +1755,7 @@ function removeVideo() {
$("#videowrap").remove();
$("#chatwrap").removeClass("col-lg-5 col-md-5").addClass("col-md-12");
if (event) event.preventDefault();
}
/* channel administration stuff */
@ -2514,6 +2529,11 @@ function formatUserPlaylistList() {
.attr("title", "Delete playlist")
.appendTo(btns)
.click(function () {
var really = confirm("Are you sure you want to delete" +
" this playlist? This cannot be undone.");
if (!really) {
return;
}
socket.emit("deletePlaylist", {
name: pl.name
});
@ -2581,10 +2601,14 @@ function initPm(user) {
var buffer = $("<div/>").addClass("pm-buffer linewrap").appendTo(body);
$("<hr/>").appendTo(body);
var input = $("<input/>").addClass("form-control pm-input").attr("type", "text")
.attr("maxlength", 240)
.appendTo(body);
input.keydown(function (ev) {
if (ev.keyCode === 13) {
if (CHATTHROTTLE) {
return;
}
var meta = {};
var msg = input.val();
if (msg.trim() === "") {
@ -2836,3 +2860,106 @@ function googlePlusSimulator2014(data) {
data.contentType = data.meta.gpdirect[q].contentType;
return data;
}
function EmoteList() {
this.modal = $("#emotelist");
this.modal.on("hidden.bs.modal", unhidePlayer);
this.table = document.querySelector("#emotelist table");
this.cols = 5;
this.itemsPerPage = 25;
this.emotes = [];
this.emoteListChanged = true;
this.page = 0;
}
EmoteList.prototype.handleChange = function () {
this.emotes = CHANNEL.emotes.slice();
if (USEROPTS.emotelist_sort) {
this.emotes.sort(function (a, b) {
var x = a.name.toLowerCase();
var y = b.name.toLowerCase();
if (x < y) {
return -1;
} else if (x > y) {
return 1;
} else {
return 0;
}
});
}
if (this.filter) {
this.emotes = this.emotes.filter(this.filter);
}
this.paginator = new NewPaginator(this.emotes.length, this.itemsPerPage,
this.loadPage.bind(this));
var container = document.getElementById("emotelist-paginator-container");
container.innerHTML = "";
container.appendChild(this.paginator.elem);
this.paginator.loadPage(this.page);
this.emoteListChanged = false;
};
EmoteList.prototype.show = function () {
if (this.emoteListChanged) {
this.handleChange();
}
this.modal.modal();
};
EmoteList.prototype.loadPage = function (page) {
var tbody = this.table.children[0];
tbody.innerHTML = "";
var row;
var start = page * this.itemsPerPage;
if (start >= this.emotes.length) return;
var end = Math.min(start + this.itemsPerPage, this.emotes.length);
var _this = this;
for (var i = start; i < end; i++) {
if ((i - start) % this.cols === 0) {
row = document.createElement("tr");
tbody.appendChild(row);
}
(function (emote) {
var td = document.createElement("td");
td.className = "emote-preview-container";
// Trick element to vertically align the emote within the container
var hax = document.createElement("span");
hax.className = "emote-preview-hax";
td.appendChild(hax);
var img = document.createElement("img");
img.src = emote.image;
img.className = "emote-preview";
img.title = emote.name;
img.onclick = function () {
var val = chatline.value;
if (!val) {
chatline.value = emote.name;
} else {
if (!val.charAt(val.length - 1).match(/\s/)) {
chatline.value += " ";
}
chatline.value += emote.name;
}
_this.modal.modal("hide");
chatline.focus();
};
td.appendChild(img);
row.appendChild(td);
})(this.emotes[i]);
}
this.page = page;
};
window.EMOTELIST = new EmoteList();