Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d97b10c20 | |||
| 8739f889d3 | |||
| 0e368ade2f | |||
| 9acb6b04eb | |||
| 2a33bc70b7 | |||
| a775a55291 | |||
| c365cd1ff2 | |||
| 4d802aba94 | |||
| 668eb52673 | |||
| 90684ba27f | |||
| 08778bf0a5 | |||
| 63972faa63 | |||
| f0729f43c9 | |||
| 91b31ed114 | |||
| 6df7d68f5a | |||
| 8615cc4d0d | |||
| b9e1740e10 | |||
| 4c874cbaa4 | |||
| 36a61e978b | |||
| 6e145fbf56 | |||
| 2adec3b76c |
+2
-1
@@ -1,8 +1,9 @@
|
||||
FROM python:3.13.12-slim-trixie
|
||||
FROM python:3.13.13-slim-trixie
|
||||
|
||||
ENV PATH=/usr/local/bin:$PATH
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||
COPY . .
|
||||
CMD ["python", "-u", "main.py"]
|
||||
@@ -4,12 +4,15 @@ A bot for Fluxer which posts a post from ich_iel every hour.
|
||||
|
||||
## Installation:
|
||||
|
||||
Clone repo:
|
||||
```
|
||||
git clone https://git.scrunkly.cat/Michelle/ich_iel-Bot.git
|
||||
```
|
||||
|
||||
Check .env file and add your FLUXER_TOKEN.
|
||||
|
||||
Then start the container:
|
||||
```
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
services:
|
||||
ich_iel-bot:
|
||||
build: .
|
||||
image: git.scrunkly.cat/michelle/ich_iel-bot:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
env_file: ".env"
|
||||
@@ -1,20 +1,25 @@
|
||||
import aiohttp
|
||||
import fluxer
|
||||
import requests
|
||||
import json
|
||||
import asyncio
|
||||
import base64
|
||||
from dotenv import load_dotenv
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
import random
|
||||
import yt_player
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# this is a bot which posts the latest image post from ich_iel
|
||||
# the code probably sucks, but it works, so I don't care
|
||||
|
||||
load_dotenv()
|
||||
prefix = os.getenv("COMMAND_PREFIX", "/")
|
||||
bot = fluxer.Bot(command_prefix=prefix, intents=fluxer.Intents.GUILD_MESSAGES | fluxer.Intents.GUILDS | fluxer.Intents.MESSAGE_CONTENT)
|
||||
bot = fluxer.Bot(command_prefix=prefix, intents=fluxer.Intents.GUILD_MESSAGES | fluxer.Intents.GUILDS | fluxer.Intents.MESSAGE_CONTENT | fluxer.Intents.GUILD_VOICE_STATES)
|
||||
yt_player.setup(bot)
|
||||
|
||||
task = None
|
||||
|
||||
@@ -49,32 +54,30 @@ async def post_reddit_periodically():
|
||||
|
||||
async def get_latest_post(subreddit):
|
||||
post_limit = os.getenv("POST_LIMIT", 20)
|
||||
url = f"https://www.reddit.com/r/{subreddit}/hot.json?limit={post_limit}"
|
||||
headers = {"User-Agent": "Mozilla/5.0 (compatible; ich_iel-Bot/0.3)"}
|
||||
url = f"https://www.reddit.com/r/{subreddit}/hot.rss?limit={post_limit}"
|
||||
headers = {"User-Agent": "Mozilla/5.0 (compatible; ich_iel-Bot/0.6.0)"}
|
||||
response = requests.get(url, headers=headers)
|
||||
if response.status_code == 200:
|
||||
if response.headers.get("Content-Type", "").startswith("application/json"):
|
||||
try:
|
||||
data = response.json()
|
||||
logging.debug(f"Fetched data from Reddit: {data}")
|
||||
posts = []
|
||||
if data["data"]["children"]:
|
||||
for child in data["data"]["children"]:
|
||||
post = child["data"]
|
||||
if post.get("post_hint") == "image" and post.get("url", "").endswith((".jpg", ".png", ".jpeg", ".gif")):
|
||||
logging.info(f"Found image post: {post['title']} - {post['url']}")
|
||||
posts.append((post["title"], post["url"]))
|
||||
return posts
|
||||
except (KeyError, json.JSONDecodeError):
|
||||
logging.error(f"Error parsing Reddit response: {response.text}")
|
||||
return []
|
||||
else:
|
||||
logging.warning(f"Unexpected content type from Reddit: {response.headers.get('Content-Type')}")
|
||||
logging.warning(f"Response content: {response.text}")
|
||||
return []
|
||||
else:
|
||||
logging.error(f"Failed to fetch Reddit data (maybe a block?): {response.status_code}")
|
||||
if response.status_code != 200:
|
||||
logging.error(f"Failed to fetch RSS feed: {response.status_code}")
|
||||
return []
|
||||
posts = []
|
||||
try:
|
||||
root = ET.fromstring(response.text)
|
||||
ns = {"atom": "http://www.w3.org/2005/Atom"}
|
||||
for entry in root.findall("atom:entry", ns):
|
||||
post_title = entry.find("atom:title", ns)
|
||||
content = entry.find("atom:content", ns)
|
||||
if post_title is None or content is None or content.text is None:
|
||||
continue
|
||||
link_match = re.search(r'<a href="([^"]+)">\[link\]</a>', content.text)
|
||||
if link_match and link_match.group(1).lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
|
||||
logging.debug(f"Found image post: {post_title.text} - {link_match.group(1)}")
|
||||
posts.append((post_title.text, link_match.group(1)))
|
||||
except ET.ParseError as e:
|
||||
logging.error(f"Error parsing RSS feed: {e}")
|
||||
return []
|
||||
|
||||
return posts
|
||||
|
||||
async def init_db():
|
||||
try:
|
||||
@@ -118,7 +121,7 @@ async def setChannel(message):
|
||||
|
||||
@bot.command()
|
||||
async def version(message):
|
||||
await message.channel.send("Version 0.4 is running")
|
||||
await message.channel.send("Version 0.6.0 is running\nSource code: https://github.com/michelleDeko/ich_iel-bot")
|
||||
|
||||
# the cat bot died, so i wanted to add this command to this bot
|
||||
@bot.command()
|
||||
@@ -133,6 +136,46 @@ async def cat(message):
|
||||
else:
|
||||
await message.channel.send("Failed to fetch cat image")
|
||||
|
||||
# i thought dogs and foxes would be nice to have too
|
||||
@bot.command()
|
||||
async def dog(message):
|
||||
response = requests.get("https://dog.ceo/api/breeds/image/random")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data and isinstance(data, dict) and "message" in data:
|
||||
await message.channel.send(data["message"])
|
||||
else:
|
||||
await message.channel.send("Could not fetch dog image")
|
||||
else:
|
||||
await message.channel.send("Failed to fetch dog image")
|
||||
|
||||
@bot.command()
|
||||
async def fox(message):
|
||||
response = requests.get("https://randomfox.ca/floof/")
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data and isinstance(data, dict) and "image" in data:
|
||||
await message.channel.send(data["image"])
|
||||
else:
|
||||
await message.channel.send("Could not fetch fox image")
|
||||
else:
|
||||
await message.channel.send("Failed to fetch fox image")
|
||||
|
||||
# time for racoons
|
||||
@bot.command()
|
||||
async def racoon(message):
|
||||
urls = random.choice([
|
||||
"https://api.mapach.es/v1/meme",
|
||||
"https://api.mapach.es/v1/coon"
|
||||
])
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(urls) as response:
|
||||
image_bytes = await response.read()
|
||||
if response.status != 200:
|
||||
await message.channel.send("Failed to fetch racoon image")
|
||||
return
|
||||
await message.channel.send(file=fluxer.File(image_bytes, filename="racoon.jpg"))
|
||||
|
||||
async def post_reddit():
|
||||
subreddit = os.getenv("SUBREDDIT", "ich_iel")
|
||||
posts = await get_latest_post(subreddit)
|
||||
@@ -156,7 +199,7 @@ async def post_reddit():
|
||||
post_id = post_id_match.group(1)
|
||||
is_posted = cur.execute("SELECT post_id FROM posted WHERE guild_id = ? AND post_id = ?", (guild_id, post_id)).fetchone()
|
||||
if is_posted:
|
||||
logging.info(f"Post {post_id} already posted in guild {guild_id}, skipping.")
|
||||
logging.debug(f"Post {post_id} already posted in guild {guild_id}, skipping.")
|
||||
continue
|
||||
try:
|
||||
channel = await bot.fetch_channel(int(channel_id))
|
||||
@@ -170,13 +213,14 @@ async def post_reddit():
|
||||
else:
|
||||
logging.warning(f"Bot is not in guild {guild_id}, removing guild from database")
|
||||
cur.execute("DELETE FROM channels WHERE guild_id = ?", (guild_id,))
|
||||
cur.execute("DELETE FROM posted WHERE guild_id = ?", (guild_id,))
|
||||
con.commit()
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Channel {channel_id} not found for guild {guild_id}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error sending to channel {channel_id}: {e}")
|
||||
logging.info("All posts processed.")
|
||||
logging.debug("All posts processed.")
|
||||
except sqlite3.Error as e:
|
||||
logging.error(f"Database error: {e}")
|
||||
|
||||
@@ -201,27 +245,6 @@ async def check_guild(guild_id):
|
||||
logging.warning(f"Bot doesn't have access to guild {guild_id}")
|
||||
return False
|
||||
|
||||
@bot.event
|
||||
async def on_guild_remove(guild):
|
||||
if isinstance(guild, fluxer.Guild):
|
||||
guild_id = guild.id
|
||||
logging.info(f"Removed from guild: {guild.name} (ID: {guild_id}), removing from database")
|
||||
else:
|
||||
if guild.get("unavailable"):
|
||||
logging.info(f"Guild {guild.get('id')} is temporarily unavailable, ignoring")
|
||||
return
|
||||
guild_id = int(guild["id"])
|
||||
logging.info(f"Removed from guild {guild_id}, removing from database")
|
||||
try:
|
||||
con = sqlite3.connect('data/ich_iel-bot.db')
|
||||
cur = con.cursor()
|
||||
cur.execute("DELETE FROM channels WHERE guild_id = ?", (guild_id,))
|
||||
cur.execute("DELETE FROM posted WHERE guild_id = ?", (guild_id,))
|
||||
con.commit()
|
||||
logging.info(f"Guild {guild_id} removed from database successfully")
|
||||
except sqlite3.Error as e:
|
||||
logging.error(f"Database error while removing guild {guild_id}: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.info("Starting bot...")
|
||||
asyncio.run(init_db())
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
fluxer.py
|
||||
fluxer.py[voice]
|
||||
requests
|
||||
asyncio
|
||||
dotenv
|
||||
aiohttp
|
||||
yt-dlp
|
||||
@@ -0,0 +1,43 @@
|
||||
# at this point this bot isn't just a reddit bot anymore, maybe i should start renaming it lol
|
||||
import yt_dlp
|
||||
import logging
|
||||
import os
|
||||
|
||||
AUDIO_DIR = "data/audio"
|
||||
|
||||
_bot = None
|
||||
|
||||
def setup(bot):
|
||||
global _bot
|
||||
_bot = bot
|
||||
os.makedirs(AUDIO_DIR, exist_ok=True)
|
||||
bot.command()(play)
|
||||
|
||||
async def play(ctx, *, url: str):
|
||||
guild_id = ctx._guild.id
|
||||
voice_state = _bot.get_voice_state(guild_id, ctx.author.id)
|
||||
|
||||
if voice_state is None or voice_state.channel_id is None:
|
||||
await ctx.reply("You're not in a voice channel!")
|
||||
return
|
||||
channel = await _bot.fetch_channel(str(voice_state.channel_id))
|
||||
logging.info(f"Playing {url}")
|
||||
ydl_opts = {
|
||||
'format': 'm4a/bestaudio/best',
|
||||
'outtmpl': f'{AUDIO_DIR}/%(id)s.%(ext)s',
|
||||
'postprocessors': [{ # Extract audio using ffmpeg
|
||||
'key': 'FFmpegExtractAudio',
|
||||
'preferredcodec': 'm4a',
|
||||
}]
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info = ydl.extract_info(url, download=True)
|
||||
filename = ydl.prepare_filename(info).rsplit('.', 1)[0] + '.m4a'
|
||||
title = info.get('title', 'Unknown Title')
|
||||
logging.info(f"Downloaded to {filename}")
|
||||
|
||||
await ctx.reply(f"Playing {title} in {channel.mention}")
|
||||
|
||||
async with await channel.connect(_bot) as vc:
|
||||
await vc.play_file(filename)
|
||||
Reference in New Issue
Block a user