Compare commits

..

10 Commits

Author SHA1 Message Date
25f9159155 sd update 2024-03-01 09:21:29 -07:00
803acf514b switched to using pytubefix 2024-03-01 09:20:44 -07:00
542bb5d5cd Various plugin updates. New plugin for stable diffusion 2024-02-29 20:18:58 -07:00
5962eb53ad AI plugins updated, now html formatted 2024-02-17 18:37:09 -07:00
eb81f7aa67 Help plugin reformat HTML 2024-02-17 17:48:46 -07:00
c996168543 Funguy class refactor 2024-02-17 16:19:23 -07:00
784409cef6 date module update 2024-02-17 07:04:35 -07:00
e9a853c31a Rehash function should work now 2024-02-16 20:13:13 -07:00
669fd361d4 update ignores 2024-02-14 12:32:30 -07:00
58fa16663e Updated README.md 2024-02-14 05:16:45 -07:00
15 changed files with 261 additions and 142 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.env .env
.gitignore
karma.db karma.db
proxies.db proxies.db
session.txt session.txt
@@ -10,3 +9,4 @@ simplematrixbotlib*/
chromedriver chromedriver
store store
funguybot.service funguybot.service
stats.db

View File

@@ -87,7 +87,7 @@ To use the bot, invite it to a Matrix room and interact with it by sending comma
- `!karma <user>`: View or modify karma points for a user. - `!karma <user>`: View or modify karma points for a user.
- `!funguy <prompt>` Talk to the Tech AI LLM - `!funguy <prompt>` Talk to the Tech AI LLM
- `!music <prompt>` Talk to the music knowledge LLM - `!music <prompt>` Talk to the music knowledge LLM
- `!yt <search terms>` Search Youtube
For a complete list of available commands and their descriptions, use the `!commands` command. For a complete list of available commands and their descriptions, use the `!commands` command.
# 🍄 Funguy Bot Commands 🍄 # 🍄 Funguy Bot Commands 🍄

128
funguy.py
View File

@@ -10,78 +10,86 @@ import time
import sys import sys
from plugins.config import FunguyConfig from plugins.config import FunguyConfig
# Setup logging class FunguyBot:
logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO)
# Load plugins (defined before plugin load/reload functions) bot = None
PLUGINS_DIR = "plugins" config = None
PLUGINS = {}
def load_plugins(): def __init__(self):
for plugin_file in os.listdir(PLUGINS_DIR): self.PLUGINS_DIR = "plugins"
if plugin_file.endswith(".py"): self.PLUGINS = {}
plugin_name = os.path.splitext(plugin_file)[0] self.config = None
try: self.bot = None
module = importlib.import_module(f"{PLUGINS_DIR}.{plugin_name}") self.load_dotenv()
PLUGINS[plugin_name] = module self.setup_logging()
logging.info(f"Loaded plugin: {plugin_name}") self.load_plugins()
except Exception as e: self.load_config()
logging.error(f"Error loading plugin {plugin_name}: {e}")
def reload_plugins(): def load_dotenv(self):
global PLUGINS load_dotenv()
PLUGINS = {}
# Unload modules from sys.modules
for plugin_name in list(sys.modules.keys()):
if plugin_name.startswith(PLUGINS_DIR + "."):
del sys.modules[plugin_name]
load_plugins()
def rehash_config(config): def setup_logging(self):
del config logging.basicConfig(format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO)
config = FunguyConfig() logging.getLogger().setLevel(logging.INFO)
def load_plugins(self):
for plugin_file in os.listdir(self.PLUGINS_DIR):
if plugin_file.endswith(".py"):
plugin_name = os.path.splitext(plugin_file)[0]
try:
module = importlib.import_module(f"{self.PLUGINS_DIR}.{plugin_name}")
self.PLUGINS[plugin_name] = module
logging.info(f"Loaded plugin: {plugin_name}")
except Exception as e:
logging.error(f"Error loading plugin {plugin_name}: {e}")
# Load environment variables from .env file def reload_plugins(self):
load_dotenv() self.PLUGINS = {}
# Unload modules from sys.modules
for plugin_name in list(sys.modules.keys()):
if plugin_name.startswith(self.PLUGINS_DIR + "."):
del sys.modules[plugin_name]
self.load_plugins()
# Load plugins def load_config(self):
load_plugins() self.config = FunguyConfig()
# Bot configuration settings async def handle_commands(self, room, message):
MATRIX_URL = os.getenv("MATRIX_URL") match = botlib.MessageMatch(room, message, self.bot, self.config.prefix)
MATRIX_USER = os.getenv("MATRIX_USER") if match.is_not_from_this_bot() and match.prefix() and match.command("reload"):
MATRIX_PASS = os.getenv("MATRIX_PASS") if str(message.sender) == self.config.admin_user:
self.reload_plugins()
await self.bot.api.send_text_message(room.room_id, "Plugins reloaded successfully")
else:
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.")
# Get credentials from env for plugin_name, plugin_module in self.PLUGINS.items():
creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS) await plugin_module.handle_command(room, message, self.bot, self.config.prefix, self.config)
# Bot configuration if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"):
config = FunguyConfig() if str(message.sender) == self.config.admin_user:
bot = botlib.Bot(creds, config) self.rehash_config()
await self.bot.api.send_text_message(room.room_id, "Config rehashed")
else:
await self.bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.")
@bot.listener.on_message_event def rehash_config(self):
async def handle_commands(room, message): del self.config
match = botlib.MessageMatch(room, message, bot, config.prefix) self.config = FunguyConfig()
if match.is_not_from_this_bot() and match.prefix() and match.command("reload"):
if str(message.sender) == config.admin_user:
reload_plugins()
await bot.api.send_text_message(room.room_id, "Plugins reloaded successfully")
else:
await bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.")
for plugin_name, plugin_module in PLUGINS.items(): def run(self):
await plugin_module.handle_command(room, message, bot, config.prefix, config) MATRIX_URL = os.getenv("MATRIX_URL")
MATRIX_USER = os.getenv("MATRIX_USER")
MATRIX_PASS = os.getenv("MATRIX_PASS")
creds = botlib.Creds(MATRIX_URL, MATRIX_USER, MATRIX_PASS)
self.bot = botlib.Bot(creds, self.config)
@self.bot.listener.on_message_event
async def wrapper_handle_commands(room, message):
await self.handle_commands(room, message)
if match.is_not_from_this_bot() and match.prefix() and match.command("rehash"): self.bot.run()
if str(message.sender) == config.admin_user:
rehash_config(config)
await bot.api.send_text_message(room.room_id, "Config rehashed")
else:
await bot.api.send_text_message(room.room_id, "You are not authorized to reload plugins.")
if __name__ == "__main__":
bot = FunguyBot()
bot.run()
bot.run()

4
g Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
git add .;
git commit -a -m "$1";
git push -u origin "$2"

View File

@@ -8,6 +8,7 @@ import requests
import json import json
import simplematrixbotlib as botlib import simplematrixbotlib as botlib
import re import re
import markdown2
async def handle_command(room, message, bot, prefix, config): async def handle_command(room, message, bot, prefix, config):
""" """
@@ -56,9 +57,12 @@ async def handle_command(room, message, bot, prefix, config):
response.raise_for_status() # Raise HTTPError for bad responses response.raise_for_status() # Raise HTTPError for bad responses
payload = response.json() payload = response.json()
new_text = payload['choices'][0]['text'] new_text = payload['choices'][0]['text']
if new_text.count('\n') > 1: # Check if new_text has more than one paragraph new_text = markdown_to_html(new_text)
new_text = new_text.replace("\n", '<br>') print(new_text)
new_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", new_text)
if new_text.count('<p>') > 1 or new_text.count('<li>') > 1: # Check if new_text has more than one paragraph
#new_text = new_text.replace("\n", '<br>')
#new_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", new_text)
new_text = "<details><summary><strong>🎵Funguy Music GPT🎵<br>⤵Click Here To See Funguy's Response⤵</strong></summary>" + new_text + "</details>" new_text = "<details><summary><strong>🎵Funguy Music GPT🎵<br>⤵Click Here To See Funguy's Response⤵</strong></summary>" + new_text + "</details>"
await bot.api.send_markdown_message(room.room_id, new_text) await bot.api.send_markdown_message(room.room_id, new_text)
else: else:
@@ -67,3 +71,8 @@ async def handle_command(room, message, bot, prefix, config):
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logging.error(f"HTTP request failed for '{prompt}': {e}") logging.error(f"HTTP request failed for '{prompt}': {e}")
await bot.api.send_text_message(room.room_id, f"Error generating text: {e}") await bot.api.send_text_message(room.room_id, f"Error generating text: {e}")
def markdown_to_html(markdown_text):
html_content = markdown2.markdown(markdown_text)
return html_content

View File

@@ -9,6 +9,7 @@ import requests
import json import json
import simplematrixbotlib as botlib import simplematrixbotlib as botlib
import re import re
import markdown2
async def handle_command(room, message, bot, prefix, config): async def handle_command(room, message, bot, prefix, config):
""" """
@@ -57,9 +58,10 @@ async def handle_command(room, message, bot, prefix, config):
response.raise_for_status() # Raise HTTPError for bad responses response.raise_for_status() # Raise HTTPError for bad responses
payload = response.json() payload = response.json()
new_text = payload['choices'][0]['text'] new_text = payload['choices'][0]['text']
if new_text.count('\n') > 2: # Check if new_text has more than one paragraph new_text = markdown_to_html(new_text)
new_text = new_text.replace("\n", '<br>') if new_text.count('<p>') > 2: # Check if new_text has more than one paragraph
new_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", new_text) #new_text = new_text.replace("\n", '<br>')
#new_text = re.sub(r"\*\*(.*?)\*\*", r"<strong>\1</strong>", new_text)
new_text = "<details><summary><strong>🍄Funguy Tech GPT🍄<br>⤵Click Here To See Funguy's Response⤵</strong></summary>" + new_text + "</details>" new_text = "<details><summary><strong>🍄Funguy Tech GPT🍄<br>⤵Click Here To See Funguy's Response⤵</strong></summary>" + new_text + "</details>"
await bot.api.send_markdown_message(room.room_id, new_text) await bot.api.send_markdown_message(room.room_id, new_text)
else: else:
@@ -68,3 +70,7 @@ async def handle_command(room, message, bot, prefix, config):
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logging.error(f"HTTP request failed for '{prompt}': {e}") logging.error(f"HTTP request failed for '{prompt}': {e}")
await bot.api.send_text_message(room.room_id, f"Error generating text: {e}") await bot.api.send_text_message(room.room_id, f"Error generating text: {e}")
def markdown_to_html(markdown_text):
html_content = markdown2.markdown(markdown_text)
return html_content

View File

@@ -1,3 +1,6 @@
"""
Custom configuration class for the Funguy bot.
"""
# plugins/config.py # plugins/config.py
import os import os

View File

@@ -23,10 +23,39 @@ async def handle_command(room, message, bot, prefix, config):
if match.is_not_from_this_bot() and match.prefix() and match.command("date"): if match.is_not_from_this_bot() and match.prefix() and match.command("date"):
logging.info("Fetching current date and time") logging.info("Fetching current date and time")
current_datetime = datetime.datetime.now() current_datetime = datetime.datetime.now()
# Extract individual date components
day_of_week = current_datetime.strftime("%A") day_of_week = current_datetime.strftime("%A")
date_of_month = current_datetime.strftime("%d") # Day with leading zero
month = current_datetime.strftime("%B") month = current_datetime.strftime("%B")
year = current_datetime.strftime("%Y") year = current_datetime.strftime("%Y")
time = current_datetime.strftime("%I:%M:%S %p") time = current_datetime.strftime("%I:%M:%S %p")
date_message = f"📅 Today is **{day_of_week}** of **{month}** in **{year}**. The time is **⏰ {time}**"
# Format date with ordinal suffix
date_with_ordinal = f"{date_of_month}{get_ordinal_suffix(date_of_month)}"
# Construct the message
date_message = f"Date: **{day_of_week}** the {date_with_ordinal}, **{month} {year}**. \nTime: **⏰ {time}**"
await bot.api.send_markdown_message(room.room_id, date_message) await bot.api.send_markdown_message(room.room_id, date_message)
logging.info("Sent current date and time to the room") logging.info("Sent current date and time to the room")
def get_ordinal_suffix(day_of_month):
"""
Helper function to get the ordinal suffix for a day number.
Args:
day_of_month (str): The day number as a string (e.g., "1", "13")
Returns:
str: The ordinal suffix (e.g., "st", "th", "nd")
"""
if day_of_month.endswith("11") or day_of_month.endswith("12") or day_of_month.endswith("13"):
return "th"
elif day_of_month.endswith("1") or day_of_month.endswith("3"):
return "st"
elif day_of_month.endswith("2"):
return "nd"
else:
return "th"

View File

@@ -21,51 +21,52 @@ async def handle_command(room, message, bot, prefix, config):
match = botlib.MessageMatch(room, message, bot, prefix) match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("help"): if match.is_not_from_this_bot() and match.prefix() and match.command("help"):
logging.info("Fetching command help documentation") logging.info("Fetching command help documentation")
commands_message = """<details><summary><strong>🍄Funguy Bot Commands🍄<br>⤵Click Here To See Help Text⤵</strong></summary> commands_message = """
<br> <details><summary><strong>🍄Funguy Bot Commands🍄<br>⤵Click Here To See Help Text⤵</strong></summary>
<br>🃏 **!fortune** <p>
<br>Returns a random fortune message. <details><summary>🃏 <strong>!fortune</strong></summary>
<br>Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room. <p>Returns a random fortune message. Executes the `/usr/games/fortune` utility and sends the output as a message to the chat room.</p>
<br> </details>
<br>⏰ **!date**
<br>Displays the current date and time. <details><summary>⏰ <strong>!date</strong></summary>
<br>Fetches the current date and time using Python's `datetime` module and sends it in a formatted message to the chat room. <p>Displays the current date and time. Fetches the current date and time using Python's `datetime` module and sends it in a formatted message to the chat room.</p>
<br> </details>
<br>💻 **!proxy**
<br>Retrieves a tested/working random SOCKS5 proxy. <details><summary>💻 <strong>!proxy</strong></summary>
<br>Fetches a list of SOCKS5 proxies, tests their availability, and sends the first working proxy to the chat room. <p>Retrieves a tested/working random SOCKS5 proxy. Fetches a list of SOCKS5 proxies, tests their availability, and sends the first working proxy to the chat room.</p>
<br> </details>
<br>📶 **!isup [domain/ip]**
<br>Checks if the specified domain or IP address is reachable. <details><summary>📶 <strong>!isup [domain/ip]</strong></summary>
<br>Checks if the specified domain or IP address is reachable by attempting to ping it. If DNS resolution is successful, it checks HTTP and HTTPS service availability by sending requests to the domain. <p>Checks if the specified domain or IP address is reachable. Checks if the specified domain or IP address is reachable by attempting to ping it. If DNS resolution is successful, it checks HTTP and HTTPS service availability by sending requests to the domain.</p>
<br> </details>
<br>☯ **!karma [user]**
<br>Retrieves the karma points for the specified user. <details><summary>☯ <strong>!karma [user]</strong></summary>
<br>Retrieves the karma points for the specified user from a database and sends them as a message to the chat room. <p>Retrieves the karma points for the specified user. Retrieves the karma points for the specified user from a database and sends them as a message to the chat room.</p>
<br> </details>
<br>⇧ **!karma [user] up**
<br>Increases the karma points for the specified user by 1. <details><summary>⇧ <strong>!karma [user] up</strong></summary>
<br>Increases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room. <p>Increases the karma points for the specified user by 1. Increases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room.</p>
<br> </details>
<br>⇩ **!karma [user] down**
<br>Decreases the karma points for the specified user by 1. <details><summary>⇩ <strong>!karma [user] down</strong></summary>
<br>Decreases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room. <p>Decreases the karma points for the specified user by 1. Decreases the karma points for the specified user by 1 in the database and sends the updated points as a message to the chat room.</p>
<br> </details>
<br>📄 **!funguy [prompt]
<br>An AI large language model designed to serve as a chatbot within a vibrant and diverse community chat room hosted on the Matrix platform. <details><summary>📄 <strong>!funguy [prompt]</strong></summary>
<br>(Currently loaded model: **TheBloke_Mistral-7B-Instruct-v0.2-GPTQ** <p>An AI large language model designed to serve as a chatbot within a vibrant and diverse community chat room hosted on the Matrix platform. (Currently loaded model: <strong>TheBloke_Mistral-7B-Instruct-v0.2-GPTQ</strong>)</p>
<br> </details>
<br>🎝 **!music [prompt]**
<br>Your music expert! Try it out. <details><summary>🎝 <strong>!music [prompt]</strong></summary>
<br> 🌟 Funguy Bot Credits 🌟 <p>Your music expert! Try it out.</p>
<br> </details>
<br>🧙‍♂️ Creator & Developer:
<br> Hash Borgir is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org) <details><summary>🌟 <strong>Funguy Bot Credits</strong> 🌟</summary>
<br> <p>🧙‍♂️ Creator & Developer: Hash Borgir is the author of 🍄Funguy Bot🍄. (@hashborgir:mozilla.org)</p>
<br> Join our Matrix Room: <p>Join our Matrix Room: [Self-hosting | Security | Sysadmin | Homelab | Programming](https://matrix.to/#/#selfhosting:mozilla.org)</p>
<br> </details>
<br>[Self-hosting | Security | Sysadmin | Homelab | Programming](https://matrix.to/#/#selfhosting:mozilla.org) </p>
<br> </details>
<br></details>""" """
await bot.api.send_markdown_message(room.room_id, commands_message) await bot.api.send_markdown_message(room.room_id, commands_message)
logging.info("Sent help documentation to the room") logging.info("Sent help documentation to the room")

View File

@@ -1,3 +1,6 @@
"""
This plugin provides a command for the admin to load a plugin
"""
# plugins/load_plugin.py # plugins/load_plugin.py
import os import os

View File

@@ -26,9 +26,9 @@ async def handle_command(room, message, bot, prefix, config):
plugin_descriptions = get_plugin_descriptions() plugin_descriptions = get_plugin_descriptions()
# Prepend custom string before the output # Prepend custom string before the output
plugin_descriptions.insert(0, "<details><summary><strong>🔌Plugins List🔌<br>⤵Click Here to Expand⤵</strong></summary><br>") plugin_descriptions.insert(0, "<details><summary><strong>🔌Plugins List🔌<br>⤵Click Here to Expand⤵</strong></summary>")
plugins_message = "<br><br>".join(plugin_descriptions) plugins_message = "<br>".join(plugin_descriptions)
plugins_message += "</details>" plugins_message += "</details>"
await bot.api.send_markdown_message(room.room_id, plugins_message) await bot.api.send_markdown_message(room.room_id, plugins_message)
logging.info("Sent plugin list to the room") logging.info("Sent plugin list to the room")

View File

@@ -0,0 +1,42 @@
"""
This plugin provides a command to generate images using self hosted Stable Diffusion and send to the room
"""
# plugins/stable-diffusion.py
import requests
import base64
from asyncio import Queue
import simplematrixbotlib as botlib
# Queue to store pending commands
command_queue = Queue()
async def process_command(room, message, bot, prefix, config):
match = botlib.MessageMatch(room, message, bot, prefix)
if match.prefix() and match.command("sd"):
if command_queue.empty():
await handle_command(room, message, bot, prefix, config)
else:
await command_queue.put((room, message, bot, prefix, config))
async def handle_command(room, message, bot, prefix, config):
match = botlib.MessageMatch(room, message, bot, prefix)
if match.prefix() and match.command("sd"):
prompt = message.body[len(prefix) + len("sd"):].strip() # Extract prompt from message body
payload = {
"prompt": prompt,
"steps": 16
}
url = "http://127.0.0.1:7860/sdapi/v1/txt2img"
try:
response = requests.post(url=url, json=payload)
r = response.json()
with open("/tmp/output.png", 'wb') as f:
f.write(base64.b64decode(r['images'][0]))
await bot.api.send_image_message(room_id=room.room_id, image_filepath="/tmp/output.png") # Corrected argument name
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error processing the command: {str(e)}")
finally:
if not command_queue.empty():
next_command = await command_queue.get()
await handle_command(*next_command)

View File

@@ -6,25 +6,31 @@ This plugin provides a command to fetch YouTube video information from links.
import re import re
import logging import logging
from pytube import YouTube from pytubefix import YouTube
import simplematrixbotlib as botlib import simplematrixbotlib as botlib
import asyncio
def seconds_to_minutes_seconds(seconds): def seconds_to_minutes_seconds(seconds):
minutes = seconds // 60 minutes = seconds // 60
seconds %= 60 seconds %= 60
return f"{minutes:02d}:{seconds:02d}" return f"{minutes:02d}:{seconds:02d}"
async def fetch_youtube_info(youtube_url):
try:
video = YouTube(youtube_url)
title = video.title
description = video.description
length = seconds_to_minutes_seconds(video.length)
views = video.views
author = video.author
description_with_breaks = description.replace('\n', '<br>')
info_message = f"""<strong>🎬🎝 Title:</strong> {title} | <strong>Length</strong>: {length} minutes | <strong>Views</strong>: {views}\n<details><summary><strong>⤵Click Here For Description⤵</strong></summary>{description_with_breaks}</details>"""
return info_message
except Exception as e:
logging.error(f"Error fetching YouTube video information: {str(e)}")
return None
async def handle_command(room, message, bot, prefix, config): async def handle_command(room, message, bot, prefix, config):
"""
Function to handle YouTube video information from links.
Args:
room (Room): The Matrix room where the command was invoked.
message (RoomMessage): The message object containing the command.
Returns:
None
"""
match = botlib.MessageMatch(room, message, bot, prefix) match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and re.search(r'youtube\.com/watch\?v=', message.body): if match.is_not_from_this_bot() and re.search(r'youtube\.com/watch\?v=', message.body):
logging.info("YouTube link detected") logging.info("YouTube link detected")
@@ -33,16 +39,18 @@ async def handle_command(room, message, bot, prefix, config):
video_id = video_id_match.group(1) video_id = video_id_match.group(1)
youtube_url = f"https://www.youtube.com/watch?v={video_id}" youtube_url = f"https://www.youtube.com/watch?v={video_id}"
logging.info(f"Fetching information for YouTube video: {youtube_url}") logging.info(f"Fetching information for YouTube video: {youtube_url}")
try: retry_count = 3
video = YouTube(youtube_url) while retry_count > 0:
title = video.title info_message = await fetch_youtube_info(youtube_url)
description = video.description if info_message:
length = seconds_to_minutes_seconds(video.length) await bot.api.send_markdown_message(room.room_id, info_message)
views = video.views logging.info("Sent YouTube video information to the room")
author = video.author break
info_message = f"""**🎬🎝 Title:** {title} | **Length**: {length} minutes| **Views:** {views} | **Description:** {description}""" else:
await bot.api.send_markdown_message(room.room_id, info_message) logging.info("Retrying...")
logging.info("Sent YouTube video information to the room") retry_count -= 1
except Exception as e: await asyncio.sleep(1) # wait for 1 second before retrying
logging.error(f"Error fetching YouTube video information: {str(e)}") else:
# await bot.api.send__message(room.room_id, "Error fetching YouTube video information.") logging.error("Failed to fetch YouTube video information after retries")

View File

@@ -1,3 +1,6 @@
"""
This plugin provides a command to search for YouTube videos in the room
"""
# plugins/youtube_search.py # plugins/youtube_search.py
import logging import logging

View File

@@ -1,5 +1,8 @@
python-dotenv python-dotenv
requests requests
pytube pytubefix
duckduckgo_search duckduckgo_search
nio nio
markdown2
watchdog
emoji