mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 09:22:31 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cb5394e96 | ||
|
|
2b2dbd25f7 | ||
|
|
dd4cd0eeb1 | ||
|
|
ee7b0cf730 | ||
|
|
95ef5d6348 | ||
|
|
23192b9ef6 | ||
|
|
7cd98c8a63 | ||
|
|
fca7686701 | ||
|
|
be767478f4 | ||
|
|
b3ad5d90ed | ||
|
|
fb093b7411 | ||
|
|
e4ea3110e3 | ||
|
|
79676c4f72 | ||
|
|
d61827b92c | ||
|
|
1f1f46c70f | ||
|
|
9188e4a7ec | ||
|
|
e5a780eb0c | ||
|
|
d8c85a2b15 | ||
|
|
83080bc5a2 | ||
|
|
233bfc59ac | ||
|
|
c606caf3a3 |
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
25
.github/ISSUE_TEMPLATE/command_bug.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Command bugs
|
||||
|
||||
<!--
|
||||
Did you find a bug with a command? Fill out the following:
|
||||
-->
|
||||
|
||||
#### Command name
|
||||
|
||||
<!-- Replace this line with the name of the command -->
|
||||
|
||||
#### What cog is this command from?
|
||||
|
||||
<!-- Replace this line with the name of the cog -->
|
||||
|
||||
#### What were you expecting to happen?
|
||||
|
||||
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||
|
||||
#### What actually happened?
|
||||
|
||||
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||
|
||||
#### How can we reproduce this issue?
|
||||
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature_req.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Feature request
|
||||
|
||||
<!-- This template is for feature requests. Please fill out the following: -->
|
||||
|
||||
|
||||
#### Select the type of feature you are requesting:
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] Cog
|
||||
- [ ] Command
|
||||
- [ ] API functionality
|
||||
|
||||
#### Describe your requested feature
|
||||
|
||||
<!--
|
||||
Feel free to describe in as much detail as you wish.
|
||||
|
||||
If you are requesting a cog to be included in core:
|
||||
- Describe the functionality in as much detail as possible
|
||||
- Include the command structure, if possible
|
||||
- Please note that unless it's something that should be core functionality,
|
||||
we reserve the right to reject your suggestion and point you to our cog
|
||||
board to request it for a third-party cog
|
||||
|
||||
If you are requesting a command:
|
||||
- Include what cog it should be in and a name for the command
|
||||
- Describe the intended functionality for the command
|
||||
- Note any restrictions on who can use the command or where it can be used
|
||||
|
||||
If you are requesting API functionality:
|
||||
- Describe what it should do
|
||||
- Note whether it is to extend existing functionality or introduce new functionality
|
||||
|
||||
-->
|
||||
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/other_bug.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Other bugs
|
||||
|
||||
<!--
|
||||
Did you find a bug with something other than a command? Fill out the following:
|
||||
-->
|
||||
|
||||
#### What were you trying to do?
|
||||
|
||||
<!-- Replace this line with a description of what you were trying to do -->
|
||||
|
||||
#### What were you expecting to happen?
|
||||
|
||||
<!-- Replace this line with a description of what you were expecting to happen -->
|
||||
|
||||
#### What actually happened?
|
||||
|
||||
<!-- Replace this line with a description of what actually happened. Include any error messages -->
|
||||
|
||||
#### How can we reproduce this issue?
|
||||
|
||||
<!-- Replace with numbered steps to reproduce the issue -->
|
||||
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
14
.github/PULL_REQUEST_TEMPLATE/bugfix.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Bugfix request
|
||||
|
||||
<!--
|
||||
To be used for pull requests that fix a bug
|
||||
-->
|
||||
|
||||
#### Describe the bug being fixed
|
||||
|
||||
<!--
|
||||
If an issue exists for the bug, mention
|
||||
that this PR fixes that issue
|
||||
-->
|
||||
|
||||
#### Anything we need to know about this fix?
|
||||
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
20
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Enhancement request
|
||||
|
||||
<!--
|
||||
To be used for PRs which enhance existing features
|
||||
-->
|
||||
|
||||
#### Describe the enhancement
|
||||
|
||||
<!--
|
||||
Describe what your changes do.
|
||||
If adding commands, describe any restrictions on their usage.
|
||||
- For example, who can use the command? Where can it be used?
|
||||
-->
|
||||
|
||||
#### Does this enhancement break existing functionality?
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
21
.github/PULL_REQUEST_TEMPLATE/new_feature.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# New feature addition
|
||||
|
||||
<!--
|
||||
To be used for PRs which add a new feature
|
||||
Examples of this include new APIs, new core cogs, etc.
|
||||
-->
|
||||
|
||||
#### What type of feature is this?
|
||||
|
||||
<!-- To check a box, replace the space between the [] with a x -->
|
||||
|
||||
- [ ] New core cog
|
||||
- [ ] New API
|
||||
- [ ] Other
|
||||
|
||||
#### Describe the feature
|
||||
|
||||
<!--
|
||||
If you are adding a cog, describe its commands in detail (functionality, usage restrictions, etc).
|
||||
If the new feature introduces new requirements, please try to explain why they are necessary.
|
||||
-->
|
||||
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
16
.github/PULL_REQUEST_TEMPLATE/release.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# New release
|
||||
|
||||
<!--
|
||||
To be used by collaborators for doing releases.
|
||||
Most contributors will not need to use this.
|
||||
-->
|
||||
|
||||
#### Version
|
||||
|
||||
|
||||
|
||||
#### Has a draft release been created for this?
|
||||
|
||||
- [ ] Yes
|
||||
- [ ] No
|
||||
|
||||
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
5
.github/PULL_REQUEST_TEMPLATE/translations.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Translations update
|
||||
|
||||
<!--
|
||||
Used for PRs updating translations from Crowdin
|
||||
-->
|
||||
@@ -16,6 +16,12 @@ Embed Helpers
|
||||
.. automodule:: redbot.core.utils.embed
|
||||
:members:
|
||||
|
||||
Menu Helpers
|
||||
============
|
||||
|
||||
.. automodule:: redbot.core.utils.menus
|
||||
:members:
|
||||
|
||||
Mod Helpers
|
||||
===========
|
||||
|
||||
|
||||
@@ -90,6 +90,6 @@ have successfully created a cog!
|
||||
Additional resources
|
||||
--------------------
|
||||
|
||||
Be sure to check out the `migration guide </guide_migration>`_ for some resources
|
||||
Be sure to check out the :doc:`/guide_migration` for some resources
|
||||
on developing cogs for V3. This will also cover differences between V2 and V3 for
|
||||
those who developed cogs for V2.
|
||||
|
||||
@@ -14,7 +14,7 @@ Installing the pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo pacman -Sy python-pip git base-devel jre8-openjdk
|
||||
sudo pacman -Syu python-pip git base-devel jre8-openjdk
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import datetime
|
||||
import discord
|
||||
import heapq
|
||||
import lavalink
|
||||
import math
|
||||
import re
|
||||
import redbot.core
|
||||
from discord.ext import commands
|
||||
from redbot.core import Config, checks, bank
|
||||
|
||||
from .manager import shutdown_lavalink_server
|
||||
|
||||
__version__ = "0.0.5"
|
||||
__version__ = "0.0.5a"
|
||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
||||
|
||||
|
||||
@@ -46,6 +48,7 @@ class Audio:
|
||||
self.config.register_guild(**default_guild)
|
||||
self.config.register_global(**default_global)
|
||||
self.skip_votes = {}
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
async def init_config(self):
|
||||
host = await self.config.host()
|
||||
@@ -98,7 +101,8 @@ class Audio:
|
||||
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
|
||||
type=discord.ActivityType.listening))
|
||||
if playing_servers > 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
await self.bot.change_presence(
|
||||
activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
|
||||
@@ -115,7 +119,8 @@ class Audio:
|
||||
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
|
||||
type=discord.ActivityType.listening))
|
||||
if playing_servers > 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
await self.bot.change_presence(
|
||||
activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_EXCEPTION:
|
||||
@@ -143,7 +148,6 @@ class Audio:
|
||||
dj_role_id = await self.config.guild(ctx.guild).dj_role()
|
||||
if dj_role_id is None:
|
||||
await self._embed_msg(ctx, 'Please set a role to use with DJ mode. Enter the role name now.')
|
||||
|
||||
def check(m):
|
||||
return m.author == ctx.author
|
||||
try:
|
||||
@@ -172,8 +176,6 @@ class Audio:
|
||||
@checks.mod_or_permissions(administrator=True)
|
||||
async def jukebox(self, ctx, price: int):
|
||||
"""Set a price for queueing songs for non-mods. 0 to disable."""
|
||||
jukebox = await self.config.guild(ctx.guild).jukebox()
|
||||
jukebox_price = await self.config.guild(ctx.guild).jukebox_price()
|
||||
if price < 0:
|
||||
return await self._embed_msg(ctx, 'Can\'t be less than zero.')
|
||||
if price == 0:
|
||||
@@ -363,6 +365,7 @@ class Audio:
|
||||
|
||||
def check(r, u):
|
||||
return r.message.id == message.id and u == ctx.message.author
|
||||
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for('reaction_add', check=check, timeout=10.0)
|
||||
except asyncio.TimeoutError:
|
||||
@@ -492,6 +495,8 @@ class Audio:
|
||||
if not await self._currency_check(ctx, jukebox_price):
|
||||
return
|
||||
|
||||
if not query:
|
||||
return await self._embed_msg(ctx, 'No songs to play.')
|
||||
query = query.strip('<>')
|
||||
if not query.startswith('http'):
|
||||
query = 'ytsearch:{}'.format(query)
|
||||
@@ -510,7 +515,8 @@ class Audio:
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Enqueued',
|
||||
description='Added {} tracks to the queue.'.format(len(tracks)))
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(text='{} until start of playlist playback: starts at #{} in queue'.format(queue_total_duration, before_queue_length))
|
||||
embed.set_footer(text='{} until start of playlist playback: starts at #{} in queue'.format(
|
||||
queue_total_duration, before_queue_length))
|
||||
if not player.current:
|
||||
await player.play()
|
||||
else:
|
||||
@@ -519,7 +525,8 @@ class Audio:
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued',
|
||||
description='**[{}]({})**'.format(single_track.title, single_track.uri))
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, before_queue_length))
|
||||
embed.set_footer(text='{} until track playback: #{} in queue'.format(
|
||||
queue_total_duration, before_queue_length))
|
||||
if not player.current:
|
||||
await player.play()
|
||||
await ctx.send(embed=embed)
|
||||
@@ -531,17 +538,60 @@ class Audio:
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@playlist.command(name='append')
|
||||
async def _playlist_append(self, ctx, playlist_name, *url):
|
||||
"""Add a song URL, playlist link, or quick search to the end of a saved playlist."""
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
try:
|
||||
if (playlists[playlist_name]['author'] != ctx.author.id and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
to_append = await self._playlist_tracks(ctx, player, url)
|
||||
if not to_append:
|
||||
return
|
||||
track_list = playlists[playlist_name]['tracks']
|
||||
if track_list:
|
||||
playlists[playlist_name]['tracks'] = track_list + to_append
|
||||
else:
|
||||
playlists[playlist_name]['tracks'] = to_append
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'No playlist with that name.')
|
||||
if playlists[playlist_name]['playlist_url'] is not None:
|
||||
playlists[playlist_name]['playlist_url'] = None
|
||||
if len(to_append) == 1:
|
||||
track_title = to_append[0]['info']['title']
|
||||
return await self._embed_msg(ctx, '{} appended to {}.'.format(track_title, playlist_name))
|
||||
await self._embed_msg(ctx, '{} tracks appended to {}.'.format(len(to_append), playlist_name))
|
||||
|
||||
@playlist.command(name='create')
|
||||
async def _playlist_create(self, ctx, playlist_name):
|
||||
"""Create an empty playlist."""
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
if dj_enabled:
|
||||
if not await self._can_instaskip(ctx, ctx.author):
|
||||
return await self._embed_msg(ctx, 'You need the DJ role to save playlists.')
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
if playlist_name in playlists:
|
||||
return await self._embed_msg(ctx, 'Playlist name already exists, try again with a different name.')
|
||||
playlist_list = self._to_json(ctx, None, None)
|
||||
playlists[playlist_name] = playlist_list
|
||||
await self._embed_msg(ctx, 'Empty playlist {} created.'.format(playlist_name))
|
||||
|
||||
@playlist.command(name='delete')
|
||||
async def _playlist_delete(self, ctx, playlist_name):
|
||||
"""Delete a saved playlist."""
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
try:
|
||||
if playlists[playlist_name]['author'] != ctx.author.id and not await self._can_instaskip(ctx, ctx.author):
|
||||
if (playlists[playlist_name]['author'] != ctx.author.id and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
|
||||
del playlists[playlist_name]
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'No playlist with that name.')
|
||||
await self._embed_msg(ctx, '{} playlist removed.'.format(playlist_name))
|
||||
await self._embed_msg(ctx, '{} playlist deleted.'.format(playlist_name))
|
||||
|
||||
@playlist.command(name='info')
|
||||
async def _playlist_info(self, ctx, playlist_name):
|
||||
@@ -556,18 +606,15 @@ class Audio:
|
||||
try:
|
||||
track_len = len(playlists[playlist_name]['tracks'])
|
||||
except TypeError:
|
||||
track_len = 1
|
||||
track_len = 0
|
||||
if playlist_url is None:
|
||||
playlist_url = '**Not generated from a URL.**'
|
||||
playlist_url = '**Custom playlist.**'
|
||||
else:
|
||||
playlist_url = 'URL: <{}>'.format(playlist_url)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist info for {}:'.format(playlist_name),
|
||||
description='Author: **{}**\n{}'.format(author_obj,
|
||||
playlist_url))
|
||||
if track_len > 1:
|
||||
embed.set_footer(text='{} tracks'.format(track_len))
|
||||
if track_len == 1:
|
||||
embed.set_footer(text='{} track'.format(track_len))
|
||||
embed.set_footer(text='{} track(s)'.format(track_len))
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@playlist.command(name='list')
|
||||
@@ -597,11 +644,11 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
tracklist = []
|
||||
np_song = self._track_creator(ctx, player, 'np', None)
|
||||
np_song = self._track_creator(player, 'np')
|
||||
tracklist.append(np_song)
|
||||
for track in player.queue:
|
||||
queue_idx = player.queue.index(track)
|
||||
track_obj = self._track_creator(ctx, player, queue_idx, None)
|
||||
track_obj = self._track_creator(player, queue_idx)
|
||||
tracklist.append(track_obj)
|
||||
if not playlist_name:
|
||||
await self._embed_msg(ctx, 'Please enter a name for this playlist.')
|
||||
@@ -616,11 +663,38 @@ class Audio:
|
||||
return await self._embed_msg(ctx, 'Playlist name already exists, try again with a different name.')
|
||||
except asyncio.TimeoutError:
|
||||
return await self._embed_msg(ctx, 'No playlist name entered, try again later.')
|
||||
|
||||
playlist_list = self._to_json(ctx, None, tracklist, playlist_name)
|
||||
playlist_list = self._to_json(ctx, None, tracklist)
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlists[playlist_name] = playlist_list
|
||||
await self._embed_msg(ctx, 'Playlist {} saved from current queue: {} tracks added.'.format(playlist_name, len(tracklist)))
|
||||
await self._embed_msg(ctx, 'Playlist {} saved from current queue: {} tracks added.'.format(
|
||||
playlist_name, len(tracklist)))
|
||||
|
||||
@playlist.command(name='remove')
|
||||
async def _playlist_remove(self, ctx, playlist_name, url):
|
||||
"""Remove a song from a playlist by url."""
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
try:
|
||||
if (playlists[playlist_name]['author'] != ctx.author.id and not
|
||||
await self._can_instaskip(ctx, ctx.author)):
|
||||
return await self._embed_msg(ctx, 'You are not the author of that playlist.')
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'No playlist with that name.')
|
||||
track_list = playlists[playlist_name]['tracks']
|
||||
clean_list = [track for track in track_list if not url == track['info']['uri']]
|
||||
if len(playlists[playlist_name]['tracks']) == len(clean_list):
|
||||
return await self._embed_msg(ctx, 'URL not in playlist.')
|
||||
del_count = len(playlists[playlist_name]['tracks']) - len(clean_list)
|
||||
if not clean_list:
|
||||
del playlists[playlist_name]
|
||||
return await self._embed_msg(ctx, 'No songs left, removing playlist.')
|
||||
playlists[playlist_name]['tracks'] = clean_list
|
||||
if playlists[playlist_name]['playlist_url'] is not None:
|
||||
playlists[playlist_name]['playlist_url'] = None
|
||||
if del_count > 1:
|
||||
await self._embed_msg(ctx, '{} entries have been removed from the {} playlist.'.format(
|
||||
del_count, playlist_name))
|
||||
else:
|
||||
await self._embed_msg(ctx, 'The track has been removed from the {} playlist.'.format(playlist_name))
|
||||
|
||||
@playlist.command(name='save')
|
||||
async def _playlist_save(self, ctx, playlist_name, playlist_url):
|
||||
@@ -628,18 +702,13 @@ class Audio:
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
tracks = await player.get_tracks(playlist_url)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found.')
|
||||
tracklist = []
|
||||
for track in tracks:
|
||||
track_obj = self._track_creator(ctx, player, None, track)
|
||||
tracklist.append(track_obj)
|
||||
playlist_list = self._to_json(ctx, playlist_url, tracklist, playlist_name)
|
||||
|
||||
tracklist = await self._playlist_tracks(ctx, player, playlist_url)
|
||||
playlist_list = self._to_json(ctx, playlist_url, tracklist)
|
||||
if tracklist is not None:
|
||||
async with self.config.guild(ctx.guild).playlists() as playlists:
|
||||
playlists[playlist_name] = playlist_list
|
||||
return await self._embed_msg(ctx, 'Playlist {} saved: {} tracks added.'.format(playlist_name, len(tracks)))
|
||||
return await self._embed_msg(ctx, 'Playlist {} saved: {} tracks added.'.format(
|
||||
playlist_name, len(tracklist)))
|
||||
|
||||
@playlist.command(name='start')
|
||||
async def _playlist_start(self, ctx, playlist_name=None):
|
||||
@@ -647,14 +716,9 @@ class Audio:
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
playlists = await self.config.guild(ctx.guild).playlists.get_raw()
|
||||
try:
|
||||
author_id = playlists[playlist_name]["author"]
|
||||
except KeyError:
|
||||
return await self._embed_msg(ctx, 'That playlist doesn\'t exist.')
|
||||
author_obj = self.bot.get_user(author_id)
|
||||
author_obj = self.bot.get_user(ctx.author.id)
|
||||
track_count = 0
|
||||
try:
|
||||
playlist_len = len(playlists[playlist_name]["tracks"])
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
for track in playlists[playlist_name]["tracks"]:
|
||||
player.add(author_obj, lavalink.rest_api.Track(data=track))
|
||||
@@ -666,6 +730,75 @@ class Audio:
|
||||
await player.play()
|
||||
except TypeError:
|
||||
await ctx.invoke(self.play, query=playlists[playlist_name]["playlist_url"])
|
||||
except KeyError:
|
||||
await self._embed_msg(ctx, 'That playlist doesn\'t exist.')
|
||||
|
||||
@checks.is_owner()
|
||||
@playlist.command(name='upload')
|
||||
async def _playlist_upload(self, ctx):
|
||||
"""Convert a Red v2 playlist file to a playlist."""
|
||||
if not await self._playlist_check(ctx):
|
||||
return
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
await self._embed_msg(ctx, 'Please upload the playlist file. Any other message will cancel this operation.')
|
||||
def check(m):
|
||||
return m.author == ctx.author
|
||||
try:
|
||||
file_message = await ctx.bot.wait_for('message', timeout=30.0, check=check)
|
||||
except asyncio.TimeoutError:
|
||||
return await self._embed_msg(ctx, 'No file detected, try again later.')
|
||||
try:
|
||||
file_url = file_message.attachments[0].url
|
||||
except IndexError:
|
||||
return await self._embed_msg(ctx, 'Upload canceled.')
|
||||
v2_playlist_name = (file_url.split('/')[6]).split('.')[0]
|
||||
file_suffix = file_url.rsplit('.', 1)[1]
|
||||
if file_suffix != "txt":
|
||||
return await self._embed_msg(ctx, 'Only playlist files can be uploaded.')
|
||||
async with self.session.request('GET', file_url) as r:
|
||||
v2_playlist = await r.json(content_type='text/plain')
|
||||
try:
|
||||
v2_playlist_url = v2_playlist["link"]
|
||||
except KeyError:
|
||||
v2_playlist_url = None
|
||||
if (not v2_playlist_url or not self._match_yt_playlist(v2_playlist_url) or not
|
||||
await player.get_tracks(v2_playlist_url)):
|
||||
track_list = []
|
||||
track_count = 0
|
||||
async with self.config.guild(ctx.guild).playlists() as v3_playlists:
|
||||
try:
|
||||
if v3_playlists[v2_playlist_name]:
|
||||
return await self._embed_msg(ctx, 'A playlist already exists with this name.')
|
||||
except KeyError:
|
||||
pass
|
||||
embed1 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Please wait, adding tracks...')
|
||||
playlist_msg = await ctx.send(embed=embed1)
|
||||
for song_url in v2_playlist["playlist"]:
|
||||
track = await player.get_tracks(song_url)
|
||||
try:
|
||||
track_obj = self._track_creator(player, other_track=track[0])
|
||||
track_list.append(track_obj)
|
||||
track_count = track_count + 1
|
||||
except IndexError:
|
||||
pass
|
||||
if track_count % 5 == 0:
|
||||
embed2 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Loading track {}/{}...'.format(
|
||||
track_count, len(v2_playlist["playlist"])))
|
||||
await playlist_msg.edit(embed=embed2)
|
||||
if not track_list:
|
||||
return await self._embed_msg(ctx, 'No tracks found.')
|
||||
playlist_list = self._to_json(ctx, v2_playlist_url, track_list)
|
||||
v3_playlists[v2_playlist_name] = playlist_list
|
||||
if len(v2_playlist["playlist"]) != track_count:
|
||||
bad_tracks = len(v2_playlist["playlist"]) - track_count
|
||||
msg = ('Added {} tracks from the {} playlist. {} track(s) could not '
|
||||
'be loaded.'.format(track_count, v2_playlist_name, bad_tracks))
|
||||
else:
|
||||
msg = 'Added {} tracks from the {} playlist.'.format(track_count, v2_playlist_name)
|
||||
embed3 = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Playlist Saved', description=msg)
|
||||
await playlist_msg.edit(embed=embed3)
|
||||
else:
|
||||
await ctx.invoke(self._playlist_save, v2_playlist_name, v2_playlist_url)
|
||||
|
||||
async def _playlist_check(self, ctx):
|
||||
dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
|
||||
@@ -694,6 +827,27 @@ class Audio:
|
||||
await self._data_check(ctx)
|
||||
return True
|
||||
|
||||
async def _playlist_tracks(self, ctx, player, query):
|
||||
search = False
|
||||
if type(query) is tuple:
|
||||
query = " ".join(query)
|
||||
if not query.startswith('http'):
|
||||
query = " ".join(query)
|
||||
query = 'ytsearch:{}'.format(query)
|
||||
search = True
|
||||
tracks = await player.get_tracks(query)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found.')
|
||||
tracklist = []
|
||||
if not search:
|
||||
for track in tracks:
|
||||
track_obj = self._track_creator(player, other_track=track)
|
||||
tracklist.append(track_obj)
|
||||
else:
|
||||
track_obj = self._track_creator(player, other_track=tracks[0])
|
||||
tracklist.append(track_obj)
|
||||
return tracklist
|
||||
|
||||
@commands.command()
|
||||
async def prev(self, ctx):
|
||||
"""Skips to the start of the previously played track."""
|
||||
@@ -773,8 +927,8 @@ class Audio:
|
||||
|
||||
for i, track in enumerate(player.queue[start:end], start=start):
|
||||
req_user = track.requester
|
||||
next = i + 1
|
||||
queue_list += '`{}.` **[{}]({})**, requested by **{}**\n'.format(next, track.title, track.uri, req_user)
|
||||
_next = i + 1
|
||||
queue_list += '`{}.` **[{}]({})**, requested by **{}**\n'.format(_next, track.title, track.uri, req_user)
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Queue for ' + ctx.guild.name,
|
||||
description=queue_list)
|
||||
@@ -872,8 +1026,8 @@ class Audio:
|
||||
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,
|
||||
_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)
|
||||
@@ -906,7 +1060,8 @@ class Audio:
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
if not shuffle and queue_duration > 0:
|
||||
songembed.set_footer(text='{} until start of search playback: starts at #{} in queue'.format(queue_total_duration, (len(player.queue) + 1)))
|
||||
songembed.set_footer(text='{} until start of search playback: starts at #{} in queue'.format(
|
||||
queue_total_duration, (len(player.queue) + 1)))
|
||||
for track in tracks:
|
||||
player.add(ctx.author, track)
|
||||
if not player.current:
|
||||
@@ -926,7 +1081,8 @@ class Audio:
|
||||
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)))
|
||||
embed.set_footer(text='{} until track playback: #{} in queue'.format(queue_total_duration, (
|
||||
len(player.queue) + 1)))
|
||||
player.add(ctx.author, search_choice)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
@@ -1052,11 +1208,11 @@ class Audio:
|
||||
nonbots = sum(not m.bot for m in ctx.guild.get_member(self.bot.user.id).voice.channel.members)
|
||||
if nonbots == 1:
|
||||
nonbots = 2
|
||||
else:
|
||||
if ctx.guild.get_member(member.id).voice.channel.members == 1:
|
||||
elif ctx.guild.get_member(member.id).voice.channel.members == 1:
|
||||
nonbots = 1
|
||||
alone = nonbots <= 1
|
||||
return alone
|
||||
else:
|
||||
nonbots = 0
|
||||
return nonbots <= 1
|
||||
|
||||
async def _has_dj_role(self, ctx, member):
|
||||
dj_role_id = await self.config.guild(ctx.guild).dj_role()
|
||||
@@ -1165,7 +1321,8 @@ class Audio:
|
||||
await self.config.password.set('youshallnotpass')
|
||||
await self.config.rest_port.set(2333)
|
||||
await self.config.ws_port.set(2332)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='External lavalink server: {}.'.format(not external))
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='External lavalink server: {}.'.format(
|
||||
not external))
|
||||
embed.set_footer(text='Defaults reset.')
|
||||
return await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -1187,7 +1344,8 @@ class Audio:
|
||||
"""Set the lavalink server password."""
|
||||
await self.config.password.set(str(password))
|
||||
if await self._check_external():
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Server password set to {}.'.format(password))
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour,
|
||||
title='Server password set to {}.'.format(password))
|
||||
embed.set_footer(text='External lavalink server set to True.')
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -1209,7 +1367,8 @@ class Audio:
|
||||
"""Set the lavalink websocket server port."""
|
||||
await self.config.rest_port.set(ws_port)
|
||||
if await self._check_external():
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Websocket port set to {}.'.format(ws_port))
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour,
|
||||
title='Websocket port set to {}.'.format(ws_port))
|
||||
embed.set_footer(text='External lavalink server set to True.')
|
||||
await ctx.send(embed=embed)
|
||||
else:
|
||||
@@ -1255,7 +1414,8 @@ class Audio:
|
||||
if player.volume != volume:
|
||||
await player.set_volume(volume)
|
||||
|
||||
async def _draw_time(self, ctx):
|
||||
@staticmethod
|
||||
async def _draw_time(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
paused = player.paused
|
||||
pos = player.position
|
||||
@@ -1305,6 +1465,15 @@ class Audio:
|
||||
else:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _match_yt_playlist(url):
|
||||
yt_list_playlist = re.compile(
|
||||
r'^(https?\:\/\/)?(www\.)?(youtube\.com|youtu\.?be)'
|
||||
r'(\/playlist\?).*(list=)(.*)(&|$)')
|
||||
if yt_list_playlist.match(url):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def _queue_duration(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
@@ -1333,14 +1502,16 @@ class Audio:
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def _to_json(self, ctx, playlist_url, tracklist, playlist_name):
|
||||
@staticmethod
|
||||
def _to_json(ctx, playlist_url, tracklist):
|
||||
playlist = {"author": ctx.author.id, "playlist_url": playlist_url, "tracks": tracklist}
|
||||
return playlist
|
||||
|
||||
def _track_creator(self, ctx, player, position, other_track=None):
|
||||
@staticmethod
|
||||
def _track_creator(player, position=None, other_track=None):
|
||||
if position == 'np':
|
||||
queued_track = player.current
|
||||
elif position == None:
|
||||
elif position is None:
|
||||
queued_track = other_track
|
||||
else:
|
||||
queued_track = player.queue[position]
|
||||
@@ -1365,6 +1536,7 @@ class Audio:
|
||||
pass
|
||||
|
||||
def __unload(self):
|
||||
self.session.close()
|
||||
lavalink.unregister_event_listener(self.event_handler)
|
||||
self.bot.loop.create_task(lavalink.close())
|
||||
shutdown_lavalink_server()
|
||||
|
||||
@@ -92,4 +92,5 @@ def shutdown_lavalink_server():
|
||||
global proc
|
||||
if proc is not None:
|
||||
proc.terminate()
|
||||
proc.wait()
|
||||
proc = None
|
||||
|
||||
@@ -124,13 +124,23 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def user(self, ctx: RedContext, user: discord.Member or int, number: int):
|
||||
async def user(self, ctx: RedContext, user: str, number: int):
|
||||
"""Deletes last X messages from specified user.
|
||||
|
||||
Examples:
|
||||
cleanup user @\u200bTwentysix 2
|
||||
cleanup user Red 6"""
|
||||
|
||||
try:
|
||||
member = await commands.converter.MemberConverter().convert(ctx, user)
|
||||
except commands.BadArgument:
|
||||
try:
|
||||
_id = int(user)
|
||||
except ValueError:
|
||||
raise commands.BadArgument()
|
||||
else:
|
||||
_id = member.id
|
||||
|
||||
channel = ctx.channel
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
@@ -141,9 +151,7 @@ class Cleanup:
|
||||
return
|
||||
|
||||
def check(m):
|
||||
if isinstance(user, discord.Member) and m.author == user:
|
||||
return True
|
||||
elif m.author.id == user: # Allow finding messages based on an ID
|
||||
if m.author.id == _id:
|
||||
return True
|
||||
elif m == ctx.message:
|
||||
return True
|
||||
@@ -156,7 +164,7 @@ class Cleanup:
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
" made by {}({}) in channel {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
user.name, user.id, channel.name)
|
||||
member or '???', _id, channel.name)
|
||||
log.info(reason)
|
||||
|
||||
if is_bot:
|
||||
|
||||
@@ -256,6 +256,7 @@ class Mod:
|
||||
@commands.guild_only()
|
||||
async def deletedelay(self, ctx: RedContext, time: int=None):
|
||||
"""Sets the delay until the bot removes the command message.
|
||||
|
||||
Must be between -1 and 60.
|
||||
|
||||
A delay of -1 means the bot will not remove the message."""
|
||||
@@ -281,10 +282,10 @@ class Mod:
|
||||
@modset.command()
|
||||
@commands.guild_only()
|
||||
async def reinvite(self, ctx: RedContext):
|
||||
"""Toggles whether an invite will be sent when a user
|
||||
is unbanned via [p]unban. If this is True, the bot will
|
||||
attempt to create and send a single-use invite to the
|
||||
newly-unbanned user"""
|
||||
"""Toggles whether an invite will be sent when a user is unbanned via [p]unban.
|
||||
|
||||
If this is True, the bot will attempt to create and send a single-use invite
|
||||
to the newly-unbanned user"""
|
||||
guild = ctx.guild
|
||||
cur_setting = await self.settings.guild(guild).reinvite_on_unban()
|
||||
if not cur_setting:
|
||||
@@ -299,8 +300,8 @@ class Mod:
|
||||
@checks.admin_or_permissions(kick_members=True)
|
||||
async def kick(self, ctx: RedContext, user: discord.Member, *, reason: str = None):
|
||||
"""Kicks user.
|
||||
If a reason is specified, it
|
||||
will be the reason that shows up
|
||||
|
||||
If a reason is specified, it will be the reason that shows up
|
||||
in the audit log"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
|
||||
@@ -2,7 +2,8 @@ import logging
|
||||
import asyncio
|
||||
from typing import Union
|
||||
from datetime import timedelta
|
||||
|
||||
from copy import copy
|
||||
import contextlib
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
@@ -109,9 +110,10 @@ class Reports:
|
||||
ret |= await self.bot.is_owner(m)
|
||||
return ret
|
||||
|
||||
async def discover_guild(self, author: discord.User, *,
|
||||
async def discover_guild(
|
||||
self, author: discord.User, *,
|
||||
mod: bool=False,
|
||||
permissions: Union[discord.Permissions, dict]={},
|
||||
permissions: Union[discord.Permissions, dict]=None,
|
||||
prompt: str=""):
|
||||
"""
|
||||
discovers which of shared guilds between the bot
|
||||
@@ -120,10 +122,12 @@ class Reports:
|
||||
prompt is for providing a user prompt for selection
|
||||
"""
|
||||
shared_guilds = []
|
||||
if isinstance(permissions, discord.Permissions):
|
||||
if permissions is None:
|
||||
perms = discord.Permissions()
|
||||
elif isinstance(permissions, discord.Permissions):
|
||||
perms = permissions
|
||||
else:
|
||||
permissions = discord.Permissions(**perms)
|
||||
perms = discord.Permissions(**permissions)
|
||||
|
||||
for guild in self.bot.guilds:
|
||||
x = guild.get_member(author.id)
|
||||
@@ -170,26 +174,40 @@ class Reports:
|
||||
|
||||
author = guild.get_member(msg.author.id)
|
||||
report = msg.clean_content
|
||||
avatar = author.avatar_url
|
||||
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_('Report from {0.display_name}').format(author),
|
||||
icon_url=avatar
|
||||
)
|
||||
|
||||
ticket_number = await self.config.guild(guild).next_ticket()
|
||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
|
||||
channel_id = await self.config.guild(guild).output_channel()
|
||||
channel = guild.get_channel(channel_id)
|
||||
if channel is not None:
|
||||
try:
|
||||
await channel.send(embed=em)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
if channel is None:
|
||||
return None
|
||||
|
||||
files = await Tunnel.files_from_attatch(msg)
|
||||
|
||||
ticket_number = await self.config.guild(guild).next_ticket()
|
||||
await self.config.guild(guild).next_ticket.set(ticket_number + 1)
|
||||
|
||||
if await self.bot.embed_requested(channel, author):
|
||||
em = discord.Embed(description=report)
|
||||
em.set_author(
|
||||
name=_('Report from {0.display_name}').format(author),
|
||||
icon_url=author.avatar_url
|
||||
)
|
||||
em.set_footer(text=_("Report #{}").format(ticket_number))
|
||||
send_content = None
|
||||
else:
|
||||
em = None
|
||||
send_content = _(
|
||||
'Report from {author.mention} (Ticket #{number})'
|
||||
).format(author=author, number=ticket_number)
|
||||
send_content += "\n" + report
|
||||
|
||||
try:
|
||||
await Tunnel.message_forwarder(
|
||||
destination=channel,
|
||||
content=send_content,
|
||||
embed=em,
|
||||
files=files
|
||||
)
|
||||
except (discord.Forbidden, discord.HTTPException):
|
||||
return None
|
||||
|
||||
await self.config.custom('REPORT', guild.id, ticket_number).report.set(
|
||||
@@ -198,8 +216,13 @@ class Reports:
|
||||
return ticket_number
|
||||
|
||||
@commands.group(name="report", invoke_without_command=True)
|
||||
async def report(self, ctx: RedContext):
|
||||
"Follow the prompts to make a report"
|
||||
async def report(self, ctx: RedContext, *, _report: str=""):
|
||||
"""
|
||||
Follow the prompts to make a report
|
||||
|
||||
optionally use with a report message
|
||||
to use it non interactively
|
||||
"""
|
||||
author = ctx.author
|
||||
guild = ctx.guild
|
||||
if guild is None:
|
||||
@@ -243,6 +266,12 @@ class Reports:
|
||||
pass
|
||||
self.user_cache.append(author.id)
|
||||
|
||||
if _report:
|
||||
_m = copy(ctx.message)
|
||||
_m.content = _report
|
||||
_m.content = _m.clean_content
|
||||
val = await self.send_report(_m, guild)
|
||||
else:
|
||||
try:
|
||||
dm = await author.send(
|
||||
_("Please respond to this message with your Report."
|
||||
@@ -268,6 +297,8 @@ class Reports:
|
||||
)
|
||||
else:
|
||||
val = await self.send_report(message, guild)
|
||||
|
||||
with contextlib.suppress(discord.Forbidden, discord.HTTPException):
|
||||
if val is None:
|
||||
await author.send(
|
||||
_("There was an error sending your report.")
|
||||
@@ -353,7 +384,7 @@ class Reports:
|
||||
"will be forwarded to them until the communication is closed.\n"
|
||||
"You can close a communication at any point "
|
||||
"by reacting with the X to the last message recieved. "
|
||||
"\nAny message succesfully forwarded with be marked with a check."
|
||||
"\nAny message succesfully forwarded will be marked with a check."
|
||||
"\nTunnels are not persistent across bot restarts."
|
||||
)
|
||||
topic = big_topic.format(
|
||||
|
||||
@@ -17,8 +17,8 @@ async def warning_points_add_check(config: Config, ctx: RedContext, user: discor
|
||||
act = {}
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for a in registered_actions:
|
||||
if points >= registered_actions[a]["point_count"]:
|
||||
act = registered_actions[a]
|
||||
if points >= a["points"]:
|
||||
act = a
|
||||
else:
|
||||
break
|
||||
if act: # some action needs to be taken
|
||||
@@ -31,8 +31,8 @@ async def warning_points_remove_check(config: Config, ctx: RedContext, user: dis
|
||||
act = {}
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for a in registered_actions:
|
||||
if points >= registered_actions[a]["point_count"]:
|
||||
act = registered_actions[a]
|
||||
if points >= a["points"]:
|
||||
act = a
|
||||
else:
|
||||
break
|
||||
if act: # some action needs to be taken
|
||||
|
||||
@@ -120,7 +120,7 @@ class Warnings:
|
||||
registered_actions.append(to_add)
|
||||
# Sort in descending order by point count for ease in
|
||||
# finding the highest possible action to take
|
||||
registered_actions.sort(key=lambda a: a["point_count"], reverse=True)
|
||||
registered_actions.sort(key=lambda a: a["points"], reverse=True)
|
||||
await ctx.tick()
|
||||
|
||||
@warnaction.command(name="del")
|
||||
@@ -137,6 +137,11 @@ class Warnings:
|
||||
break
|
||||
if to_remove:
|
||||
registered_actions.remove(to_remove)
|
||||
await ctx.tick()
|
||||
else:
|
||||
await ctx.send(
|
||||
_("No action named {} exists!").format(action_name)
|
||||
)
|
||||
|
||||
@commands.group()
|
||||
@commands.guild_only()
|
||||
@@ -178,7 +183,7 @@ class Warnings:
|
||||
guild_settings = self.config.guild(guild)
|
||||
async with guild_settings.reasons() as registered_reasons:
|
||||
if registered_reasons.pop(reason_name.lower(), None):
|
||||
await ctx.send(_("Removed reason {}").format(reason_name))
|
||||
await ctx.tick()
|
||||
else:
|
||||
await ctx.send(_("That is not a registered reason name"))
|
||||
|
||||
@@ -191,13 +196,16 @@ class Warnings:
|
||||
guild_settings = self.config.guild(guild)
|
||||
msg_list = []
|
||||
async with guild_settings.reasons() as registered_reasons:
|
||||
for r in registered_reasons.keys():
|
||||
for r, v in registered_reasons.items():
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nAction: {}".format(
|
||||
r, r["points"], r["action"]
|
||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||
r, v["points"], v["description"]
|
||||
)
|
||||
)
|
||||
if msg_list:
|
||||
await ctx.send_interactive(msg_list)
|
||||
else:
|
||||
await ctx.send(_("There are no reasons configured!"))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@@ -210,11 +218,16 @@ class Warnings:
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for r in registered_actions:
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||
r, r["points"], r["description"]
|
||||
"Name: {}\nPoints: {}\nExceed command: {}\n"
|
||||
"Drop command: {}".format(
|
||||
r["action_name"], r["points"], r["exceed_command"],
|
||||
r["drop_command"]
|
||||
)
|
||||
)
|
||||
if msg_list:
|
||||
await ctx.send_interactive(msg_list)
|
||||
else:
|
||||
await ctx.send(_("There are no actions configured!"))
|
||||
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@@ -271,7 +284,7 @@ class Warnings:
|
||||
if userid is None:
|
||||
user = ctx.author
|
||||
else:
|
||||
if not is_admin_or_superior(self.bot, ctx.author):
|
||||
if not await is_admin_or_superior(self.bot, ctx.author):
|
||||
await ctx.send(
|
||||
warning(
|
||||
_("You are not allowed to check "
|
||||
|
||||
@@ -33,5 +33,5 @@ class VersionInfo:
|
||||
def to_json(self):
|
||||
return [self.major, self.minor, self.micro, self.releaselevel, self.serial]
|
||||
|
||||
__version__ = "3.0.0b12"
|
||||
version_info = VersionInfo(3, 0, 0, 'beta', 12)
|
||||
__version__ = "3.0.0b13"
|
||||
version_info = VersionInfo(3, 0, 0, 'beta', 13)
|
||||
|
||||
@@ -137,3 +137,32 @@ class RedContext(commands.Context):
|
||||
return await self.bot.embed_requested(
|
||||
self.channel, self.author, command=self.command
|
||||
)
|
||||
|
||||
async def maybe_send_embed(self, message: str) -> discord.Message:
|
||||
"""
|
||||
Simple helper to send a simple message to context
|
||||
without manually checking ctx.embed_requested
|
||||
This should only be used for simple messages.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message: `str`
|
||||
The string to send
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Message:
|
||||
the message which was sent
|
||||
|
||||
Raises
|
||||
------
|
||||
discord.Forbidden
|
||||
see `discord.abc.Messageable.send`
|
||||
discord.HTTPException
|
||||
see `discord.abc.Messageable.send`
|
||||
"""
|
||||
|
||||
if await self.embed_requested():
|
||||
return await self.send(embed=discord.Embed(description=message))
|
||||
else:
|
||||
return await self.send(message)
|
||||
|
||||
@@ -72,7 +72,7 @@ class Core:
|
||||
owner = app_info.owner
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
async with session.get('{}/json'.format(red_pypi)) as r:
|
||||
data = await r.json()
|
||||
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
|
||||
about = (
|
||||
@@ -280,7 +280,7 @@ class Core:
|
||||
guilds = sorted(list(self.bot.guilds),
|
||||
key=lambda s: s.name.lower())
|
||||
msg = ""
|
||||
for i, server in enumerate(guilds):
|
||||
for i, server in enumerate(guilds, 1):
|
||||
msg += "{}: {}\n".format(i, server.name)
|
||||
|
||||
msg += "\nTo leave a server, just type its number."
|
||||
@@ -313,6 +313,9 @@ class Core:
|
||||
try:
|
||||
msg = await self.bot.wait_for("message", check=conf_check, timeout=15)
|
||||
if msg.content.lower().strip() in ("yes", "y"):
|
||||
if server.owner == ctx.bot.user:
|
||||
await ctx.send("I cannot leave a guild I am the owner of.")
|
||||
return
|
||||
await server.leave()
|
||||
if server != ctx.guild:
|
||||
await ctx.send("Done.")
|
||||
@@ -875,7 +878,7 @@ class Core:
|
||||
if data_dir.exists():
|
||||
home = data_dir.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(data_dir.parent)
|
||||
os.chdir(str(data_dir.parent))
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(data_dir.stem)
|
||||
await ctx.send(_("A backup has been made of this instance. It is at {}.").format(
|
||||
|
||||
@@ -110,7 +110,7 @@ def init_events(bot, cli_flags):
|
||||
INFO.append('{} cogs with {} commands'.format(len(bot.cogs), len(bot.commands)))
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
data = await r.json()
|
||||
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
||||
INFO.append(
|
||||
|
||||
@@ -269,6 +269,13 @@ class Help(formatter.HelpFormatter):
|
||||
color=color)
|
||||
return embed
|
||||
|
||||
def cmd_has_no_subcommands(self, ctx, cmd, color=None):
|
||||
embed = self.simple_embed(
|
||||
ctx,
|
||||
title=ctx.bot.command_has_no_subcommands.format(cmd),
|
||||
color=color
|
||||
)
|
||||
return embed
|
||||
|
||||
@commands.command()
|
||||
async def help(ctx, *cmds: str):
|
||||
@@ -341,8 +348,7 @@ async def help(ctx, *cmds: str):
|
||||
embed=ctx.bot.formatter.simple_embed(
|
||||
ctx,
|
||||
title='Command "{0.name}" has no subcommands.'.format(command),
|
||||
color=ctx.bot.formatter.color,
|
||||
author=ctx.author.display_name))
|
||||
color=ctx.bot.formatter.color))
|
||||
else:
|
||||
await destination.send(
|
||||
ctx.bot.command_has_no_subcommands.format(command)
|
||||
|
||||
@@ -22,4 +22,6 @@ def safe_delete(pth: Path):
|
||||
os.chmod(root, 0o755)
|
||||
for d in dirs:
|
||||
os.chmod(os.path.join(root, d), 0o755)
|
||||
for f in files:
|
||||
os.chmod(os.path.join(root, f), 0o755)
|
||||
shutil.rmtree(str(pth), ignore_errors=True)
|
||||
|
||||
141
redbot/core/utils/menus.py
Normal file
141
redbot/core/utils/menus.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
Original source of reaction-based menu idea from
|
||||
https://github.com/Lunar-Dust/Dusty-Cogs/blob/master/menu/menu.py
|
||||
|
||||
Ported to Red V3 by Palm__ (https://github.com/palmtree5)
|
||||
"""
|
||||
import asyncio
|
||||
import discord
|
||||
|
||||
from redbot.core import RedContext
|
||||
|
||||
|
||||
async def menu(ctx: RedContext, 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: RedContext
|
||||
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: RedContext, 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: RedContext, 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: RedContext, pages: list,
|
||||
controls: dict, message: discord.Message, page: int,
|
||||
timeout: float, emoji: str):
|
||||
if message:
|
||||
await message.delete()
|
||||
return None
|
||||
|
||||
|
||||
DEFAULT_CONTROLS = {
|
||||
"⬅": prev_page,
|
||||
"❌": close_menu,
|
||||
"➡": next_page
|
||||
}
|
||||
@@ -4,6 +4,7 @@ from redbot.core.utils.chat_formatting import pagify
|
||||
import io
|
||||
import sys
|
||||
import weakref
|
||||
from typing import List
|
||||
|
||||
_instances = weakref.WeakValueDictionary({})
|
||||
|
||||
@@ -94,6 +95,88 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
def minutes_since(self):
|
||||
return int((self.last_interaction - datetime.utcnow()).seconds / 60)
|
||||
|
||||
@staticmethod
|
||||
async def message_forwarder(
|
||||
*, destination: discord.abc.Messageable,
|
||||
content: str=None, embed=None, files=[]) -> List[discord.Message]:
|
||||
"""
|
||||
This does the actual sending, use this instead of a full tunnel
|
||||
if you are using command initiated reactions instead of persistent
|
||||
event based ones
|
||||
|
||||
Parameters
|
||||
----------
|
||||
destination: `discord.abc.Messageable`
|
||||
Where to send
|
||||
content: `str`
|
||||
The message content
|
||||
embed: `discord.Embed`
|
||||
The embed to send
|
||||
files: `List[discord.Files]`
|
||||
A list of files to send.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of `discord.Message`
|
||||
The `discord.Message`(s) sent as a result
|
||||
|
||||
Raises
|
||||
------
|
||||
discord.Forbidden
|
||||
see `discord.abc.Messageable.send`
|
||||
discord.HTTPException
|
||||
see `discord.abc.Messageable.send`
|
||||
"""
|
||||
rets = []
|
||||
files = files if files else None
|
||||
if content:
|
||||
for page in pagify(content):
|
||||
rets.append(
|
||||
await destination.send(
|
||||
page, files=files, embed=embed)
|
||||
)
|
||||
if files:
|
||||
del files
|
||||
if embed:
|
||||
del embed
|
||||
elif embed or files:
|
||||
rets.append(
|
||||
await destination.send(files=files, embed=embed)
|
||||
)
|
||||
return rets
|
||||
|
||||
@staticmethod
|
||||
async def files_from_attatch(m: discord.Message) -> List[discord.File]:
|
||||
"""
|
||||
makes a list of file objects from a message
|
||||
returns an empty list if none, or if the sum of file sizes
|
||||
is too large for the bot to send
|
||||
|
||||
Parameters
|
||||
---------
|
||||
m: `discord.Message`
|
||||
A message to get attachments from
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of `discord.File`
|
||||
A list of `discord.File` objects
|
||||
|
||||
"""
|
||||
files = []
|
||||
size = 0
|
||||
max_size = 8 * 1024 * 1024
|
||||
for a in m.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
size += sys.getsizeof(_fp)
|
||||
if size > max_size:
|
||||
return []
|
||||
files.append(
|
||||
discord.File(_fp, filename=a.filename)
|
||||
)
|
||||
return files
|
||||
|
||||
async def communicate(self, *,
|
||||
message: discord.Message,
|
||||
topic: str=None,
|
||||
@@ -140,35 +223,22 @@ class Tunnel(metaclass=TunnelMeta):
|
||||
else:
|
||||
content = topic
|
||||
|
||||
attach = None
|
||||
if message.attachments:
|
||||
files = []
|
||||
size = 0
|
||||
max_size = 8 * 1024 * 1024
|
||||
for a in message.attachments:
|
||||
_fp = io.BytesIO()
|
||||
await a.save(_fp)
|
||||
size += sys.getsizeof(_fp)
|
||||
if size > max_size:
|
||||
await send_to.send(
|
||||
attach = await self.files_from_attatch(message)
|
||||
if not attach:
|
||||
await message.channel.send(
|
||||
"Could not forward attatchments. "
|
||||
"Total size of attachments in a single "
|
||||
"message must be less than 8MB."
|
||||
)
|
||||
break
|
||||
files.append(
|
||||
discord.File(_fp, filename=a.filename)
|
||||
)
|
||||
else:
|
||||
attach = files
|
||||
attach = []
|
||||
|
||||
rets = []
|
||||
for page in pagify(content):
|
||||
rets.append(
|
||||
await send_to.send(content, files=attach)
|
||||
rets = await self.message_forwarder(
|
||||
destination=send_to,
|
||||
content=content,
|
||||
files=attach
|
||||
)
|
||||
if attach:
|
||||
del attach
|
||||
|
||||
await message.add_reaction("\N{WHITE HEAVY CHECK MARK}")
|
||||
await message.add_reaction("\N{NEGATIVE SQUARED CROSS MARK}")
|
||||
|
||||
@@ -7,7 +7,10 @@ import argparse
|
||||
import asyncio
|
||||
|
||||
import pkg_resources
|
||||
from redbot.setup import basic_setup, load_existing_config, remove_instance
|
||||
from pathlib import Path
|
||||
from redbot.setup import basic_setup, load_existing_config, remove_instance, remove_instance_interaction, create_backup, save_config
|
||||
from redbot.core.utils import safe_delete
|
||||
from redbot.core.cli import confirm
|
||||
|
||||
if sys.platform == "linux":
|
||||
import distro
|
||||
@@ -60,7 +63,7 @@ def parse_cli_args():
|
||||
return parser.parse_known_args()
|
||||
|
||||
|
||||
def update_red(dev=False, voice=False, mongo=False, docs=False, test=False):
|
||||
def update_red(dev=False, reinstall=False, voice=False, mongo=False, docs=False, test=False):
|
||||
interpreter = sys.executable
|
||||
print("Updating Red...")
|
||||
# If the user ran redbot-launcher.exe, updating with pip will fail
|
||||
@@ -93,6 +96,15 @@ def update_red(dev=False, voice=False, mongo=False, docs=False, test=False):
|
||||
package = "Red-DiscordBot"
|
||||
if egg_l:
|
||||
package += "[{}]".format(", ".join(egg_l))
|
||||
if reinstall:
|
||||
code = subprocess.call([
|
||||
interpreter, "-m",
|
||||
"pip", "install", "-U", "-I",
|
||||
"--force-reinstall", "--no-cache-dir",
|
||||
"--process-dependency-links",
|
||||
package
|
||||
])
|
||||
else:
|
||||
code = subprocess.call([
|
||||
interpreter, "-m",
|
||||
"pip", "install", "-U",
|
||||
@@ -223,6 +235,37 @@ def instance_menu():
|
||||
return name_num_map[str(selection)]
|
||||
|
||||
|
||||
async def reset_red():
|
||||
instances = load_existing_config()
|
||||
|
||||
if not instances:
|
||||
print("No instance to delete.\n")
|
||||
return
|
||||
print("WARNING: You are about to remove ALL Red instances on this computer.")
|
||||
print("If you want to reset data of only one instance, "
|
||||
"please select option 5 in the launcher.")
|
||||
await asyncio.sleep(2)
|
||||
print("\nIf you continue you will remove these instanes.\n")
|
||||
for instance in list(instances.keys()):
|
||||
print(" - {}".format(instance))
|
||||
await asyncio.sleep(3)
|
||||
print('\nIf you want to reset all instances, type "I agree".')
|
||||
response = input("> ").strip()
|
||||
if response != "I agree":
|
||||
print("Cancelling...")
|
||||
return
|
||||
|
||||
if confirm("\nDo you want to create a backup for an instance? (y/n) "):
|
||||
for index, instance in instances.items():
|
||||
print("\nRemoving {}...".format(index))
|
||||
await create_backup(index, instance)
|
||||
await remove_instance(index, instance)
|
||||
else:
|
||||
for index, instance in instances.items():
|
||||
await remove_instance(index, instance)
|
||||
print("All instances have been removed.")
|
||||
|
||||
|
||||
def clear_screen():
|
||||
if IS_WINDOWS:
|
||||
os.system("cls")
|
||||
@@ -247,6 +290,33 @@ def extras_selector():
|
||||
return selected
|
||||
|
||||
|
||||
def development_choice(reinstall = False):
|
||||
while True:
|
||||
print("\n")
|
||||
print("Do you want to install stable or development version?")
|
||||
print("1. Stable version")
|
||||
print("2. Development version")
|
||||
choice = user_choice()
|
||||
print("\n")
|
||||
selected = extras_selector()
|
||||
if choice == "1":
|
||||
update_red(
|
||||
dev=False, reinstall=reinstall, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
break
|
||||
elif choice == "2":
|
||||
update_red(
|
||||
dev=True, reinstall=reinstall, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
def debug_info():
|
||||
pyver = sys.version
|
||||
redver = pkg_resources.get_distribution("Red-DiscordBot").version
|
||||
@@ -275,55 +345,64 @@ def debug_info():
|
||||
def main_menu():
|
||||
if IS_WINDOWS:
|
||||
os.system("TITLE Red - Discord Bot V3 Launcher")
|
||||
clear_screen()
|
||||
while True:
|
||||
print(INTRO)
|
||||
print("1. Run Red w/ autorestart in case of issues")
|
||||
print("2. Run Red")
|
||||
print("3. Update Red")
|
||||
print("4. Update Red (development version)")
|
||||
print("5. Create Instance")
|
||||
print("6. Remove Instance")
|
||||
print("7. Debug information (use this if having issues with the launcher or bot)")
|
||||
print("4. Create Instance")
|
||||
print("5. Remove Instance")
|
||||
print("6. Debug information (use this if having issues with the launcher or bot)")
|
||||
print("7. Reinstall Red")
|
||||
print("0. Exit")
|
||||
choice = user_choice()
|
||||
if choice == "1":
|
||||
instance = instance_menu()
|
||||
if instance:
|
||||
cli_flags = cli_flag_getter()
|
||||
if instance:
|
||||
run_red(instance, autorestart=True, cliflags=cli_flags)
|
||||
wait()
|
||||
elif choice == "2":
|
||||
instance = instance_menu()
|
||||
if instance:
|
||||
cli_flags = cli_flag_getter()
|
||||
if instance:
|
||||
run_red(instance, autorestart=False, cliflags=cli_flags)
|
||||
wait()
|
||||
elif choice == "3":
|
||||
selected = extras_selector()
|
||||
update_red(
|
||||
dev=False, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
development_choice()
|
||||
wait()
|
||||
elif choice == "4":
|
||||
selected = extras_selector()
|
||||
update_red(
|
||||
dev=True, voice=True if "voice" in selected else False,
|
||||
docs=True if "docs" in selected else False,
|
||||
test=True if "test" in selected else False,
|
||||
mongo=True if "mongo" in selected else False
|
||||
)
|
||||
wait()
|
||||
elif choice == "5":
|
||||
basic_setup()
|
||||
wait()
|
||||
elif choice == "6":
|
||||
asyncio.get_event_loop().run_until_complete(remove_instance())
|
||||
elif choice == "5":
|
||||
asyncio.get_event_loop().run_until_complete(remove_instance_interaction())
|
||||
wait()
|
||||
elif choice == "7":
|
||||
elif choice == "6":
|
||||
debug_info()
|
||||
elif choice == "7":
|
||||
while True:
|
||||
loop = asyncio.get_event_loop()
|
||||
clear_screen()
|
||||
print("==== Reinstall Red ====")
|
||||
print("1. Reinstall Red requirements (discard code changes, keep data and 3rd party cogs)")
|
||||
print("2. Reset all data")
|
||||
print("3. Factory reset (discard code changes, reset all data)")
|
||||
print("\n")
|
||||
print("0. Back")
|
||||
choice = user_choice()
|
||||
if choice == "1":
|
||||
development_choice(reinstall=True)
|
||||
wait()
|
||||
elif choice == "2":
|
||||
loop.run_until_complete(reset_red())
|
||||
wait()
|
||||
elif choice == "3":
|
||||
loop.run_until_complete(reset_red())
|
||||
development_choice(reinstall=True)
|
||||
wait()
|
||||
elif choice == "0":
|
||||
break
|
||||
elif choice == "0":
|
||||
break
|
||||
clear_screen()
|
||||
|
||||
126
redbot/setup.py
126
redbot/setup.py
@@ -302,7 +302,61 @@ async def edit_instance():
|
||||
)
|
||||
|
||||
|
||||
async def remove_instance():
|
||||
async def create_backup(selected, instance_data):
|
||||
if confirm("Would you like to make a backup of the data for this instance? (y/n)"):
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
print("Backing up the instance's data...")
|
||||
await mongo_to_json(instance_data["DATA_PATH"], instance_data["STORAGE_DETAILS"])
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent))
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem)
|
||||
print("A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
))
|
||||
|
||||
else:
|
||||
print("Backing up the instance's data...")
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent)) # str is used here because 3.5 support
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem) # add all files in that directory
|
||||
print(
|
||||
"A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def remove_instance(selected, instance_data):
|
||||
instance_list = load_existing_config()
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collections = await db.collection_names(include_system_collections=False)
|
||||
for name in collections:
|
||||
collection = await db.get_collection(name)
|
||||
await collection.drop()
|
||||
else:
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance {} has been removed\n".format(selected))
|
||||
|
||||
|
||||
async def remove_instance_interaction():
|
||||
instance_list = load_existing_config()
|
||||
if not instance_list:
|
||||
print("No instances have been set up!")
|
||||
@@ -322,78 +376,14 @@ async def remove_instance():
|
||||
return
|
||||
instance_data = instance_list[selected]
|
||||
|
||||
if confirm("Would you like to make a backup of the data for this instance? (y/n)"):
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
print("Backing up the instance's data...")
|
||||
await mongo_to_json(instance_data["DATA_PATH"], instance_data["STORAGE_DETAILS"])
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent))
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem)
|
||||
print("A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
))
|
||||
print("Removing the instance...")
|
||||
|
||||
m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collections = await db.collection_names(include_system_collections=False)
|
||||
for name in collections:
|
||||
collection = await db.get_collection(name)
|
||||
await collection.drop()
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed.")
|
||||
return
|
||||
else:
|
||||
print("Backing up the instance's data...")
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H-%M-%S")
|
||||
)
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
if pth.exists():
|
||||
home = pth.home()
|
||||
backup_file = home / backup_filename
|
||||
os.chdir(str(pth.parent)) # str is used here because 3.5 support
|
||||
with tarfile.open(str(backup_file), "w:gz") as tar:
|
||||
tar.add(pth.stem) # add all files in that directory
|
||||
print(
|
||||
"A backup of {} has been made. It is at {}".format(
|
||||
selected, backup_file
|
||||
)
|
||||
)
|
||||
print("Removing the instance...")
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed")
|
||||
return
|
||||
else:
|
||||
print("Removing the instance...")
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collections = await db.collection_names(include_system_collections=False)
|
||||
for name in collections:
|
||||
collection = await db.get_collection(name)
|
||||
await collection.drop()
|
||||
else:
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed")
|
||||
return
|
||||
await create_backup(selected, instance_data)
|
||||
await remove_instance(selected, instance_data)
|
||||
|
||||
|
||||
def main():
|
||||
if args.delete:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(remove_instance())
|
||||
loop.run_until_complete(remove_instance_interaction())
|
||||
elif args.edit:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(edit_instance())
|
||||
|
||||
Reference in New Issue
Block a user