This commit is contained in:
36
.github/workflows/test.yml
vendored
Normal file
36
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Python CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main", "master" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.14"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install flake8
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||||
|
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||||
|
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||||
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||||
|
|
||||||
|
- name: Check syntax
|
||||||
|
run: |
|
||||||
|
python -m py_compile main.py
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
config.json
|
||||||
|
__pycache__/
|
||||||
16
License
Normal file
16
License
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
MIT No Attribution
|
||||||
|
|
||||||
|
Copyright (c) 2025
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||||
|
software and associated documentation files (the "Software"), to deal in the Software
|
||||||
|
without restriction, including without limitation the rights to use, copy, modify,
|
||||||
|
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
11
config.example.json
Normal file
11
config.example.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"bot_token": "YOUR BOT TOKEN",
|
||||||
|
"prefix": ";",
|
||||||
|
"command_name": "reddit",
|
||||||
|
"comment_limit": 2,
|
||||||
|
"delete_user_message_on_dismiss": true,
|
||||||
|
"embed_color_submission": "blue",
|
||||||
|
"embed_color_comments": "green",
|
||||||
|
"uptime_kuma_url": "",
|
||||||
|
"uptime_kuma_interval": 60
|
||||||
|
}
|
||||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
bot:
|
||||||
|
working_dir: /app
|
||||||
|
command: bash -c "pip install -r requirements.txt && python main.py"
|
||||||
|
container_name: reddit-fix-please
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
146
main.py
Normal file
146
main.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
import redditwarp.SYNC
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
# Load configuration
|
||||||
|
config_path = os.path.join(os.path.dirname(__file__), "config.json")
|
||||||
|
with open(config_path, "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
bot_token = config.get("bot_token")
|
||||||
|
prefix = config.get("prefix", ";")
|
||||||
|
command_name = config.get("command_name", "rediddy")
|
||||||
|
comment_limit = config.get("comment_limit", 2)
|
||||||
|
delete_user_msg = config.get("delete_user_message_on_dismiss", True)
|
||||||
|
uptime_kuma_url = config.get("uptime_kuma_url", "")
|
||||||
|
uptime_kuma_interval = config.get("uptime_kuma_interval", 60)
|
||||||
|
|
||||||
|
# Color mapping
|
||||||
|
COLORS = {
|
||||||
|
"blue": discord.Color.blue(),
|
||||||
|
"green": discord.Color.green(),
|
||||||
|
"red": discord.Color.red(),
|
||||||
|
"gold": discord.Color.gold(),
|
||||||
|
"purple": discord.Color.purple(),
|
||||||
|
}
|
||||||
|
sub_color = COLORS.get(
|
||||||
|
config.get("embed_color_submission", "blue"), discord.Color.blue()
|
||||||
|
)
|
||||||
|
comment_color = COLORS.get(
|
||||||
|
config.get("embed_color_comments", "green"), discord.Color.green()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Setup Discord bot intents
|
||||||
|
intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
|
||||||
|
bot = commands.Bot(command_prefix=prefix, intents=intents)
|
||||||
|
reddit_client = redditwarp.SYNC.Client()
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready():
|
||||||
|
print(f"Logged in as {bot.user} (ID: {bot.user.id})")
|
||||||
|
print("------")
|
||||||
|
if uptime_kuma_url:
|
||||||
|
bot.loop.create_task(uptime_kuma_heartbeat())
|
||||||
|
|
||||||
|
|
||||||
|
async def uptime_kuma_heartbeat():
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
while not bot.is_closed():
|
||||||
|
try:
|
||||||
|
async with session.get(uptime_kuma_url) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
print("Uptime Kuma heartbeat sent.")
|
||||||
|
else:
|
||||||
|
print(f"Uptime Kuma error: {resp.status}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Uptime Kuma connection failed: {e}")
|
||||||
|
await asyncio.sleep(uptime_kuma_interval)
|
||||||
|
|
||||||
|
|
||||||
|
class RediddyView(discord.ui.View):
|
||||||
|
def __init__(self, author_msg: discord.Message, submission_url: str):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.author_msg = author_msg
|
||||||
|
self.add_item(discord.ui.Button(label="Open Submission", url=submission_url))
|
||||||
|
|
||||||
|
@discord.ui.button(label="Dismiss", style=discord.ButtonStyle.danger)
|
||||||
|
async def dismiss(
|
||||||
|
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||||
|
):
|
||||||
|
if delete_user_msg:
|
||||||
|
try:
|
||||||
|
await self.author_msg.delete()
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass # May not have permission to delete user messages
|
||||||
|
await interaction.message.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@bot.command(name=command_name)
|
||||||
|
async def fetch_reddit_submission(ctx, subreddit_name: str, *, query: str):
|
||||||
|
"""Searches Reddit and returns the top post and comments in embeds."""
|
||||||
|
try:
|
||||||
|
# Search for the subreddit name to get a valid subreddit object/ID
|
||||||
|
sr_list = reddit_client.p.subreddit.search_names(subreddit_name)
|
||||||
|
if not sr_list:
|
||||||
|
await ctx.send(f"Subreddit '{subreddit_name}' not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
target_sr = sr_list[0]
|
||||||
|
|
||||||
|
# Search for submissions within that subreddit
|
||||||
|
submissions = reddit_client.p.submission.search(target_sr, query, 1, sort="top")
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for submission_brief in submissions:
|
||||||
|
found = True
|
||||||
|
# Get full submission details
|
||||||
|
submission = reddit_client.p.submission.get(submission_brief.id)
|
||||||
|
title = submission.d.get("title", "Reddit Post")
|
||||||
|
selftext = submission.d.get("selftext", "No content.")
|
||||||
|
permalink = f"https://www.reddit.com{submission.d.get('permalink')}"
|
||||||
|
|
||||||
|
# Submission Embed
|
||||||
|
main_embed = discord.Embed(
|
||||||
|
title=title,
|
||||||
|
url=permalink,
|
||||||
|
description=selftext[:2000] if selftext else "No content.",
|
||||||
|
color=sub_color,
|
||||||
|
)
|
||||||
|
main_embed.set_author(name=f"r/{target_sr}")
|
||||||
|
|
||||||
|
embeds = [main_embed]
|
||||||
|
|
||||||
|
# Fetch comments based on limit
|
||||||
|
try:
|
||||||
|
comment_tree = reddit_client.p.comment_tree.fetch(
|
||||||
|
submission.id, limit=comment_limit, sort="top"
|
||||||
|
)
|
||||||
|
for i, child in enumerate(comment_tree.children[:comment_limit]):
|
||||||
|
comment_body = child.value.body
|
||||||
|
comment_embed = discord.Embed(
|
||||||
|
title=f"Top Comment #{i+1}",
|
||||||
|
description=comment_body[:1000],
|
||||||
|
color=comment_color,
|
||||||
|
)
|
||||||
|
embeds.append(comment_embed)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching comments: {e}")
|
||||||
|
|
||||||
|
view = RediddyView(ctx.message, permalink)
|
||||||
|
await ctx.send(embeds=embeds, view=view)
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
await ctx.send(f"No results found for '{query}' in {target_sr}.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await ctx.send(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
bot.run(bot_token)
|
||||||
63
readme.md
Normal file
63
readme.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Reddit Fix Please - Discord Bot
|
||||||
|
|
||||||
|
A configurable Discord bot that searches Reddit subreddits for specific queries and returns the top submission along with its top comments in clean embeds.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Reddit Search**: Finds the most relevant "top" submission for a given subreddit and query.
|
||||||
|
- **Rich Embeds**: Displays the submission title, body, and top comments in easy-to-read Discord embeds.
|
||||||
|
- **Interactive UI**:
|
||||||
|
- **Open Submission**: Direct link button to the Reddit thread.
|
||||||
|
- **Dismiss**: Cleans up the bot's response and (optionally) the user's command message.
|
||||||
|
- **Fully Configurable**: Manage everything from tokens to UI colors via `config.json`.
|
||||||
|
- **Uptime Kuma Integration**: Support for push monitor heartbeats to track bot status.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- A Discord Bot Token (from [Discord Developer Portal](https://discord.com/developers/applications)) (enable "Message Content Intent" for the bot)
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. Install the required dependencies:
|
||||||
|
```bash
|
||||||
|
pip install discord.py redditwarp
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Copy `config.example.json` to `config.json`:
|
||||||
|
```bash
|
||||||
|
cp config.example.json config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open `config.json` and fill in your `bot_token`.
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
| Key | Description | Default |
|
||||||
|
|-----|-------------|---------|
|
||||||
|
| `bot_token` | Your Discord bot application token | 😬 |
|
||||||
|
| `prefix` | The character(s) used to trigger the bot | `;` |
|
||||||
|
| `command_name` | The name of the search command | `reddit` |
|
||||||
|
| `comment_limit` | Number of top comments to display | `2` |
|
||||||
|
| `delete_user_message_on_dismiss` | Whether the Dismiss button deletes the user's message | `true` |
|
||||||
|
| `embed_color_submission` | Color of the main submission embed | `blue` |
|
||||||
|
| `embed_color_comments` | Color of the comment embeds | `green` |
|
||||||
|
| `uptime_kuma_url` | Uptime Kuma Push Monitor URL | `""` |
|
||||||
|
| `uptime_kuma_interval` | Interval in seconds for heartbeats | `60` |
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Start the bot:
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
In Discord, use the configured command (default is `;reddit`):
|
||||||
|
```text
|
||||||
|
;reddit [subreddit] [query]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
`;reddit valorantechsupport van-57`
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
redditwarp
|
||||||
|
discord.py
|
||||||
|
aiohttp
|
||||||
Reference in New Issue
Block a user