我的机器人基于经常更新的数据库。 其中一个团队的机器人提供了课程主题的选择(取自表格)。它似乎有效,但如果在机器人运行时对表进行更改,那么在不重新启动它的情况下,将不会出现新的选择选项
命令:
@app_commands.command()
@app_commands.default_permissions(administrator=True)
@app_commands.choices(lesson=lesson_choices())
async def create_or_update_mark(self, interaction: Interaction,
student: discord.Member,
lesson: Choice[str],
logiks: app_commands.Range[int, 0, 8]):
with MarkRepository(student_id=student.id, lesson_title=lesson.value) as lr:
lr.create_or_update(mark_dto=MarkCreateDTO(logiks=logiks))
await interaction.response.send_message("logiks has been changed", ephemeral=True)
选择列表取自函数
lesson_choices
def lesson_choices() -> list[Choice[str]]:
return [
Choice(name=lesson.title, value=lesson.title)
for lesson in LessonRepository.get_all_lessons()
]
看起来很明显装饰器决定了运行代码时的选择,但这不适合我。有什么方法可以帮助更改选择选项而不重新启动机器人吗?
我想使用
autocomplete
,但我什至没有基本的选项。也许我没有正确实现它
自动完成命令
@app_commands.command()
@app_commands.default_permissions(administrator=True)
@app_commands.autocomplete(lesson=lesson_autocomplete)
async def create_or_update_mark(self, interaction: Interaction,
student: discord.Member,
lesson: str,
logiks: app_commands.Range[int, 0, 8]):
with MarkRepository(student_id=student.id, lesson_title=lesson) as lr:
lr.create_or_update(mark_dto=MarkCreateDTO(logiks=logiks))
await interaction.response.send_message("logiks has been changed", ephemeral=True)
lesson_autocomplete
:
async def lesson_autocomplete(interaction: Interaction, current: str) -> list[app_commands.Choice[str]]:
lessons = [lesson_dto.title for lesson_dto in LessonRepository.get_all_lessons()]
print(lessons)
return [
app_commands.Choice(name=lesson, value=lesson)
for lesson in lessons if current.lower() in lesson.lower()
]
我发现您想要显示一个取决于数据库当前状态的动态选项列表,并且您在显示它们时遇到了一些问题。这是我注意到的,也是我建议您这样做的:
for lesson in lessons if current.lower() in lesson.lower()
-> 这意味着自动完成功能仅在用户完整输入课程标题时才会显示某些内容,而不会根据他们当前输入的内容建议课程。LessonRepository.get_all_lessons()
-> 每次用户键入新字符(或删除一个字符)时都会调用自动完成方法,因此该方法每秒被调用多次。您希望将其移至缓存,以便不需要进行任何此类调用。这很重要,因为您只有 3 秒钟的时间来响应自动完成的交互。在将我们的逻辑移至Transformer(处理用于转换参数并提供自动完成支持的复杂逻辑)时进行这些建议的更改,将导致我们的代码发生以下更改:
from typing import TYPE_CHECKING, Dict, List, Any, Optional, TypeAlias, Union
import discord
from discord.ext import commands
from discord import app_commands
import difflib
GUILD_ID: TypeAlias = int
MEMBER_ID: TypeAlias = int
LESSON_ID: TypeAlias = int
# Some class definition that encapsulates the information from your database.
class Lesson:
id: int
title: str
# ... some other data here
class MyBot(commands.Bot):
if TYPE_CHECKING:
some_function_for_loading_the_lessons_cache: Any
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
# We want to keep track of all the lessons your bot is
# holding here for each student in each guild. To achieve this, let's store a nested dictionary of
# {guild_id: {student_id: {lesson_name: lesson_information}}}. Of course, you can change this to your needs!
self.lessons_cache: Dict[GUILD_ID, Dict[MEMBER_ID, Dict[LESSON_ID, Lesson]]] = {}
async def setup_hook(self):
# This async function is called once during your bots
# startup, and can be used to load your cache and sync your application commands.
await self.tree.sync()
await self.some_function_for_loading_the_lessons_cache()
class LessonTransformer(app_commands.Transformer):
async def find_similar_lesson_titles(self, lessons: Dict[LESSON_ID, Lesson], title: str) -> Dict[LESSON_ID, Lesson]:
# We can use difflib to find similar lesson titles, a standard python library utility!
similar = difflib.get_close_matches(title, map(lambda l: l.title, lessons.values()), n=15, cutoff=1)
return {lesson.id: lesson for lesson in lessons.values() if lesson.title in similar}
async def autocomplete(self, interaction: discord.Interaction[MyBot], value: str, /) -> List[app_commands.Choice[str]]:
# Prerequisite: We know that this command can ONLY be invoked in a server (guild)
assert interaction.guild is not None
# Check if the invoker of this command has already filled out the "student" argument of this command,
# if they have, then we can use that information to filter to all lessons from only that student
student: Optional[discord.Member] = interaction.namespace.get('student')
if student is None:
# This person skipped the student argument before going to the lesson argument, so we can only show them
# so much.
lessons: Dict[MEMBER_ID, Dict[LESSON_ID, Lesson]] = interaction.client.lessons_cache.get(
interaction.guild.id, {}
)
# Flatten this dict into just a list of lessons
flat_lessons: Dict[LESSON_ID, Lesson] = {
s_lesson.id: s_lesson for student_lessons in lessons.values() for s_lesson in student_lessons.values()
}
similar_lessons = await self.find_similar_lesson_titles(flat_lessons, value)
else:
# We can get more specific!
student_lessons: Dict[LESSON_ID, Lesson] = interaction.client.lessons_cache.get(interaction.guild.id, {}).get(
student.id, {}
)
similar_lessons = await self.find_similar_lesson_titles(student_lessons, value)
return [
app_commands.Choice(name=lesson.title, value=str(lesson_id)) for lesson_id, lesson in similar_lessons.items()
]
async def transform(self, interaction: discord.Interaction[MyBot], value: str, /) -> Union[Lesson, LESSON_ID]:
# Prerequisite: We know that this command can ONLY be invoked in a server (guild)
assert interaction.guild is not None
# It should be noted that autocompletes are mere SUGGESTIONS, we need to make sure the user entered okay data.
# Find the lesson from the cache, where value should be a lesson id
if not value.isdigit():
raise Exception("Invalid lesson id") # Can raise better exception here
lesson_id = int(value)
student: Optional[discord.Member] = interaction.namespace.get('student')
if student is None:
return lesson_id # We will check that this lesson ID is to the actual student later
# We can get more specific!
lesson = interaction.client.lessons_cache.get(interaction.guild.id, {}).get(student.id, {}).get(lesson_id)
if lesson is None:
raise Exception("Invalid lesson id")
return lesson
class SomeCog(commands.Cog):
@app_commands.command()
@app_commands.guild_only()
@app_commands.default_permissions(administrator=True)
async def create_or_update_mark(
self,
interaction: discord.Interaction[MyBot],
student: discord.Member,
lesson: app_commands.Transform[Union[Lesson, LESSON_ID], LessonTransformer],
logiks: app_commands.Range[int, 0, 8],
):
# Prerequisite: We know that this command can ONLY be invoked in a server (guild)
assert interaction.guild is not None
reveal_type(lesson) # Type of "lesson" is "Lesson | int"
if isinstance(lesson, int):
# This is a lesson ID, but we need to check that it belongs to the student (or complain)
potential_lesson = interaction.client.lessons_cache.get(interaction.guild.id, {}).get(student.id, {}).get(lesson)
if potential_lesson is None:
raise Exception("Invalid lesson id")
lesson = potential_lesson
reveal_type(lesson) # Type of "lesson" is "Lesson"
# ... perform our operations with the lesson object here
所以,回顾一下这个潜在的例子:
Lesson
类型,表示存储在我们数据库中的数据。通过这些更改,您的问题将得到解决。请注意,我尚未测试此代码,它只是“如何”重构您的设置的示例。如果您有任何疑问,请随时提问。另外,我强烈建议您查看 discord.py Discord Server,它有帮助渠道以加快响应速度。