Author SHA1 Message Date
AllfatherHatt f22f1d46d1 Merged 2026-02-04 18:23:41 +01:00
AllfatherHatt 7bcdae7b79 Aligning activity logger 2026-02-04 18:18:58 +01:00
AllfatherHatt d51bc4965e Merge pull request 'feature_fitness_logger' (#16) from feature_fitness_logger into master
Reviewed-on: #16
2026-02-04 18:01:02 +01:00
AllfatherHatt d7a6ca31d1 Merge branch 'master' into feature_fitness_logger 2026-02-04 17:55:54 +01:00
AllfatherHatt 6cf112be49 Merge pull request 'Attempting to be a bit more direct about activity path' (#15) from debug_fitness into master
Reviewed-on: #15
Reviewed-by: T-BENZIN <[email protected]>
2026-02-04 17:50:44 +01:00
T-BENZIN f20a4abc81 Original message is put in lower case whole and redundant .lower() methods are removed.
Modified usage message.
2026-02-04 21:42:25 +05:00
AllfatherHatt 175eeced0c Attempting to be a bit more direct about activity path 2026-02-04 14:02:16 +01:00
AllfatherHatt 2be9cc5795 Merge pull request 'Called correct function' (#14) from minor-typo-fix into master
Reviewed-on: #14
Reviewed-by: T-BENZIN <[email protected]>
2026-02-04 13:42:29 +01:00
AllfatherHatt 07a2bf7e38 Merge pull request 'Fixed wrong json key being pulled (I forgot to replace the old name)' (#13) from json-key-fix into master
Reviewed-on: #13
Reviewed-by: AllfatherHatt <[email protected]>
2026-02-04 12:32:49 +01:00
AllfatherHatt 1ee949eae2 Called correct function 2026-02-04 12:29:22 +01:00
T-BENZIN 8bec9a159b Fixed wrong json key being pulled (I forgot to replace the old name) 2026-02-04 12:10:34 +01:00
AllfatherHatt ee77279d6e Merge pull request 'Fixed minor import issue' (#12) from minor-import-fix into master
Reviewed-on: #12
2026-02-04 11:46:39 +01:00
AllfatherHatt 1b67240976 Fixed minor import issue 2026-02-04 11:44:44 +01:00
AllfatherHatt 16a19e6913 Merge pull request 'Added cog for fitness activity logging feature.' (#11) from feature_fitness_logger into master
Reviewed-on: #11
Reviewed-by: AllfatherHatt <[email protected]>
2026-02-04 11:14:40 +01:00
T-BENZIN 4ae03c9b9d Added cog for fitness activity logging feature.
At its starting state it would accept following commands:
- !getunits <activity>: Would send a JSON file with activity units via Discord message. Those files are responsible for setting up a fitness activity to log.
- !getlog <activity>: Would send a CSV file with fitness activity log. Those files contain records with timestamp, username, and reported activity measurements.
- !<activity> <value> <unit (optional)>: Would create a record with your fitness activity in corresponding CSV log file. If you choose to report different unit (feet, for example), it is going to convert as specified in its corresponding JSON file with units (to meters, for example) if the activity specified as convertable. Non-convertable activities' units are ignored.
2026-01-11 10:03:55 +05:00
T-BENZIN 074ff8bbb4 Merge pull request 'Made improvements in AI Agent's tool get_forecast_weather' (#8) from dev into master
Reviewed-on: #8
Reviewed-by: AllfatherHatt <[email protected]>
2025-06-18 17:13:22 +02:00
16 changed files with 339 additions and 515 deletions
+1
View File
@@ -2,6 +2,7 @@
cogs made for our Royal butt.
## Our Cogs:
- [FitnessCog](./fitnessCog)
- [RecruitmentCog](./recruitmentCog/)
- [ReginaldCog](./reginaldCog/)
- [TrafficCog](./trafficCog)
+6
View File
@@ -0,0 +1,6 @@
from redbot.core.bot import Red
from .fitness import FitnessCog
async def setup(bot: Red):
cog = FitnessCog(bot)
await bot.add_cog(cog)
+48
View File
@@ -0,0 +1,48 @@
{
"convertable": true,
"units": {
"meter": {
"factor": 1,
"aliases": [
"meter",
"meters",
"m",
"m."
]
},
"kilometer": {
"factor": 1000,
"aliases": [
"kilometer",
"kilometers",
"km",
"km."
]
},
"foot": {
"factor": 0.3048,
"aliases": [
"foot",
"feet",
"ft",
"ft."
]
},
"mile": {
"factor": 1609.344,
"aliases": [
"mile",
"miles",
"mi",
"mi."
]
},
"step": {
"factor": 0.18,
"aliases": [
"step",
"steps"
]
}
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"convertable": false
}
+105
View File
@@ -0,0 +1,105 @@
import csv
import json
from pathlib import Path
from typing import Optional
def activity_log_path(activity: str, logs_dir: Optional[Path] = None) -> Path:
"""
Returns the path of the activity log file in .csv format.
"""
logs_dir = logs_dir or (Path.cwd() / "activity_logs")
return logs_dir / f"{activity}.csv"
def activity_units_path(activity: str, units_dir: Optional[Path] = None) -> Path:
"""
Returns the path of the activity units file in .json format.
"""
units_dir = units_dir or (Path.cwd() / "activities")
return units_dir / f"{activity}.json"
def is_convertable(activity: str, units_dir: Optional[Path] = None) -> bool:
filename = activity_units_path(activity, units_dir)
with filename.open("r", encoding="utf-8") as f:
raw_data = json.load(f)
return bool(raw_data.get("convertable"))
def create_activity_log_file(
activity: str,
*,
units_dir: Optional[Path] = None,
logs_dir: Optional[Path] = None,
) -> None:
if not activity_units_path(activity, units_dir).exists():
raise ValueError(f"{activity} is not a valid activity")
logs_dir = logs_dir or (Path.cwd() / "activity_logs")
logs_dir.mkdir(parents=True, exist_ok=True)
filename = activity_log_path(activity, logs_dir)
if filename.exists():
return # don't re-add headers
with filename.open("w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["timestamp", "username", "value"])
def convert_units(
activity: str,
value: int | float,
unit: str | None,
*,
units_dir: Optional[Path] = None,
) -> float:
filename = activity_units_path(activity, units_dir)
if not filename.exists():
raise ValueError(f"{activity} is not a valid activity")
# Consider None unit value as factor of 1
if unit is None:
return float(value)
with filename.open("r", encoding="utf-8") as f:
raw_data = json.load(f)
units_data = raw_data.get("units") or {}
unit_l = unit.lower()
for _, data in units_data.items():
aliases = [a.lower() for a in (data.get("aliases") or [])]
if unit_l in aliases:
return float(value) * float(data["factor"])
raise ValueError(f"{unit} is not a valid unit")
def log_activity(
timestamp: int,
username: str,
activity: str,
value: int | float,
unit: str | None,
*,
units_dir: Optional[Path] = None,
logs_dir: Optional[Path] = None,
) -> None:
if not isinstance(value, (int, float)) or value <= 0:
raise ValueError(f"{value} is not a valid value")
filename = activity_log_path(activity, logs_dir)
if not filename.exists():
create_activity_log_file(activity, units_dir=units_dir, logs_dir=logs_dir)
if is_convertable(activity, units_dir):
converted_value = round(convert_units(activity, value, unit, units_dir=units_dir), 2)
else:
converted_value = round(float(value), 2)
with filename.open("a", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([timestamp, username, converted_value])
+143
View File
@@ -0,0 +1,143 @@
import os
from pathlib import Path
import discord
from redbot.core import commands
from redbot.core.data_manager import cog_data_path
from .activity_logger import log_activity
class FitnessCog(commands.Cog):
def __init__(self, bot):
self.bot = bot
default_threads = "1457402451530350727,1328363409648910356"
threads_raw = os.getenv("THREADS_ID", default_threads)
self.threads_id: list[int] = []
for x in threads_raw.split(","):
x = x.strip()
if x.isdigit():
self.threads_id.append(int(x))
self.confirm_reactions = os.getenv("CONFIRMATION_REACTIONS", "🐈💨")
# Units are shipped with the cog in: fitnessCog/activities/*.json
self.units_dir: Path = Path(__file__).parent / "activities"
# Logs are writable and belong in Red's data dir
self.logs_dir: Path = cog_data_path(self) / "activity_logs"
self.logs_dir.mkdir(parents=True, exist_ok=True)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.author.bot:
return
if message.channel.id not in self.threads_id:
return
content = (message.content or "").strip()
if not content.startswith("!"):
return
cmdline = content[1:].strip().lower()
if not cmdline:
return
cmd, *rest = cmdline.split(" ", 1)
arg = rest[0].strip() if rest else ""
if cmd == "getunits":
await self.get_units_file(message, arg)
return
if cmd == "getlog":
await self.get_log_file(message, arg)
return
await self.log_fitness(message)
async def get_units_file(self, message: discord.Message, activity: str):
try:
if not activity:
await message.channel.send(
"Usage: `!getunits <activity>` (example: `!getunits pushups`)"
)
return
activity = activity.lower().strip()
path = self.units_dir / f"{activity}.json"
if not path.exists():
await message.channel.send(f"No units JSON found for `{activity}`.")
return
await message.channel.send(file=discord.File(fp=str(path)))
except Exception as e:
await message.channel.send(str(e))
async def get_log_file(self, message: discord.Message, activity: str):
try:
if not activity:
await message.channel.send(
"Usage: `!getlog <activity>` (example: `!getlog pushups`)"
)
return
activity = activity.lower().strip()
path = self.logs_dir / f"{activity}.csv"
if not path.exists():
await message.channel.send(f"No log CSV found for `{activity}`.")
return
await message.channel.send(file=discord.File(fp=str(path)))
except Exception as e:
await message.channel.send(str(e))
async def log_fitness(self, message: discord.Message):
try:
parts = message.content[1:].strip().split()
if len(parts) < 2:
await message.reply(
"Usage: `!<activity> <value> [unit]` (example: `!pushups 20` or `!run 5 km`)"
)
return
activity = parts[0]
try:
value = float(parts[1])
except ValueError:
await message.reply(
"Invalid number. Usage: `!<activity> <value> [unit]` (example: `!run 5 km`)"
)
return
unit = parts[2] if len(parts) > 2 else "m"
timestamp = int(message.created_at.timestamp())
username = message.author.name
log_activity(
timestamp,
username,
activity,
value,
unit,
units_dir=self.units_dir,
logs_dir=self.logs_dir,
)
for r in self.confirm_reactions:
await message.add_reaction(r)
except Exception as e:
await message.reply(str(e))
def setup(bot):
bot.add_cog(FitnessCog(bot))
-38
View File
@@ -1,38 +0,0 @@
import os
import discord
from discord.ext import commands
from reginaldCog.messenger_clients.services import MessageService
TOKEN = os.getenv('SCREAMING_OPOSSUM') # Your Discord bot token goes here
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix='!', intents=intents)
@bot.event
async def on_ready():
print(f'Logged in as {bot.user} (ID: {bot.user.id})')
print('------')
try:
synced = await bot.tree.sync()
print(f'Synced {len(synced)} command(s).')
except Exception as e:
print(f'Failed to sync commands: {e}')
@bot.event
async def on_message(message: discord.Message):
if message.author == bot.user:
return
async with message.channel.typing():
message_service = MessageService(message)
response = await message_service.get_llm_response()
await message.channel.send(response)
print(response)
if __name__ == '__main__':
if TOKEN is None:
raise RuntimeError('Discord token is not set')
bot.run(TOKEN)
View File
-192
View File
@@ -1,192 +0,0 @@
from abc import ABC, abstractmethod
from enum import Enum
from openai import OpenAI
from discord import Message
from reginaldCog.messenger_clients.messenger_client import ClientMessage, DiscordMessageAdapter
class ILLMContent(ABC):
pass
class OpenAIContent(ILLMContent):
def __init__(self):
self._content_items: list[dict[str, str]] = []
@property
def content_items(self) -> list[dict]:
return self._content_items
@content_items.setter
def content_items(self, value: list[dict[str, str]]):
self._content_items = value
class ILLMContentBuilder(ABC):
pass
class OpenAIContentBuilder(ILLMContentBuilder):
def __init__(self, content: OpenAIContent):
self.content = content
def add_output_text(self, text: str):
item = {"type": "output_text", "text": text}
self.content.content_items.append(item)
return self
def add_input_text(self, text: str):
item = {"type": "input_text", "text": text}
self.content.content_items.append(item)
return self
def add_input_image(self, image_url: str):
item = {"type": "input_image", "image_url": image_url}
self.content.content_items.append(item)
return self
def add_from_dict(self, item: dict):
self.content.content_items.append(item)
return self
class ILLMMessage(ABC):
pass
class OpenAIMessage(ILLMMessage):
def __init__(self):
self.content = OpenAIContent()
self.role = ""
@property
def to_dict(self):
return {"role": self.role, "content": self.content.content_items}
class ILLMMessageBuilder(ABC):
pass
class OpenAIMessageBuilder(ILLMMessageBuilder):
def __init__(self, message: OpenAIMessage):
self.message = message
def set_role(self, role: str):
self.message.role = role
return self
def set_content(self, content: OpenAIContent):
self.message.content = content
return self
class ILLMPrompt(ABC):
pass
class OpenAIPrompt(ILLMPrompt):
def __init__(self):
self.messages = []
@property
def to_list(self):
return [i_message.to_dict for i_message in self.messages]
class ILLMPromptBuilder(ABC):
@abstractmethod
def __init__(self, prompt: ILLMPrompt):
pass
@abstractmethod
def add_message(self, message: ILLMMessage):
pass
class OpenAIPromptBuilder(ILLMPromptBuilder):
def __init__(self, prompt: OpenAIPrompt):
self.prompt = prompt
def add_message(self, message: OpenAIMessage):
self.prompt.messages.append(message)
class ILLMClient(ABC):
@abstractmethod
def get_response(self, prompt: ILLMPrompt):
pass
class OpenAIClient(ILLMClient):
content_class = OpenAIContent
content_builder_class = OpenAIContentBuilder
message_class = OpenAIMessage
message_builder_class = OpenAIMessageBuilder
prompt_class = OpenAIPrompt
prompt_builder_class = OpenAIPromptBuilder
def __init__(self):
self.model = 'gpt-4.1-mini'
self.client = OpenAI()
def get_response(self, prompt: OpenAIPrompt):
response_input = {"model": self.model, "input": prompt.to_list}
return self.client.responses.create(**response_input)
class IMessageAdapter(ABC):
@abstractmethod
def to_message(self) -> ILLMMessage:
pass
class OpenAIResponseAdapter(IMessageAdapter):
def __init__(self, response):
self.response = response
self.response_output = response.output[0]
def to_message(self) -> OpenAIMessage:
content = OpenAIContent()
content_builder = OpenAIContentBuilder(content)
message = OpenAIMessage()
message_builder = OpenAIMessageBuilder(message)
message_builder.set_role(self.response_output.role)\
.set_content(content)
for i_content_item in self.response_output.content:
item = i_content_item.to_dict()
content_builder.add_from_dict(item)
return message
class LMMClientType(Enum):
OPENAI = OpenAIClient
class MessengerClientMessageAdapter(IMessageAdapter):
def __init__(self, message: ClientMessage, llm_client: LMMClientType):
self.message = message
self.llm_client = llm_client
def to_message(self) -> ILLMMessage:
content = self.llm_client.value.content_class()
content_builder = self.llm_client.value.content_builder_class(content)
message = self.llm_client.value.message_class()
message_builder = self.llm_client.value.message_builder_class(message)
message_builder.set_role("user")\
.set_content(content)
if self.message:
content_builder.add_input_text(self.message.content)
for i_image_url in self.message.image_urls:
content_builder.add_input_image(i_image_url)
return message
if __name__ == "__main__":
pass
@@ -1,71 +0,0 @@
from dataclasses import dataclass, field
from abc import ABC
# region Content classes
@dataclass(frozen=True)
class Content(ABC):
type: str = field(init=False, default='')
@dataclass(frozen=True)
class InputText(Content):
type: str = field(init=False, default='input_text')
text: str
@dataclass(frozen=True)
class OutputText(Content):
type: str = field(init=False, default='output_text')
text: str
@dataclass(frozen=True)
class InputImage(Content):
type: str = field(init=False, default='input_image')
image_url: str | None = field(default=None)
file_id: str | None = field(default=None)
def __post_init__(self):
if self.image_url is None and self.file_id is None:
raise ValueError('Either `image_url` or `file_id` must be provided.')
@dataclass(frozen=True)
class UrlCitation(Content):
type: str = field(init=False, default='url_citation')
# To be done
@dataclass(frozen=True)
class FunctionCall(Content):
type: str = field(init=False, default='function_call')
id: str
call_id: str
name: str
arguments: dict
@dataclass(frozen=True)
class FunctionCallOutput(Content):
type: str = field(init=False, default='function_call_output')
call_id: str
output: str
# endregion Content classes
@dataclass(frozen=True)
class Message:
role: str
content: list[Content]
@dataclass(frozen=True)
class Prompt:
model: str
input: list[Message]
@dataclass(frozen=True)
class Response:
output: list[Message]
@@ -1,65 +0,0 @@
from dataclasses import dataclass, field
from datetime import datetime
# region dataclasses
@dataclass(frozen=True)
class Attachment:
content_type: str
filename: str
id: int
size: int
url: str
ephemeral: bool = False
@dataclass(frozen=True)
class Channel:
created_at: datetime
id: int
name: str
def __str__(self) -> str:
return self.name
@dataclass(frozen=True)
class Role:
id: int
name: str
def __str__(self) -> str:
return self.name
@dataclass(frozen=True)
class Member:
bot: bool
created_at: datetime
display_name: str # For regular users this is just their global name or their username, but if they have a guild specific nickname then that is returned instead.
global_name: str # The user's global nickname, taking precedence over the username in display.
id: int
joined_at: datetime
mention: str
name: str # The user's username.
nick: str # The guild specific nickname of the user. Takes precedence over the global name.
roles: list[Role] = field(default_factory=list)
def __str__(self) -> str:
return self.name
@dataclass(frozen=True)
class Message:
author: Member
channel: Channel
created_at: datetime
id: int
attachments: list[Attachment] = field(default_factory=list)
channel_mentions: list[Channel] = field(default_factory=list)
content: str = ''
mentions: list[Member] = field(default_factory=list)
if __name__ == '__main__':
pass
@@ -1,77 +0,0 @@
from abc import ABC, abstractmethod
from enum import Enum
from discord import Message, Attachment
class IClientMessage(ABC):
@abstractmethod
def __init__(self):
pass
class ClientMessage(IClientMessage):
def __init__(self):
self.author_name: str = ''
self.content: str = ''
self.image_urls: list = []
class IMessageBuilder(ABC):
@abstractmethod
def __init__(self):
pass
@abstractmethod
def set_content(self, value: str):
pass
@abstractmethod
def set_author_name(self, value: str):
pass
@abstractmethod
def set_image_urls(self, value: list[str]):
pass
class DiscordMessageBuilder(IMessageBuilder):
def __init__(self, message: IClientMessage):
self.message = message
def set_author_name(self, value: str):
self.message.author_name = value
return self
def set_content(self, value: str):
self.message.content = value
return self
def set_image_urls(self, value: list[str]):
self.message.image_urls = value
return self
class IMessageAdapter(ABC):
@abstractmethod
def create_message(self, message: object) -> IClientMessage:
pass
@staticmethod
def validate_image_urls(urls_list: list[Attachment]) -> list[str]:
supported_image_formats = ('image/jpeg', 'image/png', 'image/webp', 'image/gif')
return [i_attachment.url for i_attachment in urls_list if i_attachment.content_type in supported_image_formats]
class DiscordMessageAdapter(IMessageAdapter):
def create_message(self, message: Message) -> IClientMessage:
client_message = ClientMessage()
message_builder = DiscordMessageBuilder(client_message)
urls_list = self.validate_image_urls(message.attachments)
message_builder.set_content(message.content)\
.set_author_name(message.author.name)\
.set_image_urls(urls_list)
return client_message
if __name__ == "__main__":
pass
-39
View File
@@ -1,39 +0,0 @@
import asyncio
from discord import Message
from reginaldCog.messenger_clients.messenger_client import ClientMessage, DiscordMessageAdapter as MessengerDiscordAdapter
from reginaldCog.llm_clients.llm_client import LMMClientType, MessengerClientMessageAdapter
class MessageService:
def __init__(self, message: Message, llm_client: LMMClientType = LMMClientType.OPENAI):
self.message = message
self.llm_client = llm_client
async def get_llm_response(self) -> str:
# Adapt discord.Message to ClientMessage domain object
client_message: ClientMessage = MessengerDiscordAdapter().create_message(self.message)
# Create prompt and prompt builder for this LLM client
prompt = self.llm_client.value.prompt_class()
prompt_builder = self.llm_client.value.prompt_builder_class(prompt)
# Adapt the messenger client message into LLM message and add it to prompt
llm_message = MessengerClientMessageAdapter(client_message, self.llm_client).to_message()
prompt_builder.add_message(llm_message)
# Call the LLM client; run in executor if method is blocking sync
llm_client_instance = self.llm_client.value()
loop = asyncio.get_running_loop()
# Assuming get_response is blocking - run in executor:
response = await loop.run_in_executor(None, llm_client_instance.get_response, prompt)
# Extract plain text from the response (assuming OpenAIResponseAdapter is present)
from reginaldCog.llm_clients.llm_client import OpenAIResponseAdapter
response_adapter = OpenAIResponseAdapter(response)
message_obj = response_adapter.to_message()
# Concatenate all textual outputs for sending to Discord
texts = [item.get("text", "") for item in message_obj.content.content_items if item.get("type") == "output_text"]
return "\n".join(texts) if texts else "Sorry, no response generated."
+20 -20
View File
@@ -16,6 +16,7 @@ from .weather import time_now, get_current_weather, get_weather_forecast
from .tools_description import TOOLS
from .debug_stuff import debug
CALLABLE_FUNCTIONS = {
# Dictionary with functions to call.
# You can use globals()[func_name](**args) instead, but that's too implicit.
@@ -79,6 +80,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
if not (await self.is_admin(message) or await self.has_access(message.author)):
return # Ignore message if user has no permissions
guild = message.guild
channel_id = str(message.channel.id)
user_id = str(message.author.id)
@@ -143,10 +145,10 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
"content": f"[{summary['timestamp']}] Topics: {', '.join(summary['topics'])}\n{summary['summary']}"
})
formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in
memory]
formatted_messages += [{"role": "user", "content": f"{entry['user']}: {entry['content']}"} for entry in memory]
formatted_messages.append({"role": "user", "content": f"{user_name}: {prompt}"})
##################################################
# #
## Generate AI Response, put into response_text ##
@@ -164,8 +166,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
memory.append({"user": "Reginald", "content": response_text})
if len(memory) > self.short_term_memory_limit:
summary = await self.summarize_memory(message, memory[:int(
self.short_term_memory_limit * self.summary_retention_ratio)])
summary = await self.summarize_memory(message, memory[:int(self.short_term_memory_limit * self.summary_retention_ratio)])
mid_memory.setdefault(channel_id, []).append({
"timestamp": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
"topics": self.extract_topics_from_summary(summary),
@@ -173,13 +174,13 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
})
if len(mid_memory[channel_id]) > self.summary_retention_limit:
mid_memory[channel_id].pop(0)
memory = memory[-(self.short_term_memory_limit - int(
self.short_term_memory_limit * self.summary_retention_ratio)):]
memory = memory[-(self.short_term_memory_limit - int(self.short_term_memory_limit * self.summary_retention_ratio)):]
short_memory[channel_id] = memory
await self.send_split_message(message.channel, response_text)
def should_reginald_interject(self, message_content: str) -> bool:
"""Determines if Reginald should respond to a message based on keywords."""
direct_invocation = {
@@ -260,8 +261,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
await self.config.guild(ctx.guild).openai_api_key.set(api_key)
await ctx.send("OpenAI API key set successfully.")
@commands.command(name="reginald_set_listening_channel",
help="Set the channel where Reginald listens for messages.")
@commands.command(name="reginald_set_listening_channel", help="Set the channel where Reginald listens for messages.")
@commands.has_permissions(administrator=True)
async def set_listening_channel(self, ctx, channel: discord.TextChannel):
"""Sets the channel where Reginald will listen for passive responses."""
@@ -273,8 +273,7 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
await self.config.guild(ctx.guild).listening_channel.set(channel.id)
await ctx.send(f"✅ Reginald will now listen only in {channel.mention}.")
@commands.command(name="reginald_get_listening_channel",
help="Check which channel Reginald is currently listening in.")
@commands.command(name="reginald_get_listening_channel", help="Check which channel Reginald is currently listening in.")
@commands.has_permissions(administrator=True)
async def get_listening_channel(self, ctx):
"""Displays the current listening channel."""
@@ -289,16 +288,17 @@ class ReginaldCog(PermissionsMixin, BlacklistMixin, MemoryMixin, commands.Cog):
else:
await ctx.send("❌ No listening channel has been set.")
# async def send_long_message(self, ctx, message, prefix: str = ""):
# """Splits and sends a long message to avoid Discord's 2000-character limit."""
# chunk_size = 1900 # Leave some space for formatting
# if prefix:
# prefix_length = len(prefix)
# chunk_size -= prefix_length
#
# for i in range(0, len(message), chunk_size):
# chunk = message[i:i + chunk_size]
# await ctx.send(f"{prefix}{chunk}")
async def send_long_message(self, ctx, message, prefix: str = ""):
"""Splits and sends a long message to avoid Discord's 2000-character limit."""
chunk_size = 1900 # Leave some space for formatting
if prefix:
prefix_length = len(prefix)
chunk_size -= prefix_length
for i in range(0, len(message), chunk_size):
chunk = message[i:i + chunk_size]
await ctx.send(f"{prefix}{chunk}")
async def send_split_message(self, ctx, content: str, prefix: str = ""):
"""