diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 6415786..4748fa7 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -16,37 +16,47 @@ 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: + 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 }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/VERSION b/VERSION index 8a9ecc2..bcab45a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.1 \ No newline at end of file +0.0.3 diff --git a/examples/daily_plugin_bot.py b/examples/daily_plugin_bot.py index 558fe63..2a8db62 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 # type: ignore + 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/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 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/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/__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..a106033 --- /dev/null +++ b/src/wechaty_plugin_contrib/finders/contact_finder.py @@ -0,0 +1,56 @@ +"""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): + # pytype: disable=bad-return-type + 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..6206ddd --- /dev/null +++ b/src/wechaty_plugin_contrib/finders/room_finder.py @@ -0,0 +1,55 @@ +"""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): + # pytype: disable=bad-return-type + 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/__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..00c5c4f --- /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/__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..f61cd9e --- /dev/null +++ b/src/wechaty_plugin_contrib/gitlab_event_plugin/options.py @@ -0,0 +1,53 @@ +""" +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 typing import Optional, List + +from wechaty import ( + WechatyPluginOptions +) + + +@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] = 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 new file mode 100644 index 0000000..2e146f0 --- /dev/null +++ b/src/wechaty_plugin_contrib/gitlab_event_plugin/plugin.py @@ -0,0 +1,76 @@ +""" +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 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) + + 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}' + ) 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..2e08bcb --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/contact_matcher.py @@ -0,0 +1,44 @@ +"""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): + # pytype: disable=bad-return-type + 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..10fd33d --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/message_matcher.py @@ -0,0 +1,44 @@ +"""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): + # pytype: disable=bad-return-type + 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..e23a56f --- /dev/null +++ b/src/wechaty_plugin_contrib/matchers/room_matcher.py @@ -0,0 +1,44 @@ +"""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): + # pytype: disable=bad-return-type + 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 29b368e..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 - - -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/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 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() 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