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

[Dashboard] Add third parties. #83

Open
wants to merge 15 commits into
base: backend_rewrite
Choose a base branch
from

Conversation

AAA3A-AAA3A
Copy link

@AAA3A-AAA3A AAA3A-AAA3A commented Apr 4, 2023

Type

  • Bugfix
  • Enhancement
  • New feature

Description of the changes

Hello,

I tried to integrate myself the third parties in Red-Dashboard. I personally think that every cog should be able to add their own pages to the dashboard, easily, without having to modify the source code of Red-Dashboard or cog Dashboard.
I haven't done any documentation.

How does it work in practice:

On the Red-Dashboard side:

An API endpoint point has been added: /third_party/<cog_name>/[page]?**[params]. The local Dashboard cog sends the list of third parties and pages to Red-Dashboard, in the get_variables RPC method, which is already called at regular intervals. Thus, the code checks if the cog, and the page exist. Depending on the parameters provided by the cog creator, the code will deny requests if the method used is not one of the allowed ones (HEAD, GET, OPTIONS, POST, PATH and DELETE). If user_id is a required parameter, then the Dashboard will request the OAuth login of the current user. If guild_id is required, then the current dashboard.html page will be displayed to allow the choice of a server: the html file has been modified to allow the BASE_GUILD_URL variable to be changed optionally. user_id, guild_id, member_id, role_id and channel_id are context variables, which should be `int': at the moment, choice is not possible for members, roles and channels, but these parameters could be provided by cogs manually in Discord. If parameters are required, the Dashboard will display an error on the browser. A web request will be sent to the local cog Dashboard which will dispatch the data correctly and get a response.

Types of responses from third parties:
The third parties would return to the local cog Dashboard a dict like a real RPC method would.
Several keys are supported by the API endpoint:

  • status: Any request response should have it, but it is not used.
  • web-content: The Flask/Django/Jinja2 template will be displayed on the browser. It can contain HTML, CSS and JavaScript, and should start with {% extends "base-site.html" %} to display the base dashboard layout. The variables in the response will be passed in.
  • error_message: Using the new html file error_message.html, the provided message will be displayed directly to the user, without having to code a different html content.
  • redirect : The existing template with its name will be displayed. The variables of the response will be passed on.
    If the request methods are other than HEAD and GET, the data will be returned directly as JSON.

\app\base\static\assets\js\plugins\utils.js:
A new JavaScript script has been added as a utility to the Dashboard. It contains several methods that can be called in a template.

  • $.sendNotification: Easily display notifications on the Dashboard, with only the type and message as required parameters.
  • $.postData: Easily send data to the bot with the POST method. By default, the current URL window.location.href will be used for the request.
  • $.showTableRegular : Allows to display the provided data in tabular form.
  • $.generateForm: Generates a form with many features and supporting all possible inputs (name, label, type, placeholder, required, validate, error). By default, data is sent to the bot with $.postData.

On the Dashboard local cog side:
A DashboardRPC_ThirdParties extension has been added and is accessible at Dashboard.rpc.third_parties_handler. A third party is linked to a commands.Cog object which must be loaded, in order to be used.
The DashboardRPC_ThirdParties.add_third_party method must be used to add a cog as a third party. The page parameters are stored in DashboardRPC_ThirdParties.third_parties.

The decorator dashboard.rpc.thirdparties.dashboard_page allows to provide parameters for each page. All attributes of the cog class that have a __dashboard_params__ attribute will be automatically added to the Dashboard when the add third party method is called. Context parameters (user_id/user, guild_id/guild, member_id/member, role_id/role, channel_id/channel) and required parameters are detected in the parameter names.
Here are its parameters:

  • name: None so that the user does not have to specify the name to get this page. A name will have the same limitations as the Discord slash command names for ease of use.
  • methods: The web request methods allowed to call the third party page.
  • required_kwargs : To manually specify required parameters.
  • permissions_required : The user's required permissions on the server.
  • hidden: A parameter not used at this time. Maybe the pages will be listed somewhere someday.

The RPC method DashboardRPC_ThirdParties.data_receive receives the data from Red-Dashboard for the endpoint API I mentioned earlier. In case, the existence of the third party and the page is checked as new. If the cog is no longer loaded, the request is "refused" with an error message. If a context_ids variable is provided (user_id, guild_id, member_id, role_id or channel_id), the code checks if the bot has access to it and if the Discord objects exist. The parameters user, guild, member, role and channel are then added eventually.

The parameters received from the Red-Dashboard (and passed to cogs) are method, **context_ids, **kwargs and lang_code. Cogs should use **kwargs last, as the user (or Flask) is free to add whatever parameters they wish to the pages.

Quick cog example:

from redbot.core import commands
try:
    from dashboard.rpc.thirdparties import dashboard_page
except ImportError:
    def dashboard_page(*args, **kwargs):  # fake decorator
        def decorator(func: typing.Callable):
            return func
        return decorator

class Cog(commands.Cog):
    def __init__(self, bot: Red) -> None:
        self.bot: Red = bot
        self.cache: typing.Dict[discord.Guild, dict] = {}

    async def cog_load(self) -> None:
        if (dashboard_cog := self.bot.get_cog("Dashboard")) is not None and dashboard_page is not None:
          dashboard_cog.rpc.third_parties_handler.add_third_party(self)

    async def cog_unload(self) -> None:
        if (dashboard_cog := self.bot.get_cog("Dashboard")) is not None and dashboard_page is not None:
            dashboard_cog.rpc.third_parties_handler.remove_third_party(self)

    @dashboard_page(name=None, methods=["GET", "POST"])
    async def rpc_callback(self, method: str, user: discord.User, guild: discord.Guild, **kwargs) -> None:
        if method == "GET":
            return {"status": 0, "data": self.cache.get(guild.id, {})}
        elif method == "POST":
            self.cache[guild] = kwargs.get("data", {})
            return {"status": 0, "data": self.cache[guild.id]}

await ctx.bot.add_cog(Cog(bot))

Minor corrections:

  • Say that the guild was not found if the parameter cannot be converted to str.
  • Replace all except: with except Exception.
  • Added highlight filter to Flask to allow formatted code to be displayed in multiple languages. (<code class="language-python">' + hljs.highlight('python', item).value + '</code>)

Thank you very much in advance,
Have a nice day,
AAA3A

@AAA3A-AAA3A
Copy link
Author

AAA3A-AAA3A commented Apr 15, 2023

I tried to integrate two of my cogs with this PR as a third party to Dashboard, and I noticed a small problem.
The cogs would use decorators from the dashboarď module, but this one can load afterwards when the bot starts. Also, the Dashboard cog could not be reloaded without losing all third parties.
To solve this problem, I added the dashboard_cog_add event which will be dispatched when the cog Dashboard is loaded. When a new cog is loaded, as dpy does, the cog will look for events for that cog and call the associated methods. This way, the pages will always work on the bot.

from redbot.core import commands
from redbot.core.bot import Red 
import discord
import typing 


def dashboard_page(*args, **kwargs):
    def decorator(func: typing.Callable):
        func.__dashboard_decorator_params__ = (args, kwargs)
        return func
    return decorator


class DashboardIntegration:
    bot: Red

    @commands.Cog.listener()
    async def on_dashboard_cog_add(self, dashboard_cog: commands.Cog) -> None:
        try:
            from dashboard.rpc.thirdparties import dashboard_page
        except ImportError:  # Should never happen because the event would not be dispatched by the Dashboard cog.
            return
        for attr in dir(self):
            if hasattr((func := getattr(self, attr)), "__dashboard_decorator_params__"):
                setattr(self, attr, func.__class__(dashboard_page(*func.__dashboard_decorator_params__[0], **func.__dashboard_decorator_params__[1])(func.__func__), func.__self__))
        dashboard_cog.rpc.third_parties_handler.add_third_party(self)

    async def cog_unload(self) -> None:
        if (dashboard_cog := self.bot.get_cog("Dashboard")) is not None:
            dashboard_cog.rpc.third_parties_handler.remove_third_party(self)

    @dashboard_page(name=None)
    async def rpc_callback(self, user: discord.User, **kwargs) -> None:
        return {"status": 0, "web-content": web_content}

web_content = """
{% extends "base-site.html" %}

{% block title %} {{ _('TicketTool cog') }} {% endblock title %}

{% block content %}
<h2>TicketToolCog</h2>
<div class="row">
    <div class="col-md-12">
        <div class="card">
            <div class="card-body">
                <button class="btn", onclick="window.location.href = window.location.origin + window.location.pathname + '/settings';">Access to Settings</button>
            </div>
        </div>
    </div>
</div>
{% endblock content %}
"""

image

When Cog-Creators/Red-DiscordBot#5570 is merged into the Red repo, the cog Dashboard will use the new on_cog_remove event to remove third parties, without cogs having to implement the cog_unload method.

@AAA3A-AAA3A
Copy link
Author

I also added a new "Third Parties" page that appears once you log in.
The menu is similar to the one in the command list. The Dashboard will retrieve the cog description from this list based on the name of the third party.
The hidden kwarg I mentioned earlier allows you to hide pages in this menu. If all pages are hidden, the third party will not appear.

image
image

AAA3A-AAA3A added a commit to AAA3A-AAA3A/AAA3A_utils that referenced this pull request Apr 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant