380 lines
14 KiB
Python
380 lines
14 KiB
Python
# Copyright (c) 2021 - Jojo#7791
|
|
# Licensed under MIT
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import enum
|
|
import logging
|
|
import random
|
|
import re
|
|
try:
|
|
from datetime import datetime, UTC as DatetimeUTC
|
|
def get_datetime():
|
|
return datetime.now(DatetimeUTC)
|
|
except ImportError:
|
|
from datetime import datetime
|
|
def get_datetime():
|
|
return datetime.utcnow()
|
|
|
|
from typing import Any, Final, List, Dict, Optional, TYPE_CHECKING
|
|
|
|
import discord
|
|
from discord.ext import tasks
|
|
from redbot.core import Config, commands
|
|
from redbot.core.bot import Red
|
|
from redbot.core.utils.chat_formatting import humanize_list, humanize_number, pagify
|
|
from redbot.core.utils.predicates import MessagePredicate
|
|
|
|
from .menus import Menu, Page, PositiveInt
|
|
|
|
log = logging.getLogger("red.JojoCogs.cyclestatus")
|
|
_config_structure = {
|
|
"global": {
|
|
"statuses": [],
|
|
"use_help": True,
|
|
"next_iter": 0,
|
|
"toggled": True, # Toggle if the status should be cycled or not
|
|
"random": False,
|
|
"status_type": 0, # int, the value corresponds with a `discord.ActivityType` value
|
|
"status_mode": "online", # str, the value that corresponds with a `discord.Status` value
|
|
},
|
|
}
|
|
|
|
_bot_guild_var: Final[str] = r"{bot_guild_count}"
|
|
_bot_member_var: Final[str] = r"{bot_member_count}"
|
|
_bot_prefix_var: Final[str] = r"{bot_prefix}"
|
|
|
|
|
|
def humanize_enum_vals(e: enum.Enum) -> str:
|
|
return humanize_list(
|
|
list(map(lambda c: f"`{c.name.replace('_', ' ')}`", e)) # type:ignore
|
|
)
|
|
|
|
|
|
class ActivityType(enum.Enum):
|
|
"""Copy of `discord.ActivityType` minus `unknown`"""
|
|
|
|
playing = 0
|
|
listening = 2
|
|
watching = 3
|
|
custom = 4
|
|
competing = 5
|
|
|
|
def __int__(self):
|
|
return self.value
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
ActivityConverter = ActivityType
|
|
else:
|
|
class ActivityConverter(commands.Converter):
|
|
async def convert(self, ctx: commands.Context, arg: str) -> ActivityType:
|
|
arg = arg.lower()
|
|
ret = getattr(ActivityType, arg, None)
|
|
if not ret:
|
|
raise commands.BadArgument(
|
|
f"The argument must be one of the following: {humanize_enum_vals(ActivityType)}"
|
|
)
|
|
return ret
|
|
|
|
|
|
class Status(enum.Enum):
|
|
online = "online"
|
|
idle = "idle"
|
|
do_not_disturb = dnd = "dnd"
|
|
|
|
def __str__(self) -> str:
|
|
return self.value
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
StatusConverter = Status
|
|
else:
|
|
class StatusConverter(commands.Converter):
|
|
async def convert(self, ctx: commands.Context, arg: str) -> Status:
|
|
arg = arg.lower().replace(" ", "_")
|
|
try:
|
|
return Status(arg)
|
|
except ValueError:
|
|
raise commands.BadArgument(
|
|
f"The argument must be one of the following: {humanize_enum_vals(Status)}"
|
|
)
|
|
|
|
|
|
class CycleStatus(commands.Cog):
|
|
"""Automatically change the status of your bot every minute"""
|
|
|
|
__authors__: Final[List[str]] = ["Jojo#7791"]
|
|
# These people have suggested something for this cog!
|
|
__suggesters__: Final[List[str]] = ["ItzXenonUnity | Lou#2369", "StormyGalaxy#1297"]
|
|
__version__: Final[str] = "1.0.16"
|
|
|
|
def __init__(self, bot: Red):
|
|
self.bot = bot
|
|
self.config = Config.get_conf(self, 115849, True)
|
|
self.config.register_global(**_config_structure["global"])
|
|
self.toggled: Optional[bool] = None
|
|
self.random: Optional[bool] = None
|
|
self.last_random: Optional[int] = None
|
|
self.main_task.start()
|
|
|
|
async def cog_load(self) -> None:
|
|
self.toggled = await self.config.toggled()
|
|
self.random = await self.config.random()
|
|
|
|
async def cog_unload(self) -> None:
|
|
self.main_task.cancel()
|
|
|
|
def format_help_for_context(self, ctx: commands.Context) -> str:
|
|
pre = super().format_help_for_context(ctx)
|
|
plural = "s" if len(self.__authors__) > 1 else ""
|
|
return (
|
|
f"{pre}\n"
|
|
f"Author{plural}: `{humanize_list(self.__authors__)}`\n"
|
|
f"Version: `{self.__version__}`\n"
|
|
f"People who have put in suggestions: `{humanize_list(self.__suggesters__)}`"
|
|
)
|
|
|
|
@commands.command(name="cyclestatusversion", aliases=["csversion"], hidden=True)
|
|
async def cycle_status_version(self, ctx: commands.Context):
|
|
"""Get the version of Cycle Status that [botname] is running"""
|
|
await ctx.send(
|
|
f"Cycle Status, Version `{self.__version__}`. Made with :heart: by Jojo#7791"
|
|
)
|
|
|
|
@commands.group(name="cyclestatus", aliases=["cstatus"])
|
|
@commands.is_owner()
|
|
async def status(self, ctx: commands.Context):
|
|
"""Commands working with the status"""
|
|
pass
|
|
|
|
@status.command(name="type")
|
|
async def status_type(self, ctx: commands.Context, status: ActivityConverter):
|
|
"""Change the type of [botname]'s status
|
|
|
|
**Arguments**
|
|
- `status` The status type. Valid types are
|
|
`playing, listening, watching, custom, and competing`
|
|
"""
|
|
await self.config.status_type.set(status.value)
|
|
await ctx.send(f"Done, set the status type to `{status.name}`.")
|
|
|
|
@status.command(name="mode")
|
|
async def status_mode(self, ctx: commands.Context, mode: StatusConverter):
|
|
"""Change [botname]'s status mode
|
|
|
|
**Arguments**
|
|
- `mode` The mode type. Valid types are:
|
|
`online, idle, dnd, and do not disturb`
|
|
"""
|
|
await self.config.status_mode.set(mode.value)
|
|
await ctx.send(f"Done, set the status mode to `{mode.value}`.")
|
|
|
|
@status.command()
|
|
@commands.check(lambda ctx: ctx.cog.random is False) # type:ignore
|
|
async def forcenext(self, ctx: commands.Context):
|
|
"""Force the next status to display on the bot"""
|
|
nl = await self.config.next_iter()
|
|
statuses = await self.config.statuses()
|
|
if not statuses:
|
|
return await ctx.send("There are no statuses")
|
|
if len(statuses) == 1:
|
|
await ctx.tick()
|
|
return await self._status_add(statuses[0], await self.config.use_help())
|
|
try:
|
|
status = statuses[nl]
|
|
except IndexError:
|
|
status = statuses[0]
|
|
nl = 0
|
|
await self.config.next_iter.set(nl + 1 if nl < len(statuses) else 0)
|
|
await self._status_add(status, await self.config.use_help())
|
|
await ctx.tick()
|
|
|
|
@status.command(name="usehelp")
|
|
async def status_set(self, ctx: commands.Context, toggle: Optional[bool] = None):
|
|
"""Change whether the status should have ` | [p]help`
|
|
|
|
**Arguments**
|
|
- `toggle` Whether help should be used or not.
|
|
"""
|
|
if toggle is None:
|
|
msg = f"Added help is {'enabled' if await self.config.use_help() else 'disabled'}"
|
|
return await ctx.send(msg)
|
|
await self.config.use_help.set(toggle)
|
|
await ctx.tick()
|
|
|
|
@status.command(name="add")
|
|
async def status_add(self, ctx: commands.Context, *, status: str):
|
|
"""Add a status to the list
|
|
|
|
Put `{bot_guild_count}` or `{bot_member_count}` in your message to have the user count and guild count of your bot!
|
|
You can also put `{bot_prefix}` in your message to have the bot's prefix be displayed (eg. `{bot_prefix}ping`)
|
|
|
|
**Arguments**
|
|
- `status` The status to add to the cycle.
|
|
"""
|
|
if len(status) > 100:
|
|
return await ctx.send("Statuses cannot be longer than 100 characters.")
|
|
async with self.config.statuses() as s:
|
|
s.append(status)
|
|
await ctx.tick()
|
|
|
|
@status.command(name="remove", aliases=["del", "rm", "delete"])
|
|
async def status_remove(self, ctx: commands.Context, num: Optional[PositiveInt] = None):
|
|
"""Remove a status from the list
|
|
|
|
**Arguments**
|
|
- `num` The index of the status you want to remove.
|
|
"""
|
|
if num is None:
|
|
return await ctx.invoke(self.status_list)
|
|
num -= 1
|
|
async with self.config.statuses() as sts:
|
|
if num >= len(sts):
|
|
return await ctx.send("You don't have that many statuses, silly")
|
|
del sts[num]
|
|
await ctx.tick()
|
|
|
|
@status.command(name="list")
|
|
async def status_list(self, ctx: commands.Context):
|
|
"""List the available statuses"""
|
|
if not (status := await self.config.statuses()):
|
|
return await ctx.send("There are no statuses")
|
|
await self._show_statuses(ctx=ctx, statuses=status)
|
|
|
|
@status.command(name="clear")
|
|
async def status_clear(self, ctx: commands.Context):
|
|
"""Clear all of the statuses"""
|
|
msg = await ctx.send("Would you like to clear all of your statuses? (y/n)")
|
|
pred = MessagePredicate.yes_or_no()
|
|
try:
|
|
await self.bot.wait_for("message", check=pred)
|
|
except asyncio.TimeoutError:
|
|
pass
|
|
await msg.delete()
|
|
if not pred.result:
|
|
return await ctx.send("Okay! I won't remove your statuses")
|
|
|
|
await self.config.statuses.set([])
|
|
await self.bot.change_presence()
|
|
await ctx.tick()
|
|
|
|
@status.command(name="random")
|
|
async def status_random(self, ctx: commands.Context, value: bool):
|
|
"""Have the bot cycle to a random status
|
|
|
|
**Arguments**
|
|
- `value` Whether to have random statuses be enabled or not
|
|
"""
|
|
|
|
if value == self.random:
|
|
enabled = "enabled" if value else "disabled"
|
|
return await ctx.send(f"Random statuses are already {enabled}")
|
|
self.random = value
|
|
await self.config.random.set(value)
|
|
now_no_longer = "now" if value else "no longer"
|
|
await ctx.send(f"Statuses will {now_no_longer} be random")
|
|
|
|
@status.command(name="toggle")
|
|
async def status_toggle(self, ctx: commands.Context, value: Optional[bool]):
|
|
"""Toggle whether the status should be cycled.
|
|
|
|
This is handy for if you want to keep your statuses but don't want them displayed at the moment
|
|
|
|
**Arguments**
|
|
- `value` Whether to toggle cycling statues
|
|
"""
|
|
if value is None:
|
|
await ctx.send(f"Cycling Statuses is {'enabled' if self.toggled else 'disabled'}")
|
|
return
|
|
if value == self.toggled:
|
|
enabled = "enabled" if value else "disabled"
|
|
return await ctx.send(f"Cycling statuses is already {enabled}")
|
|
self.toggled = value
|
|
await self.config.toggled.set(value)
|
|
now_not = "now" if value else "not"
|
|
await ctx.send(f"I will {now_not} cycle statuses")
|
|
|
|
@status.command(name="settings")
|
|
async def status_settings(self, ctx: commands.Context):
|
|
"""Show your current settings for the cycle status cog"""
|
|
settings = {
|
|
"Randomized statuses?": "Enabled" if self.random else "Disabled",
|
|
"Toggled?": "Yes" if self.toggled else "No",
|
|
"Statuses?": f"See `{ctx.clean_prefix}cyclestatus list`",
|
|
"Status Type?": ActivityType(await self.config.status_type()).name,
|
|
}
|
|
title = "Your Cycle Status settings"
|
|
kwargs: Dict[str, Any] = {
|
|
"content": f"**{title}**\n\n" + "\n".join(f"**{k}** {v}" for k, v in settings.items())
|
|
}
|
|
if await ctx.embed_requested():
|
|
embed = discord.Embed(
|
|
title=title, colour=await ctx.embed_colour(), timestamp=get_datetime()
|
|
)
|
|
[embed.add_field(name=k, value=v, inline=False) for k, v in settings.items()]
|
|
kwargs = {"embed": embed}
|
|
await ctx.send(**kwargs)
|
|
|
|
@tasks.loop(minutes=1)
|
|
async def main_task(self):
|
|
if not (statuses := await self.config.statuses()) or not self.toggled:
|
|
return
|
|
if self.random:
|
|
if self.last_random is not None and len(statuses) > 1:
|
|
statuses.pop(self.last_random) # Remove that last picked one
|
|
msg = random.choice(statuses)
|
|
self.last_random = statuses.index(msg)
|
|
else:
|
|
try:
|
|
# So, sometimes this gets larger than the list of the statuses
|
|
# so, if this raises an `IndexError` we need to reset the next iter
|
|
msg = statuses[(nl := await self.config.next_iter())]
|
|
except IndexError:
|
|
nl = 0 # Hard reset
|
|
msg = statuses[0]
|
|
await self._status_add(msg, await self.config.use_help())
|
|
if not self.random:
|
|
nl = 0 if len(statuses) - 1 == nl else nl + 1
|
|
await self.config.next_iter.set(nl)
|
|
|
|
@main_task.before_loop
|
|
async def main_tas_before_loop(self) -> None:
|
|
await self.bot.wait_until_red_ready()
|
|
|
|
async def _num_lists(self, data: List[str]) -> List[str]:
|
|
"""|coro|
|
|
|
|
Return a list of numbered items
|
|
"""
|
|
return [f"{num}. {d}" for num, d in enumerate(data, 1)]
|
|
|
|
async def _show_statuses(self, ctx: commands.Context, statuses: List[str]) -> None:
|
|
source = Page(
|
|
list(pagify("\n".join(await self._num_lists(statuses)), page_length=400)),
|
|
title="Statuses",
|
|
)
|
|
await Menu(source=source, bot=self.bot, ctx=ctx).start()
|
|
|
|
async def red_delete_data_for_user(self, *, requester: str, user_id: int) -> None:
|
|
"""Nothing to delete"""
|
|
return
|
|
|
|
async def _status_add(self, status: str, use_help: bool) -> None:
|
|
status = status.replace(_bot_guild_var, humanize_number(len(self.bot.guilds))).replace(
|
|
_bot_member_var, humanize_number(len(self.bot.users))
|
|
)
|
|
|
|
prefix = (await self.bot.get_valid_prefixes())[0]
|
|
prefix = re.sub(rf"<@!?{self.bot.user.id}>", f"@{self.bot.user.name}", prefix) # type:ignore
|
|
|
|
status = status.replace(_bot_prefix_var, prefix)
|
|
|
|
if use_help:
|
|
status += f" | {prefix}help"
|
|
|
|
# For some reason using `discord.Activity(type=discord.ActivityType.custom)` will result in the bot not changing its status
|
|
# So I'm gonna use this until I figure out something better lmao
|
|
game = discord.Activity(type=status_type, name=status) if (status_type := await self.config.status_type()) != 4 else discord.CustomActivity(name=status)
|
|
await self.bot.change_presence(activity=game, status=await self.config.status_mode())
|