DEV Community

Cover image for Running a Discord Bot on Raspberry Pi
beretests
beretests

Posted on

Running a Discord Bot on Raspberry Pi

Cover Photo by Daniel Tafjord on Unsplash

I recently completed a software engineering bootcamp, started working on LeetCode easy questions and felt it would help keep me accountable if I had a daily reminder to solve questions. I decided to implement this using a discord bot running on a 24 hour schedule (on my trusty raspberry pi, of course) which would do the following:

  • go to a predefined databank of easy leetcode questions
  • grab a question which has not been posted to the discord channel
  • post the leetcode question as a thread in the discord channel (so you can easily add your solution)
  • question is marked as posted to avoid posting it to the channel again

Image description

I realize it may be easier to just go to LeetCode and solve a question a day but I got to learn a lot about Python and Discord with help from ChatGPT on this mini-project. This is also my first attempt at sketchnoting so please bear with lol

Image description

Setup

1. Use python virtual environment
2. Install dependencies
3. Set up Leetcode easy questions database
4. Set up environment variables
5. Create Discord app
6. Run the Bot!

1. Use python virtual environment

I recommend the use of a python virtual environment because when I initially tested this on Ubuntu 24.04, I encountered the error below

Image description

Setting it up is relatively easy, just run the following commands and voila, you're in a python virtual environment!

python3 -m venv ~/py_envs
ls ~/py_envs  # to confirm the environment was created
source ~/py_envs/bin/activate
Enter fullscreen mode Exit fullscreen mode

2. Install dependencies

The following dependencies are required:

  • AWS CLI

Install AWS CLI by running the following:

curl -O 'https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip'
unzip awscli-exe-linux-aarch64.zip 
sudo ./aws/install
aws --version
Enter fullscreen mode Exit fullscreen mode

Then run aws configure to add the required credentials. See Configure the AWS CLI doc.

  • pip dependencies

The following pip dependencies can be installed with a requirements file by running pip install -r requirements.txt.

# requirements.txt

discord.py
# must install this version of numpy to prevent conflict with
# pandas, both of which are required by leetscrape
numpy==1.26.4   
leetscrape
python-dotenv
Enter fullscreen mode Exit fullscreen mode

3. Set up leetcode easy questions database

Leetscrape was vital for this step. To learn more about it, see the Leetscrape docs.
I only want to work on leetcode easy questions (to me, they're even quite difficult) so I did the following:

  • grab the list of all questions from leetcode using leetscrape and save list to csv
from leetscrape import GetQuestionsList

ls = GetQuestionsList()
ls.scrape() # Scrape the list of questions
ls.questions.head() # Get the list of questions
ls.to_csv(directory="path/to/csv/file")
Enter fullscreen mode Exit fullscreen mode
  • create an Amazon DynamoDB table and populate it with list of easy questions filtered from csv saved in previous step.
import csv
import boto3
from botocore.exceptions import BotoCoreError, ClientError

# Initialize the DynamoDB client
dynamodb = boto3.resource('dynamodb')

def filter_and_format_csv_for_dynamodb(input_csv):
    result = []

    with open(input_csv, mode='r') as file:
        csv_reader = csv.DictReader(file)

        for row in csv_reader:
            # Filter based on difficulty and paidOnly fields
            if row['difficulty'] == 'Easy' and row['paidOnly'] == 'False':
                item = {
                    'QID': {'N': str(row['QID'])},  
                    'titleSlug': {'S': row['titleSlug']}, 
                    'topicTags': {'S': row['topicTags']},  
                    'categorySlug': {'S': row['categorySlug']},  
                    'posted': {'BOOL': False}  
                }
                result.append(item)

    return result

def upload_to_dynamodb(items, table_name):
    table = dynamodb.Table(table_name)

    try:
        with table.batch_writer() as batch:
            for item in items:
                batch.put_item(Item={
                    'QID': int(item['QID']['N']),  
                    'titleSlug': item['titleSlug']['S'],
                    'topicTags': item['topicTags']['S'],
                    'categorySlug': item['categorySlug']['S'],
                    'posted': item['posted']['BOOL']
                })
        print(f"Data uploaded successfully to {table_name}")

    except (BotoCoreError, ClientError) as error:
        print(f"Error uploading data to DynamoDB: {error}")

def create_table():
    try:
        table = dynamodb.create_table(
            TableName='leetcode-easy-qs',
            KeySchema=[
                {
                    'AttributeName': 'QID',
                    'KeyType': 'HASH'  # Partition key
                }
            ],
            AttributeDefinitions=[
                {
                    'AttributeName': 'QID',
                    'AttributeType': 'N'  # Number type
                }
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 5,
                'WriteCapacityUnits': 5
            }
        )

        # Wait until the table exists
        table.meta.client.get_waiter('table_exists').wait(TableName='leetcode-easy-qs')
        print(f"Table {table.table_name} created successfully!")

    except Exception as e:
        print(f"Error creating table: {e}")

# Call function to create the table
create_table()

# Example usage
input_csv = 'getql.pyquestions.csv'  # Your input CSV file
table_name = 'leetcode-easy-qs'      # DynamoDB table name

# Step 1: Filter and format the CSV data
questions = filter_and_format_csv_for_dynamodb(input_csv)

# Step 2: Upload data to DynamoDB
upload_to_dynamodb(questions, table_name)
Enter fullscreen mode Exit fullscreen mode

4. Set up environment variables

Create a .env file to store environment variables

DISCORD_BOT_TOKEN=*****
Enter fullscreen mode Exit fullscreen mode

5. Create Discord app

Follow the instructions in the Discord Developer docs to create a Discord app and bot with adequate permissions. Be sure to authorize the bot with at least the following OAuth permissions:

  • Send Messages
  • Create Public Threads
  • Send Messages in Threads

6. Run the Bot!

Below is the code for the bot which can be run with the python3 discord-leetcode-qs.py command.

import os
import discord
import boto3
from leetscrape import GetQuestion
from discord.ext import tasks
from dotenv import load_dotenv
import re
load_dotenv()

# Discord bot token
TOKEN = os.getenv('DISCORD_TOKEN')

# Set the intents for the bot
intents = discord.Intents.default()
intents.message_content = True # Ensure the bot can read messages

# Initialize the bot
bot = discord.Client(intents=intents)
# DynamoDB setup
dynamodb = boto3.client('dynamodb')

TABLE_NAME = 'leetcode-easy-qs'
CHANNEL_ID = 1211111111111111111  # Replace with the actual channel ID

# Function to get the first unposted item from DynamoDB
def get_unposted_item():
    response = dynamodb.scan(
        TableName=TABLE_NAME,
        FilterExpression='posted = :val',
        ExpressionAttributeValues={':val': {'BOOL': False}},
    )
    items = response.get('Items', [])
    if items:
        return items[0]
    return None

# Function to mark the item as posted in DynamoDB
def mark_as_posted(qid):
    dynamodb.update_item(
        TableName=TABLE_NAME,
        Key={'QID': {'N': str(qid)}},
        UpdateExpression='SET posted = :val',
        ExpressionAttributeValues={':val': {'BOOL': True}}
    )

MAX_MESSAGE_LENGTH = 2000
AUTO_ARCHIVE_DURATION = 2880

# Function to split a question into words by spaces or newlines
def split_question(question, max_length):
    parts = []
    while len(question) > max_length:
        split_at = question.rfind(' ', 0, max_length)
        if split_at == -1:
            split_at = question.rfind('\n', 0, max_length)
        if split_at == -1:
            split_at = max_length

        parts.append(question[:split_at].strip())
        # Continue with the remaining text
        question = question[split_at:].strip()

    if question:
        parts.append(question)

    return parts

def clean_question(question):
    first_line, _, remaining_question = message.partition('\n')
    return re.sub(r'\n{3,}', '\n', remaining_question)

def extract_first_line(question):
    lines = question.splitlines()
    return lines[0] if lines else ""

# Task that runs on a schedule
@tasks.loop(minutes=1440) 
async def scheduled_task():
    channel = bot.get_channel(CHANNEL_ID)
    item = get_unposted_item()

    if item:
        title_slug = item['titleSlug']['S']
        qid = item['QID']['N']
        question = "%s" % (GetQuestion(titleSlug=title_slug).scrape())

        first_line = extract_first_line(question)
        cleaned_question = clean_message(question)
        parts = split_message(cleaned_question, MAX_MESSAGE_LENGTH)

        thread = await channel.create_thread(
            name=first_line, 
            type=discord.ChannelType.public_thread
        )

        for part in parts:
            await thread.send(part)

        mark_as_posted(qid)
    else:
        print("No unposted items found.")

@bot.event
async def on_ready():
    print(f'{bot.user} has connected to Discord!')
    scheduled_task.start()

@bot.event
async def on_thread_create(thread):
    await thread.send("\nYour challenge starts here! Good Luck!")

# Run the bot
bot.run(TOKEN)
Enter fullscreen mode Exit fullscreen mode

There are multiple options to run the bot. Right now, I'm just running this in a tmux shell but you could also run this in a docker container or on a VPC from AWS, Azure, DigitalOcean or other cloud providers.

Now I just have to actually attempt solving the Leetcode questions...

Top comments (0)