21
21
from __future__ import annotations
22
22
23
23
import asyncio
24
+ from dataclasses import asdict
25
+ import inspect
24
26
from logging import Logger
25
27
import os
26
28
import sys
27
29
import re
28
30
from abc import ABC
29
31
from collections import OrderedDict
30
32
from copy import deepcopy
31
- from dataclasses import dataclass
32
33
from datetime import datetime
33
- from enum import Enum
34
34
from telnetlib import Telnet
35
35
import socket
36
+ import json
36
37
from typing import (
37
38
TYPE_CHECKING ,
38
39
Iterable ,
51
52
from apscheduler .triggers .cron import CronTrigger
52
53
from apscheduler .triggers .interval import IntervalTrigger
53
54
from apscheduler .jobstores .sqlalchemy import SQLAlchemyJobStore
54
- from quart import Quart
55
+ from quart import Quart , request , send_file , Response
55
56
from quart_cors import cors
56
57
57
58
from wechaty_puppet import (
63
64
ScanStatus
64
65
)
65
66
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
+
66
79
from .config import config
67
80
68
81
from .exceptions import (
@@ -175,26 +188,6 @@ async def shutdown(trigger: Callable[[], Coroutine[Any, Any, Any]]) -> None:
175
188
sys .exit (0 )
176
189
177
190
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
-
198
191
class WechatySchedulerMixin :
199
192
"""scheduler mixin for wechaty
200
193
"""
@@ -388,6 +381,12 @@ class WechatyPlugin(ABC, WechatySchedulerMixin, WechatyEventMixin):
388
381
389
382
listen events from
390
383
"""
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
391
390
392
391
def __init__ (self , options : Optional [WechatyPluginOptions ] = None ):
393
392
self .output : Dict [str , Any ] = {}
@@ -398,6 +397,60 @@ def __init__(self, options: Optional[WechatyPluginOptions] = None):
398
397
self ._default_logger : Optional [Logger ] = None
399
398
self ._cache_dir : Optional [str ] = None
400
399
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
+
401
454
def set_bot (self , bot : Wechaty ) -> None :
402
455
"""set bot instance to WechatyPlugin
403
456
@@ -503,18 +556,6 @@ async def on_running(self) -> None:
503
556
"""hook the plugin event when active"""
504
557
505
558
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
-
518
559
class WechatyPluginManager : # pylint: disable=too-many-instance-attributes
519
560
"""manage the wechaty plugin, It will support some features."""
520
561
@@ -543,6 +584,83 @@ def __init__(
543
584
scheduler .add_jobstore (scheduler_options .job_store , scheduler_options .job_store_alias )
544
585
self .scheduler : AsyncIOScheduler = scheduler
545
586
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
+
546
664
# pylint: disable=R1711
547
665
@staticmethod
548
666
def _load_plugin_from_local_file (plugin_path : str ) -> Optional [WechatyPlugin ]:
@@ -594,6 +712,8 @@ def add_plugin(self, plugin: Union[str, WechatyPlugin]) -> None:
594
712
plugin_instance = plugin
595
713
596
714
# set the scheduler
715
+ self .static_file_cacher .add_dir (plugin_instance .cache_dir )
716
+
597
717
plugin_instance .scheduler = self .scheduler
598
718
self ._plugins [plugin_instance .name ] = plugin_instance
599
719
# default wechaty plugin status is Running
@@ -689,12 +809,16 @@ async def start(self) -> None:
689
809
# 3. list all valid endpoints in web service
690
810
# checking the number of registered blueprints
691
811
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 )
698
822
699
823
log .info ('============================starting web service========================' )
700
824
log .info ('starting web service at endpoint: <{%s}:{%d}>' , host , port )
0 commit comments