|
1 | 1 | from redbot.core import commands, checks, Config |
2 | 2 | from redbot.core.utils.chat_formatting import * |
3 | 3 | from redbot.core.data_manager import cog_data_path |
| 4 | +from redbot.core.utils.mod import is_mod_or_superior |
4 | 5 | from redbot.core.utils.predicates import MessagePredicate |
5 | 6 | from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, start_adding_reactions |
6 | 7 | from redbot.core.commands.converter import parse_timedelta |
|
10 | 11 | from .model_apis.openai_api import OpenAIModel |
11 | 12 | from .model_apis.api import GeneralAPI |
12 | 13 | from .model_apis.ollama_api import OllamaModel |
13 | | -from .menus import ConfigSelectView, ConfigMenuView |
| 14 | +from .menus import ConfigSelectView, ConfigMenuView, EndChatVote |
14 | 15 | from .rag import RagDatabase, generate_unique_id, get_metadata_format |
15 | 16 | from .prompts import ( |
16 | 17 | CHAT_PROMPT, |
|
24 | 25 | USER_LEARNING_PROMPT, |
25 | 26 | GENERAL_QUERY_PROMPT, |
26 | 27 | PARTIAL_SUMMARY_PROMPT, |
| 28 | + SHUTUP_CHAT_PROMPT, |
| 29 | + END_CHAT_PROMPT, |
27 | 30 | ) |
28 | 31 |
|
29 | 32 | from typing import Literal, List, Union, Dict, Optional, Tuple, Any |
@@ -62,6 +65,7 @@ def __init__(self, bot): |
62 | 65 | "welcomes": True, |
63 | 66 | "learning_blacklist": [], |
64 | 67 | "user_learning_max_time": 0, |
| 68 | + "ignore_channels": [], |
65 | 69 | } |
66 | 70 | self.default_channel = { |
67 | 71 | "autoreply": False, |
@@ -91,7 +95,12 @@ def __init__(self, bot): |
91 | 95 | }, |
92 | 96 | }, |
93 | 97 | "chat_prompt": CHAT_PROMPT, |
94 | | - "other_chat_prompts": {"goodbye": [GOODBYE_PROMPT], "welcome": [WELCOME_PROMPT]}, |
| 98 | + "other_chat_prompts": { |
| 99 | + "goodbye": [GOODBYE_PROMPT], |
| 100 | + "welcome": [WELCOME_PROMPT], |
| 101 | + "end_chat": [END_CHAT_PROMPT], |
| 102 | + "shutup": [SHUTUP_CHAT_PROMPT], |
| 103 | + }, |
95 | 104 | "summarize_prompt": SUMMARIZE_PROMPT, |
96 | 105 | "partial_summary_prompt": PARTIAL_SUMMARY_PROMPT, |
97 | 106 | "tldr_prompt": TLDR_PROMPT, |
@@ -167,6 +176,8 @@ def __init__(self, bot): |
167 | 176 | self.talking_channels: Dict[int, datetime] = {} |
168 | 177 | # maps channel -> last history number of messages objects |
169 | 178 | self.history: Dict[int, List[discord.Message]] = {} |
| 179 | + # channels where end votes are currently running: |
| 180 | + self.end_voting: Dict[int, discord.Message] = {} |
170 | 181 | # when generating for a channel, ignore new messages |
171 | 182 | self.channel_lock: Dict[int, asyncio.Lock] = defaultdict(asyncio.Lock) |
172 | 183 | # user histories for processing user profiles, per guild (user_id, guild_id) |
@@ -1686,6 +1697,28 @@ async def chatbot(self, ctx): |
1686 | 1697 | """ |
1687 | 1698 | pass |
1688 | 1699 |
|
| 1700 | + @chatbot.command(name="ignore") |
| 1701 | + async def chatbot_ignore( |
| 1702 | + self, |
| 1703 | + ctx: commands.Context, |
| 1704 | + *, |
| 1705 | + channel: Union[discord.TextChannel, discord.VoiceChannel, discord.Thread, discord.ForumChannel], |
| 1706 | + ): |
| 1707 | + """ |
| 1708 | + Add or remove a channel from the ignore list |
| 1709 | + The bot will not respond to chat requests in the ignore channel |
| 1710 | + """ |
| 1711 | + text = "" |
| 1712 | + async with self.config.guild(ctx.guild).ignore_channels() as ignore_channels: |
| 1713 | + if channel.id in ignore_channels: |
| 1714 | + ignore_channels.remove(channel.id) |
| 1715 | + text = info(f"{channel.mention} has been removed from the ignore list.") |
| 1716 | + else: |
| 1717 | + ignore_channels.append(channel.id) |
| 1718 | + text = info(f"{channel.mention} has been added to the ignore list.") |
| 1719 | + |
| 1720 | + await ctx.reply(text, mention_author=False, delete_after=30) |
| 1721 | + |
1689 | 1722 | @chatbot.command(name="welcomes") |
1690 | 1723 | async def chatbot_welcome(self, ctx: commands.Context, enable: bool): |
1691 | 1724 | """ |
@@ -2085,6 +2118,130 @@ async def dad_joke(self, ctx: commands.Context): |
2085 | 2118 | start_adding_reactions(msg, QA_EMOJIS) |
2086 | 2119 | self.qa_data[(msg.id, ctx.channel.id)] = data |
2087 | 2120 |
|
| 2121 | + @commands.hybrid_command(name="shutup") |
| 2122 | + @commands.cooldown(1, 60, commands.BucketType.channel) |
| 2123 | + @commands.guild_only() |
| 2124 | + async def shutup(self, ctx: commands.Context, force: Optional[bool] = False): |
| 2125 | + """ |
| 2126 | + Remove the bot from the current conversation. |
| 2127 | + If ran by a mod or higher, the bot instantly leaves. Otherwise a vote is issued for users to vote on letting the bot stay or not. |
| 2128 | +
|
| 2129 | + If force is true, the bot will not reply with a response when leaving. (Mod only) |
| 2130 | + """ |
| 2131 | + channel = ctx.channel |
| 2132 | + if channel.id not in self.talking_channels: |
| 2133 | + await ctx.reply("I ain't even talking here!", mention_author=False, delete_after=30) |
| 2134 | + ctx.command.reset_cooldown(ctx) |
| 2135 | + return |
| 2136 | + global_timeout = await self.config.guild(ctx.guild).timeout() |
| 2137 | + channel_timeout = await self.config.channel_from_id(channel.id).timeout() |
| 2138 | + timeout = channel_timeout if channel_timeout > 0 else global_timeout |
| 2139 | + if await is_mod_or_superior(self.bot, ctx.message): |
| 2140 | + try: |
| 2141 | + del self.talking_channels[channel.id] |
| 2142 | + except: |
| 2143 | + pass |
| 2144 | + |
| 2145 | + if channel.id in self.end_voting: |
| 2146 | + try: |
| 2147 | + await self.end_voting[channel.id].delete() |
| 2148 | + except: |
| 2149 | + pass |
| 2150 | + del self.end_voting[channel.id] |
| 2151 | + |
| 2152 | + if not force: |
| 2153 | + prompt = await self.config.other_chat_prompts() |
| 2154 | + prompt = random.choice(prompt["end_chat"]) |
| 2155 | + should_qa = await self.config.allow_qa() |
| 2156 | + response = await self.chat( |
| 2157 | + ctx.guild, |
| 2158 | + "Create a response.", |
| 2159 | + self.history[channel.id], |
| 2160 | + override_prompt=prompt, |
| 2161 | + return_qa=should_qa, |
| 2162 | + ) |
| 2163 | + if should_qa and isinstance(response, dict): |
| 2164 | + msg = await ctx.send(response["response"]) |
| 2165 | + start_adding_reactions(msg, QA_EMOJIS) |
| 2166 | + response["channel"] = channel.id |
| 2167 | + response["message_id"] = msg.id |
| 2168 | + self.qa_data[(msg.id, channel.id)] = response |
| 2169 | + else: |
| 2170 | + await ctx.send(response) |
| 2171 | + asyncio.create_task(self.cooldown_lock(channel, timeout)) |
| 2172 | + else: |
| 2173 | + asyncio.create_task(self.cooldown_lock(channel, timeout)) |
| 2174 | + return await ctx.tick() |
| 2175 | + else: # vote |
| 2176 | + if channel.id in self.end_voting: |
| 2177 | + await ctx.reply( |
| 2178 | + warning(f"A vote is already occuring: {self.end_voting[channel.id].jump_url}"), |
| 2179 | + mention_author=False, |
| 2180 | + delete_after=15, |
| 2181 | + ) |
| 2182 | + return |
| 2183 | + else: |
| 2184 | + # start vote |
| 2185 | + vote_time = 30 |
| 2186 | + vote_menu = EndChatVote(vote_time) |
| 2187 | + self.end_voting[channel.id] = await ctx.send( |
| 2188 | + f"## A vote has been started to remove {ctx.guild.me.mention} from the conversation.\nVote ends in {humanize_timedelta(seconds=vote_time)}.", |
| 2189 | + view=vote_menu, |
| 2190 | + ) |
| 2191 | + prompt = await self.config.other_chat_prompts() |
| 2192 | + prompt = random.choice(prompt["shutup"]) |
| 2193 | + should_qa = await self.config.allow_qa() |
| 2194 | + response = await self.chat( |
| 2195 | + ctx.guild, |
| 2196 | + "Create a response.", |
| 2197 | + self.history[channel.id], |
| 2198 | + override_prompt=prompt, |
| 2199 | + return_qa=should_qa, |
| 2200 | + ) |
| 2201 | + if should_qa and isinstance(response, dict): |
| 2202 | + msg = await ctx.send(response["response"]) |
| 2203 | + start_adding_reactions(msg, QA_EMOJIS) |
| 2204 | + response["channel"] = channel.id |
| 2205 | + response["message_id"] = msg.id |
| 2206 | + self.qa_data[(msg.id, channel.id)] = response |
| 2207 | + else: |
| 2208 | + await ctx.send(response) |
| 2209 | + await asyncio.sleep(vote_time + 1) |
| 2210 | + vote_menu.end() |
| 2211 | + try: |
| 2212 | + await self.end_voting[channel.id].delete() |
| 2213 | + except: |
| 2214 | + pass |
| 2215 | + finally: |
| 2216 | + del self.end_voting[channel.id] |
| 2217 | + |
| 2218 | + if vote_menu.winner == "yes": |
| 2219 | + try: |
| 2220 | + del self.talking_channels[channel.id] |
| 2221 | + except: |
| 2222 | + pass |
| 2223 | + prompt = await self.config.other_chat_prompts() |
| 2224 | + prompt = random.choice(prompt["end_chat"]) |
| 2225 | + should_qa = await self.config.allow_qa() |
| 2226 | + response = await self.chat( |
| 2227 | + ctx.guild, |
| 2228 | + "Create a response.", |
| 2229 | + self.history[channel.id], |
| 2230 | + override_prompt=prompt, |
| 2231 | + return_qa=should_qa, |
| 2232 | + ) |
| 2233 | + asyncio.create_task(self.cooldown_lock(channel, timeout)) |
| 2234 | + if should_qa and isinstance(response, dict): |
| 2235 | + msg = await ctx.send(response["response"]) |
| 2236 | + start_adding_reactions(msg, QA_EMOJIS) |
| 2237 | + response["channel"] = channel.id |
| 2238 | + response["message_id"] = msg.id |
| 2239 | + self.qa_data[(msg.id, channel.id)] = response |
| 2240 | + else: |
| 2241 | + return await ctx.send(response) |
| 2242 | + else: |
| 2243 | + return await ctx.send("Can't get rid of me that easily!") |
| 2244 | + |
2088 | 2245 | @commands.hybrid_command(name="summary") |
2089 | 2246 | @checks.mod_or_permissions(administrator=True) |
2090 | 2247 | @commands.guild_only() |
@@ -2346,6 +2503,10 @@ async def on_message(self, message: discord.Message): |
2346 | 2503 | ) |
2347 | 2504 | ) |
2348 | 2505 |
|
| 2506 | + ignore_channels = await self.config.guild(guild).ignore_channels() |
| 2507 | + if channel.id in ignore_channels: |
| 2508 | + return |
| 2509 | + |
2349 | 2510 | lock = self.channel_lock[channel.id] |
2350 | 2511 | # if the lock is already taken, end after updating history |
2351 | 2512 | if lock.locked(): |
|
0 commit comments