Compare commits

...

38 Commits

Author SHA1 Message Date
palmtree5
c4b641e62a [V3] Bump version to 3.0.0b10 (#1418) 2018-03-20 16:36:32 -08:00
James
153d710eb4 [V3] Allow load, unload and reload to deal with multiple packages (#1441)
* [V3] Allow load, unload and reload to deal with multiple packages

This PR allows multiple packages to be loaded, unloaded or reloaded with the one command, the packages are delimited by the space character as suggested by Will
This is just the functionality, I'm sure the code could be better

* introduced helper function for getting package load strings

* missed characters

* forgotten import 👀

Forgot to include the import `inline` method from chat_formatting
2018-03-20 16:32:54 -08:00
palmtree5
83471e0866 [V3 Core] implement commands and settings for embeds (#1437)
* [V3 Core] add settings for whether to use embeds or not

* Implement commands to toggle embed settings

* Add a function to context for finding whether to use embeds or not

* Hide [p]embedset for now

* Move embed_requested to bot

* Add a simple helper in context
2018-03-21 01:17:40 +01:00
Michael H
01b9843883 [V3] Change presence (continued fixes) (#1438)
* This better fixes the root issue rather than attempting to work around it

* move bot_in_guild to checks, and use the correct syntax
2018-03-21 01:12:40 +01:00
retke
749af89e9f [V3] Set token command (#1425)
* [V3] Add set token command

* DM check

* warning message + delete if token isn't send in DM

* Update string formatting
2018-03-21 01:08:36 +01:00
palmtree5
27b61a2770 [V3 Streams] Fix community alerts (#1426)
* [V3 Streams] fix community alerts

* drop unneeded parentheses + delete messages when removing from cache

* move to one embed for a community

* fix adding image to embed

* Remove unneeded params

* Really helps to point to the right place

* drop the game, move channel name+link to value

* Use stream title in embeds

* Increase number of streams to grab to 100

* filter out streams with existing individual alerts in the channel

* need channel id, not stream id in filtering function

* sample from the list

* we only want to break out if the alert is a TwitchStream alert

* drop off sending the mentions if there are none
2018-03-20 19:25:23 -04:00
palmtree5
eb3b6346bb [V3] Set r/w access before deleting files (#1412)
* [V3] set access before deleting

* [V3] move+rename do_delete and use in repo removal in downloader
2018-03-20 18:46:15 -04:00
aikaterna
2e9a0de4a1 [V3 Audio] Update audio to use Red-Lavalink (#1440)
* [V3 Audio] Update audio for Red-Lavalink

* Update requirements for new library

* Fix logic for connect time storage for audiostats

Only store time when connecting to the voice channel.
2018-03-20 23:28:31 +01:00
palmtree5
f83e3cc3e7 [V3 Economy] Expand payday output (#1386)
* [V3 Economy] implement suggestions from #1371

* Fix a typo

* Add functions for getting leaderboard and leaderboard position

* Use the new functions to get leaderboard position and leaderboard (overrides https://github.com/Cog-Creators/Red-DiscordBot/pull/1435)

* Actually implement showing only guild members on leaderboard when bank is global

* get_leaderboard_position needs to be awaited

* For global bank, pass None for guild to get_leaderboard when trying to find position

* Remove some unneeded code

* Wrong index...

* Combine 3 messages into 1

* Fix guild leaderboard while bank is global

* add missing parentheses

* Modify the leaderboard formatting

* More work on leaderboard formatting

* no subtraction
2018-03-20 18:22:10 -04:00
palmtree5
a8f4659552 [V3 Instance Setup] Storage swapping (#1421)
* [V3 Instance Setup] start work on storage swapping

* This should do the trick for Mongo -> JSON

* Fix typo

* Fix a few more typos

* resolve the data path

* Upsert the imported data

* need a list of the documents

* to_list is a coro
2018-03-16 13:37:12 -08:00
Will
25a5c3dec9 Pip install to library dir (#1432) 2018-03-16 16:12:05 +01:00
Wyn
c49cb4a213 [V3 Readme] Update Patreon link (#1434)
Add on to #1431
2018-03-14 19:22:50 -04:00
palmtree5
c4dbbc2d1e [V3 Warnings] Action list isn't a dict (fix #1382) (#1408) 2018-03-14 19:10:07 -04:00
palmtree5
fe3d6f57af [V3 Streams] Add support for Youtube streams (#1385) 2018-03-14 19:07:14 -04:00
palmtree5
052af2f9bf [V3 Cleanup] Code consolidation (#1347)
* [V3 Cleanup] refactor to consolidate duplicate code

* [V3 Cleanup] make the prompting function a staticmethod

* Drop continuing message + add punctuation to docstrings
2018-03-14 19:03:11 -04:00
Adam
16da9f52ac [V3 Audio] update Lavalink build (#1427) 2018-03-12 21:48:13 -08:00
palmtree5
22a342d36d [V3 Bank/Economy] Fix #1404 and an issue with [p]bank reset (#1407)
* [V3 Bank] fix an issue with checks

* [V3 Economy] fix issues with [p]bank reset
2018-03-12 19:10:46 -04:00
palmtree5
4fcf32b5e9 [V3 Bank] Add confirmation prompt on [p]bankset toggleglobal (#1402) 2018-03-12 19:05:21 -04:00
palmtree5
5bdb455bc0 [V3 Instance setup] Change backup time separator for Windows (#1419) 2018-03-12 18:48:13 -04:00
BaIthamel
1cb74f0ea7 [V3] Fix for issue #1363 (#1424)
[p]cogs and [p]repo now print alphabetically
2018-03-12 18:44:30 -04:00
Tobotimus
c7e8c95640 [V3] discord.Game -> discord.Activity (#1397)
* [V3] discord.Game -> discord.Activity

* Update userinfo to reference new enum
2018-03-12 18:41:37 -04:00
Will
ccb322d08e [Audio] V3/auto autostart only (#1420)
* Download jar at audio load

* Messy...

* Remove leftover log file stuff

* Keep application.yml

* Damn you windows
2018-03-12 00:49:08 +01:00
Wyn
b27e0f2d21 [V3 Audio] Rename Game to Activity (#1409)
* Rename `Game` to `Activity` for audio

Add on to pull #1397

* Update audio.py

Whoops, don't need this here

* Update audio.py

Now fixed
2018-03-08 20:55:03 -05:00
aikaterna
6a715d87dd [V3 Audio] Add prev command, bugfixes for audiostats, search (#1405)
* Update audiostats to display days in timer

* Add prev command

* Update search button to play enqueued song
2018-03-08 20:50:54 -05:00
Tobotimus
6138b78c07 [V3 Admin] Make [p]announce consume all args into the message (#1394)
* Fix announce (resolves #1390)

* Make other commands consume all args

Also fixes [p]announce channel
2018-03-08 20:45:41 -05:00
palmtree5
f84ef48819 [V3 Docs] add java install to install docs (#1389) 2018-03-08 20:06:27 +01:00
palmtree5
cda27944b6 [V3 Mod] fix #1401 (#1403) 2018-03-07 09:48:22 +11:00
Will
c9281f734b [V3 Config] Fix clear throwing errors (#1374) 2018-03-06 10:28:37 +11:00
palmtree5
f378ea0d2e [V3 Core] add update check (#1388)
* [V3 Core] add update check

* [V3 Core] have it DM the owner if out of date
2018-03-06 10:21:01 +11:00
James
40c37b5c06 [V3 Help][ fix help_formatter to obey length limits (#1375)
Help didn't account for docstrings passing length limits which I noticed a while ago and as noticed again when Palm forgot a dual newline in a command docstring.
This PR sees to fix this by enforcing length limits on description, field names and field values
2018-03-06 10:10:56 +11:00
Will
57b7db6956 [V3 Admin] Remove guild default channel (#1381)
* Remove guild default channel

* Fix weird set thing
2018-03-06 09:52:26 +11:00
Michael H
b4f5c2c0a1 Revert "[V3]Encoding issue fix" (#1392)
* Revert "[V3 Core] Encoding issue fix (#1365)"

This reverts commit f6903cf582.

* Don't let the system encoding screw with things,
specify opens as happening with utf-8 encoding

(cherry picked from commit c10e4dddca)
2018-03-06 08:18:41 +11:00
Michael H
f6903cf582 [V3 Core] Encoding issue fix (#1365)
* Don't let the system encoding screw with things,
specify opens as happening with utf-8 encoding

* And also deal with encoding issues because windows is a special snowflake
(see: #1366)

* let's just use the encoding param in str() rather than encode/decode...
2018-03-04 13:02:04 -05:00
aikaterna
3816385228 [V3 Audio] Fix repeat, Message deletions (#1379)
* Fix repeat

Also remove restriction on if the player is playing.

* Auto-delete existing Now Playing message

* Auto-delete existing notify message

* Wrap line on Now Playing message

* Add connected duration to audiostats

_dynamic_time based on a function written by Redjumpman

* Return negative seek past song start as 00:00:00

* Version number
2018-03-03 18:55:24 -05:00
Will
f65085946c [V3 Audio] Initial V3 addition (#1373)
* Initial audio

* Add application data and modify port

* Modify codeowners

* Need extra newline

* add yml to manifest

* lock lavalink version
2018-03-03 15:42:20 +11:00
Thomas Mercurio
b10b746d9e [V3 Config] Correct Mongo connection URI without credentials (#1362)
* Correct connection string without credentials

* Remove extra whitespace.
2018-03-02 18:39:45 +01:00
James
ed5945e182 fixed exception (#1372)
`requirements.remove` will raise a ValueError if the item is not there, not `IndexError`
2018-03-02 18:35:28 +01:00
Will
cf48a13fc7 Potentially fix auto deploy issues (#1357) 2018-02-28 13:08:47 +01:00
43 changed files with 2045 additions and 594 deletions

2
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,5 @@
include README.rst
include LICENSE
include requirements.txt
include discord/bin/*.dll
include discord/bin/*.dll
include redbot/cogs/audio/application.yml

View File

@@ -9,6 +9,10 @@
.. image:: https://d322cqt584bo4o.cloudfront.net/red-discordbot/localized.svg
: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

View File

@@ -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 -)"
--------------

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
--------------

View File

@@ -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.
"""

View File

@@ -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

View File

@@ -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())

View 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

View File

@@ -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:
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:
await ctx.send(_("I'm not even connected to a voice channel!"), delete_after=2)
await ctx.message.delete()
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()

View 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()

View File

@@ -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,16 +64,23 @@ 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")
await ctx.send(_("The bank is now {}.").format(word))
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")
@check_global_setting_guildowner()

View File

@@ -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)

View File

@@ -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):

View File

@@ -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]

View File

@@ -216,18 +216,9 @@ 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 ctx.send(_("All bank accounts of this guild have been "
"deleted."))
await bank.wipe_bank()
await ctx.send(_("All bank accounts of this guild have been "
"deleted."))
@commands.command()
@guild_only_check()
@@ -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"))

View File

@@ -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)

View File

@@ -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

View File

@@ -14,7 +14,11 @@ class APIError(StreamsError):
pass
class InvalidCredentials(StreamsError):
class InvalidTwitchCredentials(StreamsError):
pass
class InvalidYoutubeCredentials(StreamsError):
pass

View File

@@ -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)
stream = _class(name=channel_name,
token=token)
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 "
"channel is streaming to the {} community"
"".format(community.name))
await ctx.send(_("I'll send a notification in this channel when a "
"channel is streaming to the {} community"
"").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 "
"to the {} community in this channel anymore"
"".format(community.name))
await ctx.send(_("I won't send notifications about channels streaming "
"to the {} community in this channel anymore"
"").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)
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:
for channel in community.channels:
chn = self.bot.get_channel(channel)
await chn.send(embed=emb)
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 = []

View File

@@ -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()

View File

@@ -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:

View File

@@ -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:

View File

@@ -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"]

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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,71 +312,155 @@ class Core:
@commands.command()
@checks.is_owner()
async def load(self, ctx, *, cog_name: str):
"""Loads a package"""
try:
spec = await ctx.bot.cog_mgr.find_cog(cog_name)
except RuntimeError:
await ctx.send(_("No module by that name was found in any"
" cog path."))
"""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(c)
cogspecs.append((spec, c))
except RuntimeError:
notfound_packages.append(inline(c))
#await ctx.send(_("No module named '{}' was found in any"
# " cog path.").format(c))
if len(cogspecs) == 0:
return
try:
await ctx.bot.load_extension(spec)
except Exception as e:
log.exception("Package loading failed", exc_info=e)
for spec, name in cogspecs:
try:
await ctx.bot.load_extension(spec)
except Exception as e:
log.exception("Package loading failed", exc_info=e)
exception_log = ("Exception in command '{}'\n"
"".format(ctx.command.qualified_name))
exception_log += "".join(traceback.format_exception(type(e),
e, e.__traceback__))
self.bot._last_exception = exception_log
exception_log = ("Exception in command '{}'\n"
"".format(ctx.command.qualified_name))
exception_log += "".join(traceback.format_exception(type(e),
e, e.__traceback__))
self.bot._last_exception = exception_log
failed_packages.append(inline(name))
else:
await ctx.bot.add_loaded_package(name)
loaded_packages.append(inline(name))
await ctx.send(_("Failed to load package. Check your console or "
"logs for details."))
else:
await ctx.bot.add_loaded_package(cog_name)
await ctx.send(_("Done."))
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."))
else:
await ctx.send(_("That extension is not loaded."))
"""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:
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"""
try:
spec = await ctx.bot.cog_mgr.find_cog(cog_name)
except RuntimeError:
await ctx.send(_("No module by that name was found in any"
" cog path."))
return
cognames = [c.strip() for c in cog_name.split(' ')]
for c in cognames:
ctx.bot.unload_extension(c)
try:
self.cleanup_and_refresh_modules(spec.name)
await ctx.bot.load_extension(spec)
except Exception as e:
log.exception("Package reloading failed", exc_info=e)
cogspecs = []
failed_packages = []
loaded_packages = []
notfound_packages = []
exception_log = ("Exception in command '{}'\n"
"".format(ctx.command.qualified_name))
exception_log += "".join(traceback.format_exception(type(e),
e, e.__traceback__))
self.bot._last_exception = exception_log
for c in cognames:
try:
spec = await ctx.bot.cog_mgr.find_cog(c)
cogspecs.append((spec, c))
except RuntimeError:
notfound_packages.append(inline(c))
await ctx.send(_("Failed to reload package. Check your console or "
"logs for details."))
else:
await ctx.send(_("Done."))
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)
exception_log = ("Exception in command '{}'\n"
"".format(ctx.command.qualified_name))
exception_log += "".join(traceback.format_exception(type(e),
e, e.__traceback__))
self.bot._last_exception = exception_log
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"])
@@ -563,6 +739,27 @@ class Core:
await ctx.send(_("You have been set as owner."))
else:
await ctx.send(_("Invalid token."))
@_set.command()
@checks.is_owner()
async def token(self, ctx, token: str):
"""Change bot token."""
if not 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()

View File

@@ -62,10 +62,11 @@ class JSON(BaseDriver):
async def clear(self, *identifiers: str):
partial = self.data
full_identifiers = (self.unique_cog_identifier, *identifiers)
for i in full_identifiers[:-1]:
if i not in partial:
break
partial = partial[i]
else:
try:
for i in full_identifiers[:-1]:
partial = partial[i]
del partial[identifiers[-1]]
await self.jsonIO._threadsafe_save_json(self.data)
except KeyError:
pass
else:
await self.jsonIO._threadsafe_save_json(self.data)

View File

@@ -14,10 +14,15 @@ def _initialize(**kwargs):
admin_pass = kwargs['PASSWORD']
db_name = kwargs.get('DB_NAME', 'default_db')
url = "mongodb://{}:{}@{}:{}/{}".format(
admin_user, admin_pass, host, port,
db_name
)
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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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,40 +308,32 @@ 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"])
home = pth.home()
backup_file = home / backup_filename
os.chdir(str(pth.parent)) # str is used here because 3.5 support
with tarfile.open(str(backup_file), "w:gz") as tar:
tar.add(pth.stem) # add all files in that directory
print(
"A backup of {} has been made. It is at {}".format(
selected, backup_file
if pth.exists():
home = pth.home()
backup_file = home / backup_filename
os.chdir(str(pth.parent)) # str is used here because 3.5 support
with tarfile.open(str(backup_file), "w:gz") as tar:
tar.add(pth.stem) # add all files in that directory
print(
"A backup of {} has been made. It is at {}".format(
selected, backup_file
)
)
)
print("Removing the instance...")
try:
shutil.rmtree(str(pth))
except FileNotFoundError:
pass # data dir was removed manually
print("Removing the instance...")
safe_delete(pth)
save_config(selected, {}, remove=True)
print("The instance has been removed")
return
elif yesno.lower() == "n":
else:
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!")
return
def main():
@@ -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()

View File

@@ -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']
}
)