1 var ChannelModule
= require("./module");
2 var Config
= require("../config");
3 var Utilities
= require("../utilities");
4 var url
= require("url");
6 function realTypeOf(thing
) {
7 return thing
=== null ? 'null' : typeof thing
;
10 function OptionsModule(_channel
) {
11 ChannelModule
.apply(this, arguments
);
13 allow_voteskip
: true, // Allow users to voteskip
14 voteskip_ratio
: 0.5, // Ratio of skip votes:non-afk users needed to skip the video
15 afk_timeout
: 600, // Number of seconds before a user is automatically marked afk
16 pagetitle
: this.channel
.name
, // Title of the browser tab
17 maxlength
: 0, // Maximum length (in seconds) of a video queued
18 externalcss
: "", // Link to external stylesheet
19 externaljs
: "", // Link to external script
20 chat_antiflood
: false, // Throttle chat messages
21 chat_antiflood_params
: {
22 burst
: 4, // Number of messages to allow with no throttling
23 sustained
: 1, // Throttle rate (messages/second)
24 cooldown
: 4 // Number of seconds with no messages before burst is reset
26 show_public
: false, // List the channel on the index page
27 enable_link_regex
: true, // Use the built-in link filter
28 password
: false, // Channel password (false -> no password required for entry)
29 allow_dupes
: false, // Allow duplicate videos on the playlist
30 torbanned
: false, // Block connections from Tor exit nodes
31 allow_anon_chat
: true, // Disables guest loggin for anonymous posting
32 block_anonymous_users
: false, //Only allow connections from registered users.
33 allow_ascii_control
: false, // Allow ASCII control characters (\x00-\x1f)
34 playlist_max_per_user
: 0, // Maximum number of playlist items per user
35 new_user_chat_delay
: 0, // Minimum account/IP age to chat
36 new_user_chat_link_delay
: 0, // Minimum account/IP age to post links
37 playlist_max_duration_per_user
: 0 // Maximum total playlist time per user
40 this.supportsDirtyCheck
= true;
43 OptionsModule
.prototype = Object
.create(ChannelModule
.prototype);
45 OptionsModule
.prototype.load = function (data
) {
47 for (var key
in this.opts
) {
48 if (key
in data
.opts
) {
49 this.opts
[key
] = data
.opts
[key
];
54 this.opts
.pagetitle
= unzalgo(this.opts
.pagetitle
);
56 this.opts
.chat_antiflood_params
.burst
= Math
.min(
58 this.opts
.chat_antiflood_params
.burst
60 this.opts
.chat_antiflood_params
.sustained
= Math
.min(
62 this.opts
.chat_antiflood_params
.sustained
64 this.opts
.afk_timeout
= Math
.min(86400 /* one day */, this.opts
.afk_timeout
);
69 OptionsModule
.prototype.save = function (data
) {
70 data
.opts
= this.opts
;
73 OptionsModule
.prototype.packInfo = function (data
, isAdmin
) {
74 data
.pagetitle
= this.opts
.pagetitle
;
75 data
.public = this.opts
.show_public
;
77 data
.hasPassword
= this.opts
.password
!== false;
81 OptionsModule
.prototype.get = function (key
) {
82 return this.opts
[key
];
85 OptionsModule
.prototype.set = function (key
, value
) {
86 this.opts
[key
] = value
;
89 OptionsModule
.prototype.onUserPostJoin = function (user
) {
90 user
.socket
.on("setOptions", this.handleSetOptions
.bind(this, user
));
92 this.sendOpts([user
]);
95 OptionsModule
.prototype.sendOpts = function (users
) {
98 if (users
=== this.channel
.users
) {
99 this.channel
.broadcastAll("channelOpts", opts
);
101 users
.forEach(function (user
) {
102 user
.socket
.emit("channelOpts", opts
);
107 OptionsModule
.prototype.getPermissions = function () {
108 return this.channel
.modules
.permissions
;
111 OptionsModule
.prototype.handleSetOptions = function (user
, data
) {
112 if (typeof data
!== "object") {
116 if (!this.getPermissions().canSetOptions(user
)) {
117 user
.kick("Attempted setOptions as a non-moderator");
121 var sendUpdate
= false;
123 if ("allow_voteskip" in data
) {
124 this.opts
.allow_voteskip
= Boolean(data
.allow_voteskip
);
128 if ("voteskip_ratio" in data
) {
129 var ratio
= parseFloat(data
.voteskip_ratio
);
130 if (isNaN(ratio
) || ratio
< 0) {
131 user
.socket
.emit("validationError", {
132 target
: "#cs-voteskip_ratio",
133 message
: `Input must be a number 0 or greater, not "${data.voteskip_ratio}"`
136 this.opts
.voteskip_ratio
= ratio
;
138 user
.socket
.emit("validationPassed", {
139 target
: "#cs-voteskip_ratio"
144 if ("afk_timeout" in data
) {
145 var tm
= parseInt(data
.afk_timeout
);
146 if (isNaN(tm
) || tm
< 0 || tm
> 86400 /* one day */) {
148 user
.socket
.emit("validationError", {
149 target
: "#cs-afk_timeout",
150 message
: "AFK timeout must be between 1 and 86400 seconds (or 0 to disable)"
153 user
.socket
.emit("validationPassed", {
154 target
: "#cs-afk_timeout",
157 var same
= tm
=== this.opts
.afk_timeout
;
158 this.opts
.afk_timeout
= tm
;
160 this.channel
.users
.forEach(function (u
) {
168 if ("pagetitle" in data
&& user
.account
.effectiveRank
>= 3) {
169 var title
= unzalgo(("" + data
.pagetitle
).substring(0, 100));
170 if (!title
.trim().match(Config
.get("reserved-names.pagetitles"))) {
171 this.opts
.pagetitle
= title
;
174 user
.socket
.emit("errorMsg", {
175 msg
: "That pagetitle is reserved",
181 if ("maxlength" in data
) {
183 if (typeof data
.maxlength
!== "number") {
184 ml
= Utilities
.parseTime(data
.maxlength
);
186 ml
= parseInt(data
.maxlength
);
189 if (isNaN(ml
) || ml
< 0) {
192 this.opts
.maxlength
= ml
;
196 if ("playlist_max_duration_per_user" in data
) {
197 const max
= data
.playlist_max_duration_per_user
;
198 if (typeof max
!== "number" || isNaN(max
) || max
< 0) {
199 user
.socket
.emit("errorMsg", {
200 msg
: `Expected number for playlist_max_duration_per_user, not "${max}"`
203 this.opts
.playlist_max_duration_per_user
= max
;
208 if ("externalcss" in data
&& user
.account
.effectiveRank
>= 3) {
209 var prefix
= "Invalid URL for external CSS: ";
210 if (typeof data
.externalcss
!== "string") {
211 user
.socket
.emit("validationError", {
212 target
: "#cs-externalcss",
213 message
: prefix
+ "URL must be a string, not " + realTypeOf(data
.externalcss
)
217 var link
= data
.externalcss
.substring(0, 255).trim();
219 sendUpdate
= this.opts
.externalcss
!== "";
220 this.opts
.externalcss
= "";
221 user
.socket
.emit("validationPassed", {
222 target
: "#cs-externalcss"
225 var urldata
= url
.parse(link
);
226 if (!urldata
.protocol
|| urldata
.protocol
!== 'https:') {
227 user
.socket
.emit("validationError", {
228 target
: "#cs-externalcss",
229 message
: prefix
+ " URL must begin with 'https://'"
231 } else if (!urldata
.host
) {
232 user
.socket
.emit("validationError", {
233 target
: "#cs-externalcss",
234 message
: prefix
+ "missing hostname"
237 user
.socket
.emit("validationPassed", {
238 target
: "#cs-externalcss"
240 this.opts
.externalcss
= urldata
.href
;
246 if ("externaljs" in data
&& user
.account
.effectiveRank
>= 3) {
247 const prefix
= "Invalid URL for external JS: ";
248 if (typeof data
.externaljs
!== "string") {
249 user
.socket
.emit("validationError", {
250 target
: "#cs-externaljs",
251 message
: prefix
+ "URL must be a string, not " + realTypeOf(data
.externaljs
)
255 const link
= data
.externaljs
.substring(0, 255).trim();
257 sendUpdate
= this.opts
.externaljs
!== "";
258 this.opts
.externaljs
= "";
259 user
.socket
.emit("validationPassed", {
260 target
: "#cs-externaljs"
263 const urldata
= url
.parse(link
);
264 if (!urldata
.protocol
|| urldata
.protocol
!== 'https:') {
265 user
.socket
.emit("validationError", {
266 target
: "#cs-externaljs",
267 message
: prefix
+ " URL must begin with 'https://'"
269 } else if (!urldata
.host
) {
270 user
.socket
.emit("validationError", {
271 target
: "#cs-externaljs",
272 message
: prefix
+ "missing hostname"
275 user
.socket
.emit("validationPassed", {
276 target
: "#cs-externaljs"
278 this.opts
.externaljs
= urldata
.href
;
284 if ("chat_antiflood" in data
) {
285 this.opts
.chat_antiflood
= Boolean(data
.chat_antiflood
);
289 if ("chat_antiflood_params" in data
) {
290 if (typeof data
.chat_antiflood_params
!== "object") {
291 data
.chat_antiflood_params
= {
297 var b
= parseInt(data
.chat_antiflood_params
.burst
);
298 if (isNaN(b
) || b
< 0) {
304 var s
= parseFloat(data
.chat_antiflood_params
.sustained
);
305 if (isNaN(s
) || s
<= 0) {
312 this.opts
.chat_antiflood_params
= {
320 if ("show_public" in data
&& user
.account
.effectiveRank
>= 3) {
321 this.opts
.show_public
= Boolean(data
.show_public
);
325 if ("enable_link_regex" in data
) {
326 this.opts
.enable_link_regex
= Boolean(data
.enable_link_regex
);
330 if ("password" in data
&& user
.account
.effectiveRank
>= 3) {
331 var pw
= data
.password
+ "";
332 pw
= pw
=== "" ? false : pw
.substring(0, 100);
333 this.opts
.password
= pw
;
337 if ("allow_dupes" in data
) {
338 this.opts
.allow_dupes
= Boolean(data
.allow_dupes
);
342 if ("torbanned" in data
&& user
.account
.effectiveRank
>= 3) {
343 this.opts
.torbanned
= Boolean(data
.torbanned
);
347 if ("allow_anon_chat" in data
&& user
.account
.effectiveRank
>= 3) {
348 this.opts
.allow_anon_chat
= Boolean(data
.allow_anon_chat
);
352 if ("block_anonymous_users" in data
&& user
.account
.effectiveRank
>= 3) {
353 this.opts
.block_anonymous_users
= Boolean(data
.block_anonymous_users
);
357 if ("allow_ascii_control" in data
&& user
.account
.effectiveRank
>= 3) {
358 this.opts
.allow_ascii_control
= Boolean(data
.allow_ascii_control
);
362 if ("playlist_max_per_user" in data
&& user
.account
.effectiveRank
>= 3) {
363 var max
= parseInt(data
.playlist_max_per_user
);
364 if (!isNaN(max
) && max
>= 0) {
365 this.opts
.playlist_max_per_user
= max
;
370 if ("new_user_chat_delay" in data
) {
371 const delay
= data
.new_user_chat_delay
;
372 if (!isNaN(delay
) && delay
>= 0) {
373 this.opts
.new_user_chat_delay
= delay
;
378 if ("new_user_chat_link_delay" in data
) {
379 const delay
= data
.new_user_chat_link_delay
;
380 if (!isNaN(delay
) && delay
>= 0) {
381 this.opts
.new_user_chat_link_delay
= delay
;
386 this.channel
.logger
.log("[mod] " + user
.getName() + " updated channel options");
389 this.sendOpts(this.channel
.users
);
394 const combiners
= /[\u0300-\u036f\u0483-\u0487\u0591-\u05bd\u05bf-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7-\u05c7\u0610-\u061a\u064b-\u065f\u0670-\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711-\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08e4-\u0902\u093a-\u093a\u093c-\u093c\u0941-\u0948\u094d-\u094d\u0951-\u0957\u0962-\u0963\u0981-\u0981\u09bc-\u09bc\u09c1-\u09c4\u09cd-\u09cd\u09e2-\u09e3\u0a01-\u0a02\u0a3c-\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51-\u0a51\u0a70-\u0a71\u0a75-\u0a75\u0a81-\u0a82\u0abc-\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd-\u0acd\u0ae2-\u0ae3\u0b01-\u0b01\u0b3c-\u0b3c\u0b3f-\u0b3f\u0b41-\u0b44\u0b4d-\u0b4d\u0b56-\u0b56\u0b62-\u0b63\u0b82-\u0b82\u0bc0-\u0bc0\u0bcd-\u0bcd\u0c00-\u0c00\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0c81-\u0c81\u0cbc-\u0cbc\u0cbf-\u0cbf\u0cc6-\u0cc6\u0ccc-\u0ccd\u0ce2-\u0ce3\u0d01-\u0d01\u0d41-\u0d44\u0d4d-\u0d4d\u0d62-\u0d63\u0dca-\u0dca\u0dd2-\u0dd4\u0dd6-\u0dd6\u0e31-\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1-\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35-\u0f35\u0f37-\u0f37\u0f39-\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6-\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082-\u1082\u1085-\u1086\u108d-\u108d\u109d-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b4-\u17b5\u17b7-\u17bd\u17c6-\u17c6\u17c9-\u17d3\u17dd-\u17dd\u180b-\u180d\u18a9-\u18a9\u1920-\u1922\u1927-\u1928\u1932-\u1932\u1939-\u193b\u1a17-\u1a18\u1a1b-\u1a1b\u1a56-\u1a56\u1a58-\u1a5e\u1a60-\u1a60\u1a62-\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f-\u1a7f\u1ab0-\u1abd\u1b00-\u1b03\u1b34-\u1b34\u1b36-\u1b3a\u1b3c-\u1b3c\u1b42-\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1bab-\u1bad\u1be6-\u1be6\u1be8-\u1be9\u1bed-\u1bed\u1bef-\u1bf1\u1c2c-\u1c33\u1c36-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced-\u1ced\u1cf4-\u1cf4\u1cf8-\u1cf9\u1dc0-\u1df5\u1dfc-\u1dff\u20d0-\u20dc\u20e1-\u20e1\u20e5-\u20f0\u2cef-\u2cf1\u2d7f-\u2d7f\u2de0-\u2dff\u302a-\u302d\u3099-\u309a\ua66f-\ua66f\ua674-\ua67d\ua69f-\ua69f\ua6f0-\ua6f1\ua802-\ua802\ua806-\ua806\ua80b-\ua80b\ua825-\ua826\ua8c4-\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3-\ua9b3\ua9b6-\ua9b9\ua9bc-\ua9bc\ua9e5-\ua9e5\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43-\uaa43\uaa4c-\uaa4c\uaa7c-\uaa7c\uaab0-\uaab0\uaab2-\uaab4\uaab7-\uaab8\uaabe-\uaabf\uaac1-\uaac1\uaaec-\uaaed\uaaf6-\uaaf6\uabe5-\uabe5\uabe8-\uabe8\uabed-\uabed\ufb1e-\ufb1e\ufe00-\ufe0f\ufe20-\ufe2d]/g;
396 function unzalgo(text
) {
397 // TODO: consider only removing stacked combiners so that legitimate
398 // single combining characters can be used.
400 return text
.replace(combiners
, '');
403 module
.exports
= OptionsModule
;
404 //# sourceMappingURL=opts.js.map