Skip to content

Commit e12cd7c

Browse files
committed
[PLUGINS] Add new plugin runner for managing webhooks and API calls
1 parent 5d9a159 commit e12cd7c

File tree

5 files changed

+191
-8
lines changed

5 files changed

+191
-8
lines changed

unmanic/libs/uiserver.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,11 +353,16 @@ def make_web_app(self):
353353
# Add widgets routes
354354
from unmanic.webserver.plugins import DataPanelRequestHandler
355355
from unmanic.webserver.plugins import PluginStaticFileHandler
356+
from unmanic.webserver.plugins import PluginAPIRequestHandler
356357
app.add_handlers(r'.*', [
357358
(
358359
PathMatches(r"/unmanic/panel/[^/]+(/(?!static/|assets$).*)?$"),
359360
DataPanelRequestHandler
360361
),
362+
(
363+
PathMatches(r"/unmanic/plugin_api/[^/]+(/(?!static/|assets$).*)?$"),
364+
PluginAPIRequestHandler
365+
),
361366
(r"/unmanic/panel/.*/static/(.*)", PluginStaticFileHandler, dict(
362367
path=tornado_settings['static_img']
363368
)),

unmanic/libs/unplugins/executor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ def __init__(self, plugins_directory=None):
5555
'id': 'frontend.panel',
5656
'has_flow': False,
5757
},
58+
{
59+
'id': 'frontend.plugin_api',
60+
'has_flow': False,
61+
},
5862
{
5963
'id': 'library_management.file_test',
6064
'has_flow': True,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
unmanic.plugin_api.py
6+
7+
Written by: Josh.5 <[email protected]>
8+
Date: 05 Mar 2022, (11:38 PM)
9+
10+
Copyright:
11+
Copyright (C) Josh Sunnex - All Rights Reserved
12+
13+
Permission is hereby granted, free of charge, to any person obtaining a copy
14+
of this software and associated documentation files (the "Software"), to deal
15+
in the Software without restriction, including without limitation the rights
16+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
copies of the Software, and to permit persons to whom the Software is
18+
furnished to do so, subject to the following conditions:
19+
20+
The above copyright notice and this permission notice shall be included in all
21+
copies or substantial portions of the Software.
22+
23+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
26+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
27+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
28+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
29+
OR OTHER DEALINGS IN THE SOFTWARE.
30+
31+
"""
32+
33+
from ..plugin_type_base import PluginType
34+
35+
36+
class PluginAPI(PluginType):
37+
name = "Frontend - PluginAPI"
38+
runner = "render_plugin_api"
39+
runner_docstring = """
40+
Runner function - provide a custom API endpoint managed by a plugin.
41+
42+
The 'data' object argument includes:
43+
content_type - The content type to be set when writing back to the browser.
44+
content - The content to print to the browser.
45+
path - The path received after the '/unmanic/plugin_api' path.
46+
arguments - A dictionary of GET arguments received.
47+
48+
:param data:
49+
:return:
50+
"""
51+
data_schema = {
52+
"content_type": {
53+
"required": True,
54+
"type": str,
55+
},
56+
"content": {
57+
"required": True,
58+
"type": dict,
59+
},
60+
"path": {
61+
"required": False,
62+
"type": str,
63+
},
64+
"arguments": {
65+
"required": False,
66+
"type": dict,
67+
},
68+
}
69+
test_data = {
70+
'content_type': 'application/json',
71+
'content': {},
72+
'path': "/webhook",
73+
'arguments': {'param': [b'true'], 'foo': [b'ba']},
74+
}

unmanic/webserver/helpers/plugins.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,40 @@ def get_enabled_plugin_data_panels():
159159
return plugin_handler.get_enabled_plugin_modules_by_type('frontend.panel')
160160

161161

162-
def exec_plugin_runner(data, plugin_id):
162+
def exec_data_panels_plugin_runner(data, plugin_id):
163+
"""
164+
Exec a frontend.panel plugin runner
165+
166+
:param data:
167+
:param plugin_id:
168+
:return:
169+
"""
163170
plugin_handler = PluginsHandler()
164171
return plugin_handler.exec_plugin_runner(data, plugin_id, 'frontend.panel')
165172

166173

174+
def get_enabled_plugin_plugin_apis():
175+
"""
176+
Returns a list of all enabled plugin APIs
177+
178+
:return:
179+
"""
180+
plugin_handler = PluginsHandler()
181+
return plugin_handler.get_enabled_plugin_modules_by_type('frontend.plugin_api')
182+
183+
184+
def exec_plugin_api_plugin_runner(data, plugin_id):
185+
"""
186+
Exec a frontend.plugin_api plugin runner
187+
188+
:param data:
189+
:param plugin_id:
190+
:return:
191+
"""
192+
plugin_handler = PluginsHandler()
193+
return plugin_handler.exec_plugin_runner(data, plugin_id, 'frontend.plugin_api')
194+
195+
167196
def save_enabled_plugin_flows_for_plugin_type(plugin_type, library_id, plugin_flow):
168197
"""
169198
Save a plugin flow given the plugin type and library ID
@@ -363,6 +392,7 @@ def prepare_plugin_info_and_settings(plugin_id, prefer_local=True, library_id=No
363392

364393
return plugin_data
365394

395+
366396
def check_if_plugin_is_installed(plugin_id):
367397
"""
368398
Returns true if the given plugin is installed
@@ -381,6 +411,7 @@ def check_if_plugin_is_installed(plugin_id):
381411

382412
return plugin_installed
383413

414+
384415
def update_plugin_settings(plugin_id, settings, library_id=None):
385416
"""
386417
Updates the settings for the requested plugin_id

unmanic/webserver/plugins.py

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
3131
"""
3232
import os
33+
import sys
34+
import traceback
3335

3436
import tornado.web
3537
import tornado.log
@@ -39,9 +41,15 @@
3941

4042
def get_plugin_by_path(path):
4143
# Get the plugin ID from the url
42-
plugin_id = path.split('/')[3]
43-
# Fetch all frontend plugins
44-
results = plugins.get_enabled_plugin_data_panels()
44+
split_path = path.split('/')
45+
plugin_type = split_path[2]
46+
plugin_id = split_path[3]
47+
if plugin_type == 'plugin_api':
48+
# Fetch all api plugins
49+
results = plugins.get_enabled_plugin_plugin_apis()
50+
else:
51+
# Fetch all frontend plugins
52+
results = plugins.get_enabled_plugin_data_panels()
4553
# Check if their path matches
4654
plugin_module = None
4755
for result in results:
@@ -75,7 +83,7 @@ def handle_panel_request(self):
7583
'path': "/" + "/".join(path),
7684
'uri': self.request.uri,
7785
'query': self.request.query,
78-
'arguments': self.request.arguments
86+
'arguments': self.request.arguments,
7987
}
8088
plugin_module = get_plugin_by_path(self.request.path)
8189
if not plugin_module:
@@ -84,10 +92,9 @@ def handle_panel_request(self):
8492
return
8593

8694
# Run plugin and fetch return data
87-
if not plugins.exec_plugin_runner(data, plugin_module.get('plugin_id')):
95+
if not plugins.exec_data_panels_plugin_runner(data, plugin_module.get('plugin_id')):
8896
tornado.log.app_log.exception(
89-
"Exception while carrying out plugin runner on postprocessor task result '{}'".format(
90-
plugin_module.get('plugin_id')))
97+
"Exception while carrying out plugin runner on DataPanel '{}'".format(plugin_module.get('plugin_id')))
9198

9299
self.render_data(data)
93100
return
@@ -97,6 +104,68 @@ def render_data(self, data):
97104
self.write(data.get('content'))
98105

99106

107+
class PluginAPIRequestHandler(tornado.web.RequestHandler):
108+
name = None
109+
110+
def initialize(self):
111+
self.name = 'PluginAPI'
112+
113+
def get(self, path):
114+
self.handle_panel_request()
115+
116+
def post(self, path):
117+
self.handle_panel_request()
118+
119+
def handle_panel_request(self):
120+
path = list(filter(None, self.request.path.split('/')[4:]))
121+
122+
# Generate default data
123+
data = {
124+
'content_type': 'application/json',
125+
'content': {},
126+
'path': "/" + "/".join(path),
127+
'uri': self.request.uri,
128+
'query': self.request.query,
129+
'arguments': self.request.arguments,
130+
'body': self.request.body,
131+
}
132+
plugin_module = get_plugin_by_path(self.request.path)
133+
if not plugin_module:
134+
self.set_status(404, reason="404 Not Found")
135+
status_code = self.get_status()
136+
self.write({
137+
'error': "%(code)d: %(message)s" % {"code": status_code, "message": self._reason},
138+
'messages': {},
139+
})
140+
return
141+
142+
# Run plugin and fetch return data
143+
try:
144+
if not plugins.exec_plugin_api_plugin_runner(data, plugin_module.get('plugin_id')):
145+
tornado.log.app_log.exception(
146+
"Exception while carrying out plugin runner on PluginAPI '{}'".format(plugin_module.get('plugin_id')))
147+
except Exception as e:
148+
self.set_status(500, reason="Error running plugin API: {}".format(str(e)))
149+
status_code = self.get_status()
150+
exc_info = sys.exc_info()
151+
traceback_lines = []
152+
if exc_info and exc_info[0]:
153+
for line in traceback.format_exception(*exc_info):
154+
traceback_lines.append(line)
155+
self.write({
156+
'error': "%(code)d: %(message)s" % {"code": status_code, "message": self._reason},
157+
'messages': {},
158+
'traceback': traceback_lines
159+
})
160+
return
161+
162+
self.render_data(data)
163+
164+
def render_data(self, data):
165+
self.set_header("Content-Type", data.get('content_type', 'application/json'))
166+
self.write(data.get('content'))
167+
168+
100169
class PluginStaticFileHandler(tornado.web.StaticFileHandler):
101170
"""
102171
A static file handler which serves static content from a plugin '/static/' directory.

0 commit comments

Comments
 (0)