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

Collapse long messages into gist command #362

Closed
wants to merge 18 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions futaba/cogs/optional/gist/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#
# cogs/optional/gist/__init__.py
#
# futaba - A Discord Mod bot for the Programming server
# Copyright (c) 2017-2020 Jake Richardson, Ammon Smith, jackylam5
#
# futaba is available free of charge under the terms of the MIT
# License. You are free to redistribute and/or modify it under those
# terms. It is distributed in the hopes that it will be useful, but
# WITHOUT ANY WARRANTY. See the LICENSE file for more details.
#

from .core import Gist

# Setup for when cog is loaded
def setup(bot):
setup_gist(bot)


def setup_gist(bot):
cog = Gist(bot)
bot.add_cog(cog)


# Remove all the cogs when cog is unloaded
def teardown(bot):
teardown_gist(bot)


def teardown_gist(bot):
bot.remove_cog(Gist.__name__)
249 changes: 249 additions & 0 deletions futaba/cogs/optional/gist/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
#
# cogs/optional/gist/core.py
#
# futaba - A Discord Mod bot for the Programming server
# Copyright (c) 2017-2020 Jake Richardson, Ammon Smith, jackylam5
#
# futaba is available free of charge under the terms of the MIT
# License. You are free to redistribute and/or modify it under those
# terms. It is distributed in the hopes that it will be useful, but
# WITHOUT ANY WARRANTY. See the LICENSE file for more details.
#

"""
Cog for creating gists from messages
"""

# REMOVE THIS IN REGULAR COGS:
# pylint: disable=unused-import

import asyncio
import logging
import math

import discord
from discord.ext import commands

from futaba import permissions
from futaba.cogs.abc import AbstractCog
from futaba.converters import MessageConv
from futaba.exceptions import CommandFailed, ManualCheckFailure, SendHelp
from futaba.permissions import mod_perm
from futaba.utils import user_discrim

from .gist import create_single_gist

logger = logging.getLogger(__name__)


class Gist(AbstractCog):
__slots__ = ("journal",)

def __init__(self, bot):
super().__init__(bot)
self.journal = bot.get_broadcaster("/gist")
self.default_settings = {
"token": "",
"description": "Messages uploaded by futaba",
"filename": "messages.md",
"public": False,
}

def setup(self):
# Fetching information from the database for this cog
pass

def get_settings(self, guild):
return self.bot.sql.settings.get_optional_cog_settings(guild, "gist")

def set_settings(self, guild, settings):
return self.bot.sql.settings.set_optional_cog_settings(guild, "gist", settings)

def change_setting(self, guild, setting, value):
settings = self.get_settings(guild)

if len(settings) == 0:
settings = self.default_settings

settings[setting] = value

self.set_settings(guild, settings)

@staticmethod
def format_message_contents(message):
contents = message.content.split("\n")
contents = " \n".join(">" + content for content in contents)

return f"{user_discrim(message.author)}: \n{contents} \n \n"

@commands.command(name="gist", aliases=["msgupload"])
@commands.guild_only()
async def upload_message(self, ctx, *messages: MessageConv):
"""
Concatenates the range of messages and upload to a gist.
A link to the gist is posted after a successful upload.
"""
if len(messages) == 0:
raise CommandFailed(
content="Please specify the messages that should be uploaded."
)

settings = self.get_settings(ctx.guild)
oauth_token = settings.get("token")

if not oauth_token:
raise CommandFailed(content="The gist oauth token is not configured.")

messages_content = "\n".join(
self.format_message_contents(message) for message in messages
)
messages_ids = ", ".join(str(message.id) for message in messages)

gist_url = await create_single_gist(
token=oauth_token,
content=messages_content,
filename=settings.get("filename"),
description=settings.get("description"),
public=settings.get("public"),
)

logger.info(
"Successfully uploaded %d messages[%s] into a gist. Requested by user '%s' (id=%d, guild=%d)",
len(messages),
messages_ids,
ctx.author.name,
ctx.author.id,
ctx.guild.id,
)

embed = discord.Embed(description="Done! Messages successfully uploaded!")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Bot shouldn't be this excited, I would limit it to a single exclamation point

embed.add_field(name="Permalink", value=gist_url)
embed.colour = discord.Colour.dark_teal()

await ctx.send(embed=embed)

@commands.command(name="mvgist", aliases=["msgcollapse"])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also add the full form messagecollapse and movegist aliases

@commands.guild_only()
async def collapse_message(self, ctx, *messages: MessageConv):
"""
Concatenates the range of messages and uploads to a gist.
The original messages are deleted and a link to the gist is posted.

Note: The messages specified should be by the same user
"""

if not permissions.has_perm(ctx, "manage_messages") and any(
message.author.id != ctx.author.id for message in messages
):
# check if the messages were created by the same user
raise ManualCheckFailure(content="I can only collapse your messages")

await self.upload_message(ctx, *messages)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would prefer if this was a common helper method instead of directly invoking another command. If arguments or things involving pre/post command hooks change then this will cause issues.


for message in messages:
await message.delete()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Parallelize this with asyncio.gather()


logger.info(
"Removed %d messages because of message collapse request by user '%s'(id=%d, guild=%d)",
len(messages),
ctx.author.name,
ctx.author.id,
ctx.guild.id,
)

@commands.group(name="gistconf")
@commands.guild_only()
async def gist_settings(self, ctx):
""" Manages settings related to gists """

if ctx.invoked_subcommand is None:
raise SendHelp()

@gist_settings.command(name="get")
@commands.guild_only()
@permissions.check_admin()
async def settings_get(self, ctx, setting: str = None):
"""
Gets the current settings
Optionally a setting name can be specified
"""

settings = self.get_settings(ctx.guild)

embed = discord.Embed(
description="Gist cog settings", colour=discord.Colour.dark_teal()
)

for key, val in settings.items():
if setting is None or setting == key:
embed.add_field(name=key, value=val)

await ctx.send(embed=embed)

@gist_settings.command(name="token")
@commands.guild_only()
@permissions.check_admin()
async def settings_token(self, ctx, value: str = None):
"""
Gets the currently set github token
If you are an administrator you can change this value
"""

if value is not None:
self.change_setting(ctx.guild, "token", value)

await self.settings_get(ctx, "token")

@gist_settings.command(name="description")
@commands.guild_only()
async def settings_description(self, ctx, value: str = None):
"""
Gets the currently set gist description
If you are a moderator you can change this value
"""

if value is not None:
if mod_perm(ctx):
self.change_setting(ctx.guild, "description", value)
else:
raise ManualCheckFailure(
content="You do not have persmissions to change the gist description"
)

await self.settings_get(ctx, "description")

@gist_settings.command(name="filename")
@commands.guild_only()
async def settings_filename(self, ctx, value: str = None):
"""
Gets the currently set gist filename
If you are a moderator you can change this value
"""

if value is not None:
if mod_perm(ctx):
self.change_setting(ctx.guild, "filename", value)
else:
raise ManualCheckFailure(
content="You do not have persmissions to change the gist filename"
)

await self.settings_get(ctx, "filename")

@gist_settings.command(name="public")
@commands.guild_only()
async def settings_public(self, ctx, value: bool = None):
"""
Gets whether gists are public and available for anyone to see
If you are a moderator you can change this value
"""

if value is not None:
if mod_perm(ctx):
self.change_setting(ctx.guild, "public", value)
else:
raise ManualCheckFailure(
content="You do not have persmissions to change whether uploaded gists are public"
)

await self.settings_get(ctx, "public")
49 changes: 49 additions & 0 deletions futaba/cogs/optional/gist/gist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#
# cogs/optional/gist/gist.py
#
# futaba - A Discord Mod bot for the Programming server
# Copyright (c) 2017-2020 Jake Richardson, Ammon Smith, jackylam5
#
# futaba is available free of charge under the terms of the MIT
# License. You are free to redistribute and/or modify it under those
# terms. It is distributed in the hopes that it will be useful, but
# WITHOUT ANY WARRANTY. See the LICENSE file for more details.
#

import logging
import urllib.parse

import aiohttp

logger = logging.getLogger(__name__)

__all__ = ["create_single_gist"]

github_api_url = "https://api.github.com/"
github_gist_endpoint = urllib.parse.urljoin(github_api_url, "/gists")
Copy link
Collaborator

Choose a reason for hiding this comment

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

constants should be SCREAMING_SNAKE_CASE



async def create_single_gist(
token, content, filename="message.md", description="", public: bool = True
) -> str:
"""
Creates a new gist as specified by the parameters
Returns the url that the new gist can be accessed by
"""

github_headers = {
"Accept": "application/vnd.github.v3+json",
"Authorization": f"token {token}",
}
request_data = {
"description": description,
"public": public,
"files": {filename: {"content": content}},
}

async with aiohttp.ClientSession(
headers=github_headers, raise_for_status=True
) as session:
async with session.post(github_gist_endpoint, json=request_data) as resp:
response_object = await resp.json()
return response_object.get("html_url")
2 changes: 1 addition & 1 deletion futaba/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def wrapper(value):
"prefix": str,
"error-channel-id": Or(And(str, ID_REGEX.match), "0"),
},
"cogs": {"example": object, "statbot": object},
"cogs": {"example": object, "statbot": object, "gist": object},
"moderation": {
"max-cleanup-messages": And(str, _check_gtz(int)),
"ping-cooldown": And(str, _check_gtz(int)),
Expand Down
6 changes: 6 additions & 0 deletions misc/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ enabled = true
enabled = false
url = "postgres://statbot:passwordhere@localhost/statbot_ro"

[cogs.gist]
# Gist cog allows automatic pasting of messages into a gist
# Note: In order for the cog to work correctly, each guild should
# set a github token using the command "!gistconf set token yourtoken"
enabled = true

[moderation]
# Maximum number of messages that may be specified
# when doing a bulk message cleanup
Expand Down