Skip to content

Instantly share code, notes, and snippets.

@gummiforweb
Forked from firatkucuk/delete-slack-messages.js
Last active August 12, 2022 11:51
Show Gist options
  • Save gummiforweb/f3dbfebfcd5fd1fc4e42da1c0e2b41c8 to your computer and use it in GitHub Desktop.
Save gummiforweb/f3dbfebfcd5fd1fc4e42da1c0e2b41c8 to your computer and use it in GitHub Desktop.
Clean up your Slack messages or files from either public channel, private channel, private message or group message. Fully interactive.
/**
* User Defined Values
*/
const agreedDisclaimer = false;
const apiToken = '';
/**
* Dont need to touch anything below
*/
const readline = require('readline');
const https = require('https');
const querystring = require('querystring');
class SlackCleaner
{
constructor() {
this.slackApiUrl = 'https://slack.com/api/';
this.apiToken = apiToken;
this.userId = '';
this.channelId = '';
this.operation = '';
this.agreed = agreedDisclaimer;
this.dryRun = null;
this.readline = null;
this.perPage = 500;
this.rateDelay = 30000; // Web API Tier 3 is 50/m
}
async start() {
this.readline = readline.createInterface({
input: process.stdin,
output: process.stdout
});
await this.welcomeMessage();
await this.checkDisclaimer();
await this.checkAuthentication();
await this.selectOperation();
await this.selectChannel();
await this.selectDryRun();
await this.doOperation();
// console.log(JSON.stringify(this, null, 4));
console.log('')
console.log('\x1b[1m%s\x1b[0m', '== Ended ==');
console.log('\x1b[2m%s\x1b[0m', 'Thank you for using. If you have any question, please visit https://gist.github.com/gummi-io/f3dbfebfcd5fd1fc4e42da1c0e2b41c8');
this.readline.close()
process.exit();
}
welcomeMessage() {
console.clear()
console.log('');
console.log('\x1b[1m%s\x1b[0m', 'Slack Cleaner');
console.log('\x1b[1m%s\x1b[0m', '=============');
console.log('\x1b[2m%s\x1b[0m', 'Clean up your messages or files from either public channel, private channel, private message or group message.');
console.log('');
console.log('\x1b[31m%s\x1b[0m', '***********************************************************************************');
console.log('\x1b[31m%s\x1b[0m', '| DISCLAIMER: Use on your own risk. I do not take any responsibility of any kind. |');
console.log('\x1b[31m%s\x1b[0m', '***********************************************************************************');
}
showApiInstructions() {
console.log('');
console.log('\x1b[1m%s\x1b[0m', 'How to get your Slack Api Token??');
console.log('\x1b[1m%s\x1b[0m', '=============');
console.log('\x1b[2m%s\x1b[0m', '1. Login to "Your Apps" https://api.slack.com/apps.');
console.log('\x1b[2m%s\x1b[0m', '2. Create a new app and select the workplace you would like to connect to.');
console.log('\x1b[2m%s\x1b[0m', '3. From the sidebar, go to "OAuth & Permissions" page.');
console.log('\x1b[2m%s\x1b[0m', '4. Under "Scopes" section -> "User Token Scopes" select the following scopes:');
console.log('\x1b[2m%s\x1b[0m', ' - channels:history');
console.log('\x1b[2m%s\x1b[0m', ' - groups:history');
console.log('\x1b[2m%s\x1b[0m', ' - im:history');
console.log('\x1b[2m%s\x1b[0m', ' - mpim:history');
console.log('\x1b[2m%s\x1b[0m', ' - files:read');
console.log('\x1b[2m%s\x1b[0m', ' - chat:write');
console.log('\x1b[2m%s\x1b[0m', ' - files:write');
console.log('\x1b[2m%s\x1b[0m', '5. Under "OAuth Tokens & Redirect URLs" section, click "Install App to Workspace" and follow the instructions.');
console.log('\x1b[2m%s\x1b[0m', '6. You should now be able to get the "OAuth Access Token".');
}
showChannelInstructions() {
console.log('');
console.log('\x1b[1m%s\x1b[0m', 'How to get your channel ID??');
console.log('\x1b[1m%s\x1b[0m', '=============');
console.log('\x1b[2m%s\x1b[0m', '1. Go to your slack from the web, and navigate to the channel.');
console.log('\x1b[2m%s\x1b[0m', '2. You acn see the channel ID from the url, for example: https://app.slack.com/client/{TEAM_ID}/{CHANNEL_ID}.');
console.log('\x1b[2m%s\x1b[0m', '');
console.log('\x1b[2m%s\x1b[0m', 'Note: If you are deleting files, you may use "all" to target all channels.');
}
async checkDisclaimer() {
while (! this.agreed) {
let input = await this.getLine('Yes, I understand the risk, please continue? (yes, no) ')
if (! this.resolveEquals(input, 'yes')) process.exit()
this.agreed = true;
}
}
async checkAuthentication() {
await this.checkApiUser();
while (! this.userId) {
let input = await this.getLine('Please enter your slack api token: (type "help" for instruction) ')
if (this.resolveEquals(input, 'help')) {
this.showApiInstructions();
this.apiToken = ''
} else {
this.apiToken = input
}
await this.checkApiUser()
}
}
async checkApiUser() {
if (this.apiToken) {
let re = await this.sendRequest('auth.test')
if (re.ok) {
console.log(`Authorized. ${re.user} - ${re.user_id}`)
this.userId = re.user_id
} else {
console.log(`Unauthorized. Please enter a valid api token.`)
}
}
}
async selectOperation() {
while (! this.operation) {
let input = await this.getLine('What do you wish to delete? (messages, files) ')
if (this.resolveEquals(input, 'messages')) this.operation = 'messages'
if (this.resolveEquals(input, 'files')) this.operation = 'files'
}
}
async selectChannel() {
while (! this.channelId) {
let input = await this.getLine('Please enter your channel/conversation Id: (type "help" for instruction) ')
if (this.resolveEquals(input, 'help')) {
this.showChannelInstructions();
this.channelId = ''
} else {
this.channelId = input
}
}
}
async selectDryRun() {
while (this.dryRun === null) {
let input = await this.getLine('Do a dry run first? No data will be deleted. (yes, no) ')
this.dryRun = this.resolveEquals(input, 'yes')
}
}
async doOperation() {
switch (this.operation) {
case 'messages': return this.doMessagesOperation();
case 'files': return this.doFilesOperation();
}
}
async doMessagesOperation() {
let dryRun = this.dryRun;
let hasMore = true;
let cursor = '';
let limit = this.perPage;
let batch = 1;
console.log(`Fetching for messages from channel "${this.channelId}" by user "${this.userId}" with ${limit} per batch.`)
while (hasMore) {
process.stdout.write(`Batch ${batch}: fetching...`);
let results = await this.sendRequest('conversations.history', {
channel: this.channelId,
limit,
cursor
});
if (! results.ok) {
this.clearWithConsole('\x1b[31m%s\x1b[0m', `Batch ${batch}: error. ${results.error}.`)
process.exit();
}
hasMore = results.has_more;
cursor = results.response_metadata? results.response_metadata.next_cursor : '';
let messages = results.messages
.filter(m => m.type == 'message')
.filter(m => ! m.subtype || m.subtype == 'sh_room_created')
.filter(m => m.user == this.userId)
.map(m => m.ts);
this.clearWithConsole('\x1b[33m%s\x1b[0m', `Batch ${batch}: ${messages.length} message(s) found.`)
if (! dryRun && messages.length > 0) {
process.stdout.write(`Batch ${batch}: deleting...`);
let deleted = await this.deleteMessages(messages, batch);
this.clearWithConsole('\x1b[33m%s\x1b[0m', `Batch ${batch}: ${deleted.length} message(s) deleted.`)
deleted = []
}
batch ++
messages = []
if (hasMore) console.log('');
}
if (this.dryRun) {
let input = await this.getLine('Look good? Ready to do a real run now? (yes, no) ')
if (this.resolveEquals(input, 'yes')) {
this.dryRun = false
await this.doMessagesOperation()
}
}
}
async doFilesOperation() {
let dryRun = this.dryRun;
let hasMore = true;
let limit = this.perPage;
let batch = 1;
console.log(`Fetching for files from channel "${this.channelId}" by user "${this.userId}" with ${limit} per batch.`)
while (hasMore) {
process.stdout.write(`Batch ${batch}: fetching...`);
let args = {
count: limit,
page: batch
}
if (this.channelId != 'all') {
args.channel = this.channelId;
}
let results = await this.sendRequest('files.list', args);
if (! results.ok) {
this.clearWithConsole('\x1b[31m%s\x1b[0m', `Batch ${batch}: error. ${results.error}.`)
process.exit();
}
let {pages, page} = results.paging
hasMore = pages > 0 && pages != page;
let files = results.files
.filter(f => f.user == this.userId)
.map(f => f.id);
this.clearWithConsole('\x1b[33m%s\x1b[0m', `Batch ${batch}: ${files.length} file(s) found.`)
if (! dryRun && files.length > 0) {
process.stdout.write(`Batch ${batch}: deleting...`);
let deleted = await this.deleteFiles(files);
this.clearWithConsole('\x1b[33m%s\x1b[0m', `Batch ${batch}: ${deleted.length} file(s) deleted.`)
deleted = []
}
batch ++
files = []
if (hasMore) console.log('');
}
if (this.dryRun) {
let input = await this.getLine('Look good? Ready to do a real run now? (yes, no) ')
if (this.resolveEquals(input, 'yes')) {
this.dryRun = false
await this.doFilesOperation()
}
}
}
async deleteMessages(messages, batch) {
return await this.asyncForEach(messages, async (message, i) => {
let progress = `${i + 1}/${messages.length}`
this.clearWithLine(`Batch ${batch}: deleting... (${progress})`)
let response = await this.sendRequest('chat.delete', {
channel: this.channelId,
ts: message
});
if (! response.ok && response.error == 'ratelimited') {
this.clearWithLine(`Batch ${batch}: deleting... (${progress}) Limit reached, pausing for ${this.rateDelay/1000} seconds.`)
await this.delay(this.rateDelay);
this.clearWithLine(`Batch ${batch}: deleting... (${progress})`)
response = await this.sendRequest('chat.delete', {
channel: this.channelId,
ts: message
});
}
return response;
})
}
async deleteFiles(files, batch) {
return await this.asyncForEach(files, async (file, i) => {
let progress = `${i + 1}/${files.length}`
this.clearWithLine(`Batch ${batch}: deleting... (${progress})`)
let response = await this.sendRequest('files.delete', {
file: file
});
if (! response.ok && response.error == 'ratelimited') {
this.clearWithLine(`Batch ${batch}: deleting... (${progress}) Limit reached, pausing for ${this.rateDelay/1000} seconds.`)
await this.delay(this.rateDelay);
this.clearWithLine(`Batch ${batch}: deleting... (${progress})`)
response = await this.sendRequest('files.delete', {
file: file
});
}
return response;
})
}
async getLine(message) {
return new Promise(resolve => {
console.log('');
this.readline.question(`\x1b[1m\x1b[32m${message}\x1b[0m`, value => resolve(value));
});
}
async sendRequest(endpoint, params, method = 'get') {
const query = querystring.stringify({
token: this.apiToken,
...params
});
return new Promise(resolve => {
https.get(`${this.slackApiUrl}/${endpoint}?${query}`, (response) => {
let data = '';
response.on('data', (chunk) => data += chunk)
response.on('end', () => resolve(JSON.parse(data)))
}).on('error', (err) => console.log("Error: " + err.message));
})
}
async delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
async asyncForEach(array, callback) {
const responses = [];
for (let index = 0; index < array.length; index++) {
responses.push(await callback(array[index], index, array));
}
return responses;
}
resolveEquals(value, check) {
const lowercaseCheck = check.toLowerCase();
const firstChar = lowercaseCheck.substr(0, 1);
return value.toLowerCase() === lowercaseCheck || value.toLowerCase() === firstChar;
}
clearWithConsole() {
process.stdout.clearLine();
process.stdout.cursorTo(0);
console.log.apply(null, arguments);
}
clearWithLine(message) {
process.stdout.clearLine();
process.stdout.cursorTo(0);
process.stdout.write(message);
}
}
(new SlackCleaner).start()
@gummiforweb
Copy link
Author

It's the time of the year where you want to clean up slack messages/files. The script is now updated to latest Slack API (Feb 2020).
The script is now also fully interactive with helper messages. No need to edit file no more.

Still, if want to bypass the disclaimer and always use the same api token, you can edit the first 2 variables in the file.

Enjoy 😘

@gummiforweb
Copy link
Author

gummiforweb commented Feb 15, 2020

Instruction,

  1. download the file
  2. open terminal, go to where the file is, and type node slack-cleaner.js

Image+2020-02-15+at+1 48 57+AM

@andremoreira1
Copy link

andremoreira1 commented Feb 27, 2020

I'm getting the message: Batch 1: error. missing_scope.

I just checked and all changes in the scope that the code asks for are already authorized.
I'm trying to delete private messages. Some help?

Captura de Tela 2020-02-27 às 13 29 06

@andremoreira1
Copy link

Got it! The token I was using was wrong!

@gummiforweb
Copy link
Author

😅 after adding the missing scope, also need to regenerate a new api key too, for anyone who's wondering.

@alexauvray
Copy link

alexauvray commented Mar 11, 2020

Nice but not working with dm from bots

Batch 1: 0 message(s) found.

Batch 2: 0 message(s) found.

@uthens
Copy link

uthens commented Mar 22, 2020

i tested today.. why still show like this i think it's not work ?

image

@gummiforweb
Copy link
Author

@uthens

This is actually correct. The script will only delete the messages created by you.

The get message api can only fetch all the messages in specified channel. That's why when you ran the script and the result still show 6 batches. Those are messages not created by you.

So according to your screenshot, all messages sent by you should all be deleted. Can you verify your messages are all delete by logging into slack?

@jaison10
Copy link

Does it delete the messages sent by the other person in private message or only my messages?

And can I delete only few messages? ( like recent 200 messages)

@hautph
Copy link

hautph commented May 20, 2020

Does it delete the messages sent by the other person in private message or only my messages?

And can I delete only few messages? ( like recent 200 messages)

This script delete only your message, if you want delete all messages, using this scripts https://gist.github.com/firatkucuk/ee898bc919021da621689f5e47e7abac

@misterpyrrhuloxia
Copy link

@gummi-io, I don't really know js. Would it be possible for you to modify your script to also ask for the path to a text file that contains a list of channel IDs instead of only being able to run the script against one channel ID at a time?

@BinaryHammer
Copy link

I am impressed. The interactivity with the help explanations made it so simple. Thank you.

@kadiiskiFFW
Copy link

Hey guys, i'm the owner of the channel and I added an APP and added all scopes needed and genereted a toke, but it always says "Unauthorized. Please enter a valid api token."

Which exactly token am I supposed to use or maybe I'm doing something wrong?

@david-cazarin
Copy link

Hey guys, i'm the owner of the channel and I added an APP and added all scopes needed and genereted a toke, but it always says "Unauthorized. Please enter a valid api token."

Which exactly token am I supposed to use or maybe I'm doing something wrong?

I too cannot get the the token show authorized in this script, I was able to get delete-slack-message.js to work correctly with the token but I was hoping to use both to clean up my channels, because some I just need to delete specific users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment