mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2025-11-06 03:08:55 -05:00
[V3 RPC] Add basic RPC functionality (#1017)
* Add basic RPC functionality * Add load/unload/reload rpc commands * Reorganize without rpc_ready event * Remove rpc ready event * Removed bot reference from rpc module * Close RPC server cleanly * refactor bot * Refactor a bit and make RPC server initialization based on a cli flag * Fix version resolver * standardize version getters * Pick a new port number
This commit is contained in:
parent
8d8e1c61d8
commit
f459a21bef
12
docs/framework_events.rst
Normal file
12
docs/framework_events.rst
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.. framework events list
|
||||||
|
|
||||||
|
=============
|
||||||
|
Custom Events
|
||||||
|
=============
|
||||||
|
|
||||||
|
RPC Server
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
.. py:method:: Red.on_shutdown()
|
||||||
|
|
||||||
|
Dispatched when the bot begins it's shutdown procedures.
|
||||||
@ -21,6 +21,7 @@ from redbot.core.sentry_setup import init_sentry_logging
|
|||||||
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
from redbot.core.cli import interactive_config, confirm, parse_cli_flags, ask_sentry
|
||||||
from redbot.core.core_commands import Core
|
from redbot.core.core_commands import Core
|
||||||
from redbot.core.dev_commands import Dev
|
from redbot.core.dev_commands import Dev
|
||||||
|
from redbot.core import rpc
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import logging
|
import logging
|
||||||
@ -96,7 +97,7 @@ def main():
|
|||||||
red = Red(cli_flags, description=description, pm_help=None)
|
red = Red(cli_flags, description=description, pm_help=None)
|
||||||
init_global_checks(red)
|
init_global_checks(red)
|
||||||
init_events(red, cli_flags)
|
init_events(red, cli_flags)
|
||||||
red.add_cog(Core())
|
red.add_cog(Core(red))
|
||||||
red.add_cog(CogManagerUI())
|
red.add_cog(CogManagerUI())
|
||||||
if cli_flags.dev:
|
if cli_flags.dev:
|
||||||
red.add_cog(Dev())
|
red.add_cog(Dev())
|
||||||
@ -141,6 +142,7 @@ def main():
|
|||||||
sentry_log.critical("Fatal Exception", exc_info=e)
|
sentry_log.critical("Fatal Exception", exc_info=e)
|
||||||
loop.run_until_complete(red.logout())
|
loop.run_until_complete(red.logout())
|
||||||
finally:
|
finally:
|
||||||
|
rpc.clean_up()
|
||||||
if cleanup_tasks:
|
if cleanup_tasks:
|
||||||
pending = asyncio.Task.all_tasks(loop=red.loop)
|
pending = asyncio.Task.all_tasks(loop=red.loop)
|
||||||
gathered = asyncio.gather(*pending, loop=red.loop)
|
gathered = asyncio.gather(*pending, loop=red.loop)
|
||||||
|
|||||||
@ -6,6 +6,6 @@ from .context import RedContext
|
|||||||
__all__ = ["Config", "RedContext", "__version__"]
|
__all__ = ["Config", "RedContext", "__version__"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
__version__ = pkg_resources.require("Red-DiscordBot")[0].version
|
__version__ = pkg_resources.get_distribution("Red-DiscordBot").version
|
||||||
except pkg_resources.DistributionNotFound:
|
except pkg_resources.DistributionNotFound:
|
||||||
__version__ = "3.0.0"
|
__version__ = "3.0.0"
|
||||||
|
|||||||
@ -10,10 +10,29 @@ from discord.ext.commands.bot import BotBase
|
|||||||
from discord.ext.commands import GroupMixin
|
from discord.ext.commands import GroupMixin
|
||||||
|
|
||||||
from .cog_manager import CogManager
|
from .cog_manager import CogManager
|
||||||
from . import Config, i18n, RedContext
|
from . import (
|
||||||
|
Config,
|
||||||
|
i18n,
|
||||||
|
RedContext,
|
||||||
|
rpc
|
||||||
|
)
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from aiohttp_json_rpc import JsonRpc
|
||||||
|
|
||||||
|
|
||||||
class RedBase(BotBase):
|
# 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):
|
||||||
"""Mixin for the main bot class.
|
"""Mixin for the main bot class.
|
||||||
|
|
||||||
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
This exists because `Red` inherits from `discord.AutoShardedClient`, which
|
||||||
@ -26,6 +45,7 @@ class RedBase(BotBase):
|
|||||||
self._shutdown_mode = ExitCodes.CRITICAL
|
self._shutdown_mode = ExitCodes.CRITICAL
|
||||||
self.db = Config.get_core_conf(force_registration=True)
|
self.db = Config.get_core_conf(force_registration=True)
|
||||||
self._co_owners = cli_flags.co_owner
|
self._co_owners = cli_flags.co_owner
|
||||||
|
self.rpc_enabled = cli_flags.rpc
|
||||||
|
|
||||||
self.db.register_global(
|
self.db.register_global(
|
||||||
token=None,
|
token=None,
|
||||||
@ -73,6 +93,8 @@ class RedBase(BotBase):
|
|||||||
|
|
||||||
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),))
|
self.cog_mgr = CogManager(paths=(str(self.main_dir / 'cogs'),))
|
||||||
|
|
||||||
|
self.register_rpc_methods()
|
||||||
|
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
async def _dict_abuse(self, indict):
|
async def _dict_abuse(self, indict):
|
||||||
@ -194,6 +216,10 @@ class RedBase(BotBase):
|
|||||||
del self.extensions[name]
|
del self.extensions[name]
|
||||||
# del sys.modules[name]
|
# del sys.modules[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):
|
class Red(RedBase, discord.AutoShardedClient):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -102,6 +102,10 @@ def parse_cli_flags(args):
|
|||||||
parser.add_argument("--dev",
|
parser.add_argument("--dev",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Enables developer mode")
|
help="Enables developer mode")
|
||||||
|
parser.add_argument("--rpc",
|
||||||
|
action="store_true",
|
||||||
|
help="Enables the built-in RPC server. Please read the docs"
|
||||||
|
"prior to enabling this!")
|
||||||
parser.add_argument("instance_name",
|
parser.add_argument("instance_name",
|
||||||
help="Name of the bot instance created during `redbot-setup`.")
|
help="Name of the bot instance created during `redbot-setup`.")
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,12 @@ from discord.ext import commands
|
|||||||
|
|
||||||
from redbot.core import checks
|
from redbot.core import checks
|
||||||
from redbot.core import i18n
|
from redbot.core import i18n
|
||||||
|
from redbot.core import rpc
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
__all__ = ["Core"]
|
__all__ = ["Core"]
|
||||||
|
|
||||||
@ -29,6 +35,12 @@ _ = i18n.CogI18n("Core", __file__)
|
|||||||
|
|
||||||
class Core:
|
class Core:
|
||||||
"""Commands related to core functions"""
|
"""Commands related to core functions"""
|
||||||
|
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()
|
@commands.command()
|
||||||
@checks.is_owner()
|
@checks.is_owner()
|
||||||
@ -410,3 +422,25 @@ class Core:
|
|||||||
"to %s") % destination)
|
"to %s") % destination)
|
||||||
else:
|
else:
|
||||||
await ctx.send(_("Message delivered to %s") % destination)
|
await ctx.send(_("Message delivered to %s") % destination)
|
||||||
|
|
||||||
|
# RPC handlers
|
||||||
|
async def rpc_load(self, request):
|
||||||
|
cog_name = request.params[0]
|
||||||
|
|
||||||
|
spec = await self.bot.cog_mgr.find_cog(cog_name)
|
||||||
|
if spec is None:
|
||||||
|
raise LookupError("No such cog found.")
|
||||||
|
|
||||||
|
self.cleanup_and_refresh_modules(spec.name)
|
||||||
|
|
||||||
|
self.bot.load_extension(spec)
|
||||||
|
|
||||||
|
async def rpc_unload(self, request):
|
||||||
|
cog_name = request.params[0]
|
||||||
|
|
||||||
|
self.bot.unload_extension(cog_name)
|
||||||
|
|
||||||
|
async def rpc_reload(self, request):
|
||||||
|
await self.rpc_unload(request)
|
||||||
|
await self.rpc_load(request)
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import discord
|
|||||||
from .sentry_setup import should_log
|
from .sentry_setup import should_log
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
from . import __version__
|
||||||
from .data_manager import storage_type
|
from .data_manager import storage_type
|
||||||
from .utils.chat_formatting import inline, bordered
|
from .utils.chat_formatting import inline, bordered
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
from .rpc import initialize
|
||||||
|
|
||||||
log = logging.getLogger("red")
|
log = logging.getLogger("red")
|
||||||
sentry_log = logging.getLogger("red.sentry")
|
sentry_log = logging.getLogger("red.sentry")
|
||||||
@ -70,12 +72,13 @@ def init_events(bot, cli_flags):
|
|||||||
|
|
||||||
prefixes = await bot.db.prefix()
|
prefixes = await bot.db.prefix()
|
||||||
lang = await bot.db.locale()
|
lang = await bot.db.locale()
|
||||||
red_pkg = pkg_resources.get_distribution('Red_DiscordBot')
|
red_version = __version__
|
||||||
dpy_version = pkg_resources.get_distribution('discord.py').version
|
red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
|
||||||
|
dpy_version = discord.__version__
|
||||||
|
|
||||||
INFO = [str(bot.user), "Prefixes: {}".format(', '.join(prefixes)),
|
INFO = [str(bot.user), "Prefixes: {}".format(', '.join(prefixes)),
|
||||||
'Language: {}'.format(lang),
|
'Language: {}'.format(lang),
|
||||||
"Red Bot Version: {}".format(red_pkg.version),
|
"Red Bot Version: {}".format(red_version),
|
||||||
"Discord.py Version: {}".format(dpy_version),
|
"Discord.py Version: {}".format(dpy_version),
|
||||||
"Shards: {}".format(bot.shard_count)]
|
"Shards: {}".format(bot.shard_count)]
|
||||||
|
|
||||||
@ -125,6 +128,9 @@ def init_events(bot, cli_flags):
|
|||||||
if invite_url:
|
if invite_url:
|
||||||
print("\nInvite URL: {}\n".format(invite_url))
|
print("\nInvite URL: {}\n".format(invite_url))
|
||||||
|
|
||||||
|
if bot.rpc_enabled:
|
||||||
|
await initialize(bot)
|
||||||
|
|
||||||
@bot.event
|
@bot.event
|
||||||
async def on_command_error(ctx, error):
|
async def on_command_error(ctx, error):
|
||||||
if isinstance(error, commands.MissingRequiredArgument):
|
if isinstance(error, commands.MissingRequiredArgument):
|
||||||
|
|||||||
82
redbot/core/rpc.py
Normal file
82
redbot/core/rpc.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from typing import NewType, TYPE_CHECKING
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aiohttp.web import Application
|
||||||
|
from aiohttp_json_rpc import JsonRpc
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .bot import Red
|
||||||
|
|
||||||
|
log = logging.getLogger('red.rpc')
|
||||||
|
JsonSerializable = NewType('JsonSerializable', dict)
|
||||||
|
|
||||||
|
_rpc = JsonRpc(logger=log)
|
||||||
|
|
||||||
|
_rpc_server = None # type: asyncio.AbstractServer
|
||||||
|
|
||||||
|
|
||||||
|
async def initialize(bot: "Red"):
|
||||||
|
global _rpc_server
|
||||||
|
|
||||||
|
app = Application(loop=bot.loop)
|
||||||
|
app.router.add_route('*', '/rpc', _rpc)
|
||||||
|
|
||||||
|
handler = app.make_handler()
|
||||||
|
|
||||||
|
_rpc_server = await bot.loop.create_server(handler, '127.0.0.1', 6133)
|
||||||
|
|
||||||
|
log.debug('Created RPC _rpc_server listener.')
|
||||||
|
|
||||||
|
|
||||||
|
def add_topic(topic_name: str):
|
||||||
|
"""
|
||||||
|
Adds a topic for clients to listen to.
|
||||||
|
|
||||||
|
:param topic_name:
|
||||||
|
"""
|
||||||
|
_rpc.add_topics(topic_name)
|
||||||
|
|
||||||
|
|
||||||
|
def notify(topic_name: str, data: JsonSerializable):
|
||||||
|
"""
|
||||||
|
Publishes a notification for the given topic name to all listening clients.
|
||||||
|
|
||||||
|
data MUST be json serializable.
|
||||||
|
|
||||||
|
note::
|
||||||
|
|
||||||
|
This method will fail silently.
|
||||||
|
|
||||||
|
:param topic_name:
|
||||||
|
:param data:
|
||||||
|
"""
|
||||||
|
_rpc.notify(topic_name, data)
|
||||||
|
|
||||||
|
|
||||||
|
def add_method(prefix, method):
|
||||||
|
"""
|
||||||
|
Makes a method available to RPC clients. The name given to clients will be as
|
||||||
|
follows::
|
||||||
|
|
||||||
|
"{}__{}".format(prefix, method.__name__)
|
||||||
|
|
||||||
|
note::
|
||||||
|
|
||||||
|
This method will fail silently.
|
||||||
|
|
||||||
|
:param prefix:
|
||||||
|
:param method:
|
||||||
|
MUST BE A COROUTINE OR OBJECT.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
_rpc.add_methods(
|
||||||
|
('', method),
|
||||||
|
prefix=prefix
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_up():
|
||||||
|
_rpc_server.close()
|
||||||
@ -2,3 +2,4 @@ appdirs
|
|||||||
youtube_dl
|
youtube_dl
|
||||||
raven
|
raven
|
||||||
colorama
|
colorama
|
||||||
|
aiohttp-json-rpc
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user