diff --git a/src/mass-ban-tool/index.jsx b/src/mass-ban-tool/index.jsx new file mode 100644 index 0000000..e65dd70 --- /dev/null +++ b/src/mass-ban-tool/index.jsx @@ -0,0 +1,299 @@ +'use strict'; + +import CSS_URL from './style.scss'; + +const { openFile, createElement } = FrankerFaceZ.utilities.dom, + { sleep } = FrankerFaceZ.utilities.object; + +class MassBanTool extends Addon { + constructor( ...args ) { + super( ...args ); + + this.massBanToolCSS = document.createElement( 'link' ); + + this.massBanToolCSS.rel = 'stylesheet'; + this.massBanToolCSS.id = 'ffz-mass-ban-tool-css'; + this.massBanToolCSS.href = CSS_URL; + + this.toolIsRunning = false; + + this.inject( 'site.chat' ); + } + + buildMassBanToolModal() { + const modalCloseBtn = ( ), + fileUploadBtn = ( ); + + this.massBanToolModal = (
+
+

Mass Ban Tool

+ +
+
+ +
+
+
+
+
+
+
+ + + +
+ +
+
+

The list of users you wish to action. List one username per line.

+
+
+
+
+ +
+
+
+ +
+ +
+
+

Optionally you can upload a .txt file here to populate the Users List. Make sure your list consists of one username per line.

+
+
+
+
+ +
+
+
+ + + +
+ +
+
+

Select whether to ban or unban the listed users.

+
+
+
+
+ +
+
+
+ + + +
+
+ +
+
+

An optional reason for the ban which will be applied to every ban. Not applicable when unbanning.

+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+
); + + /** + * Disable "Ban Reason" field when action is set to "Unban" + */ + this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-action' )[0].addEventListener( 'change', ( event ) => { + this.toggleBanReasonField( event ); + } ); + + /** + * Add confirmation and run events to "Run Tool" button + */ + this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-run-btn' )[0].getElementsByClassName( 'tw-button' )[0].addEventListener( 'click', () => { + if ( this.toolIsRunning ) { + window.alert( 'The tool is currently running. Please wait until it is finished to run it again.' ); + } else if ( window.confirm( 'Are you absolutely sure you want to run this tool? The process cannot be stopped once started.' ) ) { + this.runTool( + this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-users-list' )[0].value, + this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-action' )[0].value, + this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-ban-reason' )[0].value + ); + + this.removeMassBanToolModal(); + } + } ); + + this.massBanToolModal.getElementsByTagName( 'header' )[0].append( modalCloseBtn ); + this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-upload-field' )[0].append( fileUploadBtn ); + } + + insertmassBanToolModal() { + document.body.append( this.massBanToolModal ); + } + + removeMassBanToolModal() { + this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-form' )[0].reset(); + + this.toggleBanReasonField( { target: { value: 'ban' } } ); + + if ( document.body.contains( this.massBanToolModal ) ) { + document.body.removeChild( this.massBanToolModal ); + } + } + + checkForModView() { + this.modViewContainer = document.querySelector( '.modview-dock > div:last-child' ); + + if ( this.modViewContainer ) { + return true; + } + + return false; + } + + insertCSS() { + document.head.append( this.massBanToolCSS ); + } + + removeCSS() { + if ( document.head.contains( this.massBanToolCSS ) ) { + document.head.removeChild( this.massBanToolCSS ); + } + } + + insertModViewButton() { + this.modViewBtn =(
this.insertmassBanToolModal() }> +
+ + +
Mass Ban Tool
+
+
); + + // Insert mod view button + this.modViewContainer.insertBefore( this.modViewBtn, document.getElementsByClassName( 'ffz-mod-view-button' )[0].nextElementSibling ); + } + + removeModViewButton() { + if ( this.modViewContainer.contains( this.modViewBtn ) ) { + this.modViewContainer.removeChild( this.modViewBtn ); + } + } + + async openFileSelector() { + const usersListFile = await openFile( 'text/plain', false ), + usersListTextarea = document.getElementById( 'ffz-mass-ban-tool-users-list' ); + + if ( usersListFile.type === 'text/plain' ) { + usersListFile.text() + .then( ( contents ) => { + const usersListArray = contents.replace( /\r\n/gm, '\n' ).match( /^.*$/gm ); + + for ( const user of usersListArray ) { + if ( usersListTextarea.value[0] !== usersListTextarea.value[ usersListTextarea.value.length - 1 ] && usersListTextarea.value[ usersListTextarea.value.length - 1 ] !== '\n' ) { + usersListTextarea.value += '\n'; + } + + usersListTextarea.value += user; + } + + usersListTextarea.value += '\n'; + } ); + } + } + + toggleBanReasonField( event ) { + const massBanToolBanReason = this.massBanToolModal.getElementsByClassName( 'ffz-mass-ban-tool-ban-reason' )[0]; + + if ( event.target.value === 'unban' ) { + massBanToolBanReason.disabled = true; + } else { + massBanToolBanReason.disabled = false; + } + } + + async runTool( users, action, reason ) { + const usersArray = users.trim().match( /^.*$/gm ); + + this.toolIsRunning = true; + + for ( const user of usersArray ) { + await this.actionUser( user, action, reason ); + } + + this.toolIsRunning = false; + } + + async actionUser( user, action, reason ) { + let command = '/' + action + ' ' + user; + + if ( action === 'ban' && reason.trim() !== '' ) { + command += ' ' + reason; + } + + this.chat.sendMessage( this.channelName, command ); + + /** + * Twitch chat limit for mods/broadcasters is 100 messages every 30 seconds + * so the following delay is set slightly above the fastest possible time increment in order + * to avoid hitting that limit + */ + await sleep( 350 ); + } + + onDisable() { + this.removeCSS(); + + this.removeMassBanToolModal(); + + this.removeModViewButton(); + + this.log.info( 'Mass Ban Tool add-on successfully disabled.' ); + } + + onEnable() { + if ( this.checkForModView() ) { + this.channelName = this.chat.router.match[1]; + + this.insertCSS(); + + this.insertModViewButton(); + + this.buildMassBanToolModal(); + } + + this.log.info( 'Mass Ban Tool add-on successfully enabled.' ); + } +} + +MassBanTool.register(); diff --git a/src/mass-ban-tool/logo.png b/src/mass-ban-tool/logo.png new file mode 100644 index 0000000..65d7715 Binary files /dev/null and b/src/mass-ban-tool/logo.png differ diff --git a/src/mass-ban-tool/manifest.json b/src/mass-ban-tool/manifest.json new file mode 100644 index 0000000..3fc47f4 --- /dev/null +++ b/src/mass-ban-tool/manifest.json @@ -0,0 +1,12 @@ +{ + "enabled": true, + "requires": [], + "version": "1.0.0", + "short_name": "MassBanTool", + "name": "Mass Ban Tool", + "author": "ArgoWizbang", + "description": "A tool for mass banning/unbanning users via mod view in channels that you moderate or own.\n\nThe button to open the tool can be found in the mod dock on the bottom left section of the mod view page.", + "website": "https://argowizbang.com/ffz-add-on/mass-ban-tool/", + "created": "2025-01-31T05:34:58.994Z", + "updated": "2025-02-03T22:52:21.851Z" +} \ No newline at end of file diff --git a/src/mass-ban-tool/style.scss b/src/mass-ban-tool/style.scss new file mode 100644 index 0000000..9483550 --- /dev/null +++ b/src/mass-ban-tool/style.scss @@ -0,0 +1,24 @@ +#ffz-mass-ban-tool-modal { + --width: min(40vw, 128rem); + max-width: 40vw; + + & > header { + cursor: initial; + } + + & > section { + padding: 0.9rem 1rem 0.9rem 2rem; + } + + & .ffz--widget button { + margin: 0.5rem 0 !important + } + + & .ffz--widget:has(.ffz-mass-ban-tool-ban-reason:disabled) .tw-flex:hover { + cursor: not-allowed; + } + + & .ffz-mass-ban-tool-run-btn { + text-align: right; + } +}