mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 17:32:31 -05:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
501aff41ea | ||
|
|
449b1bfe9e | ||
|
|
4a8358ecb4 | ||
|
|
8f74e4dd31 | ||
|
|
2b35d9f012 | ||
|
|
35001107e0 | ||
|
|
a7d7b90ae8 | ||
|
|
119ba7ef8b | ||
|
|
28bbe9c646 | ||
|
|
8739c04024 | ||
|
|
57240d25b9 | ||
|
|
15ea5440a3 | ||
|
|
1e60d1c265 | ||
|
|
b7cd097c43 | ||
|
|
6c934b02e6 | ||
|
|
fcb9b40b43 | ||
|
|
7a6884e4b1 | ||
|
|
e86698cfeb | ||
|
|
53650aefa6 | ||
|
|
1d80a0cad1 | ||
|
|
f6d27a0f43 | ||
|
|
f71aa9dd21 | ||
|
|
1cb5394e96 | ||
|
|
2b2dbd25f7 | ||
|
|
dd4cd0eeb1 | ||
|
|
ee7b0cf730 | ||
|
|
95ef5d6348 | ||
|
|
23192b9ef6 | ||
|
|
7cd98c8a63 | ||
|
|
fca7686701 | ||
|
|
be767478f4 | ||
|
|
b3ad5d90ed | ||
|
|
fb093b7411 | ||
|
|
e4ea3110e3 | ||
|
|
79676c4f72 | ||
|
|
d61827b92c | ||
|
|
1f1f46c70f | ||
|
|
9188e4a7ec | ||
|
|
e5a780eb0c | ||
|
|
d8c85a2b15 | ||
|
|
83080bc5a2 | ||
|
|
233bfc59ac | ||
|
|
c606caf3a3 |
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Command bugs
|
||||
|
||||
<!--
|
||||
Did you find a bug with a command? Fill out the following:
|
||||
-->
|
||||
|
||||
#### Command name
|
||||
|
||||
<!-- Replace this line with the name of the command -->
|
||||
|
||||
#### What cog is this command from?
|
||||
|
||||
<!-- Replace this line with the name of the cog -->
|
||||
|
||||
#### What were you expecting to happen?
|
||||
|
||||
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||
|
||||
#### What actually happened?
|
||||
|
||||
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||
|
||||
#### How can we reproduce this issue?
|
||||
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Feature request
|
||||
|
||||
<!-- This template is for feature requests. Please fill out the following: -->
|
||||
|
||||
|
||||
#### Select the type of feature you are requesting:
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] Cog
|
||||
- [ ] Command
|
||||
- [ ] API functionality
|
||||
|
||||
#### Describe your requested feature
|
||||
|
||||
<!--
|
||||
Feel free to describe in as much detail as you wish.
|
||||
|
||||
If you are requesting a cog to be included in core:
|
||||
- Describe the functionality in as much detail as possible
|
||||
- Include the command structure, if possible
|
||||
- Please note that unless it's something that should be core functionality,
|
||||
we reserve the right to reject your suggestion and point you to our cog
|
||||
board to request it for a third-party cog
|
||||
|
||||
If you are requesting a command:
|
||||
- Include what cog it should be in and a name for the command
|
||||
- Describe the intended functionality for the command
|
||||
- Note any restrictions on who can use the command or where it can be used
|
||||
|
||||
If you are requesting API functionality:
|
||||
- Describe what it should do
|
||||
- Note whether it is to extend existing functionality or introduce new functionality
|
||||
|
||||
-->
|
||||
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Other bugs
|
||||
|
||||
<!--
|
||||
Did you find a bug with something other than a command? Fill out the following:
|
||||
-->
|
||||
|
||||
#### What were you trying to do?
|
||||
|
||||
<!-- Replace this line with a description of what you were trying to do -->
|
||||
|
||||
#### What were you expecting to happen?
|
||||
|
||||
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||
|
||||
#### What actually happened?
|
||||
|
||||
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||
|
||||
#### How can we reproduce this issue?
|
||||
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Bugfix request
|
||||
|
||||
<!--
|
||||
To be used for pull requests that fix a bug
|
||||
-->
|
||||
|
||||
#### Describe the bug being fixed
|
||||
|
||||
<!--
|
||||
If an issue exists for the bug, mention
|
||||
that this PR fixes that issue
|
||||
-->
|
||||
|
||||
#### Anything we need to know about this fix?
|
||||
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Enhancement request
|
||||
|
||||
<!--
|
||||
To be used for PRs which enhance existing features
|
||||
-->
|
||||
|
||||
#### Describe the enhancement
|
||||
|
||||
<!--
|
||||
Describe what your changes do.
|
||||
If adding commands, describe any restrictions on their usage.
|
||||
- For example, who can use the command? Where can it be used?
|
||||
-->
|
||||
|
||||
#### Does this enhancement break existing functionality?
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# New feature addition
|
||||
|
||||
<!--
|
||||
To be used for PRs which add a new feature
|
||||
Examples of this include new APIs, new core cogs, etc.
|
||||
-->
|
||||
|
||||
#### What type of feature is this?
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] New core cog
|
||||
- [ ] New API
|
||||
- [ ] Other
|
||||
|
||||
#### Describe the feature
|
||||
|
||||
<!--
|
||||
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
|
||||
If the new feature introduces new requirements, please try to explain why they are necessary.
|
||||
-->
|
||||
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# New release
|
||||
|
||||
<!--
|
||||
To be used by collaborators for doing releases.
|
||||
Most contributors will not need to use this.
|
||||
-->
|
||||
|
||||
#### Version
|
||||
|
||||
|
||||
|
||||
#### Has a draft release been created for this?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Translations update
|
||||
|
||||
<!--
|
||||
Used for PRs updating translations from Crowdin
|
||||
-->
|
||||
@@ -37,6 +37,8 @@ Save and exit :code:`ctrl + O; enter; ctrl + x`
|
||||
Starting and enabling the service
|
||||
---------------------------
|
||||
|
||||
.. note:: This same file can be used to start as many instances of the bot as you wish, without creating more service files, just start and enable more services and add any bot instance name after the **@**
|
||||
|
||||
To start the bot, run the service and add the instance name after the **@**:
|
||||
|
||||
:code:`sudo systemctl start red@instancename`
|
||||
@@ -45,4 +47,6 @@ To set the bot to start on boot, you must enable the service, again adding the i
|
||||
|
||||
:code:`sudo systemctl enable red@instancename`
|
||||
|
||||
.. note:: This same file can be used to start as many instances of the bot as you wish, without creating more service files, just start and enable more services and add any bot instance name after the **@**
|
||||
To view Red’s log, you can acccess through journalctl:
|
||||
|
||||
:code:`sudo journalctl -u red@instancename`
|
||||
|
||||
21
docs/framework_commands.rst
Normal file
21
docs/framework_commands.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
.. red commands module documentation
|
||||
|
||||
================
|
||||
Commands Package
|
||||
================
|
||||
|
||||
This package acts almost identically to ``discord.ext.commands``; i.e. they both have the same
|
||||
attributes. Some of these attributes, however, have been slightly modified, as outlined below.
|
||||
|
||||
.. autofunction:: redbot.core.commands.command
|
||||
|
||||
.. autofunction:: redbot.core.commands.group
|
||||
|
||||
.. autoclass:: redbot.core.commands.Command
|
||||
:members:
|
||||
|
||||
.. autoclass:: redbot.core.commands.Group
|
||||
:members:
|
||||
|
||||
.. autoclass:: redbot.core.commands.Context
|
||||
:members:
|
||||
@@ -29,7 +29,7 @@ Basic Usage
|
||||
|
||||
@commands.command()
|
||||
async def return_some_data(self, ctx):
|
||||
await ctx.send(await config.foo())
|
||||
await ctx.send(await self.config.foo())
|
||||
|
||||
********
|
||||
Tutorial
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
.. red invocation context documentation
|
||||
|
||||
==========================
|
||||
Command Invocation Context
|
||||
==========================
|
||||
|
||||
.. automodule:: redbot.core.context
|
||||
|
||||
.. autoclass:: redbot.core.RedContext
|
||||
:members:
|
||||
@@ -13,11 +13,12 @@ Basic Usage
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from discord.ext import commands
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
_ = CogI18n("ExampleCog", __file__)
|
||||
_ = Translator("ExampleCog", __file__)
|
||||
|
||||
@cog_i18n(_)
|
||||
class ExampleCog:
|
||||
"""description"""
|
||||
|
||||
@@ -39,16 +40,19 @@ In a command prompt in your cog's package (where yourcog.py is),
|
||||
create a directory called "locales".
|
||||
Then do one of the following:
|
||||
|
||||
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -n -p locales`
|
||||
Windows: :code:`python <your python install path>\Tools\i18n\pygettext.py -D -n -p locales`
|
||||
|
||||
Mac: ?
|
||||
|
||||
Linux: :code:`pygettext3 -n -p locales`
|
||||
Linux: :code:`pygettext3 -D -n -p locales`
|
||||
|
||||
This will generate a messages.pot file with strings to be translated
|
||||
This will generate a messages.pot file with strings to be translated, including
|
||||
docstrings.
|
||||
|
||||
-------------
|
||||
API Reference
|
||||
-------------
|
||||
|
||||
.. automodule:: redbot.core.i18n
|
||||
.. automodule:: redbot.core.i18n
|
||||
:members:
|
||||
:special-members: __call__
|
||||
|
||||
@@ -16,6 +16,12 @@ Embed Helpers
|
||||
.. automodule:: redbot.core.utils.embed
|
||||
:members:
|
||||
|
||||
Menu Helpers
|
||||
============
|
||||
|
||||
.. automodule:: redbot.core.utils.menus
|
||||
:members:
|
||||
|
||||
Mod Helpers
|
||||
===========
|
||||
|
||||
|
||||
@@ -90,6 +90,6 @@ have successfully created a cog!
|
||||
Additional resources
|
||||
--------------------
|
||||
|
||||
Be sure to check out the `migration guide </guide_migration>`_ for some resources
|
||||
Be sure to check out the :doc:`/guide_migration` for some resources
|
||||
on developing cogs for V3. This will also cover differences between V2 and V3 for
|
||||
those who developed cogs for V2.
|
||||
|
||||
@@ -37,12 +37,12 @@ Welcome to Red - Discord Bot's documentation!
|
||||
framework_bot
|
||||
framework_cogmanager
|
||||
framework_config
|
||||
framework_context
|
||||
framework_datamanager
|
||||
framework_downloader
|
||||
framework_events
|
||||
framework_i18n
|
||||
framework_modlog
|
||||
framework_commands
|
||||
framework_rpc
|
||||
framework_utils
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Installing the pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo pacman -Sy python-pip git base-devel jre8-openjdk
|
||||
sudo pacman -Syu python-pip git base-devel jre8-openjdk
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
|
||||
@@ -10,10 +10,6 @@ Needed Software
|
||||
|
||||
* `Python <https://python.org/downloads/>`_ - Red needs at least Python 3.5
|
||||
|
||||
.. attention:: Please note that 3.6 has issues on some versions of Windows.
|
||||
If you try using Red with 3.6 and experience issues, uninstall
|
||||
Python 3.6 and install the latest version of Python 3.5
|
||||
|
||||
.. note:: Please make sure that the box to add Python to PATH is CHECKED, otherwise
|
||||
you may run into issues when trying to run Red
|
||||
|
||||
@@ -44,4 +40,4 @@ Installing Red
|
||||
running the bot)
|
||||
|
||||
4. Once done setting up the instance, run :code:`redbot <your instance name>` to run Red.
|
||||
It will walk through the initial setup, asking for your token and a prefix
|
||||
It will walk through the initial setup, asking for your token and a prefix
|
||||
|
||||
@@ -3,17 +3,17 @@ from re import search
|
||||
from typing import Generator, Tuple, Iterable
|
||||
|
||||
import discord
|
||||
from redbot.core import Config
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import Config, commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from .alias_entry import AliasEntry
|
||||
|
||||
_ = CogI18n("Alias", __file__)
|
||||
_ = Translator("Alias", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Alias:
|
||||
"""
|
||||
Alias
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Tuple
|
||||
from discord.ext import commands
|
||||
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
class AliasEntry:
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import datetime
|
||||
import discord
|
||||
import heapq
|
||||
import lavalink
|
||||
import math
|
||||
import re
|
||||
import redbot.core
|
||||
from discord.ext import commands
|
||||
from redbot.core import Config, checks, bank
|
||||
|
||||
from redbot.core import Config, commands, checks, bank
|
||||
from redbot.core.utils.menus import menu, DEFAULT_CONTROLS, prev_page, next_page, close_menu
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from .manager import shutdown_lavalink_server
|
||||
|
||||
__version__ = "0.0.5"
|
||||
_ = Translator("Audio", __file__)
|
||||
|
||||
__version__ = "0.0.6"
|
||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Audio:
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
@@ -46,6 +51,7 @@ class Audio:
|
||||
self.config.register_guild(**default_guild)
|
||||
self.config.register_global(**default_global)
|
||||
self.skip_votes = {}
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
async def init_config(self):
|
||||
host = await self.config.host()
|
||||
@@ -96,10 +102,11 @@ class Audio:
|
||||
await self.bot.change_presence(activity=None)
|
||||
if playing_servers == 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
|
||||
type=discord.ActivityType.listening))
|
||||
type=discord.ActivityType.listening))
|
||||
if playing_servers > 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
await self.bot.change_presence(
|
||||
activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
|
||||
notify_channel = player.fetch('channel')
|
||||
@@ -113,10 +120,11 @@ class Audio:
|
||||
await self.bot.change_presence(activity=None)
|
||||
if playing_servers == 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
|
||||
type=discord.ActivityType.listening))
|
||||
type=discord.ActivityType.listening))
|
||||
if playing_servers > 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
await self.bot.change_presence(
|
||||
activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_EXCEPTION:
|
||||
message_channel = player.fetch('channel')
|
||||
@@ -124,7 +132,7 @@ class Audio:
|
||||
message_channel = self.bot.get_channel(message_channel)
|
||||
embed = discord.Embed(colour=message_channel.guild.me.top_role.colour, title='Track Error',
|
||||
description='{}\n**[{}]({})**'.format(extra, player.current.title,
|
||||
player.current.uri))
|
||||
player.current.uri))
|
||||
embed.set_footer(text='Skipping...')
|
||||
await message_channel.send(embed=embed)
|
||||
await player.skip()
|
||||
@@ -146,6 +154,7 @@ class Audio:
|
||||
|
||||
def check(m):
|
||||
return m.author == ctx.author
|
||||
|
||||
try:
|
||||
dj_role = await ctx.bot.wait_for('message', timeout=15.0, check=check)
|
||||
dj_role_obj = discord.utils.get(ctx.guild.roles, name=dj_role.content)
|
||||
@@ -172,8 +181,6 @@ class Audio:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def jukebox(self, ctx, price: int):
|
||||
"""Set a price for queueing songs for non-mods. 0 to disable."""
|
||||
jukebox = await self.config.guild(ctx.guild).jukebox()
|
||||
jukebox_price = await self.config.guild(ctx.guild).jukebox_price()
|
||||
if price < 0:
|
||||
return await self._embed_msg(ctx, 'Can\'t be less than zero.')
|
||||
if price == 0:
|
||||
@@ -182,7 +189,7 @@ class Audio:
|
||||
else:
|
||||
jukebox = True
|
||||
await self._embed_msg(ctx, 'Track queueing command price set to {} {}.'.format(
|
||||
price, await bank.get_currency_name(ctx.guild)))
|
||||
price, await bank.get_currency_name(ctx.guild)))
|
||||
|
||||
await self.config.guild(ctx.guild).jukebox_price.set(price)
|
||||
await self.config.guild(ctx.guild).jukebox.set(jukebox)
|
||||
@@ -266,10 +273,10 @@ class Audio:
|
||||
connect_dur = self._dynamic_time(int((datetime.datetime.utcnow() - connect_start).total_seconds()))
|
||||
try:
|
||||
server_list.append('{} [`{}`]: **[{}]({})**'.format(p.channel.guild.name, connect_dur,
|
||||
p.current.title, p.current.uri))
|
||||
p.current.title, p.current.uri))
|
||||
except AttributeError:
|
||||
server_list.append('{} [`{}`]: **{}**'.format(p.channel.guild.name, connect_dur,
|
||||
'Nothing playing.'))
|
||||
'Nothing playing.'))
|
||||
if server_num == 0:
|
||||
servers = 'Not connected anywhere.'
|
||||
else:
|
||||
@@ -286,7 +293,7 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to bump a song.')
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author):
|
||||
@@ -298,7 +305,7 @@ class Audio:
|
||||
bump_song = player.queue[bump_index]
|
||||
player.queue.insert(0, bump_song)
|
||||
removed = player.queue.pop(index)
|
||||
await self._embed_msg(ctx, 'Moved **' + removed.title + '** to the top of the queue.')
|
||||
await self._embed_msg(ctx, 'Moved {} to the top of the queue.'.format(removed.title))
|
||||
|
||||
@commands.command(aliases=['dc'])
|
||||
async def disconnect(self, ctx):
|
||||
@@ -363,6 +370,7 @@ class Audio:
|
||||
|
||||
def check(r, u):
|
||||
return r.message.id == message.id and u == ctx.message.author
|
||||
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for('reaction_add', check=check, timeout=10.0)
|
||||
except asyncio.TimeoutError:
|
||||
@@ -390,7 +398,7 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to pause the music.')
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx, ctx.author):
|
||||
@@ -487,11 +495,13 @@ class Audio:
|
||||
player.store('guild', ctx.guild.id)
|
||||
await self._data_check(ctx)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to use the play command.')
|
||||
if not await self._currency_check(ctx, jukebox_price):
|
||||
return
|
||||
|
||||
if not query:
|
||||
return await self._embed_msg(ctx, 'No songs to play.')
|
||||
query = query.strip('<>')
|
||||
if not query.startswith('http'):
|
||||
query = 'ytsearch:{}'.format(query)
|
||||
@@ -510,7 +520,8 @@ class Audio:
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Enqueued',
|
||||
description='Added {} tracks to the queue.'.format(len(tracks)))
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(text='{} until start of playlist playback: starts at #{} in queue'.format(queue_total_duration, before_queue_length))
|
||||
embed.set_footer(text='{} until start of playlist playback: starts at #{} in queue'.format(
|
||||
queue_total_duration, before_queue_length))
|
||||
if not player.current:
|
||||
await player.play()
|
||||
else:
|
||||
@@ -519,7 +530,10 @@ class Audio:
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued',
|
||||
description='**[{}]({})**'.format(single_track.title, single_track.uri))
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, before_queue_length))
|
||||
embed.set_footer(text='{} until track playback: #{} in queue'.format(
|
||||
queue_total_duration, before_queue_length))
|
||||
elif queue_duration > 0:
|
||||
embed.set_footer(text='#{} in queue'.format(len(player.queue) + 1))
|
||||
if not player.current:
|
||||
await player.play()
|
||||
await ctx.send(embed=embed)
|
||||
@@ -531,17 +545,60 @@ class Audio:
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@playlist.command(name='append')
|
||||
async def _playlist_append(self, ctx, playlist_name, *url):
|
||||
"""Add a song URL, playlist link, or quick search to the end of a saved playlist."""
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
try:
|
||||
if (playlists[playlist_name]['author'] != ctx.author.id and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
to_append = await self._playlist_tracks(ctx, player, url)
|
||||
if not to_append:
|
||||
return
|
||||
track_list = playlists[playlist_name]['tracks']
|
||||
if track_list:
|
||||
playlists[playlist_name]['tracks'] = track_list + to_append
|
||||
else:
|
||||
playlists[playlist_name]['tracks'] = to_append
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'No playlist with that name.')
|
||||
if playlists[playlist_name]['playlist_url'] is not None:
|
||||
playlists[playlist_name]['playlist_url'] = None
|
||||
if len(to_append) == 1:
|
||||
track_title = to_append[0]['info']['title']
|
||||
return await self._embed_msg(ctx, '{} appended to {}.'.format(track_title, playlist_name))
|
||||
await self._embed_msg(ctx, '{} tracks appended to {}.'.format(len(to_append), playlist_name))
|
||||
|
||||
@playlist.command(name='create')
|
||||
async def _playlist_create(self, ctx, playlist_name):
|
||||
"""Create an empty playlist."""
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self._embed_msg(ctx, 'You need the DJ role to save playlists.')
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
if playlist_name in playlists:
|
||||
return await self._embed_msg(ctx, 'Playlist name already exists, try again with a different name.')
|
||||
playlist_list = self._to_json(ctx, None, None)
|
||||
playlists[playlist_name] = playlist_list
|
||||
await self._embed_msg(ctx, 'Empty playlist {} created.'.format(playlist_name))
|
||||
|
||||
@playlist.command(name='delete')
|
||||
async def _playlist_delete(self, ctx, playlist_name):
|
||||
"""Delete a saved playlist."""
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
try:
|
||||
if playlists[playlist_name]['author'] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author):
|
||||
if (playlists[playlist_name]['author'] != ctx.author.id and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
|
||||
del playlists[playlist_name]
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'No playlist with that name.')
|
||||
await self._embed_msg(ctx, '{} playlist removed.'.format(playlist_name))
|
||||
await self._embed_msg(ctx, '{} playlist deleted.'.format(playlist_name))
|
||||
|
||||
@playlist.command(name='info')
|
||||
async def _playlist_info(self, ctx, playlist_name):
|
||||
@@ -556,18 +613,15 @@ class Audio:
|
||||
try:
|
||||
track_len = len(playlists[playlist_name]['tracks'])
|
||||
except TypeError:
|
||||
track_len = 1
|
||||
track_len = 0
|
||||
if playlist_url is None:
|
||||
playlist_url = '**Not generated from a URL.**'
|
||||
playlist_url = '**Custom playlist.**'
|
||||
else:
|
||||
playlist_url = 'URL: <{}>'.format(playlist_url)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist info for {}:'.format(playlist_name),
|
||||
description='Author: **{}**\n{}'.format(author_obj,
|
||||
playlist_url))
|
||||
if track_len > 1:
|
||||
embed.set_footer(text='{} tracks'.format(track_len))
|
||||
if track_len == 1:
|
||||
embed.set_footer(text='{} track'.format(track_len))
|
||||
playlist_url))
|
||||
embed.set_footer(text='{} track(s)'.format(track_len))
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@playlist.command(name='list')
|
||||
@@ -597,11 +651,11 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
tracklist = []
|
||||
np_song = self._track_creator(ctx, player, 'np', None)
|
||||
np_song = self._track_creator(player, 'np')
|
||||
tracklist.append(np_song)
|
||||
for track in player.queue:
|
||||
queue_idx = player.queue.index(track)
|
||||
track_obj = self._track_creator(ctx, player, queue_idx, None)
|
||||
track_obj = self._track_creator(player, queue_idx)
|
||||
tracklist.append(track_obj)
|
||||
if not playlist_name:
|
||||
await self._embed_msg(ctx, 'Please enter a name for this playlist.')
|
||||
@@ -616,11 +670,38 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Playlist name already exists, try again with a different name.')
|
||||
except asyncio.TimeoutError:
|
||||
return await self._embed_msg(ctx, 'No playlist name entered, try again later.')
|
||||
|
||||
playlist_list = self._to_json(ctx, None, tracklist, playlist_name)
|
||||
playlist_list = self._to_json(ctx, None, tracklist)
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlists[playlist_name] = playlist_list
|
||||
await self._embed_msg(ctx, 'Playlist {} saved from current queue: {} tracks added.'.format(playlist_name, len(tracklist)))
|
||||
await self._embed_msg(ctx, 'Playlist {} saved from current queue: {} tracks added.'.format(
|
||||
playlist_name, len(tracklist)))
|
||||
|
||||
@playlist.command(name='remove')
|
||||
async def _playlist_remove(self, ctx, playlist_name, url):
|
||||
"""Remove a song from a playlist by url."""
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
try:
|
||||
if (playlists[playlist_name]['author'] != ctx.author.id and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'No playlist with that name.')
|
||||
track_list = playlists[playlist_name]['tracks']
|
||||
clean_list = [track for track in track_list if not url == track['info']['uri']]
|
||||
if len(playlists[playlist_name]['tracks']) == len(clean_list):
|
||||
return await self._embed_msg(ctx, 'URL not in playlist.')
|
||||
del_count = len(playlists[playlist_name]['tracks']) - len(clean_list)
|
||||
if not clean_list:
|
||||
del playlists[playlist_name]
|
||||
return await self._embed_msg(ctx, 'No songs left, removing playlist.')
|
||||
playlists[playlist_name]['tracks'] = clean_list
|
||||
if playlists[playlist_name]['playlist_url'] is not None:
|
||||
playlists[playlist_name]['playlist_url'] = None
|
||||
if del_count > 1:
|
||||
await self._embed_msg(ctx, '{} entries have been removed from the {} playlist.'.format(
|
||||
del_count, playlist_name))
|
||||
else:
|
||||
await self._embed_msg(ctx, 'The track has been removed from the {} playlist.'.format(playlist_name))
|
||||
|
||||
@playlist.command(name='save')
|
||||
async def _playlist_save(self, ctx, playlist_name, playlist_url):
|
||||
@@ -628,18 +709,13 @@ class Audio:
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
tracks = await player.get_tracks(playlist_url)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found.')
|
||||
tracklist = []
|
||||
for track in tracks:
|
||||
track_obj = self._track_creator(ctx, player, None, track)
|
||||
tracklist.append(track_obj)
|
||||
playlist_list = self._to_json(ctx, playlist_url, tracklist, playlist_name)
|
||||
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlists[playlist_name] = playlist_list
|
||||
return await self._embed_msg(ctx, 'Playlist {} saved: {} tracks added.'.format(playlist_name, len(tracks)))
|
||||
tracklist = await self._playlist_tracks(ctx, player, playlist_url)
|
||||
playlist_list = self._to_json(ctx, playlist_url, tracklist)
|
||||
if tracklist is not None:
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlists[playlist_name] = playlist_list
|
||||
return await self._embed_msg(ctx, 'Playlist {} saved: {} tracks added.'.format(
|
||||
playlist_name, len(tracklist)))
|
||||
|
||||
@playlist.command(name='start')
|
||||
async def _playlist_start(self, ctx, playlist_name=None):
|
||||
@@ -647,25 +723,92 @@ class Audio:
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
playlists = await self.config.guild(ctx.guild).playlists.get_raw()
|
||||
try:
|
||||
author_id = playlists[playlist_name]["author"]
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'That playlist doesn\'t exist.')
|
||||
author_obj = self.bot.get_user(author_id)
|
||||
author_obj = self.bot.get_user(ctx.author.id)
|
||||
track_count = 0
|
||||
try:
|
||||
playlist_len = len(playlists[playlist_name]["tracks"])
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
for track in playlists[playlist_name]["tracks"]:
|
||||
player.add(author_obj, lavalink.rest_api.Track(data=track))
|
||||
track_count = track_count + 1
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Enqueued',
|
||||
description='Added {} tracks to the queue.'.format(track_count))
|
||||
description='Added {} tracks to the queue.'.format(track_count))
|
||||
await ctx.send(embed=embed)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
except TypeError:
|
||||
await ctx.invoke(self.play, query=playlists[playlist_name]["playlist_url"])
|
||||
except KeyError:
|
||||
await self._embed_msg(ctx, 'That playlist doesn\'t exist.')
|
||||
|
||||
@checks.is_owner()
|
||||
@playlist.command(name='upload')
|
||||
async def _playlist_upload(self, ctx):
|
||||
"""Convert a Red v2 playlist file to a playlist."""
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
await self._embed_msg(ctx, 'Please upload the playlist file. Any other message will cancel this operation.')
|
||||
|
||||
def check(m):
|
||||
return m.author == ctx.author
|
||||
|
||||
try:
|
||||
file_message = await ctx.bot.wait_for('message', timeout=30.0, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
return await self._embed_msg(ctx, 'No file detected, try again later.')
|
||||
try:
|
||||
file_url = file_message.attachments[0].url
|
||||
except IndexError:
|
||||
return await self._embed_msg(ctx, 'Upload canceled.')
|
||||
v2_playlist_name = (file_url.split('/')[6]).split('.')[0]
|
||||
file_suffix = file_url.rsplit('.', 1)[1]
|
||||
if file_suffix != "txt":
|
||||
return await self._embed_msg(ctx, 'Only playlist files can be uploaded.')
|
||||
async with self.session.request('GET', file_url) as r:
|
||||
v2_playlist = await r.json(content_type='text/plain')
|
||||
try:
|
||||
v2_playlist_url = v2_playlist["link"]
|
||||
except KeyError:
|
||||
v2_playlist_url = None
|
||||
if (not v2_playlist_url or not self._match_yt_playlist(v2_playlist_url) or not
|
||||
await player.get_tracks(v2_playlist_url)):
|
||||
track_list = []
|
||||
track_count = 0
|
||||
async with self.config.guild(ctx.guild).playlists() as v3_playlists:
|
||||
try:
|
||||
if v3_playlists[v2_playlist_name]:
|
||||
return await self._embed_msg(ctx, 'A playlist already exists with this name.')
|
||||
except KeyError:
|
||||
pass
|
||||
embed1 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Please wait, adding tracks...')
|
||||
playlist_msg = await ctx.send(embed=embed1)
|
||||
for song_url in v2_playlist["playlist"]:
|
||||
track = await player.get_tracks(song_url)
|
||||
try:
|
||||
track_obj = self._track_creator(player, other_track=track[0])
|
||||
track_list.append(track_obj)
|
||||
track_count = track_count + 1
|
||||
except IndexError:
|
||||
pass
|
||||
if track_count % 5 == 0:
|
||||
embed2 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Loading track {}/{}...'.format(
|
||||
track_count, len(v2_playlist["playlist"])))
|
||||
await playlist_msg.edit(embed=embed2)
|
||||
if not track_list:
|
||||
return await self._embed_msg(ctx, 'No tracks found.')
|
||||
playlist_list = self._to_json(ctx, v2_playlist_url, track_list)
|
||||
async with self.config.guild(ctx.guild).playlists() as v3_playlists:
|
||||
v3_playlists[v2_playlist_name] = playlist_list
|
||||
if len(v2_playlist["playlist"]) != track_count:
|
||||
bad_tracks = len(v2_playlist["playlist"]) - track_count
|
||||
msg = ('Added {} tracks from the {} playlist. {} track(s) could not '
|
||||
'be loaded.'.format(track_count, v2_playlist_name, bad_tracks))
|
||||
else:
|
||||
msg = 'Added {} tracks from the {} playlist.'.format(track_count, v2_playlist_name)
|
||||
embed3 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Saved', description=msg)
|
||||
await playlist_msg.edit(embed=embed3)
|
||||
else:
|
||||
await ctx.invoke(self._playlist_save, v2_playlist_name, v2_playlist_url)
|
||||
|
||||
async def _playlist_check(self, ctx):
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
@@ -686,7 +829,7 @@ class Audio:
|
||||
player.store('channel', ctx.channel.id)
|
||||
player.store('guild', ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._embed_msg(ctx, 'You must be in the voice channel to use the playlist command.')
|
||||
return False
|
||||
if not await self._currency_check(ctx, jukebox_price):
|
||||
@@ -694,6 +837,27 @@ class Audio:
|
||||
await self._data_check(ctx)
|
||||
return True
|
||||
|
||||
async def _playlist_tracks(self, ctx, player, query):
|
||||
search = False
|
||||
if type(query) is tuple:
|
||||
query = " ".join(query)
|
||||
if not query.startswith('http'):
|
||||
query = " ".join(query)
|
||||
query = 'ytsearch:{}'.format(query)
|
||||
search = True
|
||||
tracks = await player.get_tracks(query)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found.')
|
||||
tracklist = []
|
||||
if not search:
|
||||
for track in tracks:
|
||||
track_obj = self._track_creator(player, other_track=track)
|
||||
tracklist.append(track_obj)
|
||||
else:
|
||||
track_obj = self._track_creator(player, other_track=tracks[0])
|
||||
tracklist.append(track_obj)
|
||||
return tracklist
|
||||
|
||||
@commands.command()
|
||||
async def prev(self, ctx):
|
||||
"""Skips to the start of the previously played track."""
|
||||
@@ -706,7 +870,7 @@ class Audio:
|
||||
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx, ctx.author):
|
||||
return await self._embed_msg(ctx, 'You need the DJ role to skip songs.')
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to skip the music.')
|
||||
if shuffle:
|
||||
return await self._embed_msg(ctx, 'Turn shuffle off to use this command.')
|
||||
@@ -733,17 +897,24 @@ class Audio:
|
||||
"""Lists the queue."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'There\'s nothing in the queue.')
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if not player.queue:
|
||||
return await self._embed_msg(ctx, 'There\'s nothing in the queue.')
|
||||
len_queue_pages = math.ceil(len(player.queue) / 10)
|
||||
queue_page_list = []
|
||||
for page_num in range(1, len_queue_pages + 1):
|
||||
embed = await self._build_queue_page(ctx, player, page_num)
|
||||
queue_page_list.append(embed)
|
||||
if page > len_queue_pages:
|
||||
page = len_queue_pages
|
||||
await menu(ctx, queue_page_list, DEFAULT_CONTROLS, page=(page - 1))
|
||||
|
||||
items_per_page = 10
|
||||
pages = math.ceil(len(player.queue) / items_per_page)
|
||||
start = (page - 1) * items_per_page
|
||||
end = start + items_per_page
|
||||
|
||||
async def _build_queue_page(self, ctx, player, page_num):
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
queue_num_pages = math.ceil(len(player.queue) / 10)
|
||||
queue_idx_start = (page_num - 1) * 10
|
||||
queue_idx_end = queue_idx_start + 10
|
||||
queue_list = ''
|
||||
try:
|
||||
arrow = await self._draw_time(ctx)
|
||||
@@ -771,22 +942,27 @@ class Audio:
|
||||
arrow, pos, dur
|
||||
)
|
||||
|
||||
for i, track in enumerate(player.queue[start:end], start=start):
|
||||
for i, track in enumerate(player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start):
|
||||
if len(track.title) > 40:
|
||||
track_title = str(track.title).replace('[', '')
|
||||
track_title = '{}...'.format((track_title[:40]).rstrip(' '))
|
||||
else:
|
||||
track_title = track.title
|
||||
req_user = track.requester
|
||||
next = i + 1
|
||||
queue_list += '`{}.` **[{}]({})**, requested by **{}**\n'.format(next, track.title, track.uri, req_user)
|
||||
track_idx = i + 1
|
||||
queue_list += '`{}.` **[{}]({})**, requested by **{}**\n'.format(track_idx, track_title, track.uri, req_user)
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Queue for ' + ctx.guild.name,
|
||||
description=queue_list)
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
text = 'Page {}/{} | {} tracks, {} remaining'.format(page, pages, len(player.queue) + 1, queue_total_duration)
|
||||
text = 'Page {}/{} | {} tracks, {} remaining'.format(page_num, queue_num_pages, len(player.queue) + 1, queue_total_duration)
|
||||
if repeat:
|
||||
text += ' | Repeat: \N{WHITE HEAVY CHECK MARK}'
|
||||
if shuffle:
|
||||
text += ' | Shuffle: \N{WHITE HEAVY CHECK MARK}'
|
||||
embed.set_footer(text=text)
|
||||
await ctx.send(embed=embed)
|
||||
return embed
|
||||
|
||||
@commands.command()
|
||||
async def repeat(self, ctx):
|
||||
@@ -802,7 +978,7 @@ class Audio:
|
||||
await self._data_check(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to toggle repeat.')
|
||||
await self._embed_msg(ctx, 'Repeat songs: {}.'.format(repeat))
|
||||
|
||||
@@ -819,27 +995,20 @@ class Audio:
|
||||
if not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self._embed_msg(ctx, 'You need the DJ role to remove songs.')
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to manage the queue.')
|
||||
if index > len(player.queue) or index < 1:
|
||||
return await self._embed_msg(ctx, 'Song number must be greater than 1 and within the queue limit.')
|
||||
index -= 1
|
||||
removed = player.queue.pop(index)
|
||||
await self._embed_msg(ctx, 'Removed **' + removed.title + '** from the queue.')
|
||||
await self._embed_msg(ctx, 'Removed {} from the queue.'.format(removed.title))
|
||||
|
||||
@commands.command()
|
||||
async def search(self, ctx, *, query):
|
||||
"""Pick a song with a search.
|
||||
Use [p]search list <search term> to queue all songs.
|
||||
Use [p]search list <search term> to queue all songs found on YouTube.
|
||||
[p]search sc <search term> will search SoundCloud instead of YouTube.
|
||||
"""
|
||||
expected = ("1⃣", "2⃣", "3⃣", "4⃣", "5⃣")
|
||||
emoji = {
|
||||
"one": "1⃣",
|
||||
"two": "2⃣",
|
||||
"three": "3⃣",
|
||||
"four": "4⃣",
|
||||
"five": "5⃣"
|
||||
}
|
||||
if not self._player_check(ctx):
|
||||
try:
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
@@ -852,85 +1021,118 @@ class Audio:
|
||||
player.store('channel', ctx.channel.id)
|
||||
player.store('guild', ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to enqueue songs.')
|
||||
await self._data_check(ctx)
|
||||
|
||||
query = query.strip('<>')
|
||||
if query.startswith('sc '):
|
||||
query = 'scsearch:{}'.format(query.strip('sc '))
|
||||
elif not query.startswith('http') or query.startswith('sc '):
|
||||
query = 'ytsearch:{}'.format(query)
|
||||
|
||||
tracks = await player.get_tracks(query)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found 👀')
|
||||
if 'list' not in query and 'ytsearch:' or 'scsearch:' in query:
|
||||
page = 1
|
||||
items_per_page = 5
|
||||
pages = math.ceil(len(tracks) / items_per_page)
|
||||
start = (page - 1) * items_per_page
|
||||
end = start + items_per_page
|
||||
search_list = ''
|
||||
for i, track in enumerate(tracks[start:end], start=start):
|
||||
next = i + 1
|
||||
search_list += '`{0}.` [**{1}**]({2})\n'.format(next, track.title,
|
||||
track.uri)
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Tracks Found:', description=search_list)
|
||||
embed.set_footer(text='Page {}/{} | {} search results'.format(page, pages, len(tracks)))
|
||||
message = await ctx.send(embed=embed)
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author):
|
||||
return
|
||||
|
||||
def check(r, u):
|
||||
return r.message.id == message.id and u == ctx.message.author
|
||||
|
||||
for i in range(5):
|
||||
await message.add_reaction(expected[i])
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for('reaction_add', check=check, timeout=30.0)
|
||||
except asyncio.TimeoutError:
|
||||
await self._clear_react(message)
|
||||
return
|
||||
reacts = {v: k for k, v in emoji.items()}
|
||||
react = reacts[r.emoji]
|
||||
choice = {'one': 0, 'two': 1, 'three': 2, 'four': 3, 'five': 4}
|
||||
await self._search_button(ctx, message, tracks, entry=choice[react])
|
||||
|
||||
else:
|
||||
await self._data_check(ctx)
|
||||
if query.startswith('list '):
|
||||
query = 'ytsearch:{}'.format(query.lstrip('list '))
|
||||
tracks = await player.get_tracks(query)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found 👀')
|
||||
songembed = discord.Embed(colour=ctx.guild.me.top_role.colour,
|
||||
title='Queued {} track(s).'.format(len(tracks)))
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
if not shuffle and queue_duration > 0:
|
||||
songembed.set_footer(text='{} until start of search playback: starts at #{} in queue'.format(queue_total_duration, (len(player.queue) + 1)))
|
||||
songembed.set_footer(text='{} until start of search playback: starts at #{} in queue'.format(
|
||||
queue_total_duration, (len(player.queue) + 1)))
|
||||
for track in tracks:
|
||||
player.add(ctx.author, track)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
message = await ctx.send(embed=songembed)
|
||||
return await ctx.send(embed=songembed)
|
||||
if query.startswith('sc '):
|
||||
query = 'scsearch:{}'.format(query.lstrip('sc '))
|
||||
elif not query.startswith('http'):
|
||||
query = 'ytsearch:{}'.format(query)
|
||||
tracks = await player.get_tracks(query)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found 👀')
|
||||
|
||||
async def _search_button(self, ctx, message, tracks, entry: int):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
jukebox_price = await self.config.guild(ctx.guild).jukebox_price()
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
await self._clear_react(message)
|
||||
if not await self._currency_check(ctx, jukebox_price):
|
||||
return
|
||||
search_choice = tracks[entry]
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued',
|
||||
description='**[{}]({})**'.format(search_choice.title, search_choice.uri))
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, (len(player.queue) + 1)))
|
||||
player.add(ctx.author, search_choice)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
return await ctx.send(embed=embed)
|
||||
len_search_pages = math.ceil(len(tracks) / 5)
|
||||
search_page_list = []
|
||||
for page_num in range(1, len_search_pages + 1):
|
||||
embed = await self._build_search_page(ctx, tracks, page_num)
|
||||
search_page_list.append(embed)
|
||||
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author):
|
||||
return await menu(ctx, search_page_list, DEFAULT_CONTROLS)
|
||||
|
||||
async def _search_menu(ctx: commands.Context, pages: list,
|
||||
controls: dict, message: discord.Message, page: int,
|
||||
timeout: float, emoji: str):
|
||||
if message:
|
||||
await _search_button_action(ctx, tracks, emoji, page)
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
SEARCH_CONTROLS = {
|
||||
"1⃣": _search_menu,
|
||||
"2⃣": _search_menu,
|
||||
"3⃣": _search_menu,
|
||||
"4⃣": _search_menu,
|
||||
"5⃣": _search_menu,
|
||||
"⬅": prev_page,
|
||||
"❌": close_menu,
|
||||
"➡": next_page
|
||||
}
|
||||
|
||||
async def _search_button_action(ctx, tracks, emoji, page):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
jukebox_price = await self.config.guild(ctx.guild).jukebox_price()
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
if not await self._currency_check(ctx, jukebox_price):
|
||||
return
|
||||
try:
|
||||
if emoji == "1⃣":
|
||||
search_choice = tracks[0 + (page * 5)]
|
||||
if emoji == "2⃣":
|
||||
search_choice = tracks[1 + (page * 5)]
|
||||
if emoji == "3⃣":
|
||||
search_choice = tracks[2 + (page * 5)]
|
||||
if emoji == "4⃣":
|
||||
search_choice = tracks[3 + (page * 5)]
|
||||
if emoji == "5⃣":
|
||||
search_choice = tracks[4 + (page * 5)]
|
||||
except IndexError:
|
||||
search_choice = tracks[-1]
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued',
|
||||
description='**[{}]({})**'.format(search_choice.title, search_choice.uri))
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, (
|
||||
len(player.queue) + 1)))
|
||||
elif queue_duration > 0:
|
||||
embed.set_footer(text='#{} in queue'.format(len(player.queue) + 1))
|
||||
|
||||
player.add(ctx.author, search_choice)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
await menu(ctx, search_page_list, SEARCH_CONTROLS)
|
||||
|
||||
async def _build_search_page(self, ctx, tracks, page_num):
|
||||
search_num_pages = math.ceil(len(tracks) / 5)
|
||||
search_idx_start = (page_num - 1) * 5
|
||||
search_idx_end = search_idx_start + 5
|
||||
search_list = ''
|
||||
for i, track in enumerate(tracks[search_idx_start:search_idx_end], start=search_idx_start):
|
||||
search_track_num = i + 1
|
||||
if search_track_num > 5:
|
||||
search_track_num = search_track_num % 5
|
||||
if search_track_num == 0:
|
||||
search_track_num = 5
|
||||
search_list += '`{0}.` **[{1}]({2})**\n'.format(search_track_num, track.title, track.uri)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Tracks Found:', description=search_list)
|
||||
embed.set_footer(text='Page {}/{} | {} search results'.format(page_num, search_num_pages, len(tracks)))
|
||||
return embed
|
||||
|
||||
@commands.command()
|
||||
async def seek(self, ctx, seconds: int=30):
|
||||
@@ -940,7 +1142,7 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to use seek.')
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx, ctx.author):
|
||||
@@ -973,7 +1175,7 @@ class Audio:
|
||||
await self._data_check(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to toggle shuffle.')
|
||||
await self._embed_msg(ctx, 'Shuffle songs: {}.'.format(shuffle))
|
||||
|
||||
@@ -984,7 +1186,7 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to skip the music.')
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
vote_enabled = await self.config.guild(ctx.guild).vote_enabled()
|
||||
@@ -1052,11 +1254,11 @@ class Audio:
|
||||
nonbots = sum(not m.bot for m in ctx.guild.get_member(self.bot.user.id).voice.channel.members)
|
||||
if nonbots == 1:
|
||||
nonbots = 2
|
||||
elif ctx.guild.get_member(member.id).voice.channel.members == 1:
|
||||
nonbots = 1
|
||||
else:
|
||||
if ctx.guild.get_member(member.id).voice.channel.members == 1:
|
||||
nonbots = 1
|
||||
alone = nonbots <= 1
|
||||
return alone
|
||||
nonbots = 0
|
||||
return nonbots <= 1
|
||||
|
||||
async def _has_dj_role(self, ctx, member):
|
||||
dj_role_id = await self.config.guild(ctx.guild).dj_role()
|
||||
@@ -1098,7 +1300,7 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to stop the music.')
|
||||
if vote_enabled or vote_enabled and dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx, ctx.author):
|
||||
@@ -1128,7 +1330,7 @@ class Audio:
|
||||
if self._player_check(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to change the volume.')
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author) and not await self._has_dj_role(ctx, ctx.author):
|
||||
@@ -1165,7 +1367,8 @@ class Audio:
|
||||
await self.config.password.set('youshallnotpass')
|
||||
await self.config.rest_port.set(2333)
|
||||
await self.config.ws_port.set(2332)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='External lavalink server: {}.'.format(not external))
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='External lavalink server: {}.'.format(
|
||||
not external))
|
||||
embed.set_footer(text='Defaults reset.')
|
||||
return await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -1187,7 +1390,8 @@ class Audio:
|
||||
"""Set the lavalink server password."""
|
||||
await self.config.password.set(str(password))
|
||||
if await self._check_external():
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Server password set to {}.'.format(password))
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour,
|
||||
title='Server password set to {}.'.format(password))
|
||||
embed.set_footer(text='External lavalink server set to True.')
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -1209,7 +1413,8 @@ class Audio:
|
||||
"""Set the lavalink websocket server port."""
|
||||
await self.config.rest_port.set(ws_port)
|
||||
if await self._check_external():
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Websocket port set to {}.'.format(ws_port))
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour,
|
||||
title='Websocket port set to {}.'.format(ws_port))
|
||||
embed.set_footer(text='External lavalink server set to True.')
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -1255,7 +1460,8 @@ class Audio:
|
||||
if player.volume != volume:
|
||||
await player.set_volume(volume)
|
||||
|
||||
async def _draw_time(self, ctx):
|
||||
@staticmethod
|
||||
async def _draw_time(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
paused = player.paused
|
||||
pos = player.position
|
||||
@@ -1305,6 +1511,15 @@ class Audio:
|
||||
else:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _match_yt_playlist(url):
|
||||
yt_list_playlist = re.compile(
|
||||
r'^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)'
|
||||
r'(\/playlist\?).*(list=)(.*)(&|$)')
|
||||
if yt_list_playlist.match(url):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _queue_duration(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
@@ -1312,7 +1527,7 @@ class Audio:
|
||||
for i in range(len(player.queue)):
|
||||
if not player.queue[i].is_stream:
|
||||
duration.append(player.queue[i].length)
|
||||
queue_duration = sum(duration)
|
||||
queue_duration = sum(duration)
|
||||
if not player.queue:
|
||||
queue_duration = 0
|
||||
try:
|
||||
@@ -1333,14 +1548,16 @@ class Audio:
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _to_json(self, ctx, playlist_url, tracklist, playlist_name):
|
||||
@staticmethod
|
||||
def _to_json(ctx, playlist_url, tracklist):
|
||||
playlist = {"author": ctx.author.id, "playlist_url": playlist_url, "tracks": tracklist}
|
||||
return playlist
|
||||
|
||||
def _track_creator(self, ctx, player, position, other_track=None):
|
||||
@staticmethod
|
||||
def _track_creator(player, position=None, other_track=None):
|
||||
if position == 'np':
|
||||
queued_track = player.current
|
||||
elif position == None:
|
||||
elif position is None:
|
||||
queued_track = other_track
|
||||
else:
|
||||
queued_track = player.queue[position]
|
||||
@@ -1365,6 +1582,7 @@ class Audio:
|
||||
pass
|
||||
|
||||
def __unload(self):
|
||||
self.session.close()
|
||||
lavalink.unregister_event_listener(self.event_handler)
|
||||
self.bot.loop.create_task(lavalink.close())
|
||||
shutdown_lavalink_server()
|
||||
|
||||
@@ -92,4 +92,5 @@ def shutdown_lavalink_server():
|
||||
global proc
|
||||
if proc is not None:
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
proc = None
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import discord
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
from redbot.core import checks, bank
|
||||
from redbot.core.i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
from redbot.core import checks, bank, commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
from redbot.core.bot import Red # Only used for type hints
|
||||
|
||||
_ = CogI18n('Bank', __file__)
|
||||
_ = Translator('Bank', __file__)
|
||||
|
||||
|
||||
def check_global_setting_guildowner():
|
||||
@@ -48,6 +47,7 @@ def check_global_setting_admin():
|
||||
return commands.check(pred)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Bank:
|
||||
"""Bank"""
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import re
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, RedContext
|
||||
from redbot.core import checks, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||
from redbot.cogs.mod.log import log
|
||||
|
||||
_ = CogI18n("Cleanup", __file__)
|
||||
_ = Translator("Cleanup", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Cleanup:
|
||||
"""Commands for cleaning messages"""
|
||||
|
||||
@@ -19,19 +19,26 @@ class Cleanup:
|
||||
self.bot = bot
|
||||
|
||||
@staticmethod
|
||||
async def check_100_plus(ctx: RedContext, number: int) -> bool:
|
||||
async def check_100_plus(ctx: commands.Context, number: int) -> bool:
|
||||
"""
|
||||
Called when trying to delete more than 100 messages at once
|
||||
Called when trying to delete more than 100 messages at once.
|
||||
|
||||
Prompts the user to choose whether they want to continue or not
|
||||
Prompts the user to choose whether they want to continue or not.
|
||||
|
||||
Tries its best to cleanup after itself if the response is positive.
|
||||
"""
|
||||
def author_check(message):
|
||||
return message.author == ctx.author
|
||||
|
||||
await ctx.send(_('Are you sure you want to delete {} messages? (y/n)').format(number))
|
||||
prompt = await ctx.send(_('Are you sure you want to delete {} messages? (y/n)').format(number))
|
||||
response = await ctx.bot.wait_for('message', check=author_check)
|
||||
|
||||
if response.content.lower().startswith('y'):
|
||||
await prompt.delete()
|
||||
try:
|
||||
await response.delete()
|
||||
except:
|
||||
pass
|
||||
return True
|
||||
else:
|
||||
await ctx.send(_('Cancelled.'))
|
||||
@@ -39,8 +46,9 @@ class Cleanup:
|
||||
|
||||
@staticmethod
|
||||
async def get_messages_for_deletion(
|
||||
ctx: RedContext, channel: discord.TextChannel, number,
|
||||
check=lambda x: True, limit=100, before=None, after=None
|
||||
ctx: commands.Context, channel: discord.TextChannel, number,
|
||||
check=lambda x: True, limit=100, before=None, after=None,
|
||||
delete_pinned=False
|
||||
) -> list:
|
||||
"""
|
||||
Gets a list of messages meeting the requirements to be deleted.
|
||||
@@ -50,6 +58,7 @@ class Cleanup:
|
||||
- The message passes a provided check (if no check is provided,
|
||||
this is automatically true)
|
||||
- The message is less than 14 days old
|
||||
- The message is not pinned
|
||||
"""
|
||||
to_delete = []
|
||||
too_old = False
|
||||
@@ -59,8 +68,12 @@ class Cleanup:
|
||||
async for message in channel.history(limit=limit,
|
||||
before=before,
|
||||
after=after):
|
||||
if (not number or len(to_delete) - 1 < number) and check(message) \
|
||||
and (ctx.message.created_at - message.created_at).days < 14:
|
||||
if (
|
||||
(not number or len(to_delete) - 1 < number)
|
||||
and check(message)
|
||||
and (ctx.message.created_at - message.created_at).days < 14
|
||||
and (delete_pinned or not message.pinned)
|
||||
):
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
too_old = True
|
||||
@@ -75,7 +88,7 @@ class Cleanup:
|
||||
|
||||
@commands.group()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def cleanup(self, ctx: RedContext):
|
||||
async def cleanup(self, ctx: commands.Context):
|
||||
"""Deletes messages."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
@@ -83,7 +96,7 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def text(self, ctx: RedContext, text: str, number: int):
|
||||
async def text(self, ctx: commands.Context, text: str, number: int, delete_pinned: bool=False):
|
||||
"""Deletes last X messages matching the specified text.
|
||||
|
||||
Example:
|
||||
@@ -94,12 +107,12 @@ class Cleanup:
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
|
||||
def check(m):
|
||||
if text in m.content:
|
||||
return True
|
||||
@@ -109,7 +122,8 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message)
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
||||
delete_pinned=delete_pinned)
|
||||
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
" containing '{}' in channel {}.".format(author.name,
|
||||
@@ -124,26 +138,35 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def user(self, ctx: RedContext, user: discord.Member or int, number: int):
|
||||
async def user(self, ctx: commands.Context, user: str, number: int, delete_pinned: bool=False):
|
||||
"""Deletes last X messages from specified user.
|
||||
|
||||
Examples:
|
||||
cleanup user @\u200bTwentysix 2
|
||||
cleanup user Red 6"""
|
||||
|
||||
member = None
|
||||
try:
|
||||
member = await commands.converter.MemberConverter().convert(ctx, user)
|
||||
except commands.BadArgument:
|
||||
try:
|
||||
_id = int(user)
|
||||
except ValueError:
|
||||
raise commands.BadArgument()
|
||||
else:
|
||||
_id = member.id
|
||||
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
def check(m):
|
||||
if isinstance(user, discord.Member) and m.author == user:
|
||||
return True
|
||||
elif m.author.id == user: # Allow finding messages based on an ID
|
||||
if m.author.id == _id:
|
||||
return True
|
||||
elif m == ctx.message:
|
||||
return True
|
||||
@@ -151,12 +174,13 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
||||
delete_pinned=delete_pinned
|
||||
)
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
" made by {}({}) in channel {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
user.name, user.id, channel.name)
|
||||
member or '???', _id, channel.name)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
@@ -168,7 +192,7 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def after(self, ctx: RedContext, message_id: int):
|
||||
async def after(self, ctx: commands.Context, message_id: int, delete_pinned: bool=False):
|
||||
"""Deletes all messages after specified message.
|
||||
|
||||
To get a message id, enable developer mode in Discord's
|
||||
@@ -194,7 +218,7 @@ class Cleanup:
|
||||
return
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, 0, limit=None, after=after
|
||||
ctx, channel, 0, limit=None, after=after, delete_pinned=delete_pinned
|
||||
)
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}."\
|
||||
@@ -207,7 +231,7 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def messages(self, ctx: RedContext, number: int):
|
||||
async def messages(self, ctx: commands.Context, number: int, delete_pinned: bool=False):
|
||||
"""Deletes last X messages.
|
||||
|
||||
Example:
|
||||
@@ -217,15 +241,17 @@ class Cleanup:
|
||||
author = ctx.author
|
||||
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
|
||||
if number > 100:
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, limit=1000, before=ctx.message
|
||||
ctx, channel, number, limit=1000, before=ctx.message,
|
||||
delete_pinned=delete_pinned
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}."\
|
||||
"".format(author.name, author.id,
|
||||
@@ -240,7 +266,7 @@ class Cleanup:
|
||||
@cleanup.command(name='bot')
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def cleanup_bot(self, ctx: RedContext, number: int):
|
||||
async def cleanup_bot(self, ctx: commands.Context, number: int, delete_pinned: bool=False):
|
||||
"""Cleans up command messages and messages from the bot."""
|
||||
|
||||
channel = ctx.message.channel
|
||||
@@ -272,8 +298,10 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
||||
delete_pinned=delete_pinned
|
||||
)
|
||||
to_delete.append(ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} "\
|
||||
" command messages in channel {}."\
|
||||
@@ -287,7 +315,9 @@ class Cleanup:
|
||||
await slow_deletion(to_delete)
|
||||
|
||||
@cleanup.command(name='self')
|
||||
async def cleanup_self(self, ctx: RedContext, number: int, match_pattern: str = None):
|
||||
async def cleanup_self(
|
||||
self, ctx: commands.Context, number: int,
|
||||
match_pattern: str = None, delete_pinned: bool=False):
|
||||
"""Cleans up messages owned by the bot.
|
||||
|
||||
By default, all messages are cleaned. If a third argument is specified,
|
||||
@@ -337,7 +367,8 @@ class Cleanup:
|
||||
return False
|
||||
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message,
|
||||
delete_pinned=delete_pinned
|
||||
)
|
||||
|
||||
# Selfbot convenience, delete trigger message
|
||||
|
||||
@@ -4,13 +4,12 @@ import random
|
||||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config, checks
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.utils.chat_formatting import box, pagify
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
_ = CogI18n("CustomCommands", __file__)
|
||||
_ = Translator("CustomCommands", __file__)
|
||||
|
||||
|
||||
class CCError(Exception):
|
||||
@@ -152,6 +151,7 @@ class CommandObj:
|
||||
command, value=None)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class CustomCommands:
|
||||
"""Custom commands
|
||||
Creates commands used to display text"""
|
||||
@@ -179,7 +179,18 @@ class CustomCommands:
|
||||
ctx: commands.Context):
|
||||
"""
|
||||
CCs can be enhanced with arguments:
|
||||
https: // twentysix26.github.io / Red - Docs / red_guide_command_args/
|
||||
|
||||
Argument What it will be substituted with
|
||||
|
||||
{message} message
|
||||
|
||||
{author} message.author
|
||||
|
||||
{channel} message.channel
|
||||
|
||||
{guild} message.guild
|
||||
|
||||
{server} message.guild
|
||||
"""
|
||||
if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand,
|
||||
commands.Group):
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
from pathlib import Path
|
||||
import asyncio
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, RedContext
|
||||
from redbot.core import checks, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.cogs.dataconverter.core_specs import SpecResolver
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
_ = CogI18n('DataConverter', __file__)
|
||||
_ = Translator('DataConverter', __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class DataConverter:
|
||||
"""
|
||||
Cog for importing Red v2 Data
|
||||
@@ -22,7 +21,7 @@ class DataConverter:
|
||||
|
||||
@checks.is_owner()
|
||||
@commands.command(name="convertdata")
|
||||
async def dataconversioncommand(self, ctx: RedContext, v2path: str):
|
||||
async def dataconversioncommand(self, ctx: commands.Context, v2path: str):
|
||||
"""
|
||||
Interactive prompt for importing data from Red v2
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import sys
|
||||
from redbot.core import Config
|
||||
from redbot.core import checks
|
||||
from redbot.core.data_manager import cog_data_path
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box, pagify
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
from .checks import install_agreement
|
||||
@@ -22,9 +22,10 @@ from .installable import Installable
|
||||
from .log import log
|
||||
from .repo_manager import RepoManager, Repo
|
||||
|
||||
_ = CogI18n('Downloader', __file__)
|
||||
_ = Translator('Downloader', __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Downloader:
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
@@ -420,7 +421,7 @@ class Downloader:
|
||||
cog_name = cog_installable.name
|
||||
else:
|
||||
made_by = "26 & co."
|
||||
repo_url = "https://github.com/Twentysix26/Red-DiscordBot"
|
||||
repo_url = "https://github.com/Cog-Creators/Red-DiscordBot"
|
||||
cog_name = cog_installable.__class__.__name__
|
||||
|
||||
msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}")
|
||||
|
||||
@@ -7,14 +7,13 @@ from enum import Enum
|
||||
import discord
|
||||
|
||||
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
|
||||
from redbot.core import Config, bank
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import Config, bank, commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.bot import Red
|
||||
|
||||
_ = CogI18n("Economy", __file__)
|
||||
_ = Translator("Economy", __file__)
|
||||
|
||||
logger = logging.getLogger("red.economy")
|
||||
|
||||
@@ -104,6 +103,7 @@ class SetParser:
|
||||
raise RuntimeError
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Economy:
|
||||
"""Economy
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, Config, modlog, RedContext
|
||||
from redbot.core import checks, Config, modlog, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
from redbot.core.utils.mod import is_mod_or_superior
|
||||
|
||||
_ = CogI18n("Filter", __file__)
|
||||
_ = Translator("Filter", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Filter:
|
||||
"""Filter-related commands"""
|
||||
|
||||
@@ -46,7 +46,7 @@ class Filter:
|
||||
@commands.group(name="filter")
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def _filter(self, ctx: RedContext):
|
||||
async def _filter(self, ctx: commands.Context):
|
||||
"""Adds/removes words from filter
|
||||
|
||||
Use double quotes to add/remove sentences
|
||||
@@ -129,7 +129,7 @@ class Filter:
|
||||
await ctx.send(_("Those words weren't in the filter."))
|
||||
|
||||
@_filter.command(name="names")
|
||||
async def filter_names(self, ctx: RedContext):
|
||||
async def filter_names(self, ctx: commands.Context):
|
||||
"""
|
||||
Toggles whether or not to check names and nicknames against the filter
|
||||
This is disabled by default
|
||||
@@ -149,7 +149,7 @@ class Filter:
|
||||
)
|
||||
|
||||
@_filter.command(name="defaultname")
|
||||
async def filter_default_name(self, ctx: RedContext, name: str):
|
||||
async def filter_default_name(self, ctx: commands.Context, name: str):
|
||||
"""
|
||||
Sets the default name to use if filtering names is enabled
|
||||
Note that this has no effect if filtering names is disabled
|
||||
|
||||
@@ -6,12 +6,12 @@ from urllib.parse import quote_plus
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from redbot.core.i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
from redbot.core import commands
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
|
||||
from redbot.core.utils.chat_formatting import escape, italics, pagify
|
||||
|
||||
_ = CogI18n("General", __file__)
|
||||
_ = Translator("General", __file__)
|
||||
|
||||
|
||||
class RPS(Enum):
|
||||
@@ -33,6 +33,7 @@ class RPSParser:
|
||||
raise
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class General:
|
||||
"""General commands."""
|
||||
|
||||
@@ -48,11 +49,6 @@ class General:
|
||||
_("My sources say no"), _("Outlook not so good"), _("Very doubtful")
|
||||
]
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def ping(self, ctx):
|
||||
"""Pong."""
|
||||
await ctx.send("Pong.")
|
||||
|
||||
@commands.command()
|
||||
async def choose(self, ctx, *choices):
|
||||
"""Chooses between multiple choices.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
from random import shuffle
|
||||
|
||||
import aiohttp
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import checks, Config
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core import checks, Config, commands
|
||||
|
||||
_ = CogI18n("Image", __file__)
|
||||
_ = Translator("Image", __file__)
|
||||
|
||||
GIPHY_API_KEY = "dc6zaTOxFJmzC"
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Image:
|
||||
"""Image related commands."""
|
||||
default_global = {
|
||||
|
||||
@@ -3,21 +3,20 @@ from datetime import datetime, timedelta
|
||||
from collections import deque, defaultdict, namedtuple
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, Config, modlog, RedContext
|
||||
from redbot.core import checks, Config, modlog, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box, escape
|
||||
from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions
|
||||
from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, \
|
||||
get_audit_reason
|
||||
from .log import log
|
||||
|
||||
_ = CogI18n("Mod", __file__)
|
||||
|
||||
_ = Translator("Mod", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Mod:
|
||||
"""Moderation tools."""
|
||||
|
||||
@@ -174,7 +173,7 @@ class Mod:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def modset(self, ctx: RedContext):
|
||||
async def modset(self, ctx: commands.Context):
|
||||
"""Manages server administration settings."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
guild = ctx.guild
|
||||
@@ -200,7 +199,7 @@ class Mod:
|
||||
|
||||
@modset.command()
|
||||
@commands.guild_only()
|
||||
async def hierarchy(self, ctx: RedContext):
|
||||
async def hierarchy(self, ctx: commands.Context):
|
||||
"""Toggles role hierarchy check for mods / admins"""
|
||||
guild = ctx.guild
|
||||
toggled = await self.settings.guild(guild).respect_hierarchy()
|
||||
@@ -215,7 +214,7 @@ class Mod:
|
||||
|
||||
@modset.command()
|
||||
@commands.guild_only()
|
||||
async def banmentionspam(self, ctx: RedContext, max_mentions: int=False):
|
||||
async def banmentionspam(self, ctx: commands.Context, max_mentions: int=False):
|
||||
"""Enables auto ban for messages mentioning X different people
|
||||
|
||||
Accepted values: 5 or superior"""
|
||||
@@ -240,7 +239,7 @@ class Mod:
|
||||
|
||||
@modset.command()
|
||||
@commands.guild_only()
|
||||
async def deleterepeats(self, ctx: RedContext):
|
||||
async def deleterepeats(self, ctx: commands.Context):
|
||||
"""Enables auto deletion of repeated messages"""
|
||||
guild = ctx.guild
|
||||
cur_setting = await self.settings.guild(guild).delete_repeats()
|
||||
@@ -254,9 +253,10 @@ class Mod:
|
||||
|
||||
@modset.command()
|
||||
@commands.guild_only()
|
||||
async def deletedelay(self, ctx: RedContext, time: int=None):
|
||||
async def deletedelay(self, ctx: commands.Context, time: int=None):
|
||||
"""Sets the delay until the bot removes the command message.
|
||||
Must be between -1 and 60.
|
||||
|
||||
Must be between -1 and 60.
|
||||
|
||||
A delay of -1 means the bot will not remove the message."""
|
||||
guild = ctx.guild
|
||||
@@ -280,11 +280,11 @@ class Mod:
|
||||
|
||||
@modset.command()
|
||||
@commands.guild_only()
|
||||
async def reinvite(self, ctx: RedContext):
|
||||
"""Toggles whether an invite will be sent when a user
|
||||
is unbanned via [p]unban. If this is True, the bot will
|
||||
attempt to create and send a single-use invite to the
|
||||
newly-unbanned user"""
|
||||
async def reinvite(self, ctx: commands.Context):
|
||||
"""Toggles whether an invite will be sent when a user is unbanned via [p]unban.
|
||||
|
||||
If this is True, the bot will attempt to create and send a single-use invite
|
||||
to the newly-unbanned user"""
|
||||
guild = ctx.guild
|
||||
cur_setting = await self.settings.guild(guild).reinvite_on_unban()
|
||||
if not cur_setting:
|
||||
@@ -297,10 +297,10 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(kick_members=True)
|
||||
async def kick(self, ctx: RedContext, user: discord.Member, *, reason: str = None):
|
||||
async def kick(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Kicks user.
|
||||
If a reason is specified, it
|
||||
will be the reason that shows up
|
||||
|
||||
If a reason is specified, it will be the reason that shows up
|
||||
in the audit log"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
@@ -337,7 +337,7 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def ban(self, ctx: RedContext, user: discord.Member, days: str = None, *, reason: str = None):
|
||||
async def ban(self, ctx: commands.Context, user: discord.Member, days: str = None, *, reason: str = None):
|
||||
"""Bans user and deletes last X days worth of messages.
|
||||
|
||||
If days is not a number, it's treated as the first word of the reason.
|
||||
@@ -398,7 +398,7 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def hackban(self, ctx: RedContext, user_id: int, *, reason: str = None):
|
||||
async def hackban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
|
||||
"""Preemptively bans user from the server
|
||||
|
||||
A user ID needs to be provided in order to ban
|
||||
@@ -451,7 +451,7 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def tempban(self, ctx: RedContext, user: discord.Member, days: int=1, *, reason: str=None):
|
||||
async def tempban(self, ctx: commands.Context, user: discord.Member, days: int=1, *, reason: str=None):
|
||||
"""Tempbans the user for the specified number of days"""
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
@@ -499,7 +499,7 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def softban(self, ctx: RedContext, user: discord.Member, *, reason: str = None):
|
||||
async def softban(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Kicks the user, deleting 1 day worth of messages."""
|
||||
guild = ctx.guild
|
||||
channel = ctx.channel
|
||||
@@ -578,9 +578,9 @@ class Mod:
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
async def unban(self, ctx: RedContext, user_id: int, *, reason: str = None):
|
||||
async def unban(self, ctx: commands.Context, user_id: int, *, reason: str = None):
|
||||
"""Unbans the target user.
|
||||
|
||||
|
||||
Requires specifying the target user's ID. To find this, you may either:
|
||||
1. Copy it from the mod log case (if one was created), or
|
||||
2. enable developer mode, go to Bans in this server's settings, right-
|
||||
@@ -636,7 +636,7 @@ class Mod:
|
||||
.format(invite.url))
|
||||
|
||||
@staticmethod
|
||||
async def get_invite_for_reinvite(ctx: RedContext, max_age: int=86400):
|
||||
async def get_invite_for_reinvite(ctx: commands.Context, max_age: int=86400):
|
||||
"""Handles the reinvite logic for getting an invite
|
||||
to send the newly unbanned user
|
||||
:returns: :class:`Invite`"""
|
||||
@@ -671,7 +671,7 @@ class Mod:
|
||||
@commands.guild_only()
|
||||
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
|
||||
async def voiceban(self, ctx: RedContext, user: discord.Member, *, reason: str=None):
|
||||
async def voiceban(self, ctx: commands.Context, user: discord.Member, *, reason: str=None):
|
||||
"""Bans the target user from speaking and listening in voice channels in the server"""
|
||||
user_voice_state = user.voice
|
||||
if user_voice_state is None:
|
||||
@@ -708,7 +708,7 @@ class Mod:
|
||||
@commands.guild_only()
|
||||
@admin_or_voice_permissions(mute_members=True, deafen_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True, deafen_members=True)
|
||||
async def voiceunban(self, ctx: RedContext, user: discord.Member, *, reason: str=None):
|
||||
async def voiceunban(self, ctx: commands.Context, user: discord.Member, *, reason: str=None):
|
||||
"""Unbans the user from speaking/listening in the server's voice channels"""
|
||||
user_voice_state = user.voice
|
||||
if user_voice_state is None:
|
||||
@@ -742,7 +742,7 @@ class Mod:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_nicknames=True)
|
||||
async def rename(self, ctx: RedContext, user: discord.Member, *, nickname=""):
|
||||
async def rename(self, ctx: commands.Context, user: discord.Member, *, nickname=""):
|
||||
"""Changes user's nickname
|
||||
|
||||
Leaving the nickname empty will remove it."""
|
||||
@@ -762,7 +762,7 @@ class Mod:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_channel=True)
|
||||
async def mute(self, ctx: RedContext):
|
||||
async def mute(self, ctx: commands.Context):
|
||||
"""Mutes user in the channel/server"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
@@ -771,7 +771,7 @@ class Mod:
|
||||
@commands.guild_only()
|
||||
@mod_or_voice_permissions(mute_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True)
|
||||
async def voice_mute(self, ctx: RedContext, user: discord.Member,
|
||||
async def voice_mute(self, ctx: commands.Context, user: discord.Member,
|
||||
*, reason: str = None):
|
||||
"""Mutes the user in a voice channel"""
|
||||
user_voice_state = user.voice
|
||||
@@ -810,7 +810,7 @@ class Mod:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@mute.command(name="channel")
|
||||
@commands.guild_only()
|
||||
async def channel_mute(self, ctx: RedContext, user: discord.Member, *, reason: str = None):
|
||||
async def channel_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Mutes user in the current channel"""
|
||||
author = ctx.message.author
|
||||
channel = ctx.message.channel
|
||||
@@ -838,7 +838,7 @@ class Mod:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@mute.command(name="server", aliases=["guild"])
|
||||
@commands.guild_only()
|
||||
async def guild_mute(self, ctx: RedContext, user: discord.Member, *, reason: str = None):
|
||||
async def guild_mute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Mutes user in the server"""
|
||||
author = ctx.message.author
|
||||
guild = ctx.guild
|
||||
@@ -885,11 +885,11 @@ class Mod:
|
||||
return False, mute_unmute_issues["hierarchy_problem"]
|
||||
|
||||
perms_cache[str(channel.id)] = {
|
||||
"send_messages": overwrites.send_messages,
|
||||
"send_messages": overwrites.send_messages,
|
||||
"add_reactions": overwrites.add_reactions
|
||||
}
|
||||
overwrites.send_messages = False
|
||||
overwrites.add_reactions = False
|
||||
overwrites.update(send_messages=False,
|
||||
add_reactions=False)
|
||||
try:
|
||||
await channel.set_permissions(user, overwrite=overwrites, reason=reason)
|
||||
except discord.Forbidden:
|
||||
@@ -901,7 +901,7 @@ class Mod:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod_or_permissions(manage_channel=True)
|
||||
async def unmute(self, ctx: RedContext):
|
||||
async def unmute(self, ctx: commands.Context):
|
||||
"""Unmutes user in the channel/server
|
||||
|
||||
Defaults to channel"""
|
||||
@@ -912,7 +912,7 @@ class Mod:
|
||||
@commands.guild_only()
|
||||
@mod_or_voice_permissions(mute_members=True)
|
||||
@bot_has_voice_permissions(mute_members=True)
|
||||
async def voice_unmute(self, ctx: RedContext, user: discord.Member, *, reason: str = None):
|
||||
async def voice_unmute(self, ctx: commands.Context, user: discord.Member, *, reason: str = None):
|
||||
"""Unmutes the user in a voice channel"""
|
||||
user_voice_state = user.voice
|
||||
if user_voice_state:
|
||||
@@ -946,7 +946,7 @@ class Mod:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@unmute.command(name="channel")
|
||||
@commands.guild_only()
|
||||
async def channel_unmute(self, ctx: RedContext, user: discord.Member, *, reason: str=None):
|
||||
async def channel_unmute(self, ctx: commands.Context, user: discord.Member, *, reason: str=None):
|
||||
"""Unmutes user in the current channel"""
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
@@ -969,7 +969,7 @@ class Mod:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
@unmute.command(name="server", aliases=["guild"])
|
||||
@commands.guild_only()
|
||||
async def guild_unmute(self, ctx: RedContext, user: discord.Member, *, reason: str=None):
|
||||
async def guild_unmute(self, ctx: commands.Context, user: discord.Member, *, reason: str=None):
|
||||
"""Unmutes user in the server"""
|
||||
guild = ctx.guild
|
||||
author = ctx.author
|
||||
@@ -1013,9 +1013,9 @@ class Mod:
|
||||
if channel.id in perms_cache:
|
||||
old_values = perms_cache[channel.id]
|
||||
else:
|
||||
old_values = None
|
||||
overwrites.send_messages = old_values["send_messages"]
|
||||
overwrites.add_reactions = old_values["add_reactions"]
|
||||
old_values = {"send_messages": None, "add_reactions": None}
|
||||
overwrites.update(send_messages=old_values["send_messages"],
|
||||
add_reactions=old_values["add_reactions"])
|
||||
is_empty = self.are_overwrites_empty(overwrites)
|
||||
|
||||
try:
|
||||
@@ -1037,14 +1037,14 @@ class Mod:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_channels=True)
|
||||
async def ignore(self, ctx: RedContext):
|
||||
async def ignore(self, ctx: commands.Context):
|
||||
"""Adds servers/channels to ignorelist"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
await ctx.send(await self.count_ignored())
|
||||
|
||||
@ignore.command(name="channel")
|
||||
async def ignore_channel(self, ctx: RedContext, channel: discord.TextChannel=None):
|
||||
async def ignore_channel(self, ctx: commands.Context, channel: discord.TextChannel=None):
|
||||
"""Ignores channel
|
||||
|
||||
Defaults to current one"""
|
||||
@@ -1057,7 +1057,8 @@ class Mod:
|
||||
await ctx.send(_("Channel already in ignore list."))
|
||||
|
||||
@ignore.command(name="server", aliases=["guild"])
|
||||
async def ignore_guild(self, ctx: RedContext):
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
async def ignore_guild(self, ctx: commands.Context):
|
||||
"""Ignores current server"""
|
||||
guild = ctx.guild
|
||||
if not await self.settings.guild(guild).ignored():
|
||||
@@ -1069,14 +1070,14 @@ class Mod:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_channels=True)
|
||||
async def unignore(self, ctx: RedContext):
|
||||
async def unignore(self, ctx: commands.Context):
|
||||
"""Removes servers/channels from ignorelist"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
await ctx.send(await self.count_ignored())
|
||||
|
||||
@unignore.command(name="channel")
|
||||
async def unignore_channel(self, ctx: RedContext, channel: discord.TextChannel=None):
|
||||
async def unignore_channel(self, ctx: commands.Context, channel: discord.TextChannel=None):
|
||||
"""Removes channel from ignore list
|
||||
|
||||
Defaults to current one"""
|
||||
@@ -1090,7 +1091,8 @@ class Mod:
|
||||
await ctx.send(_("That channel is not in the ignore list."))
|
||||
|
||||
@unignore.command(name="server", aliases=["guild"])
|
||||
async def unignore_guild(self, ctx: RedContext):
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
async def unignore_guild(self, ctx: commands.Context):
|
||||
"""Removes current guild from ignore list"""
|
||||
guild = ctx.message.guild
|
||||
if await self.settings.guild(guild).ignored():
|
||||
@@ -1130,11 +1132,8 @@ class Mod:
|
||||
chann_ignored and not perms.manage_channels)
|
||||
|
||||
@commands.command()
|
||||
async def names(self, ctx: RedContext, user: discord.Member):
|
||||
async def names(self, ctx: commands.Context, user: discord.Member):
|
||||
"""Show previous names/nicknames of a user"""
|
||||
async with self.settings.user(user).past_names() as name_list:
|
||||
while None in name_list: # clean out null entries from a bug
|
||||
name_list.remove(None)
|
||||
names = await self.settings.user(user).past_names()
|
||||
nicks = await self.settings.member(user).past_nicks()
|
||||
msg = ""
|
||||
@@ -1224,7 +1223,7 @@ class Mod:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def on_command(self, ctx: RedContext):
|
||||
async def on_command(self, ctx: commands.Context):
|
||||
"""Currently used for:
|
||||
* delete delay"""
|
||||
guild = ctx.guild
|
||||
@@ -1253,7 +1252,7 @@ class Mod:
|
||||
valid_user = isinstance(author, discord.Member) and not author.bot
|
||||
if not valid_user:
|
||||
return
|
||||
|
||||
|
||||
# Bots and mods or superior are ignored from the filter
|
||||
mod_or_superior = await is_mod_or_superior(self.bot, obj=author)
|
||||
if mod_or_superior:
|
||||
@@ -1358,15 +1357,13 @@ class Mod:
|
||||
if entry.target == target:
|
||||
return entry
|
||||
|
||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||
async def on_member_update(self, before, after):
|
||||
if before.name != after.name:
|
||||
async with self.settings.user(before).past_names() as name_list:
|
||||
while None in name_list: # clean out null entries from a bug
|
||||
name_list.remove(None)
|
||||
if after.name in name_list:
|
||||
if after.nick in name_list:
|
||||
# Ensure order is maintained without duplicates occuring
|
||||
name_list.remove(after.name)
|
||||
name_list.append(after.name)
|
||||
name_list.remove(after.nick)
|
||||
name_list.append(after.nick)
|
||||
while len(name_list) > 20:
|
||||
name_list.pop(0)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks, modlog, RedContext
|
||||
from redbot.core import checks, modlog, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
_ = CogI18n('ModLog', __file__)
|
||||
_ = Translator('ModLog', __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class ModLog:
|
||||
"""Log for mod actions"""
|
||||
|
||||
@@ -17,14 +17,14 @@ class ModLog:
|
||||
|
||||
@commands.group()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def modlogset(self, ctx: RedContext):
|
||||
async def modlogset(self, ctx: commands.Context):
|
||||
"""Settings for the mod log"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
async def modlog(self, ctx: RedContext, channel: discord.TextChannel = None):
|
||||
async def modlog(self, ctx: commands.Context, channel: discord.TextChannel = None):
|
||||
"""Sets a channel as mod log
|
||||
|
||||
Leaving the channel parameter empty will deactivate it"""
|
||||
@@ -53,7 +53,7 @@ class ModLog:
|
||||
|
||||
@modlogset.command(name='cases')
|
||||
@commands.guild_only()
|
||||
async def set_cases(self, ctx: RedContext, action: str = None):
|
||||
async def set_cases(self, ctx: commands.Context, action: str = None):
|
||||
"""Enables or disables case creation for each type of mod action"""
|
||||
guild = ctx.guild
|
||||
|
||||
@@ -87,7 +87,7 @@ class ModLog:
|
||||
|
||||
@modlogset.command()
|
||||
@commands.guild_only()
|
||||
async def resetcases(self, ctx: RedContext):
|
||||
async def resetcases(self, ctx: commands.Context):
|
||||
"""Resets modlog's cases"""
|
||||
guild = ctx.guild
|
||||
await modlog.reset_cases(guild)
|
||||
@@ -95,7 +95,7 @@ class ModLog:
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def case(self, ctx: RedContext, number: int):
|
||||
async def case(self, ctx: commands.Context, number: int):
|
||||
"""Shows the specified case"""
|
||||
try:
|
||||
case = await modlog.get_case(number, ctx.guild, self.bot)
|
||||
@@ -107,7 +107,7 @@ class ModLog:
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def reason(self, ctx: RedContext, case: int, *, reason: str = ""):
|
||||
async def reason(self, ctx: commands.Context, case: int, *, reason: str = ""):
|
||||
"""Lets you specify a reason for mod-log's cases
|
||||
Please note that you can only edit cases you are
|
||||
the owner of unless you are a mod/admin or the server owner"""
|
||||
@@ -134,7 +134,7 @@ class ModLog:
|
||||
audit_case = None
|
||||
async for entry in guild.audit_logs(action=audit_type):
|
||||
if entry.target.id == case_before.user.id and \
|
||||
entry.user.id == case_before.moderator.id:
|
||||
entry.action == audit_type:
|
||||
audit_case = entry
|
||||
break
|
||||
if audit_case:
|
||||
|
||||
@@ -2,23 +2,24 @@ import logging
|
||||
import asyncio
|
||||
from typing import Union
|
||||
from datetime import timedelta
|
||||
|
||||
from copy import copy
|
||||
import contextlib
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import Config, checks, RedContext
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from redbot.core.utils.antispam import AntiSpam
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.tunnel import Tunnel
|
||||
|
||||
|
||||
_ = CogI18n("Reports", __file__)
|
||||
_ = Translator("Reports", __file__)
|
||||
|
||||
log = logging.getLogger("red.reports")
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Reports:
|
||||
|
||||
default_guild_settings = {
|
||||
@@ -65,7 +66,7 @@ class Reports:
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@commands.guild_only()
|
||||
@commands.group(name="reportset")
|
||||
async def reportset(self, ctx: RedContext):
|
||||
async def reportset(self, ctx: commands.Context):
|
||||
"""
|
||||
settings for reports
|
||||
"""
|
||||
@@ -73,14 +74,14 @@ class Reports:
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="output")
|
||||
async def setoutput(self, ctx: RedContext, channel: discord.TextChannel):
|
||||
async def setoutput(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||
"""sets the output channel"""
|
||||
await self.config.guild(ctx.guild).output_channel.set(channel.id)
|
||||
await ctx.send(_("Report Channel Set."))
|
||||
|
||||
@checks.admin_or_permissions(manage_guild=True)
|
||||
@reportset.command(name="toggleactive")
|
||||
async def report_toggle(self, ctx: RedContext):
|
||||
async def report_toggle(self, ctx: commands.Context):
|
||||
"""Toggles whether the Reporting tool is enabled or not"""
|
||||
|
||||
active = await self.config.guild(ctx.guild).active()
|
||||
@@ -109,10 +110,11 @@ class Reports:
|
||||
ret |= await self.bot.is_owner(m)
|
||||
return ret
|
||||
|
||||
async def discover_guild(self, author: discord.User, *,
|
||||
mod: bool=False,
|
||||
permissions: Union[discord.Permissions, dict]={},
|
||||
prompt: str=""):
|
||||
async def discover_guild(
|
||||
self, author: discord.User, *,
|
||||
mod: bool=False,
|
||||
permissions: Union[discord.Permissions, dict]=None,
|
||||
prompt: str=""):
|
||||
"""
|
||||
discovers which of shared guilds between the bot
|
||||
and provided user based on conditions (mod or permissions is an or)
|
||||
@@ -120,10 +122,12 @@ class Reports:
|
||||
prompt is for providing a user prompt for selection
|
||||
"""
|
||||
shared_guilds = []
|
||||
if isinstance(permissions, discord.Permissions):
|
||||
if permissions is None:
|
||||
perms = discord.Permissions()
|
||||
elif isinstance(permissions, discord.Permissions):
|
||||
perms = permissions
|
||||
else:
|
||||
permissions = discord.Permissions(**perms)
|
||||
perms = discord.Permissions(**permissions)
|
||||
|
||||
for guild in self.bot.guilds:
|
||||
x = guild.get_member(author.id)
|
||||
@@ -132,7 +136,6 @@ class Reports:
|
||||
shared_guilds.append(guild)
|
||||
if len(shared_guilds) == 0:
|
||||
raise ValueError("No Qualifying Shared Guilds")
|
||||
return
|
||||
if len(shared_guilds) == 1:
|
||||
return shared_guilds[0]
|
||||
output = ""
|
||||
@@ -170,26 +173,40 @@ class Reports:
|
||||
|
||||
author = guild.get_member(msg.author.id)
|
||||
report = msg.clean_content
|
||||
avatar = author.avatar_url
|
||||
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_('Report from {0.display_name}').format(author),
|
||||
icon_url=avatar
|
||||
)
|
||||
|
||||
ticket_number = await self.config.guild(guild).next_ticket()
|
||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
|
||||
channel_id = await self.config.guild(guild).output_channel()
|
||||
channel = guild.get_channel(channel_id)
|
||||
if channel is not None:
|
||||
try:
|
||||
await channel.send(embed=em)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
return None
|
||||
if channel is None:
|
||||
return None
|
||||
|
||||
files = await Tunnel.files_from_attatch(msg)
|
||||
|
||||
ticket_number = await self.config.guild(guild).next_ticket()
|
||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||
|
||||
if await self.bot.embed_requested(channel, author):
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_('Report from {0.display_name}').format(author),
|
||||
icon_url=author.avatar_url
|
||||
)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
send_content = None
|
||||
else:
|
||||
em = None
|
||||
send_content = _(
|
||||
'Report from {author.mention} (Ticket #{number})'
|
||||
).format(author=author, number=ticket_number)
|
||||
send_content += "\n" + report
|
||||
|
||||
try:
|
||||
await Tunnel.message_forwarder(
|
||||
destination=channel,
|
||||
content=send_content,
|
||||
embed=em,
|
||||
files=files
|
||||
)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
return None
|
||||
|
||||
await self.config.custom('REPORT', guild.id, ticket_number).report.set(
|
||||
@@ -198,8 +215,13 @@ class Reports:
|
||||
return ticket_number
|
||||
|
||||
@commands.group(name="report", invoke_without_command=True)
|
||||
async def report(self, ctx: RedContext):
|
||||
"Follow the prompts to make a report"
|
||||
async def report(self, ctx: commands.Context, *, _report: str=""):
|
||||
"""
|
||||
Follow the prompts to make a report
|
||||
|
||||
optionally use with a report message
|
||||
to use it non interactively
|
||||
"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
if guild is None:
|
||||
@@ -243,31 +265,39 @@ class Reports:
|
||||
pass
|
||||
self.user_cache.append(author.id)
|
||||
|
||||
try:
|
||||
dm = await author.send(
|
||||
_("Please respond to this message with your Report."
|
||||
"\nYour report should be a single message")
|
||||
)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(
|
||||
_("This requires DMs enabled.")
|
||||
)
|
||||
self.user_cache.remove(author.id)
|
||||
return
|
||||
|
||||
def pred(m):
|
||||
return m.author == author and m.channel == dm.channel
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for(
|
||||
'message', check=pred, timeout=180
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await author.send(
|
||||
_("You took too long. Try again later.")
|
||||
)
|
||||
if _report:
|
||||
_m = copy(ctx.message)
|
||||
_m.content = _report
|
||||
_m.content = _m.clean_content
|
||||
val = await self.send_report(_m, guild)
|
||||
else:
|
||||
val = await self.send_report(message, guild)
|
||||
try:
|
||||
dm = await author.send(
|
||||
_("Please respond to this message with your Report."
|
||||
"\nYour report should be a single message")
|
||||
)
|
||||
except discord.Forbidden:
|
||||
await ctx.send(
|
||||
_("This requires DMs enabled.")
|
||||
)
|
||||
self.user_cache.remove(author.id)
|
||||
return
|
||||
|
||||
def pred(m):
|
||||
return m.author == author and m.channel == dm.channel
|
||||
|
||||
try:
|
||||
message = await self.bot.wait_for(
|
||||
'message', check=pred, timeout=180
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
await author.send(
|
||||
_("You took too long. Try again later.")
|
||||
)
|
||||
else:
|
||||
val = await self.send_report(message, guild)
|
||||
|
||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||
if val is None:
|
||||
await author.send(
|
||||
_("There was an error sending your report.")
|
||||
@@ -276,7 +306,7 @@ class Reports:
|
||||
await author.send(
|
||||
_("Your report was submitted. (Ticket #{})").format(val)
|
||||
)
|
||||
self.antispam[guild.id][author.id].stamp()
|
||||
self.antispam[guild.id][author.id].stamp()
|
||||
|
||||
self.user_cache.remove(author.id)
|
||||
|
||||
@@ -353,7 +383,7 @@ class Reports:
|
||||
"will be forwarded to them until the communication is closed.\n"
|
||||
"You can close a communication at any point "
|
||||
"by reacting with the X to the last message recieved. "
|
||||
"\nAny message succesfully forwarded with be marked with a check."
|
||||
"\nAny message succesfully forwarded will be marked with a check."
|
||||
"\nTunnels are not persistent across bot restarts."
|
||||
)
|
||||
topic = big_topic.format(
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import Config, checks, RedContext
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity, YoutubeStream
|
||||
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials,
|
||||
CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials)
|
||||
@@ -15,9 +14,10 @@ import re
|
||||
CHECK_DELAY = 60
|
||||
|
||||
|
||||
_ = CogI18n("Streams", __file__)
|
||||
_ = Translator("Streams", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Streams:
|
||||
|
||||
global_defaults = {
|
||||
@@ -64,7 +64,7 @@ class Streams:
|
||||
self.task = self.bot.loop.create_task(self._stream_alerts())
|
||||
|
||||
@commands.command()
|
||||
async def twitch(self, ctx, channel_name: str):
|
||||
async def twitch(self, ctx: commands.Context, channel_name: str):
|
||||
"""Checks if a Twitch channel is streaming"""
|
||||
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
|
||||
stream = TwitchStream(name=channel_name,
|
||||
@@ -72,7 +72,7 @@ class Streams:
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
async def youtube(self, ctx, channel_id_or_name: str):
|
||||
async def youtube(self, ctx: commands.Context, channel_id_or_name: str):
|
||||
"""
|
||||
Checks if a Youtube channel is streaming
|
||||
"""
|
||||
@@ -85,24 +85,24 @@ class Streams:
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
async def hitbox(self, ctx, channel_name: str):
|
||||
async def hitbox(self, ctx: commands.Context, channel_name: str):
|
||||
"""Checks if a Hitbox channel is streaming"""
|
||||
stream = HitboxStream(name=channel_name)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
async def mixer(self, ctx, channel_name: str):
|
||||
async def mixer(self, ctx: commands.Context, channel_name: str):
|
||||
"""Checks if a Mixer channel is streaming"""
|
||||
stream = MixerStream(name=channel_name)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
async def picarto(self, ctx, channel_name: str):
|
||||
async def picarto(self, ctx: commands.Context, channel_name: str):
|
||||
"""Checks if a Picarto channel is streaming"""
|
||||
stream = PicartoStream(name=channel_name)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
async def check_online(self, ctx, stream):
|
||||
async def check_online(self, ctx: commands.Context, stream):
|
||||
try:
|
||||
embed = await stream.is_online()
|
||||
except OfflineStream:
|
||||
@@ -124,49 +124,49 @@ class Streams:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.mod()
|
||||
async def streamalert(self, ctx):
|
||||
async def streamalert(self, ctx: commands.Context):
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@streamalert.group(name="twitch")
|
||||
async def _twitch(self, ctx):
|
||||
async def _twitch(self, ctx: commands.Context):
|
||||
"""Twitch stream alerts"""
|
||||
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch:
|
||||
await ctx.send_help()
|
||||
|
||||
@_twitch.command(name="channel")
|
||||
async def twitch_alert_channel(self, ctx: RedContext, channel_name: str):
|
||||
async def twitch_alert_channel(self, ctx: commands.Context, channel_name: str):
|
||||
"""Sets a Twitch stream alert notification in the channel"""
|
||||
await self.stream_alert(ctx, TwitchStream, channel_name.lower())
|
||||
|
||||
@_twitch.command(name="community")
|
||||
async def twitch_alert_community(self, ctx: RedContext, community: str):
|
||||
async def twitch_alert_community(self, ctx: commands.Context, community: str):
|
||||
"""Sets a Twitch stream alert notification in the channel
|
||||
for the specified community."""
|
||||
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
||||
|
||||
@streamalert.command(name="youtube")
|
||||
async def youtube_alert(self, ctx: RedContext, channel_name_or_id: str):
|
||||
async def youtube_alert(self, ctx: commands.Context, channel_name_or_id: str):
|
||||
"""Sets a Youtube stream alert notification in the channel"""
|
||||
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
||||
|
||||
@streamalert.command(name="hitbox")
|
||||
async def hitbox_alert(self, ctx, channel_name: str):
|
||||
async def hitbox_alert(self, ctx: commands.Context, channel_name: str):
|
||||
"""Sets a Hitbox stream alert notification in the channel"""
|
||||
await self.stream_alert(ctx, HitboxStream, channel_name)
|
||||
|
||||
@streamalert.command(name="mixer")
|
||||
async def mixer_alert(self, ctx, channel_name: str):
|
||||
async def mixer_alert(self, ctx: commands.Context, channel_name: str):
|
||||
"""Sets a Mixer stream alert notification in the channel"""
|
||||
await self.stream_alert(ctx, MixerStream, channel_name)
|
||||
|
||||
@streamalert.command(name="picarto")
|
||||
async def picarto_alert(self, ctx, channel_name: str):
|
||||
async def picarto_alert(self, ctx: commands.Context, channel_name: str):
|
||||
"""Sets a Picarto stream alert notification in the channel"""
|
||||
await self.stream_alert(ctx, PicartoStream, channel_name)
|
||||
|
||||
@streamalert.command(name="stop")
|
||||
async def streamalert_stop(self, ctx, _all: bool=False):
|
||||
async def streamalert_stop(self, ctx: commands.Context, _all: bool=False):
|
||||
"""Stops all stream notifications in the channel
|
||||
|
||||
Adding 'yes' will disable all notifications in the server"""
|
||||
@@ -197,7 +197,7 @@ class Streams:
|
||||
await ctx.send(msg)
|
||||
|
||||
@streamalert.command(name="list")
|
||||
async def streamalert_list(self, ctx):
|
||||
async def streamalert_list(self, ctx: commands.Context):
|
||||
streams_list = defaultdict(list)
|
||||
guild_channels_ids = [c.id for c in ctx.guild.channels]
|
||||
msg = _("Active stream alerts:\n\n")
|
||||
@@ -218,7 +218,7 @@ class Streams:
|
||||
for page in pagify(msg):
|
||||
await ctx.send(page)
|
||||
|
||||
async def stream_alert(self, ctx, _class, channel_name):
|
||||
async def stream_alert(self, ctx: commands.Context, _class, channel_name):
|
||||
stream = self.get_stream(_class, channel_name)
|
||||
if not stream:
|
||||
token = await self.db.tokens.get_raw(_class.__name__, default=None)
|
||||
@@ -251,7 +251,7 @@ class Streams:
|
||||
|
||||
await self.add_or_remove(ctx, stream)
|
||||
|
||||
async def community_alert(self, ctx, _class, community_name):
|
||||
async def community_alert(self, ctx: commands.Context, _class, community_name):
|
||||
community = self.get_community(_class, community_name)
|
||||
if not community:
|
||||
token = await self.db.tokens.get_raw(_class.__name__, default=None)
|
||||
@@ -278,13 +278,13 @@ class Streams:
|
||||
|
||||
@commands.group()
|
||||
@checks.mod()
|
||||
async def streamset(self, ctx):
|
||||
async def streamset(self, ctx: commands.Context):
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@streamset.command()
|
||||
@checks.is_owner()
|
||||
async def twitchtoken(self, ctx, token: str):
|
||||
async def twitchtoken(self, ctx: commands.Context, token: str):
|
||||
"""Set the Client ID for twitch.
|
||||
|
||||
To do this, follow these steps:
|
||||
@@ -302,7 +302,7 @@ class Streams:
|
||||
|
||||
@streamset.command()
|
||||
@checks.is_owner()
|
||||
async def youtubekey(self, ctx: RedContext, key: str):
|
||||
async def youtubekey(self, ctx: commands.Context, key: str):
|
||||
"""Sets the API key for Youtube.
|
||||
|
||||
To get one, do the following:
|
||||
@@ -318,14 +318,14 @@ class Streams:
|
||||
|
||||
@streamset.group()
|
||||
@commands.guild_only()
|
||||
async def mention(self, ctx):
|
||||
async def mention(self, ctx: commands.Context):
|
||||
"""Sets mentions for stream alerts."""
|
||||
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention:
|
||||
await ctx.send_help()
|
||||
|
||||
@mention.command(aliases=["everyone"])
|
||||
@commands.guild_only()
|
||||
async def all(self, ctx):
|
||||
async def all(self, ctx: commands.Context):
|
||||
"""Toggles everyone mention"""
|
||||
guild = ctx.guild
|
||||
current_setting = await self.db.guild(guild).mention_everyone()
|
||||
@@ -340,7 +340,7 @@ class Streams:
|
||||
|
||||
@mention.command(aliases=["here"])
|
||||
@commands.guild_only()
|
||||
async def online(self, ctx):
|
||||
async def online(self, ctx: commands.Context):
|
||||
"""Toggles here mention"""
|
||||
guild = ctx.guild
|
||||
current_setting = await self.db.guild(guild).mention_here()
|
||||
@@ -355,7 +355,7 @@ class Streams:
|
||||
|
||||
@mention.command()
|
||||
@commands.guild_only()
|
||||
async def role(self, ctx, *, role: discord.Role):
|
||||
async def role(self, ctx: commands.Context, *, role: discord.Role):
|
||||
"""Toggles role mention"""
|
||||
current_setting = await self.db.role(role).mention()
|
||||
if not role.mentionable:
|
||||
@@ -373,7 +373,7 @@ class Streams:
|
||||
|
||||
@streamset.command()
|
||||
@commands.guild_only()
|
||||
async def autodelete(self, ctx, on_off: bool):
|
||||
async def autodelete(self, ctx: commands.Context, on_off: bool):
|
||||
"""Toggles automatic deletion of notifications for streams that go offline"""
|
||||
await self.db.guild(ctx.guild).autodelete.set(on_off)
|
||||
if on_off:
|
||||
@@ -382,7 +382,7 @@ class Streams:
|
||||
else:
|
||||
await ctx.send("Notifications will never be deleted.")
|
||||
|
||||
async def add_or_remove(self, ctx, stream):
|
||||
async def add_or_remove(self, ctx: commands.Context, stream):
|
||||
if ctx.channel.id not in stream.channels:
|
||||
stream.channels.append(ctx.channel.id)
|
||||
if stream not in self.streams:
|
||||
@@ -398,7 +398,7 @@ class Streams:
|
||||
|
||||
await self.save_streams()
|
||||
|
||||
async def add_or_remove_community(self, ctx, community):
|
||||
async def add_or_remove_community(self, ctx: commands.Context, community):
|
||||
if ctx.channel.id not in community.channels:
|
||||
community.channels.append(ctx.channel.id)
|
||||
if community not in self.communities:
|
||||
@@ -473,6 +473,7 @@ class Streams:
|
||||
except:
|
||||
pass
|
||||
stream._messages_cache.clear()
|
||||
await self.save_streams()
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
@@ -490,6 +491,7 @@ class Streams:
|
||||
try:
|
||||
m = await channel.send(content, embed=embed)
|
||||
stream._messages_cache.append(m)
|
||||
await self.save_streams()
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -521,6 +523,7 @@ class Streams:
|
||||
except:
|
||||
pass
|
||||
community._messages_cache.clear()
|
||||
await self.save_communities()
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
@@ -536,11 +539,13 @@ class Streams:
|
||||
else:
|
||||
msg = await chn.send(embed=emb)
|
||||
community._messages_cache.append(msg)
|
||||
await self.save_communities()
|
||||
else:
|
||||
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
|
||||
community._messages_cache.remove(chn_msg)
|
||||
await chn_msg.edit(embed=emb)
|
||||
community._messages_cache.append(chn_msg)
|
||||
await self.save_communities()
|
||||
|
||||
async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
|
||||
filtered = []
|
||||
@@ -561,7 +566,12 @@ class Streams:
|
||||
_class = getattr(StreamClasses, raw_stream["type"], None)
|
||||
if not _class:
|
||||
continue
|
||||
|
||||
raw_msg_cache = raw_stream["messages"]
|
||||
raw_stream["_messages_cache"] = []
|
||||
for raw_msg in raw_msg_cache:
|
||||
chn = self.bot.get_channel(raw_msg["channel"])
|
||||
msg = await chn.get_message(raw_msg["message"])
|
||||
raw_stream["_messages_cache"].append(msg)
|
||||
token = await self.db.tokens.get_raw(_class.__name__)
|
||||
streams.append(_class(token=token, **raw_stream))
|
||||
|
||||
@@ -581,7 +591,12 @@ class Streams:
|
||||
_class = getattr(StreamClasses, raw_community["type"], None)
|
||||
if not _class:
|
||||
continue
|
||||
|
||||
raw_msg_cache = raw_community["messages"]
|
||||
raw_community["_messages_cache"] = []
|
||||
for raw_msg in raw_msg_cache:
|
||||
chn = self.bot.get_channel(raw_msg["channel"])
|
||||
msg = await chn.get_message(raw_msg["message"])
|
||||
raw_community["_messages_cache"].append(msg)
|
||||
token = await self.db.tokens.get_raw(_class.__name__, default=None)
|
||||
communities.append(_class(token=token, **raw_community))
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class TwitchCommunity:
|
||||
self.name = kwargs.pop("name")
|
||||
self.id = kwargs.pop("id", None)
|
||||
self.channels = kwargs.pop("channels", [])
|
||||
self._messages_cache = []
|
||||
self._messages_cache = kwargs.pop("_messages_cache", [])
|
||||
self._token = kwargs.pop("token", None)
|
||||
self.type = self.__class__.__name__
|
||||
|
||||
@@ -115,6 +115,9 @@ class TwitchCommunity:
|
||||
for k, v in self.__dict__.items():
|
||||
if not k.startswith("_"):
|
||||
data[k] = v
|
||||
data["messages"] = []
|
||||
for m in self._messages_cache:
|
||||
data["messages"].append({"channel": m.channel.id, "message": m.id})
|
||||
return data
|
||||
|
||||
def __repr__(self):
|
||||
@@ -126,7 +129,7 @@ class Stream:
|
||||
self.name = kwargs.pop("name", None)
|
||||
self.channels = kwargs.pop("channels", [])
|
||||
#self.already_online = kwargs.pop("already_online", False)
|
||||
self._messages_cache = []
|
||||
self._messages_cache = kwargs.pop("_messages_cache", [])
|
||||
self.type = self.__class__.__name__
|
||||
|
||||
async def is_online(self):
|
||||
@@ -140,6 +143,9 @@ class Stream:
|
||||
for k, v in self.__dict__.items():
|
||||
if not k.startswith("_"):
|
||||
data[k] = v
|
||||
data["messages"] = []
|
||||
for m in self._messages_cache:
|
||||
data["messages"].append({"channel": m.channel.id, "message": m.id})
|
||||
return data
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import Counter
|
||||
import yaml
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import redbot.trivia
|
||||
from redbot.ext import trivia as ext_trivia
|
||||
from redbot.core import Config, checks
|
||||
from redbot.core.data_manager import cog_data_path
|
||||
from redbot.core.utils.chat_formatting import box, pagify
|
||||
@@ -482,7 +482,7 @@ class Trivia:
|
||||
personal_lists = tuple(p.resolve()
|
||||
for p in cog_data_path(self).glob("*.yaml"))
|
||||
|
||||
return personal_lists + tuple(redbot.trivia.lists())
|
||||
return personal_lists + tuple(ext_trivia.lists())
|
||||
|
||||
def __unload(self):
|
||||
for session in self.trivia_sessions:
|
||||
|
||||
@@ -1,48 +1,47 @@
|
||||
from copy import copy
|
||||
from discord.ext import commands
|
||||
import asyncio
|
||||
import inspect
|
||||
import discord
|
||||
|
||||
from redbot.core import RedContext, Config, checks
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core import Config, checks, commands
|
||||
from redbot.core.i18n import Translator
|
||||
|
||||
_ = CogI18n("Warnings", __file__)
|
||||
_ = Translator("Warnings", __file__)
|
||||
|
||||
|
||||
async def warning_points_add_check(config: Config, ctx: RedContext, user: discord.Member, points: int):
|
||||
async def warning_points_add_check(config: Config, ctx: commands.Context, user: discord.Member, points: int):
|
||||
"""Handles any action that needs to be taken or not based on the points"""
|
||||
guild = ctx.guild
|
||||
guild_settings = config.guild(guild)
|
||||
act = {}
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for a in registered_actions:
|
||||
if points >= registered_actions[a]["point_count"]:
|
||||
act = registered_actions[a]
|
||||
if points >= a["points"]:
|
||||
act = a
|
||||
else:
|
||||
break
|
||||
if act: # some action needs to be taken
|
||||
await create_and_invoke_context(ctx, act["exceed_command"], user)
|
||||
|
||||
|
||||
async def warning_points_remove_check(config: Config, ctx: RedContext, user: discord.Member, points: int):
|
||||
async def warning_points_remove_check(config: Config, ctx: commands.Context, user: discord.Member, points: int):
|
||||
guild = ctx.guild
|
||||
guild_settings = config.guild(guild)
|
||||
act = {}
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for a in registered_actions:
|
||||
if points >= registered_actions[a]["point_count"]:
|
||||
act = registered_actions[a]
|
||||
if points >= a["points"]:
|
||||
act = a
|
||||
else:
|
||||
break
|
||||
if act: # some action needs to be taken
|
||||
await create_and_invoke_context(ctx, act["drop_command"], user)
|
||||
|
||||
|
||||
async def create_and_invoke_context(realctx: RedContext, command_str: str, user: discord.Member):
|
||||
async def create_and_invoke_context(realctx: commands.Context, command_str: str, user: discord.Member):
|
||||
m = copy(realctx.message)
|
||||
m.content = command_str.format(user=user.mention, prefix=realctx.prefix)
|
||||
fctx = await realctx.bot.get_context(m, cls=RedContext)
|
||||
fctx = await realctx.bot.get_context(m, cls=commands.Context)
|
||||
try:
|
||||
await realctx.bot.invoke(fctx)
|
||||
except (commands.CheckFailure, commands.CommandOnCooldown):
|
||||
@@ -69,7 +68,7 @@ def get_command_from_input(bot, userinput: str):
|
||||
return "{prefix}" + orig, None
|
||||
|
||||
|
||||
async def get_command_for_exceeded_points(ctx: RedContext):
|
||||
async def get_command_for_exceeded_points(ctx: commands.Context):
|
||||
"""Gets the command to be executed when the user is at or exceeding
|
||||
the points threshold for the action"""
|
||||
await ctx.send(
|
||||
@@ -102,7 +101,7 @@ async def get_command_for_exceeded_points(ctx: RedContext):
|
||||
return command
|
||||
|
||||
|
||||
async def get_command_for_dropping_points(ctx: RedContext):
|
||||
async def get_command_for_dropping_points(ctx: commands.Context):
|
||||
"""
|
||||
Gets the command to be executed when the user drops below the points
|
||||
threshold
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from discord.ext import commands
|
||||
import discord
|
||||
import asyncio
|
||||
|
||||
from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \
|
||||
get_command_for_dropping_points, warning_points_remove_check
|
||||
from redbot.core import Config, modlog, checks
|
||||
from redbot.core import Config, modlog, checks, commands
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.context import RedContext
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.i18n import Translator, cog_i18n
|
||||
from redbot.core.utils.mod import is_admin_or_superior
|
||||
from redbot.core.utils.chat_formatting import warning, pagify
|
||||
|
||||
_ = CogI18n("Warnings", __file__)
|
||||
_ = Translator("Warnings", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class Warnings:
|
||||
"""A warning system for Red"""
|
||||
|
||||
@@ -51,14 +50,14 @@ class Warnings:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warningset(self, ctx: RedContext):
|
||||
async def warningset(self, ctx: commands.Context):
|
||||
"""Warning settings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@warningset.command()
|
||||
@commands.guild_only()
|
||||
async def allowcustomreasons(self, ctx: RedContext, allowed: bool):
|
||||
async def allowcustomreasons(self, ctx: commands.Context, allowed: bool):
|
||||
"""Allow or disallow custom reasons for a warning"""
|
||||
guild = ctx.guild
|
||||
await self.config.guild(guild).allow_custom_reasons.set(allowed)
|
||||
@@ -69,14 +68,14 @@ class Warnings:
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warnaction(self, ctx: RedContext):
|
||||
async def warnaction(self, ctx: commands.Context):
|
||||
"""Action management"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@warnaction.command(name="add")
|
||||
@commands.guild_only()
|
||||
async def action_add(self, ctx: RedContext, name: str, points: int):
|
||||
async def action_add(self, ctx: commands.Context, name: str, points: int):
|
||||
"""Create an action to be taken at a specified point count
|
||||
Duplicate action names are not allowed"""
|
||||
guild = ctx.guild
|
||||
@@ -120,12 +119,12 @@ class Warnings:
|
||||
registered_actions.append(to_add)
|
||||
# Sort in descending order by point count for ease in
|
||||
# finding the highest possible action to take
|
||||
registered_actions.sort(key=lambda a: a["point_count"], reverse=True)
|
||||
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
||||
await ctx.tick()
|
||||
|
||||
@warnaction.command(name="del")
|
||||
@commands.guild_only()
|
||||
async def action_del(self, ctx: RedContext, action_name: str):
|
||||
async def action_del(self, ctx: commands.Context, action_name: str):
|
||||
"""Delete the point count action with the specified name"""
|
||||
guild = ctx.guild
|
||||
guild_settings = self.config.guild(guild)
|
||||
@@ -137,18 +136,23 @@ class Warnings:
|
||||
break
|
||||
if to_remove:
|
||||
registered_actions.remove(to_remove)
|
||||
await ctx.tick()
|
||||
else:
|
||||
await ctx.send(
|
||||
_("No action named {} exists!").format(action_name)
|
||||
)
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def warnreason(self, ctx: RedContext):
|
||||
async def warnreason(self, ctx: commands.Context):
|
||||
"""Add reasons for warnings"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@warnreason.command(name="add")
|
||||
@commands.guild_only()
|
||||
async def reason_add(self, ctx: RedContext, name: str, points: int, *, description: str):
|
||||
async def reason_add(self, ctx: commands.Context, name: str, points: int, *, description: str):
|
||||
"""Add a reason to be available for warnings"""
|
||||
guild = ctx.guild
|
||||
|
||||
@@ -172,37 +176,40 @@ class Warnings:
|
||||
|
||||
@warnreason.command(name="del")
|
||||
@commands.guild_only()
|
||||
async def reason_del(self, ctx: RedContext, reason_name: str):
|
||||
async def reason_del(self, ctx: commands.Context, reason_name: str):
|
||||
"""Delete the reason with the specified name"""
|
||||
guild = ctx.guild
|
||||
guild_settings = self.config.guild(guild)
|
||||
async with guild_settings.reasons() as registered_reasons:
|
||||
if registered_reasons.pop(reason_name.lower(), None):
|
||||
await ctx.send(_("Removed reason {}").format(reason_name))
|
||||
await ctx.tick()
|
||||
else:
|
||||
await ctx.send(_("That is not a registered reason name"))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def reasonlist(self, ctx: RedContext):
|
||||
async def reasonlist(self, ctx: commands.Context):
|
||||
"""List all configured reasons for warnings"""
|
||||
guild = ctx.guild
|
||||
guild_settings = self.config.guild(guild)
|
||||
msg_list = []
|
||||
async with guild_settings.reasons() as registered_reasons:
|
||||
for r in registered_reasons.keys():
|
||||
for r, v in registered_reasons.items():
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nAction: {}".format(
|
||||
r, r["points"], r["action"]
|
||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||
r, v["points"], v["description"]
|
||||
)
|
||||
)
|
||||
await ctx.send_interactive(msg_list)
|
||||
if msg_list:
|
||||
await ctx.send_interactive(msg_list)
|
||||
else:
|
||||
await ctx.send(_("There are no reasons configured!"))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def actionlist(self, ctx: RedContext):
|
||||
async def actionlist(self, ctx: commands.Context):
|
||||
"""List the actions to be taken at specific point values"""
|
||||
guild = ctx.guild
|
||||
guild_settings = self.config.guild(guild)
|
||||
@@ -210,16 +217,21 @@ class Warnings:
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for r in registered_actions:
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||
r, r["points"], r["description"]
|
||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
||||
"Drop command: {}".format(
|
||||
r["action_name"], r["points"], r["exceed_command"],
|
||||
r["drop_command"]
|
||||
)
|
||||
)
|
||||
await ctx.send_interactive(msg_list)
|
||||
if msg_list:
|
||||
await ctx.send_interactive(msg_list)
|
||||
else:
|
||||
await ctx.send(_("There are no actions configured!"))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def warn(self, ctx: RedContext, user: discord.Member, reason: str):
|
||||
async def warn(self, ctx: commands.Context, user: discord.Member, reason: str):
|
||||
"""Warn the user for the specified reason
|
||||
Reason must be a registered reason, or custom if custom reasons are allowed"""
|
||||
reason_type = {}
|
||||
@@ -263,7 +275,7 @@ class Warnings:
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
async def warnings(self, ctx: RedContext, userid: int=None):
|
||||
async def warnings(self, ctx: commands.Context, userid: int=None):
|
||||
"""Show warnings for the specified user.
|
||||
If userid is None, show warnings for the person running the command
|
||||
Note that showing warnings for users other than yourself requires
|
||||
@@ -271,7 +283,7 @@ class Warnings:
|
||||
if userid is None:
|
||||
user = ctx.author
|
||||
else:
|
||||
if not is_admin_or_superior(self.bot, ctx.author):
|
||||
if not await is_admin_or_superior(self.bot, ctx.author):
|
||||
await ctx.send(
|
||||
warning(
|
||||
_("You are not allowed to check "
|
||||
@@ -313,7 +325,7 @@ class Warnings:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(ban_members=True)
|
||||
async def unwarn(self, ctx: RedContext, user_id: int, warn_id: str):
|
||||
async def unwarn(self, ctx: commands.Context, user_id: int, warn_id: str):
|
||||
"""Removes the specified warning from the user specified"""
|
||||
guild = ctx.guild
|
||||
member = guild.get_member(user_id)
|
||||
@@ -334,7 +346,7 @@ class Warnings:
|
||||
await ctx.tick()
|
||||
|
||||
@staticmethod
|
||||
async def custom_warning_reason(ctx: RedContext):
|
||||
async def custom_warning_reason(ctx: commands.Context):
|
||||
"""Handles getting description and points for custom reasons"""
|
||||
to_add = {
|
||||
"points": 0,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from .config import Config
|
||||
from .context import RedContext
|
||||
|
||||
__all__ = ["Config", "RedContext", "__version__"]
|
||||
__all__ = ["Config", "__version__"]
|
||||
|
||||
|
||||
class VersionInfo:
|
||||
@@ -33,5 +32,5 @@ class VersionInfo:
|
||||
def to_json(self):
|
||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||
|
||||
__version__ = "3.0.0b12"
|
||||
version_info = VersionInfo(3, 0, 0, 'beta', 12)
|
||||
__version__ = "3.0.0b14"
|
||||
version_info = VersionInfo(3, 0, 0, 'beta', 14)
|
||||
|
||||
@@ -20,7 +20,7 @@ from .cog_manager import CogManager
|
||||
from . import (
|
||||
Config,
|
||||
i18n,
|
||||
RedContext,
|
||||
commands,
|
||||
rpc
|
||||
)
|
||||
from .help_formatter import Help, help as help_
|
||||
@@ -193,7 +193,7 @@ class RedBase(BotBase, RpcMethodMixin):
|
||||
admin_role = await self.db.guild(member.guild).admin_role()
|
||||
return any(role.id in (mod_role, admin_role) for role in member.roles)
|
||||
|
||||
async def get_context(self, message, *, cls=RedContext):
|
||||
async def get_context(self, message, *, cls=commands.Context):
|
||||
return await super().get_context(message, cls=cls)
|
||||
|
||||
def list_packages(self):
|
||||
|
||||
@@ -2,9 +2,26 @@ import discord
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
async def check_overrides(ctx, *, level):
|
||||
if await ctx.bot.is_owner(ctx.author):
|
||||
return True
|
||||
perm_cog = ctx.bot.get_cog('Permissions')
|
||||
if not perm_cog or ctx.cog == perm_cog:
|
||||
return None
|
||||
# don't break if someone loaded a cog named
|
||||
# permissions that doesn't implement this
|
||||
func = getattr(perm_cog, 'check_overrides', None)
|
||||
val = None if func is None else await func(ctx, level)
|
||||
return val
|
||||
|
||||
|
||||
def is_owner(**kwargs):
|
||||
async def check(ctx):
|
||||
return await ctx.bot.is_owner(ctx.author, **kwargs)
|
||||
override = await check_overrides(ctx, level='owner')
|
||||
return (
|
||||
override if override is not None
|
||||
else await ctx.bot.is_owner(ctx.author, **kwargs)
|
||||
)
|
||||
return commands.check(check)
|
||||
|
||||
|
||||
@@ -15,14 +32,16 @@ async def check_permissions(ctx, perms):
|
||||
return False
|
||||
resolved = ctx.channel.permissions_for(ctx.author)
|
||||
|
||||
return all(getattr(resolved, name, None) == value for name, value in perms.items())
|
||||
return all(
|
||||
getattr(resolved, name, None) == value
|
||||
for name, value in perms.items()
|
||||
)
|
||||
|
||||
|
||||
def mod_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||
if ctx.guild is None:
|
||||
return has_perms_or_is_owner
|
||||
async def is_mod_or_superior(ctx):
|
||||
if ctx.guild is None:
|
||||
return await ctx.bot.is_owner(ctx.author)
|
||||
else:
|
||||
author = ctx.author
|
||||
settings = ctx.bot.db.guild(ctx.guild)
|
||||
mod_role_id = await settings.mod_role()
|
||||
@@ -31,25 +50,50 @@ def mod_or_permissions(**perms):
|
||||
mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id)
|
||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
||||
|
||||
is_staff = mod_role in author.roles or admin_role in author.roles
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
return (
|
||||
await ctx.bot.is_owner(ctx.author)
|
||||
or mod_role in author.roles
|
||||
or admin_role in author.roles
|
||||
or author == ctx.guild.owner
|
||||
)
|
||||
|
||||
return is_staff or has_perms_or_is_owner or is_guild_owner
|
||||
|
||||
async def is_admin_or_superior(ctx):
|
||||
if ctx.guild is None:
|
||||
return await ctx.bot.is_owner(ctx.author)
|
||||
else:
|
||||
author = ctx.author
|
||||
settings = ctx.bot.db.guild(ctx.guild)
|
||||
admin_role_id = await settings.admin_role()
|
||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
||||
|
||||
return (
|
||||
await ctx.bot.is_owner(ctx.author)
|
||||
or admin_role in author.roles
|
||||
or author == ctx.guild.owner
|
||||
)
|
||||
|
||||
|
||||
def mod_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
override = await check_overrides(ctx, level='mod')
|
||||
return (
|
||||
override if override is not None
|
||||
else await check_permissions(ctx, perms)
|
||||
or await is_mod_or_superior(ctx)
|
||||
)
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def admin_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||
if ctx.guild is None:
|
||||
return has_perms_or_is_owner
|
||||
author = ctx.author
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
admin_role_id = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
admin_role = discord.utils.get(ctx.guild.roles, id=admin_role_id)
|
||||
|
||||
return admin_role in author.roles or has_perms_or_is_owner or is_guild_owner
|
||||
override = await check_overrides(ctx, level='admin')
|
||||
return (
|
||||
override if override is not None
|
||||
else await check_permissions(ctx, perms)
|
||||
or await is_admin_or_superior(ctx)
|
||||
)
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
@@ -58,7 +102,7 @@ def bot_in_a_guild(**kwargs):
|
||||
async def predicate(ctx):
|
||||
return len(ctx.bot.guilds) > 0
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
|
||||
def guildowner_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
@@ -67,7 +111,11 @@ def guildowner_or_permissions(**perms):
|
||||
return has_perms_or_is_owner
|
||||
is_guild_owner = ctx.author == ctx.guild.owner
|
||||
|
||||
return is_guild_owner or has_perms_or_is_owner
|
||||
override = await check_overrides(ctx, level='guildowner')
|
||||
return (
|
||||
override if override is not None
|
||||
else is_guild_owner or has_perms_or_is_owner
|
||||
)
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
@@ -81,4 +129,4 @@ def admin():
|
||||
|
||||
|
||||
def mod():
|
||||
return mod_or_permissions()
|
||||
return mod_or_permissions()
|
||||
|
||||
@@ -89,6 +89,9 @@ def parse_cli_flags(args):
|
||||
parser.add_argument("--no-cogs",
|
||||
action="store_true",
|
||||
help="Starts Red with no cogs loaded, only core")
|
||||
parser.add_argument("--load-cogs", type=str, nargs="*",
|
||||
help="Force loading specified cogs from the installed packages. "
|
||||
"Can be used with the --no-cogs flag to load these cogs exclusively.")
|
||||
parser.add_argument("--self-bot",
|
||||
action='store_true',
|
||||
help="Specifies if Red should log in as selfbot")
|
||||
@@ -126,3 +129,4 @@ def parse_cli_flags(args):
|
||||
args.prefix = []
|
||||
|
||||
return args
|
||||
|
||||
|
||||
@@ -8,11 +8,10 @@ from typing import Tuple, Union, List
|
||||
import redbot.cogs
|
||||
import discord
|
||||
|
||||
from . import checks
|
||||
from . import checks, commands
|
||||
from .config import Config
|
||||
from .i18n import CogI18n
|
||||
from .i18n import Translator, cog_i18n
|
||||
from .data_manager import cog_data_path
|
||||
from discord.ext import commands
|
||||
|
||||
from .utils.chat_formatting import box, pagify
|
||||
|
||||
@@ -303,10 +302,13 @@ class CogManager:
|
||||
invalidate_caches()
|
||||
|
||||
|
||||
_ = CogI18n("CogManagerUI", __file__)
|
||||
_ = Translator("CogManagerUI", __file__)
|
||||
|
||||
|
||||
@cog_i18n(_)
|
||||
class CogManagerUI:
|
||||
"""Commands to interface with Red's cog manager."""
|
||||
|
||||
async def visible_paths(self, ctx):
|
||||
install_path = await ctx.bot.cog_mgr.install_path()
|
||||
cog_paths = await ctx.bot.cog_mgr.paths()
|
||||
|
||||
4
redbot/core/commands/__init__.py
Normal file
4
redbot/core/commands/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
from discord.ext.commands import *
|
||||
from .commands import *
|
||||
from .context import *
|
||||
74
redbot/core/commands/commands.py
Normal file
74
redbot/core/commands/commands.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Module for command helpers and classes.
|
||||
|
||||
This module contains extended classes and functions which are intended to
|
||||
replace those from the `discord.ext.commands` module.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
__all__ = ["Command", "Group", "command", "group"]
|
||||
|
||||
|
||||
class Command(commands.Command):
|
||||
"""Command class for Red.
|
||||
|
||||
This should not be created directly, and instead via the decorator.
|
||||
|
||||
This class inherits from `discord.ext.commands.Command`.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._help_override = kwargs.pop('help_override', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.translator = kwargs.pop("i18n", None)
|
||||
|
||||
@property
|
||||
def help(self):
|
||||
"""Help string for this command.
|
||||
|
||||
If the :code:`help` kwarg was passed into the decorator, it will
|
||||
default to that. If not, it will attempt to translate the docstring
|
||||
of the command's callback function.
|
||||
"""
|
||||
if self._help_override is not None:
|
||||
return self._help_override
|
||||
if self.translator is None:
|
||||
translator = lambda s: s
|
||||
else:
|
||||
translator = self.translator
|
||||
return inspect.cleandoc(translator(self.callback.__doc__))
|
||||
|
||||
@help.setter
|
||||
def help(self, value):
|
||||
# We don't want our help property to be overwritten, namely by super()
|
||||
pass
|
||||
|
||||
|
||||
class Group(Command, commands.Group):
|
||||
"""Group command class for Red.
|
||||
|
||||
This class inherits from `discord.ext.commands.Group`, with `Command` mixed
|
||||
in.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# decorators
|
||||
|
||||
def command(name=None, cls=Command, **attrs):
|
||||
"""A decorator which transforms an async function into a `Command`.
|
||||
|
||||
Same interface as `discord.ext.commands.command`.
|
||||
"""
|
||||
attrs["help_override"] = attrs.pop("help", None)
|
||||
return commands.command(name, cls, **attrs)
|
||||
|
||||
|
||||
def group(name=None, **attrs):
|
||||
"""A decorator which transforms an async function into a `Group`.
|
||||
|
||||
Same interface as `discord.ext.commands.group`.
|
||||
"""
|
||||
return command(name, cls=Group, **attrs)
|
||||
@@ -1,26 +1,23 @@
|
||||
"""
|
||||
The purpose of this module is to allow for Red to further customise the command
|
||||
invocation context provided by discord.py.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Iterable, List
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
__all__ = ["RedContext"]
|
||||
|
||||
TICK = "\N{WHITE HEAVY CHECK MARK}"
|
||||
|
||||
__all__ = ["Context"]
|
||||
|
||||
class RedContext(commands.Context):
|
||||
|
||||
class Context(commands.Context):
|
||||
"""Command invocation context for Red.
|
||||
|
||||
All context passed into commands will be of this type.
|
||||
|
||||
This class inherits from `commands.Context <discord.ext.commands.Context>`.
|
||||
This class inherits from `discord.ext.commands.Context`.
|
||||
"""
|
||||
|
||||
async def send_help(self) -> List[discord.Message]:
|
||||
@@ -128,12 +125,44 @@ class RedContext(commands.Context):
|
||||
async def embed_requested(self):
|
||||
"""
|
||||
Simple helper to call bot.embed_requested
|
||||
with logic around if embed permissions are available
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool:
|
||||
:code:`True` if an embed is requested
|
||||
"""
|
||||
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
|
||||
return False
|
||||
return await self.bot.embed_requested(
|
||||
self.channel, self.author, command=self.command
|
||||
)
|
||||
|
||||
async def maybe_send_embed(self, message: str) -> discord.Message:
|
||||
"""
|
||||
Simple helper to send a simple message to context
|
||||
without manually checking ctx.embed_requested
|
||||
This should only be used for simple messages.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message: `str`
|
||||
The string to send
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message:
|
||||
the message which was sent
|
||||
|
||||
Raises
|
||||
------
|
||||
discord.Forbidden
|
||||
see `discord.abc.Messageable.send`
|
||||
discord.HTTPException
|
||||
see `discord.abc.Messageable.send`
|
||||
"""
|
||||
|
||||
if await self.embed_requested():
|
||||
return await self.send(embed=discord.Embed(description=message))
|
||||
else:
|
||||
return await self.send(message)
|
||||
@@ -16,13 +16,12 @@ from distutils.version import StrictVersion
|
||||
import aiohttp
|
||||
import discord
|
||||
import pkg_resources
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import __version__
|
||||
from redbot.core import checks
|
||||
from redbot.core import i18n
|
||||
from redbot.core import rpc
|
||||
from redbot.core.context import RedContext
|
||||
from redbot.core import commands
|
||||
from .utils import TYPE_CHECKING
|
||||
from .utils.chat_formatting import pagify, box, inline
|
||||
|
||||
@@ -39,9 +38,10 @@ OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be "
|
||||
"system.** ⚠")
|
||||
|
||||
|
||||
_ = i18n.CogI18n("Core", __file__)
|
||||
_ = i18n.Translator("Core", __file__)
|
||||
|
||||
|
||||
@i18n.cog_i18n(_)
|
||||
class Core:
|
||||
"""Commands related to core functions"""
|
||||
def __init__(self, bot):
|
||||
@@ -51,8 +51,16 @@ class Core:
|
||||
rpc.add_method('core', self.rpc_unload)
|
||||
rpc.add_method('core', self.rpc_reload)
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def ping(self, ctx):
|
||||
"""Pong."""
|
||||
if ctx.guild is None or ctx.channel.permissions_for(ctx.guild.me).add_reactions:
|
||||
await ctx.message.add_reaction("\U0001f3d3") # ping pong paddle
|
||||
else:
|
||||
await ctx.maybe_send_embed("Pong.")
|
||||
|
||||
@commands.command()
|
||||
async def info(self, ctx: RedContext):
|
||||
async def info(self, ctx: commands.Context):
|
||||
"""Shows info about Red"""
|
||||
author_repo = "https://github.com/Twentysix26"
|
||||
org_repo = "https://github.com/Cog-Creators"
|
||||
@@ -72,7 +80,7 @@ class Core:
|
||||
owner = app_info.owner
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
async with session.get('{}/json'.format(red_pypi)) as r:
|
||||
data = await r.json()
|
||||
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
|
||||
about = (
|
||||
@@ -103,7 +111,7 @@ class Core:
|
||||
await ctx.send("I need the `Embed links` permission to send this")
|
||||
|
||||
@commands.command()
|
||||
async def uptime(self, ctx: RedContext):
|
||||
async def uptime(self, ctx: commands.Context):
|
||||
"""Shows Red's uptime"""
|
||||
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
|
||||
passed = self.get_bot_uptime()
|
||||
@@ -112,7 +120,7 @@ class Core:
|
||||
passed, since
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_bot_uptime(self, *, brief=False):
|
||||
# Courtesy of Danny
|
||||
now = datetime.datetime.utcnow()
|
||||
@@ -134,7 +142,7 @@ class Core:
|
||||
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
||||
|
||||
@commands.group()
|
||||
async def embedset(self, ctx: RedContext):
|
||||
async def embedset(self, ctx: commands.Context):
|
||||
"""
|
||||
Commands for toggling embeds on or off.
|
||||
|
||||
@@ -157,7 +165,7 @@ class Core:
|
||||
|
||||
@embedset.command(name="global")
|
||||
@checks.is_owner()
|
||||
async def embedset_global(self, ctx: RedContext):
|
||||
async def embedset_global(self, ctx: commands.Context):
|
||||
"""
|
||||
Toggle the global embed setting.
|
||||
|
||||
@@ -175,7 +183,7 @@ class Core:
|
||||
|
||||
@embedset.command(name="guild")
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def embedset_guild(self, ctx: RedContext, enabled: bool=None):
|
||||
async def embedset_guild(self, ctx: commands.Context, enabled: bool=None):
|
||||
"""
|
||||
Toggle the guild's embed setting.
|
||||
|
||||
@@ -200,7 +208,7 @@ class Core:
|
||||
)
|
||||
|
||||
@embedset.command(name="user")
|
||||
async def embedset_user(self, ctx: RedContext, enabled: bool=None):
|
||||
async def embedset_user(self, ctx: commands.Context, enabled: bool=None):
|
||||
"""
|
||||
Toggle the user's embed setting.
|
||||
|
||||
@@ -280,7 +288,7 @@ class Core:
|
||||
guilds = sorted(list(self.bot.guilds),
|
||||
key=lambda s: s.name.lower())
|
||||
msg = ""
|
||||
for i, server in enumerate(guilds):
|
||||
for i, server in enumerate(guilds, 1):
|
||||
msg += "{}: {}\n".format(i, server.name)
|
||||
|
||||
msg += "\nTo leave a server, just type its number."
|
||||
@@ -298,7 +306,9 @@ class Core:
|
||||
await ctx.send("I guess not.")
|
||||
break
|
||||
try:
|
||||
msg = int(msg.content)
|
||||
msg = int(msg.content) - 1
|
||||
if msg < 0:
|
||||
break
|
||||
await self.leave_confirmation(guilds[msg], owner, ctx)
|
||||
break
|
||||
except (IndexError, ValueError, AttributeError):
|
||||
@@ -313,6 +323,9 @@ class Core:
|
||||
try:
|
||||
msg = await self.bot.wait_for("message", check=conf_check, timeout=15)
|
||||
if msg.content.lower().strip() in ("yes", "y"):
|
||||
if server.owner == ctx.bot.user:
|
||||
await ctx.send("I cannot leave a guild I am the owner of.")
|
||||
return
|
||||
await server.leave()
|
||||
if server != ctx.guild:
|
||||
await ctx.send("Done.")
|
||||
@@ -407,7 +420,7 @@ class Core:
|
||||
"""Reloads packages"""
|
||||
|
||||
cognames = [c.strip() for c in cog_name.split(' ')]
|
||||
|
||||
|
||||
for c in cognames:
|
||||
ctx.bot.unload_extension(c)
|
||||
|
||||
@@ -423,7 +436,7 @@ class Core:
|
||||
except RuntimeError:
|
||||
notfound_packages.append(inline(c))
|
||||
|
||||
for spec, name in cogspecs:
|
||||
for spec, name in cogspecs:
|
||||
try:
|
||||
self.cleanup_and_refresh_modules(spec.name)
|
||||
await ctx.bot.load_extension(spec)
|
||||
@@ -484,7 +497,7 @@ class Core:
|
||||
except:
|
||||
pass
|
||||
await ctx.bot.shutdown()
|
||||
|
||||
|
||||
@commands.command(name="restart")
|
||||
@checks.is_owner()
|
||||
async def _restart(self, ctx, silently: bool=False):
|
||||
@@ -771,26 +784,26 @@ class Core:
|
||||
await ctx.send(_("You have been set as owner."))
|
||||
else:
|
||||
await ctx.send(_("Invalid token."))
|
||||
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def token(self, ctx, token: str):
|
||||
"""Change bot token."""
|
||||
|
||||
if not isinstance(ctx.channel, discord.DMChannel):
|
||||
|
||||
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
|
||||
await ctx.send(
|
||||
_("Please use that command in DM. Since users probably saw your token,"
|
||||
" it is recommended to reset it right now. Go to the following link and"
|
||||
" select `Reveal Token` and `Generate a new token?`."
|
||||
"\n\nhttps://discordapp.com/developers/applications/me/{}").format(self.bot.user.id))
|
||||
return
|
||||
|
||||
|
||||
await ctx.bot.db.token.set(token)
|
||||
await ctx.send("Token set. Restart me.")
|
||||
|
||||
@@ -829,7 +842,7 @@ class Core:
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def listlocales(self, ctx: RedContext):
|
||||
async def listlocales(self, ctx: commands.Context):
|
||||
"""
|
||||
Lists all available locales
|
||||
|
||||
@@ -875,7 +888,7 @@ class Core:
|
||||
if data_dir.exists():
|
||||
home = data_dir.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(data_dir.parent)
|
||||
os.chdir(str(data_dir.parent))
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(data_dir.stem)
|
||||
await ctx.send(_("A backup has been made of this instance. It is at {}.").format(
|
||||
@@ -1046,7 +1059,7 @@ class Core:
|
||||
await ctx.send(_("User has been removed from whitelist."))
|
||||
else:
|
||||
await ctx.send(_("User was not in the whitelist."))
|
||||
|
||||
|
||||
@whitelist.command(name='clear')
|
||||
async def whitelist_clear(self, ctx):
|
||||
"""
|
||||
|
||||
@@ -7,9 +7,8 @@ from contextlib import redirect_stdout
|
||||
from copy import copy
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from . import checks
|
||||
from .i18n import CogI18n
|
||||
from . import checks, commands
|
||||
from .i18n import Translator
|
||||
from .utils.chat_formatting import box, pagify
|
||||
"""
|
||||
Notice:
|
||||
@@ -19,7 +18,7 @@ Notice:
|
||||
https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py
|
||||
"""
|
||||
|
||||
_ = CogI18n("Dev", __file__)
|
||||
_ = Translator("Dev", __file__)
|
||||
|
||||
|
||||
class Dev:
|
||||
|
||||
@@ -61,12 +61,17 @@ def init_events(bot, cli_flags):
|
||||
return
|
||||
|
||||
bot.uptime = datetime.datetime.utcnow()
|
||||
packages = []
|
||||
|
||||
if cli_flags.no_cogs is False:
|
||||
print("Loading packages...")
|
||||
failed = []
|
||||
packages = await bot.db.packages()
|
||||
packages.extend(await bot.db.packages())
|
||||
|
||||
if cli_flags.load_cogs:
|
||||
packages.extend(cli_flags.load_cogs)
|
||||
|
||||
if packages:
|
||||
to_remove = []
|
||||
print("Loading packages...")
|
||||
for package in packages:
|
||||
try:
|
||||
spec = await bot.cog_mgr.find_cog(package)
|
||||
@@ -75,6 +80,9 @@ def init_events(bot, cli_flags):
|
||||
log.exception("Failed to load package {}".format(package),
|
||||
exc_info=e)
|
||||
await bot.remove_loaded_package(package)
|
||||
to_remove.append(package)
|
||||
for package in to_remove:
|
||||
packages.remove(package)
|
||||
if packages:
|
||||
print("Loaded packages: " + ", ".join(packages))
|
||||
|
||||
@@ -110,7 +118,7 @@ def init_events(bot, cli_flags):
|
||||
INFO.append('{} cogs with {} commands'.format(len(bot.cogs), len(bot.commands)))
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
data = await r.json()
|
||||
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
||||
INFO.append(
|
||||
@@ -277,3 +285,4 @@ def _get_startup_screen_specs():
|
||||
ascii_border = False
|
||||
|
||||
return on_symbol, off_symbol, ascii_border
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""The checks in this module run on every command."""
|
||||
from discord.ext import commands
|
||||
from . import commands
|
||||
|
||||
|
||||
def init_global_checks(bot):
|
||||
|
||||
@@ -28,7 +28,6 @@ from collections import namedtuple
|
||||
from typing import List
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import formatter
|
||||
import inspect
|
||||
import itertools
|
||||
@@ -36,6 +35,8 @@ import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from . import commands
|
||||
|
||||
|
||||
EMPTY_STRING = u'\u200b'
|
||||
|
||||
@@ -133,7 +134,12 @@ class Help(formatter.HelpFormatter):
|
||||
'fields': []
|
||||
}
|
||||
|
||||
description = self.command.description if not self.is_cog() else inspect.getdoc(self.command)
|
||||
if self.is_cog():
|
||||
translator = getattr(self.command, '__translator__', lambda s: s)
|
||||
description = inspect.cleandoc(translator(self.command.__doc__))
|
||||
else:
|
||||
description = self.command.description
|
||||
|
||||
if not description == '' and description is not None:
|
||||
description = '*{0}*'.format(description)
|
||||
|
||||
@@ -269,10 +275,18 @@ class Help(formatter.HelpFormatter):
|
||||
color=color)
|
||||
return embed
|
||||
|
||||
def cmd_has_no_subcommands(self, ctx, cmd, color=None):
|
||||
embed = self.simple_embed(
|
||||
ctx,
|
||||
title=ctx.bot.command_has_no_subcommands.format(cmd),
|
||||
color=color
|
||||
)
|
||||
return embed
|
||||
|
||||
@commands.command()
|
||||
async def help(ctx, *cmds: str):
|
||||
"""Shows help documentation.
|
||||
|
||||
[p]**help**: Shows the help manual.
|
||||
[p]**help** command: Show help for a command
|
||||
[p]**help** Category: Show commands and description for a category"""
|
||||
@@ -341,8 +355,7 @@ async def help(ctx, *cmds: str):
|
||||
embed=ctx.bot.formatter.simple_embed(
|
||||
ctx,
|
||||
title='Command "{0.name}" has no subcommands.'.format(command),
|
||||
color=ctx.bot.formatter.color,
|
||||
author=ctx.author.display_name))
|
||||
color=ctx.bot.formatter.color))
|
||||
else:
|
||||
await destination.send(
|
||||
ctx.bot.command_has_no_subcommands.format(command)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ['get_locale', 'set_locale', 'reload_locales', 'CogI18n']
|
||||
from . import commands
|
||||
|
||||
__all__ = ['get_locale', 'set_locale', 'reload_locales', 'cog_i18n',
|
||||
'Translator']
|
||||
|
||||
_current_locale = 'en_us'
|
||||
|
||||
@@ -13,7 +16,7 @@ IN_MSGSTR = 4
|
||||
MSGID = 'msgid "'
|
||||
MSGSTR = 'msgstr "'
|
||||
|
||||
_i18n_cogs = {}
|
||||
_translators = []
|
||||
|
||||
|
||||
def get_locale():
|
||||
@@ -27,8 +30,8 @@ def set_locale(locale):
|
||||
|
||||
|
||||
def reload_locales():
|
||||
for cog_name, i18n in _i18n_cogs.items():
|
||||
i18n.load_translations()
|
||||
for translator in _translators:
|
||||
translator.load_translations()
|
||||
|
||||
|
||||
def _parse(translation_file):
|
||||
@@ -121,6 +124,9 @@ def _normalize(string, remove_newline=False):
|
||||
s += ' '
|
||||
return s
|
||||
|
||||
if string is None:
|
||||
return ""
|
||||
|
||||
string = string.replace('\\n\\n', '\n\n')
|
||||
string = string.replace('\\n', ' ')
|
||||
string = string.replace('\\"', '"')
|
||||
@@ -145,25 +151,36 @@ def get_locale_path(cog_folder: Path, extension: str) -> Path:
|
||||
return cog_folder / 'locales' / "{}.{}".format(get_locale(), extension)
|
||||
|
||||
|
||||
class CogI18n:
|
||||
class Translator:
|
||||
"""Function to get translated strings at runtime."""
|
||||
|
||||
def __init__(self, name, file_location):
|
||||
"""
|
||||
Initializes the internationalization object for a given cog.
|
||||
Initializes an internationalization object.
|
||||
|
||||
:param name: Your cog name.
|
||||
:param file_location:
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
Your cog name.
|
||||
file_location : `str` or `pathlib.Path`
|
||||
This should always be ``__file__`` otherwise your localizations
|
||||
will not load.
|
||||
|
||||
"""
|
||||
self.cog_folder = Path(file_location).resolve().parent
|
||||
self.cog_name = name
|
||||
self.translations = {}
|
||||
|
||||
_i18n_cogs.update({self.cog_name: self})
|
||||
_translators.append(self)
|
||||
|
||||
self.load_translations()
|
||||
|
||||
def __call__(self, untranslated: str):
|
||||
"""Translate the given string.
|
||||
|
||||
This will look for the string in the translator's :code:`.pot` file,
|
||||
with respect to the current locale.
|
||||
"""
|
||||
normalized_untranslated = _normalize(untranslated, True)
|
||||
try:
|
||||
return self.translations[normalized_untranslated]
|
||||
@@ -172,7 +189,7 @@ class CogI18n:
|
||||
|
||||
def load_translations(self):
|
||||
"""
|
||||
Loads the current translations for this cog.
|
||||
Loads the current translations.
|
||||
"""
|
||||
self.translations = {}
|
||||
translation_file = None
|
||||
@@ -201,3 +218,14 @@ class CogI18n:
|
||||
if translated:
|
||||
self.translations.update({untranslated: translated})
|
||||
|
||||
|
||||
def cog_i18n(translator: Translator):
|
||||
"""Get a class decorator to link the translator to this cog."""
|
||||
def decorator(cog_class: type):
|
||||
cog_class.__translator__ = translator
|
||||
for name, attr in cog_class.__dict__.items():
|
||||
if isinstance(attr, (commands.Group, commands.Command)):
|
||||
attr.translator = translator
|
||||
setattr(cog_class, name, attr)
|
||||
return cog_class
|
||||
return decorator
|
||||
|
||||
197
redbot/core/locales/debugging.po
Normal file
197
redbot/core/locales/debugging.po
Normal file
@@ -0,0 +1,197 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR ORGANIZATION
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2017-12-06 11:27+1100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=cp1252\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Generated-By: pygettext.py 1.5\n"
|
||||
|
||||
|
||||
#: ../cog_manager.py:21
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Directory manager for Red's cogs.\n"
|
||||
"\n"
|
||||
" This module allows you to load cogs from multiple directories and even from\n"
|
||||
" outside the bot directory. You may also set a directory for downloader to\n"
|
||||
" install new cogs to, the default being the :code:`cogs/` folder in the root\n"
|
||||
" bot directory.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:40
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Get all currently valid path directories.\n"
|
||||
"\n"
|
||||
" Returns\n"
|
||||
" -------\n"
|
||||
" `tuple` of `pathlib.Path`\n"
|
||||
" All valid cog paths.\n"
|
||||
"\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:64
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Get the install path for 3rd party cogs.\n"
|
||||
"\n"
|
||||
" Returns\n"
|
||||
" -------\n"
|
||||
" pathlib.Path\n"
|
||||
" The path to the directory where 3rd party cogs are stored.\n"
|
||||
"\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:273
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Finds the names of all available modules to load.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:285
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Re-evaluate modules in the py cache.\n"
|
||||
"\n"
|
||||
" This is an alias for an importlib internal and should be called\n"
|
||||
" any time that a new module has been installed to a cog directory.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:298
|
||||
#, docstring
|
||||
msgid ""
|
||||
"Commands to interface with Red's cog manager."
|
||||
msgstr ""
|
||||
"(TRANSLATED) Commands to interface with Red's cog manager."
|
||||
|
||||
#: ../cog_manager.py:302
|
||||
#, docstring
|
||||
msgid ""
|
||||
"\n"
|
||||
" Lists current cog paths in order of priority."
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" (TRANSLATED) Lists current cog paths in order of priority."
|
||||
" "
|
||||
|
||||
#: ../cog_manager.py:321
|
||||
#, docstring
|
||||
msgid ""
|
||||
"\n"
|
||||
" Add a path to the list of available cog paths."
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" (TRANSLATED) Add a path to the list of available cog paths."
|
||||
" "
|
||||
|
||||
#: ../cog_manager.py:340
|
||||
#, docstring
|
||||
msgid ""
|
||||
"\n"
|
||||
" Removes a path from the available cog paths given the path_number"
|
||||
" from !paths"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" (TRANSLATED) Removes a path from the available cog paths given the path_number"
|
||||
" from !paths"
|
||||
" "
|
||||
|
||||
#: ../cog_manager.py:357
|
||||
#, docstring
|
||||
msgid ""
|
||||
"\n"
|
||||
" Reorders paths internally to allow discovery of different cogs."
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" (TRANSLATED) Reorders paths internally to allow discovery of different cogs."
|
||||
" "
|
||||
|
||||
#: ../cog_manager.py:383
|
||||
#, docstring
|
||||
msgid ""
|
||||
"\n"
|
||||
" Returns the current install path or sets it if one is provided."
|
||||
" The provided path must be absolute or relative to the bot's"
|
||||
" directory and it must already exist."
|
||||
"\n"
|
||||
" No installed cogs will be transferred in the process."
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" (TRANSLATED) Returns the current install path or sets it if one is provided."
|
||||
" The provided path must be absolute or relative to the bot's"
|
||||
" directory and it must already exist."
|
||||
"\n"
|
||||
" No installed cogs will be transferred in the process."
|
||||
" "
|
||||
|
||||
#: ../cog_manager.py:406
|
||||
#, docstring
|
||||
msgid ""
|
||||
"\n"
|
||||
" Lists all loaded and available cogs."
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" (TRANSLATED) Lists all loaded and available cogs."
|
||||
" "
|
||||
|
||||
#: ../cog_manager.py:309
|
||||
msgid ""
|
||||
"Install Path: {}\n"
|
||||
"\n"
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:325
|
||||
msgid "That path is does not exist or does not point to a valid directory."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:334
|
||||
msgid "Path successfully added."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:347
|
||||
msgid "That is an invalid path number."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:351
|
||||
msgid "Path successfully removed."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:367
|
||||
msgid "Invalid 'from' index."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:373
|
||||
msgid "Invalid 'to' index."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:377
|
||||
msgid "Paths reordered."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:395
|
||||
msgid "That path does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: ../cog_manager.py:399
|
||||
msgid "The bot will install new cogs to the `{}` directory."
|
||||
msgstr ""
|
||||
|
||||
@@ -22,4 +22,6 @@ def safe_delete(pth: Path):
|
||||
os.chmod(root, 0o755)
|
||||
for d in dirs:
|
||||
os.chmod(os.path.join(root, d), 0o755)
|
||||
for f in files:
|
||||
os.chmod(os.path.join(root, f), 0o755)
|
||||
shutil.rmtree(str(pth), ignore_errors=True)
|
||||
|
||||
141
redbot/core/utils/menus.py
Normal file
141
redbot/core/utils/menus.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Original source of reaction-based menu idea from
|
||||
https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
|
||||
|
||||
Ported to Red V3 by Palm__ (https://github.com/palmtree5)
|
||||
"""
|
||||
import asyncio
|
||||
import discord
|
||||
|
||||
from redbot.core import commands
|
||||
|
||||
|
||||
async def menu(ctx: commands.Context, pages: list,
|
||||
controls: dict,
|
||||
message: discord.Message=None, page: int=0,
|
||||
timeout: float=30.0):
|
||||
"""
|
||||
An emoji-based menu
|
||||
|
||||
.. note:: All pages should be of the same type
|
||||
|
||||
.. note:: All functions for handling what a particular emoji does
|
||||
should be coroutines (i.e. :code:`async def`). Additionally,
|
||||
they must take all of the parameters of this function, in
|
||||
addition to a string representing the emoji reacted with.
|
||||
This parameter should be the last one, and none of the
|
||||
parameters in the handling functions are optional
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx: commands.Context
|
||||
The command context
|
||||
pages: `list` of `str` or `discord.Embed`
|
||||
The pages of the menu.
|
||||
controls: dict
|
||||
A mapping of emoji to the function which handles the action for the
|
||||
emoji.
|
||||
message: discord.Message
|
||||
The message representing the menu. Usually :code:`None` when first opening
|
||||
the menu
|
||||
page: int
|
||||
The current page number of the menu
|
||||
timeout: float
|
||||
The time (in seconds) to wait for a reaction
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If either of the notes above are violated
|
||||
"""
|
||||
if not all(isinstance(x, discord.Embed) for x in pages) and\
|
||||
not all(isinstance(x, str) for x in pages):
|
||||
raise RuntimeError("All pages must be of the same type")
|
||||
for key, value in controls.items():
|
||||
if not asyncio.iscoroutinefunction(value):
|
||||
raise RuntimeError("Function must be a coroutine")
|
||||
current_page = pages[page]
|
||||
|
||||
if not message:
|
||||
if isinstance(current_page, discord.Embed):
|
||||
message = await ctx.send(embed=current_page)
|
||||
else:
|
||||
message = await ctx.send(current_page)
|
||||
for key in controls.keys():
|
||||
await message.add_reaction(key)
|
||||
else:
|
||||
if isinstance(current_page, discord.Embed):
|
||||
await message.edit(embed=current_page)
|
||||
else:
|
||||
await message.edit(content=current_page)
|
||||
|
||||
def react_check(r, u):
|
||||
return u == ctx.author and r.message.id == message.id and \
|
||||
str(r.emoji) in controls.keys()
|
||||
|
||||
try:
|
||||
react, user = await ctx.bot.wait_for(
|
||||
"reaction_add",
|
||||
check=react_check,
|
||||
timeout=timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
try:
|
||||
await message.clear_reactions()
|
||||
except discord.Forbidden: # cannot remove all reactions
|
||||
for key in controls.keys():
|
||||
await message.remove_reaction(key, ctx.bot.user)
|
||||
return None
|
||||
|
||||
return await controls[react.emoji](ctx, pages, controls,
|
||||
message, page,
|
||||
timeout, react.emoji)
|
||||
|
||||
|
||||
async def next_page(ctx: commands.Context, pages: list,
|
||||
controls: dict, message: discord.Message, page: int,
|
||||
timeout: float, emoji: str):
|
||||
perms = message.channel.permissions_for(ctx.guild.me)
|
||||
if perms.manage_messages: # Can manage messages, so remove react
|
||||
try:
|
||||
await message.remove_reaction(emoji, ctx.author)
|
||||
except discord.NotFound:
|
||||
pass
|
||||
if page == len(pages) - 1:
|
||||
page = 0 # Loop around to the first item
|
||||
else:
|
||||
page = page + 1
|
||||
return await menu(ctx, pages, controls, message=message,
|
||||
page=page, timeout=timeout)
|
||||
|
||||
|
||||
async def prev_page(ctx: commands.Context, pages: list,
|
||||
controls: dict, message: discord.Message, page: int,
|
||||
timeout: float, emoji: str):
|
||||
perms = message.channel.permissions_for(ctx.guild.me)
|
||||
if perms.manage_messages: # Can manage messages, so remove react
|
||||
try:
|
||||
await message.remove_reaction(emoji, ctx.author)
|
||||
except discord.NotFound:
|
||||
pass
|
||||
if page == 0:
|
||||
next_page = len(pages) - 1 # Loop around to the last item
|
||||
else:
|
||||
next_page = page - 1
|
||||
return await menu(ctx, pages, controls, message=message,
|
||||
page=next_page, timeout=timeout)
|
||||
|
||||
|
||||
async def close_menu(ctx: commands.Context, pages: list,
|
||||
controls: dict, message: discord.Message, page: int,
|
||||
timeout: float, emoji: str):
|
||||
if message:
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
|
||||
DEFAULT_CONTROLS = {
|
||||
"⬅": prev_page,
|
||||
"❌": close_menu,
|
||||
"➡": next_page
|
||||
}
|
||||
@@ -4,6 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
|
||||
import io
|
||||
import sys
|
||||
import weakref
|
||||
from typing import List
|
||||
|
||||
_instances = weakref.WeakValueDictionary({})
|
||||
|
||||
@@ -94,6 +95,88 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
def minutes_since(self):
|
||||
return int((self.last_interaction - datetime.utcnow()).seconds / 60)
|
||||
|
||||
@staticmethod
|
||||
async def message_forwarder(
|
||||
*, destination: discord.abc.Messageable,
|
||||
content: str=None, embed=None, files=[]) -> List[discord.Message]:
|
||||
"""
|
||||
This does the actual sending, use this instead of a full tunnel
|
||||
if you are using command initiated reactions instead of persistent
|
||||
event based ones
|
||||
|
||||
Parameters
|
||||
----------
|
||||
destination: `discord.abc.Messageable`
|
||||
Where to send
|
||||
content: `str`
|
||||
The message content
|
||||
embed: `discord.Embed`
|
||||
The embed to send
|
||||
files: `List[discord.Files]`
|
||||
A list of files to send.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of `discord.Message`
|
||||
The `discord.Message`(s) sent as a result
|
||||
|
||||
Raises
|
||||
------
|
||||
discord.Forbidden
|
||||
see `discord.abc.Messageable.send`
|
||||
discord.HTTPException
|
||||
see `discord.abc.Messageable.send`
|
||||
"""
|
||||
rets = []
|
||||
files = files if files else None
|
||||
if content:
|
||||
for page in pagify(content):
|
||||
rets.append(
|
||||
await destination.send(
|
||||
page, files=files, embed=embed)
|
||||
)
|
||||
if files:
|
||||
del files
|
||||
if embed:
|
||||
del embed
|
||||
elif embed or files:
|
||||
rets.append(
|
||||
await destination.send(files=files, embed=embed)
|
||||
)
|
||||
return rets
|
||||
|
||||
@staticmethod
|
||||
async def files_from_attatch(m: discord.Message) -> List[discord.File]:
|
||||
"""
|
||||
makes a list of file objects from a message
|
||||
returns an empty list if none, or if the sum of file sizes
|
||||
is too large for the bot to send
|
||||
|
||||
Parameters
|
||||
---------
|
||||
m: `discord.Message`
|
||||
A message to get attachments from
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of `discord.File`
|
||||
A list of `discord.File` objects
|
||||
|
||||
"""
|
||||
files = []
|
||||
size = 0
|
||||
max_size = 8 * 1024 * 1024
|
||||
for a in m.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
size += sys.getsizeof(_fp)
|
||||
if size > max_size:
|
||||
return []
|
||||
files.append(
|
||||
discord.File(_fp, filename=a.filename)
|
||||
)
|
||||
return files
|
||||
|
||||
async def communicate(self, *,
|
||||
message: discord.Message,
|
||||
topic: str=None,
|
||||
@@ -140,35 +223,22 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
else:
|
||||
content = topic
|
||||
|
||||
attach = None
|
||||
if message.attachments:
|
||||
files = []
|
||||
size = 0
|
||||
max_size = 8 * 1024 * 1024
|
||||
for a in message.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
size += sys.getsizeof(_fp)
|
||||
if size > max_size:
|
||||
await send_to.send(
|
||||
"Could not forward attatchments. "
|
||||
"Total size of attachments in a single "
|
||||
"message must be less than 8MB."
|
||||
)
|
||||
break
|
||||
files.append(
|
||||
discord.File(_fp, filename=a.filename)
|
||||
attach = await self.files_from_attatch(message)
|
||||
if not attach:
|
||||
await message.channel.send(
|
||||
"Could not forward attatchments. "
|
||||
"Total size of attachments in a single "
|
||||
"message must be less than 8MB."
|
||||
)
|
||||
else:
|
||||
attach = files
|
||||
else:
|
||||
attach = []
|
||||
|
||||
rets = []
|
||||
for page in pagify(content):
|
||||
rets.append(
|
||||
await send_to.send(content, files=attach)
|
||||
)
|
||||
if attach:
|
||||
del attach
|
||||
rets = await self.message_forwarder(
|
||||
destination=send_to,
|
||||
content=content,
|
||||
files=attach
|
||||
)
|
||||
|
||||
await message.add_reaction("\N{WHITE HEAVY CHECK MARK}")
|
||||
await message.add_reaction("\N{NEGATIVE SQUARED CROSS MARK}")
|
||||
|
||||
@@ -7,7 +7,10 @@ import argparse
|
||||
import asyncio
|
||||
|
||||
import pkg_resources
|
||||
from redbot.setup import basic_setup, load_existing_config, remove_instance
|
||||
from pathlib import Path
|
||||
from redbot.setup import basic_setup, load_existing_config, remove_instance, remove_instance_interaction, create_backup, save_config
|
||||
from redbot.core.utils import safe_delete
|
||||
from redbot.core.cli import confirm
|
||||
|
||||
if sys.platform == "linux":
|
||||
import distro
|
||||
@@ -60,7 +63,7 @@ def parse_cli_args():
|
||||
return parser.parse_known_args()
|
||||
|
||||
|
||||
def update_red(dev=False, voice=False, mongo=False, docs=False, test=False):
|
||||
def update_red(dev=False, reinstall=False, voice=False, mongo=False, docs=False, test=False):
|
||||
interpreter = sys.executable
|
||||
print("Updating Red...")
|
||||
# If the user ran redbot-launcher.exe, updating with pip will fail
|
||||
@@ -93,12 +96,21 @@ def update_red(dev=False, voice=False, mongo=False, docs=False, test=False):
|
||||
package = "Red-DiscordBot"
|
||||
if egg_l:
|
||||
package += "[{}]".format(", ".join(egg_l))
|
||||
code = subprocess.call([
|
||||
interpreter, "-m",
|
||||
"pip", "install", "-U",
|
||||
"--process-dependency-links",
|
||||
package
|
||||
])
|
||||
if reinstall:
|
||||
code = subprocess.call([
|
||||
interpreter, "-m",
|
||||
"pip", "install", "-U", "-I",
|
||||
"--force-reinstall", "--no-cache-dir",
|
||||
"--process-dependency-links",
|
||||
package
|
||||
])
|
||||
else:
|
||||
code = subprocess.call([
|
||||
interpreter, "-m",
|
||||
"pip", "install", "-U",
|
||||
"--process-dependency-links",
|
||||
package
|
||||
])
|
||||
if code == 0:
|
||||
print("Red has been updated")
|
||||
else:
|
||||
@@ -223,6 +235,37 @@ def instance_menu():
|
||||
return name_num_map[str(selection)]
|
||||
|
||||
|
||||
async def reset_red():
|
||||
instances = load_existing_config()
|
||||
|
||||
if not instances:
|
||||
print("No instance to delete.\n")
|
||||
return
|
||||
print("WARNING: You are about to remove ALL Red instances on this computer.")
|
||||
print("If you want to reset data of only one instance, "
|
||||
"please select option 5 in the launcher.")
|
||||
await asyncio.sleep(2)
|
||||
print("\nIf you continue you will remove these instanes.\n")
|
||||
for instance in list(instances.keys()):
|
||||
print(" - {}".format(instance))
|
||||
await asyncio.sleep(3)
|
||||
print('\nIf you want to reset all instances, type "I agree".')
|
||||
response = input("> ").strip()
|
||||
if response != "I agree":
|
||||
print("Cancelling...")
|
||||
return
|
||||
|
||||
if confirm("\nDo you want to create a backup for an instance? (y/n) "):
|
||||
for index, instance in instances.items():
|
||||
print("\nRemoving {}...".format(index))
|
||||
await create_backup(index, instance)
|
||||
await remove_instance(index, instance)
|
||||
else:
|
||||
for index, instance in instances.items():
|
||||
await remove_instance(index, instance)
|
||||
print("All instances have been removed.")
|
||||
|
||||
|
||||
def clear_screen():
|
||||
if IS_WINDOWS:
|
||||
os.system("cls")
|
||||
@@ -247,6 +290,33 @@ def extras_selector():
|
||||
return selected
|
||||
|
||||
|
||||
def development_choice(reinstall = False):
|
||||
while True:
|
||||
print("\n")
|
||||
print("Do you want to install stable or development version?")
|
||||
print("1. Stable version")
|
||||
print("2. Development version")
|
||||
choice = user_choice()
|
||||
print("\n")
|
||||
selected = extras_selector()
|
||||
if choice == "1":
|
||||
update_red(
|
||||
dev=False, reinstall=reinstall, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
break
|
||||
elif choice == "2":
|
||||
update_red(
|
||||
dev=True, reinstall=reinstall, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
def debug_info():
|
||||
pyver = sys.version
|
||||
redver = pkg_resources.get_distribution("Red-DiscordBot").version
|
||||
@@ -275,55 +345,64 @@ def debug_info():
|
||||
def main_menu():
|
||||
if IS_WINDOWS:
|
||||
os.system("TITLE Red - Discord Bot V3 Launcher")
|
||||
clear_screen()
|
||||
while True:
|
||||
print(INTRO)
|
||||
print("1. Run Red w/ autorestart in case of issues")
|
||||
print("2. Run Red")
|
||||
print("3. Update Red")
|
||||
print("4. Update Red (development version)")
|
||||
print("5. Create Instance")
|
||||
print("6. Remove Instance")
|
||||
print("7. Debug information (use this if having issues with the launcher or bot)")
|
||||
print("4. Create Instance")
|
||||
print("5. Remove Instance")
|
||||
print("6. Debug information (use this if having issues with the launcher or bot)")
|
||||
print("7. Reinstall Red")
|
||||
print("0. Exit")
|
||||
choice = user_choice()
|
||||
if choice == "1":
|
||||
instance = instance_menu()
|
||||
cli_flags = cli_flag_getter()
|
||||
if instance:
|
||||
cli_flags = cli_flag_getter()
|
||||
run_red(instance, autorestart=True, cliflags=cli_flags)
|
||||
wait()
|
||||
elif choice == "2":
|
||||
instance = instance_menu()
|
||||
cli_flags = cli_flag_getter()
|
||||
if instance:
|
||||
cli_flags = cli_flag_getter()
|
||||
run_red(instance, autorestart=False, cliflags=cli_flags)
|
||||
wait()
|
||||
elif choice == "3":
|
||||
selected = extras_selector()
|
||||
update_red(
|
||||
dev=False, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
development_choice()
|
||||
wait()
|
||||
elif choice == "4":
|
||||
selected = extras_selector()
|
||||
update_red(
|
||||
dev=True, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
wait()
|
||||
elif choice == "5":
|
||||
basic_setup()
|
||||
wait()
|
||||
elif choice == "6":
|
||||
asyncio.get_event_loop().run_until_complete(remove_instance())
|
||||
elif choice == "5":
|
||||
asyncio.get_event_loop().run_until_complete(remove_instance_interaction())
|
||||
wait()
|
||||
elif choice == "7":
|
||||
elif choice == "6":
|
||||
debug_info()
|
||||
elif choice == "7":
|
||||
while True:
|
||||
loop = asyncio.get_event_loop()
|
||||
clear_screen()
|
||||
print("==== Reinstall Red ====")
|
||||
print("1. Reinstall Red requirements (discard code changes, keep data and 3rd party cogs)")
|
||||
print("2. Reset all data")
|
||||
print("3. Factory reset (discard code changes, reset all data)")
|
||||
print("\n")
|
||||
print("0. Back")
|
||||
choice = user_choice()
|
||||
if choice == "1":
|
||||
development_choice(reinstall=True)
|
||||
wait()
|
||||
elif choice == "2":
|
||||
loop.run_until_complete(reset_red())
|
||||
wait()
|
||||
elif choice == "3":
|
||||
loop.run_until_complete(reset_red())
|
||||
development_choice(reinstall=True)
|
||||
wait()
|
||||
elif choice == "0":
|
||||
break
|
||||
elif choice == "0":
|
||||
break
|
||||
clear_screen()
|
||||
|
||||
128
redbot/setup.py
128
redbot/setup.py
@@ -302,7 +302,61 @@ async def edit_instance():
|
||||
)
|
||||
|
||||
|
||||
async def remove_instance():
|
||||
async def create_backup(selected, instance_data):
|
||||
if confirm("Would you like to make a backup of the data for this instance? (y/n)"):
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
print("Backing up the instance's data...")
|
||||
await mongo_to_json(instance_data["DATA_PATH"], instance_data["STORAGE_DETAILS"])
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent))
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem)
|
||||
print("A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
))
|
||||
|
||||
else:
|
||||
print("Backing up the instance's data...")
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent)) # str is used here because 3.5 support
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem) # add all files in that directory
|
||||
print(
|
||||
"A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def remove_instance(selected, instance_data):
|
||||
instance_list = load_existing_config()
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collections = await db.collection_names(include_system_collections=False)
|
||||
for name in collections:
|
||||
collection = await db.get_collection(name)
|
||||
await collection.drop()
|
||||
else:
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance {} has been removed\n".format(selected))
|
||||
|
||||
|
||||
async def remove_instance_interaction():
|
||||
instance_list = load_existing_config()
|
||||
if not instance_list:
|
||||
print("No instances have been set up!")
|
||||
@@ -321,79 +375,15 @@ async def remove_instance():
|
||||
print("That isn't a valid instance!")
|
||||
return
|
||||
instance_data = instance_list[selected]
|
||||
|
||||
if confirm("Would you like to make a backup of the data for this instance? (y/n)"):
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
print("Backing up the instance's data...")
|
||||
await mongo_to_json(instance_data["DATA_PATH"], instance_data["STORAGE_DETAILS"])
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent))
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem)
|
||||
print("A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
))
|
||||
print("Removing the instance...")
|
||||
|
||||
m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collections = await db.collection_names(include_system_collections=False)
|
||||
for name in collections:
|
||||
collection = await db.get_collection(name)
|
||||
await collection.drop()
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed.")
|
||||
return
|
||||
else:
|
||||
print("Backing up the instance's data...")
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent)) # str is used here because 3.5 support
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem) # add all files in that directory
|
||||
print(
|
||||
"A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
)
|
||||
)
|
||||
print("Removing the instance...")
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed")
|
||||
return
|
||||
else:
|
||||
print("Removing the instance...")
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collections = await db.collection_names(include_system_collections=False)
|
||||
for name in collections:
|
||||
collection = await db.get_collection(name)
|
||||
await collection.drop()
|
||||
else:
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed")
|
||||
return
|
||||
|
||||
await create_backup(selected, instance_data)
|
||||
await remove_instance(selected, instance_data)
|
||||
|
||||
|
||||
def main():
|
||||
if args.delete:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(remove_instance())
|
||||
loop.run_until_complete(remove_instance_interaction())
|
||||
elif args.edit:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(edit_instance())
|
||||
|
||||
@@ -5,4 +5,4 @@ raven==6.5.0
|
||||
colorama==0.3.9
|
||||
aiohttp-json-rpc==0.8.7
|
||||
pyyaml==3.12
|
||||
Red-Trivia
|
||||
Red-Trivia>=1.1.1
|
||||
|
||||
Reference in New Issue
Block a user