-
-
Save gummiforweb/f3dbfebfcd5fd1fc4e42da1c0e2b41c8 to your computer and use it in GitHub Desktop.
/** | |
* 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() |
Added ability to assign multiple channels
/**
* Available Types:
* channels - Public Channel
* groups - Private Channel
* conversations - Direct Message
* conversations - Group Message
* files - Files
*/
var channels = [
{channel: 'xxxxxxxxxx', type: 'conversations'},
{channel: 'xxxxxxxxxx', type: 'groups'},
{channel: 'xxxxxxxxxx', type: 'channels'},
{channel: '', type: 'files'},
];
... add more or remove as you want
Does not work anymore, even for public channel, can you check again?
Not working for public channels :(
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 😘
Got it! The token I was using was wrong!
😅 after adding the missing scope, also need to regenerate a new api key too, for anyone who's wondering.
Nice but not working with dm from bots
Batch 1: 0 message(s) found.
Batch 2: 0 message(s) found.
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?
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)
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
@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?
I am impressed. The interactivity with the help explanations made it so simple. Thank you.
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?
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.
Originally from https://medium.com/@jjerryhan/cleaning-all-messages-on-slack-channel-c46d71615c9a
Run
node delete-slack-messages.js
Automatically wait for 30 seconds if it timed out, instead of keep retrying.
Automatically run the next batch if there's more messages.
Skip group purpose and group join messages. (they cannot seem to be deleted)
dryRun
to true to see the stats without actually deleting any messages: like:userID
to performs faster deletion by only sending the delete request if it's the user's messages. Leave empty to delete all messages (if permission granted). You can find your use id here https://api.slack.com/methods/users.info/testSetprivate
to true, if you are deleting messages in a private channel.Setdirect
to true, if you are deleting messages in a direct message channel. (user to user)