mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-12-06 09:22:31 -05:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4b641e62a | ||
|
|
153d710eb4 | ||
|
|
83471e0866 | ||
|
|
01b9843883 | ||
|
|
749af89e9f | ||
|
|
27b61a2770 | ||
|
|
eb3b6346bb | ||
|
|
2e9a0de4a1 | ||
|
|
f83e3cc3e7 | ||
|
|
a8f4659552 | ||
|
|
25a5c3dec9 | ||
|
|
c49cb4a213 | ||
|
|
c4dbbc2d1e | ||
|
|
fe3d6f57af | ||
|
|
052af2f9bf | ||
|
|
16da9f52ac | ||
|
|
22a342d36d | ||
|
|
4fcf32b5e9 | ||
|
|
5bdb455bc0 | ||
|
|
1cb74f0ea7 | ||
|
|
c7e8c95640 | ||
|
|
ccb322d08e | ||
|
|
b27e0f2d21 | ||
|
|
6a715d87dd | ||
|
|
6138b78c07 | ||
|
|
f84ef48819 | ||
|
|
cda27944b6 | ||
|
|
c9281f734b | ||
|
|
f378ea0d2e | ||
|
|
40c37b5c06 | ||
|
|
57b7db6956 | ||
|
|
b4f5c2c0a1 | ||
|
|
f6903cf582 | ||
|
|
3816385228 | ||
|
|
f65085946c | ||
|
|
b10b746d9e | ||
|
|
ed5945e182 | ||
|
|
cf48a13fc7 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -25,7 +25,7 @@ redbot/core/utils/mod.py @palmtree5
|
||||
# Cogs
|
||||
redbot/cogs/admin/* @tekulvw
|
||||
redbot/cogs/alias/* @tekulvw
|
||||
redbot/cogs/audio/* @tekulvw
|
||||
redbot/cogs/audio/* @aikaterna @atiwiex
|
||||
redbot/cogs/bank/* @tekulvw
|
||||
redbot/cogs/cleanup/* @palmtree5
|
||||
redbot/cogs/customcom/* @palmtree5
|
||||
|
||||
@@ -8,11 +8,9 @@ python:
|
||||
- 3.5.3
|
||||
- 3.6.1
|
||||
install:
|
||||
- echo "pytest>3" >> requirements.txt
|
||||
- echo "pytest-asyncio" >> requirements.txt
|
||||
- echo "git+https://github.com/Rapptz/discord.py.git@rewrite#egg=discord.py[voice]" >> requirements.txt
|
||||
- pip install -r requirements.txt
|
||||
- pip install .
|
||||
- pip install .[test]
|
||||
script:
|
||||
- python -m compileall ./redbot/cogs
|
||||
- python -m pytest
|
||||
|
||||
@@ -2,3 +2,4 @@ include README.rst
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
include discord/bin/*.dll
|
||||
include redbot/cogs/audio/application.yml
|
||||
@@ -10,6 +10,10 @@
|
||||
:target: https://crowdin.com/project/red-discordbot
|
||||
:alt: Crowdin
|
||||
|
||||
.. image:: https://img.shields.io/badge/Support-Red!-orange.svg
|
||||
:target: https://www.patreon.com/Red_Devs
|
||||
:alt: Patreon
|
||||
|
||||
********************
|
||||
Red - Discord Bot v3
|
||||
********************
|
||||
|
||||
@@ -12,7 +12,7 @@ Installing pre-requirements
|
||||
|
||||
yum -y groupinstall development
|
||||
yum -y install https://centos7.iuscommunity.org/ius-release.rpm
|
||||
yum -y install yum-utils wget which python35u python35u-pip python35u-devel openssl-devel libffi-devel git opus-devel
|
||||
yum -y install yum-utils wget which python35u python35u-pip python35u-devel openssl-devel libffi-devel git opus-devel java-1.8.0-openjdk
|
||||
sh -c "$(wget https://gist.githubusercontent.com/mustafaturan/7053900/raw/27f4c8bad3ee2bb0027a1a52dc8501bf1e53b270/latest-ffmpeg-centos6.sh -O -)"
|
||||
|
||||
--------------
|
||||
|
||||
@@ -14,7 +14,7 @@ Installing pre-requirements
|
||||
|
||||
echo "deb http://httpredir.debian.org/debian stretch-backports main contrib non-free" >> /etc/apt/sources.list
|
||||
apt-get update
|
||||
apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git ffmpeg libopus-dev unzip -y
|
||||
apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git ffmpeg libopus-dev unzip default-jre -y
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
|
||||
@@ -17,6 +17,8 @@ Installing pre-requirements
|
||||
* :code:`brew install git`
|
||||
* :code:`brew install ffmpeg --with-ffplay`
|
||||
* :code:`brew install opus`
|
||||
* :code:`brew tap caskroom/versions`
|
||||
* :code:`brew cask install java8`
|
||||
|
||||
--------------
|
||||
Installing Red
|
||||
|
||||
@@ -10,7 +10,7 @@ Installing pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git libav-tools libopus-dev unzip -y
|
||||
sudo apt-get install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git libav-tools libopus-dev unzip default-jre -y
|
||||
|
||||
--------------
|
||||
Installing Red
|
||||
|
||||
@@ -12,7 +12,7 @@ Installing the pre-requirements
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
sudo apt install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git ffmpeg libopus-dev unzip -y
|
||||
sudo apt install python3.5-dev python3-pip build-essential libssl-dev libffi-dev git ffmpeg libopus-dev unzip default-jre -y
|
||||
|
||||
------------------
|
||||
Installing the bot
|
||||
|
||||
@@ -21,6 +21,10 @@ Needed Software
|
||||
|
||||
.. attention:: Please choose the option to "Run Git from the Windows Command Prompt" in Git's setup
|
||||
|
||||
* `Java <https://java.com/en/download/manual.jsp>`_ - needed for Audio
|
||||
|
||||
.. attention:: Please choose the "Windows Online" installer
|
||||
|
||||
--------------
|
||||
Installing Red
|
||||
--------------
|
||||
|
||||
@@ -134,7 +134,7 @@ class Admin:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def addrole(self, ctx: commands.Context, rolename: discord.Role,
|
||||
async def addrole(self, ctx: commands.Context, rolename: discord.Role, *,
|
||||
user: MemberDefaultAuthor=None):
|
||||
"""
|
||||
Adds a role to a user. If user is left blank it defaults to the
|
||||
@@ -151,7 +151,7 @@ class Admin:
|
||||
@commands.command()
|
||||
@commands.guild_only()
|
||||
@checks.admin_or_permissions(manage_roles=True)
|
||||
async def removerole(self, ctx: commands.Context, rolename: discord.Role,
|
||||
async def removerole(self, ctx: commands.Context, rolename: discord.Role, *,
|
||||
user: MemberDefaultAuthor=None):
|
||||
"""
|
||||
Removes a role from a user. If user is left blank it defaults to the
|
||||
@@ -227,7 +227,7 @@ class Admin:
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
@checks.is_owner()
|
||||
async def announce(self, ctx: commands.Context, message: str):
|
||||
async def announce(self, ctx: commands.Context, *, message: str):
|
||||
"""
|
||||
Announces a message to all servers the bot is in.
|
||||
"""
|
||||
@@ -259,13 +259,13 @@ class Admin:
|
||||
@announce.command(name="channel")
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def announce_channel(self, ctx, channel: discord.TextChannel=None):
|
||||
async def announce_channel(self, ctx, *, channel: discord.TextChannel=None):
|
||||
"""
|
||||
Changes the channel on which the bot makes announcements.
|
||||
"""
|
||||
if channel is None:
|
||||
channel = ctx.channel
|
||||
await self.conf.guild(ctx.guild).set("announce_channel", channel.id)
|
||||
await self.conf.guild(ctx.guild).announce_channel.set(channel.id)
|
||||
|
||||
await ctx.send("The announcement channel has been set to {}".format(
|
||||
channel.mention
|
||||
@@ -274,7 +274,7 @@ class Admin:
|
||||
@announce.command(name="ignore")
|
||||
@commands.guild_only()
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def announce_ignore(self, ctx, guild: discord.Guild=None):
|
||||
async def announce_ignore(self, ctx, *, guild: discord.Guild=None):
|
||||
"""
|
||||
Toggles whether the announcements will ignore the given server.
|
||||
Defaults to the current server if none is provided.
|
||||
@@ -310,7 +310,7 @@ class Admin:
|
||||
return valid_roles
|
||||
|
||||
@commands.group(invoke_without_command=True)
|
||||
async def selfrole(self, ctx: commands.Context, selfrole: SelfRole):
|
||||
async def selfrole(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Add a role to yourself that server admins have configured as
|
||||
user settable.
|
||||
@@ -319,7 +319,7 @@ class Admin:
|
||||
await self._addrole(ctx, ctx.author, selfrole)
|
||||
|
||||
@selfrole.command(name="remove")
|
||||
async def selfrole_remove(self, ctx: commands.Context, selfrole: SelfRole):
|
||||
async def selfrole_remove(self, ctx: commands.Context, *, selfrole: SelfRole):
|
||||
"""
|
||||
Removes a selfrole from yourself.
|
||||
"""
|
||||
@@ -328,7 +328,7 @@ class Admin:
|
||||
|
||||
@selfrole.command(name="add")
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def selfrole_add(self, ctx: commands.Context, role: discord.Role):
|
||||
async def selfrole_add(self, ctx: commands.Context, *, role: discord.Role):
|
||||
"""
|
||||
Add a role to the list of available selfroles.
|
||||
"""
|
||||
@@ -340,7 +340,7 @@ class Admin:
|
||||
|
||||
@selfrole.command(name="delete")
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def selfrole_delete(self, ctx: commands.Context, role: SelfRole):
|
||||
async def selfrole_delete(self, ctx: commands.Context, *, role: SelfRole):
|
||||
"""
|
||||
Removes a role from the list of available selfroles.
|
||||
"""
|
||||
|
||||
@@ -43,7 +43,10 @@ class Announcer:
|
||||
channel = guild.get_channel(channel_id)
|
||||
|
||||
if channel is None:
|
||||
channel = guild.default_channel
|
||||
channel = guild.system_channel
|
||||
|
||||
if channel is None:
|
||||
channel = guild.text_channels[0]
|
||||
|
||||
return channel
|
||||
|
||||
|
||||
@@ -1,5 +1,57 @@
|
||||
from pathlib import Path
|
||||
from aiohttp import ClientSession
|
||||
import shutil
|
||||
import asyncio
|
||||
|
||||
from .audio import Audio
|
||||
from .manager import start_lavalink_server
|
||||
from discord.ext import commands
|
||||
from redbot.core.data_manager import cog_data_path
|
||||
|
||||
LAVALINK_BUILD = 3112
|
||||
LAVALINK_BUILD_URL = (
|
||||
"https://ci.fredboat.com/repository/download/"
|
||||
"Lavalink_Build/{}:id/Lavalink.jar?guest=1"
|
||||
).format(LAVALINK_BUILD)
|
||||
|
||||
LAVALINK_DOWNLOAD_DIR = cog_data_path(raw_name="Audio")
|
||||
LAVALINK_JAR_FILE = LAVALINK_DOWNLOAD_DIR / "Lavalink.jar"
|
||||
|
||||
APP_YML_FILE = LAVALINK_DOWNLOAD_DIR / "application.yml"
|
||||
BUNDLED_APP_YML_FILE = Path(__file__).parent / "application.yml"
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(Audio(bot))
|
||||
async def download_lavalink(session):
|
||||
with LAVALINK_JAR_FILE.open(mode='wb') as f:
|
||||
async with session.get(LAVALINK_BUILD_URL) as resp:
|
||||
while True:
|
||||
chunk = await resp.content.read(512)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
async def maybe_download_lavalink(loop, cog):
|
||||
jar_exists = LAVALINK_JAR_FILE.exists()
|
||||
current_build = await cog.config.current_build()
|
||||
|
||||
if not jar_exists or current_build < LAVALINK_BUILD:
|
||||
LAVALINK_DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with ClientSession(loop=loop) as session:
|
||||
await download_lavalink(session)
|
||||
await cog.config.current_build.set(LAVALINK_BUILD)
|
||||
|
||||
shutil.copyfile(str(BUNDLED_APP_YML_FILE), str(APP_YML_FILE))
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
cog = Audio(bot)
|
||||
await maybe_download_lavalink(bot.loop, cog)
|
||||
await start_lavalink_server(bot.loop)
|
||||
|
||||
async def _finish():
|
||||
await asyncio.sleep(10)
|
||||
await cog.init_config()
|
||||
bot.add_cog(cog)
|
||||
|
||||
bot.loop.create_task(_finish())
|
||||
|
||||
20
redbot/cogs/audio/application.yml
Normal file
20
redbot/cogs/audio/application.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
server:
|
||||
port: 2333 # REST server
|
||||
lavalink:
|
||||
server:
|
||||
password: "youshallnotpass"
|
||||
ws:
|
||||
host: "localhost"
|
||||
port: 2332
|
||||
sources:
|
||||
youtube: true
|
||||
bandcamp: true
|
||||
soundcloud: true
|
||||
twitch: true
|
||||
vimeo: true
|
||||
mixer: true
|
||||
http: true
|
||||
local: false
|
||||
sentryDsn: ""
|
||||
bufferDurationMs: 400
|
||||
youtubePlaylistLoadLimit: 10000
|
||||
@@ -1,116 +1,845 @@
|
||||
from discord.ext import commands
|
||||
from discord import FFmpegPCMAudio, PCMVolumeTransformer
|
||||
import os
|
||||
import youtube_dl
|
||||
import asyncio
|
||||
import datetime
|
||||
import discord
|
||||
import heapq
|
||||
import lavalink
|
||||
import math
|
||||
from discord.ext import commands
|
||||
from redbot.core import Config, checks
|
||||
|
||||
from redbot.core.i18n import CogI18n
|
||||
from .manager import shutdown_lavalink_server
|
||||
|
||||
_ = CogI18n("Audio", __file__)
|
||||
|
||||
|
||||
# Just a little experimental audio cog not meant for final release
|
||||
__version__ = "0.0.3a"
|
||||
__author__ = ["aikaterna", "billy/bollo/ati"]
|
||||
|
||||
|
||||
class Audio:
|
||||
"""Audio commands"""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, 2711759128, force_registration=True)
|
||||
|
||||
default_global = {
|
||||
"host": 'localhost',
|
||||
"rest_port": '2333',
|
||||
"ws_port": '2332',
|
||||
"password": 'youshallnotpass',
|
||||
"status": False,
|
||||
"current_build": 0
|
||||
}
|
||||
|
||||
default_guild = {
|
||||
"notify": False,
|
||||
"repeat": False,
|
||||
"shuffle": False,
|
||||
"volume": 100
|
||||
}
|
||||
|
||||
self.config.register_guild(**default_guild)
|
||||
self.config.register_global(**default_global)
|
||||
|
||||
async def init_config(self):
|
||||
host = await self.config.host()
|
||||
password = await self.config.password()
|
||||
rest_port = await self.config.rest_port()
|
||||
ws_port = await self.config.ws_port()
|
||||
|
||||
await lavalink.initialize(
|
||||
bot=self.bot, host=host, password=password, rest_port=rest_port, ws_port=ws_port
|
||||
)
|
||||
lavalink.register_event_listener(self.event_handler)
|
||||
|
||||
async def event_handler(self, player, event_type, extra):
|
||||
notify = await self.config.guild(player.channel.guild).notify()
|
||||
status = await self.config.status()
|
||||
try:
|
||||
get_players = [p for p in lavalink.players if p.current is not None]
|
||||
get_single_title = get_players[0].current.title
|
||||
playing_servers = len(get_players)
|
||||
except IndexError:
|
||||
playing_servers = 0
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_START:
|
||||
playing_song = player.fetch('playing_song')
|
||||
requester = player.fetch('requester')
|
||||
player.store('prev_song', playing_song)
|
||||
player.store('prev_requester', requester)
|
||||
player.store('playing_song', player.current.uri)
|
||||
player.store('requester', player.current.requester)
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_START and notify:
|
||||
notify_channel = player.fetch('channel')
|
||||
if notify_channel:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
if player.fetch('notify_message') is not None:
|
||||
try:
|
||||
await player.fetch('notify_message').delete()
|
||||
except discord.errors.NotFound:
|
||||
pass
|
||||
embed = discord.Embed(colour=notify_channel.guild.me.top_role.colour, title='Now Playing',
|
||||
description='**[{}]({})**'.format(player.current.title, player.current.uri))
|
||||
notify_message = await notify_channel.send(embed=embed)
|
||||
player.store('notify_message', notify_message)
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.TRACK_START and status:
|
||||
if playing_servers == 0:
|
||||
await self.bot.change_presence(activity=None)
|
||||
if playing_servers == 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
|
||||
type=discord.ActivityType.listening))
|
||||
if playing_servers > 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.QUEUE_END and notify:
|
||||
notify_channel = player.fetch('channel')
|
||||
if notify_channel:
|
||||
notify_channel = self.bot.get_channel(notify_channel)
|
||||
embed = discord.Embed(colour=notify_channel.guild.me.top_role.colour, title='Queue ended.')
|
||||
await notify_channel.send(embed=embed)
|
||||
|
||||
if event_type == lavalink.LavalinkEvents.QUEUE_END and status:
|
||||
if playing_servers == 0:
|
||||
await self.bot.change_presence(activity=None)
|
||||
if playing_servers == 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name=get_single_title,
|
||||
type=discord.ActivityType.listening))
|
||||
if playing_servers > 1:
|
||||
await self.bot.change_presence(activity=discord.Activity(name='music in {} servers'.format(playing_servers),
|
||||
type=discord.ActivityType.playing))
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def audioset(self, ctx):
|
||||
"""Music configuration options."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@audioset.command()
|
||||
async def notify(self, ctx):
|
||||
"""Toggle song announcement and other bot messages."""
|
||||
notify = await self.config.guild(ctx.guild).notify()
|
||||
await self.config.guild(ctx.guild).notify.set(not notify)
|
||||
get_notify = await self.config.guild(ctx.guild).notify()
|
||||
await self._embed_msg(ctx, 'Verbose mode on: {}.'.format(get_notify))
|
||||
|
||||
@audioset.command()
|
||||
async def settings(self, ctx):
|
||||
"""Show the current settings."""
|
||||
notify = await self.config.guild(ctx.guild).notify()
|
||||
status = await self.config.status()
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
|
||||
msg = '```ini\n'
|
||||
msg += '----Guild Settings----\n'
|
||||
msg += 'audioset notify: [{}]\n'.format(notify)
|
||||
msg += 'audioset status: [{}]\n'.format(status)
|
||||
msg += 'Repeat: [{}]\n'.format(repeat)
|
||||
msg += 'Shuffle: [{}]\n'.format(shuffle)
|
||||
msg += '---Lavalink Settings---\n'
|
||||
msg += 'Cog version: {}\n```'.format(__version__)
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, description=msg)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
@audioset.command()
|
||||
@checks.is_owner()
|
||||
async def status(self, ctx):
|
||||
"""Enables/disables songs' titles as status."""
|
||||
status = await self.config.status()
|
||||
await self.config.status.set(not status)
|
||||
get_status = await self.config.status()
|
||||
await self._embed_msg(ctx, 'Song titles as status: {}.'.format(get_status))
|
||||
|
||||
@commands.command()
|
||||
async def local(self, ctx, *, filename: str):
|
||||
"""Play mp3"""
|
||||
if ctx.author.voice is None:
|
||||
await ctx.send(_("Join a voice channel first!"))
|
||||
return
|
||||
async def audiostats(self, ctx):
|
||||
"""Audio stats."""
|
||||
server_num = len([p for p in lavalink.players if p.current is not None])
|
||||
server_list = []
|
||||
|
||||
if ctx.voice_client:
|
||||
if ctx.voice_client.channel != ctx.author.voice.channel:
|
||||
await ctx.voice_client.disconnect()
|
||||
path = os.path.join("cogs", "audio", "songs", filename + ".mp3")
|
||||
if not os.path.isfile(path):
|
||||
await ctx.send(_("Let's play a file that exists pls"))
|
||||
return
|
||||
player = PCMVolumeTransformer(FFmpegPCMAudio(path), volume=1)
|
||||
voice = await ctx.author.voice.channel.connect()
|
||||
voice.play(player)
|
||||
await ctx.send(_("{} is playing a song...").format(ctx.author))
|
||||
|
||||
@commands.command()
|
||||
async def play(self, ctx, url: str):
|
||||
"""Play youtube url"""
|
||||
url = url.strip("<").strip(">")
|
||||
if ctx.author.voice is None:
|
||||
await ctx.send(_("Join a voice channel first!"))
|
||||
return
|
||||
elif "youtube.com" not in url.lower():
|
||||
await ctx.send(_("Youtube links pls"))
|
||||
return
|
||||
|
||||
if ctx.voice_client:
|
||||
if ctx.voice_client.channel != ctx.author.voice.channel:
|
||||
await ctx.voice_client.disconnect()
|
||||
yt = YoutubeSource(url)
|
||||
player = PCMVolumeTransformer(yt, volume=1)
|
||||
voice = await ctx.author.voice.channel.connect()
|
||||
voice.play(player)
|
||||
await ctx.send(_("{} is playing a song...").format(ctx.author))
|
||||
|
||||
@commands.command()
|
||||
async def stop(self, ctx):
|
||||
"""Stops the music and disconnects"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.source.cleanup()
|
||||
await ctx.voice_client.disconnect()
|
||||
for p in lavalink.players:
|
||||
connect_start = p.fetch('connect')
|
||||
connect_dur = self._dynamic_time(int((datetime.datetime.utcnow() - connect_start).total_seconds()))
|
||||
try:
|
||||
server_list.append('{} [`{}`]: **[{}]({})**'.format(p.channel.guild.name, connect_dur,
|
||||
p.current.title, p.current.uri))
|
||||
except AttributeError:
|
||||
server_list.append('{} [`{}`]: **{}**'.format(p.channel.guild.name, connect_dur,
|
||||
'Nothing playing.'))
|
||||
if server_num == 0:
|
||||
servers = 'Not connected anywhere.'
|
||||
else:
|
||||
await ctx.send(_("I'm not even connected to a voice channel!"), delete_after=2)
|
||||
await ctx.message.delete()
|
||||
servers = '\n'.join(server_list)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Connected in {} servers:'.format(server_num),
|
||||
description=servers)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
async def bump(self, ctx, index: int):
|
||||
"""Bump a song number to the top of the queue."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
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.')
|
||||
|
||||
bump_index = index - 1
|
||||
bump_song = player.queue[bump_index]
|
||||
player.queue.insert(0, bump_song)
|
||||
removed = player.queue.pop(index)
|
||||
await self._embed_msg(ctx, 'Moved **' + removed.title + '** to the top of the queue.')
|
||||
|
||||
@commands.command(aliases=['dc'])
|
||||
async def disconnect(self, ctx):
|
||||
"""Disconnect from the voice channel."""
|
||||
if self._player_check(ctx):
|
||||
await lavalink.get_player(ctx.guild.id).stop()
|
||||
await lavalink.get_player(ctx.guild.id).disconnect()
|
||||
|
||||
@commands.command(aliases=['np', 'n', 'song'])
|
||||
async def now(self, ctx):
|
||||
"""Now playing."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
expected = ['⏮', '⏹', '⏸', '⏭']
|
||||
emoji = {
|
||||
'prev': '⏮',
|
||||
'stop': '⏹',
|
||||
'pause': '⏸',
|
||||
'next': '⏭'
|
||||
}
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
song = 'Nothing'
|
||||
if player.current:
|
||||
arrow = await self._draw_time(ctx)
|
||||
pos = lavalink.utils.format_time(player.position)
|
||||
if player.current.is_stream:
|
||||
dur = 'LIVE'
|
||||
else:
|
||||
dur = lavalink.utils.format_time(player.current.length)
|
||||
song = '**[{}]({})**\nRequested by: **{}**\n\n{}`{}`/`{}`'.format(
|
||||
player.current.title, player.current.uri,
|
||||
player.current.requester, arrow, pos, dur
|
||||
)
|
||||
else:
|
||||
song = 'Nothing.'
|
||||
|
||||
if player.fetch('np_message') is not None:
|
||||
try:
|
||||
await player.fetch('np_message').delete()
|
||||
except discord.errors.NotFound:
|
||||
pass
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Now Playing', description=song)
|
||||
message = await ctx.send(embed=embed)
|
||||
player.store('np_message', message)
|
||||
|
||||
def check(r, u):
|
||||
return r.message.id == message.id and u == ctx.message.author
|
||||
|
||||
if player.current:
|
||||
for i in range(4):
|
||||
await message.add_reaction(expected[i])
|
||||
try:
|
||||
(r, u) = await self.bot.wait_for('reaction_add', check=check, timeout=10.0)
|
||||
except asyncio.TimeoutError:
|
||||
return await self._clear_react(message)
|
||||
|
||||
reacts = {v: k for k, v in emoji.items()}
|
||||
react = reacts[r.emoji]
|
||||
|
||||
if react == 'prev':
|
||||
await self._clear_react(message)
|
||||
await ctx.invoke(self.prev)
|
||||
elif react == 'stop':
|
||||
await self._clear_react(message)
|
||||
await ctx.invoke(self.stop)
|
||||
elif react == 'pause':
|
||||
await self._clear_react(message)
|
||||
await ctx.invoke(self.pause)
|
||||
elif react == 'next':
|
||||
await self._clear_react(message)
|
||||
await ctx.invoke(self.skip)
|
||||
|
||||
@commands.command(aliases=['resume'])
|
||||
async def pause(self, ctx):
|
||||
"""Pauses the music"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.pause()
|
||||
await ctx.send("👌", delete_after=2)
|
||||
else:
|
||||
await ctx.send(_("I'm not even connected to a voice channel!"), delete_after=2)
|
||||
await ctx.message.delete()
|
||||
"""Pause and resume."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if not ctx.author.voice or ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to pause the music.')
|
||||
|
||||
if player.current and not player.paused:
|
||||
await player.pause()
|
||||
embed = discord.Embed(
|
||||
colour=ctx.guild.me.top_role.colour, title='Track Paused',
|
||||
description='**[{}]({})**'.format(
|
||||
player.current.title,
|
||||
player.current.uri
|
||||
)
|
||||
)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
if player.paused:
|
||||
await player.pause(False)
|
||||
embed = discord.Embed(
|
||||
colour=ctx.guild.me.top_role.colour,
|
||||
title='Track Resumed',
|
||||
description='**[{}]({})**'.format(
|
||||
player.current.title,
|
||||
player.current.uri
|
||||
)
|
||||
)
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
await self._embed_msg(ctx, 'Nothing playing.')
|
||||
|
||||
@commands.command()
|
||||
async def resume(self, ctx):
|
||||
"""Resumes the music"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.resume()
|
||||
await ctx.send("👌", delete_after=2)
|
||||
else:
|
||||
await ctx.send(_("I'm not even connected to a voice channel!"), delete_after=2)
|
||||
await ctx.message.delete()
|
||||
async def percent(self, ctx):
|
||||
"""Queue percentage."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
queue_tracks = player.queue
|
||||
queue_len = len(queue_tracks)
|
||||
requesters = {'total': 0, 'users': {}}
|
||||
|
||||
@commands.command(hidden=True)
|
||||
async def volume(self, ctx, n: float):
|
||||
"""Sets the volume"""
|
||||
if ctx.voice_client:
|
||||
ctx.voice_client.source.volume = n
|
||||
await ctx.send(_("Volume set."), delete_after=2)
|
||||
async def _usercount(req_username):
|
||||
if req_username in requesters['users']:
|
||||
requesters['users'][req_username]['songcount'] += 1
|
||||
requesters['total'] += 1
|
||||
else:
|
||||
await ctx.send(_("I'm not even connected to a voice channel!"), delete_after=2)
|
||||
await ctx.message.delete()
|
||||
requesters['users'][req_username] = {}
|
||||
requesters['users'][req_username]['songcount'] = 1
|
||||
requesters['total'] += 1
|
||||
|
||||
for track in queue_tracks:
|
||||
req_username = '{}#{}'.format(track.requester.name, track.requester.discriminator)
|
||||
await _usercount(req_username)
|
||||
|
||||
try:
|
||||
req_username = '{}#{}'.format(player.current.requester.name, player.current.requester.discriminator)
|
||||
await _usercount(req_username)
|
||||
except AttributeError:
|
||||
return await self._embed_msg(ctx, 'Nothing in the queue.')
|
||||
|
||||
for req_username in requesters['users']:
|
||||
percentage = float(requesters['users'][req_username]['songcount']) / float(requesters['total'])
|
||||
requesters['users'][req_username]['percent'] = round(percentage * 100, 1)
|
||||
|
||||
top_queue_users = heapq.nlargest(20, [(x, requesters['users'][x][y]) for x in requesters['users'] for y in
|
||||
requesters['users'][x] if y == 'percent'], key=lambda x: x[1])
|
||||
queue_user = ["{}: {:g}%".format(x[0], x[1]) for x in top_queue_users]
|
||||
queue_user_list = '\n'.join(queue_user)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Queued and playing songs:',
|
||||
description=queue_user_list)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(aliases=['p'])
|
||||
async def play(self, ctx, *, query):
|
||||
"""Play a URL or search for a song."""
|
||||
if not ctx.author.voice:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to use the play command.')
|
||||
if not self._player_check(ctx):
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store('connect', datetime.datetime.utcnow())
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
player.store('channel', ctx.channel.id)
|
||||
player.store('guild', ctx.guild.id)
|
||||
await self._data_check(ctx)
|
||||
|
||||
query = query.strip('<>')
|
||||
if 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.')
|
||||
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
|
||||
if 'list' in query and 'ytsearch:' not in query:
|
||||
for track in tracks:
|
||||
player.add(ctx.author, track)
|
||||
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'.format(queue_total_duration))
|
||||
if not player.current:
|
||||
await player.play()
|
||||
else:
|
||||
single_track = tracks[0]
|
||||
player.add(ctx.author, single_track)
|
||||
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'.format(queue_total_duration))
|
||||
if not player.current:
|
||||
await player.play()
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
async def prev(self, ctx):
|
||||
"""Skips to the start of the previously played track."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
if not ctx.author.voice or ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to skip the music.')
|
||||
if shuffle:
|
||||
return await self._embed_msg(ctx, 'Turn shuffle off to use this command.')
|
||||
if player.fetch('prev_song') is None:
|
||||
return await self._embed_msg(ctx, 'No previous track.')
|
||||
else:
|
||||
last_track = await player.get_tracks(player.fetch('prev_song'))
|
||||
player.add(player.fetch('prev_requester').id, last_track[0])
|
||||
queue_len = len(player.queue)
|
||||
bump_song = player.queue[-1]
|
||||
player.queue.insert(0, bump_song)
|
||||
player.queue.pop(queue_len)
|
||||
await player.skip()
|
||||
embed = discord.Embed(
|
||||
colour=ctx.guild.me.top_role.colour,
|
||||
title='Replaying Track', description='**[{}]({})**'.format(
|
||||
player.current.title, player.current.uri
|
||||
)
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(aliases=['q'])
|
||||
async def queue(self, ctx, page: int=1):
|
||||
"""Lists the queue."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'There\'s nothing in the queue.')
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if not player.queue:
|
||||
return await self._embed_msg(ctx, 'There\'s nothing in the queue.')
|
||||
|
||||
items_per_page = 10
|
||||
pages = math.ceil(len(player.queue) / items_per_page)
|
||||
start = (page - 1) * items_per_page
|
||||
end = start + items_per_page
|
||||
|
||||
queue_list = ''
|
||||
arrow = await self._draw_time(ctx)
|
||||
pos = lavalink.utils.format_time(player.position)
|
||||
|
||||
if player.current.is_stream:
|
||||
dur = 'LIVE'
|
||||
else:
|
||||
dur = lavalink.utils.format_time(player.current.length)
|
||||
|
||||
if player.current.is_stream:
|
||||
queue_list += '**Currently livestreaming:** **[{}]({})**\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n'.format(
|
||||
player.current.title,
|
||||
player.current.uri,
|
||||
player.current.requester,
|
||||
arrow, pos, dur
|
||||
)
|
||||
else:
|
||||
queue_list += 'Playing: **[{}]({})**\nRequested by: **{}**\n\n{}`{}`/`{}`\n\n'.format(
|
||||
player.current.title,
|
||||
player.current.uri,
|
||||
player.current.requester,
|
||||
arrow, pos, dur
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Queue for ' + ctx.guild.name,
|
||||
description=queue_list)
|
||||
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
text = 'Page {}/{} | {} tracks, {} remaining'.format(page, pages, len(player.queue) + 1, queue_total_duration)
|
||||
if repeat:
|
||||
text += ' | Repeat: \N{WHITE HEAVY CHECK MARK}'
|
||||
if shuffle:
|
||||
text += ' | Shuffle: \N{WHITE HEAVY CHECK MARK}'
|
||||
embed.set_footer(text=text)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
async def repeat(self, ctx):
|
||||
"""Toggles repeat."""
|
||||
repeat_msg = 'You must be in the voice channel to toggle repeat.'
|
||||
if not ctx.author.voice:
|
||||
return await self._embed_msg(ctx, repeat_msg)
|
||||
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
await self.config.guild(ctx.guild).repeat.set(not repeat)
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
await self._embed_msg(ctx, 'Repeat songs: {}.'.format(repeat))
|
||||
|
||||
if self._player_check(ctx):
|
||||
await self._data_check(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, repeat_msg)
|
||||
|
||||
@commands.command()
|
||||
async def remove(self, ctx, index: int):
|
||||
"""Remove a specific song number from the queue."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if not player.queue:
|
||||
return await self._embed_msg(ctx, 'Nothing queued.')
|
||||
if index > len(player.queue) or index < 1:
|
||||
return await self._embed_msg(ctx, 'Song number must be greater than 1 and within the queue limit.')
|
||||
index -= 1
|
||||
removed = player.queue.pop(index)
|
||||
await self._embed_msg(ctx, 'Removed **' + removed.title + '** from the queue.')
|
||||
|
||||
@commands.command()
|
||||
async def search(self, ctx, *, query):
|
||||
"""Pick a song with a search.
|
||||
Use [p]search list <search term> to queue all songs.
|
||||
"""
|
||||
expected = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣"]
|
||||
emoji = {
|
||||
"one": "1⃣",
|
||||
"two": "2⃣",
|
||||
"three": "3⃣",
|
||||
"four": "4⃣",
|
||||
"five": "5⃣"
|
||||
}
|
||||
if not ctx.author.voice:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to enqueue songs.')
|
||||
if not self._player_check(ctx):
|
||||
await lavalink.connect(ctx.author.voice.channel)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
player.store('connect', datetime.datetime.utcnow())
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
player.store('channel', ctx.channel.id)
|
||||
player.store('guild', ctx.guild.id)
|
||||
if ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to enqueue songs.')
|
||||
|
||||
query = query.strip('<>')
|
||||
if query.startswith('sc '):
|
||||
query = 'scsearch:{}'.format(query.strip('sc '))
|
||||
elif not query.startswith('http') or query.startswith('sc '):
|
||||
query = 'ytsearch:{}'.format(query)
|
||||
|
||||
tracks = await player.get_tracks(query)
|
||||
if not tracks:
|
||||
return await self._embed_msg(ctx, 'Nothing found 👀')
|
||||
if 'list' not in query and 'ytsearch:' or 'scsearch:' in query:
|
||||
page = 1
|
||||
items_per_page = 5
|
||||
pages = math.ceil(len(tracks) / items_per_page)
|
||||
start = (page - 1) * items_per_page
|
||||
end = start + items_per_page
|
||||
search_list = ''
|
||||
for i, track in enumerate(tracks[start:end], start=start):
|
||||
next = i + 1
|
||||
search_list += '`{0}.` [**{1}**]({2})\n'.format(next, track.title,
|
||||
track.uri)
|
||||
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Tracks Found:', description=search_list)
|
||||
embed.set_footer(text='Page {}/{} | {} search results'.format(page, pages, len(tracks)))
|
||||
message = await ctx.send(embed=embed)
|
||||
|
||||
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,
|
||||
title='Queued {} track(s).'.format(len(tracks)))
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
if not shuffle and queue_duration > 0:
|
||||
songembed.set_footer(text='{} until start of search playback'.format(queue_total_duration))
|
||||
for track in tracks:
|
||||
player.add(ctx.author, track)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
message = await ctx.send(embed=songembed)
|
||||
|
||||
async def _search_button(self, ctx, message, tracks, entry: int):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
await self._clear_react(message)
|
||||
search_choice = tracks[entry]
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Track Enqueued',
|
||||
description='**[{}]({})**'.format(search_choice.title, search_choice.uri))
|
||||
queue_duration = await self._queue_duration(ctx)
|
||||
queue_total_duration = lavalink.utils.format_time(queue_duration)
|
||||
if not shuffle and queue_duration > 0:
|
||||
embed.set_footer(text='{} until track playback'.format(queue_total_duration))
|
||||
player.add(ctx.author, search_choice)
|
||||
if not player.current:
|
||||
await player.play()
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
@commands.command()
|
||||
async def seek(self, ctx, seconds: int=30):
|
||||
"""Seeks ahead or behind on a track by seconds."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if not ctx.author.voice or ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to use seek.')
|
||||
if player.current:
|
||||
if player.current.is_stream:
|
||||
return await self._embed_msg(ctx, 'Can\'t seek on a stream.')
|
||||
else:
|
||||
time_sec = seconds * 1000
|
||||
seek = player.position + time_sec
|
||||
if seek <= 0:
|
||||
await self._embed_msg(ctx, 'Moved {}s to 00:00:00'.format(seconds))
|
||||
else:
|
||||
await self._embed_msg(ctx, 'Moved {}s to {}'.format(seconds, lavalink.utils.format_time(seek)))
|
||||
return await player.seek(seek)
|
||||
else:
|
||||
await self._embed_msg(ctx, 'Nothing playing.')
|
||||
|
||||
@commands.command()
|
||||
async def shuffle(self, ctx):
|
||||
"""Toggles shuffle."""
|
||||
shuffle_msg = 'You must be in the voice channel to toggle shuffle.'
|
||||
if not ctx.author.voice:
|
||||
return await self._embed_msg(ctx, shuffle_msg)
|
||||
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
await self.config.guild(ctx.guild).shuffle.set(not shuffle)
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
await self._embed_msg(ctx, 'Shuffle songs: {}.'.format(shuffle))
|
||||
|
||||
if self._player_check(ctx):
|
||||
await self._data_check(ctx)
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, shuffle_msg)
|
||||
|
||||
@commands.command(aliases=['forceskip', 'fs'])
|
||||
async def skip(self, ctx):
|
||||
"""Skips to the next track."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
else:
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
|
||||
if not player.current:
|
||||
return await self._embed_msg(ctx, 'There\'s nothing in the queue.')
|
||||
|
||||
if not player.queue:
|
||||
pos, dur = player.position, player.current.length
|
||||
time_remain = lavalink.utils.format_time(dur - pos)
|
||||
if player.current.is_stream:
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='There\'s nothing in the queue.')
|
||||
embed.set_footer(text='Currently livestreaming {}'.format(player.current.title))
|
||||
else:
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='There\'s nothing in the queue.')
|
||||
embed.set_footer(text='{} left on {}'.format(time_remain, player.current.title))
|
||||
return await ctx.send(embed=embed)
|
||||
|
||||
if not ctx.author.voice or ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to skip the music.')
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=ctx.guild.me.top_role.colour, title='Track Skipped',
|
||||
description='**[{}]({})**'.format(
|
||||
player.current.title, player.current.uri
|
||||
)
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
await player.skip()
|
||||
|
||||
@commands.command(aliases=['s'])
|
||||
async def stop(self, ctx):
|
||||
"""Stops playback and clears the queue."""
|
||||
if not self._player_check(ctx):
|
||||
return await self._embed_msg(ctx, 'Nothing playing.')
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
if not ctx.author.voice or ctx.author.voice.channel != player.channel:
|
||||
return await self._embed_msg(ctx, 'You must be in the voice channel to stop the music.')
|
||||
if player.is_playing:
|
||||
await self._embed_msg(ctx, 'Stopping...')
|
||||
await player.stop()
|
||||
player.store('prev_requester', None)
|
||||
player.store('prev_song', None)
|
||||
player.store('playing_song', None)
|
||||
player.store('requester', None)
|
||||
|
||||
@commands.command()
|
||||
async def volume(self, ctx, vol: int=None):
|
||||
"""Sets the volume, 1% - 150%."""
|
||||
if not vol:
|
||||
vol = await self.config.guild(ctx.guild).volume()
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Current Volume:',
|
||||
description=str(vol) + '%')
|
||||
if not self._player_check(ctx):
|
||||
embed.set_footer(text='Nothing playing.')
|
||||
return await ctx.send(embed=embed)
|
||||
if vol > 150:
|
||||
vol = 150
|
||||
await self.config.guild(ctx.guild).volume.set(vol)
|
||||
if self._player_check:
|
||||
await lavalink.get_player(ctx.guild.id).set_volume(vol)
|
||||
else:
|
||||
await self.config.guild(ctx.guild).volume.set(vol)
|
||||
if self._player_check:
|
||||
await lavalink.get_player(ctx.guild.id).set_volume(vol)
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title='Volume:',
|
||||
description=str(vol) + '%')
|
||||
if not self._player_check(ctx):
|
||||
embed.set_footer(text='Nothing playing.')
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.group(aliases=['llset'])
|
||||
@checks.is_owner()
|
||||
async def llsetup(self, ctx):
|
||||
"""Lavalink server configuration options."""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@llsetup.command()
|
||||
async def host(self, ctx, host):
|
||||
"""Set the lavalink server host."""
|
||||
await self.config.host.set(host)
|
||||
get_host = await self.config.host()
|
||||
await self._embed_msg(ctx, 'Host set to {}.'.format(get_host))
|
||||
|
||||
@llsetup.command()
|
||||
async def password(self, ctx, passw):
|
||||
"""Set the lavalink server password."""
|
||||
await self.config.passw.set(str(passw))
|
||||
get_passw = await self.config.passw()
|
||||
await self._embed_msg(ctx, 'Server password set to {}.'.format(get_passw))
|
||||
|
||||
@llsetup.command()
|
||||
async def restport(self, ctx, rest_port):
|
||||
"""Set the lavalink REST server port."""
|
||||
await self.config.rest_port.set(str(rest_port))
|
||||
get_rest_port = await self.config.rest_port()
|
||||
await self._embed_msg(ctx, 'REST port set to {}.'.format(get_rest_port))
|
||||
|
||||
@llsetup.command()
|
||||
async def wsport(self, ctx, rest_port):
|
||||
"""Set the lavalink websocket server port."""
|
||||
await self.config.ws_port.set(str(ws_port))
|
||||
get_rest_port = await self.config.ws_port()
|
||||
await self._embed_msg(ctx, 'Websocket port set to {}.'.format(get_ws_port))
|
||||
|
||||
async def _clear_react(self, message):
|
||||
try:
|
||||
await message.clear_reactions()
|
||||
except:
|
||||
return
|
||||
|
||||
async def _data_check(self, ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
shuffle = await self.config.guild(ctx.guild).shuffle()
|
||||
repeat = await self.config.guild(ctx.guild).repeat()
|
||||
volume = await self.config.guild(ctx.guild).volume()
|
||||
if player.repeat != repeat:
|
||||
player.repeat = repeat
|
||||
if player.shuffle != shuffle:
|
||||
player.shuffle = shuffle
|
||||
if player.volume != volume:
|
||||
await player.set_volume(volume)
|
||||
|
||||
async def _draw_time(self, ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
paused = player.paused
|
||||
pos = player.position
|
||||
dur = player.current.length
|
||||
sections = 12
|
||||
loc_time = round((pos / dur) * sections)
|
||||
bar = '\N{BOX DRAWINGS HEAVY HORIZONTAL}'
|
||||
seek = '\N{RADIO BUTTON}'
|
||||
if paused:
|
||||
msg = '\N{DOUBLE VERTICAL BAR}'
|
||||
else:
|
||||
msg = '\N{BLACK RIGHT-POINTING TRIANGLE}'
|
||||
for i in range(sections):
|
||||
if i == loc_time:
|
||||
msg += seek
|
||||
else:
|
||||
msg += bar
|
||||
return msg
|
||||
|
||||
def _dynamic_time(self, time):
|
||||
m, s = divmod(time, 60)
|
||||
h, m = divmod(m, 60)
|
||||
d, h = divmod(h, 24)
|
||||
|
||||
if d > 0:
|
||||
msg = "{0}d {1}h"
|
||||
elif d == 0 and h > 0:
|
||||
msg = "{1}h {2}m"
|
||||
elif d == 0 and h == 0 and m > 0:
|
||||
msg = "{2}m {3}s"
|
||||
elif d == 0 and h == 0 and m == 0 and s > 0:
|
||||
msg = "{3}s"
|
||||
return msg.format(d, h, m, s)
|
||||
|
||||
async def _embed_msg(self, ctx, title):
|
||||
embed = discord.Embed(colour=ctx.guild.me.top_role.colour, title=title)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
async def _get_playing(self, ctx):
|
||||
if self._player_check(ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
return len([player for p in lavalink.players if p.is_playing])
|
||||
else:
|
||||
return 0
|
||||
|
||||
async def _queue_duration(self, ctx):
|
||||
player = lavalink.get_player(ctx.guild.id)
|
||||
duration = []
|
||||
for i in range(len(player.queue)):
|
||||
if not player.queue[i].is_stream:
|
||||
duration.append(player.queue[i].length)
|
||||
queue_duration = sum(duration)
|
||||
if player.queue == []:
|
||||
queue_duration = 0
|
||||
try:
|
||||
if not player.current.is_stream:
|
||||
remain = player.current.length - player.position
|
||||
else:
|
||||
remain = 0
|
||||
except AttributeError:
|
||||
remain = 0
|
||||
queue_total_duration = remain + queue_duration
|
||||
return queue_total_duration
|
||||
|
||||
def _player_check(self, ctx):
|
||||
try:
|
||||
lavalink.get_player(ctx.guild.id)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def __unload(self):
|
||||
for vc in self.bot.voice_clients:
|
||||
if vc.source:
|
||||
vc.source.cleanup()
|
||||
self.bot.loop.create_task(vc.disconnect())
|
||||
|
||||
|
||||
class YoutubeSource(discord.FFmpegPCMAudio):
|
||||
def __init__(self, url):
|
||||
opts = {
|
||||
'format': 'webm[abr>0]/bestaudio/best',
|
||||
'prefer_ffmpeg': True,
|
||||
'quiet': True
|
||||
}
|
||||
ytdl = youtube_dl.YoutubeDL(opts)
|
||||
self.info = ytdl.extract_info(url, download=False)
|
||||
super().__init__(self.info['url'])
|
||||
lavalink.unregister_event_listener(self.event_handler)
|
||||
self.bot.loop.create_task(lavalink.close())
|
||||
shutdown_lavalink_server()
|
||||
|
||||
41
redbot/cogs/audio/manager.py
Normal file
41
redbot/cogs/audio/manager.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import shlex
|
||||
import asyncio
|
||||
from subprocess import Popen, DEVNULL
|
||||
import os
|
||||
|
||||
proc = None
|
||||
SHUTDOWN = asyncio.Event()
|
||||
|
||||
|
||||
async def monitor_lavalink_server(loop):
|
||||
while not SHUTDOWN.is_set():
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
if not SHUTDOWN.is_set():
|
||||
print("Lavalink jar shutdown, restarting.")
|
||||
await start_lavalink_server(loop)
|
||||
|
||||
|
||||
async def start_lavalink_server(loop):
|
||||
from . import LAVALINK_DOWNLOAD_DIR, LAVALINK_JAR_FILE
|
||||
start_cmd = "java -jar {}".format(LAVALINK_JAR_FILE.resolve())
|
||||
|
||||
global proc
|
||||
proc = Popen(
|
||||
shlex.split(start_cmd, posix=os.name == 'posix'),
|
||||
cwd=str(LAVALINK_DOWNLOAD_DIR),
|
||||
stdout=DEVNULL, stderr=DEVNULL
|
||||
)
|
||||
|
||||
print("Lavalink jar started. PID: {}".format(proc.pid))
|
||||
|
||||
loop.create_task(monitor_lavalink_server(loop))
|
||||
|
||||
|
||||
def shutdown_lavalink_server():
|
||||
print("Shutting down lavalink server.")
|
||||
SHUTDOWN.set()
|
||||
if proc is not None:
|
||||
proc.terminate()
|
||||
@@ -1,3 +1,5 @@
|
||||
import discord
|
||||
|
||||
from redbot.core import checks, bank
|
||||
from redbot.core.i18n import CogI18n
|
||||
from discord.ext import commands
|
||||
@@ -17,6 +19,8 @@ def check_global_setting_guildowner():
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
return author == ctx.guild.owner or permissions.administrator
|
||||
|
||||
@@ -33,6 +37,8 @@ def check_global_setting_admin():
|
||||
if await ctx.bot.is_owner(author):
|
||||
return True
|
||||
if not await bank.is_global():
|
||||
if not isinstance(ctx.channel, discord.abc.GuildChannel):
|
||||
return False
|
||||
permissions = ctx.channel.permissions_for(author)
|
||||
is_guild_owner = author == ctx.guild.owner
|
||||
admin_role = await ctx.bot.db.guild(ctx.guild).admin_role()
|
||||
@@ -58,15 +64,22 @@ class Bank:
|
||||
|
||||
@bankset.command(name="toggleglobal")
|
||||
@checks.is_owner()
|
||||
async def bankset_toggleglobal(self, ctx: commands.Context):
|
||||
async def bankset_toggleglobal(self, ctx: commands.Context, confirm: bool=False):
|
||||
"""Toggles whether the bank is global or not
|
||||
If the bank is global, it will become per-guild
|
||||
If the bank is per-guild, it will become global"""
|
||||
cur_setting = await bank.is_global()
|
||||
await bank.set_global(not cur_setting)
|
||||
|
||||
word = _("per-guild") if cur_setting else _("global")
|
||||
|
||||
if confirm is False:
|
||||
await ctx.send(
|
||||
_("This will toggle the bank to be {}, deleting all accounts "
|
||||
"in the process! If you're sure, type `{}`").format(
|
||||
word, "{}bankset toggleglobal yes".format(ctx.prefix)
|
||||
)
|
||||
)
|
||||
else:
|
||||
await bank.set_global(not cur_setting)
|
||||
await ctx.send(_("The bank is now {}.").format(word))
|
||||
|
||||
@bankset.command(name="bankname")
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from redbot.core import checks
|
||||
from redbot.core import checks, RedContext
|
||||
from redbot.core.bot import Red
|
||||
from redbot.core.i18n import CogI18n
|
||||
from redbot.core.utils.mod import slow_deletion, mass_purge
|
||||
from redbot.cogs.mod.log import log
|
||||
from redbot.core.context import RedContext
|
||||
|
||||
_ = CogI18n("Cleanup", __file__)
|
||||
|
||||
@@ -20,6 +18,57 @@ class Cleanup:
|
||||
def __init__(self, bot: Red):
|
||||
self.bot = bot
|
||||
|
||||
@staticmethod
|
||||
async def check_100_plus(ctx: RedContext, number: int) -> bool:
|
||||
"""
|
||||
Called when trying to delete more than 100 messages at once
|
||||
|
||||
Prompts the user to choose whether they want to continue or not
|
||||
"""
|
||||
def author_check(message):
|
||||
return message.author == ctx.author
|
||||
|
||||
await ctx.send(_('Are you sure you want to delete {} messages? (y/n)').format(number))
|
||||
response = await ctx.bot.wait_for('message', check=author_check)
|
||||
|
||||
if response.content.lower().startswith('y'):
|
||||
return True
|
||||
else:
|
||||
await ctx.send(_('Cancelled.'))
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def get_messages_for_deletion(
|
||||
ctx: RedContext, channel: discord.TextChannel, number,
|
||||
check=lambda x: True, limit=100, before=None, after=None
|
||||
) -> list:
|
||||
"""
|
||||
Gets a list of messages meeting the requirements to be deleted.
|
||||
|
||||
Generally, the requirements are:
|
||||
- We don't have the number of messages to be deleted already
|
||||
- The message passes a provided check (if no check is provided,
|
||||
this is automatically true)
|
||||
- The message is less than 14 days old
|
||||
"""
|
||||
to_delete = []
|
||||
too_old = False
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
async for message in channel.history(limit=limit,
|
||||
before=before,
|
||||
after=after):
|
||||
if (not number or len(to_delete) - 1 < number) and check(message) \
|
||||
and (ctx.message.created_at - message.created_at).days < 14:
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
too_old = True
|
||||
break
|
||||
elif number and len(to_delete) >= number:
|
||||
break
|
||||
before = message
|
||||
return to_delete
|
||||
|
||||
@commands.group()
|
||||
@checks.mod_or_permissions(manage_messages=True)
|
||||
async def cleanup(self, ctx: RedContext):
|
||||
@@ -30,7 +79,7 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def text(self, ctx: commands.Context, text: str, number: int):
|
||||
async def text(self, ctx: RedContext, text: str, number: int):
|
||||
"""Deletes last X messages matching the specified text.
|
||||
|
||||
Example:
|
||||
@@ -42,18 +91,10 @@ class Cleanup:
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
def author_check(message):
|
||||
return message.author == author
|
||||
|
||||
if number > 100:
|
||||
await ctx.send('Are you sure you want to delete {} messages? (y/n)'.format(number))
|
||||
response = await self.bot.wait_for('message', check=author_check)
|
||||
|
||||
if response.content.startswith('y'):
|
||||
tmp = await ctx.send('Continuing..')
|
||||
await tmp.delete()
|
||||
else:
|
||||
return await ctx.send('Cancelled.')
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
def check(m):
|
||||
if text in m.content:
|
||||
@@ -63,25 +104,11 @@ class Cleanup:
|
||||
else:
|
||||
return False
|
||||
|
||||
to_delete = [ctx.message]
|
||||
too_old = False
|
||||
tmp = ctx.message
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
async for message in channel.history(limit=1000,
|
||||
before=tmp):
|
||||
if len(to_delete) - 1 < number and check(message) and\
|
||||
(ctx.message.created_at - message.created_at).days < 14:
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
too_old = True
|
||||
break
|
||||
elif len(to_delete) >= number:
|
||||
break
|
||||
tmp = message
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message)
|
||||
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
" containing '{}' in channel {}".format(author.name,
|
||||
" containing '{}' in channel {}.".format(author.name,
|
||||
author.id, len(to_delete), text, channel.id)
|
||||
log.info(reason)
|
||||
|
||||
@@ -93,7 +120,7 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def user(self, ctx: commands.Context, user: discord.Member or int, number: int):
|
||||
async def user(self, ctx: RedContext, user: discord.Member or int, number: int):
|
||||
"""Deletes last X messages from specified user.
|
||||
|
||||
Examples:
|
||||
@@ -104,18 +131,10 @@ class Cleanup:
|
||||
author = ctx.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
def author_check(message):
|
||||
return message.author == author
|
||||
|
||||
if number > 100:
|
||||
await ctx.send('Are you sure you want to delete {} messages? (y/n)'.format(number))
|
||||
response = await self.bot.wait_for('message', check=author_check)
|
||||
|
||||
if response.content.startswith('y'):
|
||||
tmp = await ctx.send('Continuing..')
|
||||
await tmp.delete()
|
||||
else:
|
||||
return await ctx.send('Cancelled.')
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
def check(m):
|
||||
if isinstance(user, discord.Member) and m.author == user:
|
||||
@@ -127,24 +146,11 @@ class Cleanup:
|
||||
else:
|
||||
return False
|
||||
|
||||
to_delete = []
|
||||
too_old = False
|
||||
tmp = ctx.message
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
async for message in channel.history(limit=1000,
|
||||
before=tmp):
|
||||
if len(to_delete) - 1 < number and check(message) and\
|
||||
(ctx.message.created_at - message.created_at).days < 14:
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
too_old = True
|
||||
break
|
||||
elif len(to_delete) >= number:
|
||||
break
|
||||
tmp = message
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
)
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
" made by {}({}) in channel {}"\
|
||||
" made by {}({}) in channel {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
user.name, user.id, channel.name)
|
||||
log.info(reason)
|
||||
@@ -158,8 +164,8 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def after(self, ctx: commands.Context, message_id: int):
|
||||
"""Deletes all messages after specified message
|
||||
async def after(self, ctx: RedContext, message_id: int):
|
||||
"""Deletes all messages after specified message.
|
||||
|
||||
To get a message id, enable developer mode in Discord's
|
||||
settings, 'appearance' tab. Then right click a message
|
||||
@@ -183,15 +189,11 @@ class Cleanup:
|
||||
await ctx.send(_("Message not found."))
|
||||
return
|
||||
|
||||
to_delete = []
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, 0, limit=None, after=after
|
||||
)
|
||||
|
||||
async for message in channel.history(after=after):
|
||||
if (ctx.message.created_at - message.created_at).days < 14:
|
||||
# Only add messages that are less than
|
||||
# 14 days old to the deletion queue
|
||||
to_delete.append(message)
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}"\
|
||||
reason = "{}({}) deleted {} messages in channel {}."\
|
||||
"".format(author.name, author.id,
|
||||
len(to_delete), channel.name)
|
||||
log.info(reason)
|
||||
@@ -201,7 +203,7 @@ class Cleanup:
|
||||
@cleanup.command()
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def messages(self, ctx: commands.Context, number: int):
|
||||
async def messages(self, ctx: RedContext, number: int):
|
||||
"""Deletes last X messages.
|
||||
|
||||
Example:
|
||||
@@ -212,36 +214,16 @@ class Cleanup:
|
||||
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
def author_check(message):
|
||||
return message.author == author
|
||||
|
||||
if number > 100:
|
||||
await ctx.send('Are you sure you want to delete {} messages? (y/n)'.format(number))
|
||||
response = await self.bot.wait_for('message', check=author_check)
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
if response.content.startswith('y'):
|
||||
tmp = await ctx.send('Continuing..')
|
||||
await tmp.delete()
|
||||
else:
|
||||
return await ctx.send('Cancelled.')
|
||||
else:
|
||||
tmp = ctx.message
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, limit=1000, before=ctx.message
|
||||
)
|
||||
|
||||
to_delete = []
|
||||
|
||||
done = False
|
||||
|
||||
while len(to_delete) - 1 < number and not done:
|
||||
async for message in channel.history(limit=1000, before=tmp):
|
||||
if len(to_delete) - 1 < number and \
|
||||
(ctx.message.created_at - message.created_at).days < 14:
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
done = True
|
||||
break
|
||||
tmp = message
|
||||
|
||||
reason = "{}({}) deleted {} messages in channel {}"\
|
||||
reason = "{}({}) deleted {} messages in channel {}."\
|
||||
"".format(author.name, author.id,
|
||||
number, channel.name)
|
||||
log.info(reason)
|
||||
@@ -254,25 +236,17 @@ class Cleanup:
|
||||
@cleanup.command(name='bot')
|
||||
@commands.guild_only()
|
||||
@commands.bot_has_permissions(manage_messages=True)
|
||||
async def cleanup_bot(self, ctx: commands.Context, number: int):
|
||||
"""Cleans up command messages and messages from the bot"""
|
||||
async def cleanup_bot(self, ctx: RedContext, number: int):
|
||||
"""Cleans up command messages and messages from the bot."""
|
||||
|
||||
channel = ctx.message.channel
|
||||
author = ctx.message.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
def author_check(message):
|
||||
return message.author == author
|
||||
|
||||
if number > 100:
|
||||
await ctx.send('Are you sure you want to delete {} messages? (y/n)'.format(number))
|
||||
response = await self.bot.wait_for('message', check=author_check)
|
||||
|
||||
if response.content.startswith('y'):
|
||||
tmp = await ctx.send('Continuing..')
|
||||
await tmp.delete()
|
||||
else:
|
||||
return await ctx.send('Cancelled.')
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
prefixes = await self.bot.get_prefix(ctx.message) # This returns all server prefixes
|
||||
if isinstance(prefixes, str):
|
||||
@@ -293,24 +267,12 @@ class Cleanup:
|
||||
return bool(self.bot.get_command(cmd_name))
|
||||
return False
|
||||
|
||||
to_delete = [ctx.message]
|
||||
too_old = False
|
||||
tmp = ctx.message
|
||||
|
||||
while not too_old and len(to_delete) - 1 < number:
|
||||
async for message in channel.history(limit=1000, before=tmp):
|
||||
if len(to_delete) - 1 < number and check(message) and\
|
||||
(ctx.message.created_at - message.created_at).days < 14:
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
too_old = True
|
||||
break
|
||||
elif len(to_delete) >= number:
|
||||
break
|
||||
tmp = message
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
)
|
||||
|
||||
reason = "{}({}) deleted {} "\
|
||||
" command messages in channel {}"\
|
||||
" command messages in channel {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
channel.name)
|
||||
log.info(reason)
|
||||
@@ -321,7 +283,7 @@ class Cleanup:
|
||||
await slow_deletion(to_delete)
|
||||
|
||||
@cleanup.command(name='self')
|
||||
async def cleanup_self(self, ctx: commands.Context, number: int, match_pattern: str = None):
|
||||
async def cleanup_self(self, ctx: RedContext, number: int, match_pattern: str = None):
|
||||
"""Cleans up messages owned by the bot.
|
||||
|
||||
By default, all messages are cleaned. If a third argument is specified,
|
||||
@@ -336,18 +298,10 @@ class Cleanup:
|
||||
author = ctx.message.author
|
||||
is_bot = self.bot.user.bot
|
||||
|
||||
def author_check(message):
|
||||
return message.author == author
|
||||
|
||||
if number > 100:
|
||||
await ctx.send('Are you sure you want to delete {} messages? (y/n)'.format(number))
|
||||
response = await self.bot.wait_for('message', check=author_check)
|
||||
|
||||
if response.content.startswith('y'):
|
||||
tmp = await ctx.send('Continuing..')
|
||||
await tmp.delete()
|
||||
else:
|
||||
return await ctx.send('Cancelled.')
|
||||
cont = await self.check_100_plus(ctx, number)
|
||||
if not cont:
|
||||
return
|
||||
|
||||
# You can always delete your own messages, this is needed to purge
|
||||
can_mass_purge = False
|
||||
@@ -378,25 +332,13 @@ class Cleanup:
|
||||
return True
|
||||
return False
|
||||
|
||||
to_delete = []
|
||||
to_delete = await self.get_messages_for_deletion(
|
||||
ctx, channel, number, check=check, limit=1000, before=ctx.message
|
||||
)
|
||||
|
||||
# Selfbot convenience, delete trigger message
|
||||
if author == self.bot.user:
|
||||
to_delete.append(ctx.message)
|
||||
number += 1
|
||||
too_old = False
|
||||
tmp = ctx.message
|
||||
while not too_old and len(to_delete) < number:
|
||||
async for message in channel.history(limit=1000, before=tmp):
|
||||
if len(to_delete) < number and check(message) and\
|
||||
(ctx.message.created_at - message.created_at).days < 14:
|
||||
to_delete.append(message)
|
||||
elif (ctx.message.created_at - message.created_at).days >= 14:
|
||||
# Found a message that is 14 or more days old, stop here
|
||||
too_old = True
|
||||
break
|
||||
elif len(to_delete) >= number:
|
||||
break
|
||||
tmp = message
|
||||
|
||||
if channel.name:
|
||||
channel_name = 'channel ' + channel.name
|
||||
@@ -404,7 +346,7 @@ class Cleanup:
|
||||
channel_name = str(channel)
|
||||
|
||||
reason = "{}({}) deleted {} messages "\
|
||||
"sent by the bot in {}"\
|
||||
"sent by the bot in {}."\
|
||||
"".format(author.name, author.id, len(to_delete),
|
||||
channel_name)
|
||||
log.info(reason)
|
||||
|
||||
@@ -43,7 +43,7 @@ class Downloader:
|
||||
self.LIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||
self.SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||
if not self.SHAREDLIB_INIT.exists():
|
||||
with self.SHAREDLIB_INIT.open(mode='w') as _:
|
||||
with self.SHAREDLIB_INIT.open(mode='w', encoding='utf-8') as _:
|
||||
pass
|
||||
|
||||
if str(self.LIB_PATH) not in syspath:
|
||||
@@ -192,7 +192,7 @@ class Downloader:
|
||||
Installs a group of dependencies using pip.
|
||||
"""
|
||||
repo = Repo("", "", "", Path.cwd(), loop=ctx.bot.loop)
|
||||
success = await repo.install_raw_requirements(deps, self.SHAREDLIB_PATH)
|
||||
success = await repo.install_raw_requirements(deps, self.LIB_PATH)
|
||||
|
||||
if success:
|
||||
await ctx.send(_("Libraries installed."))
|
||||
@@ -248,6 +248,7 @@ class Downloader:
|
||||
Lists all installed repos.
|
||||
"""
|
||||
repos = self._repo_manager.get_all_repo_names()
|
||||
repos = sorted(repos, key=str.lower)
|
||||
joined = _("Installed Repos:\n") + "\n".join(["+ " + r for r in repos])
|
||||
|
||||
for page in pagify(joined, ["\n"], shorten_by=16):
|
||||
|
||||
@@ -13,6 +13,7 @@ from discord.ext import commands
|
||||
|
||||
from redbot.core import Config
|
||||
from redbot.core import data_manager
|
||||
from redbot.core.utils import safe_delete
|
||||
from .errors import *
|
||||
from .installable import Installable, InstallableType
|
||||
from .json_mixins import RepoJSONMixin
|
||||
@@ -614,7 +615,7 @@ class RepoManager:
|
||||
if repo is None:
|
||||
raise MissingGitRepo("There is no repo with the name {}".format(name))
|
||||
|
||||
shutil.rmtree(str(repo.folder_path))
|
||||
safe_delete(repo.folder_path)
|
||||
|
||||
try:
|
||||
del self._repos[name]
|
||||
|
||||
@@ -216,16 +216,7 @@ class Economy:
|
||||
)
|
||||
)
|
||||
else:
|
||||
if await bank.is_global():
|
||||
# Bank being global means that the check would cause only
|
||||
# the owner and any co-owners to be able to run the command
|
||||
# so if we're in the function, it's safe to assume that the
|
||||
# author is authorized to use owner-only commands
|
||||
user = ctx.author
|
||||
else:
|
||||
user = ctx.guild.owner
|
||||
success = await bank.wipe_bank()
|
||||
if success:
|
||||
await bank.wipe_bank()
|
||||
await ctx.send(_("All bank accounts of this guild have been "
|
||||
"deleted."))
|
||||
|
||||
@@ -244,14 +235,17 @@ class Economy:
|
||||
await bank.deposit_credits(author, await self.config.PAYDAY_CREDITS())
|
||||
next_payday = cur_time + await self.config.PAYDAY_TIME()
|
||||
await self.config.user(author).next_payday.set(next_payday)
|
||||
await ctx.send(
|
||||
_("{} Here, take some {}. Enjoy! (+{}"
|
||||
" {}!)").format(
|
||||
author.mention, credits_name,
|
||||
str(await self.config.PAYDAY_CREDITS()),
|
||||
credits_name
|
||||
)
|
||||
)
|
||||
|
||||
pos = await bank.get_leaderboard_position(author)
|
||||
await ctx.send(_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2}\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
).format(
|
||||
author, credits_name, str(await self.config.PAYDAY_CREDITS()),
|
||||
str(await bank.get_balance(author)), pos
|
||||
))
|
||||
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
await ctx.send(
|
||||
@@ -264,12 +258,15 @@ class Economy:
|
||||
await bank.deposit_credits(author, await self.config.guild(guild).PAYDAY_CREDITS())
|
||||
next_payday = cur_time + await self.config.guild(guild).PAYDAY_TIME()
|
||||
await self.config.member(author).next_payday.set(next_payday)
|
||||
await ctx.send(
|
||||
_("{} Here, take some {}. Enjoy! (+{}"
|
||||
" {}!)").format(
|
||||
author.mention, credits_name,
|
||||
str(await self.config.guild(guild).PAYDAY_CREDITS()),
|
||||
credits_name))
|
||||
pos = await bank.get_leaderboard_position(author)
|
||||
await ctx.send(_(
|
||||
"{0.mention} Here, take some {1}. Enjoy! (+{2})\n\n"
|
||||
"You currently have {3} {1}.\n\n"
|
||||
"You are currently #{4} on the leaderboard!"
|
||||
).format(
|
||||
author, credits_name, str(await self.config.PAYDAY_CREDITS()),
|
||||
str(await bank.get_balance(author)), pos
|
||||
))
|
||||
else:
|
||||
dtime = self.display_time(next_payday - cur_time)
|
||||
await ctx.send(
|
||||
@@ -278,7 +275,7 @@ class Economy:
|
||||
|
||||
@commands.command()
|
||||
@guild_only_check()
|
||||
async def leaderboard(self, ctx: commands.Context, top: int = 10):
|
||||
async def leaderboard(self, ctx: commands.Context, top: int = 10, show_global: bool=False):
|
||||
"""Prints out the leaderboard
|
||||
|
||||
Defaults to top 10"""
|
||||
@@ -286,26 +283,23 @@ class Economy:
|
||||
guild = ctx.guild
|
||||
if top < 1:
|
||||
top = 10
|
||||
if await bank.is_global():
|
||||
bank_sorted = sorted(await bank.get_global_accounts(),
|
||||
key=lambda x: x.balance, reverse=True)
|
||||
else:
|
||||
bank_sorted = sorted(await bank.get_guild_accounts(guild),
|
||||
key=lambda x: x.balance, reverse=True)
|
||||
if await bank.is_global() and show_global: # show_global is only applicable if bank is global
|
||||
guild = None
|
||||
bank_sorted = await bank.get_leaderboard(positions=top, guild=guild)
|
||||
if len(bank_sorted) < top:
|
||||
top = len(bank_sorted)
|
||||
topten = bank_sorted[:top]
|
||||
highscore = ""
|
||||
place = 1
|
||||
for acc in topten:
|
||||
dname = str(acc.name)
|
||||
if len(dname) >= 23 - len(str(acc.balance)):
|
||||
dname = dname[:(23 - len(str(acc.balance))) - 3]
|
||||
dname += "... "
|
||||
highscore += str(place).ljust(len(str(top)) + 1)
|
||||
highscore += dname.ljust(23 - len(str(acc.balance)))
|
||||
highscore += str(acc.balance) + "\n"
|
||||
place += 1
|
||||
for pos, acc in enumerate(bank_sorted, 1):
|
||||
pos = pos
|
||||
poswidth = 2
|
||||
name = acc[1]["name"]
|
||||
namewidth = 35
|
||||
balance = acc[1]["balance"]
|
||||
balwidth = 2
|
||||
highscore += "{pos: <{poswidth}} {name: <{namewidth}s} {balance: >{balwidth}}\n".format(
|
||||
pos=pos, poswidth=poswidth, name=name, namewidth=namewidth,
|
||||
balance=balance, balwidth=balwidth
|
||||
)
|
||||
if highscore != "":
|
||||
for page in pagify(highscore, shorten_by=12):
|
||||
await ctx.send(box(page, lang="py"))
|
||||
|
||||
@@ -216,24 +216,24 @@ class General:
|
||||
created_on = _("{}\n({} days ago)").format(user_created, since_created)
|
||||
joined_on = _("{}\n({} days ago)").format(user_joined, since_joined)
|
||||
|
||||
game = _("Chilling in {} status").format(user.status)
|
||||
if user.game is None: # Default status
|
||||
activity = _("Chilling in {} status").format(user.status)
|
||||
if user.activity is None: # Default status
|
||||
pass
|
||||
elif user.game.type == 0: # "Playing" status
|
||||
game = _("Playing {}").format(user.game.name)
|
||||
elif user.game.type == 1: # "Streaming" status
|
||||
game = _("Streaming [{}]({})").format(user.game.name, user.game.url)
|
||||
elif user.game.type == 2: # "Listening" status
|
||||
game = _("Listening to {}").format(user.game.name)
|
||||
elif user.game.type == 3: # "Watching" status
|
||||
game = _("Watching {}").format(user.game.name)
|
||||
elif user.activity.type == discord.ActivityType.playing:
|
||||
activity = _("Playing {}").format(user.activity.name)
|
||||
elif user.activity.type == discord.ActivityType.streaming:
|
||||
activity = _("Streaming [{}]({})").format(user.activity.name, user.activity.url)
|
||||
elif user.activity.type == discord.ActivityType.listening:
|
||||
activity = _("Listening to {}").format(user.activity.name)
|
||||
elif user.activity.type == discord.ActivityType.watching:
|
||||
activity = _("Watching {}").format(user.activity.name)
|
||||
|
||||
if roles:
|
||||
roles = ", ".join([x.name for x in roles])
|
||||
else:
|
||||
roles = _("None")
|
||||
|
||||
data = discord.Embed(description=game, colour=user.colour)
|
||||
data = discord.Embed(description=activity, colour=user.colour)
|
||||
data.add_field(name=_("Joined Discord on"), value=created_on)
|
||||
data.add_field(name=_("Joined this guild on"), value=joined_on)
|
||||
data.add_field(name=_("Roles"), value=roles, inline=False)
|
||||
|
||||
@@ -359,6 +359,10 @@ class Mod:
|
||||
if days.isdigit():
|
||||
days = int(days)
|
||||
else:
|
||||
if reason:
|
||||
reason = "{} {}".format(days, reason)
|
||||
else:
|
||||
reason = days
|
||||
days = 0
|
||||
else:
|
||||
days = 0
|
||||
|
||||
@@ -14,7 +14,11 @@ class APIError(StreamsError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidCredentials(StreamsError):
|
||||
class InvalidTwitchCredentials(StreamsError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidYoutubeCredentials(StreamsError):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from redbot.core import Config, checks, RedContext
|
||||
from redbot.core.utils.chat_formatting import pagify, box
|
||||
from redbot.core.utils.chat_formatting import pagify
|
||||
from redbot.core.bot import Red
|
||||
from .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity
|
||||
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidCredentials,
|
||||
CommunityNotFound, OfflineCommunity, StreamsError)
|
||||
from redbot.core.i18n import CogI18n
|
||||
from .streamtypes import TwitchStream, HitboxStream, MixerStream, PicartoStream, TwitchCommunity, YoutubeStream
|
||||
from .errors import (OfflineStream, StreamNotFound, APIError, InvalidYoutubeCredentials,
|
||||
CommunityNotFound, OfflineCommunity, StreamsError, InvalidTwitchCredentials)
|
||||
from . import streamtypes as StreamClasses
|
||||
from collections import defaultdict
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
CHECK_DELAY = 60
|
||||
|
||||
|
||||
_ = CogI18n("Streams", __file__)
|
||||
|
||||
|
||||
class Streams:
|
||||
|
||||
global_defaults = {
|
||||
@@ -44,6 +49,14 @@ class Streams:
|
||||
|
||||
self.bot.loop.create_task(self._initialize_lists())
|
||||
|
||||
self.yt_cid_pattern = re.compile("^UC[-_A-Za-z0-9]{21}[AQgw]$")
|
||||
|
||||
def check_name_or_id(self, data: str):
|
||||
matched = self.yt_cid_pattern.fullmatch(data)
|
||||
if matched is None:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def _initialize_lists(self):
|
||||
self.streams = await self.load_streams()
|
||||
self.communities = await self.load_communities()
|
||||
@@ -58,6 +71,19 @@ class Streams:
|
||||
token=token)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
async def youtube(self, ctx, channel_id_or_name: str):
|
||||
"""
|
||||
Checks if a Youtube channel is streaming
|
||||
"""
|
||||
apikey = await self.db.tokens.get_raw(YoutubeStream.__name__, default=None)
|
||||
is_name = self.check_name_or_id(channel_id_or_name)
|
||||
if is_name:
|
||||
stream = YoutubeStream(name=channel_id_or_name, token=apikey)
|
||||
else:
|
||||
stream = YoutubeStream(id=channel_id_or_name, token=apikey)
|
||||
await self.check_online(ctx, stream)
|
||||
|
||||
@commands.command()
|
||||
async def hitbox(self, ctx, channel_name: str):
|
||||
"""Checks if a Hitbox channel is streaming"""
|
||||
@@ -80,15 +106,18 @@ class Streams:
|
||||
try:
|
||||
embed = await stream.is_online()
|
||||
except OfflineStream:
|
||||
await ctx.send("The stream is offline.")
|
||||
await ctx.send(_("The stream is offline."))
|
||||
except StreamNotFound:
|
||||
await ctx.send("The channel doesn't seem to exist.")
|
||||
except InvalidCredentials:
|
||||
await ctx.send("The twitch token is either invalid or has not been set. "
|
||||
"See `{}streamset twitchtoken`.".format(ctx.prefix))
|
||||
await ctx.send(_("The channel doesn't seem to exist."))
|
||||
except InvalidTwitchCredentials:
|
||||
await ctx.send(_("The twitch token is either invalid or has not been set. "
|
||||
"See `{}`.").format("{}streamset twitchtoken".format(ctx.prefix)))
|
||||
except InvalidYoutubeCredentials:
|
||||
await ctx.send(_("The Youtube API key is either invalid or has not been set. "
|
||||
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix)))
|
||||
except APIError:
|
||||
await ctx.send("Something went wrong whilst trying to contact the "
|
||||
"stream service's API.")
|
||||
await ctx.send(_("Something went wrong whilst trying to contact the "
|
||||
"stream service's API."))
|
||||
else:
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@@ -116,6 +145,11 @@ class Streams:
|
||||
for the specified community."""
|
||||
await self.community_alert(ctx, TwitchCommunity, community.lower())
|
||||
|
||||
@streamalert.command(name="youtube")
|
||||
async def youtube_alert(self, ctx: RedContext, channel_name_or_id: str):
|
||||
"""Sets a Youtube stream alert notification in the channel"""
|
||||
await self.stream_alert(ctx, YoutubeStream, channel_name_or_id)
|
||||
|
||||
@streamalert.command(name="hitbox")
|
||||
async def hitbox_alert(self, ctx, channel_name: str):
|
||||
"""Sets a Hitbox stream alert notification in the channel"""
|
||||
@@ -157,8 +191,8 @@ class Streams:
|
||||
self.streams = streams
|
||||
await self.save_streams()
|
||||
|
||||
msg = "All {}'s stream alerts have been disabled." \
|
||||
"".format("server" if _all else "channel")
|
||||
msg = _("All {}'s stream alerts have been disabled."
|
||||
"").format("server" if _all else "channel")
|
||||
|
||||
await ctx.send(msg)
|
||||
|
||||
@@ -166,7 +200,7 @@ class Streams:
|
||||
async def streamalert_list(self, ctx):
|
||||
streams_list = defaultdict(list)
|
||||
guild_channels_ids = [c.id for c in ctx.guild.channels]
|
||||
msg = "Active stream alerts:\n\n"
|
||||
msg = _("Active stream alerts:\n\n")
|
||||
|
||||
for stream in self.streams:
|
||||
for channel_id in stream.channels:
|
||||
@@ -174,7 +208,7 @@ class Streams:
|
||||
streams_list[channel_id].append(stream.name.lower())
|
||||
|
||||
if not streams_list:
|
||||
await ctx.send("There are no active stream alerts in this server.")
|
||||
await ctx.send(_("There are no active stream alerts in this server."))
|
||||
return
|
||||
|
||||
for channel_id, streams in streams_list.items():
|
||||
@@ -185,24 +219,34 @@ class Streams:
|
||||
await ctx.send(page)
|
||||
|
||||
async def stream_alert(self, ctx, _class, channel_name):
|
||||
stream = self.get_stream(_class, channel_name.lower())
|
||||
stream = self.get_stream(_class, channel_name)
|
||||
if not stream:
|
||||
token = await self.db.tokens.get_raw(_class.__name__, default=None)
|
||||
is_yt = _class.__name__ == "YoutubeStream"
|
||||
if is_yt and not self.check_name_or_id(channel_name):
|
||||
stream = _class(id=channel_name, token=token)
|
||||
else:
|
||||
stream = _class(name=channel_name,
|
||||
token=token)
|
||||
try:
|
||||
exists = await self.check_exists(stream)
|
||||
except InvalidCredentials:
|
||||
await ctx.send("The twitch token is either invalid or has not been set. "
|
||||
"See `{}streamset twitchtoken`.".format(ctx.prefix))
|
||||
except InvalidTwitchCredentials:
|
||||
await ctx.send(
|
||||
_("The twitch token is either invalid or has not been set. "
|
||||
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix)))
|
||||
return
|
||||
except InvalidYoutubeCredentials:
|
||||
await ctx.send(_("The Youtube API key is either invalid or has not been set. "
|
||||
"See {}.").format("`{}streamset youtubekey`".format(ctx.prefix)))
|
||||
return
|
||||
except APIError:
|
||||
await ctx.send("Something went wrong whilst trying to contact the "
|
||||
"stream service's API.")
|
||||
await ctx.send(
|
||||
_("Something went wrong whilst trying to contact the "
|
||||
"stream service's API."))
|
||||
return
|
||||
else:
|
||||
if not exists:
|
||||
await ctx.send("That channel doesn't seem to exist.")
|
||||
await ctx.send(_("That channel doesn't seem to exist."))
|
||||
return
|
||||
|
||||
await self.add_or_remove(ctx, stream)
|
||||
@@ -214,18 +258,18 @@ class Streams:
|
||||
community = _class(name=community_name, token=token)
|
||||
try:
|
||||
await community.get_community_streams()
|
||||
except InvalidCredentials:
|
||||
except InvalidTwitchCredentials:
|
||||
await ctx.send(
|
||||
"The twitch token is either invalid or has not been set. "
|
||||
"See `{}streamset twitchtoken`.".format(ctx.prefix))
|
||||
_("The twitch token is either invalid or has not been set. "
|
||||
"See {}.").format("`{}streamset twitchtoken`".format(ctx.prefix)))
|
||||
return
|
||||
except CommunityNotFound:
|
||||
await ctx.send("That community doesn't seem to exist.")
|
||||
await ctx.send(_("That community doesn't seem to exist."))
|
||||
return
|
||||
except APIError:
|
||||
await ctx.send(
|
||||
"Something went wrong whilst trying to contact the "
|
||||
"stream service's API.")
|
||||
_("Something went wrong whilst trying to contact the "
|
||||
"stream service's API."))
|
||||
return
|
||||
except OfflineCommunity:
|
||||
pass
|
||||
@@ -252,11 +296,25 @@ class Streams:
|
||||
5. Paste the Client ID into this command. Done!
|
||||
|
||||
"""
|
||||
tokens = await self.db.tokens()
|
||||
tokens["TwitchStream"] = token
|
||||
tokens["TwitchCommunity"] = token
|
||||
await self.db.tokens.set(tokens)
|
||||
await ctx.send("Twitch token set.")
|
||||
await self.db.tokens.set_raw("TwitchStream", value=token)
|
||||
await self.db.tokens.set_raw("TwitchCommunity", value=token)
|
||||
await ctx.send(_("Twitch token set."))
|
||||
|
||||
@streamset.command()
|
||||
@checks.is_owner()
|
||||
async def youtubekey(self, ctx: RedContext, key: str):
|
||||
"""Sets the API key for Youtube.
|
||||
|
||||
To get one, do the following:
|
||||
|
||||
1. Create a project (see https://support.google.com/googleapi/answer/6251787 for details)
|
||||
2. Enable the Youtube Data API v3 (see https://support.google.com/googleapi/answer/6158841 for instructions)
|
||||
3. Set up your API key (see https://support.google.com/googleapi/answer/6158862 for instructions)
|
||||
4. Copy your API key and paste it into this command. Done!
|
||||
|
||||
"""
|
||||
await self.db.tokens.set_raw("YoutubeStream", value=key)
|
||||
await ctx.send(_("Youtube key set."))
|
||||
|
||||
@streamset.group()
|
||||
@commands.guild_only()
|
||||
@@ -273,12 +331,12 @@ class Streams:
|
||||
current_setting = await self.db.guild(guild).mention_everyone()
|
||||
if current_setting:
|
||||
await self.db.guild(guild).mention_everyone.set(False)
|
||||
await ctx.send("@\u200beveryone will no longer be mentioned "
|
||||
"for a stream alert.")
|
||||
await ctx.send(_("{} will no longer be mentioned "
|
||||
"for a stream alert.").format("@\u200beveryone"))
|
||||
else:
|
||||
await self.db.guild(guild).mention_everyone.set(True)
|
||||
await ctx.send("When a stream configured for stream alerts "
|
||||
"comes online, @\u200beveryone will be mentioned")
|
||||
await ctx.send(_("When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned").format("@\u200beveryone"))
|
||||
|
||||
@mention.command(aliases=["here"])
|
||||
@commands.guild_only()
|
||||
@@ -288,12 +346,12 @@ class Streams:
|
||||
current_setting = await self.db.guild(guild).mention_here()
|
||||
if current_setting:
|
||||
await self.db.guild(guild).mention_here.set(False)
|
||||
await ctx.send("@\u200bhere will no longer be mentioned "
|
||||
"for a stream alert.")
|
||||
await ctx.send(_("{} will no longer be mentioned "
|
||||
"for a stream alert.").format("@\u200bhere"))
|
||||
else:
|
||||
await self.db.guild(guild).mention_here.set(True)
|
||||
await ctx.send("When a stream configured for stream alerts "
|
||||
"comes online, @\u200bhere will be mentioned")
|
||||
await ctx.send(_("When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned").format("@\u200bhere"))
|
||||
|
||||
@mention.command()
|
||||
@commands.guild_only()
|
||||
@@ -305,13 +363,13 @@ class Streams:
|
||||
return
|
||||
if current_setting:
|
||||
await self.db.role(role).mention.set(False)
|
||||
await ctx.send("@\u200b{} will no longer be mentioned "
|
||||
"for a stream alert".format(role.name))
|
||||
await ctx.send(_("{} will no longer be mentioned "
|
||||
"for a stream alert").format("@\u200b{}".format(role.name)))
|
||||
else:
|
||||
await self.db.role(role).mention.set(True)
|
||||
await ctx.send("When a stream configured for stream alerts "
|
||||
"comes online, @\u200b{} will be mentioned"
|
||||
"".format(role.name))
|
||||
await ctx.send(_("When a stream configured for stream alerts "
|
||||
"comes online, {} will be mentioned"
|
||||
"").format("@\u200b{}".format(role.name)))
|
||||
|
||||
@streamset.command()
|
||||
@commands.guild_only()
|
||||
@@ -329,14 +387,14 @@ class Streams:
|
||||
stream.channels.append(ctx.channel.id)
|
||||
if stream not in self.streams:
|
||||
self.streams.append(stream)
|
||||
await ctx.send("I'll send a notification in this channel when {} "
|
||||
"is online.".format(stream.name))
|
||||
await ctx.send(_("I'll send a notification in this channel when {} "
|
||||
"is online.").format(stream.name))
|
||||
else:
|
||||
stream.channels.remove(ctx.channel.id)
|
||||
if not stream.channels:
|
||||
self.streams.remove(stream)
|
||||
await ctx.send("I won't send notifications about {} in this "
|
||||
"channel anymore.".format(stream.name))
|
||||
await ctx.send(_("I won't send notifications about {} in this "
|
||||
"channel anymore.").format(stream.name))
|
||||
|
||||
await self.save_streams()
|
||||
|
||||
@@ -345,16 +403,16 @@ class Streams:
|
||||
community.channels.append(ctx.channel.id)
|
||||
if community not in self.communities:
|
||||
self.communities.append(community)
|
||||
await ctx.send("I'll send a notification in this channel when a "
|
||||
await ctx.send(_("I'll send a notification in this channel when a "
|
||||
"channel is streaming to the {} community"
|
||||
"".format(community.name))
|
||||
"").format(community.name))
|
||||
else:
|
||||
community.channels.remove(ctx.channel.id)
|
||||
if not community.channels:
|
||||
self.communities.remove(community)
|
||||
await ctx.send("I won't send notifications about channels streaming "
|
||||
await ctx.send(_("I won't send notifications about channels streaming "
|
||||
"to the {} community in this channel anymore"
|
||||
"".format(community.name))
|
||||
"").format(community.name))
|
||||
await self.save_communities()
|
||||
|
||||
def get_stream(self, _class, name):
|
||||
@@ -365,6 +423,12 @@ class Streams:
|
||||
# isinstance will always return False
|
||||
# As a workaround, we'll compare the class' name instead.
|
||||
# Good enough.
|
||||
if _class.__name__ == "YoutubeStream" and stream.type == _class.__name__:
|
||||
# Because name could be a username or a channel id
|
||||
if self.check_name_or_id(name) and stream.name.lower() == name.lower():
|
||||
return stream
|
||||
elif not self.check_name_or_id(name) and stream.id == name:
|
||||
return stream
|
||||
if stream.type == _class.__name__ and stream.name.lower() == name.lower():
|
||||
return stream
|
||||
|
||||
@@ -403,7 +467,7 @@ class Streams:
|
||||
except OfflineStream:
|
||||
for message in stream._messages_cache:
|
||||
try:
|
||||
autodelete = self.db.guild(message.guild).autodelete()
|
||||
autodelete = await self.db.guild(message.guild).autodelete()
|
||||
if autodelete:
|
||||
await message.delete()
|
||||
except:
|
||||
@@ -444,30 +508,51 @@ class Streams:
|
||||
async def check_communities(self):
|
||||
for community in self.communities:
|
||||
try:
|
||||
streams = community.get_community_streams()
|
||||
stream_list = await community.get_community_streams()
|
||||
except CommunityNotFound:
|
||||
print("Community {} not found!".format(community.name))
|
||||
print(_("Community {} not found!").format(community.name))
|
||||
continue
|
||||
except OfflineCommunity:
|
||||
pass
|
||||
else:
|
||||
token = self.db.tokens().get(TwitchStream.__name__)
|
||||
for channel in community.channels:
|
||||
chn = self.bot.get_channel(channel)
|
||||
await chn.send("Online streams for {}".format(community.name))
|
||||
for stream in streams:
|
||||
stream_obj = TwitchStream(
|
||||
token=token, name=stream["channel"]["name"],
|
||||
id=stream["_id"]
|
||||
)
|
||||
for message in community._messages_cache:
|
||||
try:
|
||||
emb = await stream_obj.is_online()
|
||||
autodelete = await self.db.guild(message.guild).autodelete()
|
||||
if autodelete:
|
||||
await message.delete()
|
||||
except:
|
||||
pass
|
||||
community._messages_cache.clear()
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
for channel in community.channels:
|
||||
chn = self.bot.get_channel(channel)
|
||||
await chn.send(embed=emb)
|
||||
streams = await self.filter_streams(stream_list, chn)
|
||||
emb = await community.make_embed(streams)
|
||||
chn_msg = [m for m in community._messages_cache if m.channel == chn]
|
||||
if not chn_msg:
|
||||
mentions = await self._get_mention_str(chn.guild)
|
||||
if mentions:
|
||||
msg = await chn.send(mentions, embed=emb)
|
||||
else:
|
||||
msg = await chn.send(embed=emb)
|
||||
community._messages_cache.append(msg)
|
||||
else:
|
||||
chn_msg = sorted(chn_msg, key=lambda x: x.created_at, reverse=True)[0]
|
||||
community._messages_cache.remove(chn_msg)
|
||||
await chn_msg.edit(embed=emb)
|
||||
community._messages_cache.append(chn_msg)
|
||||
|
||||
async def filter_streams(self, streams: list, channel: discord.TextChannel) -> list:
|
||||
filtered = []
|
||||
for stream in streams:
|
||||
tw_id = str(stream["channel"]["_id"])
|
||||
for alert in self.streams:
|
||||
if isinstance(alert, TwitchStream) and alert.id == tw_id:
|
||||
if channel.id in alert.channels:
|
||||
break
|
||||
else:
|
||||
filtered.append(stream)
|
||||
return filtered
|
||||
|
||||
async def load_streams(self):
|
||||
streams = []
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .errors import StreamNotFound, APIError, InvalidCredentials, OfflineStream, CommunityNotFound, OfflineCommunity
|
||||
from random import choice
|
||||
from .errors import StreamNotFound, APIError, OfflineStream, CommunityNotFound, OfflineCommunity, \
|
||||
InvalidYoutubeCredentials, InvalidTwitchCredentials
|
||||
from random import choice, sample
|
||||
from string import ascii_letters
|
||||
import discord
|
||||
import aiohttp
|
||||
@@ -10,6 +11,11 @@ TWITCH_ID_ENDPOINT = TWITCH_BASE_URL + "/kraken/users?login="
|
||||
TWITCH_STREAMS_ENDPOINT = TWITCH_BASE_URL + "/kraken/streams/"
|
||||
TWITCH_COMMUNITIES_ENDPOINT = TWITCH_BASE_URL + "/kraken/communities"
|
||||
|
||||
YOUTUBE_BASE_URL = "https://www.googleapis.com/youtube/v3"
|
||||
YOUTUBE_CHANNELS_ENDPOINT = YOUTUBE_BASE_URL + "/channels"
|
||||
YOUTUBE_SEARCH_ENDPOINT = YOUTUBE_BASE_URL + "/search"
|
||||
YOUTUBE_VIDEOS_ENDPOINT = YOUTUBE_BASE_URL + "/videos"
|
||||
|
||||
|
||||
def rnd(url):
|
||||
"""Appends a random parameter to the url to avoid Discord's caching"""
|
||||
@@ -21,6 +27,7 @@ class TwitchCommunity:
|
||||
self.name = kwargs.pop("name")
|
||||
self.id = kwargs.pop("id", None)
|
||||
self.channels = kwargs.pop("channels", [])
|
||||
self._messages_cache = []
|
||||
self._token = kwargs.pop("token", None)
|
||||
self.type = self.__class__.__name__
|
||||
|
||||
@@ -38,7 +45,7 @@ class TwitchCommunity:
|
||||
if r.status == 200:
|
||||
return data["_id"]
|
||||
elif r.status == 400:
|
||||
raise InvalidCredentials()
|
||||
raise InvalidTwitchCredentials()
|
||||
elif r.status == 404:
|
||||
raise CommunityNotFound()
|
||||
else:
|
||||
@@ -55,7 +62,8 @@ class TwitchCommunity:
|
||||
"Client-ID": str(self._token)
|
||||
}
|
||||
params = {
|
||||
"community_id": self.id
|
||||
"community_id": self.id,
|
||||
"limit": 100
|
||||
}
|
||||
url = TWITCH_BASE_URL + "/kraken/streams"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
@@ -67,12 +75,41 @@ class TwitchCommunity:
|
||||
else:
|
||||
return data["streams"]
|
||||
elif r.status == 400:
|
||||
raise InvalidCredentials()
|
||||
raise InvalidTwitchCredentials()
|
||||
elif r.status == 404:
|
||||
raise CommunityNotFound()
|
||||
else:
|
||||
raise APIError()
|
||||
|
||||
async def make_embed(self, streams: list) -> discord.Embed:
|
||||
headers = {
|
||||
"Accept": "application/vnd.twitchtv.v5+json",
|
||||
"Client-ID": str(self._token)
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(
|
||||
"{}/{}".format(TWITCH_COMMUNITIES_ENDPOINT, self.id),
|
||||
headers=headers) as r:
|
||||
data = await r.json()
|
||||
|
||||
avatar = data["avatar_image_url"]
|
||||
title = "Channels currently streaming to {}".format(data["display_name"])
|
||||
url = "https://www.twitch.tv/communities/{}".format(self.name)
|
||||
embed = discord.Embed(title=title, url=url)
|
||||
embed.set_image(url=avatar)
|
||||
if len(streams) >= 10:
|
||||
stream_list = sample(streams, 10)
|
||||
else:
|
||||
stream_list = streams
|
||||
for stream in stream_list:
|
||||
name = "[{}]({})".format(
|
||||
stream["channel"]["display_name"], stream["channel"]["url"]
|
||||
)
|
||||
embed.add_field(name=stream["channel"]["status"], value=name, inline=False)
|
||||
embed.color = 0x6441A4
|
||||
|
||||
return embed
|
||||
|
||||
def export(self):
|
||||
data = {}
|
||||
for k, v in self.__dict__.items():
|
||||
@@ -86,7 +123,7 @@ class TwitchCommunity:
|
||||
|
||||
class Stream:
|
||||
def __init__(self, **kwargs):
|
||||
self.name = kwargs.pop("name")
|
||||
self.name = kwargs.pop("name", None)
|
||||
self.channels = kwargs.pop("channels", [])
|
||||
#self.already_online = kwargs.pop("already_online", False)
|
||||
self._messages_cache = []
|
||||
@@ -109,6 +146,75 @@ class Stream:
|
||||
return "<{0.__class__.__name__}: {0.name}>".format(self)
|
||||
|
||||
|
||||
class YoutubeStream(Stream):
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop("id", None)
|
||||
self._token = kwargs.pop("token", None)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
async def is_online(self):
|
||||
if not self.id:
|
||||
self.id = await self.fetch_id()
|
||||
url = YOUTUBE_SEARCH_ENDPOINT
|
||||
params = {
|
||||
"key": self._token,
|
||||
"part": "snippet",
|
||||
"channelId": self.id,
|
||||
"type": "video",
|
||||
"eventType": "live"
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params) as r:
|
||||
data = await r.json()
|
||||
if "items" in data and len(data["items"]) == 0:
|
||||
raise OfflineStream()
|
||||
elif "items" in data:
|
||||
vid_id = data["items"][0]["id"]["videoId"]
|
||||
params = {
|
||||
"key": self._token,
|
||||
"id": vid_id,
|
||||
"part": "snippet"
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(YOUTUBE_VIDEOS_ENDPOINT, params=params) as r:
|
||||
data = await r.json()
|
||||
return self.make_embed(data)
|
||||
|
||||
def make_embed(self, data):
|
||||
vid_data = data["items"][0]
|
||||
video_url = "https://youtube.com/watch?v={}".format(vid_data["id"])
|
||||
title = vid_data["snippet"]["title"]
|
||||
thumbnail = vid_data["snippet"]["thumbnails"]["default"]["url"]
|
||||
channel_title = data["snippet"]["channelTitle"]
|
||||
embed = discord.Embed(title=title, url=video_url)
|
||||
embed.set_author(name=channel_title)
|
||||
embed.set_image(url=rnd(thumbnail))
|
||||
embed.colour = 0x9255A5
|
||||
return embed
|
||||
|
||||
async def fetch_id(self):
|
||||
params = {
|
||||
"key": self._token,
|
||||
"forUsername": self.name,
|
||||
"part": "id"
|
||||
}
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(YOUTUBE_CHANNELS_ENDPOINT, params=params) as r:
|
||||
data = await r.json()
|
||||
|
||||
if "error" in data and data["error"]["code"] == 400 and\
|
||||
data["error"]["errors"][0]["reason"] == "keyInvalid":
|
||||
raise InvalidYoutubeCredentials()
|
||||
elif "items" in data and len(data["items"]) == 0:
|
||||
raise StreamNotFound()
|
||||
elif "items" in data:
|
||||
return data["items"][0]["id"]
|
||||
raise APIError()
|
||||
|
||||
def __repr__(self):
|
||||
return "<{0.__class__.__name__}: {0.name} (ID: {0.id})>".format(self)
|
||||
|
||||
|
||||
class TwitchStream(Stream):
|
||||
def __init__(self, **kwargs):
|
||||
self.id = kwargs.pop("id", None)
|
||||
@@ -137,7 +243,7 @@ class TwitchStream(Stream):
|
||||
self.name = data["stream"]["channel"]["name"]
|
||||
return self.make_embed(data)
|
||||
elif r.status == 400:
|
||||
raise InvalidCredentials()
|
||||
raise InvalidTwitchCredentials()
|
||||
elif r.status == 404:
|
||||
raise StreamNotFound()
|
||||
else:
|
||||
@@ -159,7 +265,7 @@ class TwitchStream(Stream):
|
||||
raise StreamNotFound()
|
||||
return data["users"][0]["_id"]
|
||||
elif r.status == 400:
|
||||
raise InvalidCredentials()
|
||||
raise InvalidTwitchCredentials()
|
||||
else:
|
||||
raise APIError()
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ class Trivia:
|
||||
raise FileNotFoundError("Could not find the `{}` category"
|
||||
"".format(category))
|
||||
|
||||
with path.open() as file:
|
||||
with path.open(encoding='utf-8') as file:
|
||||
try:
|
||||
dict_ = yaml.load(file)
|
||||
except yaml.error.YAMLError as exc:
|
||||
|
||||
@@ -16,7 +16,7 @@ async def warning_points_add_check(config: Config, ctx: RedContext, user: discor
|
||||
guild_settings = config.guild(guild)
|
||||
act = {}
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for a in registered_actions.keys():
|
||||
for a in registered_actions:
|
||||
if points >= registered_actions[a]["point_count"]:
|
||||
act = registered_actions[a]
|
||||
else:
|
||||
@@ -30,7 +30,7 @@ async def warning_points_remove_check(config: Config, ctx: RedContext, user: dis
|
||||
guild_settings = config.guild(guild)
|
||||
act = {}
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for a in registered_actions.keys():
|
||||
for a in registered_actions:
|
||||
if points >= registered_actions[a]["point_count"]:
|
||||
act = registered_actions[a]
|
||||
else:
|
||||
|
||||
@@ -208,7 +208,7 @@ class Warnings:
|
||||
guild_settings = self.config.guild(guild)
|
||||
msg_list = []
|
||||
async with guild_settings.actions() as registered_actions:
|
||||
for r in registered_actions.keys():
|
||||
for r in registered_actions:
|
||||
msg_list.append(
|
||||
"Name: {}\nPoints: {}\nDescription: {}".format(
|
||||
r, r["points"], r["description"]
|
||||
|
||||
@@ -243,7 +243,7 @@ async def deposit_credits(member: discord.Member, amount: int) -> int:
|
||||
|
||||
"""
|
||||
if _invalid_amount(amount):
|
||||
raise ValueError("Invalid withdrawal amount {} <= 0".format(amount))
|
||||
raise ValueError("Invalid deposit amount {} <= 0".format(amount))
|
||||
|
||||
bal = await get_balance(member)
|
||||
return await set_balance(member, amount + bal)
|
||||
@@ -287,62 +287,81 @@ async def wipe_bank():
|
||||
await _conf.clear_all_members()
|
||||
|
||||
|
||||
async def get_guild_accounts(guild: discord.Guild) -> List[Account]:
|
||||
"""Get all account data for the given guild.
|
||||
async def get_leaderboard(positions: int=None, guild: discord.Guild=None) -> List[tuple]:
|
||||
"""
|
||||
Gets the bank's leaderboard
|
||||
|
||||
Parameters
|
||||
----------
|
||||
positions : `int`
|
||||
The number of positions to get
|
||||
guild : discord.Guild
|
||||
The guild to get accounts for.
|
||||
The guild to get the leaderboard of. If the bank is global and this
|
||||
is provided, get only guild members on the leaderboard
|
||||
|
||||
Returns
|
||||
-------
|
||||
`list` of `Account`
|
||||
A list of all guild accounts.
|
||||
`list` of `tuple`
|
||||
The sorted leaderboard in the form of :code:`(user_id, raw_account)`
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the bank is currently global.
|
||||
TypeError
|
||||
If the bank is guild-specific and no guild was specified
|
||||
|
||||
"""
|
||||
if await is_global():
|
||||
raise RuntimeError("The bank is currently global.")
|
||||
|
||||
ret = []
|
||||
accs = await _conf.all_members(guild)
|
||||
for user_id, acc in accs.items():
|
||||
acc_data = acc.copy() # There ya go kowlin
|
||||
acc_data['created_at'] = _decode_time(acc_data['created_at'])
|
||||
ret.append(Account(**acc_data))
|
||||
return ret
|
||||
raw_accounts = await _conf.all_users()
|
||||
if guild is not None:
|
||||
tmp = raw_accounts.copy()
|
||||
for acc in tmp:
|
||||
if not guild.get_member(acc):
|
||||
del raw_accounts[acc]
|
||||
else:
|
||||
if guild is None:
|
||||
raise TypeError("Expected a guild, got NoneType object instead!")
|
||||
raw_accounts = await _conf.all_members(guild)
|
||||
sorted_acc = sorted(raw_accounts.items(), key=lambda x: x[1]['balance'], reverse=True)
|
||||
if positions is None:
|
||||
return sorted_acc
|
||||
else:
|
||||
return sorted_acc[:positions]
|
||||
|
||||
|
||||
async def get_global_accounts() -> List[Account]:
|
||||
"""Get all global account data.
|
||||
async def get_leaderboard_position(member: Union[discord.User, discord.Member]) -> Union[int, None]:
|
||||
"""
|
||||
Get the leaderboard position for the specified user
|
||||
|
||||
Parameters
|
||||
----------
|
||||
member : `discord.User` or `discord.Member`
|
||||
The user to get the leaderboard position of
|
||||
|
||||
Returns
|
||||
-------
|
||||
`list` of `Account`
|
||||
A list of all global accounts.
|
||||
`int`
|
||||
The position of the user on the leaderboard
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the bank is currently guild specific.
|
||||
TypeError
|
||||
If the bank is currently guild-specific and a `discord.User` object was passed in
|
||||
|
||||
"""
|
||||
if not await is_global():
|
||||
raise RuntimeError("The bank is not currently global.")
|
||||
|
||||
ret = []
|
||||
accs = await _conf.all_users() # this is a dict of user -> acc
|
||||
for user_id, acc in accs.items():
|
||||
acc_data = acc.copy()
|
||||
acc_data['created_at'] = _decode_time(acc_data['created_at'])
|
||||
ret.append(Account(**acc_data))
|
||||
|
||||
return ret
|
||||
if await is_global():
|
||||
guild = None
|
||||
else:
|
||||
guild = member.guild if hasattr(member, "guild") else None
|
||||
try:
|
||||
leaderboard = await get_leaderboard(None, guild)
|
||||
except TypeError:
|
||||
raise
|
||||
else:
|
||||
pos = discord.utils.find(lambda x: x[1][0] == member.id, enumerate(leaderboard, 1))
|
||||
if pos is None:
|
||||
return None
|
||||
else:
|
||||
return pos[0]
|
||||
|
||||
|
||||
async def get_account(member: Union[discord.Member, discord.User]) -> Account:
|
||||
|
||||
@@ -58,7 +58,8 @@ class RedBase(BotBase, RpcMethodMixin):
|
||||
whitelist=[],
|
||||
blacklist=[],
|
||||
enable_sentry=None,
|
||||
locale='en'
|
||||
locale='en',
|
||||
embeds=True
|
||||
)
|
||||
|
||||
self.db.register_guild(
|
||||
@@ -66,7 +67,12 @@ class RedBase(BotBase, RpcMethodMixin):
|
||||
whitelist=[],
|
||||
blacklist=[],
|
||||
admin_role=None,
|
||||
mod_role=None
|
||||
mod_role=None,
|
||||
embeds=None
|
||||
)
|
||||
|
||||
self.db.register_user(
|
||||
embeds=None
|
||||
)
|
||||
|
||||
async def prefix_manager(bot, message):
|
||||
@@ -136,6 +142,38 @@ class RedBase(BotBase, RpcMethodMixin):
|
||||
indict['owner_id'] = await self.db.owner()
|
||||
i18n.set_locale(await self.db.locale())
|
||||
|
||||
async def embed_requested(self, channel, user, command=None) -> bool:
|
||||
"""
|
||||
Determine if an embed is requested for a response.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
channel : `discord.abc.GuildChannel` or `discord.abc.PrivateChannel`
|
||||
The channel to check embed settings for.
|
||||
user : `discord.abc.User`
|
||||
The user to check embed settings for.
|
||||
command
|
||||
(Optional) the command ran.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
:code:`True` if an embed is requested
|
||||
"""
|
||||
if isinstance(channel, discord.abc.PrivateChannel) or (
|
||||
command and command == self.get_command("help")
|
||||
):
|
||||
user_setting = await self.db.user(user).embeds()
|
||||
if user_setting is not None:
|
||||
return user_setting
|
||||
else:
|
||||
guild_setting = await self.db.guild(channel.guild).embeds()
|
||||
if command and command != self.get_command("help"):
|
||||
if guild_setting is not None:
|
||||
return guild_setting
|
||||
global_setting = await self.db.embeds()
|
||||
return global_setting
|
||||
|
||||
async def is_owner(self, user):
|
||||
if user.id in self._co_owners:
|
||||
return True
|
||||
|
||||
@@ -54,6 +54,12 @@ def admin_or_permissions(**perms):
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def bot_in_a_guild(**kwargs):
|
||||
async def predicate(ctx):
|
||||
return len(ctx.bot.guilds) > 0
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
def guildowner_or_permissions(**perms):
|
||||
async def predicate(ctx):
|
||||
has_perms_or_is_owner = await check_permissions(ctx, perms)
|
||||
|
||||
@@ -415,6 +415,9 @@ class CogManagerUI:
|
||||
|
||||
unloaded = all - loaded
|
||||
|
||||
loaded = sorted(list(loaded), key=str.lower)
|
||||
unloaded = sorted(list(unloaded), key=str.lower)
|
||||
|
||||
loaded = ('**{} loaded:**\n').format(len(loaded)) + ", ".join(loaded)
|
||||
unloaded = ('**{} unloaded:**\n').format(len(unloaded)) + ", ".join(unloaded)
|
||||
|
||||
|
||||
@@ -124,3 +124,16 @@ class RedContext(commands.Context):
|
||||
# or chanel is a DM
|
||||
await query.delete()
|
||||
return ret
|
||||
|
||||
async def embed_requested(self):
|
||||
"""
|
||||
Simple helper to call bot.embed_requested
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool:
|
||||
:code:`True` if an embed is requested
|
||||
"""
|
||||
return await self.bot.embed_requested(
|
||||
self.channel, self.author, command=self.command
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from collections import namedtuple
|
||||
from pathlib import Path
|
||||
from random import SystemRandom
|
||||
from string import ascii_letters, digits
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
@@ -21,7 +22,7 @@ from redbot.core import i18n
|
||||
from redbot.core import rpc
|
||||
from redbot.core.context import RedContext
|
||||
from .utils import TYPE_CHECKING
|
||||
from .utils.chat_formatting import pagify, box
|
||||
from .utils.chat_formatting import pagify, box, inline
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from redbot.core.bot import Red
|
||||
@@ -68,6 +69,10 @@ class Core:
|
||||
app_info = await self.bot.application_info()
|
||||
owner = app_info.owner
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get("http://pypi.python.org/pypi/red-discordbot/json") as r:
|
||||
data = await r.json()
|
||||
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__)
|
||||
about = (
|
||||
"This is an instance of [Red, an open source Discord bot]({}) "
|
||||
"created by [Twentysix]({}) and [improved by many]({}).\n\n"
|
||||
@@ -81,7 +86,13 @@ class Core:
|
||||
embed.add_field(name="Python", value=python_version)
|
||||
embed.add_field(name="discord.py", value=dpy_version)
|
||||
embed.add_field(name="Red version", value=red_version)
|
||||
if outdated:
|
||||
embed.add_field(name="Outdated", value="Yes, {} is available".format(
|
||||
data["info"]["version"]
|
||||
)
|
||||
)
|
||||
embed.add_field(name="About Red", value=about, inline=False)
|
||||
|
||||
embed.set_footer(text="Bringing joy since 02 Jan 2016 (over "
|
||||
"{} days ago!)".format(days_since))
|
||||
try:
|
||||
@@ -120,6 +131,88 @@ class Core:
|
||||
|
||||
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
|
||||
|
||||
@commands.group(hidden=True)
|
||||
async def embedset(self, ctx: RedContext):
|
||||
"""
|
||||
Commands for toggling embeds on or off.
|
||||
|
||||
This setting determines whether or not to
|
||||
use embeds as a response to a command (for
|
||||
commands that support it). The default is to
|
||||
use embeds.
|
||||
"""
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send_help()
|
||||
|
||||
@embedset.command(name="global")
|
||||
@checks.is_owner()
|
||||
async def embedset_global(self, ctx: RedContext):
|
||||
"""
|
||||
Toggle the global embed setting.
|
||||
|
||||
This is used as a fallback if the user
|
||||
or guild hasn't set a preference. The
|
||||
default is to use embeds.
|
||||
"""
|
||||
current = await self.bot.db.embeds()
|
||||
await self.bot.db.embeds.set(not current)
|
||||
await ctx.send(
|
||||
_("Embeds are now {} by default.").format(
|
||||
"disabled" if current else "enabled"
|
||||
)
|
||||
)
|
||||
|
||||
@embedset.command(name="guild")
|
||||
@checks.guildowner_or_permissions(administrator=True)
|
||||
async def embedset_guild(self, ctx: RedContext, enabled: bool=None):
|
||||
"""
|
||||
Toggle the guild's embed setting.
|
||||
|
||||
If enabled is None, the setting will be unset and
|
||||
the global default will be used instead.
|
||||
|
||||
If set, this is used instead of the global default
|
||||
to determine whether or not to use embeds. This is
|
||||
used for all commands done in a guild channel except
|
||||
for help commands.
|
||||
"""
|
||||
await self.bot.db.guild(ctx.guild).embeds.set(enabled)
|
||||
if enabled is None:
|
||||
await ctx.send(
|
||||
_("Embeds will now fall back to the global setting.")
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
_("Embeds are now {} for this guild.").format(
|
||||
"enabled" if enabled else "disabled"
|
||||
)
|
||||
)
|
||||
|
||||
@embedset.command(name="user")
|
||||
async def embedset_user(self, ctx: RedContext, enabled: bool=None):
|
||||
"""
|
||||
Toggle the user's embed setting.
|
||||
|
||||
If enabled is None, the setting will be unset and
|
||||
the global default will be used instead.
|
||||
|
||||
If set, this is used instead of the global default
|
||||
to determine whether or not to use embeds. This is
|
||||
used for all commands done in a DM with the bot, as
|
||||
well as all help commands everywhere.
|
||||
"""
|
||||
await self.bot.db.user(ctx.author).embeds.set(enabled)
|
||||
if enabled is None:
|
||||
await ctx.send(
|
||||
_("Embeds will now fall back to the global setting.")
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
_("Embeds are now {} for you.").format(
|
||||
"enabled" if enabled else "disabled"
|
||||
)
|
||||
)
|
||||
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def traceback(self, ctx, public: bool=False):
|
||||
@@ -219,14 +312,28 @@ class Core:
|
||||
@commands.command()
|
||||
@checks.is_owner()
|
||||
async def load(self, ctx, *, cog_name: str):
|
||||
"""Loads a package"""
|
||||
"""Loads packages"""
|
||||
|
||||
failed_packages = []
|
||||
loaded_packages = []
|
||||
notfound_packages = []
|
||||
|
||||
cognames = [c.strip() for c in cog_name.split(' ')]
|
||||
cogspecs = []
|
||||
|
||||
for c in cognames:
|
||||
try:
|
||||
spec = await ctx.bot.cog_mgr.find_cog(cog_name)
|
||||
spec = await ctx.bot.cog_mgr.find_cog(c)
|
||||
cogspecs.append((spec, c))
|
||||
except RuntimeError:
|
||||
await ctx.send(_("No module by that name was found in any"
|
||||
" cog path."))
|
||||
notfound_packages.append(inline(c))
|
||||
#await ctx.send(_("No module named '{}' was found in any"
|
||||
# " cog path.").format(c))
|
||||
|
||||
if len(cogspecs) == 0:
|
||||
return
|
||||
|
||||
for spec, name in cogspecs:
|
||||
try:
|
||||
await ctx.bot.load_extension(spec)
|
||||
except Exception as e:
|
||||
@@ -237,40 +344,80 @@ class Core:
|
||||
exception_log += "".join(traceback.format_exception(type(e),
|
||||
e, e.__traceback__))
|
||||
self.bot._last_exception = exception_log
|
||||
|
||||
await ctx.send(_("Failed to load package. Check your console or "
|
||||
"logs for details."))
|
||||
failed_packages.append(inline(name))
|
||||
else:
|
||||
await ctx.bot.add_loaded_package(cog_name)
|
||||
await ctx.send(_("Done."))
|
||||
await ctx.bot.add_loaded_package(name)
|
||||
loaded_packages.append(inline(name))
|
||||
|
||||
if loaded_packages:
|
||||
fmt = "Loaded {packs}"
|
||||
formed = self.get_package_strings(loaded_packages, fmt)
|
||||
await ctx.send(_(formed))
|
||||
|
||||
if failed_packages:
|
||||
fmt = ("Failed to load package{plural} {packs}. Check your console or "
|
||||
"logs for details.")
|
||||
formed = self.get_package_strings(failed_packages, fmt)
|
||||
await ctx.send(_(formed))
|
||||
|
||||
if notfound_packages:
|
||||
fmt = 'The package{plural} {packs} {other} not found in any cog path.'
|
||||
formed = self.get_package_strings(notfound_packages, fmt, ('was', 'were'))
|
||||
await ctx.send(_(formed))
|
||||
|
||||
@commands.group()
|
||||
@checks.is_owner()
|
||||
async def unload(self, ctx, *, cog_name: str):
|
||||
"""Unloads a package"""
|
||||
if cog_name in ctx.bot.extensions:
|
||||
ctx.bot.unload_extension(cog_name)
|
||||
await ctx.bot.remove_loaded_package(cog_name)
|
||||
await ctx.send(_("Done."))
|
||||
"""Unloads packages"""
|
||||
cognames = [c.strip() for c in cog_name.split(' ')]
|
||||
failed_packages = []
|
||||
unloaded_packages = []
|
||||
|
||||
for c in cognames:
|
||||
if c in ctx.bot.extensions:
|
||||
ctx.bot.unload_extension(c)
|
||||
await ctx.bot.remove_loaded_package(c)
|
||||
unloaded_packages.append(inline(c))
|
||||
else:
|
||||
await ctx.send(_("That extension is not loaded."))
|
||||
failed_packages.append(inline(c))
|
||||
|
||||
if unloaded_packages:
|
||||
fmt = "Package{plural} {packs} {other} unloaded."
|
||||
formed = self.get_package_strings(unloaded_packages, fmt, ('was', 'were'))
|
||||
await ctx.send(_(formed))
|
||||
|
||||
if failed_packages:
|
||||
fmt = "The package{plural} {packs} {other} not loaded."
|
||||
formed = self.get_package_strings(failed_packages, fmt, ('is', 'are'))
|
||||
await ctx.send(_(formed))
|
||||
|
||||
@commands.command(name="reload")
|
||||
@checks.is_owner()
|
||||
async def _reload(self, ctx, *, cog_name: str):
|
||||
"""Reloads a package"""
|
||||
ctx.bot.unload_extension(cog_name)
|
||||
"""Reloads packages"""
|
||||
|
||||
cognames = [c.strip() for c in cog_name.split(' ')]
|
||||
|
||||
for c in cognames:
|
||||
ctx.bot.unload_extension(c)
|
||||
|
||||
cogspecs = []
|
||||
failed_packages = []
|
||||
loaded_packages = []
|
||||
notfound_packages = []
|
||||
|
||||
for c in cognames:
|
||||
try:
|
||||
spec = await ctx.bot.cog_mgr.find_cog(cog_name)
|
||||
spec = await ctx.bot.cog_mgr.find_cog(c)
|
||||
cogspecs.append((spec, c))
|
||||
except RuntimeError:
|
||||
await ctx.send(_("No module by that name was found in any"
|
||||
" cog path."))
|
||||
return
|
||||
notfound_packages.append(inline(c))
|
||||
|
||||
for spec, name in cogspecs:
|
||||
try:
|
||||
self.cleanup_and_refresh_modules(spec.name)
|
||||
await ctx.bot.load_extension(spec)
|
||||
loaded_packages.append(inline(name))
|
||||
except Exception as e:
|
||||
log.exception("Package reloading failed", exc_info=e)
|
||||
|
||||
@@ -280,10 +427,40 @@ class Core:
|
||||
e, e.__traceback__))
|
||||
self.bot._last_exception = exception_log
|
||||
|
||||
await ctx.send(_("Failed to reload package. Check your console or "
|
||||
"logs for details."))
|
||||
else:
|
||||
await ctx.send(_("Done."))
|
||||
failed_packages.append(inline(name))
|
||||
|
||||
if loaded_packages:
|
||||
fmt = "Package{plural} {packs} {other} reloaded."
|
||||
formed = self.get_package_strings(loaded_packages, fmt, ('was', 'were'))
|
||||
await ctx.send(_(formed))
|
||||
|
||||
if failed_packages:
|
||||
fmt = ("Failed to reload package{plural} {packs}. Check your "
|
||||
"logs for details")
|
||||
formed = self.get_package_strings(failed_packages, fmt)
|
||||
await ctx.send(_(formed))
|
||||
|
||||
if notfound_packages:
|
||||
fmt = 'The package{plural} {packs} {other} not found in any cog path.'
|
||||
formed = self.get_package_strings(notfound_packages, fmt, ('was', 'were'))
|
||||
await ctx.send(_(formed))
|
||||
|
||||
def get_package_strings(self, packages: list, fmt: str, other: tuple=None):
|
||||
"""
|
||||
Gets the strings needed for the load, unload and reload commands
|
||||
"""
|
||||
if other is None:
|
||||
other = ('', '')
|
||||
plural = 's' if len(packages) > 1 else ''
|
||||
use_and, other = ('', other[0]) if len(packages) == 1 else (' and ', other[1])
|
||||
packages_string = ', '.join(packages[:-1]) + use_and + packages[-1]
|
||||
|
||||
form = {'plural': plural,
|
||||
'packs' : packages_string,
|
||||
'other' : other
|
||||
}
|
||||
final_string = fmt.format(**form)
|
||||
return final_string
|
||||
|
||||
@commands.command(name="shutdown")
|
||||
@checks.is_owner()
|
||||
@@ -375,6 +552,7 @@ class Core:
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="game")
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def _game(self, ctx, *, game: str=None):
|
||||
"""Sets Red's playing status"""
|
||||
@@ -385,11 +563,11 @@ class Core:
|
||||
game = None
|
||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \
|
||||
else discord.Status.online
|
||||
for shard in ctx.bot.shards:
|
||||
await ctx.bot.change_presence(status=status, game=game)
|
||||
await ctx.bot.change_presence(status=status, activity=game)
|
||||
await ctx.send(_("Game set."))
|
||||
|
||||
@_set.command(name="listening")
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def _listening(self, ctx, *, listening: str=None):
|
||||
"""Sets Red's listening status"""
|
||||
@@ -397,14 +575,14 @@ class Core:
|
||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \
|
||||
else discord.Status.online
|
||||
if listening:
|
||||
listening = discord.Game(name=listening, type=2)
|
||||
activity = discord.Activity(name=listening, type=discord.ActivityType.listening)
|
||||
else:
|
||||
listening = None
|
||||
for shard in ctx.bot.shards:
|
||||
await ctx.bot.change_presence(status=status, game=listening)
|
||||
activity = None
|
||||
await ctx.bot.change_presence(status=status, activity=activity)
|
||||
await ctx.send(_("Listening set."))
|
||||
|
||||
@_set.command(name="watching")
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def _watching(self, ctx, *, watching: str=None):
|
||||
"""Sets Red's watching status"""
|
||||
@@ -412,14 +590,14 @@ class Core:
|
||||
status = ctx.bot.guilds[0].me.status if len(ctx.bot.guilds) > 0 \
|
||||
else discord.Status.online
|
||||
if watching:
|
||||
watching = discord.Game(name=watching, type=3)
|
||||
activity = discord.Activity(name=watching, type=discord.ActivityType.watching)
|
||||
else:
|
||||
watching = None
|
||||
for shard in ctx.bot.shards:
|
||||
await ctx.bot.change_presence(status=status, game=watching)
|
||||
activity = None
|
||||
await ctx.bot.change_presence(status=status, activity=activity)
|
||||
await ctx.send(_("Watching set."))
|
||||
|
||||
@_set.command()
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def status(self, ctx, *, status: str):
|
||||
"""Sets Red's status
|
||||
@@ -438,17 +616,17 @@ class Core:
|
||||
"invisible": discord.Status.invisible
|
||||
}
|
||||
|
||||
game = ctx.bot.guilds[0].me.game if len(ctx.bot.guilds) > 0 else None
|
||||
game = ctx.bot.guilds[0].me.activity if len(ctx.bot.guilds) > 0 else None
|
||||
try:
|
||||
status = statuses[status.lower()]
|
||||
except KeyError:
|
||||
await ctx.send_help()
|
||||
else:
|
||||
for shard in ctx.bot.shards:
|
||||
await ctx.bot.change_presence(status=status, game=game)
|
||||
await ctx.bot.change_presence(status=status, activity=game)
|
||||
await ctx.send(_("Status changed to %s.") % status)
|
||||
|
||||
@_set.command()
|
||||
@checks.bot_in_a_guild()
|
||||
@checks.is_owner()
|
||||
async def stream(self, ctx, streamer=None, *, stream_title=None):
|
||||
"""Sets Red's streaming status
|
||||
@@ -461,15 +639,13 @@ class Core:
|
||||
stream_title = stream_title.strip()
|
||||
if "twitch.tv/" not in streamer:
|
||||
streamer = "https://www.twitch.tv/" + streamer
|
||||
game = discord.Game(type=1, url=streamer, name=stream_title)
|
||||
for shard in ctx.bot.shards:
|
||||
await ctx.bot.change_presence(status=status, game=game)
|
||||
activity = discord.Streaming(url=streamer, name=stream_title)
|
||||
await ctx.bot.change_presence(status=status, activity=activity)
|
||||
elif streamer is not None:
|
||||
await ctx.send_help()
|
||||
return
|
||||
else:
|
||||
for shard in ctx.bot.shards:
|
||||
await ctx.bot.change_presence(game=None, status=status)
|
||||
await ctx.bot.change_presence(activity=None, status=status)
|
||||
await ctx.send(_("Done."))
|
||||
|
||||
@_set.command(name="username", aliases=["name"])
|
||||
@@ -564,6 +740,27 @@ class Core:
|
||||
else:
|
||||
await ctx.send(_("Invalid token."))
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def token(self, ctx, token: str):
|
||||
"""Change bot token."""
|
||||
if not not isinstance(ctx.channel, discord.DMChannel):
|
||||
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
await ctx.send(
|
||||
_("Please use that command in DM. Since users probably saw your token,"
|
||||
" it is recommended to reset it right now. Go to the following link and"
|
||||
" select `Reveal Token` and `Generate a new token?`."
|
||||
"\n\nhttps://discordapp.com/developers/applications/me/{}").format(self.bot.user.id))
|
||||
return
|
||||
|
||||
await ctx.bot.db.token.set(token)
|
||||
await ctx.send("Token set. Restart me.")
|
||||
|
||||
@_set.command()
|
||||
@checks.is_owner()
|
||||
async def locale(self, ctx: commands.Context, locale_name: str):
|
||||
|
||||
@@ -62,10 +62,11 @@ class JSON(BaseDriver):
|
||||
async def clear(self, *identifiers: str):
|
||||
partial = self.data
|
||||
full_identifiers = (self.unique_cog_identifier, *identifiers)
|
||||
try:
|
||||
for i in full_identifiers[:-1]:
|
||||
if i not in partial:
|
||||
break
|
||||
partial = partial[i]
|
||||
else:
|
||||
del partial[identifiers[-1]]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
await self.jsonIO._threadsafe_save_json(self.data)
|
||||
|
||||
@@ -14,10 +14,15 @@ def _initialize(**kwargs):
|
||||
admin_pass = kwargs['PASSWORD']
|
||||
db_name = kwargs.get('DB_NAME', 'default_db')
|
||||
|
||||
if admin_user is not None and admin_pass is not None:
|
||||
url = "mongodb://{}:{}@{}:{}/{}".format(
|
||||
admin_user, admin_pass, host, port,
|
||||
db_name
|
||||
)
|
||||
else:
|
||||
url = "mongodb://{}:{}/{}".format(
|
||||
host, port, db_name
|
||||
)
|
||||
|
||||
global _conn
|
||||
_conn = motor.motor_asyncio.AsyncIOMotorClient(url)
|
||||
|
||||
@@ -2,6 +2,9 @@ import sys
|
||||
import codecs
|
||||
import datetime
|
||||
import logging
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
import aiohttp
|
||||
import pkg_resources
|
||||
import traceback
|
||||
from pkg_resources import DistributionNotFound
|
||||
@@ -106,6 +109,24 @@ 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:
|
||||
data = await r.json()
|
||||
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version):
|
||||
INFO.append(
|
||||
"Outdated version! {} is available "
|
||||
"but you're using {}".format(data["info"]["version"], red_version)
|
||||
)
|
||||
owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id)
|
||||
try:
|
||||
await owner.send(
|
||||
"Your Red instance is out of date! {} is the current "
|
||||
"version, however you are using {}!".format(
|
||||
data["info"]["version"], red_version
|
||||
)
|
||||
)
|
||||
except:
|
||||
pass
|
||||
INFO2 = []
|
||||
|
||||
sentry = await bot.db.enable_sentry()
|
||||
|
||||
@@ -139,7 +139,7 @@ class Help(formatter.HelpFormatter):
|
||||
|
||||
if description:
|
||||
# <description> portion
|
||||
emb['embed']['description'] = description
|
||||
emb['embed']['description'] = description[:2046]
|
||||
|
||||
if isinstance(self.command, discord.ext.commands.core.Command):
|
||||
# <signature portion>
|
||||
@@ -149,11 +149,11 @@ class Help(formatter.HelpFormatter):
|
||||
# <long doc> section
|
||||
if self.command.help:
|
||||
name = '__{0}__'.format(self.command.help.split('\n\n')[0])
|
||||
name_length = len(name) - 4
|
||||
name_length = len(name)
|
||||
value = self.command.help[name_length:].replace('[p]', self.clean_prefix)
|
||||
if value == '':
|
||||
value = EMPTY_STRING
|
||||
field = EmbedField(name, value, False)
|
||||
field = EmbedField(name[:252], value[:1024], False)
|
||||
emb['fields'].append(field)
|
||||
|
||||
# end it here if it's just a regular command
|
||||
|
||||
@@ -180,9 +180,9 @@ class CogI18n:
|
||||
try:
|
||||
|
||||
try:
|
||||
translation_file = locale_path.open('ru')
|
||||
translation_file = locale_path.open('ru', encoding='utf-8')
|
||||
except ValueError: # We are using Windows
|
||||
translation_file = locale_path.open('r')
|
||||
translation_file = locale_path.open('r', encoding='utf-8')
|
||||
self._parse(translation_file)
|
||||
except (IOError, FileNotFoundError): # The translation is unavailable
|
||||
pass
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
__all__ = ['TYPE_CHECKING', 'NewType']
|
||||
__all__ = ['TYPE_CHECKING', 'NewType', 'safe_delete']
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
|
||||
try:
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -10,3 +14,12 @@ try:
|
||||
except ImportError:
|
||||
def NewType(name, tp):
|
||||
return type(name, (tp,), {})
|
||||
|
||||
|
||||
def safe_delete(pth: Path):
|
||||
if pth.exists():
|
||||
for root, dirs, files in os.walk(str(pth)):
|
||||
os.chmod(root, 0o755)
|
||||
for d in dirs:
|
||||
os.chmod(os.path.join(root, d), 0o755)
|
||||
shutil.rmtree(str(pth), ignore_errors=True)
|
||||
|
||||
201
redbot/setup.py
201
redbot/setup.py
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -13,6 +15,7 @@ import appdirs
|
||||
from redbot.core.cli import confirm
|
||||
from redbot.core.data_manager import basic_config_default
|
||||
from redbot.core.json_io import JsonIO
|
||||
from redbot.core.utils import safe_delete
|
||||
|
||||
config_dir = None
|
||||
appdir = appdirs.AppDirs("Red-DiscordBot")
|
||||
@@ -40,6 +43,11 @@ def parse_cli_args():
|
||||
help="Interactively delete an instance",
|
||||
action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--edit", "-e",
|
||||
help="Interactively edit an instance",
|
||||
action="store_true"
|
||||
)
|
||||
return parser.parse_known_args()
|
||||
|
||||
|
||||
@@ -59,12 +67,7 @@ def save_config(name, data, remove=False):
|
||||
JsonIO(config_file)._save_json(config)
|
||||
|
||||
|
||||
def basic_setup():
|
||||
"""
|
||||
Creates the data storage folder.
|
||||
:return:
|
||||
"""
|
||||
|
||||
def get_data_dir():
|
||||
default_data_dir = Path(appdir.user_data_dir)
|
||||
|
||||
print("Hello! Before we begin the full configuration process we need to"
|
||||
@@ -96,10 +99,10 @@ def basic_setup():
|
||||
if not confirm("Please confirm (y/n):"):
|
||||
print("Please start the process over.")
|
||||
sys.exit(0)
|
||||
return default_data_dir
|
||||
|
||||
default_dirs = deepcopy(basic_config_default)
|
||||
default_dirs['DATA_PATH'] = str(default_data_dir.resolve())
|
||||
|
||||
def get_storage_type():
|
||||
storage_dict = {
|
||||
1: "JSON",
|
||||
2: "MongoDB"
|
||||
@@ -118,15 +121,10 @@ def basic_setup():
|
||||
else:
|
||||
if storage not in storage_dict:
|
||||
storage = None
|
||||
return storage
|
||||
|
||||
default_dirs['STORAGE_TYPE'] = storage_dict.get(storage, 1)
|
||||
|
||||
if storage_dict.get(storage, 1) == "MongoDB":
|
||||
from redbot.core.drivers.red_mongo import get_config_details
|
||||
default_dirs['STORAGE_DETAILS'] = get_config_details()
|
||||
else:
|
||||
default_dirs['STORAGE_DETAILS'] = {}
|
||||
|
||||
def get_name():
|
||||
name = ""
|
||||
while len(name) == 0:
|
||||
print()
|
||||
@@ -135,7 +133,35 @@ def basic_setup():
|
||||
name = input("> ")
|
||||
if " " in name:
|
||||
name = ""
|
||||
return name
|
||||
|
||||
|
||||
def basic_setup():
|
||||
"""
|
||||
Creates the data storage folder.
|
||||
:return:
|
||||
"""
|
||||
|
||||
default_data_dir = get_data_dir()
|
||||
|
||||
default_dirs = deepcopy(basic_config_default)
|
||||
default_dirs['DATA_PATH'] = str(default_data_dir.resolve())
|
||||
|
||||
storage = get_storage_type()
|
||||
|
||||
storage_dict = {
|
||||
1: "JSON",
|
||||
2: "MongoDB"
|
||||
}
|
||||
default_dirs['STORAGE_TYPE'] = storage_dict.get(storage, 1)
|
||||
|
||||
if storage_dict.get(storage, 1) == "MongoDB":
|
||||
from redbot.core.drivers.red_mongo import get_config_details
|
||||
default_dirs['STORAGE_DETAILS'] = get_config_details()
|
||||
else:
|
||||
default_dirs['STORAGE_DETAILS'] = {}
|
||||
|
||||
name = get_name()
|
||||
save_config(name, default_dirs)
|
||||
|
||||
print()
|
||||
@@ -143,6 +169,116 @@ def basic_setup():
|
||||
" continue your setup process and to run the bot.")
|
||||
|
||||
|
||||
async def edit_instance():
|
||||
instance_list = load_existing_config()
|
||||
if not instance_list:
|
||||
print("No instances have been set up!")
|
||||
return
|
||||
|
||||
print(
|
||||
"You have chosen to edit an instance. The following "
|
||||
"is a list of instances that currently exist:\n"
|
||||
)
|
||||
for instance in instance_list.keys():
|
||||
print("{}\n".format(instance))
|
||||
print("Please select one of the above by entering its name")
|
||||
selected = input("> ")
|
||||
|
||||
if selected not in instance_list.keys():
|
||||
print("That isn't a valid instance!")
|
||||
return
|
||||
instance_data = instance_list[selected]
|
||||
default_dirs = deepcopy(basic_config_default)
|
||||
|
||||
current_data_dir = Path(instance_data["DATA_PATH"])
|
||||
print(
|
||||
"You have selected '{}' as the instance to modify.".format(selected)
|
||||
)
|
||||
if not confirm("Please confirm (y/n):"):
|
||||
print("Ok, we will not continue then.")
|
||||
return
|
||||
|
||||
print("Ok, we will continue on.")
|
||||
print()
|
||||
if confirm("Would you like to change the instance name? (y/n)"):
|
||||
name = get_name()
|
||||
else:
|
||||
name = selected
|
||||
|
||||
if confirm("Would you like to change the data location? (y/n)"):
|
||||
default_data_dir = get_data_dir()
|
||||
default_dirs["DATA_PATH"] = str(default_data_dir.resolve())
|
||||
else:
|
||||
default_dirs["DATA_PATH"] = str(current_data_dir.resolve())
|
||||
|
||||
if confirm("Would you like to change the storage type? (y/n):"):
|
||||
storage = get_storage_type()
|
||||
|
||||
storage_dict = {
|
||||
1: "JSON",
|
||||
2: "MongoDB"
|
||||
}
|
||||
default_dirs["STORAGE_TYPE"] = storage_dict[storage]
|
||||
if storage_dict.get(storage, 1) == "MongoDB":
|
||||
from redbot.core.drivers.red_mongo import get_config_details, Mongo
|
||||
storage_details = get_config_details()
|
||||
default_dirs["STORAGE_DETAILS"] = storage_details
|
||||
|
||||
if instance_data["STORAGE_TYPE"] == "JSON":
|
||||
if confirm("Would you like to import your data? (y/n)"):
|
||||
core_data_file = list(current_data_dir.glob("core/settings.json"))[0]
|
||||
m = Mongo("Core", **storage_details)
|
||||
with core_data_file.open(mode="r") as f:
|
||||
core_data = json.loads(f.read())
|
||||
m.unique_cog_identifier = 0
|
||||
collection = m.get_collection()
|
||||
await collection.update_one(
|
||||
{'_id': m.unique_cog_identifier},
|
||||
update={"$set": core_data["0"]},
|
||||
upsert=True
|
||||
)
|
||||
for p in current_data_dir.glob("cogs/**/settings.json"):
|
||||
cog_m = Mongo(p.parent.stem, **storage_details)
|
||||
cog_c = cog_m.get_collection()
|
||||
with p.open(mode="r") as f:
|
||||
cog_data = json.loads(f.read())
|
||||
for ident in list(cog_data.keys()):
|
||||
cog_m.unique_cog_identifier = int(ident)
|
||||
await cog_c.update_one(
|
||||
{"_id": cog_m.unique_cog_identifier},
|
||||
update={"$set": cog_data[ident]},
|
||||
upsert=True
|
||||
)
|
||||
else:
|
||||
default_dirs["STORAGE_DETAILS"] = {}
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
from redbot.core.drivers.red_mongo import Mongo
|
||||
from redbot.core.drivers.red_json import JSON
|
||||
m = Mongo("Core", **instance_data["STORAGE_DETAILS"])
|
||||
db = m.db
|
||||
collection_names = await db.collection_names(include_system_collections=False)
|
||||
for c_name in collection_names:
|
||||
if c_name == "Core":
|
||||
c_data_path = current_data_dir / "core"
|
||||
else:
|
||||
c_data_path = current_data_dir / "cogs/{}".format(c_name)
|
||||
output = {}
|
||||
docs = await db[c_name].find().to_list(None)
|
||||
for item in docs:
|
||||
item_id = str(item.pop("_id"))
|
||||
output[item_id] = item
|
||||
target = JSON(c_name, data_path_override=c_data_path)
|
||||
await target.jsonIO._threadsafe_save_json(output)
|
||||
|
||||
if name != selected:
|
||||
save_config(selected, {}, remove=True)
|
||||
save_config(name, default_dirs)
|
||||
|
||||
print(
|
||||
"Your basic configuration has been edited"
|
||||
)
|
||||
|
||||
|
||||
def remove_instance():
|
||||
instance_list = load_existing_config()
|
||||
if not instance_list:
|
||||
@@ -162,12 +298,8 @@ def remove_instance():
|
||||
print("That isn't a valid instance!")
|
||||
return
|
||||
instance_data = instance_list[selected]
|
||||
print(
|
||||
"Would you like to make a backup of "
|
||||
"the data for this instance (y/n)?"
|
||||
)
|
||||
yesno = input("> ")
|
||||
if yesno.lower() == "y":
|
||||
|
||||
if confirm("Would you like to make a backup of the data for this instance? (y/n)"):
|
||||
if instance_data["STORAGE_TYPE"] == "MongoDB":
|
||||
raise NotImplementedError(
|
||||
"Support for removing instances with MongoDB as the storage "
|
||||
@@ -176,9 +308,10 @@ def remove_instance():
|
||||
else:
|
||||
print("Backing up the instance's data...")
|
||||
backup_filename = "redv3-{}-{}.tar.gz".format(
|
||||
selected, dt.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
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
|
||||
@@ -190,25 +323,16 @@ def remove_instance():
|
||||
)
|
||||
)
|
||||
print("Removing the instance...")
|
||||
try:
|
||||
shutil.rmtree(str(pth))
|
||||
except FileNotFoundError:
|
||||
pass # data dir was removed manually
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed")
|
||||
return
|
||||
elif yesno.lower() == "n":
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
print("Removing the instance...")
|
||||
try:
|
||||
shutil.rmtree(str(pth))
|
||||
except FileNotFoundError:
|
||||
pass # data dir was removed manually
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed")
|
||||
return
|
||||
else:
|
||||
print("That's not a valid option!")
|
||||
pth = Path(instance_data["DATA_PATH"])
|
||||
print("Removing the instance...")
|
||||
safe_delete(pth)
|
||||
save_config(selected, {}, remove=True)
|
||||
print("The instance has been removed")
|
||||
return
|
||||
|
||||
|
||||
@@ -218,6 +342,9 @@ def main():
|
||||
remove_instance()
|
||||
except NotImplementedError as e:
|
||||
print(str(e))
|
||||
elif args.edit:
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(edit_instance())
|
||||
else:
|
||||
basic_setup()
|
||||
|
||||
|
||||
14
setup.py
14
setup.py
@@ -23,9 +23,12 @@ def get_package_list():
|
||||
def get_requirements():
|
||||
with open('requirements.txt') as f:
|
||||
requirements = f.read().splitlines()
|
||||
if IS_TRAVIS and not IS_DEPLOYING:
|
||||
try:
|
||||
requirements.remove('git+https://github.com/Rapptz/discord.py.git@rewrite#egg=discord.py[voice]')
|
||||
else:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if IS_DEPLOYING or not IS_TRAVIS:
|
||||
requirements.append('discord.py>=1.0.0a0') # Because RTD
|
||||
if sys.platform.startswith("linux"):
|
||||
requirements.append("distro")
|
||||
@@ -90,9 +93,10 @@ def find_locale_folders():
|
||||
|
||||
setup(
|
||||
name='Red-DiscordBot',
|
||||
version="{}.{}.{}b9".format(*get_version()),
|
||||
version="{}.{}.{}b10".format(*get_version()),
|
||||
packages=get_package_list(),
|
||||
package_data=find_locale_folders(),
|
||||
include_package_data=True,
|
||||
url='https://github.com/Cog-Creators/Red-DiscordBot',
|
||||
license='GPLv3',
|
||||
author='Cog-Creators',
|
||||
@@ -122,10 +126,10 @@ setup(
|
||||
dependency_links=dep_links,
|
||||
extras_require={
|
||||
'test': [
|
||||
'pytest>=3', 'pytest-asyncio'
|
||||
'pytest>3', 'pytest-asyncio'
|
||||
],
|
||||
'mongo': ['motor'],
|
||||
'docs': ['sphinx', 'sphinxcontrib-asyncio', 'sphinx_rtd_theme'],
|
||||
'voice': ['PyNaCl']
|
||||
'voice': ['red-lavalink>=0.0.4']
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user