From 9d2bfeb509e89f5a7631491a7585e0fcaecd158e Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 11:23:35 -0500 Subject: [PATCH 01/11] create youtube plugin --- server/chat-plugins/youtube.ts | 423 +++++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 server/chat-plugins/youtube.ts diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts new file mode 100644 index 000000000000..4c8e46d9a217 --- /dev/null +++ b/server/chat-plugins/youtube.ts @@ -0,0 +1,423 @@ +/** + * Youtube room chat-plugin. + * Supports adding channels and selecting a random channel. + * Also supports showing video data on request. + * Written by mia-pi, with some code / design concepts from Asheviere. + */ + +import * as https from 'https'; +import {FS} from '../../lib/fs'; +const ROOT = 'https://www.googleapis.com/youtube/v3/'; +const CHANNEL = `${ROOT}channels`; +const STORAGE_PATH = 'config/chat-plugins/youtube.json'; + +let channelData: AnyObject; + +try { + channelData = JSON.parse(FS(STORAGE_PATH).readSync()) || {}; +} catch (e) { + channelData = {}; +} + +export const YouTube = new class { + interval: NodeJS.Timeout | null; + constructor() { + this.interval = null; + } + async getChannelData(channel: string, username?: string) { + const queryUrl = `${CHANNEL}?part=snippet%2Cstatistics&id=${encodeURIComponent(channel)}&key=${Config.youtubeKey}`; + const query = new Promise((resolve, reject) => { + https.get(queryUrl, res => { + const data: string[] = []; + res.setEncoding('utf8'); + res.on('data', chunk => data.push(chunk)); + res.on('end', () => resolve(JSON.parse(data.join('')))); + }).on('error', reject); + }); + const res: any = await query.catch(() => {}); + if (!res) return null; + const data = res.items[0]; + const cache = { + name: data.snippet.title, + description: data.snippet.description, + url: data.snippet.customUrl, + icon: data.snippet.thumbnails.medium.url, + videos: Number(data.statistics.videoCount), + subs: Number(data.statistics.subscriberCount), + views: Number(data.statistics.viewCount), + username: username, + }; + channelData[channel] = {...cache}; + FS(STORAGE_PATH).writeUpdate(() => JSON.stringify(channelData)); + return cache; + } + async generateChannelDisplay(id: string) { + // url isn't needed but it destructures wrong without it + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {name, description, url, icon, videos, subs, views, username} = await this.get(id); + // credits asheviere for most of the html + let buf = `
`; + buf += `
`; + buf += `
`; + buf += ``; + buf += `

`; + buf += `${name}`; + buf += `

`; + buf += `

`; + buf += `${videos} videos | ${subs} subscribers | ${views} video views

`; + buf += `

`; + buf += `${description.slice(0, 400).replace(/\n/g, ' ')}${description.length > 400 ? '(...)' : ''}

`; + if (username) { + buf += `

PS username: ${username}

`; + } else { + buf += ''; + } + return buf; + } + randChannel() { + const keys = Object.keys(channelData); + const id = keys[Math.floor(Math.random() * keys.length)].trim(); + return this.generateChannelDisplay(id); + } + get(id: string, username?: string) { + if (!(id in channelData)) return this.getChannelData(id, username); + return {...channelData[id]}; + } + channelSearch(search: string) { + let channel; + if (channelData[search]) { + channel = search; + } else { + for (const id of Object.keys(channelData)) { + if (toID(channelData[id].name) === toID(search)) { + channel = id; + break; // don't iterate through everything once a match is found + } + } + } + return channel; + } + async generateVideoDisplay(link: string) { + let [, id] = link.split('v='); + if (id.includes('&')) id = id.split('&')[0]; + const queryUrl = `${ROOT}videos?part=snippet%2Cstatistics&id=${encodeURIComponent(id)}&key=${Config.youtubeKey}`; + const query = new Promise((resolve, reject) => { + https.get(queryUrl, res => { + const data: string[] = []; + res.setEncoding('utf8'); + res.on('data', chunk => data.push(chunk)); + res.on('end', () => resolve(JSON.parse(data.join('')))); + }).on('error', reject); + }); + const res: any = await query.catch(() => {}); + if (!res.items) return; + const video = res.items[0]; + const info = { + title: video.snippet.title, + date: new Date(video.snippet.publishedAt), + description: video.snippet.description, + channel: video.snippet.channelTitle, + channelUrl: video.snippet.channelId, + views: video.statistics.viewCount, + thumbnail: video.snippet.thumbnails.default.url, + likes: video.statistics.likeCount, + dislikes: video.statistics.dislikeCount, + }; + let buf = ``; + buf += ``; + return buf; + } + randomize() { + const rearranged: string[] = []; + const keys = Object.keys(channelData); + while (keys.length > 1) { + const item = keys[Math.floor(Math.random() * keys.length)]; + if (rearranged.includes(item)) continue; + const index = keys.indexOf(item); + if (index > -1) { + keys.splice(index, 1); + } + rearranged.push(item); + } + if (rearranged.length !== keys.length) { + for (const key of keys) { + if (!rearranged.includes(key)) rearranged.push(key); + } + } + return rearranged; + } +}; + +export const commands: ChatCommands = { + randchannel(target, room, user) { + if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); + this.runBroadcast(); + if (this.broadcasting) { + if (!this.can('broadcast', null, room)) return false; + return YouTube.randChannel().then(res => { + this.addBox(res); + room.update(); + }); + } else { + return YouTube.randChannel().then(res => this.sendReplyBox(res)); + } + }, + randchannelhelp: [`/randchannel - View data of a random channel from the YouTube database.`], + + yt: 'youtube', + youtube: { + async addchannel(target, room, user) { + if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); + let [id, name] = target.split(','); + if (id.includes('youtu')) id = id.split('channel/')[1]; + if (!id) return this.errorReply('Specify a channel ID.'); + if (!(await YouTube.getChannelData(id, name))) { + return this.errorReply(`Error in retrieving channel data.`); + } + this.modlog('ADDCHANNEL', null, `${id} ${name ? `username: ${name}` : ''}`); + this.privateModAction(`(${user.name} added channel with ID ${id} to the random channel pool.)`); + return this.sendReply(`Added channel with id ${id} ${name ? `and username (${name}) ` : ''} to the random channel pool.`); + }, + addchannelhelp: [`/addchannel - Add channel data to the Youtube database. Requires: % @ # ~`], + + removechannel(target, room, user) { + if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); + if (!this.can('ban', null, room)) return false; + const id = YouTube.channelSearch(target); + if (!id) return this.errorReply(`Channel with ID or name ${target} not found.`); + delete channelData[id]; + this.privateModAction(`(${user.name} deleted channel with ID or name ${target}.)`); + return this.modlog(`REMOVECHANNEL`, null, id); + }, + removechannelhelp: [`/youtube removechannel - Delete channel data from the YouTube database. Requires: % @ # ~`], + + channel(target, room, user) { + if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); + const channel = YouTube.channelSearch(target); + if (!channel) return this.errorReply(`No channels with ID or name ${target} found.`); + this.runBroadcast(); + if (this.broadcasting) { + return YouTube.generateChannelDisplay(channel).then(res => { + this.addBox(res); + room.update(); + }); + } else { + return YouTube.generateChannelDisplay(channel).then(res => this.sendReplyBox(res)); + } + }, + channelhelp: [ + '/youtube channel - View the data of a specified channel. Can be either channel ID or channel name.', + ], + async video(target, room, user) { + if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); + if (!target) return this.errorReply(`Provide a valid youtube link.`); + const html = await YouTube.generateVideoDisplay(target); + if (!html) return this.errorReply(`No such video found.`); + this.runBroadcast(); + if (this.broadcasting) { + this.addBox(html); + return room.update(); + } else { + return this.sendReplyBox(html); + } + }, + videohelp: [`/youtube video - View data of a specified video. Can be either channel ID or channel name`], + + channels(target, room, user) { + let all; + if (toID(target) === 'all') all = true; + return this.parse(`/j view-channels${all ? '-all' : ''}`); + }, + help(target, room, user) { + const buf = ( + `YouTube plugin commands:
` + + `/randchannel - View data of a random channel from the YouTube database.
` + + `/youtube addchannel [channel[- Add channel data to the Youtube database. Requires: % @ # ~
` + + `/youtube removechannel [channel]- Delete channel data from the YouTube database. Requires: % @ # ~
` + + `/youtube channel [channel] - View the data of a specified channel. Can be either channel ID or channel name.
` + + `/youtube video [video] - View data of a specified video. Can be either channel ID or channel name.
` + + `/youtube update [channel], [name] - sets a channel's PS username to [name]. Requires: % @ # ~
` + + `/youtube repeat [time] - Sets an interval for [time] minutes, showing a random channel each time. Requires: # & ~
` + ); + this.runBroadcast(); + if (this.broadcasting) { + return this.add(`|html|
${buf}
`); + } + return this.sendReplyBox(buf); + }, + update(target, room, user) { + if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); + if (!this.can('ban', null, room)) return false; + const [channel, name] = target.split(','); + const id = YouTube.channelSearch(channel); + if (!id) return this.errorReply(`Channel ${channel} is not in the database.`); + channelData[id].username = name; + this.modlog(`UPDATECHANNEL`, null, name); + this.privateModAction(`(${user.name} updated channel ${id}'s username to ${name}.)`); + return FS(STORAGE_PATH).writeUpdate(() => JSON.stringify(channelData)); + }, + repeat(target, room, user) { + if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); + if (!this.can('declare', null, room)) return false; + if (!target || isNaN(parseInt(target))) return this.errorReply(`Specify a number (in minutes) for the interval.`); + let interval = Number(target); + if (interval < 10) return this.errorReply(`${interval} is too low - set it above 10 minutes.`); + interval = interval * 60 * 1000; + if (YouTube.interval) clearInterval(YouTube.interval); + YouTube.interval = setInterval( + () => void YouTube.randChannel().then(data => { + this.addBox(data); + room.update(); + }), + interval + ); + this.privateModAction(`(${user.name} set a randchannel interval to ${target} minutes)`); + return this.modlog(`CHANNELINTERVAL`, null, `${target} minutes`); + }, + }, + requestapproval(target, room, user) { + if (!this.canTalk()) return false; + if (this.can('mute', null, room)) return this.errorReply(`Use !link instead.`); + if (room.pendingApprovals.has(user.id)) return this.errorReply('You have a request pending already.'); + if (!toID(target)) return this.parse(`/help requestapproval`); + if (!/^https?:\/\//.test(target)) target = `http://${target}`; + room.pendingApprovals.set(user.id, target); + this.sendReply(`You have requested for the link ${target} to be displayed.`); + room.sendMods( + `|uhtml|request-${user.id}|
${user.name} has requested approval to show media.
` + + `
` + + `
` + ); + return room.update(); + }, + requestapprovalhelp: [`/requestapproval [link], [comment] - Requests permission to show media in the room.`], + + approvelink(target, room, user) { + if (!this.can('mute', null, room)) return false; + target = toID(target); + if (!target) return this.parse(`/help approvelink`); + const id = room.pendingApprovals.get(target); + if (!id) return this.errorReply(`${target} has no pending request.`); + this.privateModAction(`(${user.name} approved ${target}'s media display request.)`); + this.modlog(`APPROVELINK`, null, `${target} (${id})`); + room.pendingApprovals.delete(target); + if (id.includes('youtu')) { + return YouTube.generateVideoDisplay(id).then(res => { + room.add(`|uhtmlchange|request-${target}|`).update(); + res += `

(Suggested by ${target})

`; + this.addBox(res as string); + room.update(); + }); + } else { + void Chat.fitImage(id).then(([width, height]) => { + this.addBox(Chat.html``); + room.add(`|uhtmlchange|request-${target}|`); + room.update(); + }); + } + }, + approvelinkhelp: [`/approvelink [user] - Approves the media display request of [user]. Requires: % @ # & ~`], + + denylink(target, room, user) { + if (!this.can('mute', null, room)) return false; + target = toID(target); + if (!target) return this.parse(`/help denylink`); + const id = room.pendingApprovals.get(target); + if (!id) return this.errorReply(`${target} has no pending request.`); + room.pendingApprovals.delete(target); + room.add(`|uhtmlchange|request-${target}|`).update(); + this.privateModAction(`(${user.name} denied ${target}'s media display request.)`); + this.modlog(`DENYLINK`, null, `${target} (${id})`); + }, + denylinkhelp: [`/denylink [user] - Denies the media display request of [user]. Requires: % @ # & ~`], + + link(target, room, user) { + if (!this.can('mute', null, room) && !(user.id in room.chatRoomData!.whitelist)) return false; + if (!toID(target).trim()) return this.parse(`/help link`); + const [link, comment] = target.split(','); + this.runBroadcast(); + if (target.includes('youtu')) { + return YouTube.generateVideoDisplay(link).then(res => { + let buf = res; + buf += `

(Suggested by ${user.name})

`; + if (comment) buf += `
(${comment})`; + this.addBox(buf as string); + room.update(); + }); + } else { + void Chat.fitImage(link).then(([width, height]) => { + let buf = Chat.html``; + if (comment) buf += `
(${comment})`; + if (!this.broadcasting) return this.sendReplyBox(buf); + this.addBox(buf); + room.update(); + }); + } + }, + linkhelp: [`/link [url] - shows an image or video url in chat. Requires: whitelist % @ # & ~`], + + whitelist(target, room, user) { + if (!this.can('ban', null, room)) return false; + target = toID(target); + if (!target) return this.parse(`/help whitelist`); + if (!Users.get(target)) return this.errorReply(`User not found.`); + if (target in room.auth! && room.auth![target] !== '+' || Users.get(target)!.isStaff) { + return this.errorReply(`You don't need to whitelist staff - they can just use !link.`); + } + if (!room.whitelistUser(target)) return this.errorReply(`${target} is already whitelisted.`); + if (Users.get(target)) Users.get(target)?.popup(`You have been whitelisted for linking in ${room}.`); + this.privateModAction(`(${user.name} whitelisted ${target} for links.)`); + this.modlog(`WHITELIST`, target); + }, + whitelisthelp: [`/whitelist [user] - Whitelists [user] to post media in the room. Requires: % @ & # ~`], + + unwhitelist(target, room, user) { + if (!this.can('mute', null, room)) return false; + target = toID(target); + if (Users.get(target)) Users.get(target)?.popup(`You have been removed from the link whitelist in ${room}.`); + if (!target) return this.parse(`/help unwhitelist`); + if (!room.unwhitelistUser(target)) return this.errorReply(`${target} is not whitelisted.`); + this.sendReply(`Unwhitelisted ${target} for linking.`); + this.modlog(`UNWHITELIST`, target); + this.privateModAction(`(${user} removed ${target} from the link whitelist.)`); + return Rooms.global.writeChatRoomData(); + }, +}; + +export const pages: PageTable = { + async channels(args, user) { + const all = toID(args[0]) === 'all'; + this.title = `[Channels] ${all ? 'All' : ''}`; + let buffer = `

Channels in the Youtube database:`; + buffer += `
`; + if (all) buffer += `(All)`; + buffer += `


`; + const isStaff = user.can('mute', null, Rooms.get('youtube')); + for (const id of YouTube.randomize()) { + const name = YouTube.get(id).name; + const psid = YouTube.get(id).username; + if (!all && !psid) continue; + buffer += `
${name}`; + if (isStaff) buffer += ` (Channel ID: ${id})`; + if (psid) buffer += ` (PS name: ${psid})`; + buffer += ``; + buffer += await YouTube.generateChannelDisplay(id); + if (!isStaff) buffer += `(Channel ID: ${id})`; + buffer += `

`; + } + buffer += `
`; + return buffer; + }, +}; From e6e5220851de802d90c03ea1af5b0673fc12c766 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 11:23:58 -0500 Subject: [PATCH 02/11] make necessary rooms changes --- server/rooms.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/server/rooms.ts b/server/rooms.ts index 406f1436a0f7..d4d61b76aa53 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -1080,6 +1080,8 @@ export class BasicChatRoom extends BasicRoom { game: RoomGame | null; battle: RoomBattle | null; tour: Tournament | null; + pendingApprovals: Map; + whitelist: AnyObject; constructor(roomid: RoomID, title?: string, options: AnyObject = {}) { super(roomid, title); @@ -1111,6 +1113,7 @@ export class BasicChatRoom extends BasicRoom { this.chatRoomData = (options.isPersonal ? null : options); this.minorActivity = null; + this.whitelist = {}; Object.assign(this, options); if (this.auth) Object.setPrototypeOf(this.auth, null); this.parent = null; @@ -1147,6 +1150,8 @@ export class BasicChatRoom extends BasicRoom { this.tour = null; this.game = null; this.battle = null; + this.pendingApprovals = new Map(); + this.whitelist = this.chatRoomData!.whitelist || {}; } /** @@ -1428,6 +1433,20 @@ export class BasicChatRoom extends BasicRoom { return this.log.rename(newID); } + whitelistUser(userid: string) { + if (userid in this.whitelist) return false; + this.whitelist[userid] = true; + this.chatRoomData!.whitelist = this.whitelist; + Rooms.global.writeChatRoomData(); + return true; + } + unwhitelistUser(userid: string) { + if (!(userid in this.whitelist)) return false; + delete this.whitelist[userid]; + this.chatRoomData!.whitelist = this.whitelist; + Rooms.global.writeChatRoomData(); + return true; + } destroy() { // deallocate ourself From 26180556485fb8181493a8b01230e43822063ca5 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 11:25:23 -0500 Subject: [PATCH 03/11] doesnt need to be an anyobject --- server/rooms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/rooms.ts b/server/rooms.ts index d4d61b76aa53..6d227555d1a4 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -1081,7 +1081,7 @@ export class BasicChatRoom extends BasicRoom { battle: RoomBattle | null; tour: Tournament | null; pendingApprovals: Map; - whitelist: AnyObject; + whitelist: {[k: string]: boolean}; constructor(roomid: RoomID, title?: string, options: AnyObject = {}) { super(roomid, title); From 7c3d2d737006c84fa721bef99e4729c517bf7889 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 13:53:32 -0500 Subject: [PATCH 04/11] update help / perms --- server/chat-plugins/youtube.ts | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index 4c8e46d9a217..35f7a8e65874 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -240,22 +240,9 @@ export const commands: ChatCommands = { return this.parse(`/j view-channels${all ? '-all' : ''}`); }, help(target, room, user) { - const buf = ( - `YouTube plugin commands:
` + - `/randchannel - View data of a random channel from the YouTube database.
` + - `/youtube addchannel [channel[- Add channel data to the Youtube database. Requires: % @ # ~
` + - `/youtube removechannel [channel]- Delete channel data from the YouTube database. Requires: % @ # ~
` + - `/youtube channel [channel] - View the data of a specified channel. Can be either channel ID or channel name.
` + - `/youtube video [video] - View data of a specified video. Can be either channel ID or channel name.
` + - `/youtube update [channel], [name] - sets a channel's PS username to [name]. Requires: % @ # ~
` + - `/youtube repeat [time] - Sets an interval for [time] minutes, showing a random channel each time. Requires: # & ~
` - ); - this.runBroadcast(); - if (this.broadcasting) { - return this.add(`|html|
${buf}
`); - } - return this.sendReplyBox(buf); + return this.parse('/help youtube') }, + update(target, room, user) { if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); if (!this.can('ban', null, room)) return false; @@ -286,6 +273,18 @@ export const commands: ChatCommands = { return this.modlog(`CHANNELINTERVAL`, null, `${target} minutes`); }, }, + + youtubehelp: [ + `YouTube commands:`, + `/randchannel - View data of a random channel from the YouTube database.`, + `/youtube addchannel [channel[- Add channel data to the Youtube database. Requires: % @ # ~`, + `/youtube removechannel [channel]- Delete channel data from the YouTube database. Requires: % @ # ~`, + `/youtube channel [channel] - View the data of a specified channel. Can be either channel ID or channel name.`, + `/youtube video [video] - View data of a specified video. Can be either channel ID or channel name.`, + `/youtube update [channel], [name] - sets a channel's PS username to [name]. Requires: % @ # ~`, + `/youtube repeat [time] - Sets an interval for [time] minutes, showing a random channel each time. Requires: # & ~`, + ], + requestapproval(target, room, user) { if (!this.canTalk()) return false; if (this.can('mute', null, room)) return this.errorReply(`Use !link instead.`); @@ -383,7 +382,7 @@ export const commands: ChatCommands = { whitelisthelp: [`/whitelist [user] - Whitelists [user] to post media in the room. Requires: % @ & # ~`], unwhitelist(target, room, user) { - if (!this.can('mute', null, room)) return false; + if (!this.can('ban', null, room)) return false; target = toID(target); if (Users.get(target)) Users.get(target)?.popup(`You have been removed from the link whitelist in ${room}.`); if (!target) return this.parse(`/help unwhitelist`); From 6a8308a7b84c7bb469e2f17bcd525a4c0b318aac Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 13:57:09 -0500 Subject: [PATCH 05/11] semicolon --- server/chat-plugins/youtube.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index 35f7a8e65874..89e2ebd0a29c 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -240,7 +240,7 @@ export const commands: ChatCommands = { return this.parse(`/j view-channels${all ? '-all' : ''}`); }, help(target, room, user) { - return this.parse('/help youtube') + return this.parse('/help youtube'); }, update(target, room, user) { From bfa70001fd4832696619f6301c3e5c3833a2f4d1 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 15:22:11 -0500 Subject: [PATCH 06/11] Apply suggestions from code review Co-authored-by: Kris Johnson <11083252+KrisXV@users.noreply.github.com> --- server/chat-plugins/youtube.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index 89e2ebd0a29c..9eaeaefa2d35 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -14,12 +14,12 @@ const STORAGE_PATH = 'config/chat-plugins/youtube.json'; let channelData: AnyObject; try { - channelData = JSON.parse(FS(STORAGE_PATH).readSync()) || {}; + channelData = JSON.parse(FS(STORAGE_PATH).readIfExistsSync() || "{}"); } catch (e) { channelData = {}; } -export const YouTube = new class { +export class Youtube { interval: NodeJS.Timeout | null; constructor() { this.interval = null; @@ -76,7 +76,7 @@ export const YouTube = new class { } randChannel() { const keys = Object.keys(channelData); - const id = keys[Math.floor(Math.random() * keys.length)].trim(); + const id = Dex.shuffle(keys)[0].trim(); return this.generateChannelDisplay(id); } get(id: string, username?: string) { From e69ed12cd9570762d15ae4d7740948cad3dc43f2 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 15:24:35 -0500 Subject: [PATCH 07/11] Update youtube.ts --- server/chat-plugins/youtube.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index 9eaeaefa2d35..4f04085e808e 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -19,7 +19,7 @@ try { channelData = {}; } -export class Youtube { +export class YoutubeInterface { interval: NodeJS.Timeout | null; constructor() { this.interval = null; @@ -159,6 +159,7 @@ export class Youtube { } }; +const YouTube = new YoutubeInterface(); export const commands: ChatCommands = { randchannel(target, room, user) { if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); From 981b0df637f126cd3da09a1efabccf72c778897b Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 24 May 2020 15:29:46 -0500 Subject: [PATCH 08/11] Update youtube.ts --- server/chat-plugins/youtube.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index 4f04085e808e..e771a9a139c1 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -20,7 +20,7 @@ try { } export class YoutubeInterface { - interval: NodeJS.Timeout | null; + interval: NodeJS.Timer | null; constructor() { this.interval = null; } @@ -157,9 +157,10 @@ export class YoutubeInterface { } return rearranged; } -}; +} const YouTube = new YoutubeInterface(); + export const commands: ChatCommands = { randchannel(target, room, user) { if (room.roomid !== 'youtube') return this.errorReply(`This command can only be used in the YouTube room.`); From 51bc3286e4ab9664df07f84333ddf201ed65983d Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Mon, 25 May 2020 16:01:10 -0500 Subject: [PATCH 09/11] remove approvals/link will be in a different commit --- server/chat-plugins/youtube.ts | 113 +-------------------------------- 1 file changed, 3 insertions(+), 110 deletions(-) diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index e771a9a139c1..4c3d59be9442 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -129,12 +129,13 @@ export class YoutubeInterface { buf += ``; buf += `

`; buf += `${info.title}`; - buf += `

`; return buf; } @@ -286,114 +287,6 @@ export const commands: ChatCommands = { `/youtube update [channel], [name] - sets a channel's PS username to [name]. Requires: % @ # ~`, `/youtube repeat [time] - Sets an interval for [time] minutes, showing a random channel each time. Requires: # & ~`, ], - - requestapproval(target, room, user) { - if (!this.canTalk()) return false; - if (this.can('mute', null, room)) return this.errorReply(`Use !link instead.`); - if (room.pendingApprovals.has(user.id)) return this.errorReply('You have a request pending already.'); - if (!toID(target)) return this.parse(`/help requestapproval`); - if (!/^https?:\/\//.test(target)) target = `http://${target}`; - room.pendingApprovals.set(user.id, target); - this.sendReply(`You have requested for the link ${target} to be displayed.`); - room.sendMods( - `|uhtml|request-${user.id}|
${user.name} has requested approval to show media.
` + - `
` + - `
` - ); - return room.update(); - }, - requestapprovalhelp: [`/requestapproval [link], [comment] - Requests permission to show media in the room.`], - - approvelink(target, room, user) { - if (!this.can('mute', null, room)) return false; - target = toID(target); - if (!target) return this.parse(`/help approvelink`); - const id = room.pendingApprovals.get(target); - if (!id) return this.errorReply(`${target} has no pending request.`); - this.privateModAction(`(${user.name} approved ${target}'s media display request.)`); - this.modlog(`APPROVELINK`, null, `${target} (${id})`); - room.pendingApprovals.delete(target); - if (id.includes('youtu')) { - return YouTube.generateVideoDisplay(id).then(res => { - room.add(`|uhtmlchange|request-${target}|`).update(); - res += `

(Suggested by ${target})

`; - this.addBox(res as string); - room.update(); - }); - } else { - void Chat.fitImage(id).then(([width, height]) => { - this.addBox(Chat.html``); - room.add(`|uhtmlchange|request-${target}|`); - room.update(); - }); - } - }, - approvelinkhelp: [`/approvelink [user] - Approves the media display request of [user]. Requires: % @ # & ~`], - - denylink(target, room, user) { - if (!this.can('mute', null, room)) return false; - target = toID(target); - if (!target) return this.parse(`/help denylink`); - const id = room.pendingApprovals.get(target); - if (!id) return this.errorReply(`${target} has no pending request.`); - room.pendingApprovals.delete(target); - room.add(`|uhtmlchange|request-${target}|`).update(); - this.privateModAction(`(${user.name} denied ${target}'s media display request.)`); - this.modlog(`DENYLINK`, null, `${target} (${id})`); - }, - denylinkhelp: [`/denylink [user] - Denies the media display request of [user]. Requires: % @ # & ~`], - - link(target, room, user) { - if (!this.can('mute', null, room) && !(user.id in room.chatRoomData!.whitelist)) return false; - if (!toID(target).trim()) return this.parse(`/help link`); - const [link, comment] = target.split(','); - this.runBroadcast(); - if (target.includes('youtu')) { - return YouTube.generateVideoDisplay(link).then(res => { - let buf = res; - buf += `

(Suggested by ${user.name})

`; - if (comment) buf += `
(${comment})`; - this.addBox(buf as string); - room.update(); - }); - } else { - void Chat.fitImage(link).then(([width, height]) => { - let buf = Chat.html``; - if (comment) buf += `
(${comment})`; - if (!this.broadcasting) return this.sendReplyBox(buf); - this.addBox(buf); - room.update(); - }); - } - }, - linkhelp: [`/link [url] - shows an image or video url in chat. Requires: whitelist % @ # & ~`], - - whitelist(target, room, user) { - if (!this.can('ban', null, room)) return false; - target = toID(target); - if (!target) return this.parse(`/help whitelist`); - if (!Users.get(target)) return this.errorReply(`User not found.`); - if (target in room.auth! && room.auth![target] !== '+' || Users.get(target)!.isStaff) { - return this.errorReply(`You don't need to whitelist staff - they can just use !link.`); - } - if (!room.whitelistUser(target)) return this.errorReply(`${target} is already whitelisted.`); - if (Users.get(target)) Users.get(target)?.popup(`You have been whitelisted for linking in ${room}.`); - this.privateModAction(`(${user.name} whitelisted ${target} for links.)`); - this.modlog(`WHITELIST`, target); - }, - whitelisthelp: [`/whitelist [user] - Whitelists [user] to post media in the room. Requires: % @ & # ~`], - - unwhitelist(target, room, user) { - if (!this.can('ban', null, room)) return false; - target = toID(target); - if (Users.get(target)) Users.get(target)?.popup(`You have been removed from the link whitelist in ${room}.`); - if (!target) return this.parse(`/help unwhitelist`); - if (!room.unwhitelistUser(target)) return this.errorReply(`${target} is not whitelisted.`); - this.sendReply(`Unwhitelisted ${target} for linking.`); - this.modlog(`UNWHITELIST`, target); - this.privateModAction(`(${user} removed ${target} from the link whitelist.)`); - return Rooms.global.writeChatRoomData(); - }, }; export const pages: PageTable = { From 1ee45880278ce0e3ad3c14fff2cae84aecf22479 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Mon, 25 May 2020 16:01:42 -0500 Subject: [PATCH 10/11] Update rooms.ts --- server/rooms.ts | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/server/rooms.ts b/server/rooms.ts index 6d227555d1a4..406f1436a0f7 100644 --- a/server/rooms.ts +++ b/server/rooms.ts @@ -1080,8 +1080,6 @@ export class BasicChatRoom extends BasicRoom { game: RoomGame | null; battle: RoomBattle | null; tour: Tournament | null; - pendingApprovals: Map; - whitelist: {[k: string]: boolean}; constructor(roomid: RoomID, title?: string, options: AnyObject = {}) { super(roomid, title); @@ -1113,7 +1111,6 @@ export class BasicChatRoom extends BasicRoom { this.chatRoomData = (options.isPersonal ? null : options); this.minorActivity = null; - this.whitelist = {}; Object.assign(this, options); if (this.auth) Object.setPrototypeOf(this.auth, null); this.parent = null; @@ -1150,8 +1147,6 @@ export class BasicChatRoom extends BasicRoom { this.tour = null; this.game = null; this.battle = null; - this.pendingApprovals = new Map(); - this.whitelist = this.chatRoomData!.whitelist || {}; } /** @@ -1433,20 +1428,6 @@ export class BasicChatRoom extends BasicRoom { return this.log.rename(newID); } - whitelistUser(userid: string) { - if (userid in this.whitelist) return false; - this.whitelist[userid] = true; - this.chatRoomData!.whitelist = this.whitelist; - Rooms.global.writeChatRoomData(); - return true; - } - unwhitelistUser(userid: string) { - if (!(userid in this.whitelist)) return false; - delete this.whitelist[userid]; - this.chatRoomData!.whitelist = this.whitelist; - Rooms.global.writeChatRoomData(); - return true; - } destroy() { // deallocate ourself From 1923595d00d3b0f9f1a343c9e8117d74683e057b Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Mon, 25 May 2020 19:34:40 -0500 Subject: [PATCH 11/11] save data upon deletion --- server/chat-plugins/youtube.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/chat-plugins/youtube.ts b/server/chat-plugins/youtube.ts index 4c3d59be9442..344dcf454cc2 100644 --- a/server/chat-plugins/youtube.ts +++ b/server/chat-plugins/youtube.ts @@ -200,6 +200,7 @@ export const commands: ChatCommands = { const id = YouTube.channelSearch(target); if (!id) return this.errorReply(`Channel with ID or name ${target} not found.`); delete channelData[id]; + FS(STORAGE_PATH).writeUpdate(() => JSON.stringify(channelData)); this.privateModAction(`(${user.name} deleted channel with ID or name ${target}.)`); return this.modlog(`REMOVECHANNEL`, null, id); },
`; + buf += `
`; + buf += ``; + buf += `

`; + buf += `${info.title}`; + buf += `

`; + buf += `

`; + buf += `${info.likes} likes | ${info.dislikes} dislikes | ${info.views} video views

`; + buf += `Published on ${info.date} | ID: ${id}

`; + buf += `
Video Description

`; + buf += `

`; + buf += `${info.description.slice(0, 400).replace(/\n/g, ' ')}${info.description.length > 400 ? '(...)' : ''}

`; + buf += `

`; buf += `

`; buf += `${info.likes} likes | ${info.dislikes} dislikes | ${info.views} video views

`; buf += `Published on ${info.date} | ID: ${id}

`; buf += `
Video Description

`; - buf += `

`; + buf += `

`; buf += `${info.description.slice(0, 400).replace(/\n/g, ' ')}${info.description.length > 400 ? '(...)' : ''}