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 = (
);
+
+ /**
+ * 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() }>
+
+
);
+
+ // 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;
+ }
+}