Paginate and Print Channel Messages Using the Discord API

This post shows a simple example of reading messages from a Discord channel by paginating the Discord REST API.

Note that a bot is not required for this to work.
We use the auth token as used in the browser where you are logged into the Discord server.

Note also that the messages are printed newest-first.

In the script, we read the auth token from a dotenv file.
The .env file is below:

.env

# Discord API auth header.
# Copy from the call sent by the browser.

DISCORD_API_AUTH_HEADER="12345..."

CHANNEL_ID="92..."

Make sure to copy the entire actual auth header and paste it into the dotenv file.

Also copy the Channel ID from the Discord URL from the browser. It should look like this:

discord.com/channels/{ServerID}/{ChannelID}

The Channel ID is the second ID appearing in the URL.
We read the channel ID from the dotenv file as well.

Discord uses Snowflake for data storage and pagination is done using Snowflake IDs.

The Snowflake ID acts like a cursor we can use to request data with: for each page we take the minimum Snowflake ID and get items with a lower ID.

The script takes the earliest date to paginate back to as input.

We convert that date into a Snowflake ID.

While the Snowflake IDs of the current page do not reach that far back, we keep looping.

Full script:

import os
import requests
import urllib

from datetime import datetime, timezone
from dotenv import load_dotenv
from time import time

# Discord API earliest timestamp.
# 2015-01-01T00:00:00Z in ms.
DISCORD_EPOCH = 1420070400000

# Helper functions for Discord API.
# Get a full timestamp Snowflake ID, assuming midnight for the time.
def snowflakeIdForDate(dateStr):
  dt = datetime
       .strptime(f"{dateStr} 00:00", "%Y-%m-%d %H:%M")
       .replace(tzinfo=timezone.utc)
  return (int(dt.timestamp() * 1000) - DISCORD_EPOCH) << 22

# Get Snowflake ID for current timestamp.
def snowflakeIdForNow():
  return (int(time() * 1000) - DISCORD_EPOCH) << 22

# Discord API setup.
load_dotenv(".env")

DISCORD_API_AUTH_HEADER = os.getenv("DISCORD_API_AUTH_HEADER", "")
CHANNEL_ID = os.getenv("CHANNEL_ID", "")

LIMIT = "20" # Messages per page.
MESSAGES_BASE_URL = "https://discord.com/api/v9/channels/" + 
                    CHANNEL_ID + "/messages?limit=" + LIMIT

reqHeaders = {
  "Content-Type": "application/json",
  "User-Agent": "Mozilla/5.0 (Linux; Android 7.0; ...)",
  "Authorization": DISCORD_API_AUTH_HEADER
}

# Main script. Read from now back to given input date.

nowSnowflakeId = snowflakeIdForNow()

earliestDateInput = input("Earliest date (YYYY-MM-DD): ")

earliestSnowflakeId = snowflakeIdForDate(earliestDateInput)

# Paginate backwards from the current date and time.
currentSnowFlakeId = nowSnowflakeId

while currentSnowFlakeId > earliestSnowflakeId:

  currentUrl = MESSAGES_BASE_URL + 
               "&before=" + 
               str(currentSnowFlakeId)

  try:
    response = requests.get(currentUrl, headers=reqHeaders)

  except urllib.HTTPError:
    print("Got HTTP error for URL")

  resultData = response.json()

  for item in resultData:
    content = item["content"] # Message text.
    print(content)
    timestamp = item["timestamp"] # Created at.
    print(timestamp)

  # For next iteration. Update SnowflakeID cursor.
  currentSnowFlakeId = min(
    int(msg["id"]) for msg in resultData
  )