Compare commits

...

43 Commits

Author SHA1 Message Date
palmtree5 501aff41ea [V3] Bump version to 3.0.0b14 (#1629) 2018-05-13 16:24:40 -08:00
Michael H 449b1bfe9e Make checks.py manageable from a permissions cog (#1547)
* This starts setting up checks.py to handle managed permission overrides

* missing else fix

* don't bypass is_owner, ever

* Modifies the predicates so that their inner functions are accesible from cogs without
being a check

* Update checks.py

Safety for existing permissions.py cogs

* block permissions cog from being unblocked by the permissions cog as a safety feature (really, co-owner exists at this point)

* un mix the 2 PRs (*sigh*)

* Update checks.py

remove debug prints that got lost inshuffle
2018-05-14 10:13:16 +10:00
aikaterna 4a8358ecb4 [V3 Audio] Update queue and search to use menus (#1633)
* [V3 Audio] Update queue and search to use menus

* [V3 Audio] Fix for playlist upload saving

* [V3 Audio] Add position in queue to enqueued songs

Also a bit of cleanup.

* [V3 Audio] Improvements for mobile formatting
2018-05-14 10:01:46 +10:00
Tobotimus 8f74e4dd31 [V3 Cleanup] Cleanup commands clean up after themselves (#1602)
Resolves #1572
2018-05-13 15:51:50 -08:00
Michael H 2b35d9f012 [V3 cleanup] Respect pinned messages by default (#1596)
* This sets the default behavior for `get_messages_for_deletetion()` to not include pinned messages, while providing a way to override that

resolves #1589

* actually make commands parse for pinned deletion

* fix capitalization
2018-05-13 15:49:45 -08:00
palmtree5 35001107e0 [V3 Streams] cache stream alert messages across restarts (#1630)
* [V3 Streams] cache stream alert messages across restarts

* Add some stuff to debug this

* More debug stuff

* More debug stuff

* Actually save when updating a stream alert

* Remove debug stuff

Fixes #1620
2018-05-14 09:42:28 +10:00
Leo Garcia a7d7b90ae8 [V3] Removed py 3.6 warning for Windows (#1622)
I believe we've fixed this awhile ago.
2018-05-14 09:32:41 +10:00
Tobotimus 119ba7ef8b [V3 ModLog] Fix [p]reason when the modlog case has no moderator (#1604) 2018-05-14 09:24:17 +10:00
palmtree5 28bbe9c646 [V3 i18n] add a NoneType check on trying to normalize a string (#1632)
Fixes #1631
2018-05-14 09:10:38 +10:00
Michael H 8739c04024 [V3] Ping changes (#1618)
* moves ping to core commands
defaults ping behavior to reacting with a ping pong paddle with ball
adds an optional boolean flag to ping to get the avg latency from the bot
(strikes a middle ground with intended behavior from dev standpoint, and how users want it)

* casing for @Kowlin

* use correct check for permissions

* remove latency
2018-05-13 15:03:17 -08:00
Tobotimus 57240d25b9 [V3] Update trivia version and allow installing in develop mode (#1635)
* [V3 Trivia] Update trivia version to >1.1

* Use actually working trivia version
2018-05-13 13:43:16 -08:00
Tobotimus 15ea5440a3 [V3 i18n] Internationalise help for commands and cogs (#1143)
* Framework for internationalised command help

* Translator for class docstring of cog

* Remove references to old context module

* Use CogManagerUI as PoC

* Replace all references to RedContext

* Rename CogI18n object to avoid confusion

* Update docs

* Update i18n docs.

* Store translators in list instead of dict

* Change commands module to package, updated refs in cogs

* Updated docs and more references in cogs

* Resolve syntax error

* Update from merge
2018-05-12 01:47:49 +02:00
Michael H 1e60d1c265 [V3] adds a permissions check for embed_links in ctx.embed_requested (#1619) 2018-05-10 14:35:18 -08:00
Tobotimus b7cd097c43 [V3 Trivia] Lock trivia version to <1.1 (#1621) 2018-05-10 13:20:40 -08:00
bobloy 6c934b02e6 [V3] Fix help's help (#1606) 2018-05-10 13:14:44 -08:00
Kowlin fcb9b40b43 [V3] Fixed [p]servers bug (#1617)
* Fixed servers bug

* Added protections against going negative
2018-05-10 13:10:42 -08:00
Michael H 7a6884e4b1 [V3] Mark 3.7 as unsupported in setup.py (#1623) 2018-05-10 13:04:20 -08:00
Michael H e86698cfeb [V3] Update some user facing info (remove old, outdated info) (#1613)
* remove outdated link in favor of in docstring docsumentation

* Update default Downloader repo url to org repo url (don't rely on github redirect)
2018-05-08 22:27:38 +02:00
Bakersbakebread 53650aefa6 [Docs] Added self (#1608) 2018-05-08 19:47:11 +02:00
Tobotimus 1d80a0cad1 [V3 Mod] Fix issue with unmuting, again (#1603)
* [V3 Mod] Fix issue with unmuting, again

Resolves #1595

* Fix typo
2018-05-07 13:31:14 +02:00
retke f6d27a0f43 [V3 Parser] Added --load-cogs flag (#1601)
* [V3 Parser] Added --load-cogs flag

* Removed old PR data

* Removed old PR data

* Removed old PR data

* Slightly reword help for flag

* Stick to convention for checking if sequence is empty

* Fix some logic errors

* Don't print packages which failed to load
2018-05-07 15:01:44 +10:00
Wyn f71aa9dd21 [V3 Docs] Autostart (#1599)
Moved note to the top, added how to access red log.
2018-05-05 15:43:26 -08:00
palmtree5 1cb5394e96 [V3] bump version to 3.0.0b13 (#1583) 2018-05-04 08:48:33 +02:00
palmtree5 2b2dbd25f7 [V3 Help] fix issue with non-existent subcommands (#1565) 2018-05-03 22:19:24 -08:00
Michael H dd4cd0eeb1 provide an extra method for helping wor with embed_requested (#1558) 2018-05-04 08:16:24 +02:00
palmtree5 ee7b0cf730 [V3 Utils] fix files not being chmodded (#1578) 2018-05-04 08:10:56 +02:00
retke 95ef5d6348 [V3 Launcher] Reinstall Red option (#1536)
* [V3 Launcher] Reinstall Red option

* [V3 Setup] Divided remove_instance function

* Removing changes from another PR

* Indent fails fix

* use remove_instance_interaction for --delete

* Fix some issues with remove_instance

removed `index: int` because what's being passed there is a string
data -> instance_data

* bug fixes, working version
2018-05-04 08:01:37 +02:00
bobloy 23192b9ef6 simple_embed doesn't take author (#1555)
Simple embed doesn't use ctx.author as author
2018-05-04 07:27:44 +02:00
Michael H 7cd98c8a63 Report fixes + improvements (#1541)
* WIP

* fix perms issue

* better

* more work

* working

* working, tessted

* docs

* mutable default fix
2018-05-04 06:38:58 +02:00
palmtree5 fca7686701 [V3 Core] fix 3.5-specific issue with [p]backup (#1586) (#1588) 2018-05-04 06:18:44 +02:00
Michael H be767478f4 allow deletion based on user ID (actually this time) (#1561) 2018-05-04 06:15:27 +02:00
palmtree5 b3ad5d90ed [V3 Core] fix a couple issues with [p]servers (#1580) 2018-05-04 06:05:09 +02:00
palmtree5 fb093b7411 [V3 Utils] Menu system (#1566)
* [V3 Utils] start on a menu system

* Fix conflicting names

* [V3 Menus] change order of default controls

* [V3 Menus] add a message check to the react check

* Add a note about original source and who ported

* Compare message ids, not the objects themselves
2018-05-04 05:54:30 +02:00
palmtree5 e4ea3110e3 [V3 Warnings] fix several bugs found (#1577) 2018-05-04 05:46:59 +02:00
aikaterna 79676c4f72 Playlist additions and cleanup (#1579)
Add playlist append, create, remove, and upload.
2018-05-04 05:43:00 +02:00
Wyn d61827b92c [V3 Docs] Fixed broken link (#1567)
Guide migration went into a maze, this should fix it.
2018-05-04 05:33:01 +02:00
palmtree5 1f1f46c70f [V3] move to multiple issue/pr templates (#1585) 2018-05-04 03:58:30 +02:00
Wyn 9188e4a7ec [V3 Info] Don't rely on redirect (#1581)
* [V3 Info] Don't rely on redirect

Http -> Https

* Update core_commands.py

Use existing variable instead of new string

* Update events.py

Remove redirect, url only reference
2018-05-02 10:35:48 +02:00
Redjumpman e5a780eb0c Update mod.py (#1582)
Update doc-strings to properly format in the help text.
2018-05-01 09:13:25 +02:00
palmtree5 d8c85a2b15 [V3 Audio] fix zombie process on unload (#1575) 2018-04-29 08:19:49 +02:00
palmtree5 83080bc5a2 [V3 Mod] fix issue with unmuting (#1568) 2018-04-28 13:39:06 +10:00
Wyn 233bfc59ac [V3 Docs Arch] Upgrade dependencies (#1553)
-u parameter added to pacman for upgrading dependencies so we don't get partial upgrades.
2018-04-19 13:29:44 -08:00
Bakersbakebread c606caf3a3 Grammar Change With -> Will (#1539)
Any message successfully forward WILL be marked...
2018-04-18 12:28:12 +02:00
63 changed files with 1894 additions and 699 deletions
+25
View 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
View 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
View 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
View 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?
@@ -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
@@ -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
View 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
@@ -0,0 +1,5 @@
# Translations update
<!--
Used for PRs updating translations from Crowdin
-->
+5 -1
View File
@@ -37,6 +37,8 @@ Save and exit :code:`ctrl + O; enter; ctrl + x`
Starting and enabling the service 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 **@**: To start the bot, run the service and add the instance name after the **@**:
:code:`sudo systemctl start red@instancename` :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` :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 Reds log, you can acccess through journalctl:
:code:`sudo journalctl -u red@instancename`
+21
View 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:
+1 -1
View File
@@ -29,7 +29,7 @@ Basic Usage
@commands.command() @commands.command()
async def return_some_data(self, ctx): async def return_some_data(self, ctx):
await ctx.send(await config.foo()) await ctx.send(await self.config.foo())
******** ********
Tutorial Tutorial
-10
View File
@@ -1,10 +0,0 @@
.. red invocation context documentation
==========================
Command Invocation Context
==========================
.. automodule:: redbot.core.context
.. autoclass:: redbot.core.RedContext
:members:
+10 -6
View File
@@ -13,11 +13,12 @@ Basic Usage
.. code-block:: python .. code-block:: python
from discord.ext import commands from redbot.core import commands
from redbot.core.i18n import CogI18n from redbot.core.i18n import Translator, cog_i18n
_ = CogI18n("ExampleCog", __file__) _ = Translator("ExampleCog", __file__)
@cog_i18n(_)
class ExampleCog: class ExampleCog:
"""description""" """description"""
@@ -39,16 +40,19 @@ In a command prompt in your cog's package (where yourcog.py is),
create a directory called "locales". create a directory called "locales".
Then do one of the following: 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: ? 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 API Reference
------------- -------------
.. automodule:: redbot.core.i18n .. automodule:: redbot.core.i18n
:members:
:special-members: __call__
+6
View File
@@ -16,6 +16,12 @@ Embed Helpers
.. automodule:: redbot.core.utils.embed .. automodule:: redbot.core.utils.embed
:members: :members:
Menu Helpers
============
.. automodule:: redbot.core.utils.menus
:members:
Mod Helpers Mod Helpers
=========== ===========
+1 -1
View File
@@ -90,6 +90,6 @@ have successfully created a cog!
Additional resources 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 on developing cogs for V3. This will also cover differences between V2 and V3 for
those who developed cogs for V2. those who developed cogs for V2.
+1 -1
View File
@@ -37,12 +37,12 @@ Welcome to Red - Discord Bot's documentation!
framework_bot framework_bot
framework_cogmanager framework_cogmanager
framework_config framework_config
framework_context
framework_datamanager framework_datamanager
framework_downloader framework_downloader
framework_events framework_events
framework_i18n framework_i18n
framework_modlog framework_modlog
framework_commands
framework_rpc framework_rpc
framework_utils framework_utils
+1 -1
View File
@@ -14,7 +14,7 @@ Installing the pre-requirements
.. code-block:: none .. 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 Installing the bot
-4
View File
@@ -10,10 +10,6 @@ Needed Software
* `Python <https://python.org/downloads/>`_ - Red needs at least Python 3.5 * `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 .. 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 you may run into issues when trying to run Red
+4 -4
View File
@@ -3,17 +3,17 @@ from re import search
from typing import Generator, Tuple, Iterable from typing import Generator, Tuple, Iterable
import discord import discord
from redbot.core import Config from redbot.core import Config, commands
from redbot.core.i18n import CogI18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box
from discord.ext import commands
from redbot.core.bot import Red from redbot.core.bot import Red
from .alias_entry import AliasEntry from .alias_entry import AliasEntry
_ = CogI18n("Alias", __file__) _ = Translator("Alias", __file__)
@cog_i18n(_)
class Alias: class Alias:
""" """
Alias Alias
+1 -1
View File
@@ -1,7 +1,7 @@
from typing import Tuple from typing import Tuple
from discord.ext import commands
import discord import discord
from redbot.core import commands
class AliasEntry: class AliasEntry:
+382 -164
View File
@@ -1,19 +1,24 @@
import aiohttp
import asyncio import asyncio
import datetime import datetime
import discord import discord
import heapq import heapq
import lavalink import lavalink
import math import math
import re
import redbot.core import redbot.core
from discord.ext import commands from redbot.core import Config, commands, checks, bank
from redbot.core import Config, 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 from .manager import shutdown_lavalink_server
__version__ = "0.0.5" _ = Translator("Audio", __file__)
__version__ = "0.0.6"
__author__ = ["aikaterna", "billy/bollo/ati"] __author__ = ["aikaterna", "billy/bollo/ati"]
@cog_i18n(_)
class Audio: class Audio:
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@@ -46,6 +51,7 @@ class Audio:
self.config.register_guild(**default_guild) self.config.register_guild(**default_guild)
self.config.register_global(**default_global) self.config.register_global(**default_global)
self.skip_votes = {} self.skip_votes = {}
self.session = aiohttp.ClientSession()
async def init_config(self): async def init_config(self):
host = await self.config.host() host = await self.config.host()
@@ -96,10 +102,11 @@ class Audio:
await self.bot.change_presence(activity=None) await self.bot.change_presence(activity=None)
if playing_servers == 1: if playing_servers == 1:
await self.bot.change_presence(activity=discord.Activity(name=get_single_title, await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
type=discord.ActivityType.listening)) type=discord.ActivityType.listening))
if playing_servers > 1: if playing_servers > 1:
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers), await self.bot.change_presence(
type=discord.ActivityType.playing)) activity=discord.Activity(name='music in {} servers'.format(playing_servers),
type=discord.ActivityType.playing))
if event_type == lavalink.LavalinkEvents.QUEUE_END and notify: if event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
notify_channel = player.fetch('channel') notify_channel = player.fetch('channel')
@@ -113,10 +120,11 @@ class Audio:
await self.bot.change_presence(activity=None) await self.bot.change_presence(activity=None)
if playing_servers == 1: if playing_servers == 1:
await self.bot.change_presence(activity=discord.Activity(name=get_single_title, await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
type=discord.ActivityType.listening)) type=discord.ActivityType.listening))
if playing_servers > 1: if playing_servers > 1:
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers), await self.bot.change_presence(
type=discord.ActivityType.playing)) activity=discord.Activity(name='music in {} servers'.format(playing_servers),
type=discord.ActivityType.playing))
if event_type == lavalink.LavalinkEvents.TRACK_EXCEPTION: if event_type == lavalink.LavalinkEvents.TRACK_EXCEPTION:
message_channel = player.fetch('channel') message_channel = player.fetch('channel')
@@ -124,7 +132,7 @@ class Audio:
message_channel = self.bot.get_channel(message_channel) message_channel = self.bot.get_channel(message_channel)
embed = discord.Embed(colour=message_channel.guild.me.top_role.colour, title='Track Error', embed = discord.Embed(colour=message_channel.guild.me.top_role.colour, title='Track Error',
description='{}\n**[{}]({})**'.format(extra, player.current.title, description='{}\n**[{}]({})**'.format(extra, player.current.title,
player.current.uri)) player.current.uri))
embed.set_footer(text='Skipping...') embed.set_footer(text='Skipping...')
await message_channel.send(embed=embed) await message_channel.send(embed=embed)
await player.skip() await player.skip()
@@ -146,6 +154,7 @@ class Audio:
def check(m): def check(m):
return m.author == ctx.author return m.author == ctx.author
try: try:
dj_role = await ctx.bot.wait_for('message', timeout=15.0, check=check) 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) 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) @checks.mod_or_permissions(administrator=True)
async def jukebox(self, ctx, price: int): async def jukebox(self, ctx, price: int):
"""Set a price for queueing songs for non-mods. 0 to disable.""" """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: if price < 0:
return await self._embed_msg(ctx, 'Can\'t be less than zero.') return await self._embed_msg(ctx, 'Can\'t be less than zero.')
if price == 0: if price == 0:
@@ -182,7 +189,7 @@ class Audio:
else: else:
jukebox = True jukebox = True
await self._embed_msg(ctx, 'Track queueing command price set to {} {}.'.format( 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_price.set(price)
await self.config.guild(ctx.guild).jukebox.set(jukebox) 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())) connect_dur = self._dynamic_time(int((datetime.datetime.utcnow() - connect_start).total_seconds()))
try: try:
server_list.append('{} [`{}`]: **[{}]({})**'.format(p.channel.guild.name, connect_dur, server_list.append('{} [`{}`]: **[{}]({})**'.format(p.channel.guild.name, connect_dur,
p.current.title, p.current.uri)) p.current.title, p.current.uri))
except AttributeError: except AttributeError:
server_list.append('{} [`{}`]: **{}**'.format(p.channel.guild.name, connect_dur, server_list.append('{} [`{}`]: **{}**'.format(p.channel.guild.name, connect_dur,
'Nothing playing.')) 'Nothing playing.'))
if server_num == 0: if server_num == 0:
servers = 'Not connected anywhere.' servers = 'Not connected anywhere.'
else: else:
@@ -286,7 +293,7 @@ class Audio:
return await self._embed_msg(ctx, 'Nothing playing.') return await self._embed_msg(ctx, 'Nothing playing.')
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') return await self._embed_msg(ctx, 'You must be in the voice channel to bump a song.')
if dj_enabled: if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author): if not await self._can_instaskip(ctx, ctx.author):
@@ -298,7 +305,7 @@ class Audio:
bump_song = player.queue[bump_index] bump_song = player.queue[bump_index]
player.queue.insert(0, bump_song) player.queue.insert(0, bump_song)
removed = player.queue.pop(index) 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']) @commands.command(aliases=['dc'])
async def disconnect(self, ctx): async def disconnect(self, ctx):
@@ -363,6 +370,7 @@ class Audio:
def check(r, u): def check(r, u):
return r.message.id == message.id and u == ctx.message.author return r.message.id == message.id and u == ctx.message.author
try: try:
(r, u) = await self.bot.wait_for('reaction_add', check=check, timeout=10.0) (r, u) = await self.bot.wait_for('reaction_add', check=check, timeout=10.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
@@ -390,7 +398,7 @@ class Audio:
return await self._embed_msg(ctx, 'Nothing playing.') return await self._embed_msg(ctx, 'Nothing playing.')
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') return await self._embed_msg(ctx, 'You must be in the voice channel to pause the music.')
if dj_enabled: if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx, ctx.author): 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) player.store('guild', ctx.guild.id)
await self._data_check(ctx) await self._data_check(ctx)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') 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): if not await self._currency_check(ctx, jukebox_price):
return return
if not query:
return await self._embed_msg(ctx, 'No songs to play.')
query = query.strip('<>') query = query.strip('<>')
if not query.startswith('http'): if not query.startswith('http'):
query = 'ytsearch:{}'.format(query) query = 'ytsearch:{}'.format(query)
@@ -510,7 +520,8 @@ class Audio:
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Enqueued', embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Enqueued',
description='Added {} tracks to the queue.'.format(len(tracks))) description='Added {} tracks to the queue.'.format(len(tracks)))
if not shuffle and queue_duration > 0: 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: if not player.current:
await player.play() await player.play()
else: else:
@@ -519,7 +530,10 @@ class Audio:
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued', embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued',
description='**[{}]({})**'.format(single_track.title, single_track.uri)) description='**[{}]({})**'.format(single_track.title, single_track.uri))
if not shuffle and queue_duration > 0: 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: if not player.current:
await player.play() await player.play()
await ctx.send(embed=embed) await ctx.send(embed=embed)
@@ -531,17 +545,60 @@ class Audio:
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() 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') @playlist.command(name='delete')
async def _playlist_delete(self, ctx, playlist_name): async def _playlist_delete(self, ctx, playlist_name):
"""Delete a saved playlist.""" """Delete a saved playlist."""
async with self.config.guild(ctx.guild).playlists() as playlists: async with self.config.guild(ctx.guild).playlists() as playlists:
try: 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.') return await self._embed_msg(ctx, 'You are not the author of that playlist.')
del playlists[playlist_name] del playlists[playlist_name]
except KeyError: except KeyError:
return await self._embed_msg(ctx, 'No playlist with that name.') 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') @playlist.command(name='info')
async def _playlist_info(self, ctx, playlist_name): async def _playlist_info(self, ctx, playlist_name):
@@ -556,18 +613,15 @@ class Audio:
try: try:
track_len = len(playlists[playlist_name]['tracks']) track_len = len(playlists[playlist_name]['tracks'])
except TypeError: except TypeError:
track_len = 1 track_len = 0
if playlist_url is None: if playlist_url is None:
playlist_url = '**Not generated from a URL.**' playlist_url = '**Custom playlist.**'
else: else:
playlist_url = 'URL: <{}>'.format(playlist_url) playlist_url = 'URL: <{}>'.format(playlist_url)
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist info for {}:'.format(playlist_name), embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist info for {}:'.format(playlist_name),
description='Author: **{}**\n{}'.format(author_obj, description='Author: **{}**\n{}'.format(author_obj,
playlist_url)) playlist_url))
if track_len > 1: embed.set_footer(text='{} track(s)'.format(track_len))
embed.set_footer(text='{} tracks'.format(track_len))
if track_len == 1:
embed.set_footer(text='{} track'.format(track_len))
await ctx.send(embed=embed) await ctx.send(embed=embed)
@playlist.command(name='list') @playlist.command(name='list')
@@ -597,11 +651,11 @@ class Audio:
return await self._embed_msg(ctx, 'Nothing playing.') return await self._embed_msg(ctx, 'Nothing playing.')
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
tracklist = [] tracklist = []
np_song = self._track_creator(ctx, player, 'np', None) np_song = self._track_creator(player, 'np')
tracklist.append(np_song) tracklist.append(np_song)
for track in player.queue: for track in player.queue:
queue_idx = player.queue.index(track) 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) tracklist.append(track_obj)
if not playlist_name: if not playlist_name:
await self._embed_msg(ctx, 'Please enter a name for this playlist.') 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.') return await self._embed_msg(ctx, 'Playlist name already exists, try again with a different name.')
except asyncio.TimeoutError: except asyncio.TimeoutError:
return await self._embed_msg(ctx, 'No playlist name entered, try again later.') return await self._embed_msg(ctx, 'No playlist name entered, try again later.')
playlist_list = self._to_json(ctx, None, tracklist)
playlist_list = self._to_json(ctx, None, tracklist, playlist_name)
async with self.config.guild(ctx.guild).playlists() as playlists: async with self.config.guild(ctx.guild).playlists() as playlists:
playlists[playlist_name] = playlist_list 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') @playlist.command(name='save')
async def _playlist_save(self, ctx, playlist_name, playlist_url): async def _playlist_save(self, ctx, playlist_name, playlist_url):
@@ -628,18 +709,13 @@ class Audio:
if not await self._playlist_check(ctx): if not await self._playlist_check(ctx):
return return
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
tracks = await player.get_tracks(playlist_url) tracklist = await self._playlist_tracks(ctx, player, playlist_url)
if not tracks: playlist_list = self._to_json(ctx, playlist_url, tracklist)
return await self._embed_msg(ctx, 'Nothing found.') if tracklist is not None:
tracklist = [] async with self.config.guild(ctx.guild).playlists() as playlists:
for track in tracks: playlists[playlist_name] = playlist_list
track_obj = self._track_creator(ctx, player, None, track) return await self._embed_msg(ctx, 'Playlist {} saved: {} tracks added.'.format(
tracklist.append(track_obj) playlist_name, len(tracklist)))
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)))
@playlist.command(name='start') @playlist.command(name='start')
async def _playlist_start(self, ctx, playlist_name=None): async def _playlist_start(self, ctx, playlist_name=None):
@@ -647,25 +723,92 @@ class Audio:
if not await self._playlist_check(ctx): if not await self._playlist_check(ctx):
return return
playlists = await self.config.guild(ctx.guild).playlists.get_raw() playlists = await self.config.guild(ctx.guild).playlists.get_raw()
try: author_obj = self.bot.get_user(ctx.author.id)
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)
track_count = 0 track_count = 0
try: try:
playlist_len = len(playlists[playlist_name]["tracks"])
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
for track in playlists[playlist_name]["tracks"]: for track in playlists[playlist_name]["tracks"]:
player.add(author_obj, lavalink.rest_api.Track(data=track)) player.add(author_obj, lavalink.rest_api.Track(data=track))
track_count = track_count + 1 track_count = track_count + 1
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Enqueued', 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) await ctx.send(embed=embed)
if not player.current: if not player.current:
await player.play() await player.play()
except TypeError: except TypeError:
await ctx.invoke(self.play, query=playlists[playlist_name]["playlist_url"]) 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): async def _playlist_check(self, ctx):
dj_enabled = await self.config.guild(ctx.guild).dj_enabled() dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
@@ -686,7 +829,7 @@ class Audio:
player.store('channel', ctx.channel.id) player.store('channel', ctx.channel.id)
player.store('guild', ctx.guild.id) player.store('guild', ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') await self._embed_msg(ctx, 'You must be in the voice channel to use the playlist command.')
return False return False
if not await self._currency_check(ctx, jukebox_price): if not await self._currency_check(ctx, jukebox_price):
@@ -694,6 +837,27 @@ class Audio:
await self._data_check(ctx) await self._data_check(ctx)
return True 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() @commands.command()
async def prev(self, ctx): async def prev(self, ctx):
"""Skips to the start of the previously played track.""" """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): 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.') 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 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.') return await self._embed_msg(ctx, 'You must be in the voice channel to skip the music.')
if shuffle: if shuffle:
return await self._embed_msg(ctx, 'Turn shuffle off to use this command.') return await self._embed_msg(ctx, 'Turn shuffle off to use this command.')
@@ -733,17 +897,24 @@ class Audio:
"""Lists the queue.""" """Lists the queue."""
if not self._player_check(ctx): if not self._player_check(ctx):
return await self._embed_msg(ctx, 'There\'s nothing in the queue.') 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) player = lavalink.get_player(ctx.guild.id)
if not player.queue: if not player.queue:
return await self._embed_msg(ctx, 'There\'s nothing in the 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 async def _build_queue_page(self, ctx, player, page_num):
pages = math.ceil(len(player.queue) / items_per_page) shuffle = await self.config.guild(ctx.guild).shuffle()
start = (page - 1) * items_per_page repeat = await self.config.guild(ctx.guild).repeat()
end = start + items_per_page 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 = '' queue_list = ''
try: try:
arrow = await self._draw_time(ctx) arrow = await self._draw_time(ctx)
@@ -771,22 +942,27 @@ class Audio:
arrow, pos, dur 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 req_user = track.requester
next = i + 1 track_idx = i + 1
queue_list += '`{}.` **[{}]({})**, requested by **{}**\n'.format(next, track.title, track.uri, req_user) 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, embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Queue for ' + ctx.guild.name,
description=queue_list) description=queue_list)
queue_duration = await self._queue_duration(ctx) queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration) 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: if repeat:
text += ' | Repeat: \N{WHITE HEAVY CHECK MARK}' text += ' | Repeat: \N{WHITE HEAVY CHECK MARK}'
if shuffle: if shuffle:
text += ' | Shuffle: \N{WHITE HEAVY CHECK MARK}' text += ' | Shuffle: \N{WHITE HEAVY CHECK MARK}'
embed.set_footer(text=text) embed.set_footer(text=text)
await ctx.send(embed=embed) return embed
@commands.command() @commands.command()
async def repeat(self, ctx): async def repeat(self, ctx):
@@ -802,7 +978,7 @@ class Audio:
await self._data_check(ctx) await self._data_check(ctx)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') 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)) await self._embed_msg(ctx, 'Repeat songs: {}.'.format(repeat))
@@ -819,27 +995,20 @@ class Audio:
if not await self._can_instaskip(ctx, ctx.author): if not await self._can_instaskip(ctx, ctx.author):
return await self._embed_msg(ctx, 'You need the DJ role to remove songs.') 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 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.') 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: 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.') return await self._embed_msg(ctx, 'Song number must be greater than 1 and within the queue limit.')
index -= 1 index -= 1
removed = player.queue.pop(index) 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() @commands.command()
async def search(self, ctx, *, query): async def search(self, ctx, *, query):
"""Pick a song with a search. """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): if not self._player_check(ctx):
try: try:
await lavalink.connect(ctx.author.voice.channel) await lavalink.connect(ctx.author.voice.channel)
@@ -852,85 +1021,118 @@ class Audio:
player.store('channel', ctx.channel.id) player.store('channel', ctx.channel.id)
player.store('guild', ctx.guild.id) player.store('guild', ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') return await self._embed_msg(ctx, 'You must be in the voice channel to enqueue songs.')
await self._data_check(ctx)
query = query.strip('<>') query = query.strip('<>')
if query.startswith('sc '): if query.startswith('list '):
query = 'scsearch:{}'.format(query.strip('sc ')) query = 'ytsearch:{}'.format(query.lstrip('list '))
elif not query.startswith('http') or query.startswith('sc '): tracks = await player.get_tracks(query)
query = 'ytsearch:{}'.format(query) if not tracks:
return await self._embed_msg(ctx, 'Nothing found 👀')
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)
songembed = discord.Embed(colour=ctx.guild.me.top_role.colour, songembed = discord.Embed(colour=ctx.guild.me.top_role.colour,
title='Queued {} track(s).'.format(len(tracks))) title='Queued {} track(s).'.format(len(tracks)))
queue_duration = await self._queue_duration(ctx) queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration) queue_total_duration = lavalink.utils.format_time(queue_duration)
if not shuffle and queue_duration > 0: 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: for track in tracks:
player.add(ctx.author, track) player.add(ctx.author, track)
if not player.current: if not player.current:
await player.play() 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): len_search_pages = math.ceil(len(tracks) / 5)
player = lavalink.get_player(ctx.guild.id) search_page_list = []
jukebox_price = await self.config.guild(ctx.guild).jukebox_price() for page_num in range(1, len_search_pages + 1):
shuffle = await self.config.guild(ctx.guild).shuffle() embed = await self._build_search_page(ctx, tracks, page_num)
await self._clear_react(message) search_page_list.append(embed)
if not await self._currency_check(ctx, jukebox_price):
return dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
search_choice = tracks[entry] if dj_enabled:
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued', if not await self._can_instaskip(ctx, ctx.author):
description='**[{}]({})**'.format(search_choice.title, search_choice.uri)) return await menu(ctx, search_page_list, DEFAULT_CONTROLS)
queue_duration = await self._queue_duration(ctx)
queue_total_duration = lavalink.utils.format_time(queue_duration) async def _search_menu(ctx: commands.Context, pages: list,
if not shuffle and queue_duration > 0: controls: dict, message: discord.Message, page: int,
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, (len(player.queue) + 1))) timeout: float, emoji: str):
player.add(ctx.author, search_choice) if message:
if not player.current: await _search_button_action(ctx, tracks, emoji, page)
await player.play() await message.delete()
return await ctx.send(embed=embed) 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() @commands.command()
async def seek(self, ctx, seconds: int=30): async def seek(self, ctx, seconds: int=30):
@@ -940,7 +1142,7 @@ class Audio:
return await self._embed_msg(ctx, 'Nothing playing.') return await self._embed_msg(ctx, 'Nothing playing.')
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') return await self._embed_msg(ctx, 'You must be in the voice channel to use seek.')
if dj_enabled: if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author) and not await self._is_alone(ctx, ctx.author): 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) await self._data_check(ctx)
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') 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)) await self._embed_msg(ctx, 'Shuffle songs: {}.'.format(shuffle))
@@ -984,7 +1186,7 @@ class Audio:
return await self._embed_msg(ctx, 'Nothing playing.') return await self._embed_msg(ctx, 'Nothing playing.')
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') 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() dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
vote_enabled = await self.config.guild(ctx.guild).vote_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) nonbots = sum(not m.bot for m in ctx.guild.get_member(self.bot.user.id).voice.channel.members)
if nonbots == 1: if nonbots == 1:
nonbots = 2 nonbots = 2
elif ctx.guild.get_member(member.id).voice.channel.members == 1:
nonbots = 1
else: else:
if ctx.guild.get_member(member.id).voice.channel.members == 1: nonbots = 0
nonbots = 1 return nonbots <= 1
alone = nonbots <= 1
return alone
async def _has_dj_role(self, ctx, member): async def _has_dj_role(self, ctx, member):
dj_role_id = await self.config.guild(ctx.guild).dj_role() 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.') return await self._embed_msg(ctx, 'Nothing playing.')
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') 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 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): 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): if self._player_check(ctx):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
if ((not ctx.author.voice or ctx.author.voice.channel != player.channel) and not 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.') return await self._embed_msg(ctx, 'You must be in the voice channel to change the volume.')
if dj_enabled: if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author) and not await self._has_dj_role(ctx, ctx.author): 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.password.set('youshallnotpass')
await self.config.rest_port.set(2333) await self.config.rest_port.set(2333)
await self.config.ws_port.set(2332) 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.') embed.set_footer(text='Defaults reset.')
return await ctx.send(embed=embed) return await ctx.send(embed=embed)
else: else:
@@ -1187,7 +1390,8 @@ class Audio:
"""Set the lavalink server password.""" """Set the lavalink server password."""
await self.config.password.set(str(password)) await self.config.password.set(str(password))
if await self._check_external(): 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.') embed.set_footer(text='External lavalink server set to True.')
await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:
@@ -1209,7 +1413,8 @@ class Audio:
"""Set the lavalink websocket server port.""" """Set the lavalink websocket server port."""
await self.config.rest_port.set(ws_port) await self.config.rest_port.set(ws_port)
if await self._check_external(): 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.') embed.set_footer(text='External lavalink server set to True.')
await ctx.send(embed=embed) await ctx.send(embed=embed)
else: else:
@@ -1255,7 +1460,8 @@ class Audio:
if player.volume != volume: if player.volume != volume:
await player.set_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) player = lavalink.get_player(ctx.guild.id)
paused = player.paused paused = player.paused
pos = player.position pos = player.position
@@ -1305,6 +1511,15 @@ class Audio:
else: else:
return 0 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 @staticmethod
async def _queue_duration(ctx): async def _queue_duration(ctx):
player = lavalink.get_player(ctx.guild.id) player = lavalink.get_player(ctx.guild.id)
@@ -1312,7 +1527,7 @@ class Audio:
for i in range(len(player.queue)): for i in range(len(player.queue)):
if not player.queue[i].is_stream: if not player.queue[i].is_stream:
duration.append(player.queue[i].length) duration.append(player.queue[i].length)
queue_duration = sum(duration) queue_duration = sum(duration)
if not player.queue: if not player.queue:
queue_duration = 0 queue_duration = 0
try: try:
@@ -1333,14 +1548,16 @@ class Audio:
except KeyError: except KeyError:
return False 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} playlist = {"author": ctx.author.id, "playlist_url": playlist_url, "tracks": tracklist}
return playlist 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': if position == 'np':
queued_track = player.current queued_track = player.current
elif position == None: elif position is None:
queued_track = other_track queued_track = other_track
else: else:
queued_track = player.queue[position] queued_track = player.queue[position]
@@ -1365,6 +1582,7 @@ class Audio:
pass pass
def __unload(self): def __unload(self):
self.session.close()
lavalink.unregister_event_listener(self.event_handler) lavalink.unregister_event_listener(self.event_handler)
self.bot.loop.create_task(lavalink.close()) self.bot.loop.create_task(lavalink.close())
shutdown_lavalink_server() shutdown_lavalink_server()
+1
View File
@@ -92,4 +92,5 @@ def shutdown_lavalink_server():
global proc global proc
if proc is not None: if proc is not None:
proc.terminate() proc.terminate()
proc.wait()
proc = None proc = None
+4 -4
View File
@@ -1,13 +1,12 @@
import discord import discord
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box
from redbot.core import checks, bank from redbot.core import checks, bank, commands
from redbot.core.i18n import CogI18n from redbot.core.i18n import Translator, cog_i18n
from discord.ext import commands
from redbot.core.bot import Red # Only used for type hints from redbot.core.bot import Red # Only used for type hints
_ = CogI18n('Bank', __file__) _ = Translator('Bank', __file__)
def check_global_setting_guildowner(): def check_global_setting_guildowner():
@@ -48,6 +47,7 @@ def check_global_setting_admin():
return commands.check(pred) return commands.check(pred)
@cog_i18n(_)
class Bank: class Bank:
"""Bank""" """Bank"""
+60 -29
View File
@@ -1,17 +1,17 @@
import re import re
import discord 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.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.core.utils.mod import slow_deletion, mass_purge
from redbot.cogs.mod.log import log from redbot.cogs.mod.log import log
_ = CogI18n("Cleanup", __file__) _ = Translator("Cleanup", __file__)
@cog_i18n(_)
class Cleanup: class Cleanup:
"""Commands for cleaning messages""" """Commands for cleaning messages"""
@@ -19,19 +19,26 @@ class Cleanup:
self.bot = bot self.bot = bot
@staticmethod @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): def author_check(message):
return message.author == ctx.author 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) response = await ctx.bot.wait_for('message', check=author_check)
if response.content.lower().startswith('y'): if response.content.lower().startswith('y'):
await prompt.delete()
try:
await response.delete()
except:
pass
return True return True
else: else:
await ctx.send(_('Cancelled.')) await ctx.send(_('Cancelled.'))
@@ -39,8 +46,9 @@ class Cleanup:
@staticmethod @staticmethod
async def get_messages_for_deletion( async def get_messages_for_deletion(
ctx: RedContext, channel: discord.TextChannel, number, ctx: commands.Context, channel: discord.TextChannel, number,
check=lambda x: True, limit=100, before=None, after=None check=lambda x: True, limit=100, before=None, after=None,
delete_pinned=False
) -> list: ) -> list:
""" """
Gets a list of messages meeting the requirements to be deleted. 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, - The message passes a provided check (if no check is provided,
this is automatically true) this is automatically true)
- The message is less than 14 days old - The message is less than 14 days old
- The message is not pinned
""" """
to_delete = [] to_delete = []
too_old = False too_old = False
@@ -59,8 +68,12 @@ class Cleanup:
async for message in channel.history(limit=limit, async for message in channel.history(limit=limit,
before=before, before=before,
after=after): after=after):
if (not number or len(to_delete) - 1 < number) and check(message) \ if (
and (ctx.message.created_at - message.created_at).days < 14: (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) to_delete.append(message)
elif (ctx.message.created_at - message.created_at).days >= 14: elif (ctx.message.created_at - message.created_at).days >= 14:
too_old = True too_old = True
@@ -75,7 +88,7 @@ class Cleanup:
@commands.group() @commands.group()
@checks.mod_or_permissions(manage_messages=True) @checks.mod_or_permissions(manage_messages=True)
async def cleanup(self, ctx: RedContext): async def cleanup(self, ctx: commands.Context):
"""Deletes messages.""" """Deletes messages."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@@ -83,7 +96,7 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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. """Deletes last X messages matching the specified text.
Example: Example:
@@ -109,7 +122,8 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( 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 "\ reason = "{}({}) deleted {} messages "\
" containing '{}' in channel {}.".format(author.name, " containing '{}' in channel {}.".format(author.name,
@@ -124,13 +138,24 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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. """Deletes last X messages from specified user.
Examples: Examples:
cleanup user @\u200bTwentysix 2 cleanup user @\u200bTwentysix 2
cleanup user Red 6""" 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 channel = ctx.channel
author = ctx.author author = ctx.author
is_bot = self.bot.user.bot is_bot = self.bot.user.bot
@@ -141,9 +166,7 @@ class Cleanup:
return return
def check(m): def check(m):
if isinstance(user, discord.Member) and m.author == user: if m.author.id == _id:
return True
elif m.author.id == user: # Allow finding messages based on an ID
return True return True
elif m == ctx.message: elif m == ctx.message:
return True return True
@@ -151,12 +174,13 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( 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 "\ reason = "{}({}) deleted {} messages "\
" made by {}({}) in channel {}."\ " made by {}({}) in channel {}."\
"".format(author.name, author.id, len(to_delete), "".format(author.name, author.id, len(to_delete),
user.name, user.id, channel.name) member or '???', _id, channel.name)
log.info(reason) log.info(reason)
if is_bot: if is_bot:
@@ -168,7 +192,7 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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. """Deletes all messages after specified message.
To get a message id, enable developer mode in Discord's To get a message id, enable developer mode in Discord's
@@ -194,7 +218,7 @@ class Cleanup:
return return
to_delete = await self.get_messages_for_deletion( 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 {}."\ reason = "{}({}) deleted {} messages in channel {}."\
@@ -207,7 +231,7 @@ class Cleanup:
@cleanup.command() @cleanup.command()
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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. """Deletes last X messages.
Example: Example:
@@ -224,8 +248,10 @@ class Cleanup:
return return
to_delete = await self.get_messages_for_deletion( 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 {}."\ reason = "{}({}) deleted {} messages in channel {}."\
"".format(author.name, author.id, "".format(author.name, author.id,
@@ -240,7 +266,7 @@ class Cleanup:
@cleanup.command(name='bot') @cleanup.command(name='bot')
@commands.guild_only() @commands.guild_only()
@commands.bot_has_permissions(manage_messages=True) @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.""" """Cleans up command messages and messages from the bot."""
channel = ctx.message.channel channel = ctx.message.channel
@@ -272,8 +298,10 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( 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 {} "\ reason = "{}({}) deleted {} "\
" command messages in channel {}."\ " command messages in channel {}."\
@@ -287,7 +315,9 @@ class Cleanup:
await slow_deletion(to_delete) await slow_deletion(to_delete)
@cleanup.command(name='self') @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. """Cleans up messages owned by the bot.
By default, all messages are cleaned. If a third argument is specified, By default, all messages are cleaned. If a third argument is specified,
@@ -337,7 +367,8 @@ class Cleanup:
return False return False
to_delete = await self.get_messages_for_deletion( 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 # Selfbot convenience, delete trigger message
+16 -5
View File
@@ -4,13 +4,12 @@ import random
from datetime import datetime from datetime import datetime
import discord 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.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): class CCError(Exception):
@@ -152,6 +151,7 @@ class CommandObj:
command, value=None) command, value=None)
@cog_i18n(_)
class CustomCommands: class CustomCommands:
"""Custom commands """Custom commands
Creates commands used to display text""" Creates commands used to display text"""
@@ -179,7 +179,18 @@ class CustomCommands:
ctx: commands.Context): ctx: commands.Context):
""" """
CCs can be enhanced with arguments: 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, if not ctx.invoked_subcommand or isinstance(ctx.invoked_subcommand,
commands.Group): commands.Group):
+5 -6
View File
@@ -1,17 +1,16 @@
from pathlib import Path from pathlib import Path
import asyncio import asyncio
from discord.ext import commands from redbot.core import checks, commands
from redbot.core import checks, RedContext
from redbot.core.bot import Red 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.cogs.dataconverter.core_specs import SpecResolver
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box
_ = CogI18n('DataConverter', __file__) _ = Translator('DataConverter', __file__)
@cog_i18n(_)
class DataConverter: class DataConverter:
""" """
Cog for importing Red v2 Data Cog for importing Red v2 Data
@@ -22,7 +21,7 @@ class DataConverter:
@checks.is_owner() @checks.is_owner()
@commands.command(name="convertdata") @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 Interactive prompt for importing data from Red v2
+5 -4
View File
@@ -10,9 +10,9 @@ import sys
from redbot.core import Config from redbot.core import Config
from redbot.core import checks from redbot.core import checks
from redbot.core.data_manager import cog_data_path 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 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 redbot.core.bot import Red
from .checks import install_agreement from .checks import install_agreement
@@ -22,9 +22,10 @@ from .installable import Installable
from .log import log from .log import log
from .repo_manager import RepoManager, Repo from .repo_manager import RepoManager, Repo
_ = CogI18n('Downloader', __file__) _ = Translator('Downloader', __file__)
@cog_i18n(_)
class Downloader: class Downloader:
def __init__(self, bot: Red): def __init__(self, bot: Red):
self.bot = bot self.bot = bot
@@ -420,7 +421,7 @@ class Downloader:
cog_name = cog_installable.name cog_name = cog_installable.name
else: else:
made_by = "26 & co." 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__ cog_name = cog_installable.__class__.__name__
msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}") msg = _("Command: {}\nMade by: {}\nRepo: {}\nCog name: {}")
+4 -4
View File
@@ -7,14 +7,13 @@ from enum import Enum
import discord import discord
from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin from redbot.cogs.bank import check_global_setting_guildowner, check_global_setting_admin
from redbot.core import Config, bank from redbot.core import Config, bank, commands
from redbot.core.i18n import CogI18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import pagify, box from redbot.core.utils.chat_formatting import pagify, box
from discord.ext import commands
from redbot.core.bot import Red from redbot.core.bot import Red
_ = CogI18n("Economy", __file__) _ = Translator("Economy", __file__)
logger = logging.getLogger("red.economy") logger = logging.getLogger("red.economy")
@@ -104,6 +103,7 @@ class SetParser:
raise RuntimeError raise RuntimeError
@cog_i18n(_)
class Economy: class Economy:
"""Economy """Economy
+7 -7
View File
@@ -1,15 +1,15 @@
import discord 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.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.chat_formatting import pagify
from redbot.core.utils.mod import is_mod_or_superior from redbot.core.utils.mod import is_mod_or_superior
_ = CogI18n("Filter", __file__) _ = Translator("Filter", __file__)
@cog_i18n(_)
class Filter: class Filter:
"""Filter-related commands""" """Filter-related commands"""
@@ -46,7 +46,7 @@ class Filter:
@commands.group(name="filter") @commands.group(name="filter")
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_messages=True) @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 """Adds/removes words from filter
Use double quotes to add/remove sentences Use double quotes to add/remove sentences
@@ -129,7 +129,7 @@ class Filter:
await ctx.send(_("Those words weren't in the filter.")) await ctx.send(_("Those words weren't in the filter."))
@_filter.command(name="names") @_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 Toggles whether or not to check names and nicknames against the filter
This is disabled by default This is disabled by default
@@ -149,7 +149,7 @@ class Filter:
) )
@_filter.command(name="defaultname") @_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 Sets the default name to use if filtering names is enabled
Note that this has no effect if filtering names is disabled Note that this has no effect if filtering names is disabled
+4 -8
View File
@@ -6,12 +6,12 @@ from urllib.parse import quote_plus
import aiohttp import aiohttp
import discord import discord
from redbot.core.i18n import CogI18n from redbot.core import commands
from discord.ext import commands from redbot.core.i18n import Translator, cog_i18n
from redbot.core.utils.chat_formatting import escape, italics, pagify from redbot.core.utils.chat_formatting import escape, italics, pagify
_ = CogI18n("General", __file__) _ = Translator("General", __file__)
class RPS(Enum): class RPS(Enum):
@@ -33,6 +33,7 @@ class RPSParser:
raise raise
@cog_i18n(_)
class General: class General:
"""General commands.""" """General commands."""
@@ -48,11 +49,6 @@ class General:
_("My sources say no"), _("Outlook not so good"), _("Very doubtful") _("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() @commands.command()
async def choose(self, ctx, *choices): async def choose(self, ctx, *choices):
"""Chooses between multiple choices. """Chooses between multiple choices.
+4 -4
View File
@@ -1,16 +1,16 @@
from random import shuffle from random import shuffle
import aiohttp import aiohttp
from discord.ext import commands
from redbot.core.i18n import CogI18n from redbot.core.i18n import Translator, cog_i18n
from redbot.core import checks, Config from redbot.core import checks, Config, commands
_ = CogI18n("Image", __file__) _ = Translator("Image", __file__)
GIPHY_API_KEY = "dc6zaTOxFJmzC" GIPHY_API_KEY = "dc6zaTOxFJmzC"
@cog_i18n(_)
class Image: class Image:
"""Image related commands.""" """Image related commands."""
default_global = { default_global = {
+55 -58
View File
@@ -3,21 +3,20 @@ from datetime import datetime, timedelta
from collections import deque, defaultdict, namedtuple from collections import deque, defaultdict, namedtuple
import discord 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.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 redbot.core.utils.chat_formatting import box, escape
from .checks import mod_or_voice_permissions, admin_or_voice_permissions, bot_has_voice_permissions 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, \ from redbot.core.utils.mod import is_mod_or_superior, is_allowed_by_hierarchy, \
get_audit_reason get_audit_reason
from .log import log from .log import log
_ = CogI18n("Mod", __file__) _ = Translator("Mod", __file__)
@cog_i18n(_)
class Mod: class Mod:
"""Moderation tools.""" """Moderation tools."""
@@ -174,7 +173,7 @@ class Mod:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def modset(self, ctx: RedContext): async def modset(self, ctx: commands.Context):
"""Manages server administration settings.""" """Manages server administration settings."""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
guild = ctx.guild guild = ctx.guild
@@ -200,7 +199,7 @@ class Mod:
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def hierarchy(self, ctx: RedContext): async def hierarchy(self, ctx: commands.Context):
"""Toggles role hierarchy check for mods / admins""" """Toggles role hierarchy check for mods / admins"""
guild = ctx.guild guild = ctx.guild
toggled = await self.settings.guild(guild).respect_hierarchy() toggled = await self.settings.guild(guild).respect_hierarchy()
@@ -215,7 +214,7 @@ class Mod:
@modset.command() @modset.command()
@commands.guild_only() @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 """Enables auto ban for messages mentioning X different people
Accepted values: 5 or superior""" Accepted values: 5 or superior"""
@@ -240,7 +239,7 @@ class Mod:
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def deleterepeats(self, ctx: RedContext): async def deleterepeats(self, ctx: commands.Context):
"""Enables auto deletion of repeated messages""" """Enables auto deletion of repeated messages"""
guild = ctx.guild guild = ctx.guild
cur_setting = await self.settings.guild(guild).delete_repeats() cur_setting = await self.settings.guild(guild).delete_repeats()
@@ -254,9 +253,10 @@ class Mod:
@modset.command() @modset.command()
@commands.guild_only() @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. """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.""" A delay of -1 means the bot will not remove the message."""
guild = ctx.guild guild = ctx.guild
@@ -280,11 +280,11 @@ class Mod:
@modset.command() @modset.command()
@commands.guild_only() @commands.guild_only()
async def reinvite(self, ctx: RedContext): async def reinvite(self, ctx: commands.Context):
"""Toggles whether an invite will be sent when a user """Toggles whether an invite will be sent when a user is unbanned via [p]unban.
is unbanned via [p]unban. If this is True, the bot will
attempt to create and send a single-use invite to the If this is True, the bot will attempt to create and send a single-use invite
newly-unbanned user""" to the newly-unbanned user"""
guild = ctx.guild guild = ctx.guild
cur_setting = await self.settings.guild(guild).reinvite_on_unban() cur_setting = await self.settings.guild(guild).reinvite_on_unban()
if not cur_setting: if not cur_setting:
@@ -297,10 +297,10 @@ class Mod:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(kick_members=True) @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. """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""" in the audit log"""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
@@ -337,7 +337,7 @@ class Mod:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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. """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. 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.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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 """Preemptively bans user from the server
A user ID needs to be provided in order to ban A user ID needs to be provided in order to ban
@@ -451,7 +451,7 @@ class Mod:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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""" """Tempbans the user for the specified number of days"""
guild = ctx.guild guild = ctx.guild
author = ctx.author author = ctx.author
@@ -499,7 +499,7 @@ class Mod:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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.""" """Kicks the user, deleting 1 day worth of messages."""
guild = ctx.guild guild = ctx.guild
channel = ctx.channel channel = ctx.channel
@@ -578,7 +578,7 @@ class Mod:
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @checks.admin_or_permissions(ban_members=True)
@commands.bot_has_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. """Unbans the target user.
Requires specifying the target user's ID. To find this, you may either: Requires specifying the target user's ID. To find this, you may either:
@@ -636,7 +636,7 @@ class Mod:
.format(invite.url)) .format(invite.url))
@staticmethod @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 """Handles the reinvite logic for getting an invite
to send the newly unbanned user to send the newly unbanned user
:returns: :class:`Invite`""" :returns: :class:`Invite`"""
@@ -671,7 +671,7 @@ class Mod:
@commands.guild_only() @commands.guild_only()
@admin_or_voice_permissions(mute_members=True, deafen_members=True) @admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_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""" """Bans the target user from speaking and listening in voice channels in the server"""
user_voice_state = user.voice user_voice_state = user.voice
if user_voice_state is None: if user_voice_state is None:
@@ -708,7 +708,7 @@ class Mod:
@commands.guild_only() @commands.guild_only()
@admin_or_voice_permissions(mute_members=True, deafen_members=True) @admin_or_voice_permissions(mute_members=True, deafen_members=True)
@bot_has_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""" """Unbans the user from speaking/listening in the server's voice channels"""
user_voice_state = user.voice user_voice_state = user.voice
if user_voice_state is None: if user_voice_state is None:
@@ -742,7 +742,7 @@ class Mod:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_nicknames=True) @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 """Changes user's nickname
Leaving the nickname empty will remove it.""" Leaving the nickname empty will remove it."""
@@ -762,7 +762,7 @@ class Mod:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_channel=True) @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""" """Mutes user in the channel/server"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@@ -771,7 +771,7 @@ class Mod:
@commands.guild_only() @commands.guild_only()
@mod_or_voice_permissions(mute_members=True) @mod_or_voice_permissions(mute_members=True)
@bot_has_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): *, reason: str = None):
"""Mutes the user in a voice channel""" """Mutes the user in a voice channel"""
user_voice_state = user.voice user_voice_state = user.voice
@@ -810,7 +810,7 @@ class Mod:
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@mute.command(name="channel") @mute.command(name="channel")
@commands.guild_only() @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""" """Mutes user in the current channel"""
author = ctx.message.author author = ctx.message.author
channel = ctx.message.channel channel = ctx.message.channel
@@ -838,7 +838,7 @@ class Mod:
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@mute.command(name="server", aliases=["guild"]) @mute.command(name="server", aliases=["guild"])
@commands.guild_only() @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""" """Mutes user in the server"""
author = ctx.message.author author = ctx.message.author
guild = ctx.guild guild = ctx.guild
@@ -888,8 +888,8 @@ class Mod:
"send_messages": overwrites.send_messages, "send_messages": overwrites.send_messages,
"add_reactions": overwrites.add_reactions "add_reactions": overwrites.add_reactions
} }
overwrites.send_messages = False overwrites.update(send_messages=False,
overwrites.add_reactions = False add_reactions=False)
try: try:
await channel.set_permissions(user, overwrite=overwrites, reason=reason) await channel.set_permissions(user, overwrite=overwrites, reason=reason)
except discord.Forbidden: except discord.Forbidden:
@@ -901,7 +901,7 @@ class Mod:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.mod_or_permissions(manage_channel=True) @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 """Unmutes user in the channel/server
Defaults to channel""" Defaults to channel"""
@@ -912,7 +912,7 @@ class Mod:
@commands.guild_only() @commands.guild_only()
@mod_or_voice_permissions(mute_members=True) @mod_or_voice_permissions(mute_members=True)
@bot_has_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""" """Unmutes the user in a voice channel"""
user_voice_state = user.voice user_voice_state = user.voice
if user_voice_state: if user_voice_state:
@@ -946,7 +946,7 @@ class Mod:
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@unmute.command(name="channel") @unmute.command(name="channel")
@commands.guild_only() @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""" """Unmutes user in the current channel"""
channel = ctx.channel channel = ctx.channel
author = ctx.author author = ctx.author
@@ -969,7 +969,7 @@ class Mod:
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@unmute.command(name="server", aliases=["guild"]) @unmute.command(name="server", aliases=["guild"])
@commands.guild_only() @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""" """Unmutes user in the server"""
guild = ctx.guild guild = ctx.guild
author = ctx.author author = ctx.author
@@ -1013,9 +1013,9 @@ class Mod:
if channel.id in perms_cache: if channel.id in perms_cache:
old_values = perms_cache[channel.id] old_values = perms_cache[channel.id]
else: else:
old_values = None old_values = {"send_messages": None, "add_reactions": None}
overwrites.send_messages = old_values["send_messages"] overwrites.update(send_messages=old_values["send_messages"],
overwrites.add_reactions = old_values["add_reactions"] add_reactions=old_values["add_reactions"])
is_empty = self.are_overwrites_empty(overwrites) is_empty = self.are_overwrites_empty(overwrites)
try: try:
@@ -1037,14 +1037,14 @@ class Mod:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_channels=True) @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""" """Adds servers/channels to ignorelist"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
await ctx.send(await self.count_ignored()) await ctx.send(await self.count_ignored())
@ignore.command(name="channel") @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 """Ignores channel
Defaults to current one""" Defaults to current one"""
@@ -1057,7 +1057,8 @@ class Mod:
await ctx.send(_("Channel already in ignore list.")) await ctx.send(_("Channel already in ignore list."))
@ignore.command(name="server", aliases=["guild"]) @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""" """Ignores current server"""
guild = ctx.guild guild = ctx.guild
if not await self.settings.guild(guild).ignored(): if not await self.settings.guild(guild).ignored():
@@ -1069,14 +1070,14 @@ class Mod:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(manage_channels=True) @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""" """Removes servers/channels from ignorelist"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
await ctx.send(await self.count_ignored()) await ctx.send(await self.count_ignored())
@unignore.command(name="channel") @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 """Removes channel from ignore list
Defaults to current one""" Defaults to current one"""
@@ -1090,7 +1091,8 @@ class Mod:
await ctx.send(_("That channel is not in the ignore list.")) await ctx.send(_("That channel is not in the ignore list."))
@unignore.command(name="server", aliases=["guild"]) @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""" """Removes current guild from ignore list"""
guild = ctx.message.guild guild = ctx.message.guild
if await self.settings.guild(guild).ignored(): if await self.settings.guild(guild).ignored():
@@ -1130,11 +1132,8 @@ class Mod:
chann_ignored and not perms.manage_channels) chann_ignored and not perms.manage_channels)
@commands.command() @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""" """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() names = await self.settings.user(user).past_names()
nicks = await self.settings.member(user).past_nicks() nicks = await self.settings.member(user).past_nicks()
msg = "" msg = ""
@@ -1224,7 +1223,7 @@ class Mod:
return True return True
return False return False
async def on_command(self, ctx: RedContext): async def on_command(self, ctx: commands.Context):
"""Currently used for: """Currently used for:
* delete delay""" * delete delay"""
guild = ctx.guild guild = ctx.guild
@@ -1358,15 +1357,13 @@ class Mod:
if entry.target == target: if entry.target == target:
return entry 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: if before.name != after.name:
async with self.settings.user(before).past_names() as name_list: async with self.settings.user(before).past_names() as name_list:
while None in name_list: # clean out null entries from a bug if after.nick in name_list:
name_list.remove(None)
if after.name in name_list:
# Ensure order is maintained without duplicates occuring # Ensure order is maintained without duplicates occuring
name_list.remove(after.name) name_list.remove(after.nick)
name_list.append(after.name) name_list.append(after.nick)
while len(name_list) > 20: while len(name_list) > 20:
name_list.pop(0) name_list.pop(0)
+11 -11
View File
@@ -1,14 +1,14 @@
import discord 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.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 from redbot.core.utils.chat_formatting import box
_ = CogI18n('ModLog', __file__) _ = Translator('ModLog', __file__)
@cog_i18n(_)
class ModLog: class ModLog:
"""Log for mod actions""" """Log for mod actions"""
@@ -17,14 +17,14 @@ class ModLog:
@commands.group() @commands.group()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def modlogset(self, ctx: RedContext): async def modlogset(self, ctx: commands.Context):
"""Settings for the mod log""" """Settings for the mod log"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@modlogset.command() @modlogset.command()
@commands.guild_only() @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 """Sets a channel as mod log
Leaving the channel parameter empty will deactivate it""" Leaving the channel parameter empty will deactivate it"""
@@ -53,7 +53,7 @@ class ModLog:
@modlogset.command(name='cases') @modlogset.command(name='cases')
@commands.guild_only() @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""" """Enables or disables case creation for each type of mod action"""
guild = ctx.guild guild = ctx.guild
@@ -87,7 +87,7 @@ class ModLog:
@modlogset.command() @modlogset.command()
@commands.guild_only() @commands.guild_only()
async def resetcases(self, ctx: RedContext): async def resetcases(self, ctx: commands.Context):
"""Resets modlog's cases""" """Resets modlog's cases"""
guild = ctx.guild guild = ctx.guild
await modlog.reset_cases(guild) await modlog.reset_cases(guild)
@@ -95,7 +95,7 @@ class ModLog:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
async def case(self, ctx: RedContext, number: int): async def case(self, ctx: commands.Context, number: int):
"""Shows the specified case""" """Shows the specified case"""
try: try:
case = await modlog.get_case(number, ctx.guild, self.bot) case = await modlog.get_case(number, ctx.guild, self.bot)
@@ -107,7 +107,7 @@ class ModLog:
@commands.command() @commands.command()
@commands.guild_only() @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 """Lets you specify a reason for mod-log's cases
Please note that you can only edit cases you are Please note that you can only edit cases you are
the owner of unless you are a mod/admin or the server owner""" the owner of unless you are a mod/admin or the server owner"""
@@ -134,7 +134,7 @@ class ModLog:
audit_case = None audit_case = None
async for entry in guild.audit_logs(action=audit_type): async for entry in guild.audit_logs(action=audit_type):
if entry.target.id == case_before.user.id and \ if entry.target.id == case_before.user.id and \
entry.user.id == case_before.moderator.id: entry.action == audit_type:
audit_case = entry audit_case = entry
break break
if audit_case: if audit_case:
+89 -59
View File
@@ -2,23 +2,24 @@ import logging
import asyncio import asyncio
from typing import Union from typing import Union
from datetime import timedelta from datetime import timedelta
from copy import copy
import contextlib
import discord 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.chat_formatting import pagify, box
from redbot.core.utils.antispam import AntiSpam from redbot.core.utils.antispam import AntiSpam
from redbot.core.bot import Red 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 from redbot.core.utils.tunnel import Tunnel
_ = CogI18n("Reports", __file__) _ = Translator("Reports", __file__)
log = logging.getLogger("red.reports") log = logging.getLogger("red.reports")
@cog_i18n(_)
class Reports: class Reports:
default_guild_settings = { default_guild_settings = {
@@ -65,7 +66,7 @@ class Reports:
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
@commands.guild_only() @commands.guild_only()
@commands.group(name="reportset") @commands.group(name="reportset")
async def reportset(self, ctx: RedContext): async def reportset(self, ctx: commands.Context):
""" """
settings for reports settings for reports
""" """
@@ -73,14 +74,14 @@ class Reports:
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="output") @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""" """sets the output channel"""
await self.config.guild(ctx.guild).output_channel.set(channel.id) await self.config.guild(ctx.guild).output_channel.set(channel.id)
await ctx.send(_("Report Channel Set.")) await ctx.send(_("Report Channel Set."))
@checks.admin_or_permissions(manage_guild=True) @checks.admin_or_permissions(manage_guild=True)
@reportset.command(name="toggleactive") @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""" """Toggles whether the Reporting tool is enabled or not"""
active = await self.config.guild(ctx.guild).active() active = await self.config.guild(ctx.guild).active()
@@ -109,10 +110,11 @@ class Reports:
ret |= await self.bot.is_owner(m) ret |= await self.bot.is_owner(m)
return ret return ret
async def discover_guild(self, author: discord.User, *, async def discover_guild(
mod: bool=False, self, author: discord.User, *,
permissions: Union[discord.Permissions, dict]={}, mod: bool=False,
prompt: str=""): permissions: Union[discord.Permissions, dict]=None,
prompt: str=""):
""" """
discovers which of shared guilds between the bot discovers which of shared guilds between the bot
and provided user based on conditions (mod or permissions is an or) 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 prompt is for providing a user prompt for selection
""" """
shared_guilds = [] shared_guilds = []
if isinstance(permissions, discord.Permissions): if permissions is None:
perms = discord.Permissions()
elif isinstance(permissions, discord.Permissions):
perms = permissions perms = permissions
else: else:
permissions = discord.Permissions(**perms) perms = discord.Permissions(**permissions)
for guild in self.bot.guilds: for guild in self.bot.guilds:
x = guild.get_member(author.id) x = guild.get_member(author.id)
@@ -132,7 +136,6 @@ class Reports:
shared_guilds.append(guild) shared_guilds.append(guild)
if len(shared_guilds) == 0: if len(shared_guilds) == 0:
raise ValueError("No Qualifying Shared Guilds") raise ValueError("No Qualifying Shared Guilds")
return
if len(shared_guilds) == 1: if len(shared_guilds) == 1:
return shared_guilds[0] return shared_guilds[0]
output = "" output = ""
@@ -170,26 +173,40 @@ class Reports:
author = guild.get_member(msg.author.id) author = guild.get_member(msg.author.id)
report = msg.clean_content 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_id = await self.config.guild(guild).output_channel()
channel = guild.get_channel(channel_id) channel = guild.get_channel(channel_id)
if channel is not None: if channel is None:
try: return None
await channel.send(embed=em)
except (discord.Forbidden, discord.HTTPException): files = await Tunnel.files_from_attatch(msg)
return None
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: 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 return None
await self.config.custom('REPORT', guild.id, ticket_number).report.set( await self.config.custom('REPORT', guild.id, ticket_number).report.set(
@@ -198,8 +215,13 @@ class Reports:
return ticket_number return ticket_number
@commands.group(name="report", invoke_without_command=True) @commands.group(name="report", invoke_without_command=True)
async def report(self, ctx: RedContext): async def report(self, ctx: commands.Context, *, _report: str=""):
"Follow the prompts to make a report" """
Follow the prompts to make a report
optionally use with a report message
to use it non interactively
"""
author = ctx.author author = ctx.author
guild = ctx.guild guild = ctx.guild
if guild is None: if guild is None:
@@ -243,31 +265,39 @@ class Reports:
pass pass
self.user_cache.append(author.id) self.user_cache.append(author.id)
try: if _report:
dm = await author.send( _m = copy(ctx.message)
_("Please respond to this message with your Report." _m.content = _report
"\nYour report should be a single message") _m.content = _m.clean_content
) val = await self.send_report(_m, guild)
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: 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: if val is None:
await author.send( await author.send(
_("There was an error sending your report.") _("There was an error sending your report.")
@@ -276,7 +306,7 @@ class Reports:
await author.send( await author.send(
_("Your report was submitted. (Ticket #{})").format(val) _("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) self.user_cache.remove(author.id)
@@ -353,7 +383,7 @@ class Reports:
"will be forwarded to them until the communication is closed.\n" "will be forwarded to them until the communication is closed.\n"
"You can close a communication at any point " "You can close a communication at any point "
"by reacting with the X to the last message recieved. " "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." "\nTunnels are not persistent across bot restarts."
) )
topic = big_topic.format( topic = big_topic.format(
+49 -34
View File
@@ -1,9 +1,8 @@
import discord import discord
from discord.ext import commands from redbot.core import Config, checks, commands
from redbot.core import Config, checks, RedContext
from redbot.core.utils.chat_formatting import pagify from redbot.core.utils.chat_formatting import pagify
from redbot.core.bot import Red 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 .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity, YoutubeStream
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials, from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials,
CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials) CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials)
@@ -15,9 +14,10 @@ import re
CHECK_DELAY = 60 CHECK_DELAY = 60
_ = CogI18n("Streams", __file__) _ = Translator("Streams", __file__)
@cog_i18n(_)
class Streams: class Streams:
global_defaults = { global_defaults = {
@@ -64,7 +64,7 @@ class Streams:
self.task = self.bot.loop.create_task(self._stream_alerts()) self.task = self.bot.loop.create_task(self._stream_alerts())
@commands.command() @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""" """Checks if a Twitch channel is streaming"""
token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None) token = await self.db.tokens.get_raw(TwitchStream.__name__, default=None)
stream = TwitchStream(name=channel_name, stream = TwitchStream(name=channel_name,
@@ -72,7 +72,7 @@ class Streams:
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @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 Checks if a Youtube channel is streaming
""" """
@@ -85,24 +85,24 @@ class Streams:
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @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""" """Checks if a Hitbox channel is streaming"""
stream = HitboxStream(name=channel_name) stream = HitboxStream(name=channel_name)
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @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""" """Checks if a Mixer channel is streaming"""
stream = MixerStream(name=channel_name) stream = MixerStream(name=channel_name)
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
@commands.command() @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""" """Checks if a Picarto channel is streaming"""
stream = PicartoStream(name=channel_name) stream = PicartoStream(name=channel_name)
await self.check_online(ctx, stream) await self.check_online(ctx, stream)
async def check_online(self, ctx, stream): async def check_online(self, ctx: commands.Context, stream):
try: try:
embed = await stream.is_online() embed = await stream.is_online()
except OfflineStream: except OfflineStream:
@@ -124,49 +124,49 @@ class Streams:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.mod() @checks.mod()
async def streamalert(self, ctx): async def streamalert(self, ctx: commands.Context):
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@streamalert.group(name="twitch") @streamalert.group(name="twitch")
async def _twitch(self, ctx): async def _twitch(self, ctx: commands.Context):
"""Twitch stream alerts""" """Twitch stream alerts"""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch: if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self._twitch:
await ctx.send_help() await ctx.send_help()
@_twitch.command(name="channel") @_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""" """Sets a Twitch stream alert notification in the channel"""
await self.stream_alert(ctx, TwitchStream, channel_name.lower()) await self.stream_alert(ctx, TwitchStream, channel_name.lower())
@_twitch.command(name="community") @_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 """Sets a Twitch stream alert notification in the channel
for the specified community.""" for the specified community."""
await self.community_alert(ctx, TwitchCommunity, community.lower()) await self.community_alert(ctx, TwitchCommunity, community.lower())
@streamalert.command(name="youtube") @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""" """Sets a Youtube stream alert notification in the channel"""
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id) await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
@streamalert.command(name="hitbox") @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""" """Sets a Hitbox stream alert notification in the channel"""
await self.stream_alert(ctx, HitboxStream, channel_name) await self.stream_alert(ctx, HitboxStream, channel_name)
@streamalert.command(name="mixer") @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""" """Sets a Mixer stream alert notification in the channel"""
await self.stream_alert(ctx, MixerStream, channel_name) await self.stream_alert(ctx, MixerStream, channel_name)
@streamalert.command(name="picarto") @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""" """Sets a Picarto stream alert notification in the channel"""
await self.stream_alert(ctx, PicartoStream, channel_name) await self.stream_alert(ctx, PicartoStream, channel_name)
@streamalert.command(name="stop") @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 """Stops all stream notifications in the channel
Adding 'yes' will disable all notifications in the server""" Adding 'yes' will disable all notifications in the server"""
@@ -197,7 +197,7 @@ class Streams:
await ctx.send(msg) await ctx.send(msg)
@streamalert.command(name="list") @streamalert.command(name="list")
async def streamalert_list(self, ctx): async def streamalert_list(self, ctx: commands.Context):
streams_list = defaultdict(list) streams_list = defaultdict(list)
guild_channels_ids = [c.id for c in ctx.guild.channels] guild_channels_ids = [c.id for c in ctx.guild.channels]
msg = _("Active stream alerts:\n\n") msg = _("Active stream alerts:\n\n")
@@ -218,7 +218,7 @@ class Streams:
for page in pagify(msg): for page in pagify(msg):
await ctx.send(page) 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) stream = self.get_stream(_class, channel_name)
if not stream: if not stream:
token = await self.db.tokens.get_raw(_class.__name__, default=None) token = await self.db.tokens.get_raw(_class.__name__, default=None)
@@ -251,7 +251,7 @@ class Streams:
await self.add_or_remove(ctx, stream) 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) community = self.get_community(_class, community_name)
if not community: if not community:
token = await self.db.tokens.get_raw(_class.__name__, default=None) token = await self.db.tokens.get_raw(_class.__name__, default=None)
@@ -278,13 +278,13 @@ class Streams:
@commands.group() @commands.group()
@checks.mod() @checks.mod()
async def streamset(self, ctx): async def streamset(self, ctx: commands.Context):
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@streamset.command() @streamset.command()
@checks.is_owner() @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. """Set the Client ID for twitch.
To do this, follow these steps: To do this, follow these steps:
@@ -302,7 +302,7 @@ class Streams:
@streamset.command() @streamset.command()
@checks.is_owner() @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. """Sets the API key for Youtube.
To get one, do the following: To get one, do the following:
@@ -318,14 +318,14 @@ class Streams:
@streamset.group() @streamset.group()
@commands.guild_only() @commands.guild_only()
async def mention(self, ctx): async def mention(self, ctx: commands.Context):
"""Sets mentions for stream alerts.""" """Sets mentions for stream alerts."""
if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention: if ctx.invoked_subcommand is None or ctx.invoked_subcommand == self.mention:
await ctx.send_help() await ctx.send_help()
@mention.command(aliases=["everyone"]) @mention.command(aliases=["everyone"])
@commands.guild_only() @commands.guild_only()
async def all(self, ctx): async def all(self, ctx: commands.Context):
"""Toggles everyone mention""" """Toggles everyone mention"""
guild = ctx.guild guild = ctx.guild
current_setting = await self.db.guild(guild).mention_everyone() current_setting = await self.db.guild(guild).mention_everyone()
@@ -340,7 +340,7 @@ class Streams:
@mention.command(aliases=["here"]) @mention.command(aliases=["here"])
@commands.guild_only() @commands.guild_only()
async def online(self, ctx): async def online(self, ctx: commands.Context):
"""Toggles here mention""" """Toggles here mention"""
guild = ctx.guild guild = ctx.guild
current_setting = await self.db.guild(guild).mention_here() current_setting = await self.db.guild(guild).mention_here()
@@ -355,7 +355,7 @@ class Streams:
@mention.command() @mention.command()
@commands.guild_only() @commands.guild_only()
async def role(self, ctx, *, role: discord.Role): async def role(self, ctx: commands.Context, *, role: discord.Role):
"""Toggles role mention""" """Toggles role mention"""
current_setting = await self.db.role(role).mention() current_setting = await self.db.role(role).mention()
if not role.mentionable: if not role.mentionable:
@@ -373,7 +373,7 @@ class Streams:
@streamset.command() @streamset.command()
@commands.guild_only() @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""" """Toggles automatic deletion of notifications for streams that go offline"""
await self.db.guild(ctx.guild).autodelete.set(on_off) await self.db.guild(ctx.guild).autodelete.set(on_off)
if on_off: if on_off:
@@ -382,7 +382,7 @@ class Streams:
else: else:
await ctx.send("Notifications will never be deleted.") 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: if ctx.channel.id not in stream.channels:
stream.channels.append(ctx.channel.id) stream.channels.append(ctx.channel.id)
if stream not in self.streams: if stream not in self.streams:
@@ -398,7 +398,7 @@ class Streams:
await self.save_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: if ctx.channel.id not in community.channels:
community.channels.append(ctx.channel.id) community.channels.append(ctx.channel.id)
if community not in self.communities: if community not in self.communities:
@@ -473,6 +473,7 @@ class Streams:
except: except:
pass pass
stream._messages_cache.clear() stream._messages_cache.clear()
await self.save_streams()
except: except:
pass pass
else: else:
@@ -490,6 +491,7 @@ class Streams:
try: try:
m = await channel.send(content, embed=embed) m = await channel.send(content, embed=embed)
stream._messages_cache.append(m) stream._messages_cache.append(m)
await self.save_streams()
except: except:
pass pass
@@ -521,6 +523,7 @@ class Streams:
except: except:
pass pass
community._messages_cache.clear() community._messages_cache.clear()
await self.save_communities()
except: except:
pass pass
else: else:
@@ -536,11 +539,13 @@ class Streams:
else: else:
msg = await chn.send(embed=emb) msg = await chn.send(embed=emb)
community._messages_cache.append(msg) community._messages_cache.append(msg)
await self.save_communities()
else: else:
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0] chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
community._messages_cache.remove(chn_msg) community._messages_cache.remove(chn_msg)
await chn_msg.edit(embed=emb) await chn_msg.edit(embed=emb)
community._messages_cache.append(chn_msg) community._messages_cache.append(chn_msg)
await self.save_communities()
async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list: async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
filtered = [] filtered = []
@@ -561,7 +566,12 @@ class Streams:
_class = getattr(StreamClasses, raw_stream["type"], None) _class = getattr(StreamClasses, raw_stream["type"], None)
if not _class: if not _class:
continue 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__) token = await self.db.tokens.get_raw(_class.__name__)
streams.append(_class(token=token, **raw_stream)) streams.append(_class(token=token, **raw_stream))
@@ -581,7 +591,12 @@ class Streams:
_class = getattr(StreamClasses, raw_community["type"], None) _class = getattr(StreamClasses, raw_community["type"], None)
if not _class: if not _class:
continue 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) token = await self.db.tokens.get_raw(_class.__name__, default=None)
communities.append(_class(token=token, **raw_community)) communities.append(_class(token=token, **raw_community))
+8 -2
View File
@@ -27,7 +27,7 @@ class TwitchCommunity:
self.name = kwargs.pop("name") self.name = kwargs.pop("name")
self.id = kwargs.pop("id", None) self.id = kwargs.pop("id", None)
self.channels = kwargs.pop("channels", []) self.channels = kwargs.pop("channels", [])
self._messages_cache = [] self._messages_cache = kwargs.pop("_messages_cache", [])
self._token = kwargs.pop("token", None) self._token = kwargs.pop("token", None)
self.type = self.__class__.__name__ self.type = self.__class__.__name__
@@ -115,6 +115,9 @@ class TwitchCommunity:
for k, v in self.__dict__.items(): for k, v in self.__dict__.items():
if not k.startswith("_"): if not k.startswith("_"):
data[k] = v data[k] = v
data["messages"] = []
for m in self._messages_cache:
data["messages"].append({"channel": m.channel.id, "message": m.id})
return data return data
def __repr__(self): def __repr__(self):
@@ -126,7 +129,7 @@ class Stream:
self.name = kwargs.pop("name", None) self.name = kwargs.pop("name", None)
self.channels = kwargs.pop("channels", []) self.channels = kwargs.pop("channels", [])
#self.already_online = kwargs.pop("already_online", False) #self.already_online = kwargs.pop("already_online", False)
self._messages_cache = [] self._messages_cache = kwargs.pop("_messages_cache", [])
self.type = self.__class__.__name__ self.type = self.__class__.__name__
async def is_online(self): async def is_online(self):
@@ -140,6 +143,9 @@ class Stream:
for k, v in self.__dict__.items(): for k, v in self.__dict__.items():
if not k.startswith("_"): if not k.startswith("_"):
data[k] = v data[k] = v
data["messages"] = []
for m in self._messages_cache:
data["messages"].append({"channel": m.channel.id, "message": m.id})
return data return data
def __repr__(self): def __repr__(self):
+2 -2
View File
@@ -3,7 +3,7 @@ from collections import Counter
import yaml import yaml
import discord import discord
from discord.ext import commands 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 import Config, checks
from redbot.core.data_manager import cog_data_path from redbot.core.data_manager import cog_data_path
from redbot.core.utils.chat_formatting import box, pagify from redbot.core.utils.chat_formatting import box, pagify
@@ -482,7 +482,7 @@ class Trivia:
personal_lists = tuple(p.resolve() personal_lists = tuple(p.resolve()
for p in cog_data_path(self).glob("*.yaml")) 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): def __unload(self):
for session in self.trivia_sessions: for session in self.trivia_sessions:
+13 -14
View File
@@ -1,48 +1,47 @@
from copy import copy from copy import copy
from discord.ext import commands
import asyncio import asyncio
import inspect import inspect
import discord import discord
from redbot.core import RedContext, Config, checks from redbot.core import Config, checks, commands
from redbot.core.i18n import CogI18n 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""" """Handles any action that needs to be taken or not based on the points"""
guild = ctx.guild guild = ctx.guild
guild_settings = config.guild(guild) guild_settings = config.guild(guild)
act = {} act = {}
async with guild_settings.actions() as registered_actions: async with guild_settings.actions() as registered_actions:
for a in registered_actions: for a in registered_actions:
if points >= registered_actions[a]["point_count"]: if points >= a["points"]:
act = registered_actions[a] act = a
else: else:
break break
if act: # some action needs to be taken if act: # some action needs to be taken
await create_and_invoke_context(ctx, act["exceed_command"], user) 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 = ctx.guild
guild_settings = config.guild(guild) guild_settings = config.guild(guild)
act = {} act = {}
async with guild_settings.actions() as registered_actions: async with guild_settings.actions() as registered_actions:
for a in registered_actions: for a in registered_actions:
if points >= registered_actions[a]["point_count"]: if points >= a["points"]:
act = registered_actions[a] act = a
else: else:
break break
if act: # some action needs to be taken if act: # some action needs to be taken
await create_and_invoke_context(ctx, act["drop_command"], user) 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 = copy(realctx.message)
m.content = command_str.format(user=user.mention, prefix=realctx.prefix) 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: try:
await realctx.bot.invoke(fctx) await realctx.bot.invoke(fctx)
except (commands.CheckFailure, commands.CommandOnCooldown): except (commands.CheckFailure, commands.CommandOnCooldown):
@@ -69,7 +68,7 @@ def get_command_from_input(bot, userinput: str):
return "{prefix}" + orig, None 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 """Gets the command to be executed when the user is at or exceeding
the points threshold for the action""" the points threshold for the action"""
await ctx.send( await ctx.send(
@@ -102,7 +101,7 @@ async def get_command_for_exceeded_points(ctx: RedContext):
return command 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 Gets the command to be executed when the user drops below the points
threshold threshold
+41 -29
View File
@@ -1,21 +1,20 @@
from collections import namedtuple from collections import namedtuple
from discord.ext import commands
import discord import discord
import asyncio import asyncio
from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \ from redbot.cogs.warnings.helpers import warning_points_add_check, get_command_for_exceeded_points, \
get_command_for_dropping_points, warning_points_remove_check 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.bot import Red
from redbot.core.context import RedContext from redbot.core.i18n import Translator, cog_i18n
from redbot.core.i18n import CogI18n
from redbot.core.utils.mod import is_admin_or_superior from redbot.core.utils.mod import is_admin_or_superior
from redbot.core.utils.chat_formatting import warning, pagify from redbot.core.utils.chat_formatting import warning, pagify
_ = CogI18n("Warnings", __file__) _ = Translator("Warnings", __file__)
@cog_i18n(_)
class Warnings: class Warnings:
"""A warning system for Red""" """A warning system for Red"""
@@ -51,14 +50,14 @@ class Warnings:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def warningset(self, ctx: RedContext): async def warningset(self, ctx: commands.Context):
"""Warning settings""" """Warning settings"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@warningset.command() @warningset.command()
@commands.guild_only() @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""" """Allow or disallow custom reasons for a warning"""
guild = ctx.guild guild = ctx.guild
await self.config.guild(guild).allow_custom_reasons.set(allowed) await self.config.guild(guild).allow_custom_reasons.set(allowed)
@@ -69,14 +68,14 @@ class Warnings:
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def warnaction(self, ctx: RedContext): async def warnaction(self, ctx: commands.Context):
"""Action management""" """Action management"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@warnaction.command(name="add") @warnaction.command(name="add")
@commands.guild_only() @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 """Create an action to be taken at a specified point count
Duplicate action names are not allowed""" Duplicate action names are not allowed"""
guild = ctx.guild guild = ctx.guild
@@ -120,12 +119,12 @@ class Warnings:
registered_actions.append(to_add) registered_actions.append(to_add)
# Sort in descending order by point count for ease in # Sort in descending order by point count for ease in
# finding the highest possible action to take # 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() await ctx.tick()
@warnaction.command(name="del") @warnaction.command(name="del")
@commands.guild_only() @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""" """Delete the point count action with the specified name"""
guild = ctx.guild guild = ctx.guild
guild_settings = self.config.guild(guild) guild_settings = self.config.guild(guild)
@@ -137,18 +136,23 @@ class Warnings:
break break
if to_remove: if to_remove:
registered_actions.remove(to_remove) registered_actions.remove(to_remove)
await ctx.tick()
else:
await ctx.send(
_("No action named {} exists!").format(action_name)
)
@commands.group() @commands.group()
@commands.guild_only() @commands.guild_only()
@checks.guildowner_or_permissions(administrator=True) @checks.guildowner_or_permissions(administrator=True)
async def warnreason(self, ctx: RedContext): async def warnreason(self, ctx: commands.Context):
"""Add reasons for warnings""" """Add reasons for warnings"""
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
await ctx.send_help() await ctx.send_help()
@warnreason.command(name="add") @warnreason.command(name="add")
@commands.guild_only() @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""" """Add a reason to be available for warnings"""
guild = ctx.guild guild = ctx.guild
@@ -172,37 +176,40 @@ class Warnings:
@warnreason.command(name="del") @warnreason.command(name="del")
@commands.guild_only() @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""" """Delete the reason with the specified name"""
guild = ctx.guild guild = ctx.guild
guild_settings = self.config.guild(guild) guild_settings = self.config.guild(guild)
async with guild_settings.reasons() as registered_reasons: async with guild_settings.reasons() as registered_reasons:
if registered_reasons.pop(reason_name.lower(), None): if registered_reasons.pop(reason_name.lower(), None):
await ctx.send(_("Removed reason {}").format(reason_name)) await ctx.tick()
else: else:
await ctx.send(_("That is not a registered reason name")) await ctx.send(_("That is not a registered reason name"))
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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""" """List all configured reasons for warnings"""
guild = ctx.guild guild = ctx.guild
guild_settings = self.config.guild(guild) guild_settings = self.config.guild(guild)
msg_list = [] msg_list = []
async with guild_settings.reasons() as registered_reasons: async with guild_settings.reasons() as registered_reasons:
for r in registered_reasons.keys(): for r, v in registered_reasons.items():
msg_list.append( msg_list.append(
"Name: {}\nPoints: {}\nAction: {}".format( "Name: {}\nPoints: {}\nDescription: {}".format(
r, r["points"], r["action"] 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.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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""" """List the actions to be taken at specific point values"""
guild = ctx.guild guild = ctx.guild
guild_settings = self.config.guild(guild) guild_settings = self.config.guild(guild)
@@ -210,16 +217,21 @@ class Warnings:
async with guild_settings.actions() as registered_actions: async with guild_settings.actions() as registered_actions:
for r in registered_actions: for r in registered_actions:
msg_list.append( msg_list.append(
"Name: {}\nPoints: {}\nDescription: {}".format( "Name: {}\nPoints: {}\nExceed command: {}\n"
r, r["points"], r["description"] "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.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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 """Warn the user for the specified reason
Reason must be a registered reason, or custom if custom reasons are allowed""" Reason must be a registered reason, or custom if custom reasons are allowed"""
reason_type = {} reason_type = {}
@@ -263,7 +275,7 @@ class Warnings:
@commands.command() @commands.command()
@commands.guild_only() @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. """Show warnings for the specified user.
If userid is None, show warnings for the person running the command If userid is None, show warnings for the person running the command
Note that showing warnings for users other than yourself requires Note that showing warnings for users other than yourself requires
@@ -271,7 +283,7 @@ class Warnings:
if userid is None: if userid is None:
user = ctx.author user = ctx.author
else: 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( await ctx.send(
warning( warning(
_("You are not allowed to check " _("You are not allowed to check "
@@ -313,7 +325,7 @@ class Warnings:
@commands.command() @commands.command()
@commands.guild_only() @commands.guild_only()
@checks.admin_or_permissions(ban_members=True) @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""" """Removes the specified warning from the user specified"""
guild = ctx.guild guild = ctx.guild
member = guild.get_member(user_id) member = guild.get_member(user_id)
@@ -334,7 +346,7 @@ class Warnings:
await ctx.tick() await ctx.tick()
@staticmethod @staticmethod
async def custom_warning_reason(ctx: RedContext): async def custom_warning_reason(ctx: commands.Context):
"""Handles getting description and points for custom reasons""" """Handles getting description and points for custom reasons"""
to_add = { to_add = {
"points": 0, "points": 0,
+3 -4
View File
@@ -1,7 +1,6 @@
from .config import Config from .config import Config
from .context import RedContext
__all__ = ["Config", "RedContext", "__version__"] __all__ = ["Config", "__version__"]
class VersionInfo: class VersionInfo:
@@ -33,5 +32,5 @@ class VersionInfo:
def to_json(self): def to_json(self):
return [self.major, self.minor, self.micro, self.releaselevel, self.serial] return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
__version__ = "3.0.0b12" __version__ = "3.0.0b14"
version_info = VersionInfo(3, 0, 0, 'beta', 12) version_info = VersionInfo(3, 0, 0, 'beta', 14)
+2 -2
View File
@@ -20,7 +20,7 @@ from .cog_manager import CogManager
from . import ( from . import (
Config, Config,
i18n, i18n,
RedContext, commands,
rpc rpc
) )
from .help_formatter import Help, help as help_ 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() admin_role = await self.db.guild(member.guild).admin_role()
return any(role.id in (mod_role, admin_role) for role in member.roles) 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) return await super().get_context(message, cls=cls)
def list_packages(self): def list_packages(self):
+68 -20
View File
@@ -2,9 +2,26 @@ import discord
from discord.ext import commands 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): def is_owner(**kwargs):
async def check(ctx): 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) return commands.check(check)
@@ -15,14 +32,16 @@ async def check_permissions(ctx, perms):
return False return False
resolved = ctx.channel.permissions_for(ctx.author) 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 is_mod_or_superior(ctx):
async def predicate(ctx): if ctx.guild is None:
has_perms_or_is_owner = await check_permissions(ctx, perms) return await ctx.bot.is_owner(ctx.author)
if ctx.guild is None: else:
return has_perms_or_is_owner
author = ctx.author author = ctx.author
settings = ctx.bot.db.guild(ctx.guild) settings = ctx.bot.db.guild(ctx.guild)
mod_role_id = await settings.mod_role() 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) mod_role = discord.utils.get(ctx.guild.roles, id=mod_role_id)
admin_role = discord.utils.get(ctx.guild.roles, id=admin_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 return (
is_guild_owner = author == ctx.guild.owner 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) return commands.check(predicate)
def admin_or_permissions(**perms): def admin_or_permissions(**perms):
async def predicate(ctx): async def predicate(ctx):
has_perms_or_is_owner = await check_permissions(ctx, perms) override = await check_overrides(ctx, level='admin')
if ctx.guild is None: return (
return has_perms_or_is_owner override if override is not None
author = ctx.author else await check_permissions(ctx, perms)
is_guild_owner = author == ctx.guild.owner or await is_admin_or_superior(ctx)
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
return commands.check(predicate) return commands.check(predicate)
@@ -67,7 +111,11 @@ def guildowner_or_permissions(**perms):
return has_perms_or_is_owner return has_perms_or_is_owner
is_guild_owner = ctx.author == ctx.guild.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) return commands.check(predicate)
+4
View File
@@ -89,6 +89,9 @@ def parse_cli_flags(args):
parser.add_argument("--no-cogs", parser.add_argument("--no-cogs",
action="store_true", action="store_true",
help="Starts Red with no cogs loaded, only core") 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", parser.add_argument("--self-bot",
action='store_true', action='store_true',
help="Specifies if Red should log in as selfbot") help="Specifies if Red should log in as selfbot")
@@ -126,3 +129,4 @@ def parse_cli_flags(args):
args.prefix = [] args.prefix = []
return args return args
+6 -4
View File
@@ -8,11 +8,10 @@ from typing import Tuple, Union, List
import redbot.cogs import redbot.cogs
import discord import discord
from . import checks from . import checks, commands
from .config import Config from .config import Config
from .i18n import CogI18n from .i18n import Translator, cog_i18n
from .data_manager import cog_data_path from .data_manager import cog_data_path
from discord.ext import commands
from .utils.chat_formatting import box, pagify from .utils.chat_formatting import box, pagify
@@ -303,10 +302,13 @@ class CogManager:
invalidate_caches() invalidate_caches()
_ = CogI18n("CogManagerUI", __file__) _ = Translator("CogManagerUI", __file__)
@cog_i18n(_)
class CogManagerUI: class CogManagerUI:
"""Commands to interface with Red's cog manager."""
async def visible_paths(self, ctx): async def visible_paths(self, ctx):
install_path = await ctx.bot.cog_mgr.install_path() install_path = await ctx.bot.cog_mgr.install_path()
cog_paths = await ctx.bot.cog_mgr.paths() cog_paths = await ctx.bot.cog_mgr.paths()
+4
View File
@@ -0,0 +1,4 @@
from discord.ext.commands import *
from .commands import *
from .context import *
+74
View 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 import asyncio
from typing import Iterable, List from typing import Iterable, List
import discord import discord
from discord.ext import commands from discord.ext import commands
from redbot.core.utils.chat_formatting import box from redbot.core.utils.chat_formatting import box
__all__ = ["RedContext"]
TICK = "\N{WHITE HEAVY CHECK MARK}" TICK = "\N{WHITE HEAVY CHECK MARK}"
__all__ = ["Context"]
class RedContext(commands.Context):
class Context(commands.Context):
"""Command invocation context for Red. """Command invocation context for Red.
All context passed into commands will be of this type. 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]: async def send_help(self) -> List[discord.Message]:
@@ -128,12 +125,44 @@ class RedContext(commands.Context):
async def embed_requested(self): async def embed_requested(self):
""" """
Simple helper to call bot.embed_requested Simple helper to call bot.embed_requested
with logic around if embed permissions are available
Returns Returns
------- -------
bool: bool:
:code:`True` if an embed is requested :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( return await self.bot.embed_requested(
self.channel, self.author, command=self.command 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)
+27 -14
View File
@@ -16,13 +16,12 @@ from distutils.version import StrictVersion
import aiohttp import aiohttp
import discord import discord
import pkg_resources import pkg_resources
from discord.ext import commands
from redbot.core import __version__ from redbot.core import __version__
from redbot.core import checks from redbot.core import checks
from redbot.core import i18n from redbot.core import i18n
from redbot.core import rpc from redbot.core import rpc
from redbot.core.context import RedContext from redbot.core import commands
from .utils import TYPE_CHECKING from .utils import TYPE_CHECKING
from .utils.chat_formatting import pagify, box, inline from .utils.chat_formatting import pagify, box, inline
@@ -39,9 +38,10 @@ OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be "
"system.** ⚠") "system.** ⚠")
_ = i18n.CogI18n("Core", __file__) _ = i18n.Translator("Core", __file__)
@i18n.cog_i18n(_)
class Core: class Core:
"""Commands related to core functions""" """Commands related to core functions"""
def __init__(self, bot): def __init__(self, bot):
@@ -51,8 +51,16 @@ class Core:
rpc.add_method('core', self.rpc_unload) rpc.add_method('core', self.rpc_unload)
rpc.add_method('core', self.rpc_reload) 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() @commands.command()
async def info(self, ctx: RedContext): async def info(self, ctx: commands.Context):
"""Shows info about Red""" """Shows info about Red"""
author_repo = "https://github.com/Twentysix26" author_repo = "https://github.com/Twentysix26"
org_repo = "https://github.com/Cog-Creators" org_repo = "https://github.com/Cog-Creators"
@@ -72,7 +80,7 @@ class Core:
owner = app_info.owner owner = app_info.owner
async with aiohttp.ClientSession() as session: 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() data = await r.json()
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__) outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
about = ( about = (
@@ -103,7 +111,7 @@ class Core:
await ctx.send("I need the `Embed links` permission to send this") await ctx.send("I need the `Embed links` permission to send this")
@commands.command() @commands.command()
async def uptime(self, ctx: RedContext): async def uptime(self, ctx: commands.Context):
"""Shows Red's uptime""" """Shows Red's uptime"""
since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S") since = ctx.bot.uptime.strftime("%Y-%m-%d %H:%M:%S")
passed = self.get_bot_uptime() passed = self.get_bot_uptime()
@@ -134,7 +142,7 @@ class Core:
return fmt.format(d=days, h=hours, m=minutes, s=seconds) return fmt.format(d=days, h=hours, m=minutes, s=seconds)
@commands.group() @commands.group()
async def embedset(self, ctx: RedContext): async def embedset(self, ctx: commands.Context):
""" """
Commands for toggling embeds on or off. Commands for toggling embeds on or off.
@@ -157,7 +165,7 @@ class Core:
@embedset.command(name="global") @embedset.command(name="global")
@checks.is_owner() @checks.is_owner()
async def embedset_global(self, ctx: RedContext): async def embedset_global(self, ctx: commands.Context):
""" """
Toggle the global embed setting. Toggle the global embed setting.
@@ -175,7 +183,7 @@ class Core:
@embedset.command(name="guild") @embedset.command(name="guild")
@checks.guildowner_or_permissions(administrator=True) @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. Toggle the guild's embed setting.
@@ -200,7 +208,7 @@ class Core:
) )
@embedset.command(name="user") @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. Toggle the user's embed setting.
@@ -280,7 +288,7 @@ class Core:
guilds = sorted(list(self.bot.guilds), guilds = sorted(list(self.bot.guilds),
key=lambda s: s.name.lower()) key=lambda s: s.name.lower())
msg = "" msg = ""
for i, server in enumerate(guilds): for i, server in enumerate(guilds, 1):
msg += "{}: {}\n".format(i, server.name) msg += "{}: {}\n".format(i, server.name)
msg += "\nTo leave a server, just type its number." msg += "\nTo leave a server, just type its number."
@@ -298,7 +306,9 @@ class Core:
await ctx.send("I guess not.") await ctx.send("I guess not.")
break break
try: try:
msg = int(msg.content) msg = int(msg.content) - 1
if msg < 0:
break
await self.leave_confirmation(guilds[msg], owner, ctx) await self.leave_confirmation(guilds[msg], owner, ctx)
break break
except (IndexError, ValueError, AttributeError): except (IndexError, ValueError, AttributeError):
@@ -313,6 +323,9 @@ class Core:
try: try:
msg = await self.bot.wait_for("message", check=conf_check, timeout=15) msg = await self.bot.wait_for("message", check=conf_check, timeout=15)
if msg.content.lower().strip() in ("yes", "y"): 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() await server.leave()
if server != ctx.guild: if server != ctx.guild:
await ctx.send("Done.") await ctx.send("Done.")
@@ -829,7 +842,7 @@ class Core:
@commands.command() @commands.command()
@checks.is_owner() @checks.is_owner()
async def listlocales(self, ctx: RedContext): async def listlocales(self, ctx: commands.Context):
""" """
Lists all available locales Lists all available locales
@@ -875,7 +888,7 @@ class Core:
if data_dir.exists(): if data_dir.exists():
home = data_dir.home() home = data_dir.home()
backup_file = home / backup_filename 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: with tarfile.open(str(backup_file), "w:gz") as tar:
tar.add(data_dir.stem) tar.add(data_dir.stem)
await ctx.send(_("A backup has been made of this instance. It is at {}.").format( await ctx.send(_("A backup has been made of this instance. It is at {}.").format(
+3 -4
View File
@@ -7,9 +7,8 @@ from contextlib import redirect_stdout
from copy import copy from copy import copy
import discord import discord
from discord.ext import commands from . import checks, commands
from . import checks from .i18n import Translator
from .i18n import CogI18n
from .utils.chat_formatting import box, pagify from .utils.chat_formatting import box, pagify
""" """
Notice: Notice:
@@ -19,7 +18,7 @@ Notice:
https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py https://github.com/Rapptz/RoboDanny/blob/master/cogs/repl.py
""" """
_ = CogI18n("Dev", __file__) _ = Translator("Dev", __file__)
class Dev: class Dev:
+13 -4
View File
@@ -61,12 +61,17 @@ def init_events(bot, cli_flags):
return return
bot.uptime = datetime.datetime.utcnow() bot.uptime = datetime.datetime.utcnow()
packages = []
if cli_flags.no_cogs is False: if cli_flags.no_cogs is False:
print("Loading packages...") packages.extend(await bot.db.packages())
failed = []
packages = 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: for package in packages:
try: try:
spec = await bot.cog_mgr.find_cog(package) 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), log.exception("Failed to load package {}".format(package),
exc_info=e) exc_info=e)
await bot.remove_loaded_package(package) await bot.remove_loaded_package(package)
to_remove.append(package)
for package in to_remove:
packages.remove(package)
if packages: if packages:
print("Loaded packages: " + ", ".join(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))) INFO.append('{} cogs with {} commands'.format(len(bot.cogs), len(bot.commands)))
async with aiohttp.ClientSession() as session: 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() data = await r.json()
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version): if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
INFO.append( INFO.append(
@@ -277,3 +285,4 @@ def _get_startup_screen_specs():
ascii_border = False ascii_border = False
return on_symbol, off_symbol, ascii_border return on_symbol, off_symbol, ascii_border
+1 -1
View File
@@ -1,5 +1,5 @@
"""The checks in this module run on every command.""" """The checks in this module run on every command."""
from discord.ext import commands from . import commands
def init_global_checks(bot): def init_global_checks(bot):
+17 -4
View File
@@ -28,7 +28,6 @@ from collections import namedtuple
from typing import List from typing import List
import discord import discord
from discord.ext import commands
from discord.ext.commands import formatter from discord.ext.commands import formatter
import inspect import inspect
import itertools import itertools
@@ -36,6 +35,8 @@ import re
import sys import sys
import traceback import traceback
from . import commands
EMPTY_STRING = u'\u200b' EMPTY_STRING = u'\u200b'
@@ -133,7 +134,12 @@ class Help(formatter.HelpFormatter):
'fields': [] '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: if not description == '' and description is not None:
description = '*{0}*'.format(description) description = '*{0}*'.format(description)
@@ -269,10 +275,18 @@ class Help(formatter.HelpFormatter):
color=color) color=color)
return embed 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() @commands.command()
async def help(ctx, *cmds: str): async def help(ctx, *cmds: str):
"""Shows help documentation. """Shows help documentation.
[p]**help**: Shows the help manual. [p]**help**: Shows the help manual.
[p]**help** command: Show help for a command [p]**help** command: Show help for a command
[p]**help** Category: Show commands and description for a category""" [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( embed=ctx.bot.formatter.simple_embed(
ctx, ctx,
title='Command "{0.name}" has no subcommands.'.format(command), title='Command "{0.name}" has no subcommands.'.format(command),
color=ctx.bot.formatter.color, color=ctx.bot.formatter.color))
author=ctx.author.display_name))
else: else:
await destination.send( await destination.send(
ctx.bot.command_has_no_subcommands.format(command) ctx.bot.command_has_no_subcommands.format(command)
+38 -10
View File
@@ -1,7 +1,10 @@
import re import re
from pathlib import Path 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' _current_locale = 'en_us'
@@ -13,7 +16,7 @@ IN_MSGSTR = 4
MSGID = 'msgid "' MSGID = 'msgid "'
MSGSTR = 'msgstr "' MSGSTR = 'msgstr "'
_i18n_cogs = {} _translators = []
def get_locale(): def get_locale():
@@ -27,8 +30,8 @@ def set_locale(locale):
def reload_locales(): def reload_locales():
for cog_name, i18n in _i18n_cogs.items(): for translator in _translators:
i18n.load_translations() translator.load_translations()
def _parse(translation_file): def _parse(translation_file):
@@ -121,6 +124,9 @@ def _normalize(string, remove_newline=False):
s += ' ' s += ' '
return s return s
if string is None:
return ""
string = string.replace('\\n\\n', '\n\n') string = string.replace('\\n\\n', '\n\n')
string = string.replace('\\n', ' ') string = string.replace('\\n', ' ')
string = string.replace('\\"', '"') 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) 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): def __init__(self, name, file_location):
""" """
Initializes the internationalization object for a given cog. Initializes an internationalization object.
:param name: Your cog name. Parameters
:param file_location: ----------
name : str
Your cog name.
file_location : `str` or `pathlib.Path`
This should always be ``__file__`` otherwise your localizations This should always be ``__file__`` otherwise your localizations
will not load. will not load.
""" """
self.cog_folder = Path(file_location).resolve().parent self.cog_folder = Path(file_location).resolve().parent
self.cog_name = name self.cog_name = name
self.translations = {} self.translations = {}
_i18n_cogs.update({self.cog_name: self}) _translators.append(self)
self.load_translations() self.load_translations()
def __call__(self, untranslated: str): 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) normalized_untranslated = _normalize(untranslated, True)
try: try:
return self.translations[normalized_untranslated] return self.translations[normalized_untranslated]
@@ -172,7 +189,7 @@ class CogI18n:
def load_translations(self): def load_translations(self):
""" """
Loads the current translations for this cog. Loads the current translations.
""" """
self.translations = {} self.translations = {}
translation_file = None translation_file = None
@@ -201,3 +218,14 @@ class CogI18n:
if translated: if translated:
self.translations.update({untranslated: 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
View 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 ""
+2
View File
@@ -22,4 +22,6 @@ def safe_delete(pth: Path):
os.chmod(root, 0o755) os.chmod(root, 0o755)
for d in dirs: for d in dirs:
os.chmod(os.path.join(root, d), 0o755) 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) shutil.rmtree(str(pth), ignore_errors=True)
+141
View 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
}
+96 -26
View File
@@ -4,6 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
import io import io
import sys import sys
import weakref import weakref
from typing import List
_instances = weakref.WeakValueDictionary({}) _instances = weakref.WeakValueDictionary({})
@@ -94,6 +95,88 @@ class Tunnel(metaclass=TunnelMeta):
def minutes_since(self): def minutes_since(self):
return int((self.last_interaction - datetime.utcnow()).seconds / 60) 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, *, async def communicate(self, *,
message: discord.Message, message: discord.Message,
topic: str=None, topic: str=None,
@@ -140,35 +223,22 @@ class Tunnel(metaclass=TunnelMeta):
else: else:
content = topic content = topic
attach = None
if message.attachments: if message.attachments:
files = [] attach = await self.files_from_attatch(message)
size = 0 if not attach:
max_size = 8 * 1024 * 1024 await message.channel.send(
for a in message.attachments: "Could not forward attatchments. "
_fp = io.BytesIO() "Total size of attachments in a single "
await a.save(_fp) "message must be less than 8MB."
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)
) )
else: else:
attach = files attach = []
rets = [] rets = await self.message_forwarder(
for page in pagify(content): destination=send_to,
rets.append( content=content,
await send_to.send(content, files=attach) files=attach
) )
if attach:
del attach
await message.add_reaction("\N{WHITE HEAVY CHECK MARK}") await message.add_reaction("\N{WHITE HEAVY CHECK MARK}")
await message.add_reaction("\N{NEGATIVE SQUARED CROSS MARK}") await message.add_reaction("\N{NEGATIVE SQUARED CROSS MARK}")
+112 -33
View File
@@ -7,7 +7,10 @@ import argparse
import asyncio import asyncio
import pkg_resources 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": if sys.platform == "linux":
import distro import distro
@@ -60,7 +63,7 @@ def parse_cli_args():
return parser.parse_known_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 interpreter = sys.executable
print("Updating Red...") print("Updating Red...")
# If the user ran redbot-launcher.exe, updating with pip will fail # 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" package = "Red-DiscordBot"
if egg_l: if egg_l:
package += "[{}]".format(", ".join(egg_l)) package += "[{}]".format(", ".join(egg_l))
code = subprocess.call([ if reinstall:
interpreter, "-m", code = subprocess.call([
"pip", "install", "-U", interpreter, "-m",
"--process-dependency-links", "pip", "install", "-U", "-I",
package "--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: if code == 0:
print("Red has been updated") print("Red has been updated")
else: else:
@@ -223,6 +235,37 @@ def instance_menu():
return name_num_map[str(selection)] 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(): def clear_screen():
if IS_WINDOWS: if IS_WINDOWS:
os.system("cls") os.system("cls")
@@ -247,6 +290,33 @@ def extras_selector():
return selected 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(): def debug_info():
pyver = sys.version pyver = sys.version
redver = pkg_resources.get_distribution("Red-DiscordBot").version redver = pkg_resources.get_distribution("Red-DiscordBot").version
@@ -275,55 +345,64 @@ def debug_info():
def main_menu(): def main_menu():
if IS_WINDOWS: if IS_WINDOWS:
os.system("TITLE Red - Discord Bot V3 Launcher") os.system("TITLE Red - Discord Bot V3 Launcher")
clear_screen()
while True: while True:
print(INTRO) print(INTRO)
print("1. Run Red w/ autorestart in case of issues") print("1. Run Red w/ autorestart in case of issues")
print("2. Run Red") print("2. Run Red")
print("3. Update Red") print("3. Update Red")
print("4. Update Red (development version)") print("4. Create Instance")
print("5. Create Instance") print("5. Remove Instance")
print("6. Remove Instance") print("6. Debug information (use this if having issues with the launcher or bot)")
print("7. Debug information (use this if having issues with the launcher or bot)") print("7. Reinstall Red")
print("0. Exit") print("0. Exit")
choice = user_choice() choice = user_choice()
if choice == "1": if choice == "1":
instance = instance_menu() instance = instance_menu()
cli_flags = cli_flag_getter()
if instance: if instance:
cli_flags = cli_flag_getter()
run_red(instance, autorestart=True, cliflags=cli_flags) run_red(instance, autorestart=True, cliflags=cli_flags)
wait() wait()
elif choice == "2": elif choice == "2":
instance = instance_menu() instance = instance_menu()
cli_flags = cli_flag_getter()
if instance: if instance:
cli_flags = cli_flag_getter()
run_red(instance, autorestart=False, cliflags=cli_flags) run_red(instance, autorestart=False, cliflags=cli_flags)
wait() wait()
elif choice == "3": elif choice == "3":
selected = extras_selector() development_choice()
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
)
wait() wait()
elif choice == "4": 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() basic_setup()
wait() wait()
elif choice == "6": elif choice == "5":
asyncio.get_event_loop().run_until_complete(remove_instance()) asyncio.get_event_loop().run_until_complete(remove_instance_interaction())
wait() wait()
elif choice == "7": elif choice == "6":
debug_info() 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": elif choice == "0":
break break
clear_screen() clear_screen()
+58 -68
View File
@@ -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() instance_list = load_existing_config()
if not instance_list: if not instance_list:
print("No instances have been set up!") print("No instances have been set up!")
@@ -322,78 +376,14 @@ async def remove_instance():
return return
instance_data = instance_list[selected] instance_data = instance_list[selected]
if confirm("Would you like to make a backup of the data for this instance? (y/n)"): await create_backup(selected, instance_data)
if instance_data["STORAGE_TYPE"] == "MongoDB": await remove_instance(selected, instance_data)
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
def main(): def main():
if args.delete: if args.delete:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(remove_instance()) loop.run_until_complete(remove_instance_interaction())
elif args.edit: elif args.edit:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
loop.run_until_complete(edit_instance()) loop.run_until_complete(edit_instance())
+1 -1
View File
@@ -5,4 +5,4 @@ raven==6.5.0
colorama==0.3.9 colorama==0.3.9
aiohttp-json-rpc==0.8.7 aiohttp-json-rpc==0.8.7
pyyaml==3.12 pyyaml==3.12
Red-Trivia Red-Trivia>=1.1.1
+1 -1
View File
@@ -101,7 +101,7 @@ setup(
'redbot-launcher=redbot.launcher:main' 'redbot-launcher=redbot.launcher:main'
] ]
}, },
python_requires='>=3.5', python_requires='>=3.5,<3.7',
setup_requires=get_requirements(), setup_requires=get_requirements(),
install_requires=get_requirements(), install_requires=get_requirements(),
dependency_links=dep_links, dependency_links=dep_links,