Skip to content

Commit 7454a80

Browse files
authored
Merge pull request #2972 from kyb3r/local-plugins
Local plugins
2 parents 69a272e + bf1668a commit 7454a80

File tree

8 files changed

+85
-48
lines changed

8 files changed

+85
-48
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ node_modules/
133133
config.json
134134
plugins/
135135
!plugins/registry.json
136+
!plugins/@local/
136137
temp/
137138
test.py
138139

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ package-lock.json
130130
node_modules/
131131

132132
# Modmail
133-
config.json
134-
plugins/
133+
plugins/*
135134
!plugins/registry.json
135+
!plugins/@local
136+
config.json
136137
temp/
137138
test.py
138139
stack.yml

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
77
however, insignificant breaking changes do not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319). If you're a plugin developer, note the "BREAKING" section.
88

9+
10+
# v3.8.6
11+
12+
### Added
13+
14+
- Ability to install local plugins without relying on git / external sources
15+
- Simply add your extension to plugins/@local, and use `?plugin add local/plugin-name` to load the plugin as normal
16+
- Updated deps for requirements.min.txt and pyproject.toml
17+
918
# v3.8.5
1019

1120
### Added

bot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "3.8.5"
1+
__version__ = "3.8.6"
22

33

44
import asyncio

cogs/plugins.py

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,28 @@ class InvalidPluginError(commands.BadArgument):
3131

3232

3333
class Plugin:
34-
def __init__(self, user, repo, name, branch=None):
35-
self.user = user
36-
self.repo = repo
37-
self.name = name
38-
self.branch = branch if branch is not None else "master"
39-
self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip"
40-
self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}"
34+
def __init__(self, user, repo=None, name=None, branch=None):
35+
if repo is None:
36+
self.user = "@local"
37+
self.repo = "@local"
38+
self.name = user
39+
self.local = True
40+
self.branch = "@local"
41+
self.url = f"@local/{user}"
42+
self.link = f"@local/{user}"
43+
else:
44+
self.user = user
45+
self.repo = repo
46+
self.name = name
47+
self.local = False
48+
self.branch = branch if branch is not None else "master"
49+
self.url = f"https://github.com/{user}/{repo}/archive/{self.branch}.zip"
50+
self.link = f"https://github.com/{user}/{repo}/tree/{self.branch}/{name}"
4151

4252
@property
4353
def path(self):
54+
if self.local:
55+
return PurePath("plugins") / "@local" / self.name
4456
return PurePath("plugins") / self.user / self.repo / f"{self.name}-{self.branch}"
4557

4658
@property
@@ -49,6 +61,8 @@ def abs_path(self):
4961

5062
@property
5163
def cache_path(self):
64+
if self.local:
65+
raise ValueError("No cache path for local plugins!")
5266
return (
5367
Path(__file__).absolute().parent.parent
5468
/ "temp"
@@ -58,20 +72,27 @@ def cache_path(self):
5872

5973
@property
6074
def ext_string(self):
75+
if self.local:
76+
return f"plugins.@local.{self.name}.{self.name}"
6177
return f"plugins.{self.user}.{self.repo}.{self.name}-{self.branch}.{self.name}"
6278

6379
def __str__(self):
80+
if self.local:
81+
return f"@local/{self.name}"
6482
return f"{self.user}/{self.repo}/{self.name}@{self.branch}"
6583

6684
def __lt__(self, other):
6785
return self.name.lower() < other.name.lower()
6886

6987
@classmethod
7088
def from_string(cls, s, strict=False):
71-
if not strict:
72-
m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s)
73-
else:
74-
m = match(r"^(.+?)/(.+?)/(.+?)@(.+?)$", s)
89+
m = match(r"^@?local/(.+)$", s)
90+
if m is None:
91+
if not strict:
92+
m = match(r"^(.+?)/(.+?)/(.+?)(?:@(.+?))?$", s)
93+
else:
94+
m = match(r"^(.+?)/(.+?)/(.+?)@(.+?)$", s)
95+
7596
if m is not None:
7697
return Plugin(*m.groups())
7798
raise InvalidPluginError("Cannot decipher %s.", s) # pylint: disable=raising-format-tuple
@@ -152,9 +173,12 @@ async def initial_load_plugins(self):
152173
await self.bot.config.update()
153174

154175
async def download_plugin(self, plugin, force=False):
155-
if plugin.abs_path.exists() and not force:
176+
if plugin.abs_path.exists() and (not force or plugin.local):
156177
return
157178

179+
if plugin.local:
180+
raise InvalidPluginError(f"Local plugin {plugin} not found!")
181+
158182
plugin.abs_path.mkdir(parents=True, exist_ok=True)
159183

160184
if plugin.cache_path.exists() and not force:
@@ -290,7 +314,7 @@ async def parse_user_input(self, ctx, plugin_name, check_version=False):
290314
embed = discord.Embed(
291315
description="Invalid plugin name, double check the plugin name "
292316
"or use one of the following formats: "
293-
"username/repo/plugin, username/repo/plugin@branch.",
317+
"username/repo/plugin-name, username/repo/plugin-name@branch, local/plugin-name.",
294318
color=self.bot.error_color,
295319
)
296320
await ctx.send(embed=embed)
@@ -314,7 +338,8 @@ async def plugins_add(self, ctx, *, plugin_name: str):
314338
Install a new plugin for the bot.
315339
316340
`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`,
317-
or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).
341+
or a direct reference to a GitHub hosted plugin (in the format `user/repo/name[@branch]`)
342+
or `local/name` for local plugins.
318343
"""
319344

320345
plugin = await self.parse_user_input(ctx, plugin_name, check_version=True)
@@ -335,10 +360,16 @@ async def plugins_add(self, ctx, *, plugin_name: str):
335360
)
336361
return await ctx.send(embed=embed)
337362

338-
embed = discord.Embed(
339-
description=f"Starting to download plugin from {plugin.link}...",
340-
color=self.bot.main_color,
341-
)
363+
if plugin.local:
364+
embed = discord.Embed(
365+
description=f"Starting to load local plugin from {plugin.link}...",
366+
color=self.bot.main_color,
367+
)
368+
else:
369+
embed = discord.Embed(
370+
description=f"Starting to download plugin from {plugin.link}...",
371+
color=self.bot.main_color,
372+
)
342373
msg = await ctx.send(embed=embed)
343374

344375
try:
@@ -395,7 +426,7 @@ async def plugins_remove(self, ctx, *, plugin_name: str):
395426
Remove an installed plugin of the bot.
396427
397428
`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference
398-
to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).
429+
to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) or `local/name` for local plugins.
399430
"""
400431
plugin = await self.parse_user_input(ctx, plugin_name)
401432
if plugin is None:
@@ -416,17 +447,18 @@ async def plugins_remove(self, ctx, *, plugin_name: str):
416447

417448
self.bot.config["plugins"].remove(str(plugin))
418449
await self.bot.config.update()
419-
shutil.rmtree(
420-
plugin.abs_path,
421-
onerror=lambda *args: logger.warning(
422-
"Failed to remove plugin files %s: %s", plugin, str(args[2])
423-
),
424-
)
425-
try:
426-
plugin.abs_path.parent.rmdir()
427-
plugin.abs_path.parent.parent.rmdir()
428-
except OSError:
429-
pass # dir not empty
450+
if not plugin.local:
451+
shutil.rmtree(
452+
plugin.abs_path,
453+
onerror=lambda *args: logger.warning(
454+
"Failed to remove plugin files %s: %s", plugin, str(args[2])
455+
),
456+
)
457+
try:
458+
plugin.abs_path.parent.rmdir()
459+
plugin.abs_path.parent.parent.rmdir()
460+
except OSError:
461+
pass # dir not empty
430462

431463
embed = discord.Embed(
432464
description="The plugin is successfully uninstalled.", color=self.bot.main_color
@@ -477,7 +509,7 @@ async def plugins_update(self, ctx, *, plugin_name: str = None):
477509
Update a plugin for the bot.
478510
479511
`plugin_name` can be the name of the plugin found in `{prefix}plugin registry`, or a direct reference
480-
to a GitHub hosted plugin (in the format `user/repo/name[@branch]`).
512+
to a GitHub hosted plugin (in the format `user/repo/name[@branch]`) or `local/name` for local plugins.
481513
482514
To update all plugins, do `{prefix}plugins update`.
483515
"""
@@ -514,7 +546,7 @@ async def plugins_reset(self, ctx):
514546
shutil.rmtree(cache_path)
515547

516548
for entry in os.scandir(Path(__file__).absolute().parent.parent / "plugins"):
517-
if entry.is_dir():
549+
if entry.is_dir() and entry.name != "@local":
518550
shutil.rmtree(entry.path)
519551
logger.warning("Removing %s.", entry.name)
520552

plugins/@local/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ exclude = '''
2121

2222
[tool.poetry]
2323
name = 'Modmail'
24-
version = '3.6.0'
24+
version = '3.8.6'
2525
description = "Modmail is similar to Reddit's Modmail, both in functionality and purpose. It serves as a shared inbox for server staff to communicate with their users in a seamless way."
2626
license = 'AGPL-3.0-only'
2727
authors = [
@@ -36,7 +36,7 @@ keywords = ['discord', 'modmail']
3636

3737
[tool.poetry.dependencies]
3838
python = "^3.7"
39-
"discord.py" = "./discord.py-1.5.2.tar.gz"
39+
"discord.py" = "discord.py==1.6.0"
4040
uvloop = {version = ">=0.12.0", markers = "sys_platform != 'win32'"}
4141
python-dotenv = ">=0.10.3"
4242
parsedatetime = "^2.6"

requirements.min.txt

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
1-
# Generated as of June, 2020
1+
# Generated as of March, 2021
22
# This is the bare minimum requirements.txt for running Modmail.
33
# To install requirements.txt run: pip install -r requirements.min.txt
44

55
aiohttp==3.6.2
6-
async-timeout==3.0.1
7-
attrs==19.3.0
8-
chardet==3.0.4
9-
./discord.py-1.5.2.tar.gz
6+
discord.py==1.6.0
107
dnspython==1.16.0
118
emoji==0.5.4
12-
future==0.18.2
13-
idna==2.9
149
isodate==0.6.0
1510
motor==2.1.0
16-
multidict==4.7.6
1711
natural==0.2.0
1812
parsedatetime==2.6
1913
pymongo==3.10.1
2014
python-dateutil==2.8.1
2115
python-dotenv==0.14.0
22-
six==1.15.0
23-
websockets==8.1
24-
yarl==1.4.2
16+
websockets==8.1

0 commit comments

Comments
 (0)