This project requires Python 3.13. Follow the setup instructions for your operating system:
# Install pyenv if not already installed
brew install pyenv
# Add pyenv to your shell profile
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
echo 'eval "$(pyenv init -)"' >> ~/.zshrc
# Restart your terminal or reload your profile
source ~/.zshrc
# Install Python 3.13
pyenv install 3.13.0
pyenv global 3.13.0# Install pyenv-win using Git
git clone https://github.com/pyenv-win/pyenv-win.git %USERPROFILE%\.pyenv
# Add to PATH (run in Command Prompt as Administrator)
setx PATH "%USERPROFILE%\.pyenv\pyenv-win\bin;%USERPROFILE%\.pyenv\pyenv-win\shims;%PATH%"
# Restart Command Prompt and install Python 3.13
pyenv install 3.13.0
pyenv global 3.13.0# Install pyenv dependencies
sudo apt update
sudo apt install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev \
libncursesw5-dev xz-utils tk-dev libffi-dev liblzma-dev python3-openssl git
# Install pyenv
curl https://pyenv.run | bash
# Add to shell profile
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc
# Restart terminal and install Python 3.13
source ~/.bashrc
pyenv install 3.13.0
pyenv global 3.13.0After installing Python 3.13, create and activate a virtual environment:
# Create virtual environment
python3.13 -m venv venv
# Activate virtual environment
source venv/bin/activate # On Windows: venv\Scripts\activate
# Upgrade pip and install dependencies
python -m pip install --upgrade pip
pip install -r discord_bot/requirements.txtdiscord_bot/
main.py # Entry point with Flask OAuth integration
src/
bot/
init_discord_bot.py # Main bot code with slash commands
auth.py # GitHub OAuth handling
utils/ # Database and role utilities
config/
.env # Your environment variables
credentials.json # Firebase service account key
requirements.txt
deployment/ # Cloud deployment scripts
First, let's understand what we need to set up. Note the file discord_bot/config/.env.example - this shows all the environment variables we need to fill in.
Step 1: Copy .env.example to .env in the same directory:
cp discord_bot/config/.env.example discord_bot/config/.envYour .env file needs these values:
DISCORD_BOT_TOKEN=(Discord bot authentication)GITHUB_TOKEN=(GitHub API access)GITHUB_CLIENT_ID=(GitHub OAuth app ID)GITHUB_CLIENT_SECRET=(GitHub OAuth app secret)REPO_OWNER=(Your GitHub organization name)OAUTH_BASE_URL=(Your Cloud Run URL - set in Step 4)
Additional files you need:
discord_bot/config/credentials.json(Firebase/Google Cloud credentials)
GitHub repository secrets you need to configure: Go to your GitHub repository β Settings β Secrets and variables β Actions β Click "New repository secret" for each:
DISCORD_BOT_TOKENGH_TOKENGOOGLE_CREDENTIALS_JSONREPO_OWNERCLOUD_RUN_URL
What this configures:
.envfile:DISCORD_BOT_TOKEN=your_token_here- GitHub Secret:
DISCORD_BOT_TOKEN
What this does: Creates a Discord application and bot that can interact with your Discord server.
- Go to Discord Developer Portal: https://discord.com/developers/applications
- Create New Application: Click "New Application" β Enter any name you want
- Configure OAuth2 Scopes:
- Go to "OAuth2" tab
- Under "Scopes", check these boxes:
-
bot -
applications.commands
-
- Set Bot Permissions:
- Scroll down to find "Bot Permissions" section (below the Scopes section)
- Check these boxes:
-
Manage Roles -
View Channels -
Manage Channels -
Send Messages -
Embed Links -
Read Message History -
Use Slash Commands -
Use Embedded Activities -
Connect -
Attach Files
-
- Invite Bot to Your Server:
- Copy the generated URL from the OAuth2 page
- Paste it in your browser and invite the bot to your Discord server
- Enable Required Intents:
- Go to "Bot" tab
- Enable these 3 intents:
-
PRESENCE INTENT -
SERVER MEMBERS INTENT -
MESSAGE CONTENT INTENT
-
- Get Your Bot Token:
- Click "Reset Token" β Copy the token
- Add to
.env:DISCORD_BOT_TOKEN=your_token_here - Add to GitHub Secrets: Create secret named
DISCORD_BOT_TOKEN
What this configures:
- Config file:
discord_bot/config/credentials.json - GitHub Secret:
GOOGLE_CREDENTIALS_JSON
What this does: Creates a database to store Discord-GitHub user links and contribution data.
-
Create Firebase Project:
- Go to https://console.firebase.google.com
- Click "Get started" β "Create a project"
- Enter any project name
- Accept or decline Google Analytics (doesn't matter for this project)
- Click "Create project"
-
Create Firestore Database:
- In your Firebase project, click "Firestore Database" in the left sidebar
- Click "Create database"
- Choose "Start in production mode" β Click "Next"
- Select any region β Click "Done"
-
Add Test Data (Important!):
- Click "Start collection"
- Collection ID:
discord - Document ID:
123456789(any numbers) - Add field:
github_idwith value:testuser - Click "Save"
-
Download Service Account Key:
- Click the gear icon (Project Settings) in the top left
- Go to "Service accounts" tab
- Under "Admin SDK configuration snippet", select "Python"
- Click "Generate new private key"
- Download the JSON file
-
Set Up credentials.json:
- Rename the downloaded file to
credentials.json - Move it to
discord_bot/config/credentials.json
- Rename the downloaded file to
-
Create GitHub Secret:
- Open the
credentials.jsonfile in a text editor - Copy the entire JSON content
- Go to https://www.base64encode.org/
- Paste the JSON content and encode it to base64
- Copy the base64 string
- Add to GitHub Secrets: Create secret named
GOOGLE_CREDENTIALS_JSONwith the base64 string
- Open the
What this configures:
.envfile:GITHUB_TOKEN=your_token_here- GitHub Secret:
GH_TOKEN
What this does: Allows the bot to access GitHub API to fetch repository and contribution data.
- Go to GitHub Token Settings: https://github.com/settings/tokens
- Create New Token:
- Click "Generate new token" β "Generate new token (classic)"
- Set Permissions:
- Check only: [x]
repo(this gives full repository access)
- Check only: [x]
- Generate and Save:
- Click "Generate token" β Copy the token
- Add to
.env:GITHUB_TOKEN=your_token_here - Add to GitHub Secrets: Create secret named
GH_TOKEN
What this configures:
.envfile:OAUTH_BASE_URL=YOUR_CLOUD_RUN_URL
What this does: Creates a placeholder Cloud Run service to get your stable URL, which you'll need for GitHub OAuth setup.
-
Run the URL getter script:
./discord_bot/deployment/get_url.sh
This interactive script will:
- Guide you through Google Cloud authentication
- Let you select your Google Cloud project
- Choose your preferred region
- Deploy a placeholder Hello World service
- Generate your Cloud Run URL automatically
-
Save the generated information:
- The script will display your URL like:
https://discord-bot-abcd1234-uc.a.run.app - IMPORTANT: Copy this exact URL - you'll use it multiple times!
- REMEMBER YOUR PROJECT ID - you'll need it for the final deployment
- Add to
.env:OAUTH_BASE_URL=YOUR_CLOUD_RUN_URL - Example:
OAUTH_BASE_URL=https://discord-bot-abcd1234-uc.a.run.app - Add to GitHub Secrets: Create secret named
CLOUD_RUN_URLwith the same URL
- The script will display your URL like:
What this configures:
.envfile:GITHUB_CLIENT_ID=your_client_id.envfile:GITHUB_CLIENT_SECRET=your_secret
What this does: Allows users to link their Discord accounts with their GitHub accounts securely.
-
Go to GitHub Developer Settings: https://github.com/settings/developers
-
Create OAuth App:
- Click "New OAuth App"
-
Fill in Application Details:
- Application name:
Your Bot Name(anything you want) - Homepage URL:
YOUR_CLOUD_RUN_URL(from Step 4) - Authorization callback URL:
YOUR_CLOUD_RUN_URL/login/github/authorized
Example URLs: If your Cloud Run URL is
https://discord-bot-abcd1234-uc.a.run.app, then:- Homepage URL:
https://discord-bot-abcd1234-uc.a.run.app - Callback URL:
https://discord-bot-abcd1234-uc.a.run.app/login/github/authorized
- Application name:
-
Get Credentials:
- Click "Register application"
- Copy the "Client ID" β Add to
.env:GITHUB_CLIENT_ID=your_client_id - Click "Generate a new client secret" β Copy it β Add to
.env:GITHUB_CLIENT_SECRET=your_secret
What this configures:
.envfile:REPO_OWNER=your_org_name- GitHub Secret:
REPO_OWNER
What this does: Tells the bot which GitHub organization's repositories to monitor for contributions.
- Find Your Organization Name:
- Go to your organization's repositories page (example:
https://github.com/orgs/ruxailab/repositories) - The organization name is the part after
/orgs/(example:ruxailab)
- Go to your organization's repositories page (example:
- Set in Configuration:
- Add to
.env:REPO_OWNER=your_org_name(example:REPO_OWNER=ruxailab) - Add to GitHub Secrets: Create secret named
REPO_OWNERwith the same value - Important: Use ONLY the organization name, NOT the full URL
- Add to
Now that all your environment variables and GitHub OAuth settings are configured, deploy your bot:
# Deploy your bot with all settings configured
./discord_bot/deployment/deploy.shThe deployment script will:
- Build your Docker image with the updated
.envfile - Deploy to Cloud Run with your
OAUTH_BASE_URLconfigured - Set up all environment variables and secrets
After deployment completes, your bot will be fully functional with OAuth!
-
Link Your Discord Account:
- In your Discord server, type
/link - Click the URL the bot provides
- You'll be redirected to GitHub to authorize
- After authorization, you should see a success message
- You can now close the tab and return to Discord
- In your Discord server, type
-
Test Other Commands:
/getstats- View your GitHub contribution stats/halloffame- See top contributors/setup_voice_stats- Create voice channels showing repo stats/show-top-contributors- Show top contributors by PR count/show-activity-comparison- Show contributor activity comparison/show-activity-trends- Show recent activity trends
-
Test Role Updates:
# Set your repository as default for GitHub CLI gh repo set-default # Trigger the workflow to fetch data and assign roles gh workflow run update-discord-roles.yml
Common Issues:
-
Bot doesn't respond to commands:
- Check that all intents are enabled in Discord Developer Portal
- Verify the bot has proper permissions in your Discord server
- Check Cloud Run logs for errors
-
Authentication errors:
- Double-check all tokens in your
.envfile - Make sure
credentials.jsonis in the correct location - Redeploy after changing
.envfile
- Double-check all tokens in your
-
GitHub linking fails with "redirect_uri not associated":
- Make sure your GitHub OAuth app's callback URL matches your Cloud Run URL
- Should be:
YOUR_CLOUD_RUN_URL/login/github/authorized - Redeploy after updating GitHub OAuth settings
-
Role assignment doesn't work:
- Ensure the bot has "Manage Roles" permission
- Check that the bot's role is higher than the roles it's trying to assign
Need help? Contact onlineee__. on Discord for support.
This section explains the technical details of how our Discord bot serves both Discord commands AND web OAuth on the same Cloud Run service.
discord_bot/
main.py # Entry point - orchestrates everything
src/bot/
init_discord_bot.py # Discord bot with all commands
auth.py # Flask OAuth server
deployment/
entrypoint.sh # Container startup script
File: discord_bot/deployment/entrypoint.sh (Lines 42-47)
echo "Command: python -u main.py"
echo "Command executed at: $(date)" >> discord_bot_status.log
# Run the new main.py which includes both Discord bot and Flask OAuth
python -u main.py 2>&1 | tee -a discord_bot.logFile: discord_bot/main.py (Lines 15-31)
def run_discord_bot_async():
"""Run the Discord bot asynchronously using existing bot setup"""
print("π€ Starting Discord bot...")
try:
# Import the existing Discord bot with all commands
print(" Importing existing Discord bot setup...")
import src.bot.init_discord_bot as discord_bot_module
print(" Discord bot setup imported successfully")
# Get the bot instance and run it
print("π€ Starting Discord bot connection...")
discord_bot_module.bot.run(discord_bot_module.TOKEN)File: discord_bot/main.py (Lines 64-75)
# Start Discord bot in a separate thread
print("π§΅ Setting up Discord bot thread...")
def start_discord_bot():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
print("π€ Starting Discord bot in thread...")
run_discord_bot_async()
except Exception as e:
print(f" Discord bot error: {e}")
import traceback
traceback.print_exc()
discord_thread = threading.Thread(target=start_discord_bot, daemon=True)
discord_thread.start()File: discord_bot/main.py (Lines 85-94)
# Run Flask web server in main thread
oauth_app.run(
host="0.0.0.0", # Listen on all network interfaces
port=port, # Cloud Run sets PORT=8080
debug=True, # Enable debug for more logging
use_reloader=False,
threaded=True # Handle multiple requests simultaneously
)File: discord_bot/src/bot/auth.py (Lines 40-49)
@app.route("/")
def index():
return jsonify({
"service": "Discord Bot with OAuth",
"status": "running",
"endpoints": {
"start_auth": "/auth/start/<discord_user_id>",
"callback": "/auth/callback"
}
})File: discord_bot/src/bot/auth.py (Lines 51-70)
@app.route("/auth/start/<discord_user_id>")
def start_oauth(discord_user_id):
"""Start OAuth flow for a specific Discord user"""
try:
with oauth_sessions_lock:
# Clear any existing session for this user
oauth_sessions[discord_user_id] = {
'status': 'pending',
'created_at': time.time()
}
# Store user ID in session for callback
session['discord_user_id'] = discord_user_id
print(f"Starting OAuth for Discord user: {discord_user_id}")
# Redirect to GitHub OAuth
return redirect(url_for("github.login"))File: discord_bot/src/bot/auth.py (Lines 75-95)
@app.route("/auth/callback")
def github_callback():
"""Handle GitHub OAuth callback - original working version"""
try:
discord_user_id = session.get('discord_user_id')
if not discord_user_id:
return "Authentication failed: No Discord user session", 400
if not github.authorized:
print(" GitHub OAuth not authorized")
with oauth_sessions_lock:
oauth_sessions[discord_user_id] = {
'status': 'failed',
'error': 'GitHub authorization failed'
}
return "GitHub authorization failed", 400
# Get GitHub user info
resp = github.get("/user")File: discord_bot/src/bot/auth.py (Lines 11-13)
# Global state for OAuth sessions (keyed by Discord user ID)
oauth_sessions = {}
oauth_sessions_lock = threading.Lock()File: discord_bot/src/bot/auth.py (Lines 170-185)
def wait_for_username(discord_user_id, max_wait_time=300):
"""Wait for OAuth completion by polling the status"""
start_time = time.time()
while time.time() - start_time < max_wait_time:
with oauth_sessions_lock:
session_data = oauth_sessions.get(discord_user_id)
if session_data:
if session_data['status'] == 'completed':
github_username = session_data.get('github_username')
# Clean up
del oauth_sessions[discord_user_id]
return github_usernameKey Question: How does https://discord-bot-999242429166.us-central1.run.app/auth/start/123 actually reach your Flask code?
When you deploy, Cloud Run automatically creates a reverse proxy:
# Your deployment command:
gcloud run deploy discord-bot --port=8080
# Cloud Run thinks: "I'll listen on the public URL and forward to port 8080 inside the container"File: discord_bot/main.py (Lines 85-90)
# Get port from environment (Cloud Run sets PORT=8080)
port = int(os.environ.get("PORT", 8080))
# Flask says: "I'm listening on ALL network interfaces, port 8080"
oauth_app.run(
host="0.0.0.0", # β Listen on ALL interfaces (not just localhost)
port=port, # β 8080
)User types: https://discord-bot-999242429166.us-central1.run.app/auth/start/123
β
Google DNS: "discord-bot-999242429166.us-central1.run.app = Load Balancer IP 34.102.136.180"
β
Load Balancer: "This request is for service 'discord-bot' in us-central1"
β
Container Router: "discord-bot service β Container Instance #abc123"
β
Network Namespace: "Forward to container internal IP 10.4.0.15:8080"
β
Container Network: "localhost:8080/auth/start/123"
β
Flask App: "@app.route('/auth/start/<id>') matches! Call start_oauth(id='123')"
What the user sends:
GET /auth/start/123 HTTP/1.1
Host: discord-bot-999242429166.us-central1.run.appWhat Cloud Run forwards to your Flask app:
GET /auth/start/123 HTTP/1.1
Host: localhost:8080 # β Changed!
X-Forwarded-Host: discord-bot-999242429166.us-central1.run.app # β Original host saved
X-Forwarded-Proto: https # β Original protocol savedFile: discord_bot/main.py (Line 87)
oauth_app.run(host="0.0.0.0", port=8080)
# β THIS IS KEY!- If you used
host="127.0.0.1": Flask only listens on localhost interface - With
host="0.0.0.0": Flask listens on ALL network interfaces, including container's external interface
# Inside your container, this is what the network looks like:
$ ip addr show
1: lo: 127.0.0.1 # localhost
2: eth0: 10.4.0.15 # container's internal IP
$ netstat -ln
tcp 0.0.0.0:8080 LISTEN # Flask listening on ALL interfaces
# Cloud Run forwards from eth0 (10.4.0.15:8080) to your Flask app1. Public Internet
User: "I want https://discord-bot-999242429166.us-central1.run.app/auth/start/123"
2. Google's Load Balancer
LB: "discord-bot-999242429166 β Container cluster in us-central1"
3. Container Orchestrator
K8s: "discord-bot service β Pod #abc123 at IP 10.4.0.15"
4. Network Proxy
Proxy: "Forward HTTP to 10.4.0.15:8080/auth/start/123"
5. Container Network
Container: "Incoming request on eth0:8080"
6. Flask Application
Flask: "@app.route('/auth/start/<id>') β start_oauth(id='123')"
The KEY insight: Your Flask app never knows about the public domain! It just sees localhost:8080/auth/start/123 and Cloud Run handles all the networking magic invisibly.
File: discord_bot/src/bot/init_discord_bot.py (Lines 74-85)
@bot.tree.command(name="link", description="Link your Discord to GitHub")
async def link(interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
# Attempt to acquire the lock
if not verification_lock.acquire(blocking=False):
await interaction.followup.send("The verification process is currently busy. Please try again later.", ephemeral=True)
return
try:
discord_user_id = str(interaction.user.id)
# Get the OAuth URL for this specific user
oauth_url = get_github_username_for_user(discord_user_id)File: discord_bot/main.py (Lines 42-49)
# Check required environment variables
required_vars = [
"DISCORD_BOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_CLIENT_ID",
"GITHUB_CLIENT_SECRET",
"OAUTH_BASE_URL" # β This is your Cloud Run URL
]File: discord_bot/src/bot/auth.py (Lines 27-35)
# Get the base URL for OAuth callbacks (Cloud Run URL)
base_url = os.getenv("OAUTH_BASE_URL")
if not base_url:
raise ValueError("OAUTH_BASE_URL environment variable is required")
# OAuth blueprint with custom callback URL (avoiding Flask-Dance auto routes)
github_blueprint = make_github_blueprint(
client_id=os.getenv("GITHUB_CLIENT_ID"),
client_secret=os.getenv("GITHUB_CLIENT_SECRET"),
redirect_url=f"{base_url}/auth/callback" # β GitHub will redirect here
)-
Single Process, Multiple Services:
main.pyLines 64-94 show how one container runs both Discord bot (background thread) and Flask (main thread) -
Shared Memory Communication:
auth.pyLines 11-13 and 170-185 show how threads communicate via theoauth_sessionsdictionary -
URL Routing:
auth.pyLines 51-70 demonstrate Flask's@app.routedecorator for handling different URL paths -
Environment-Based Configuration:
main.pyLines 42-49 show how Cloud Run URLs are configured via environment variables -
OAuth Flow State Management:
auth.pyLines 55-65 show how user sessions are tracked across HTTP requests -
Thread-Safe Operations:
auth.pyLines 56-60 and 174-178 demonstrate proper locking for shared data structures
- View Flask logs: Check Cloud Run logs for HTTP request patterns
- Inspect OAuth sessions: Add debug prints in
auth.pyLines 56 and 110 - Monitor thread health: Add logging to
main.pyLines 70-73 - Test routes directly: Visit
YOUR_CLOUD_RUN_URL/to see the Flask index page
This architecture allows a single Cloud Run service to handle both Discord WebSocket connections and HTTP OAuth requests efficiently!