Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Source Command #485

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions bot/__main__.py
Expand Up @@ -52,6 +52,7 @@
bot.load_extension("bot.cogs.verification")

# Feature cogs
bot.load_extension("bot.cogs.source")
bot.load_extension("bot.cogs.alias")
bot.load_extension("bot.cogs.defcon")
bot.load_extension("bot.cogs.eval")
Expand Down
8 changes: 4 additions & 4 deletions bot/cogs/doc.py
Expand Up @@ -261,7 +261,7 @@ async def get_symbol_embed(self, symbol: str) -> Optional[discord.Embed]:
@commands.group(name='docs', aliases=('doc', 'd'), invoke_without_command=True)
async def docs_group(self, ctx: commands.Context, symbol: commands.clean_content = None) -> None:
"""Lookup documentation for Python symbols."""
await ctx.invoke(self.get_command, symbol)
await ctx.invoke(self.get_command)

@docs_group.command(name='get', aliases=('g',))
async def get_command(self, ctx: commands.Context, symbol: commands.clean_content = None) -> None:
Expand Down Expand Up @@ -319,9 +319,9 @@ async def set_command(

Example:
!docs set \
python \
https://docs.python.org/3/ \
https://docs.python.org/3/objects.inv
discord \
https://discordpy.readthedocs.io/en/rewrite/ \
https://discordpy.readthedocs.io/en/rewrite/objects.inv
"""
body = {
'package': package_name,
Expand Down
47 changes: 5 additions & 42 deletions bot/cogs/moderation.py
Expand Up @@ -80,43 +80,6 @@ async def on_ready(self) -> None:
if infraction["expires_at"] is not None:
self.schedule_task(self.bot.loop, infraction["id"], infraction)

@Cog.listener()
async def on_member_join(self, member: Member) -> None:
"""Reapply active mute infractions for returning members."""
active_mutes = await self.bot.api_client.get(
'bot/infractions',
params={'user__id': str(member.id), 'type': 'mute', 'active': 'true'}
)
if not active_mutes:
return

# assume a single mute because of restrictions elsewhere
mute = active_mutes[0]

# transform expiration to delay in seconds
expiration_datetime = datetime.fromisoformat(mute["expires_at"][:-1])
delay = expiration_datetime - datetime.utcnow()
delay_seconds = delay.total_seconds()

# if under a minute or in the past
if delay_seconds < 60:
log.debug(f"Marking infraction {mute['id']} as inactive (expired).")
await self._deactivate_infraction(mute)
self.cancel_task(mute["id"])

# Notify the user that they've been unmuted.
await self.notify_pardon(
user=member,
title="You have been unmuted.",
content="You may now send messages in the server.",
icon_url=Icons.user_unmute
)
return

# allowing modlog since this is a passive action that should be logged
await member.add_roles(self._muted_role, reason=f"Re-applying active mute: {mute['id']}")
log.debug(f"User {member.id} has been re-muted on rejoin.")

# region: Permanent infractions

@with_role(*MODERATION_ROLES)
Expand Down Expand Up @@ -992,11 +955,6 @@ async def _deactivate_infraction(self, infraction_object: Dict[str, Union[str, i
user_id = infraction_object["user"]
infraction_type = infraction_object["type"]

await self.bot.api_client.patch(
'bot/infractions/' + str(infraction_object['id']),
json={"active": False}
)

if infraction_type == "mute":
member: Member = guild.get_member(user_id)
if member:
Expand All @@ -1012,6 +970,11 @@ async def _deactivate_infraction(self, infraction_object: Dict[str, Union[str, i
except NotFound:
log.info(f"Tried to unban user `{user_id}`, but Discord does not have an active ban registered.")

await self.bot.api_client.patch(
'bot/infractions/' + str(infraction_object['id']),
json={"active": False}
)

def _infraction_to_string(self, infraction_object: Dict[str, Union[str, int, bool]]) -> str:
"""Convert the infraction object to a string representation."""
actor_id = infraction_object["actor"]
Expand Down
39 changes: 15 additions & 24 deletions bot/cogs/reddit.py
Expand Up @@ -21,7 +21,6 @@ class Reddit(Cog):

HEADERS = {"User-Agent": "Discord Bot: PythonDiscord (https://pythondiscord.com/)"}
URL = "https://www.reddit.com"
MAX_FETCH_RETRIES = 3

def __init__(self, bot: Bot):
self.bot = bot
Expand All @@ -43,23 +42,16 @@ async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None
if params is None:
params = {}

url = f"{self.URL}/{route}.json"
for _ in range(self.MAX_FETCH_RETRIES):
response = await self.bot.http_session.get(
url=url,
headers=self.HEADERS,
params=params
)
if response.status == 200 and response.content_type == 'application/json':
# Got appropriate response - process and return.
content = await response.json()
posts = content["data"]["children"]
return posts[:amount]
response = await self.bot.http_session.get(
url=f"{self.URL}/{route}.json",
headers=self.HEADERS,
params=params
)

await asyncio.sleep(3)
content = await response.json()
posts = content["data"]["children"]

log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}")
return list() # Failed to get appropriate response within allowed number of retries.
return posts[:amount]

async def send_top_posts(
self, channel: TextChannel, subreddit: Subreddit, content: str = None, time: str = "all"
Expand All @@ -70,14 +62,13 @@ async def send_top_posts(
embed.description = ""

# Get the posts
async with channel.typing():
posts = await self.fetch_posts(
route=f"{subreddit}/top",
amount=5,
params={
"t": time
}
)
posts = await self.fetch_posts(
route=f"{subreddit}/top",
amount=5,
params={
"t": time
}
)

if not posts:
embed.title = random.choice(ERROR_REPLIES)
Expand Down
83 changes: 83 additions & 0 deletions bot/cogs/source.py
@@ -0,0 +1,83 @@
import inspect
import logging
import os

import discord
from discord.ext import commands

from bot import constants
from bot.decorators import in_channel

logger = logging.getLogger(__name__)


class Source(commands.Cog):
"""Send source code url for a specific command."""

def __init__(self, bot: commands.Bot) -> None:
self.bot = bot

@in_channel(constants.Channels.bot)
@commands.command(name="source")
async def command_source(self, ctx: commands.Context, *, command_name: str = None) -> None:
"""View the source of a command."""
if command_name is None:
await ctx.send("> https://github.com/python-discord/bot")
Copy link

@nekitdev nekitdev Oct 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why wouldn't we do return await ctx.send(...) in one line honestly, but ok if there is a reason.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're annotating that our function returns None then it should return None, not maybe None

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I have just checked in discord.py. send returns a discord.Message instance, well then, we should return None.

return

command = self.bot.get_command(command_name)
if command is None:
await ctx.send("No such command found.")
return

if command.name == "help":
url = "https://github.com/python-discord/bot/blob/master/bot/cogs/help.py#L475-L490"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, we would better send a link without the lines here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which lines

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #L475-L490 should not be hard-coded in my opinion. I think we should just return the link to the help.py file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just return the correct lines, but without hardcoding them.

@RohanJnr we can't have any hardcoded edge cases in this solution. Please try to find a solution that solves this without hardcoding.


else:
url = self.get_command_url(command)

prefix = constants.Bot.prefix

embed = discord.Embed(colour=discord.Colour.red())
embed.title = "Command Source"
embed.description = f"**{command.name.capitalize()}**\n"
embed.description += f"`{prefix}{command.qualified_name}`\n\n {url}"

await ctx.send(embed=embed)

@staticmethod
def get_command_url(command: commands.Command) -> str:
"""Make up the url for the source of the command."""
# Get the source code
src = command.callback.__code__
file_name = src.co_filename

# get the file path
file_path = os.path.relpath(file_name).replace('\\', '/')

# Get line number and set last line number
lines_list, starting_line_no = inspect.getsourcelines(src)
lines = len(lines_list)

# Get module
module_name = command.callback.__module__
module_name = module_name.replace(".", "/") + ".py"

last_line_no = starting_line_no + lines - 1

# Make up the url and return
base_url = "https://github.com/python-discord/bot/tree/master/"

# use the module name if the file path goes into the decorators.
if "bot/decorators.py" in file_path:
final_url = f"{base_url}{module_name}#L{starting_line_no}-L{last_line_no}"
else:
final_url = f"{base_url}{file_path}#L{starting_line_no}-L{last_line_no}"

return final_url


def setup(bot: commands.Bot) -> None:
"""Load the cog."""
bot.add_cog(Source(bot))
logger.info("Source Cog loaded.")
33 changes: 2 additions & 31 deletions bot/cogs/tags.py
Expand Up @@ -86,7 +86,7 @@ def _command_on_cooldown(tag_name: str) -> bool:
max_lines=15
)

@tags_group.command(name='set', aliases=('add', 's'))
@tags_group.command(name='set', aliases=('add', 'edit', 's'))
@with_role(*MODERATION_ROLES)
async def set_command(
self,
Expand All @@ -95,7 +95,7 @@ async def set_command(
*,
tag_content: TagContentConverter,
) -> None:
"""Create a new tag."""
"""Create a new tag or update an existing one."""
body = {
'title': tag_name.lower().strip(),
'embed': {
Expand All @@ -116,35 +116,6 @@ async def set_command(
colour=Colour.blurple()
))

@tags_group.command(name='edit', aliases=('e', ))
@with_role(*MODERATION_ROLES)
async def edit_command(
self,
ctx: Context,
tag_name: TagNameConverter,
*,
tag_content: TagContentConverter,
) -> None:
"""Edit an existing tag."""
body = {
'embed': {
'title': tag_name,
'description': tag_content
}
}

await self.bot.api_client.patch(f'bot/tags/{tag_name}', json=body)

log.debug(f"{ctx.author} successfully edited the following tag in our database: \n"
f"tag_name: {tag_name}\n"
f"tag_content: '{tag_content}'\n")

await ctx.send(embed=Embed(
title="Tag successfully edited",
description=f"**{tag_name}** edited in the database.",
colour=Colour.blurple()
))

@tags_group.command(name='delete', aliases=('remove', 'rm', 'd'))
@with_role(Roles.admin, Roles.owner)
async def delete_command(self, ctx: Context, *, tag_name: TagNameConverter) -> None:
Expand Down
2 changes: 1 addition & 1 deletion bot/cogs/watchchannels/watchchannel.py
Expand Up @@ -335,7 +335,7 @@ def _remove_user(self, user_id: int) -> None:
def cog_unload(self) -> None:
"""Takes care of unloading the cog and canceling the consumption task."""
self.log.trace(f"Unloading the cog")
if self._consume_task and not self._consume_task.done():
if not self._consume_task.done():
self._consume_task.cancel()
try:
self._consume_task.result()
Expand Down
2 changes: 1 addition & 1 deletion bot/converters.py
Expand Up @@ -49,7 +49,7 @@ async def convert(ctx: Context, url: str) -> str:
async with ctx.bot.http_session.get(url) as resp:
if resp.status != 200:
raise BadArgument(
f"HTTP GET on `{url}` returned status `{resp.status}`, expected 200"
f"HTTP GET on `{url}` returned status `{resp.status_code}`, expected 200"
)
except CertificateError:
if url.startswith('https'):
Expand Down