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()
@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