From f71b34358bee2ed2971920b208e7b1ff6703ea22 Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 11 Aug 2020 14:01:03 +0800 Subject: [PATCH 01/12] add gitlab event plugin --- .../gitlab_event_plugin/__init__.py | 0 .../gitlab_event_plugin/options.py | 49 +++++++++++++ .../gitlab_event_plugin/plugin.py | 73 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 src/wechaty_plugin_contrib/gitlab_event_plugin/__init__.py create mode 100644 src/wechaty_plugin_contrib/gitlab_event_plugin/options.py create mode 100644 src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py diff --git a/src/wechaty_plugin_contrib/gitlab_event_plugin/__init__.py b/src/wechaty_plugin_contrib/gitlab_event_plugin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py b/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py new file mode 100644 index 0000000..c142b16 --- /dev/null +++ b/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py @@ -0,0 +1,49 @@ +""" +Code Generator - https://github.com/wj-Mcat/code-generator + +Authors: Jingjing WU (吴京京) + +2020-now @ Copyright wj-Mcat + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from typing import Optional, List +from wechaty import ( + WechatyPluginOptions +) +from dataclasses import dataclass + + +@dataclass +class GitlabHookItem: + """plugin can hook many gitlab projects, so the GitlabHookItem can specific + the project + """ + id: str + + room_topic: Optional[str] = None + room_id: Optional[str] = None + + contact_name: Optional[str] = None + contact_id: Optional[str] = None + + +@dataclass +class GitlabEventOptions(WechatyPluginOptions): + hook_url: Optional[str] = None + secret_token: Optional[str] = None + + listen_port: Optional[int] = 5100 + + hook_items: List[GitlabHookItem] = lambda: [] diff --git a/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py b/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py new file mode 100644 index 0000000..99ee96b --- /dev/null +++ b/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py @@ -0,0 +1,73 @@ +""" +Code Generator - https://github.com/wj-Mcat/code-generator + +Authors: Jingjing WU (吴京京) + +2020-now @ Copyright wj-Mcat + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from wechaty import ( + Wechaty, WechatyPlugin, +) + +from aiohttp import web +from wechaty_puppet import RoomQueryFilter, get_logger + +from .options import GitlabHookItem, GitlabEventOptions + +log = get_logger("GitlabEventPlugin") +routes = web.RouteTableDef() + + +@routes.get('/') +async def receive_message(request: web.Request): + """""" + log.info('') + bot: Wechaty = request.app.get('bot') + options: GitlabEventOptions = request.app.get('bot-options') + + return web.json_response(text='hello wechaty web bot') + + +class GitlabEventPlugin(WechatyPlugin): + """""" + + def __init__(self, options: GitlabEventOptions): + super().__init__(options) + self.options = options + + @property + def name(self) -> str: + return 'gitlab-event-plugin' + + async def init_plugin(self, wechaty: Wechaty): + """init the gitlab event plugin""" + log.info('starting the server') + self.bot = wechaty + + # start the server + app = web.Application() + app['bot'] = wechaty + app['bot-options'] = self.options + app.add_routes(routes) + + runner = web.AppRunner(app) + await runner.setup() + + site = web.TCPSite(runner, '0.0.0.0', self.options.listen_port) + await site.start() + log.info( + f'the server has started ... 0.0.0.0: {self.options.listen_port}' + ) From 55047bea5cd8ce78e5ee5f3f345ef04460bad15a Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 11 Aug 2020 14:07:02 +0800 Subject: [PATCH 02/12] clean the CI --- src/wechaty_plugin_contrib/gitlab_event_plugin/options.py | 2 +- src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py | 2 +- src/wechaty_plugin_contrib/validate_plugin.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py b/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py index c142b16..3ef5385 100644 --- a/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py +++ b/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py @@ -46,4 +46,4 @@ class GitlabEventOptions(WechatyPluginOptions): listen_port: Optional[int] = 5100 - hook_items: List[GitlabHookItem] = lambda: [] + hook_items: List[GitlabHookItem] = [] diff --git a/src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py b/src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py index 29b368e..b1044b5 100644 --- a/src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py +++ b/src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py @@ -18,7 +18,7 @@ limitations under the License. """ -from wechaty import Wechaty +from wechaty import Wechaty # type: ignore from wechaty import ( diff --git a/src/wechaty_plugin_contrib/validate_plugin.py b/src/wechaty_plugin_contrib/validate_plugin.py index 4e7248c..38765bb 100644 --- a/src/wechaty_plugin_contrib/validate_plugin.py +++ b/src/wechaty_plugin_contrib/validate_plugin.py @@ -2,13 +2,13 @@ plugin validator """ from __future__ import annotations -from wechaty import WechatyPlugin +from wechaty import WechatyPlugin # type: ignore def validate_plugin(plugin: WechatyPlugin): """validate the plugin""" # check the name of the plugin - if type(plugin) is WechatyPlugin: + if isinstance(plugin, WechatyPlugin): raise Exception('plugin instance should a subclass of WechatyPlugin') # check the name of plugin From c9b88cb2fcbc62af669676ea08f5ddb702c2dd4a Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 11 Aug 2020 14:12:47 +0800 Subject: [PATCH 03/12] change github action python version --- .github/workflows/pypi.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 6415786..632bdb4 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -16,21 +16,21 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.7 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - + - name: run test - run: + run: make test - + deploy: runs-on: ubuntu-latest From dc45ac5bed102ab8440343fe373bcfe56fada7ac Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 11 Aug 2020 14:25:30 +0800 Subject: [PATCH 04/12] add daily plugin basic test to pass the CI --- tests/daily_plugin_test.py | 8 -------- tests/test_daily_plugin.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 8 deletions(-) delete mode 100644 tests/daily_plugin_test.py create mode 100644 tests/test_daily_plugin.py diff --git a/tests/daily_plugin_test.py b/tests/daily_plugin_test.py deleted file mode 100644 index 9c3f955..0000000 --- a/tests/daily_plugin_test.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Unit test -""" -import pytest - - -def daily_plugin_test(): - pass diff --git a/tests/test_daily_plugin.py b/tests/test_daily_plugin.py new file mode 100644 index 0000000..da00599 --- /dev/null +++ b/tests/test_daily_plugin.py @@ -0,0 +1,10 @@ +""" +Unit test +""" +import pytest +from wechaty_plugin_contrib.daily_plugin import DailyPluginOptions + + +def test_plugin(): + options = DailyPluginOptions(name='daily-plugin') + assert options is not None From 25c6c2c5951e4c12fdaf72eef16c81cf99eb04b5 Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 11 Aug 2020 14:29:41 +0800 Subject: [PATCH 05/12] update the package dependency --- requirements-dev.txt | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9819da0..b5c19e4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,3 +11,4 @@ pyee requests qrcode apscheduler +semver diff --git a/requirements.txt b/requirements.txt index f650b39..3843131 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ wechaty>=0.5.dev1 jieba +aiohttp From 0f09115bfbb58c0830b6928ca7a9f902ae066cd6 Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 11 Aug 2020 14:31:00 +0800 Subject: [PATCH 06/12] update contrib version to 0.0.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8a9ecc2..4e379d2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.1 \ No newline at end of file +0.0.2 From 6e3a89b7369515a3b84785b663629db29e01a3c8 Mon Sep 17 00:00:00 2001 From: wj-Mcat <1435130236@qq.com> Date: Thu, 8 Oct 2020 16:31:24 +0800 Subject: [PATCH 07/12] add github webhook plugin --- .../github_webhook_plugin/__init__.py | 2 + .../github_webhook_plugin/options.py | 58 ++++++ .../github_webhook_plugin/plugin.py | 195 ++++++++++++++++++ .../gitlab_event_plugin/options.py | 10 +- .../gitlab_event_plugin/plugin.py | 9 +- 5 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 src/wechaty_plugin_contrib/github_webhook_plugin/__init__.py create mode 100644 src/wechaty_plugin_contrib/github_webhook_plugin/options.py create mode 100644 src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py diff --git a/src/wechaty_plugin_contrib/github_webhook_plugin/__init__.py b/src/wechaty_plugin_contrib/github_webhook_plugin/__init__.py new file mode 100644 index 0000000..c6feae9 --- /dev/null +++ b/src/wechaty_plugin_contrib/github_webhook_plugin/__init__.py @@ -0,0 +1,2 @@ +from .options import * +from .plugin import * diff --git a/src/wechaty_plugin_contrib/github_webhook_plugin/options.py b/src/wechaty_plugin_contrib/github_webhook_plugin/options.py new file mode 100644 index 0000000..8acd627 --- /dev/null +++ b/src/wechaty_plugin_contrib/github_webhook_plugin/options.py @@ -0,0 +1,58 @@ +""" +python-wechaty-plugin-contrib - https://github.com/wechaty/python-wechaty-plugin-contrib + +Authors: Jingjing WU (吴京京) + +2020-now @ Copyright wj-Mcat + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from dataclasses import ( + dataclass, + field +) +from enum import Enum +from typing import Optional, List + +from wechaty import ( + WechatyPluginOptions +) + + +class GithubContentType(Enum): + JSON = 0, + WWM_FORM_URLENCODED = 1 + + +@dataclass +class GithubHookItem: + """plugin can hook many gitlab projects, so the GitlabHookItem can specific + the project + """ + project_id: str + + room_topic: Optional[str] = None + room_id: Optional[str] = None + + contact_name: Optional[str] = None + contact_id: Optional[str] = None + + content_type: Optional[GithubContentType] = GithubContentType.JSON + secret_token: Optional[str] = None + + +@dataclass +class GithubWebhookOptions(WechatyPluginOptions): + listen_port: Optional[int] = 5101 + hook_items: List[GithubHookItem] = field(default_factory=list) diff --git a/src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py b/src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py new file mode 100644 index 0000000..1577889 --- /dev/null +++ b/src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py @@ -0,0 +1,195 @@ +""" +python-wechaty-plugin-contrib - https://github.com/wechaty/python-wechaty-plugin-contrib + +Authors: Jingjing WU (吴京京) + +2020-now @ Copyright wj-Mcat + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" +from __future__ import annotations + +import asyncio + +from aiohttp.web_response import StreamResponse +from wechaty_puppet import get_logger +from wechaty import WechatyPlugin, Wechaty + +from aiohttp import web, ClientRequest +from aiohttp.web_server import BaseRequest + +from .options import ( + GithubHookItem, + GithubWebhookOptions, + GithubContentType +) + +log = get_logger('GithubWebhookPlugin') + +import hashlib +import hmac +import json +import six + + +class GithubWebhookPlugin(WechatyPlugin): + """ + receive the github webhook events with the inner work + """ + + @property + def name(self) -> str: + return 'github-webhook-plugin' + + def __init__(self, options: GithubWebhookOptions): + """ + init the github webhook plugin + """ + super().__init__(options) + log.info(f'init the github-webhook-plugin <{options}>') + + if len(options.hook_items) == 0: + raise Exception('the hook_items is expected, please make sure this field is more than one') + for hook_item in options.hook_items: + if not hook_item.room_id and not hook_item.room_topic: + raise Exception( + f'when you configure project <{hook_item.project_id}>, ' + f' field or field is expected') + if not hook_item.contact_name and not hook_item.contact_id: + raise Exception( + f'when you configure project <{hook_item.project_id}>, ' + f' field or field is expected') + if not hook_item.secret_token: + raise Exception('the secret_token is expected, please make sure this filed is not None') + if isinstance(hook_item.secret_token, bytes): + hook_item.secret_token = hook_item.secret_token.decode('utf-8') + self.options: GithubWebhookOptions = options + + async def init_plugin(self, wechaty: Wechaty): + """ + init the github-webhook channel with aiohttp web framework, which can listen multi-github projects + """ + log.info(f'init the plugin :{self.name}') + app = web.Application() + + async def handle_request(request: StreamResponse): + print('================') + print(request) + print('================') + + app.add_routes([ + web.get('/', handle_request) + ]) + + runner = web.AppRunner(app) + await runner.setup() + + site = web.TCPSite(runner, '0.0.0.0', self.options.listen_port) + loop = asyncio.get_event_loop() + asyncio.run_coroutine_threadsafe(site.start(), loop) + + log.info( + f'github-webhook plugin server has started at ' + f'0.0.0.0:{self.options.listen_port}' + ) + + def hook(self, event_type="push"): + """ + Registers a function as a hook. Multiple hooks can be registered for a given type, but the + order in which they are invoke is unspecified. + :param event_type: The event type this hook will be invoked for. + """ + + def decorator(func): + self._hooks[event_type].append(func) + return func + + return decorator + + def _get_digest(self): + """Return message digest if a secret key was provided""" + + return hmac.new(self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None + + def _postreceive(self): + """Callback from Flask""" + + digest = self._get_digest() + + if digest is not None: + sig_parts = _get_header("X-Hub-Signature").split("=", 1) + if not isinstance(digest, six.text_type): + digest = six.text_type(digest) + + if len(sig_parts) < 2 or sig_parts[0] != "sha1" or not hmac.compare_digest(sig_parts[1], digest): + abort(400, "Invalid signature") + + event_type = _get_header("X-Github-Event") + content_type = _get_header("content-type") + data = ( + json.loads(request.form.to_dict(flat=True)["payload"]) + if content_type == "application/x-www-form-urlencoded" + else request.get_json() + ) + + if data is None: + abort(400, "Request body must contain json") + + self._logger.info("%s (%s)", _format_event(event_type, data), _get_header("X-Github-Delivery")) + + for hook in self._hooks.get(event_type, []): + hook(data) + + return "", 204 + + +EVENT_DESCRIPTIONS = { + "commit_comment": "{comment[user][login]} commented on " "{comment[commit_id]} in {repository[full_name]}", + "create": "{sender[login]} created {ref_type} ({ref}) in " "{repository[full_name]}", + "delete": "{sender[login]} deleted {ref_type} ({ref}) in " "{repository[full_name]}", + "deployment": "{sender[login]} deployed {deployment[ref]} to " + "{deployment[environment]} in {repository[full_name]}", + "deployment_status": "deployment of {deployement[ref]} to " + "{deployment[environment]} " + "{deployment_status[state]} in " + "{repository[full_name]}", + "fork": "{forkee[owner][login]} forked {forkee[name]}", + "gollum": "{sender[login]} edited wiki pages in {repository[full_name]}", + "issue_comment": "{sender[login]} commented on issue #{issue[number]} " "in {repository[full_name]}", + "issues": "{sender[login]} {action} issue #{issue[number]} in " "{repository[full_name]}", + "member": "{sender[login]} {action} member {member[login]} in " "{repository[full_name]}", + "membership": "{sender[login]} {action} member {member[login]} to team " "{team[name]} in {repository[full_name]}", + "page_build": "{sender[login]} built pages in {repository[full_name]}", + "ping": "ping from {sender[login]}", + "public": "{sender[login]} publicized {repository[full_name]}", + "pull_request": "{sender[login]} {action} pull #{pull_request[number]} in " "{repository[full_name]}", + "pull_request_review": "{sender[login]} {action} {review[state]} " + "review on pull #{pull_request[number]} in " + "{repository[full_name]}", + "pull_request_review_comment": "{comment[user][login]} {action} comment " + "on pull #{pull_request[number]} in " + "{repository[full_name]}", + "push": "{pusher[name]} pushed {ref} in {repository[full_name]}", + "release": "{release[author][login]} {action} {release[tag_name]} in " "{repository[full_name]}", + "repository": "{sender[login]} {action} repository " "{repository[full_name]}", + "status": "{sender[login]} set {sha} status to {state} in " "{repository[full_name]}", + "team_add": "{sender[login]} added repository {repository[full_name]} to " "team {team[name]}", + "watch": "{sender[login]} {action} watch in repository " "{repository[full_name]}", +} + + +def _format_event(event_type, data): + try: + return EVENT_DESCRIPTIONS[event_type].format(**data) + except KeyError: + return event_type diff --git a/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py b/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py index 3ef5385..f61cd9e 100644 --- a/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py +++ b/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py @@ -1,5 +1,5 @@ """ -Code Generator - https://github.com/wj-Mcat/code-generator +python-wechaty-plugin-contrib - https://github.com/wechaty/python-wechaty-plugin-contrib Authors: Jingjing WU (吴京京) @@ -18,11 +18,15 @@ limitations under the License. """ +from dataclasses import ( + dataclass, + field +) from typing import Optional, List + from wechaty import ( WechatyPluginOptions ) -from dataclasses import dataclass @dataclass @@ -46,4 +50,4 @@ class GitlabEventOptions(WechatyPluginOptions): listen_port: Optional[int] = 5100 - hook_items: List[GitlabHookItem] = [] + hook_items: List[GitlabHookItem] = field(default_factory=list) diff --git a/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py b/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py index 99ee96b..2e146f0 100644 --- a/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py +++ b/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py @@ -1,5 +1,5 @@ """ -Code Generator - https://github.com/wj-Mcat/code-generator +python-wechaty-plugin-contrib - https://github.com/wechaty/python-wechaty-plugin-contrib Authors: Jingjing WU (吴京京) @@ -17,7 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. """ - +from __future__ import annotations +import asyncio from wechaty import ( Wechaty, WechatyPlugin, ) @@ -67,7 +68,9 @@ async def init_plugin(self, wechaty: Wechaty): await runner.setup() site = web.TCPSite(runner, '0.0.0.0', self.options.listen_port) - await site.start() + + loop = asyncio.get_event_loop() + asyncio.run_coroutine_threadsafe(site.start(), loop=loop) log.info( f'the server has started ... 0.0.0.0: {self.options.listen_port}' ) From e10a91bbd7e3d7a3ae97528b5cfb9619f88fd0d9 Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 1 Dec 2020 21:45:59 +0800 Subject: [PATCH 08/12] refactor the wechaty-plugin-contrb --- examples/daily_plugin_bot.py | 24 +- src/wechaty_plugin_contrib/__init__.py | 11 +- src/wechaty_plugin_contrib/config.py | 24 ++ .../contrib/__init__.py | 11 + .../contrib/auto_reply_plugin/__init__.py | 11 + .../contrib/auto_reply_plugin/plugin.py | 53 +++ src/wechaty_plugin_contrib/daily_plugin.py | 27 +- .../finders/__init__.py | 0 .../finders/contact_finder.py | 55 +++ src/wechaty_plugin_contrib/finders/finder.py | 29 ++ .../finders/room_finder.py | 54 +++ .../github_webhook_plugin/plugin.py | 390 +++++++++--------- .../matchers/__init__.py | 9 + .../matchers/contact_matcher.py | 43 ++ .../matchers/matcher.py | 29 ++ .../matchers/message_matcher.py | 43 ++ .../matchers/room_matcher.py | 43 ++ .../messager_plugin/__init__.py | 5 - .../messager_plugin/messager_plugin.py | 47 --- .../messager_plugin/skill.py | 58 --- src/wechaty_plugin_contrib/utils.py | 14 + src/wechaty_plugin_contrib/version.py | 13 + 22 files changed, 668 insertions(+), 325 deletions(-) create mode 100644 src/wechaty_plugin_contrib/config.py create mode 100644 src/wechaty_plugin_contrib/contrib/auto_reply_plugin/__init__.py create mode 100644 src/wechaty_plugin_contrib/contrib/auto_reply_plugin/plugin.py create mode 100644 src/wechaty_plugin_contrib/finders/__init__.py create mode 100644 src/wechaty_plugin_contrib/finders/contact_finder.py create mode 100644 src/wechaty_plugin_contrib/finders/finder.py create mode 100644 src/wechaty_plugin_contrib/finders/room_finder.py create mode 100644 src/wechaty_plugin_contrib/matchers/__init__.py create mode 100644 src/wechaty_plugin_contrib/matchers/contact_matcher.py create mode 100644 src/wechaty_plugin_contrib/matchers/matcher.py create mode 100644 src/wechaty_plugin_contrib/matchers/message_matcher.py create mode 100644 src/wechaty_plugin_contrib/matchers/room_matcher.py delete mode 100644 src/wechaty_plugin_contrib/messager_plugin/__init__.py delete mode 100644 src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py delete mode 100644 src/wechaty_plugin_contrib/messager_plugin/skill.py create mode 100644 src/wechaty_plugin_contrib/utils.py create mode 100644 src/wechaty_plugin_contrib/version.py diff --git a/examples/daily_plugin_bot.py b/examples/daily_plugin_bot.py index 558fe63..dab615b 100644 --- a/examples/daily_plugin_bot.py +++ b/examples/daily_plugin_bot.py @@ -1,11 +1,21 @@ """daily plugin bot examples""" import asyncio +from datetime import datetime from wechaty import Wechaty # type: ignore +from wechaty_puppet import RoomQueryFilter + from wechaty_plugin_contrib.daily_plugin import DailyPluginOptions, DailyPlugin from wechaty_plugin_contrib.ding_dong_plugin import DingDongPlugin +async def say_hello(bot: Wechaty): + """say hello to the room""" + room = await bot.Room.find(query=RoomQueryFilter(topic='小群,小群1')) + if room: + await room.say(f'hello bupt ... {datetime.now()}') + + async def run(): """async run method""" morning_plugin = DailyPlugin(DailyPluginOptions( @@ -18,21 +28,11 @@ async def run(): }, msg='宝贝,早安,爱你哟~' )) - - eating_plugin = DailyPlugin(DailyPluginOptions( - name='girl-friend-bot-eating', - contact_id='some-one-id', - trigger='cron', - kwargs={ - 'hour': 11, - 'minute': 30 - }, - msg='中午要记得好好吃饭喔~' - )) + morning_plugin.add_interval_job(say_hello) ding_dong_plugin = DingDongPlugin() - bot = Wechaty().use(morning_plugin).use(eating_plugin).use(ding_dong_plugin) + bot = Wechaty().use(morning_plugin).use(ding_dong_plugin) await bot.start() asyncio.run(run()) diff --git a/src/wechaty_plugin_contrib/__init__.py b/src/wechaty_plugin_contrib/__init__.py index e83e78f..bd9abe3 100644 --- a/src/wechaty_plugin_contrib/__init__.py +++ b/src/wechaty_plugin_contrib/__init__.py @@ -4,7 +4,12 @@ DailyPluginOptions, DailyPlugin ) -from wechaty_plugin_contrib.messager_plugin import MessagerPlugin + +from wechaty_plugin_contrib.contrib import ( + AutoReplyRule, + AutoReplyOptions, + AutoReplyPlugin +) __all__ = [ 'DingDongPlugin', @@ -12,5 +17,7 @@ 'DailyPluginOptions', 'DailyPlugin', - 'MessagerPlugin' + 'AutoReplyRule', + 'AutoReplyOptions', + 'AutoReplyPlugin' ] diff --git a/src/wechaty_plugin_contrib/config.py b/src/wechaty_plugin_contrib/config.py new file mode 100644 index 0000000..b7b7c0f --- /dev/null +++ b/src/wechaty_plugin_contrib/config.py @@ -0,0 +1,24 @@ +"""import basic config from wechaty-puppet""" + +from wechaty import ( # type: ignore + Room, + Contact, + Message, + + Wechaty +) + +from wechaty_puppet import get_logger # type: ignore + +from .version import version + +__all__ = [ + 'get_logger', + 'version', + + 'Room', + 'Contact', + 'Message', + + 'Wechaty' +] diff --git a/src/wechaty_plugin_contrib/contrib/__init__.py b/src/wechaty_plugin_contrib/contrib/__init__.py index e69de29..cf008bf 100644 --- a/src/wechaty_plugin_contrib/contrib/__init__.py +++ b/src/wechaty_plugin_contrib/contrib/__init__.py @@ -0,0 +1,11 @@ +from .auto_reply_plugin import ( + AutoReplyRule, + AutoReplyOptions, + AutoReplyPlugin +) + +__all__ = [ + 'AutoReplyOptions', + 'AutoReplyPlugin', + 'AutoReplyRule' +] diff --git a/src/wechaty_plugin_contrib/contrib/auto_reply_plugin/__init__.py b/src/wechaty_plugin_contrib/contrib/auto_reply_plugin/__init__.py new file mode 100644 index 0000000..58b012b --- /dev/null +++ b/src/wechaty_plugin_contrib/contrib/auto_reply_plugin/__init__.py @@ -0,0 +1,11 @@ +from .plugin import ( + AutoReplyRule, + AutoReplyOptions, + AutoReplyPlugin +) + +__all__ = [ + 'AutoReplyOptions', + 'AutoReplyPlugin', + 'AutoReplyRule' +] diff --git a/src/wechaty_plugin_contrib/contrib/auto_reply_plugin/plugin.py b/src/wechaty_plugin_contrib/contrib/auto_reply_plugin/plugin.py new file mode 100644 index 0000000..21ed615 --- /dev/null +++ b/src/wechaty_plugin_contrib/contrib/auto_reply_plugin/plugin.py @@ -0,0 +1,53 @@ +"""AutoReply to someone according to keywords""" +from dataclasses import dataclass, field +from typing import Union, List, Dict + +from wechaty import ( # type: ignore + WechatyPlugin, + WechatyPluginOptions, + FileBox, + Contact, + Message +) + +from wechaty_puppet import ( # type: ignore + get_logger +) + + +@dataclass +class AutoReplyRule: + keyword: str + reply_content: Union[str, FileBox, Contact] + + +@dataclass +class AutoReplyOptions(WechatyPluginOptions): + rules: List[AutoReplyRule] = field(default_factory=list) + + +logger = get_logger('AutoReplyPlugin') + + +class AutoReplyPlugin(WechatyPlugin): + + def __init__(self, options: AutoReplyOptions): + super().__init__(options) + + self.rule_map: Dict[str, AutoReplyRule] = {} + if options.rules: + self.rule_map = {rule.keyword: rule for rule in options.rules} + + async def on_message(self, msg: Message): + """check the keyword and reply to talker""" + text = msg.text() + + if text in self.rule_map: + room = msg.room() + if room: + await room.ready() + await room.say(self.rule_map[text]) + else: + talker = msg.talker() + await talker.ready() + await talker.say(self.rule_map[text]) diff --git a/src/wechaty_plugin_contrib/daily_plugin.py b/src/wechaty_plugin_contrib/daily_plugin.py index 3ced6bf..ca2122a 100644 --- a/src/wechaty_plugin_contrib/daily_plugin.py +++ b/src/wechaty_plugin_contrib/daily_plugin.py @@ -1,9 +1,10 @@ """daily plugin""" from __future__ import annotations from dataclasses import dataclass -from typing import Optional, Union +from typing import Optional, Union, List, Any from apscheduler.schedulers.asyncio import AsyncIOScheduler # type: ignore +from apscheduler.schedulers.base import BaseScheduler # type: ignore from wechaty import Wechaty, get_logger, Room, Contact # type: ignore from wechaty.plugin import WechatyPlugin, WechatyPluginOptions # type: ignore @@ -42,6 +43,8 @@ def __init__(self, options: DailyPluginOptions): raise Exception('msg should not be none') self.options: DailyPluginOptions = options + self.scheduler: BaseScheduler = AsyncIOScheduler() + self._scheduler_jobs: List[Any] = [] @property def name(self) -> str: @@ -67,8 +70,20 @@ async def tick(self, msg: str): async def init_plugin(self, wechaty: Wechaty): """init plugin""" await super().init_plugin(wechaty) - scheduler = AsyncIOScheduler() - scheduler.add_job(self.tick, self.options.trigger, - kwargs={'msg': self.options.msg}, - **self.options.kwargs) - scheduler.start() + for job in self._scheduler_jobs: + job(wechaty) + self.scheduler.start() + + def add_interval_job(self, func): + """add interval job""" + + def add_job(bot: Wechaty): + self.scheduler.add_job( + func, + trigger='interval', + seconds=5, + kwargs={ + 'bot': bot + } + ) + self._scheduler_jobs.append(add_job) diff --git a/src/wechaty_plugin_contrib/finders/__init__.py b/src/wechaty_plugin_contrib/finders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/wechaty_plugin_contrib/finders/contact_finder.py b/src/wechaty_plugin_contrib/finders/contact_finder.py new file mode 100644 index 0000000..4fe1213 --- /dev/null +++ b/src/wechaty_plugin_contrib/finders/contact_finder.py @@ -0,0 +1,55 @@ +"""Message Finder to match the specific message""" +import re +from re import Pattern +import inspect +from typing import List + +from wechaty_plugin_contrib.config import ( + get_logger, + Contact, + Wechaty +) + +from .finder import Finder + + +logger = get_logger("MessageFinder") + + +class MessageFinder(Finder): + async def match(self, wechaty: Wechaty) -> List[Contact]: + """match the room""" + logger.info(f'MessageFinder match({Wechaty})') + + contacts: List[Contact] = [] + + for option in self.options: + if isinstance(option, Pattern): + + # search from all of the friends + re_pattern = re.compile(option) + # match the room with regex pattern + all_friends = await wechaty.Contact.find_all() + for friend in all_friends: + alias = await friend.alias() + if re.match(re_pattern, friend.name) or re.match(re_pattern, alias): + contacts.append(friend) + + elif isinstance(option, str): + contact = wechaty.Contact.load(option) + await contact.ready() + contacts.append(contact) + elif hasattr(option, '__call__'): + """check the type of the function + refer: https://stackoverflow.com/a/56240578/6894382 + """ + if inspect.iscoroutinefunction(option): + targets = await option(wechaty) + else: + targets = option(wechaty) + + if isinstance(targets, List[Contact]): + contacts.extend(targets) + else: + raise ValueError(f'unknown type option: {option}') + return contacts diff --git a/src/wechaty_plugin_contrib/finders/finder.py b/src/wechaty_plugin_contrib/finders/finder.py new file mode 100644 index 0000000..553f304 --- /dev/null +++ b/src/wechaty_plugin_contrib/finders/finder.py @@ -0,0 +1,29 @@ +"""Base finder for finding Room/Message""" +from re import Pattern +from typing import ( + Callable, + Optional, + Union, + List, +) + +from wechaty_plugin_contrib.config import ( + Room, + Contact, + Message +) + + +FinderOption = Union[str, Pattern, bool, Callable[[Union[Contact, Room, Message]], List[Union[Room, Contact]]]] +FinderOptions = Union[FinderOption, List[FinderOption]] + + +class Finder: + def __init__(self, option: FinderOptions): + if isinstance(option, list): + self.options = option + else: + self.options = [option] + + def match(self, target) -> bool: + raise NotImplementedError diff --git a/src/wechaty_plugin_contrib/finders/room_finder.py b/src/wechaty_plugin_contrib/finders/room_finder.py new file mode 100644 index 0000000..333d397 --- /dev/null +++ b/src/wechaty_plugin_contrib/finders/room_finder.py @@ -0,0 +1,54 @@ +"""Room Finder to match the specific Room""" +import re +from re import Pattern +import inspect +from typing import List + +from wechaty_plugin_contrib.config import ( + get_logger, + Room, + + Wechaty +) + +from .finder import Finder + + +logger = get_logger("RoomFinder") + + +class RoomFinder(Finder): + async def match(self, wechaty: Wechaty) -> List[Room]: + """match the room""" + logger.info(f'RoomFinder match({Wechaty})') + + rooms: List[Room] = [] + + for option in self.options: + if isinstance(option, Pattern): + + # search from all of the friends + # match the room with regex pattern + all_rooms = await wechaty.Room.find_all() + for room in all_rooms: + topic = await room.topic() + if re.match(option, topic): + rooms.append(room) + + elif isinstance(option, str): + room = wechaty.Room.load(option) + await room.ready() + rooms.append(room) + elif hasattr(option, '__call__'): + """check the type of the function + refer: https://stackoverflow.com/a/56240578/6894382 + """ + if inspect.iscoroutinefunction(option): + targets = await option(wechaty) + else: + targets = option(wechaty) + if isinstance(targets, List[Room]): + rooms.extend(targets) + else: + raise ValueError(f'unknown type option: {option}') + return rooms diff --git a/src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py b/src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py index 1577889..00c5c4f 100644 --- a/src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py +++ b/src/wechaty_plugin_contrib/github_webhook_plugin/plugin.py @@ -1,195 +1,195 @@ -""" -python-wechaty-plugin-contrib - https://github.com/wechaty/python-wechaty-plugin-contrib - -Authors: Jingjing WU (吴京京) - -2020-now @ Copyright wj-Mcat - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -from __future__ import annotations - -import asyncio - -from aiohttp.web_response import StreamResponse -from wechaty_puppet import get_logger -from wechaty import WechatyPlugin, Wechaty - -from aiohttp import web, ClientRequest -from aiohttp.web_server import BaseRequest - -from .options import ( - GithubHookItem, - GithubWebhookOptions, - GithubContentType -) - -log = get_logger('GithubWebhookPlugin') - -import hashlib -import hmac -import json -import six - - -class GithubWebhookPlugin(WechatyPlugin): - """ - receive the github webhook events with the inner work - """ - - @property - def name(self) -> str: - return 'github-webhook-plugin' - - def __init__(self, options: GithubWebhookOptions): - """ - init the github webhook plugin - """ - super().__init__(options) - log.info(f'init the github-webhook-plugin <{options}>') - - if len(options.hook_items) == 0: - raise Exception('the hook_items is expected, please make sure this field is more than one') - for hook_item in options.hook_items: - if not hook_item.room_id and not hook_item.room_topic: - raise Exception( - f'when you configure project <{hook_item.project_id}>, ' - f' field or field is expected') - if not hook_item.contact_name and not hook_item.contact_id: - raise Exception( - f'when you configure project <{hook_item.project_id}>, ' - f' field or field is expected') - if not hook_item.secret_token: - raise Exception('the secret_token is expected, please make sure this filed is not None') - if isinstance(hook_item.secret_token, bytes): - hook_item.secret_token = hook_item.secret_token.decode('utf-8') - self.options: GithubWebhookOptions = options - - async def init_plugin(self, wechaty: Wechaty): - """ - init the github-webhook channel with aiohttp web framework, which can listen multi-github projects - """ - log.info(f'init the plugin :{self.name}') - app = web.Application() - - async def handle_request(request: StreamResponse): - print('================') - print(request) - print('================') - - app.add_routes([ - web.get('/', handle_request) - ]) - - runner = web.AppRunner(app) - await runner.setup() - - site = web.TCPSite(runner, '0.0.0.0', self.options.listen_port) - loop = asyncio.get_event_loop() - asyncio.run_coroutine_threadsafe(site.start(), loop) - - log.info( - f'github-webhook plugin server has started at ' - f'0.0.0.0:{self.options.listen_port}' - ) - - def hook(self, event_type="push"): - """ - Registers a function as a hook. Multiple hooks can be registered for a given type, but the - order in which they are invoke is unspecified. - :param event_type: The event type this hook will be invoked for. - """ - - def decorator(func): - self._hooks[event_type].append(func) - return func - - return decorator - - def _get_digest(self): - """Return message digest if a secret key was provided""" - - return hmac.new(self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None - - def _postreceive(self): - """Callback from Flask""" - - digest = self._get_digest() - - if digest is not None: - sig_parts = _get_header("X-Hub-Signature").split("=", 1) - if not isinstance(digest, six.text_type): - digest = six.text_type(digest) - - if len(sig_parts) < 2 or sig_parts[0] != "sha1" or not hmac.compare_digest(sig_parts[1], digest): - abort(400, "Invalid signature") - - event_type = _get_header("X-Github-Event") - content_type = _get_header("content-type") - data = ( - json.loads(request.form.to_dict(flat=True)["payload"]) - if content_type == "application/x-www-form-urlencoded" - else request.get_json() - ) - - if data is None: - abort(400, "Request body must contain json") - - self._logger.info("%s (%s)", _format_event(event_type, data), _get_header("X-Github-Delivery")) - - for hook in self._hooks.get(event_type, []): - hook(data) - - return "", 204 - - -EVENT_DESCRIPTIONS = { - "commit_comment": "{comment[user][login]} commented on " "{comment[commit_id]} in {repository[full_name]}", - "create": "{sender[login]} created {ref_type} ({ref}) in " "{repository[full_name]}", - "delete": "{sender[login]} deleted {ref_type} ({ref}) in " "{repository[full_name]}", - "deployment": "{sender[login]} deployed {deployment[ref]} to " - "{deployment[environment]} in {repository[full_name]}", - "deployment_status": "deployment of {deployement[ref]} to " - "{deployment[environment]} " - "{deployment_status[state]} in " - "{repository[full_name]}", - "fork": "{forkee[owner][login]} forked {forkee[name]}", - "gollum": "{sender[login]} edited wiki pages in {repository[full_name]}", - "issue_comment": "{sender[login]} commented on issue #{issue[number]} " "in {repository[full_name]}", - "issues": "{sender[login]} {action} issue #{issue[number]} in " "{repository[full_name]}", - "member": "{sender[login]} {action} member {member[login]} in " "{repository[full_name]}", - "membership": "{sender[login]} {action} member {member[login]} to team " "{team[name]} in {repository[full_name]}", - "page_build": "{sender[login]} built pages in {repository[full_name]}", - "ping": "ping from {sender[login]}", - "public": "{sender[login]} publicized {repository[full_name]}", - "pull_request": "{sender[login]} {action} pull #{pull_request[number]} in " "{repository[full_name]}", - "pull_request_review": "{sender[login]} {action} {review[state]} " - "review on pull #{pull_request[number]} in " - "{repository[full_name]}", - "pull_request_review_comment": "{comment[user][login]} {action} comment " - "on pull #{pull_request[number]} in " - "{repository[full_name]}", - "push": "{pusher[name]} pushed {ref} in {repository[full_name]}", - "release": "{release[author][login]} {action} {release[tag_name]} in " "{repository[full_name]}", - "repository": "{sender[login]} {action} repository " "{repository[full_name]}", - "status": "{sender[login]} set {sha} status to {state} in " "{repository[full_name]}", - "team_add": "{sender[login]} added repository {repository[full_name]} to " "team {team[name]}", - "watch": "{sender[login]} {action} watch in repository " "{repository[full_name]}", -} - - -def _format_event(event_type, data): - try: - return EVENT_DESCRIPTIONS[event_type].format(**data) - except KeyError: - return event_type +# """ +# python-wechaty-plugin-contrib - https://github.com/wechaty/python-wechaty-plugin-contrib +# +# Authors: Jingjing WU (吴京京) +# +# 2020-now @ Copyright wj-Mcat +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# """ +# from __future__ import annotations +# +# import asyncio +# +# from aiohttp.web_response import StreamResponse +# from wechaty_puppet import get_logger +# from wechaty import WechatyPlugin, Wechaty +# +# from aiohttp import web, ClientRequest +# from aiohttp.web_server import BaseRequest +# +# from .options import ( +# GithubHookItem, +# GithubWebhookOptions, +# GithubContentType +# ) +# +# log = get_logger('GithubWebhookPlugin') +# +# import hashlib +# import hmac +# import json +# import six +# +# +# class GithubWebhookPlugin(WechatyPlugin): +# """ +# receive the github webhook events with the inner work +# """ +# +# @property +# def name(self) -> str: +# return 'github-webhook-plugin' +# +# def __init__(self, options: GithubWebhookOptions): +# """ +# init the github webhook plugin +# """ +# super().__init__(options) +# log.info(f'init the github-webhook-plugin <{options}>') +# +# if len(options.hook_items) == 0: +# raise Exception('the hook_items is expected, please make sure this field is more than one') +# for hook_item in options.hook_items: +# if not hook_item.room_id and not hook_item.room_topic: +# raise Exception( +# f'when you configure project <{hook_item.project_id}>, ' +# f' field or field is expected') +# if not hook_item.contact_name and not hook_item.contact_id: +# raise Exception( +# f'when you configure project <{hook_item.project_id}>, ' +# f' field or field is expected') +# if not hook_item.secret_token: +# raise Exception('the secret_token is expected, please make sure this filed is not None') +# if isinstance(hook_item.secret_token, bytes): +# hook_item.secret_token = hook_item.secret_token.decode('utf-8') +# self.options: GithubWebhookOptions = options +# +# async def init_plugin(self, wechaty: Wechaty): +# """ +# init the github-webhook channel with aiohttp web framework, which can listen multi-github projects +# """ +# log.info(f'init the plugin :{self.name}') +# app = web.Application() +# +# async def handle_request(request: StreamResponse): +# print('================') +# print(request) +# print('================') +# +# app.add_routes([ +# web.get('/', handle_request) +# ]) +# +# runner = web.AppRunner(app) +# await runner.setup() +# +# site = web.TCPSite(runner, '0.0.0.0', self.options.listen_port) +# loop = asyncio.get_event_loop() +# asyncio.run_coroutine_threadsafe(site.start(), loop) +# +# log.info( +# f'github-webhook plugin server has started at ' +# f'0.0.0.0:{self.options.listen_port}' +# ) +# +# def hook(self, event_type="push"): +# """ +# Registers a function as a hook. Multiple hooks can be registered for a given type, but the +# order in which they are invoke is unspecified. +# :param event_type: The event type this hook will be invoked for. +# """ +# +# def decorator(func): +# self._hooks[event_type].append(func) +# return func +# +# return decorator +# +# def _get_digest(self): +# """Return message digest if a secret key was provided""" +# +# return hmac.new(self._secret, request.data, hashlib.sha1).hexdigest() if self._secret else None +# +# def _postreceive(self): +# """Callback from Flask""" +# +# digest = self._get_digest() +# +# if digest is not None: +# sig_parts = _get_header("X-Hub-Signature").split("=", 1) +# if not isinstance(digest, six.text_type): +# digest = six.text_type(digest) +# +# if len(sig_parts) < 2 or sig_parts[0] != "sha1" or not hmac.compare_digest(sig_parts[1], digest): +# abort(400, "Invalid signature") +# +# event_type = _get_header("X-Github-Event") +# content_type = _get_header("content-type") +# data = ( +# json.loads(request.form.to_dict(flat=True)["payload"]) +# if content_type == "application/x-www-form-urlencoded" +# else request.get_json() +# ) +# +# if data is None: +# abort(400, "Request body must contain json") +# +# self._logger.info("%s (%s)", _format_event(event_type, data), _get_header("X-Github-Delivery")) +# +# for hook in self._hooks.get(event_type, []): +# hook(data) +# +# return "", 204 +# +# +# EVENT_DESCRIPTIONS = { +# "commit_comment": "{comment[user][login]} commented on " "{comment[commit_id]} in {repository[full_name]}", +# "create": "{sender[login]} created {ref_type} ({ref}) in " "{repository[full_name]}", +# "delete": "{sender[login]} deleted {ref_type} ({ref}) in " "{repository[full_name]}", +# "deployment": "{sender[login]} deployed {deployment[ref]} to " +# "{deployment[environment]} in {repository[full_name]}", +# "deployment_status": "deployment of {deployement[ref]} to " +# "{deployment[environment]} " +# "{deployment_status[state]} in " +# "{repository[full_name]}", +# "fork": "{forkee[owner][login]} forked {forkee[name]}", +# "gollum": "{sender[login]} edited wiki pages in {repository[full_name]}", +# "issue_comment": "{sender[login]} commented on issue #{issue[number]} " "in {repository[full_name]}", +# "issues": "{sender[login]} {action} issue #{issue[number]} in " "{repository[full_name]}", +# "member": "{sender[login]} {action} member {member[login]} in " "{repository[full_name]}", +# "membership": "{sender[login]} {action} member {member[login]} to team " "{team[name]} in {repository[full_name]}", +# "page_build": "{sender[login]} built pages in {repository[full_name]}", +# "ping": "ping from {sender[login]}", +# "public": "{sender[login]} publicized {repository[full_name]}", +# "pull_request": "{sender[login]} {action} pull #{pull_request[number]} in " "{repository[full_name]}", +# "pull_request_review": "{sender[login]} {action} {review[state]} " +# "review on pull #{pull_request[number]} in " +# "{repository[full_name]}", +# "pull_request_review_comment": "{comment[user][login]} {action} comment " +# "on pull #{pull_request[number]} in " +# "{repository[full_name]}", +# "push": "{pusher[name]} pushed {ref} in {repository[full_name]}", +# "release": "{release[author][login]} {action} {release[tag_name]} in " "{repository[full_name]}", +# "repository": "{sender[login]} {action} repository " "{repository[full_name]}", +# "status": "{sender[login]} set {sha} status to {state} in " "{repository[full_name]}", +# "team_add": "{sender[login]} added repository {repository[full_name]} to " "team {team[name]}", +# "watch": "{sender[login]} {action} watch in repository " "{repository[full_name]}", +# } +# +# +# def _format_event(event_type, data): +# try: +# return EVENT_DESCRIPTIONS[event_type].format(**data) +# except KeyError: +# return event_type diff --git a/src/wechaty_plugin_contrib/matchers/__init__.py b/src/wechaty_plugin_contrib/matchers/__init__.py new file mode 100644 index 0000000..fc23d3c --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/__init__.py @@ -0,0 +1,9 @@ +from .contact_matcher import ContactMatcher +from .room_matcher import RoomMatcher +from .message_matcher import MessageMatcher + +__all__ = [ + 'ContactMatcher', + 'RoomMatcher', + 'MessageMatcher' +] diff --git a/src/wechaty_plugin_contrib/matchers/contact_matcher.py b/src/wechaty_plugin_contrib/matchers/contact_matcher.py new file mode 100644 index 0000000..5f483b6 --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/contact_matcher.py @@ -0,0 +1,43 @@ +"""Contact Matcher to match the specific contact""" +import re +from re import Pattern +import inspect + +from wechaty_plugin_contrib.config import ( + get_logger, + Contact, +) + +from .matcher import Matcher + + +logger = get_logger("ContactMatcher") + + +class ContactMatcher(Matcher): + async def match(self, target: Contact) -> bool: + """match the room""" + logger.info(f'ContactMatcher match({target})') + + for option in self.options: + if isinstance(option, Pattern): + re_pattern = re.compile(option) + # match the room with regex pattern + contact_alias = await target.alias() + is_match = re.match(re_pattern, target.name) or re.match(re_pattern, contact_alias) + elif isinstance(option, str): + is_match = target.contact_id == option + elif hasattr(option, '__call__'): + """check the type of the function + refer: https://stackoverflow.com/a/56240578/6894382 + """ + if inspect.iscoroutinefunction(option): + is_match = await option(target) + else: + is_match = option(target) + else: + raise ValueError(f'unknown type option: {option}') + + if is_match: + return True + return False diff --git a/src/wechaty_plugin_contrib/matchers/matcher.py b/src/wechaty_plugin_contrib/matchers/matcher.py new file mode 100644 index 0000000..1816668 --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/matcher.py @@ -0,0 +1,29 @@ +"""Base matcher for finding""" +from re import Pattern +from typing import ( + Callable, + Optional, + Union, + List +) + +from wechaty_plugin_contrib.config import ( + Room, + Contact, + Message +) + + +MatcherOption = Union[str, Pattern, bool, Callable[[Union[Contact, Room, Message]], bool]] +MatcherOptions = Union[MatcherOption, List[MatcherOption]] + + +class Matcher: + def __init__(self, option: MatcherOptions): + if isinstance(option, list): + self.options = option + else: + self.options = [option] + + def match(self, target) -> bool: + raise NotImplementedError diff --git a/src/wechaty_plugin_contrib/matchers/message_matcher.py b/src/wechaty_plugin_contrib/matchers/message_matcher.py new file mode 100644 index 0000000..a1683bf --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/message_matcher.py @@ -0,0 +1,43 @@ +"""Message Matcher to match the specific message""" +import re +from re import Pattern +import inspect + +from wechaty_plugin_contrib.config import ( + get_logger, + Message, +) + +from .matcher import Matcher + + +logger = get_logger("MessageMatcher") + + +class MessageMatcher(Matcher): + async def match(self, target: Message) -> bool: + """match the room""" + logger.info(f'MessageMatcher match({target})') + + for option in self.options: + if isinstance(option, Pattern): + re_pattern = re.compile(option) + # match the room with regex pattern + topic = target.text() + is_match = re.match(re_pattern, topic) + elif isinstance(option, str): + is_match = target.message_id == option + elif hasattr(option, '__call__'): + """check the type of the function + refer: https://stackoverflow.com/a/56240578/6894382 + """ + if inspect.iscoroutinefunction(option): + is_match = await option(target) + else: + is_match = option(target) + else: + raise ValueError(f'unknown type option: {option}') + + if is_match: + return True + return False diff --git a/src/wechaty_plugin_contrib/matchers/room_matcher.py b/src/wechaty_plugin_contrib/matchers/room_matcher.py new file mode 100644 index 0000000..83ccf99 --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/room_matcher.py @@ -0,0 +1,43 @@ +"""Room Matcher to match the specific room""" +import re +from re import Pattern +import inspect + +from wechaty_plugin_contrib.config import ( + get_logger, + Room, +) + +from .matcher import Matcher + + +logger = get_logger("RoomMatcher") + + +class RoomMatcher(Matcher): + async def match(self, target: Room) -> bool: + """match the room""" + logger.info(f'RoomMatcher match({target})') + + for option in self.options: + if isinstance(option, Pattern): + re_pattern = re.compile(option) + # match the room with regex pattern + topic = await target.topic() + is_match = re.match(re_pattern, topic) + elif isinstance(option, str): + is_match = target.room_id == option + elif hasattr(option, '__call__'): + """check the type of the function + refer: https://stackoverflow.com/a/56240578/6894382 + """ + if inspect.iscoroutinefunction(option): + is_match = await option(target) + else: + is_match = option(target) + else: + raise ValueError(f'unknown type option: {option}') + + if is_match: + return True + return False diff --git a/src/wechaty_plugin_contrib/messager_plugin/__init__.py b/src/wechaty_plugin_contrib/messager_plugin/__init__.py deleted file mode 100644 index 89b3862..0000000 --- a/src/wechaty_plugin_contrib/messager_plugin/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .messager_plugin import MessagerPlugin - -__all__ = [ - 'MessagerPlugin' -] diff --git a/src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py b/src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py deleted file mode 100644 index b1044b5..0000000 --- a/src/wechaty_plugin_contrib/messager_plugin/messager_plugin.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Python Plugin repo - https://github.com/wechaty/python-wechaty-plugin-contrib - -Authors: Jingjing WU (吴京京) - -2020-now @ Copyright wj-Mcat - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" - -from wechaty import Wechaty # type: ignore - - -from wechaty import ( - WechatyPlugin, - Message, - MessageType -) - - -class MessagerPlugin(WechatyPlugin): - - @property - def name(self) -> str: - return 'messager-plugin' - - def on_message(self, msg: Message): - """handle the template parsing""" - - # only handle the text message type - if msg.type() != MessageType.MESSAGE_TYPE_TEXT: - return - - # - async def init_plugin(self, wechaty: Wechaty): - """初始化数据""" - pass diff --git a/src/wechaty_plugin_contrib/messager_plugin/skill.py b/src/wechaty_plugin_contrib/messager_plugin/skill.py deleted file mode 100644 index fcd5f90..0000000 --- a/src/wechaty_plugin_contrib/messager_plugin/skill.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Python Plugin repo - https://github.com/wechaty/python-wechaty-plugin-contrib - -Authors: Jingjing WU (吴京京) - -2020-now @ Copyright wj-Mcat - -Licensed under the Apache License, Version 2.0 (the 'License'); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an 'AS IS' BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" -from typing import List, Union -from enum import Enum -import jieba.posseg as pseg - -TokenType = str - - -class Token: - """token is the unit word group in sentence and store the metadata of - the tokens - - the defination of token_type is : - 标签 含义 标签 含义 标签 含义 标签 含义 - n 普通名词 f 方位名词 s 处所名词 nw 作品名 - nz 其他专名 v 普通动词 vd 动副词 vn 名动词 - a 形容词 ad 副形词 an 名形词 d 副词 - m 数量词 q 量词 r 代词 p 介词 - c 连词 u 助词 xc 其他虚词 w 标点符号 - PER 人名 LOC 地名 ORG 机构名 TIME 时间 - """ - - def __init__(self, words: str, type: str): - self.words = words - self.type = type - - -class Skill: - """this is the base interface for messager plugin""" - - def get_template(self) -> Union[List[TokenType], List[List[TokenType]]]: - """get all of the token type""" - return ['n', 'f', 'an'] - - def suit_this_skill(self, text: str, friend_names: List[str]) -> bool: - """check if it's suit for template extract""" - words = pseg.lcut - import jieba - jieba.set_dictionary - diff --git a/src/wechaty_plugin_contrib/utils.py b/src/wechaty_plugin_contrib/utils.py new file mode 100644 index 0000000..0a023e2 --- /dev/null +++ b/src/wechaty_plugin_contrib/utils.py @@ -0,0 +1,14 @@ +""" +utils to help plugins more stronger +""" +import re + + +def is_regex_pattern(pattern: str) -> bool: + """check if the string is a valid regex pattern""" + try: + re.compile(pattern) + is_valid = True + except re.error: + is_valid = False + return is_valid diff --git a/src/wechaty_plugin_contrib/version.py b/src/wechaty_plugin_contrib/version.py new file mode 100644 index 0000000..8ef5065 --- /dev/null +++ b/src/wechaty_plugin_contrib/version.py @@ -0,0 +1,13 @@ +"""get the version from VERSION file""" +import os + + +def _get_version(): + file_path = os.path.join('../../', 'VERSION') + if not os.path.exists(file_path): + return '0.0.0' + with open(file_path, 'r', encoding='utf-8') as f: + return f.read().strip('\n') + + +version = _get_version() From 1b406c5c3d1f238d79f701e7d34f387fa4ac32b3 Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 1 Dec 2020 21:49:15 +0800 Subject: [PATCH 09/12] fix daily plugin bot --- examples/daily_plugin_bot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/daily_plugin_bot.py b/examples/daily_plugin_bot.py index dab615b..2a8db62 100644 --- a/examples/daily_plugin_bot.py +++ b/examples/daily_plugin_bot.py @@ -3,7 +3,7 @@ from datetime import datetime from wechaty import Wechaty # type: ignore -from wechaty_puppet import RoomQueryFilter +from wechaty_puppet import RoomQueryFilter # type: ignore from wechaty_plugin_contrib.daily_plugin import DailyPluginOptions, DailyPlugin from wechaty_plugin_contrib.ding_dong_plugin import DingDongPlugin From ea95df83cfe16be10f384c1ed191ff621eca9861 Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 1 Dec 2020 22:15:53 +0800 Subject: [PATCH 10/12] fix bad-return-type issue --- Dockerfile | 0 src/wechaty_plugin_contrib/ding_dong_plugin.py | 13 ++++++------- .../finders/contact_finder.py | 1 + src/wechaty_plugin_contrib/finders/room_finder.py | 1 + .../matchers/contact_matcher.py | 1 + .../matchers/message_matcher.py | 1 + src/wechaty_plugin_contrib/matchers/room_matcher.py | 1 + 7 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/src/wechaty_plugin_contrib/ding_dong_plugin.py b/src/wechaty_plugin_contrib/ding_dong_plugin.py index 9715013..dba7155 100644 --- a/src/wechaty_plugin_contrib/ding_dong_plugin.py +++ b/src/wechaty_plugin_contrib/ding_dong_plugin.py @@ -1,6 +1,6 @@ """basic ding-dong bot for the wechaty plugin""" -from typing import Union, Optional, List -from dataclasses import dataclass +from typing import List, Union +from dataclasses import dataclass, field from wechaty import Message, Contact, Room, get_logger # type: ignore from wechaty.plugin import WechatyPlugin, WechatyPluginOptions # type: ignore @@ -15,13 +15,13 @@ class DingDongPluginOptions(WechatyPluginOptions): only one of [include_conversation_ids,exclude_conversation_ids] can be empty """ - include_conversation_ids: Optional[List[str]] = None - exclude_conversation_ids: Optional[List[str]] = None + include_conversation_ids: List[str] = field(default_factory=list) + exclude_conversation_ids: List[str] = field(default_factory=list) class DingDongPlugin(WechatyPlugin): """basic ding-dong plugin""" - def __init__(self, options: Optional[DingDongPluginOptions] = None): + def __init__(self, options: DingDongPluginOptions = None): super().__init__(options) if options is not None: @@ -59,8 +59,7 @@ async def on_message(self, msg: Message): text = msg.text() room = msg.room() if text == '#ding': - conversation: Union[ - Room, Contact] = from_contact if room is None else room + conversation: Union[Room, Contact] = from_contact if room is None else room conversation_id = from_contact.contact_id if room is None \ else room.room_id if self.can_send_dong(conversation_id): diff --git a/src/wechaty_plugin_contrib/finders/contact_finder.py b/src/wechaty_plugin_contrib/finders/contact_finder.py index 4fe1213..a106033 100644 --- a/src/wechaty_plugin_contrib/finders/contact_finder.py +++ b/src/wechaty_plugin_contrib/finders/contact_finder.py @@ -44,6 +44,7 @@ async def match(self, wechaty: Wechaty) -> List[Contact]: refer: https://stackoverflow.com/a/56240578/6894382 """ if inspect.iscoroutinefunction(option): + # pytype: disable=bad-return-type targets = await option(wechaty) else: targets = option(wechaty) diff --git a/src/wechaty_plugin_contrib/finders/room_finder.py b/src/wechaty_plugin_contrib/finders/room_finder.py index 333d397..6206ddd 100644 --- a/src/wechaty_plugin_contrib/finders/room_finder.py +++ b/src/wechaty_plugin_contrib/finders/room_finder.py @@ -44,6 +44,7 @@ async def match(self, wechaty: Wechaty) -> List[Room]: refer: https://stackoverflow.com/a/56240578/6894382 """ if inspect.iscoroutinefunction(option): + # pytype: disable=bad-return-type targets = await option(wechaty) else: targets = option(wechaty) diff --git a/src/wechaty_plugin_contrib/matchers/contact_matcher.py b/src/wechaty_plugin_contrib/matchers/contact_matcher.py index 5f483b6..2e08bcb 100644 --- a/src/wechaty_plugin_contrib/matchers/contact_matcher.py +++ b/src/wechaty_plugin_contrib/matchers/contact_matcher.py @@ -32,6 +32,7 @@ async def match(self, target: Contact) -> bool: refer: https://stackoverflow.com/a/56240578/6894382 """ if inspect.iscoroutinefunction(option): + # pytype: disable=bad-return-type is_match = await option(target) else: is_match = option(target) diff --git a/src/wechaty_plugin_contrib/matchers/message_matcher.py b/src/wechaty_plugin_contrib/matchers/message_matcher.py index a1683bf..10fd33d 100644 --- a/src/wechaty_plugin_contrib/matchers/message_matcher.py +++ b/src/wechaty_plugin_contrib/matchers/message_matcher.py @@ -32,6 +32,7 @@ async def match(self, target: Message) -> bool: refer: https://stackoverflow.com/a/56240578/6894382 """ if inspect.iscoroutinefunction(option): + # pytype: disable=bad-return-type is_match = await option(target) else: is_match = option(target) diff --git a/src/wechaty_plugin_contrib/matchers/room_matcher.py b/src/wechaty_plugin_contrib/matchers/room_matcher.py index 83ccf99..e23a56f 100644 --- a/src/wechaty_plugin_contrib/matchers/room_matcher.py +++ b/src/wechaty_plugin_contrib/matchers/room_matcher.py @@ -32,6 +32,7 @@ async def match(self, target: Room) -> bool: refer: https://stackoverflow.com/a/56240578/6894382 """ if inspect.iscoroutinefunction(option): + # pytype: disable=bad-return-type is_match = await option(target) else: is_match = option(target) From ef8e30f7ed6c3c38898133f9bd51c8eddb506618 Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 1 Dec 2020 22:24:01 +0800 Subject: [PATCH 11/12] update github auto deploy --- .github/workflows/pypi.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 632bdb4..4748fa7 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -32,21 +32,31 @@ jobs: make test deploy: - + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/v')) runs-on: ubuntu-latest needs: [build] steps: - uses: actions/checkout@v2 + - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' + - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine + - name: Check Branch + id: check-branch + run: | + if [[ ${{ github.ref }} =~ ^refs/heads/(master|v[0-9]+\.[0-9]+.*)$ ]]; then + echo ::set-output name=match::true + fi # See: https://stackoverflow.com/a/58869470/1123955 + - name: Build and publish + if: steps.check-branch.outputs.match == 'true' env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} From 5ada20d48a7c8e4e64cdd5291889bf247c76d49e Mon Sep 17 00:00:00 2001 From: wjmcat <1435130236@qq.com> Date: Tue, 1 Dec 2020 22:28:55 +0800 Subject: [PATCH 12/12] update version & remove duplicate package --- VERSION | 2 +- requirements-dev.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 4e379d2..bcab45a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.2 +0.0.3 diff --git a/requirements-dev.txt b/requirements-dev.txt index b5c19e4..9819da0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,4 +11,3 @@ pyee requests qrcode apscheduler -semver