Compare commits

...

13 Commits

3 changed files with 219 additions and 93 deletions

View File

@ -1,3 +1,5 @@
<p align="center"> <img src="./icon.png" alt="icon" width="500" style="vertical-align: middle; align: middle"> </p>
# DISCLAIMER # DISCLAIMER
**The author of this script takes ABSOLUTELY NO RESPONSIBILITY for your own actions or usage of the script, and if you do choose to use this script, you do so ENTIRELY AT YOUR OWN RISK.** **The author of this script takes ABSOLUTELY NO RESPONSIBILITY for your own actions or usage of the script, and if you do choose to use this script, you do so ENTIRELY AT YOUR OWN RISK.**
@ -6,7 +8,7 @@ This script uses Discord's undocumented User API, which has no official public d
**Self-botting is explicitly forbidden by the Discord Trust & Safety Team and can result in an account termination if found**. Therefore, it's not unlikely that **using this script may incur a ban on your account.** Discord's explicit condemnation of self-botting can be found [here](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-). **Self-botting is explicitly forbidden by the Discord Trust & Safety Team and can result in an account termination if found**. Therefore, it's not unlikely that **using this script may incur a ban on your account.** Discord's explicit condemnation of self-botting can be found [here](https://support.discord.com/hc/en-us/articles/115002192352-Automated-user-accounts-self-bots-).
# About # About
`purge_discord.py` is a Discord self-botting Python script that allows the user to programmatically mass-delete all of the messages that they sent in a given Discord channel, including servers, DMs, & group chats via the Discord User API. It is primarily designed to be ran from a command-line, with the URL or ID of a channel being passed to the script as arguments. `purge_discord.py` is a Discord self-botting Python script that allows the user to programmatically mass-delete all of the messages that they sent in a given Discord server, including DMs & group chats, via the Discord User API. It is primarily designed to be ran from a command-line, with the URL of a channel being passed to the script as arguments.
The purpose of the script is to enhance the user's control over their private information on Discord. Discord is not a social media platform with strong privacy practices, and I firmly believe that an individual should have the right to remove their data from any given platform quickly & easily at will. At present, there is no way to delete all of your messages from Discord. Not even deleting your account will remove your messages from the platform, and they will still be visible permanently. Deleting your account is in this sense worse for your privacy, as you lose control over your messages; Once you delete your account, you will permanently lose the ability to delete any of your messages, meaning that any information you revealed in those messages is now permanently public information. The purpose of the script is to enhance the user's control over their private information on Discord. Discord is not a social media platform with strong privacy practices, and I firmly believe that an individual should have the right to remove their data from any given platform quickly & easily at will. At present, there is no way to delete all of your messages from Discord. Not even deleting your account will remove your messages from the platform, and they will still be visible permanently. Deleting your account is in this sense worse for your privacy, as you lose control over your messages; Once you delete your account, you will permanently lose the ability to delete any of your messages, meaning that any information you revealed in those messages is now permanently public information.
@ -40,17 +42,17 @@ To get usage information, you can run the script with no arguments. The usage in
``` ```
The output should be as follows: The output should be as follows:
``` ```
Usage: ./purge_discord.py [OPTION]... [ARGUMENT]... usage: purge_discord.py [-h] -u CHANNEL_URL
Delete all the user's messages in the given Discord channel.
The channel may be specified using one of the following options: Script to delete all a user's messages in a given Discord server
-i, --channel-id delete messages in the channel corresponding to the supplied ID
-u, --channel-url delete messages in the channel corresponding to the supplied URL options:
-h, --help show this help message and exit
-u, --channel-url CHANNEL_URL
URL of a channel in the server
``` ```
The script can be ran either by supplying the channel ID or the channel URL. To get the ID of a channel, right-click on it in Discord and click the "Copy channel ID" option in the context menu. The ID can then be passed to the script as follows:
```bash The URL is the one that is shown in the address bar of your browser when you navigate to that channel in the web version of the Discord application. The URL can be passed to the script as follows:
./purge_discord.py -i 1234567890123456789
```
Alternatively, the script can be ran by supplying the channel URL. This is the URL that is shown in the address bar of your browser when you navigate to that channel in the web version of the Discord application. The URL can be passed to the script as follows:
```bash ```bash
./purge_discord.py -u https://discord.com/channels/@me/1234567890123456789 ./purge_discord.py -u https://discord.com/channels/@me/1234567890123456789
``` ```

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -1,100 +1,224 @@
#!/bin/python3 #!/bin/python3
import asyncio, aiohttp, os, sys, json import asyncio, aiohttp, os, sys, json
from asyncio.base_events import time
from dotenv import load_dotenv from dotenv import load_dotenv
import argparse
BASE_URL = "https://discord.com/api/v9/"
# method to print out the usage instructions if the program was called incorrectly def read_token():
def usage(): """
sys.exit("Usage: ./purge_discord.py [OPTION]... [ARGUMENT]...\n" Reads the user's Discord token from the environment file.
"Delete all the user's messages in the given Discord channel.\n" File should be in the format: `DISCORD_TOKEN=<insert_discord_token_here>`.
"The channel may be specified using one of the following options:\n"
"\t-i, --channel-id delete messages in the channel corresponding to the supplied ID\n"
"\t-u, --channel-url delete messages in the channel corresponding to the supplied URL")
Returns:
str: Discord token specified in environment file.
"""
async def main():
# parsing command-line arguments
if len(sys.argv) == 3:
if sys.argv[1] == "-i" or sys.argv[1] == "--channel-id":
channel = sys.argv[2]
elif sys.argv[1] == "-u" or sys.argv[1] == "--channel-url":
channel = sys.argv[2].split("/")[-1] # parsing the channel ID from the URL by splitting it on `/` and taking the last segment
else:
usage()
else:
usage()
# reading Discord token from the .env file. should be in the format `DISCORD_TOKEN=<insert_discord_token_here>`
load_dotenv() load_dotenv()
token = os.getenv("DISCORD_TOKEN") token = os.getenv("DISCORD_TOKEN")
if not token: if not token:
sys.exit("DISCORD_TOKEN environment variable is not set") raise Exception("DISCORD_TOKEN environment variable is not set")
host = "https://discord.com/api/v10" return token
headers = {"authorization": token}
offset = 0 # messages are received from the API in batches of 25, so need to keep track of the offset when requesting to make sure no old messages are received
to_delete = [] # list of messages to be deleted
# starting asynchronous context manager to keep all HTTP requests in the one session def split_channel_url(channel_url):
async with aiohttp.ClientSession() as session: """
# getting the user ID pertaining to the Discord token that the script is using Split the channel URL into the guild ID and channel ID.
async with session.request("GET", host + "/users/@me", headers=headers) as response: Note that 'guild' is just Discord's internal name for 'server'.
try:
user = (await response.json())["id"] Args:
except KeyError: channel_url (str): the channel URL.
sys.exit("User ID not found in JSON response. Actual JSON response: " + json.dumps(await response.json(), indent=4))
Returns:
string: the guild ID.
string: the channel ID.
"""
split_string = channel_url.split("/")
guild_id = split_string[-2]
channel_id = split_string[-1]
return guild_id, channel_id
def get_guild_url(guild_id, channel_id):
"""
Get the guild URL based off the guild ID and channel ID.
Args:
guild_id (str): the guild ID.
channel_id (str): the channel ID.
Returns:
str: the guild URL.
"""
if (guild_id == "@me"):
guild_url = "https://discord.com/api/v9/channels/" + channel_id
else:
guild_url = "https://discord.com/api/v9/guilds/" + guild_id
return guild_url
def get_headers(token):
"""
Returns a dictionary object containing the headers with which to make API requests, including the authorisation token.
Args:
token (string): the user's Discord token.
Returns:
dict: A dictionary containing the headers.
"""
headers = {
"authorization": token,
}
return headers
async def get_user_id(session, headers):
"""
Fetches the Discord user ID of the user's whose Discord token has been supplied.
Args:
session (aiohttp.ClientSession): the session to make the request with.
headers (dictionary): the headers with which to make the GET request.
Returns:
string: the (numerical) user ID of the Discord user.
"""
async with session.request("GET", BASE_URL + "users/@me", headers=headers) as response:
user_id = (await response.json())["id"]
return user_id
async def get_all_messages(session, headers, guild_url, user_id):
"""
Get a list of all of the user's messages in the guild.
Args:
session (aiohttp.ClientSession): the session to make the requests with.
headers (dictionary): the headers with which to make the requests.
guild_url (str): the URL of the guild from which to fetch all the messages.
user_id (str): the ID of the user's whose messages are being fetched.
Returns:
list: a list of all the user's messages in the guild.
"""
messages = []
# The Discord API returns messages in batches of 25, so an offset is used to define from which starting point to return the previous 25 messages.
offset = 0
# looping to fill up list of messages by requesting batches of messages from the API and adding them to the list
while True: while True:
# searching for the next 25 messages from the user in question from the Discord API (Discord returns search results in batches of 25) request_url = guild_url + "/messages/search?author_id=" + user_id + "&offset=" + str(offset)
async with session.request("GET", host + "/channels/" + str(channel) + "/messages/search?author_id=" + str(user) + "&offset=" + str(offset), headers=headers) as response:
# if the channel is not yet indexed, looping until Discord indexes it async with session.request("GET", request_url, headers=headers) as response:
# If the channel is not yet indexed, may be asked to try again later, so loop until messages are returned.
while True: while True:
# if the search returned an array (empty or otherwise) of messages, breaking
# Break if the request returned a list (empty or otherwise) of messages.
if "messages" in (await response.json()): if "messages" in (await response.json()):
batch = (await response.json())["messages"] batch = (await response.json())["messages"]
break break
# assuming that a "Try again later" message was received if not messages, so trying to wait the requested interval.
# if some other message was received and no interval was in the response, exiting with an error message
else: else:
try: try:
print("Channel is not yet indexed. Waiting for " + str((await response.json())["retry_after"]) + "s as requested by the Discord API") # Assuming that a "Try again later" message was received if no message list was returned
await asyncio.sleep((await response.json())["retry_after"]) time_to_wait = (await response.json())["retry_after"]
except KeyError:
sys.exit("Unexpected JSON response received from Discord after trying to search for messages. Actual JSON response: " + json.dumps(await response.json(), indent=4))
# if the batch is not empty, adding the messages in batch to the list of messages to be deleted print("Channel is not yet indexed. Waiting for " + str(time_to_wait) + "s as requested by the Discord API")
await asyncio.sleep(time_to_wait)
except KeyError:
# If some other message was received and no interval was specified in the response, throw an exception.
raise Exception("Unexpected JSON response received from Discord after trying to search for messages. Actual JSON response: " + json.dumps(await response.json(), indent=4))
# If the batch is not empty, add the messages in batch to the list of messages.
if batch: if batch:
to_delete += batch messages += batch
offset += 25 offset += 25
# if the batch is empty, there are no more messages to be added to the to_delete list so breaking out of the search loop # if the batch is empty, there are no more messages to be added to the to_delete list so breaking out of the search loop
else: break else:
break
deleted_messages = 0 # count of the number of messages that have been deleted return messages
print("Deleting " + str(len(to_delete)) + " message(s)")
# looping through messages in to_delete list and sending the request to delete them one by one
for message in to_delete:
# looping infinitely until message is successfully deleted async def delete_messages(session, headers, messages):
"""
Iterates over a list of messages and deletes them.
Args:
session (aiohttp.ClientSession): the session to make the requests with.
headers (dictionary): the headers with which to make the requests.
messages (list): a list of Discord messages.
Returns:
None
"""
print("Deleting " + str(len(messages)) + " message(s)")
num_deleted = 0 # The number of messages that have been deleted.
for message in messages:
message_url = BASE_URL + "channels/" + message[0]["channel_id"] + "/messages/" + message[0]["id"]
# Do nothing if the message is a system message (has type = 1).
if message[0]["type"] == 1:
break
# Discord may reject a DELETE request and say to try again, so loop until the message is successfully deleted (or until a break due to a forbidden message).
while True: while True:
async with session.request("DELETE", host + "/channels/" + channel + "/messages/" + message[0]["id"], headers=headers) as response:
# if successful status returned, printing success message and breaking out of loop async with session.request("DELETE", message_url, headers=headers) as response:
# If successful status code returned, printing success message and breaking out of loop
if 200 <= response.status <= 299: if 200 <= response.status <= 299:
deleted_messages += 1 num_deleted += 1
print("Successfully deleted message " + str(deleted_messages) + " of " + str(len(to_delete))) print("Successfully deleted message " + str(num_deleted) + " of " + str(len(messages)))
break; break;
# else if "Too many requests" status returned, waiting for the amount of time specified # Else if "forbidden" status returned, giving up on that message and continuing.
elif response.status == 429: elif response.status == 403:
print("Rate limited by Discord. Waiting for " + str((await response.json())["retry_after"]) + "s") print("Failed to delete message: Error 403 " + (await response.json())["message"])
await asyncio.sleep((await response.json())["retry_after"]) break
# otherwise, printing out json response and aborting # Else if "Too many requests" status returned, waiting for the amount of time specified.
elif response.status == 429:
retry_after = (await response.json())["retry_after"]
print("Rate limited by Discord. Waiting for " + str(retry_after) + "s")
await asyncio.sleep(retry_after)
# Otherwise, printing out JSON response and throwing an exception
else: else:
sys.exit("Unexpected HTTP status code received. Actual response: " + json.dumps(response.json(), indent=4)) print(response.status)
raise Exception("Unexpected HTTP status code received. Actual response: " + json.dumps(await response.json(), indent=4))
async def main():
parser = argparse.ArgumentParser(description="Script to delete all a user's messages in a given Discord server")
parser.add_argument("-u", "--channel-url", type=str, help="URL of a channel in the server", required=True)
args=parser.parse_args()
token = read_token()
headers = get_headers(token)
guild_id, channel_id = split_channel_url(args.channel_url)
guild_url = get_guild_url(guild_id, channel_id)
# starting asynchronous context manager to keep all HTTP requests in the one session
async with aiohttp.ClientSession() as session:
user_id = await get_user_id(session, headers)
to_delete = await (get_all_messages(session, headers, guild_url, user_id))
await delete_messages(session, headers, to_delete)
if __name__ == "__main__": if __name__ == "__main__":