Skip to content

Commit 519c7bb

Browse files
committed
complete ui plugin
1 parent c1f1a71 commit 519c7bb

File tree

6 files changed

+342
-44
lines changed

6 files changed

+342
-44
lines changed

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ confidence=
5252
# no Warning level messages displayed, use"--disable=all --enable=classes
5353
# --disable=W"
5454
# disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,C0103,W1401,E203,C0326
55-
disable=invalid-name,R0904
55+
disable=invalid-name,R0904,trailing-whitespace,too-many-lines
5656

5757
# Enable the message, report, category or checker with the given id(s). You can
5858
# either give multiple identifier separated by comma (,) or put this option

src/wechaty/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@
4848
from .accessory import Accessory
4949
from .plugin import (
5050
WechatyPlugin,
51-
WechatyPluginOptions
5251
)
52+
from .schema import WechatyPluginOptions
53+
5354
from .wechaty import (
5455
Wechaty,
5556
WechatyOptions,

src/wechaty/plugin.py

Lines changed: 165 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@
2121
from __future__ import annotations
2222

2323
import asyncio
24+
from dataclasses import asdict
25+
import inspect
2426
from logging import Logger
2527
import os
2628
import sys
2729
import re
2830
from abc import ABC
2931
from collections import OrderedDict
3032
from copy import deepcopy
31-
from dataclasses import dataclass
3233
from datetime import datetime
33-
from enum import Enum
3434
from telnetlib import Telnet
3535
import socket
36+
import json
3637
from typing import (
3738
TYPE_CHECKING,
3839
Iterable,
@@ -51,7 +52,7 @@
5152
from apscheduler.triggers.cron import CronTrigger
5253
from apscheduler.triggers.interval import IntervalTrigger
5354
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
54-
from quart import Quart
55+
from quart import Quart, request, send_file, Response
5556
from quart_cors import cors
5657

5758
from wechaty_puppet import (
@@ -63,6 +64,18 @@
6364
ScanStatus
6465
)
6566

67+
from wechaty.schema import (
68+
NavDTO,
69+
NavMetadata,
70+
PluginStatus,
71+
StaticFileCacher,
72+
WechatyPluginOptions,
73+
WechatySchedulerOptions,
74+
success,
75+
error
76+
)
77+
from wechaty.types import EndPoint
78+
6679
from .config import config
6780

6881
from .exceptions import (
@@ -175,26 +188,6 @@ async def shutdown(trigger: Callable[[], Coroutine[Any, Any, Any]]) -> None:
175188
sys.exit(0)
176189

177190

178-
@dataclass
179-
class WechatyPluginOptions:
180-
"""options for wechaty plugin"""
181-
name: Optional[str] = None
182-
metadata: Optional[dict] = None
183-
184-
185-
@dataclass
186-
class WechatySchedulerOptions:
187-
"""options for wechaty scheduler"""
188-
job_store: Union[str, SQLAlchemyJobStore] = f'sqlite:///{config.cache_dir}/job.db'
189-
job_store_alias: str = 'wechaty-scheduler'
190-
191-
192-
class PluginStatus(Enum):
193-
"""plugin running status"""
194-
Running = 0
195-
Stopped = 1
196-
197-
198191
class WechatySchedulerMixin:
199192
"""scheduler mixin for wechaty
200193
"""
@@ -388,6 +381,12 @@ class WechatyPlugin(ABC, WechatySchedulerMixin, WechatyEventMixin):
388381
389382
listen events from
390383
"""
384+
AUTHOR = "wechaty"
385+
AVATAR = 'https://avatars.githubusercontent.com/u/10242208?v=4'
386+
AUTHOR_LINK = "https://github.com/wj-Mcat"
387+
ICON = "https://wechaty.js.org/img/wechaty-icon.svg"
388+
VIEW_URL = None
389+
UI_DIR = None
391390

392391
def __init__(self, options: Optional[WechatyPluginOptions] = None):
393392
self.output: Dict[str, Any] = {}
@@ -398,6 +397,60 @@ def __init__(self, options: Optional[WechatyPluginOptions] = None):
398397
self._default_logger: Optional[Logger] = None
399398
self._cache_dir: Optional[str] = None
400399

400+
self.setting_file: str = os.path.join(self.cache_dir, 'setting.json')
401+
402+
def metadata(self) -> NavMetadata:
403+
"""get the default nav metadata
404+
405+
Returns:
406+
NavMetadata: the instance of metadata
407+
"""
408+
return NavMetadata(
409+
author=self.AUTHOR,
410+
author_link=self.AUTHOR_LINK,
411+
icon=self.ICON,
412+
avatar=self.AVATAR,
413+
view_url=self.VIEW_URL
414+
)
415+
416+
@property
417+
def setting(self) -> dict:
418+
"""get the setting of a plugin"""
419+
with open(self.setting_file, 'r', encoding='utf-8') as f:
420+
setting = json.load(f)
421+
return setting
422+
423+
@setting.setter
424+
def setting(self, value: dict) -> None:
425+
"""update the plugin setting"""
426+
with open(self.setting_file, 'w', encoding='utf-8') as f:
427+
json.dump(value, f, ensure_ascii=True)
428+
429+
def get_ui_dir(self) -> Optional[str]:
430+
"""get the ui asset dir
431+
"""
432+
# 1. get the customized ui dir according to the static UI_DIR attribute
433+
if self.UI_DIR:
434+
if os.path.exists(self.UI_DIR):
435+
self.logger.info("finding the UI_DIR<%s> for plugin<%s>", self.UI_DIR, type(self))
436+
return self.UI_DIR
437+
438+
# 2. get the default uidir: ui/dist
439+
plugin_dir_path = inspect.getsourcefile(self.__class__)
440+
if plugin_dir_path is None:
441+
return None
442+
plugin_dir = os.path.dirname(str(plugin_dir_path))
443+
ui_dir = os.path.join(plugin_dir, 'ui')
444+
if os.path.exists(ui_dir):
445+
self.logger.info("finding the UI_DIR<%s> for plugin<%s>", ui_dir, type(self))
446+
return ui_dir
447+
448+
self.logger.warning(
449+
'the registered UI_DIR<%s> not exist, so to fetch default ui dir',
450+
self.UI_DIR
451+
)
452+
return None
453+
401454
def set_bot(self, bot: Wechaty) -> None:
402455
"""set bot instance to WechatyPlugin
403456
@@ -503,18 +556,6 @@ async def on_running(self) -> None:
503556
"""hook the plugin event when active"""
504557

505558

506-
PluginTree = Dict[str, Union[str, List[str]]]
507-
EndPoint = Tuple[str, int]
508-
509-
510-
def _load_default_plugins() -> List[WechatyPlugin]:
511-
"""
512-
load the system default plugins to enable more default features
513-
Returns:
514-
"""
515-
# TODO: to be implemented
516-
517-
518559
class WechatyPluginManager: # pylint: disable=too-many-instance-attributes
519560
"""manage the wechaty plugin, It will support some features."""
520561

@@ -543,6 +584,83 @@ def __init__(
543584
scheduler.add_jobstore(scheduler_options.job_store, scheduler_options.job_store_alias)
544585
self.scheduler: AsyncIOScheduler = scheduler
545586

587+
self.static_file_cacher = StaticFileCacher()
588+
589+
async def register_ui_view(self, app: Quart) -> None:
590+
"""register the system ui view"""
591+
@app.route('/plugins/list')
592+
async def get_plugins_nav() -> Response:
593+
594+
navs = []
595+
for plugin in self.plugins():
596+
nav = NavDTO(
597+
name=plugin.name,
598+
status=int(
599+
self._plugin_status[plugin.name] == PluginStatus.Running
600+
))
601+
nav.update_metadata(plugin.metadata())
602+
navs.append(asdict(nav))
603+
return success(navs)
604+
605+
@app.route('/plugins/status', methods=["POST", 'PUT'])
606+
async def change_status() -> Response:
607+
data = await request.get_json()
608+
name = data.get('plugin_name', None)
609+
status = data.get('status', None)
610+
if name is None or status is None:
611+
return error('the plugin_name and status field is required ...')
612+
status = int(status)
613+
if status == 0:
614+
await self.stop_plugin(name)
615+
elif status == 1:
616+
await self.start_plugin(name)
617+
else:
618+
return error("unexpected plugin status, which should be one of<Stopped, Running>")
619+
return success('changes success ...')
620+
621+
@app.route('/plugins/setting', methods=['GET'])
622+
async def get_plugin_setting() -> Response:
623+
name = request.args.get('plugin_name', None)
624+
if name is None:
625+
return error('plugin_name field is required')
626+
627+
if name not in self._plugins:
628+
return error(f'plugin<{name}> not exist ...')
629+
plugin: WechatyPlugin = self._plugins[name]
630+
631+
config_entry = 'get_setting'
632+
if not hasattr(plugin, config_entry):
633+
return error(f'this plugin<{name}> contains no setting ...')
634+
635+
return success(plugin.setting)
636+
637+
@app.route('/plugins/setting', methods=['POST', 'PUT'])
638+
async def update_plugin_setting() -> Response:
639+
data: dict = await request.get_json()
640+
if 'setting' not in data:
641+
return error("setting field is required ...")
642+
643+
name = data.get('plugin_name', None)
644+
if not name:
645+
return error("plugin_name field is required ...")
646+
647+
if name not in self._plugins:
648+
return error(f'plugin<{name}> not exist ...')
649+
650+
plugin: WechatyPlugin = self._plugins[name]
651+
plugin.setting = data['setting']
652+
return success(None)
653+
654+
@app.route("/js/<string:name>", methods=['GET'])
655+
@app.route("/css/<string:name>", methods=['GET'])
656+
@app.route("/img/<string:name>", methods=['GET'])
657+
async def get_all_static_file(name: str) -> Response:
658+
file_path = self.static_file_cacher.find_file_path(name)
659+
if not file_path:
660+
return Response('')
661+
response = await send_file(file_path)
662+
return response
663+
546664
# pylint: disable=R1711
547665
@staticmethod
548666
def _load_plugin_from_local_file(plugin_path: str) -> Optional[WechatyPlugin]:
@@ -594,6 +712,8 @@ def add_plugin(self, plugin: Union[str, WechatyPlugin]) -> None:
594712
plugin_instance = plugin
595713

596714
# set the scheduler
715+
self.static_file_cacher.add_dir(plugin_instance.cache_dir)
716+
597717
plugin_instance.scheduler = self.scheduler
598718
self._plugins[plugin_instance.name] = plugin_instance
599719
# default wechaty plugin status is Running
@@ -689,12 +809,16 @@ async def start(self) -> None:
689809
# 3. list all valid endpoints in web service
690810
# checking the number of registered blueprints
691811
routes_txt = _list_routes_txt(self.app)
692-
# if len(routes_txt) == 0:
693-
# log.warning(
694-
# 'there is not registed blueprint in the plugins, '
695-
# 'so bot will not start the web service'
696-
# )
697-
# return
812+
if len(routes_txt) == 0:
813+
log.warning(
814+
'there is not registed blueprint in the plugins, '
815+
'so bot will not start the web service'
816+
)
817+
return
818+
819+
await self.register_ui_view(self.app)
820+
# re-fetch the routes info
821+
routes_txt = _list_routes_txt(self.app)
698822

699823
log.info('============================starting web service========================')
700824
log.info('starting web service at endpoint: <{%s}:{%d}>', host, port)

0 commit comments

Comments
 (0)