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

[V3 RPC] Initial RPC library switch #1634

Merged
merged 7 commits into from
May 23, 2018
Merged
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
34 changes: 27 additions & 7 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 32 additions & 1 deletion docs/framework_rpc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,36 @@
RPC
===

.. automodule:: redbot.core.rpc
.. currentmodule:: redbot.core.rpc

V3 comes default with an internal RPC server that may be used to remotely control the bot in various ways.
Cogs must register functions to be exposed to RPC clients.
Each of those functions must only take JSON serializable parameters and must return JSON serializable objects.

To begin, register all methods using individual calls to the :func:`Methods.add` method.

********
Examples
********

Coming soon to a docs page near you!

*************
API Reference
*************

.. py:attribute:: redbot.core.rpc.methods

An instance of the :class:`Methods` class.
All attempts to register new RPC methods **MUST** use this object.
You should never create a new instance of the :class:`Methods` class!

RPC
^^^
.. autoclass:: redbot.core.rpc.RPC
:members:

Methods
^^^^^^^
.. autoclass:: redbot.core.rpc.Methods
:members:
2 changes: 1 addition & 1 deletion redbot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def main():
sentry_log.critical("Fatal Exception", exc_info=e)
loop.run_until_complete(red.logout())
finally:
rpc.clean_up()
red.rpc.close()
if cleanup_tasks:
pending = asyncio.Task.all_tasks(loop=red.loop)
gathered = asyncio.gather(*pending, loop=red.loop, return_exceptions=True)
Expand Down
24 changes: 4 additions & 20 deletions redbot/core/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,9 @@
from . import Config, i18n, commands, rpc
from .help_formatter import Help, help as help_
from .sentry import SentryManager
from .utils import TYPE_CHECKING

if TYPE_CHECKING:
from aiohttp_json_rpc import JsonRpc

# noinspection PyUnresolvedReferences
class RpcMethodMixin:

async def rpc__cogs(self, request):
return list(self.cogs.keys())

async def rpc__extensions(self, request):
return list(self.extensions.keys())


class RedBase(BotBase, RpcMethodMixin):
class RedBase(BotBase):
"""Mixin for the main bot class.

This exists because `Red` inherits from `discord.AutoShardedClient`, which
Expand Down Expand Up @@ -104,10 +91,11 @@ async def prefix_manager(bot, message):

self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))

self.register_rpc_methods()

super().__init__(formatter=Help(), **kwargs)

if self.rpc_enabled:
self.rpc = rpc.RPC(self)

self.remove_command("help")

self.add_command(help_)
Expand Down Expand Up @@ -275,10 +263,6 @@ def unload_extension(self, name):
if pkg_name.startswith("redbot.cogs"):
del sys.modules["redbot.cogs"].__dict__[name]

def register_rpc_methods(self):
rpc.add_method("bot", self.rpc__cogs)
rpc.add_method("bot", self.rpc__extensions)


class Red(RedBase, discord.AutoShardedClient):
"""
Expand Down
4 changes: 0 additions & 4 deletions redbot/core/core_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ class Core:
def __init__(self, bot):
self.bot = bot # type: Red

rpc.add_method("core", self.rpc_load)
rpc.add_method("core", self.rpc_unload)
rpc.add_method("core", self.rpc_reload)

@commands.command(hidden=True)
async def ping(self, ctx):
"""Pong."""
Expand Down
3 changes: 1 addition & 2 deletions redbot/core/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
from . import __version__
from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered
from .rpc import initialize
from colorama import Fore, Style, init

log = logging.getLogger("red")
Expand Down Expand Up @@ -173,7 +172,7 @@ async def on_ready():
print("\nInvite URL: {}\n".format(invite_url))

if bot.rpc_enabled:
await initialize(bot)
await bot.rpc.initialize()

@bot.event
async def on_error(event_method, *args, **kwargs):
Expand Down
158 changes: 104 additions & 54 deletions redbot/core/rpc.py
Original file line number Diff line number Diff line change
@@ -1,85 +1,135 @@
import asyncio
import weakref

from aiohttp.web import Application
from aiohttp_json_rpc import JsonRpc
from aiohttp import web
import jsonrpcserver.aio

import inspect
import logging

from .utils import TYPE_CHECKING, NewType

if TYPE_CHECKING:
from .bot import Red
__all__ = ["methods", "RPC", "Methods"]

log = logging.getLogger("red.rpc")
JsonSerializable = NewType("JsonSerializable", dict)

_rpc = JsonRpc(logger=log)

_rpc_server = None # type: asyncio.AbstractServer
class Methods(jsonrpcserver.aio.AsyncMethods):
"""
Container class for all registered RPC methods, please use the existing `methods`
attribute rather than creating a new instance of this class.

.. warning::

async def initialize(bot: "Red"):
global _rpc_server
**NEVER** create a new instance of this class!
"""

app = Application(loop=bot.loop)
app.router.add_route("*", "/rpc", _rpc)
def __init__(self):
super().__init__()

handler = app.make_handler()
self._items = weakref.WeakValueDictionary()

_rpc_server = await bot.loop.create_server(handler, "127.0.0.1", 6133)
def add(self, method, name: str = None):
"""
Registers a method to the internal RPC server making it available for
RPC users to call.

log.debug("Created RPC _rpc_server listener.")
.. important::

Any method added here must take ONLY JSON serializable parameters and
MUST return a JSON serializable object.

def add_topic(topic_name: str):
"""
Adds a topic for clients to listen to.
Parameters
----------
method : function
A reference to the function to register.

Parameters
----------
topic_name
"""
_rpc.add_topics(topic_name)
name : str
Name of the function as seen by the RPC clients.
"""
if not inspect.iscoroutinefunction(method):
raise TypeError("Method must be a coroutine.")

if name is None:
name = method.__qualname__

def notify(topic_name: str, data: JsonSerializable):
"""
Publishes a notification for the given topic name to all listening clients.
self._items[str(name)] = method

data MUST be json serializable.
def remove(self, *, name: str = None, method=None):
"""
Unregisters an RPC method. Either a name or reference to the method must
be provided and name will take priority.

note::
Parameters
----------
name : str
method : function
"""
if name and name in self._items:
del self._items[name]

This method will fail silently.
elif method and method in self._items.values():
to_remove = []
for name, val in self._items.items():
if method == val:
to_remove.append(name)

Parameters
----------
topic_name
data
"""
_rpc.notify(topic_name, data)
for name in to_remove:
del self._items[name]

def all_methods(self):
"""
Lists all available method names.

def add_method(prefix, method):
"""
Makes a method available to RPC clients. The name given to clients will be as
follows::
Returns
-------
list of str
"""
return self._items.keys()

"{}__{}".format(prefix, method.__name__)

note::
methods = Methods()

This method will fail silently.

Parameters
----------
prefix
method
MUST BE A COROUTINE OR OBJECT.
"""
_rpc.add_methods(("", method), prefix=prefix)
class BaseRPCMethodMixin:

def __init__(self):
methods.add(self.all_methods, name="all_methods")

async def all_methods(self):
return list(methods.all_methods())


def clean_up():
if _rpc_server is not None:
_rpc_server.close()
class RPC(BaseRPCMethodMixin):
"""
RPC server manager.
"""

def __init__(self, bot):
self.app = web.Application(loop=bot.loop)
self.app.router.add_post("/rpc", self.handle)

self.app_handler = self.app.make_handler()

self.server = None

super().__init__()

async def initialize(self):
"""
Finalizes the initialization of the RPC server and allows it to begin
accepting queries.
"""
self.server = await self.app.loop.create_server(self.app_handler, "127.0.0.1", 6133)
log.debug("Created RPC server listener.")

def close(self):
"""
Closes the RPC server.
"""
self.server.close()

async def handle(self, request):
request = await request.text()
response = await methods.dispatch(request)
if response.is_notification:
return web.Response()
else:
return web.json_response(response, status=response.http_status)
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ aiohttp>=2.0.0,<2.3.0
appdirs==1.4.3
raven==6.5.0
colorama==0.3.9
aiohttp-json-rpc==0.8.7
jsonrpcserver
pyyaml==3.12
Red-Trivia>=1.1.1
async-timeout<3.0.0