@@ -21,9 +21,12 @@ | |||
- `deepfry` command now works all the time. | |||
- Pride filter filenames fixed. | |||
- As many spelling mistakes as possible. | |||
- If player can't find a thumbnail for the now playing embed, it will just not have one; Instead of breaking. | |||
- If music bot can't find a thumbnail for the now playing embed, it will just not have one; Instead of breaking. | |||
- Fixed bug where nowplaying embeds would have the wrong queued_by value. | |||
- NSFW commands fixed. | |||
- Roxbot can now fully function in DMs. Before, she would break. DM's have less commands. | |||
- `;subreddit`'s "subscriptable" error has been fixed. | |||
- is_anal value can be changed again and check now works. | |||
## v1.8.0 | |||
#### New Features |
@@ -25,7 +25,9 @@ SOFTWARE. | |||
""" | |||
import discord | |||
from discord.ext import commands | |||
import roxbot | |||
from roxbot import guild_settings as gs | |||
@@ -35,6 +37,8 @@ def is_owner_or_admin(): | |||
def predicate(ctx): | |||
if ctx.author.id == roxbot.owner: | |||
return True | |||
elif isinstance(ctx.channel, discord.DMChannel): | |||
return False | |||
else: | |||
for role in ctx.author.roles: | |||
if role.id in gs.get(ctx.guild).perm_roles["admin"]: | |||
@@ -46,6 +50,8 @@ def is_owner_or_admin(): | |||
def _is_admin_or_mod(ctx): | |||
if ctx.message.author.id == roxbot.owner: | |||
return True | |||
elif isinstance(ctx.channel, discord.DMChannel): | |||
return False | |||
else: | |||
admin_roles = gs.get(ctx.guild).perm_roles["admin"] | |||
mod_roles = gs.get(ctx.guild).perm_roles["mod"] | |||
@@ -60,6 +66,8 @@ def is_admin_or_mod(): | |||
def nsfw_predicate(ctx): | |||
if isinstance(ctx.channel, discord.DMChannel): | |||
return False | |||
nsfw = gs.get(ctx.guild).nsfw | |||
if not nsfw["channels"] and nsfw["enabled"]: | |||
return nsfw["enabled"] == 1 | |||
@@ -74,4 +82,12 @@ def is_nfsw_enabled(): | |||
def isnt_anal(): | |||
return commands.check(lambda ctx: gs.get(ctx.guild).is_anal["y/n"] and nsfw_predicate(ctx) or not gs.get(ctx.guild).is_anal["y/n"]) | |||
def predicate(ctx): | |||
if isinstance(ctx.channel, discord.DMChannel): | |||
return False | |||
anal = gs.get(ctx.guild).is_anal["y/n"] | |||
if not anal or (nsfw_predicate(ctx) and gs.get(ctx.guild).is_anal["y/n"]): | |||
return True | |||
else: | |||
return False | |||
return commands.check(predicate) |
@@ -75,8 +75,8 @@ class Admin: | |||
else: | |||
pass | |||
@roxbot.checks.is_admin_or_mod() | |||
@commands.guild_only() | |||
@roxbot.checks.is_admin_or_mod() | |||
@commands.bot_has_permissions(manage_messages=True) | |||
@commands.command() | |||
async def slowmode(self, ctx, seconds): | |||
@@ -120,6 +120,7 @@ class Admin: | |||
messages = await ctx.channel.purge(limit=limit, check=predicate) | |||
return await ctx.send("{} message(s) purged from chat.".format(len(messages))) | |||
@commands.guild_only() | |||
@roxbot.checks.is_admin_or_mod() | |||
@commands.group(case_insensitive=True) | |||
async def warn(self, ctx): | |||
@@ -250,6 +251,7 @@ class Admin: | |||
settings.update(settings.warnings, "warnings") | |||
return await ctx.send("Purged {} banned users from the warn list.".format(count)) | |||
@commands.guild_only() | |||
@commands.has_permissions(kick_members=True) | |||
@commands.bot_has_permissions(kick_members=True) | |||
@commands.command() | |||
@@ -261,6 +263,7 @@ class Admin: | |||
except discord.Forbidden: | |||
return await ctx.send("I can't kick the owner or users higher or equal to me.") | |||
@commands.guild_only() | |||
@commands.has_permissions(ban_members=True) | |||
@commands.bot_has_permissions(ban_members=True) | |||
@commands.command() | |||
@@ -272,6 +275,7 @@ class Admin: | |||
except discord.Forbidden: | |||
return await ctx.send("I can't kick the owner or users higher or equal to me.") | |||
@commands.guild_only() | |||
@commands.has_permissions(ban_members=True) | |||
@commands.bot_has_permissions(ban_members=True) | |||
@commands.command() |
@@ -122,6 +122,7 @@ class CustomCommands: | |||
command_output = self._get_output(settings.custom_commands["0"][command]) | |||
return await channel.send(command_output) | |||
@commands.guild_only() | |||
@commands.group(aliases=["cc"]) | |||
@roxbot.checks.is_owner_or_admin() | |||
async def custom(self, ctx): |
@@ -278,7 +278,9 @@ class Fun: | |||
@commands.command(aliases=["cf"]) | |||
async def coinflip(self, ctx): | |||
"""Flip a coin""" | |||
""" | |||
Flip a coin | |||
""" | |||
return await ctx.send("The coin landed on {}!".format(random.choice(["heads", "tails"]))) | |||
@commands.command() | |||
@@ -429,6 +431,12 @@ class Fun: | |||
@commands.command() | |||
async def zalgo(self, ctx, *, text): | |||
""" | |||
Returns Z͇͇͋Á͇͇L͇͔͇G̛͇͇O͇͇͜ | |||
:param ctx: | |||
:param text: | |||
:return: | |||
""" | |||
intensity = 10 | |||
zalgo_chars = [*[chr(i) for i in range(0x0300, 0x036F + 1)], *[u'\u0488', u'\u0489']] | |||
zalgoised = [] |
@@ -26,6 +26,7 @@ SOFTWARE. | |||
import random | |||
import discord | |||
from discord.ext import commands | |||
import roxbot | |||
@@ -43,8 +44,6 @@ class NFSW(): | |||
def __init__(self, bot_client): | |||
self.bot = bot_client | |||
self.cache = {} | |||
for guild in self.bot.guilds: | |||
self.cache[guild.id] = [] | |||
@roxbot.checks.is_nfsw_enabled() | |||
@commands.command(hidden=True) | |||
@@ -53,6 +52,13 @@ class NFSW(): | |||
tags = tags + tag_blacklist(ctx.guild) | |||
page = random.randrange(20) | |||
url = base_url + tags + '&limit=' + str(limit) + '%pid=' + str(page) | |||
if isinstance(ctx.channel, discord.DMChannel): | |||
cache_id = ctx.author.id | |||
else: | |||
cache_id = ctx.guild.id | |||
# IF ID is not in cache, create cache for ID | |||
if not self.cache.get(cache_id, False): | |||
self.cache[cache_id] = [] | |||
posts = await roxbot.http.api_request(url) | |||
@@ -64,10 +70,10 @@ class NFSW(): | |||
while counter < 20: | |||
post = random.choice(posts) | |||
md5 = post.get("md5") or post.get("hash") | |||
if md5 not in self.cache[ctx.guild.id]: | |||
self.cache[ctx.guild.id].append(md5) | |||
if len(self.cache[ctx.guild.id]) > 10: | |||
self.cache[ctx.guild.id].pop(0) | |||
if md5 not in self.cache[cache_id]: | |||
self.cache[cache_id].append(md5) | |||
if len(self.cache[cache_id]) > 10: | |||
self.cache[cache_id].pop(0) | |||
break | |||
counter += 1 | |||
@@ -26,17 +26,23 @@ SOFTWARE. | |||
import random | |||
import roxbot | |||
from html import unescape | |||
from bs4 import BeautifulSoup | |||
from roxbot import guild_settings | |||
import discord | |||
from discord.ext import commands | |||
import roxbot | |||
from roxbot import guild_settings | |||
async def _imgur_removed(url): | |||
page = await roxbot.http.get_page(url) | |||
soup = BeautifulSoup(page, 'html.parser') | |||
return bool("removed.png" in soup.img["src"]) | |||
try: | |||
return bool("removed.png" in soup.img["src"]) | |||
except TypeError: # This should protect roxbot in case bs4 returns nothing. | |||
return False | |||
async def imgur_get(url): | |||
@@ -96,8 +102,6 @@ class Reddit: | |||
def __init__(self, bot_client): | |||
self.bot = bot_client | |||
self.post_cache = {} | |||
for guild in self.bot.guilds: | |||
self.post_cache[guild.id] = [("", "")] | |||
@commands.command() | |||
@commands.has_permissions(add_reactions=True) | |||
@@ -112,6 +116,14 @@ class Reddit: | |||
links = await subreddit_request(subreddit) | |||
title = "" | |||
choice = {} | |||
if isinstance(ctx.channel, discord.DMChannel): | |||
cache_id = ctx.author.id | |||
else: | |||
cache_id = ctx.guild.id | |||
cache_amount = 10 | |||
# IF ID is not in cache, create cache for ID | |||
if not self.post_cache.get(cache_id, False): | |||
self.post_cache[cache_id] = [("", "")] | |||
if not links or not links.get("after") or links["children"][0]["kind"] == "t5": # Determine if response is valid | |||
return await ctx.send("Error ;-; That subreddit probably doesn't exist. Please check your spelling") | |||
@@ -126,7 +138,7 @@ class Reddit: | |||
if url: | |||
x_old = int(x) | |||
# If the url or id are in the cache, continue the loop. If not, proceed with the post. | |||
for cache in self.post_cache[ctx.guild.id]: | |||
for cache in self.post_cache[cache_id]: | |||
if url in cache or choice["id"] in cache: | |||
x += 1 | |||
break | |||
@@ -142,24 +154,25 @@ class Reddit: | |||
x += 1 | |||
# Check if post is NSFW, and if it is and this channel doesn't past the NSFW check, then return with the error message. | |||
if choice["over_18"] and not roxbot.checks.nsfw_predicate(ctx): | |||
if (choice["over_18"] and not roxbot.checks.nsfw_predicate(ctx)) and isinstance(ctx.channel, discord.TextChannel): | |||
return await ctx.send("This server/channel doesn't have my NSFW stuff enabled. This extends to posting NFSW content from Reddit.") | |||
if not url: # If no image posts could be found with the for loop. | |||
return await ctx.send("I couldn't find any images from that subreddit.") | |||
# Put the post ID and url in the cache. The url is used in case of two posts having the same link. Handy to avoid crossposts getting through the cache. | |||
cache_amount = 10 | |||
# CACHE SECTION | |||
# Put the post ID and url in the cache. The url is used in case of two posts having the same link to avoid crossposts getting through the cache. | |||
post = (choice["id"], url) | |||
self.post_cache[ctx.guild.id].append(post) | |||
if len(self.post_cache[ctx.guild.id]) >= cache_amount: | |||
self.post_cache[ctx.guild.id].pop(0) | |||
self.post_cache[cache_id].append(post) | |||
if len(self.post_cache[cache_id]) >= cache_amount: | |||
self.post_cache[cache_id].pop(0) | |||
if url.split("/")[-2] == "a": | |||
text = "This is an album, click on the link to see more.\n" | |||
else: | |||
text = "" | |||
if ctx.invoked_with == "subreddit": | |||
if ctx.invoked_with == "subreddit" and isinstance(ctx.channel, discord.TextChannel): | |||
# Only log the command when it is this command being used. Not the inbuilt commands. | |||
logging = guild_settings.get(ctx.guild).logging | |||
log_channel = self.bot.get_channel(logging["channel"]) |
@@ -44,6 +44,7 @@ class SelfAssign(): | |||
sa["roles"].remove(role.id) | |||
return settings.update(sa, "self_assign") | |||
@commands.guild_only() | |||
@commands.command(pass_context=True) | |||
async def listroles(self, ctx): | |||
""" | |||
@@ -64,6 +65,7 @@ class SelfAssign(): | |||
embed = discord.Embed(colour=roxbot.EmbedColours.pink, description="The self-assignable roles for this server are: \n"+roles) | |||
return await ctx.send(embed=embed) | |||
@commands.guild_only() | |||
@commands.command(pass_context=True) | |||
async def iam(self, ctx, *, role: discord.Role = None): | |||
""" | |||
@@ -95,6 +97,7 @@ class SelfAssign(): | |||
else: | |||
return await ctx.send("That role is not self-assignable.") | |||
@commands.guild_only() | |||
@commands.command(pass_context=True) | |||
async def iamn(self, ctx, *, role: discord.Role = None): | |||
""" |
@@ -297,6 +297,7 @@ class Trivia: | |||
# Commands | |||
@commands.guild_only() | |||
@commands.group(aliases=["tr"], case_insensitive=True) | |||
async def trivia(self, ctx): | |||
"""Command group for the Roxbot Trivia game.""" |
@@ -228,6 +228,7 @@ class Util(): | |||
@commands.command() | |||
async def lookup(self, ctx, ID: int): | |||
# TODO: Finish this | |||
result = self.bot.get_channel(ID) | |||
pass | |||
@@ -204,6 +204,7 @@ class Voice: | |||
return embed | |||
@roxbot.checks.is_admin_or_mod() | |||
@commands.guild_only() | |||
@commands.command() | |||
async def join(self, ctx, *, channel: discord.VoiceChannel = None): | |||
"""Joins the voice channel your in.""" | |||
@@ -221,6 +222,7 @@ class Voice: | |||
await channel.connect() | |||
return await ctx.send("Joined {0.name} :ok_hand:".format(channel)) | |||
@commands.guild_only() | |||
@commands.cooldown(1, 0.5, commands.BucketType.guild) | |||
@commands.command(aliases=["yt"]) | |||
async def play(self, ctx, *, url, stream=False, from_queue=False, queued_by=None): | |||
@@ -285,6 +287,7 @@ class Voice: | |||
embed = discord.Embed(description='Added "{}" to queue'.format(video.get("title")), colour=roxbot.EmbedColours.pink) | |||
await ctx.send(embed=embed) | |||
@commands.guild_only() | |||
@commands.cooldown(1, 0.5, commands.BucketType.guild) | |||
@commands.command() | |||
async def stream(self, ctx, *, url): | |||
@@ -303,6 +306,7 @@ class Voice: | |||
raise commands.CommandError("Roxbot is not connected to a voice channel and couldn't auto-join a voice channel.") | |||
@volume_perms() | |||
@commands.guild_only() | |||
@commands.command() | |||
async def volume(self, ctx, volume): | |||
"""Changes the player's volume. Only accepts integers representing x% between 0-100% or "show", which will show the current volume.""" | |||
@@ -326,6 +330,7 @@ class Voice: | |||
raise commands.CommandError("Volume needs to be between 0-100%") | |||
return await ctx.send("Changed volume to {}%".format(volume)) | |||
@commands.guild_only() | |||
@commands.command() | |||
async def pause(self, ctx): | |||
"""Pauses the current video, if playing.""" | |||
@@ -340,6 +345,7 @@ class Voice: | |||
ctx.voice_client.pause() | |||
return await ctx.send("Paused '{}'".format(ctx.voice_client.source.title)) | |||
@commands.guild_only() | |||
@commands.command() | |||
async def resume(self, ctx): | |||
"""Resumes the bot if paused. Also will play the next thing in the queue if the bot is stuck.""" | |||
@@ -359,6 +365,7 @@ class Voice: | |||
else: | |||
return await ctx.send("Nothing to resume.") | |||
@commands.guild_only() | |||
@commands.command() | |||
async def skip(self, ctx, option=""): | |||
"""Skips or votes to skip the current video. Use option "--force" if your an admin and """ | |||
@@ -388,6 +395,7 @@ class Voice: | |||
else: | |||
await ctx.send("I'm not playing anything.") | |||
@commands.guild_only() | |||
@commands.command(aliases=["np"]) | |||
async def nowplaying(self, ctx): | |||
"""Displays the video now playing.""" | |||
@@ -401,6 +409,7 @@ class Voice: | |||
embed = self._generate_np_embed(ctx.guild, x) | |||
return await ctx.send(embed=embed) | |||
@commands.guild_only() | |||
@commands.command() | |||
async def queue(self, ctx): | |||
"""Displays what videos are queued up and waiting to be played.""" | |||
@@ -414,6 +423,7 @@ class Voice: | |||
embed = discord.Embed(title="Queue", description=output, colour=roxbot.EmbedColours.pink) | |||
return await ctx.send(embed=embed) | |||
@commands.guild_only() | |||
@roxbot.checks.is_admin_or_mod() | |||
@commands.command() | |||
async def remove(self, ctx, index): | |||
@@ -440,6 +450,7 @@ class Voice: | |||
except IndexError: | |||
raise commands.CommandError("Valid Index not given.") | |||
@commands.guild_only() | |||
@roxbot.checks.is_admin_or_mod() | |||
@commands.command(alaises=["disconnect"]) | |||
async def stop(self, ctx): |
@@ -102,7 +102,10 @@ class ErrHandle: | |||
embed = discord.Embed(title='Command Error', colour=roxbot.EmbedColours.dark_red) | |||
embed.description = str(error) | |||
embed.add_field(name='Server', value=ctx.guild) | |||
embed.add_field(name='Channel', value=ctx.channel.mention) | |||
try: | |||
embed.add_field(name='Channel', value=ctx.channel.mention) | |||
except AttributeError: | |||
embed.add_field(name='Channel', value=ctx.channel.id) | |||
embed.add_field(name='User', value=ctx.author) | |||
embed.add_field(name='Message', value=ctx.message.content) | |||
embed.timestamp = datetime.datetime.utcnow() |
@@ -456,21 +456,22 @@ class Settings: | |||
return await ctx.send("Valid option not given.") | |||
return self.guild_settings.update(voice, "voice") | |||
@commands.guild_only() | |||
@checks.is_admin_or_mod() | |||
@commands.command() | |||
async def serverisanal(self, ctx): | |||
"""Tells the bot where the server is anal or not. | |||
This only changes if roxbot can do the suck and spank commands outside of the specified nsfw channels.""" | |||
is_anal = self.guild_settings.is_anal | |||
gs = guild_settings.get(ctx.guild) | |||
is_anal = gs.is_anal | |||
if is_anal["y/n"] == 0: | |||
is_anal["y/n"] = 1 | |||
self.guild_settings.update(is_anal, "is_anal") | |||
gs.update(is_anal, "is_anal") | |||
await ctx.send("I now know this server is anal") | |||
else: | |||
is_anal["y/n"] = 0 | |||
self.guild_settings.update(is_anal, "is_anal") | |||
gs.update(is_anal, "is_anal") | |||
await ctx.send("I now know this server is NOT anal") | |||
return self.guild_settings.update() | |||
def setup(bot_client): |
@@ -26,7 +26,7 @@ SOFTWARE. | |||
import asyncio | |||
import discord | |||
class Menu: | |||
@@ -108,7 +108,10 @@ async def delete_option(bot, ctx, message, delete_emoji, timeout=20): | |||
try: | |||
await bot.wait_for("reaction_add", timeout=timeout, check=check) | |||
await message.remove_reaction(delete_emoji, bot.user) | |||
await message.remove_reaction(delete_emoji, ctx.author) | |||
try: | |||
await message.remove_reaction(delete_emoji, ctx.author) | |||
except discord.Forbidden: | |||
pass | |||
await message.edit(content="{} requested output be deleted.".format(ctx.author), embed=None) | |||
except asyncio.TimeoutError: | |||
await message.remove_reaction(delete_emoji, bot.user) |