您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Bot that manages stack for meetings. "Stack" puts you on stack and "Pop" drops the oldest person.
当前为
- // ==UserScript==
- // @name Comrade: Stack Bot for Zoom
- // @description Bot that manages stack for meetings. "Stack" puts you on stack and "Pop" drops the oldest person.
- // @version 1.1
- // @grant none
- // @include https://zoom.us/j/*
- // @include https://*.zoom.us/j/*
- // @include https://zoom.us/s/*
- // @include https://*.zoom.us/s/*
- // @include https://*.zoom.us/wc/*
- // @namespace https://greasyfork.org/users/22981
- // ==/UserScript==
- /*
- ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4)
- Copyright © 2021 Adam Novak
- This is anti-capitalist software, released for free use by individuals and
- organizations that do not operate by capitalist principles.
- Permission is hereby granted, free of charge, to any person or organization
- (the "User") obtaining a copy of this software and associated documentation
- files (the "Software"), to use, copy, modify, merge, distribute, and/or sell
- copies of the Software, subject to the following conditions:
- 1. The above copyright notice and this permission notice shall be included in
- all copies or modified versions of the Software.
- 2. The User is one of the following:
- a. An individual person, laboring for themselves
- b. A non-profit organization
- c. An educational institution
- d. An organization that seeks shared profit for all of its members, and
- allows non-members to set the cost of their labor
- 3. If the User is an organization with owners, then all owners are workers and
- all workers are owners with equal equity and/or equal vote.
- 4. If the User is an organization, then the User is not law enforcement or
- military, or working for or under either.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY
- KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 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.
- */
- //// CONFIG
- const BOT_NAME = 'Comrade'
- const QUEUE_KEYWORD = 'stack'
- const DEQUEUE_KEYWORD = 'pop'
- const GIVEUP_KEYWORD = 'unstack'
- const REMIND_KEYWORD = 'who'
- const HELP_KEYWORD = 'help'
- const HELP_TEXT = `
- ${BOT_NAME} is a bot who can stack. Type:
- 1. "${QUEUE_KEYWORD}" to put yourself on stack.
- 2. "${DEQUEUE_KEYWORD}" when you are done so the bot can announce who is next.
- 3. "${GIVEUP_KEYWORD}" if you are on stack but don't want to be.
- 4. "${REMIND_KEYWORD}" if you forgot who is on stack.
- Type "${HELP_KEYWORD}" to see this message again.
- `
- // How many pixels can we be scrolled from the bottom and still think all
- // messages we see are the latest ones?
- const END_OF_HISTORY_HEIGHT = 10
- //// LIBRARY
- // Let async code wait.
- // See: <https://stackoverflow.com/a/39914235>
- function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- // Find a button by text. Returns the button element, or undefined.
- function findButton(text) {
- let all_buttons = document.getElementsByTagName('button')
- var this_button = undefined
- for (let button of all_buttons) {
- if (!this_button && button.innerText.includes(text)) {
- this_button = button
- }
- }
- return this_button
- }
- //// ENSURE WEB CLIENT ACCESSIBLE
- function showWebClientLink() {
- let results = document.getElementsByClassName('webclient')
- if (results[0]) {
- results[0].classList.remove('hideme')
- }
- }
- // We may be on a non-meeting page. Make sure people can join.
- showWebClientLink()
- //// BOT
- // Wait for the client to start, open necessary panes, and set name
- async function botStartup() {
- try {
- while (true) {
- // Wait until ready. We assume we are ready when the join audio button comes up
- console.log('Waiting for join audio button...')
- let audio_button = document.getElementsByClassName('join-audio-by-voip__join-btn')[0]
- if (audio_button && audio_button.offsetParent != null) {
- // It exists and is visible
- break
- }
- await sleep(1000)
- }
- console.log('Audio button visible')
- while (true) {
- // Wait for the chat and participants buttons
- // They may be in the bore button.
- console.log('Waiting for chat and participants buttons...')
- let more_button = document.getElementsByClassName('more-button')[0]
- let participants_button = findButton('Participants')
- let chat_button = findButton('Chat')
- // TODO: look in more menu
- if (participants_button && chat_button) {
- // The buttons exist, so click them and move on
- participants_button.click()
- chat_button.click()
- break
- }
- // Otherwise try again
- await sleep(1000)
- }
- // We only need to rename sometimes
- var named_right = false;
- while (true) {
- // When the user list comes up, find and hover over ourselves
- console.log('Waiting for own participant entry...')
- // We are always at the top
- let user_entry = document.getElementById('participants-list-0')
- if (user_entry) {
- // The entry exists
- if (user_entry.innerText.startsWith(BOT_NAME)) {
- // No need to change name
- named_right = true;
- } else {
- // Hover user entry to create rename button
- // We can't hit the react hover button because it's hiding in a Symbol-named property or something.
- // This fake event doesn't always work...
- let event = new MouseEvent('mouseover', {
- 'view': window,
- 'bubbles': true,
- 'cancelable': true
- })
- user_entry.dispatchEvent(event)
- }
- break
- }
- // Otherwise try again
- await sleep(1000)
- }
- while (!named_right) {
- // When the user buttons come up, find and click the rename button
- console.log('Waiting for rename button...')
- let rename_button = findButton('Rename')
- if (rename_button) {
- // The button exists, so click it and move on
- rename_button.click()
- break
- }
- // Otherwise try again
- await sleep(1000)
- }
- while (!named_right) {
- // When the rename dialog comes up, enter our name and click save.
- console.log('Waiting for rename controls...')
- let new_name_field = document.getElementById('newname')
- let save_button = findButton('Save')
- if (new_name_field && save_button) {
- // The controls exist, so operate them and move on
- console.log('Setting name')
- new_name_field.value = BOT_NAME
- console.log('Sending change')
- let event = new Event('change', {
- 'view': window,
- 'bubbles': true,
- 'cancelable': true
- })
- new_name_field.dispatchEvent(event)
- save_button.click()
- named_right = true
- break
- }
- // Otherwise try again
- await sleep(1000)
- }
- // Now grab the chat log
- let chat_log = document.getElementsByClassName('chat-virtualized-list')[0]
- console.log('Chat log: ', chat_log)
- // Watch for chats
- let chat_watcher = new MutationObserver(chatChange)
- chat_watcher.observe(chat_log, {childList: true, subtree: true, characterDataOldValue: true})
- console.log('Watching with: ', chat_watcher)
- console.log(BOT_NAME + ' is ready.')
- await sleep(3000)
- say(BOT_NAME + ' is ready.')
- showHelp()
- } catch (e) {
- console.error('Comrade initialization error: ', e)
- }
- }
- // Handle changes to the chat log and translate them into internal chat message calls
- function chatChange(mutations, chat_watcher) {
- try {
- // Find the chat scroll and see where it is scrolled to
- let chat_scroller = document.getElementsByClassName('chat-virtualized-list')[0]
- for (let record of mutations) {
- if ((record.addedNodes.length == 0 && record.type != 'characterData') || (record.addedNodes.length == 1 && record.addedNodes[0].classList.contains('chat-item__chat-info-time-stamp'))) {
- // These are noise
- continue
- }
- console.log(record)
- if (record.nextSibling) {
- // Zoom pages the chat messages in and out as we scroll the chat, and also when it feels like.
- // We can skip most of them here, but they will be re-seen if we are scrolling up and down.
- continue
- }
- // We may get new nodes, or changed text.
- if (record.type == 'characterData') {
- // New message from the same person as last time. Assume it is an append.
- let chat_entry = record.target.parentElement.parentElement
- let chat_sender_item = chat_entry.getElementsByClassName('chat-item__sender')[0]
- if (chat_sender_item) {
- let chat_sender = chat_sender_item.innerText
- // Trim off the old text and the intervening newline
- let chat_content = record.target.textContent.substr(record.oldValue.length + 1)
- console.log(chat_sender + ' also says: ' + chat_content)
- onChat(chat_sender, chat_content)
- }
- }
- if (chat_scroller && chat_scroller.scrollTopMax - chat_scroller.scrollTop > END_OF_HISTORY_HEIGHT) {
- // We are probably scrolling around the list and not getting new messages.
- console.log('Skipping new nodes as we are not at the end of history: ' + chat_scroller.scrollTop + '/' + chat_scroller.scrollTopMax)
- continue
- }
- for (let new_node of record.addedNodes) {
- let chat_sender_item = new_node.getElementsByClassName('chat-item__sender')[0]
- let chat_content_item = new_node.getElementsByTagName('pre')[0]
- if (chat_sender_item && chat_content_item) {
- let chat_sender = chat_sender_item.innerText
- let chat_content = chat_content_item.innerText
- if (chat_sender !== undefined && chat_content !== undefined) {
- console.log(chat_sender + ' says: ' + chat_content)
- onChat(chat_sender, chat_content)
- }
- }
- }
- }
- } catch (e) {
- console.error('Comrade element watch error: ', e)
- }
- }
- // Keep track of the meeting stack
- var stack = []
- // Put a person on the stack, if not on stack already
- function addToStack(who) {
- if (stack.includes(who)) {
- say(who + ' is already on stack.')
- } else {
- stack.push(who)
- reportStack()
- }
- }
- // Remove the oldest person from the stack
- function popFromStack() {
- removed = stack[0]
- stack = stack.slice(1)
- if (removed) {
- say('Removed ' + removed + ' from stack.')
- }
- reportStack()
- }
- // Drop the given person from stack
- function removeFromStack(who) {
- let new_stack = []
- var removed = false
- for (let person of stack) {
- if (person != who) {
- new_stack.push(person)
- } else {
- removed = true
- }
- }
- stack = new_stack
- if (removed) {
- say('Removed ' + who + ' from stack')
- } else {
- say(who + 'was not on stack')
- }
- reportStack()
- }
- // Read out the stack
- function reportStack() {
- if (stack.length == 0) {
- say('Stack is empty')
- } else {
- say('Next on stack is: ' + stack[0])
- if (stack.length > 1) {
- say('After that:')
- for (let i = 1; i < stack.length; i++) {
- say(i + '. ' + stack[i])
- }
- }
- }
- }
- // Print the help text
- function showHelp() {
- say(HELP_TEXT)
- }
- function onChat(sender, message) {
- try {
- command = message.toLowerCase()
- if (command == 'ping') {
- say('pong')
- } else if (command == QUEUE_KEYWORD) {
- addToStack(sender)
- } else if (command == DEQUEUE_KEYWORD) {
- popFromStack()
- } else if (command == GIVEUP_KEYWORD) {
- removeFromStack(sender)
- } else if (command == REMIND_KEYWORD) {
- reportStack()
- } else if (command == HELP_KEYWORD) {
- showHelp()
- }
- } catch (e) {
- console.error('Comrade message interpretation error: ', e)
- }
- }
- // Type in the chat.
- async function say(message) {
- try {
- console.log('Sending: ' + message)
- // Need to wait for React to settle from the user doing things
- await sleep(100)
- let chat_box = document.getElementsByClassName('chat-box__chat-textarea')[0]
- console.log('Chat box: ', chat_box)
- chat_box.value = message
- let change_event = new Event('change', {
- 'view': window,
- 'bubbles': true,
- 'cancelable': true
- })
- chat_box.dispatchEvent(change_event)
- // All the keyboard event properties are read only so we have to set them up front.
- let enter_event = new KeyboardEvent('keydown', {
- bubbles: true,
- cancelable: true,
- code: "Enter",
- key: "Enter",
- keyCode: 13,
- which: 13
- })
- chat_box.dispatchEvent(enter_event)
- } catch (e) {
- console.error('Comrade message transmission error: ', e)
- }
- }
- botStartup()