├── .codecov.yml ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── docs-report.yml │ └── feature-request.yml └── workflows │ └── pypi.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pylintrc ├── .readthedocs.yaml ├── .travis.yml ├── .vscode └── settings.json ├── LICENSE ├── MAINTAINERS ├── MANIFEST.in ├── Makefile ├── NOTICE ├── README.md ├── VERSION ├── docs ├── api │ ├── accessory.md │ ├── config.md │ ├── plugin.md │ ├── types.md │ ├── user │ │ ├── contact.md │ │ ├── contact_self.md │ │ ├── favorite.md │ │ ├── friendship.md │ │ ├── image.md │ │ ├── message.md │ │ ├── mini_program.md │ │ ├── room.md │ │ ├── room_invitation.md │ │ ├── tag.md │ │ └── url_link.md │ ├── utils │ │ ├── async_helper.md │ │ ├── date_util.md │ │ ├── link.md │ │ ├── qr_code.md │ │ ├── qrcode_terminal.md │ │ └── type_check.md │ └── wechaty.md ├── explanation │ ├── different_protocol.md │ ├── index.md │ └── why_plugin.md ├── faq │ ├── common.md │ ├── faq.md │ └── what-is-a-puppet.md ├── how-to-contribute-for-docs.md ├── how-to │ ├── how-to_add_friendship.md │ ├── how-to_auto_reply.md │ ├── how-to_finder.md │ ├── how-to_github_webhook.md │ ├── how-to_gitlab_webhook.md │ ├── how-to_introduction.md │ ├── how-to_message_forward.md │ ├── how-to_rasa.md │ ├── how-to_room_inviter.md │ ├── how-to_scheduler.md │ ├── how-to_use_plugin.md │ ├── use-padlocal-protocol.md │ └── use-web-protocol.md ├── img │ ├── favicon.ico │ ├── getting-started │ │ └── python-wechaty.png │ ├── introduction │ │ └── cloud.png │ ├── wechaty-icon-white.svg │ └── wechaty-logo.svg ├── index.md ├── introduction.md ├── introduction │ ├── index.md │ ├── use-padlocal-protocol.md │ ├── use-paimon-protocol.md │ └── use-web-protocol.md ├── references │ ├── contact-self.md │ ├── contact.md │ ├── filebox.md │ ├── friendship.md │ ├── index.md │ ├── message.md │ ├── room-invitation.md │ ├── room.md │ └── wechaty.md └── tutorials │ ├── getting-started.md │ ├── index.md │ ├── use_padlocal_getting_started.md │ ├── use_paimon_getting_started.md │ ├── use_web_getting_started.md │ └── videos.md ├── examples ├── contact-bot.py ├── ding-dong-bot-oop.py ├── ding-dong-bot.py ├── health_check_plugin.py └── plugin-server-bot.py ├── mkdocs.yml ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── scripts ├── build_ui.sh └── check_python_version.py ├── setup.cfg ├── setup.py ├── src └── wechaty │ ├── __init__.py │ ├── accessory.py │ ├── config.py │ ├── exceptions.py │ ├── fake_puppet.py │ ├── plugin.py │ ├── py.typed │ ├── schema.py │ ├── types.py │ ├── user │ ├── __init__.py │ ├── contact.py │ ├── contact_self.py │ ├── favorite.py │ ├── friendship.py │ ├── image.py │ ├── message.py │ ├── message.pyi │ ├── mini_program.py │ ├── room.py │ ├── room.pyi │ ├── room_invitation.py │ ├── tag.py │ └── url_link.py │ ├── utils │ ├── __init__.py │ ├── async_helper.py │ ├── data_util.py │ ├── date_util.py │ ├── link.py │ ├── qr_code.py │ ├── qrcode_terminal.py │ └── type_check.py │ ├── version.py │ └── wechaty.py ├── tests ├── accessory_test.py ├── config_test.py ├── conftest.py ├── plugin_test.py ├── room_test.py ├── smoke_testing_test.py ├── timestamp_test.py ├── url_link_test.py ├── user_message_test.py ├── utils_test.py ├── version_test.py └── wechaty_test.py └── wip └── wechaty └── __init__.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | indent_style = space 15 | trim_trailing_whitespace = false 16 | 17 | # 4 tab indentation 18 | [Makefile] 19 | indent_style = tab 20 | indent_size = 4 21 | 22 | [*.py] 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/about-codeowners/ 3 | # 4 | 5 | * @wechaty/python 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Wechaty问题反馈 3 | title: "[Bug]: " 4 | labels: bug 5 | body: 6 | - type: textarea 7 | attributes: 8 | label: Environment 9 | description: | 10 | * please give the following wechaty related package info: 11 | ```sh 12 | pip list | grep paddle 13 | ``` 14 | * the type of token do you use 15 | * the version of wechaty docker gateway 16 | value: | 17 | - wechaty: 18 | - wechaty-puppet: 19 | - wechaty-puppet-service: 20 | - wechaty-plugin-contrib: 21 | - token type: padlocal|wxwork|xp 22 | - the version of wechaty docker container: [0.65] 23 | render: Markdown 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Description 29 | description: | 30 | please post a detailed description for your issue, you can use the text or screen-shot to show your errors. 31 | render: Markdown 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Minimum reproducible code 37 | description: please give the minimum code which can reproduce the issue you describe, so that we can help you fix the issue quickly. 38 | validations: 39 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/docs-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Docs Report 2 | description: Report the bug in the docs 3 | title: "[Docs]: " 4 | labels: 5 | - documentation 6 | 7 | body: 8 | - type: textarea 9 | attributes: 10 | label: Environment 11 | description: | 12 | * please give the following wechaty related package info: 13 | ```sh 14 | pip list | grep paddle 15 | ``` 16 | * the type of token do you use 17 | * the version of wechaty docker gateway 18 | value: | 19 | - wechaty: 20 | - wechaty-puppet: 21 | - wechaty-puppet-service: 22 | - wechaty-plugin-contrib: 23 | - token type: padlocal|wxwork|xp 24 | - the version of wechaty docker container: [0.65] 25 | render: Markdown 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Description 31 | description: | 32 | please post a detailed description for your issue, you can use the text or screen-shot to show your errors. 33 | render: Markdown 34 | validations: 35 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: "\U0001F680 Feature request" 2 | description: Submit a proposal/request for a new wechaty feature 3 | labels: [ "feature" ] 4 | body: 5 | - type: textarea 6 | id: feature-request 7 | validations: 8 | required: true 9 | attributes: 10 | label: Feature request 11 | description: | 12 | A clear and concise description of the feature proposal. 13 | 14 | - type: textarea 15 | id: motivation 16 | validations: 17 | required: true 18 | attributes: 19 | label: Motivation 20 | description: | 21 | Please outline the motivation for the proposal. Is your feature request related to a problem? e.g., I'm always frustrated when [...]. If this is related to another GitHub issue, please link here too. 22 | 23 | 24 | - type: textarea 25 | id: contribution 26 | validations: 27 | required: true 28 | attributes: 29 | label: Your contribution 30 | description: | 31 | Is there any way that you could help, e.g. by submitting a PR 32 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v1 12 | with: 13 | python-version: 3.8 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | pip install -r requirements.txt 18 | pip install -r requirements-dev.txt 19 | - name: Test 20 | run: make test 21 | # - name: Upload coverage to Codecov 22 | # uses: codecov/codecov-action@v2 23 | # with: 24 | # directory: ./coverage/reports/ 25 | # env_vars: OS,PYTHON 26 | # fail_ci_if_error: true 27 | # files: ./coverage.xml 28 | # flags: unittests 29 | # verbose: true 30 | 31 | pack: 32 | name: Pack 33 | needs: build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - uses: actions/setup-python@v1 38 | with: 39 | python-version: 3.8 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install setuptools wheel twine 44 | make install 45 | - name: Pack Testing 46 | run: | 47 | make dist 48 | echo "To be add: pack testing" 49 | 50 | deploy: 51 | if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop-ui' || startsWith(github.ref, 'refs/heads/v')) 52 | name: Deploy 53 | needs: [build, pack] 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v2 57 | - name: Set up Python 58 | uses: actions/setup-python@v1 59 | with: 60 | python-version: 3.8 61 | - name: Install dependencies 62 | run: | 63 | python -m pip install --upgrade pip 64 | pip install setuptools wheel twine 65 | make install 66 | 67 | - name: Check Branch 68 | id: check-branch 69 | run: | 70 | if [[ ${{ github.ref }} =~ ^refs/heads/(master|develop-ui|v[0-9]+\.[0-9]+.*)$ ]]; then 71 | echo ::set-output name=match::true 72 | fi # See: https://stackoverflow.com/a/58869470/1123955 73 | 74 | - uses: actions/checkout@v3 75 | - uses: actions/setup-node@v3 76 | with: 77 | node-version: 16 78 | - name: Building Wechaty-ui 79 | id: build-ui 80 | run: | 81 | make ui 82 | 83 | - name: Is A Publish Branch 84 | if: steps.check-branch.outputs.match == 'true' 85 | env: 86 | TWINE_USERNAME: __token__ 87 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 88 | run: | 89 | make deploy-version 90 | python setup.py sdist bdist_wheel 91 | twine upload --skip-existing dist/* 92 | 93 | - name: Is Not A Publish Branch 94 | if: steps.check-branch.outputs.match != 'true' 95 | run: echo 'Not A Publish Branch' 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | t.* 107 | dist/ 108 | .pytype/ 109 | token.txt 110 | .idea/ 111 | **/logs/ 112 | **/log.txt 113 | docs/build/ 114 | docs/source/_build 115 | .pyre/ 116 | .vscode/ 117 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # the hook execution directory in under git root directory 2 | repos: 3 | - repo: local 4 | hooks: 5 | 6 | - id: pylint 7 | name: pylint 8 | description: "Pylint: Checks for errors in Python code" 9 | language: system 10 | entry: make pylint 11 | always_run: true 12 | verbose: true 13 | require_serial: true 14 | stages: [push] 15 | types: [python] 16 | 17 | - id: pycodestyle 18 | name: pycodestyle 19 | description: "pycodestyle: Check your Python code against styles conventions in PEP 8" 20 | language: system 21 | entry: make pycodestyle 22 | always_run: true 23 | verbose: true 24 | require_serial: true 25 | stages: [push] 26 | types: [python] 27 | 28 | - id: flake8 29 | name: flake8 30 | description: "flake8: Tool For Style Guide Enforcement" 31 | language: system 32 | entry: make flake8 33 | always_run: true 34 | verbose: true 35 | require_serial: true 36 | stages: [push] 37 | types: [python] 38 | 39 | - id: mypy 40 | name: mypy 41 | description: "mypy: an optional static type checker for Python" 42 | language: system 43 | entry: make mypy 44 | always_run: true 45 | verbose: true 46 | require_serial: true 47 | stages: [push] 48 | types: [python] 49 | 50 | - id: pytest 51 | name: pytest 52 | description: "pytest: run python pytest unit test" 53 | language: system 54 | entry: make pytest 55 | always_run: true 56 | verbose: true 57 | require_serial: true 58 | stages: [push] 59 | types: [python] 60 | 61 | - id: bump-version 62 | name: bump-version 63 | description: "Bumped Version: bump the version when a new commit come in" 64 | language: system 65 | always_run: true 66 | verbose: true 67 | entry: make version 68 | require_serial: true 69 | stages: [push] 70 | types: [python] 71 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | mkdocs: 9 | configuration: mkdocs.yml 10 | 11 | # Optionally set the version of Python and requirements required to build your docs 12 | python: 13 | version: 3.7 14 | install: 15 | - requirements: requirements-dev.txt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.7 4 | 5 | # command to install dependencies 6 | install: 7 | - pip3 install --upgrade six 8 | - make install 9 | 10 | # command to run tests 11 | script: 12 | - make test 13 | 14 | notifications: 15 | email: 16 | on_success: change 17 | on_failure: change 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.enabled": true, 3 | 4 | "python.linting.pylintEnabled": true, 5 | "python.linting.pylintArgs": [ 6 | "--load-plugins", 7 | "pylint_quotes" 8 | ], 9 | 10 | "python.linting.mypyEnabled": true, 11 | "python.linting.mypyArgs": [ 12 | "--ignore-missing-imports", 13 | "--follow-imports=silent", 14 | "--python-version=3" 15 | ], 16 | 17 | "python.linting.pycodestyleEnabled": false, 18 | 19 | "python.linting.flake8Enabled": true, 20 | "python.linting.flake8Args": [ 21 | "--ignore=E203,E221,E241,E272,E501,F811," 22 | ], 23 | 24 | "alignment": { 25 | "operatorPadding": "right", 26 | "indentBase": "firstline", 27 | "surroundSpace": { 28 | "colon": [1, 1], // The first number specify how much space to add to the left, can be negative. The second number is how much space to the right, can be negative. 29 | "assignment": [1, 1], // The same as above. 30 | "arrow": [1, 1], // The same as above. 31 | "comment": 2, // Special how much space to add between the trailing comment and the code. 32 | // If this value is negative, it means don't align the trailing comment. 33 | }, 34 | }, 35 | 36 | "files.exclude": { 37 | "build/": true, 38 | "dist/": true, 39 | "*_cache/": true, 40 | ".pytype/": true, 41 | "**/__pycache__": true, 42 | "**/*.egg-info/": true, 43 | }, 44 | 45 | } 46 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Huan LI (李卓桓) 2 | Jingjing WU (吴京京) 3 | Chunhong HUANG (黄纯鸿) 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.md VERSION 2 | recursive-include src **/*.py 3 | recursive-include src **/*.pyi 4 | recursive-include src/wechaty/ui **/* 5 | recursive-exclude src **/*_test.py **/__pycache__ **/.mypy_cache -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Python Wechaty 2 | # 3 | # GitHb: https://github.com/wechaty/python-wechaty 4 | # Author: Huan LI https://github.com/huan 5 | # 6 | export GLOBIGNORE=src/wechaty/fake_puppet.py 7 | 8 | SOURCE_GLOB=$(wildcard bin/*.py src/**/*.py tests/**/*.py examples/*.py) 9 | 10 | # 11 | # Huan(202003) 12 | # F811: https://github.com/PyCQA/pyflakes/issues/320#issuecomment-469337000 13 | # 14 | IGNORE_PEP=E203,E221,E241,E272,E501,F811,W293 15 | 16 | # help scripts to find the right place of wechaty module 17 | export PYTHONPATH=src/ 18 | 19 | .PHONY: all 20 | all : clean lint 21 | 22 | .PHONY: clean 23 | clean: 24 | rm -fr dist/* .pytype src/wechaty/ui ui/ scripts/ui 25 | 26 | .PHONY: lint 27 | lint: pylint pycodestyle flake8 mypy 28 | 29 | 30 | # disable: TODO list temporay 31 | .PHONY: pylint 32 | pylint: 33 | pylint \ 34 | --load-plugins pylint_quotes \ 35 | --disable=W0511,R0801,cyclic-import,C4001,R1735 \ 36 | $(SOURCE_GLOB) 37 | 38 | .PHONY: pycodestyle 39 | pycodestyle: 40 | pycodestyle \ 41 | --statistics \ 42 | --count \ 43 | --ignore="${IGNORE_PEP}" \ 44 | $(SOURCE_GLOB) 45 | 46 | .PHONY: flake8 47 | flake8: 48 | flake8 \ 49 | --ignore="${IGNORE_PEP}" \ 50 | $(SOURCE_GLOB) 51 | 52 | .PHONY: mypy 53 | mypy: 54 | MYPYPATH=stubs/ mypy \ 55 | $(SOURCE_GLOB) 56 | 57 | .PHONY: pytype 58 | pytype: 59 | pytype \ 60 | -V 3.8 \ 61 | --disable=import-error,pyi-error \ 62 | src/ 63 | pytype \ 64 | -V 3.8 \ 65 | --disable=import-error \ 66 | examples/ 67 | 68 | .PHONY: uninstall-git-hook 69 | uninstall-git-hook: 70 | pre-commit clean 71 | pre-commit gc 72 | pre-commit uninstall 73 | pre-commit uninstall --hook-type pre-push 74 | 75 | .PHONY: install-git-hook 76 | install-git-hook: 77 | # cleanup existing pre-commit configuration (if any) 78 | pre-commit clean 79 | pre-commit gc 80 | # setup pre-commit 81 | # Ensures pre-commit hooks point to latest versions 82 | pre-commit autoupdate 83 | pre-commit install 84 | pre-commit install --hook-type pre-push 85 | 86 | .PHONY: install 87 | install: 88 | pip3 install -r requirements.txt 89 | pip3 install -r requirements-dev.txt 90 | $(MAKE) install-git-hook 91 | 92 | .PHONY: pytest 93 | pytest: 94 | pytest src/ tests/ 95 | 96 | .PHONY: test-unit 97 | test-unit: pytest 98 | 99 | .PHONY: test 100 | test: check-python-version lint pytest 101 | 102 | .PHONY: format 103 | format: 104 | yapf -q $(SOURCE_GLOB) 105 | 106 | .PHONY: check-python-version 107 | check-python-version: 108 | ./scripts/check_python_version.py 109 | 110 | .PHONY: format 111 | format: 112 | yapf $(SOURCE_GLOB) 113 | 114 | code: 115 | code . 116 | 117 | .PHONY: run 118 | run: 119 | python3 bin/run.py 120 | 121 | .PHONY: ui 122 | ui: 123 | ./scripts/build_ui.sh 124 | 125 | .PHONY: dist 126 | dist: 127 | python3 setup.py sdist bdist_wheel 128 | 129 | .PHONY: publish 130 | publish: 131 | PATH=~/.local/bin:${PATH} twine upload dist/* 132 | 133 | .PHONY: bot 134 | bot: 135 | python3 examples/ding-dong-bot.py 136 | 137 | .PHONY: version 138 | version: 139 | @newVersion=$$(awk -F. '{print $$1"."$$2"."$$3+1}' < VERSION) \ 140 | && echo $${newVersion} > VERSION \ 141 | && git add VERSION \ 142 | && git commit -m "🔥 update version to $${newVersion}" > /dev/null \ 143 | && git tag "v$${newVersion}" \ 144 | && echo "Bumped version to $${newVersion}" 145 | 146 | .PHONY: deploy-version 147 | deploy-version: 148 | echo "VERSION = '$$(cat VERSION)'" > src/wechaty/version.py 149 | 150 | .PHONY: doc 151 | doc: 152 | mkdocs serve 153 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Python Wechaty Chatbot SDK 2 | Copyright 2020 Wechaty Contributors. 3 | 4 | This product includes software developed at 5 | The Wechaty Organization (https://github.com/wechaty). 6 | 7 | This software contains code derived from the Stackoverflow, 8 | including various modifications by GitHub. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # python-wechaty 3 | 4 | ![Python Wechaty](./docs/img/getting-started/python-wechaty.png) 5 | 6 | [![PyPI Version](https://img.shields.io/pypi/v/wechaty?color=blue)](https://pypi.org/project/wechaty/) 7 | [![Python Wechaty Getting Started](https://img.shields.io/badge/Python%20Wechaty-Getting%20Started-blue)](https://github.com/wechaty/python-wechaty-getting-started) 8 | [![Python 3.7](https://img.shields.io/badge/python-3.7+-blue.svg)](https://www.python.org/downloads/release/python-370/) 9 | [![Downloads](https://pepy.tech/badge/wechaty)](https://pepy.tech/project/wechaty) 10 | [![Wechaty in Python](https://img.shields.io/badge/Wechaty-Python-blue)](https://github.com/wechaty/python-wechaty) 11 | [![codecov](https://codecov.io/gh/wechaty/python-wechaty/branch/master/graph/badge.svg)](https://codecov.io/gh/wechaty/python-wechaty) 12 | [![PyPI](https://github.com/wechaty/python-wechaty/actions/workflows/pypi.yml/badge.svg)](https://github.com/wechaty/python-wechaty/actions/workflows/pypi.yml) 13 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/wechaty?color=blue) 14 | 15 | [📄 Chinese Document](https://wechaty.readthedocs.io/zh_CN/latest/) [python-wechaty-template](https://github.com/wechaty/python-wechaty-template) 16 | 17 | ## What's Python Wechaty 18 | 19 | Python Wechaty is an Open Source software application for building chatbots. It is a modern Conversational RPA SDK which Chatbot makers can use to create a bot in a few lines of code. 20 | 21 | You can use Wechaty to build a chatbot which automates conversations and interact with people through instant messaging platforms such as WhatsApp, WeChat, WeCom, Gitter and Lark among others. 22 | 23 | ## Features 24 | 25 | * **Message Processing**: You can use the simple code, similar to natural language, to process the message receving & sending. 26 | * **Plugin System**: You can use the community-contributed plugins to handle your scenario. 27 | * **Write onece, run multi IM platform**: python wechaty support many IM platforms with one code, all of you need to do is switch the token token type. 28 | * **Wechaty UI**: you can use the powerful wechaty-ui to create interactive chatbot 29 | * ... 30 | 31 | 32 | ## Getting Started 33 | 34 | There are few steps to start your bot, and we give a [bot-template](https://github.com/wechaty/python-wechaty-template) for you to getting started quickly. 35 | 36 | 37 | ## Join Us 38 | 39 | Wechaty is used in many ChatBot projects by thousands of developers. If you want to talk with other developers, just scan the following QR Code in WeChat with secret code _python wechaty_, join our **Wechaty Python Developers' Home**. 40 | 41 | ![Wechaty Friday.BOT QR Code](https://wechaty.js.org/img/friday-qrcode.svg) 42 | 43 | Scan now, because other Wechaty Python developers want to talk with you too! (secret code: _python wechaty_) 44 | 45 | 46 | ## Requirements 47 | 48 | 1. Python 3.7+ 49 | 50 | ## Install 51 | 52 | ```shell 53 | pip3 install wechaty 54 | ``` 55 | 56 | ## See Also 57 | 58 | - [Packaging Python Projects](https://packaging.python.org/tutorials/packaging-projects/) 59 | 60 | ### Static & Instance of Class 61 | 62 | - [Static variables and methods in Python](https://radek.io/2011/07/21/static-variables-and-methods-in-python/) 63 | 64 | ### Typings 65 | 66 | - [PEP 526 -- Syntax for Variable Annotations - Class and instance variable annotations](https://www.python.org/dev/peps/pep-0526/#class-and-instance-variable-annotations) 67 | - [Python Type Checking (Guide)](https://realpython.com/python-type-checking/) by [Geir Arne Hjelle](https://realpython.com/team/gahjelle/) 68 | 69 | ## History 70 | 71 | ### v0.6 (Jun 19, 2020) 72 | 73 | Python Wechaty Scala Wechaty **BETA** Released! 74 | 75 | Read more from our Multi-language Wechaty Beta Release event from our blog: 76 | 77 | - [Multi Language Wechaty Beta Release Announcement!](https://wechaty.js.org/2020/06/19/multi-language-wechaty-beta-release/) 78 | 79 | ### v0.4 (Mar 15, 2020) master 80 | 81 | Welcome [@huangaszaq](https://github.com/huangaszaq) for joining the project! [#42](https://github.com/wechaty/python-wechaty/pull/42) 82 | 83 | 1. Add a friendly exception message for PyPI users. [#24](https://github.com/wechaty/python-wechaty/issues/24) 84 | 85 | ### v0.1 (Mar 8, 2020) 86 | 87 | Welcome [@wj-Mcat](https://github.com/wj-Mcat) for joining the project! [#4](https://github.com/wechaty/python-wechaty/pull/4) 88 | 89 | 1. Starting translate TypeScript of Wechaty to Python 90 | 1. DevOps Setup 91 | 1. Type Checking: mypy & pytype 92 | 1. Unit Testing: pytest 93 | 1. Linting: pylint, pycodestyle, and flake8 94 | 1. CI/CD: GitHub Actions 95 | 1. Publish to PyPI automatically after the tests passed. 96 | 97 | ### v0.0.1 (Aug 25, 2018) 98 | 99 | Project created, publish a empty module `wechaty` on PyPI. 100 | 101 | ## Related Projects 102 | 103 | - [Wechaty](https://github.com/wechaty/wechaty) - Conversatioanl AI Chatot SDK for Wechaty Individual Accounts (TypeScript) 104 | - [Python Wechaty](https://github.com/wechaty/python-wechaty) - Python WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Python) 105 | - [Go Wechaty](https://github.com/wechaty/go-wechaty) - Go WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Go) 106 | - [Java Wechaty](https://github.com/wechaty/java-wechaty) - Java WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Java) 107 | - [Scala Wechaty](https://github.com/wechaty/scala-wechaty) - Scala WeChaty Conversational AI Chatbot SDK for WechatyIndividual Accounts (Scala) 108 | 109 | ## Badge 110 | 111 | [![Wechaty in Python](https://img.shields.io/badge/Wechaty-Python-blue)](https://github.com/wechaty/python-wechaty) 112 | 113 | ```md 114 | [![Wechaty in Python](https://img.shields.io/badge/Wechaty-Python-blue)](https://github.com/wechaty/python-wechaty) 115 | ``` 116 | 117 | ## Stargazers over time 118 | 119 | [![Stargazers over time](https://starchart.cc/wechaty/python-wechaty.svg)](https://starchart.cc/wechaty/python-wechaty) 120 | 121 | ## Contributors 122 | 123 | 124 | 125 | 126 | 127 | Made with [contrib.rocks](https://contrib.rocks). 128 | 129 | ## Support 130 | 131 | Thanks the following supported Software. 132 | 133 | [![test image size](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg?_gl=1*1lb7oaa*_ga*MjE5ODE2MzAwLjE2MzYxODMyNTE.*_ga_V0XZL7QHEB*MTY0MTI2NzU5OS41LjEuMTY0MTI2NzY3OC4w&_ga=2.157122558.411488113.1641267600-219816300.1636183251)](https://jb.gg/OpenSourceSupport) 134 | 135 | ## Committers 136 | 137 | 1. [@huangaszaq](https://github.com/huangaszaq) - Chunhong HUANG (黄纯洪) 138 | 139 | ## Creators 140 | 141 | - [@wj-Mcat](https://github.com/wj-Mcat) - Jingjing WU (吴京京) 142 | - [@huan](https://github.com/huan) - ([李卓桓](http://linkedin.com/in/zixia)) zixia@zixia.net 143 | 144 | ## Copyright & License 145 | 146 | - Code & Docs © 2018 Wechaty Contributors 147 | - Code released under the Apache-2.0 License 148 | - Docs released under Creative Commons 149 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.10.8 2 | -------------------------------------------------------------------------------- /docs/api/accessory.md: -------------------------------------------------------------------------------- 1 | # wechaty.accessory 2 | 3 | ::: wechaty.accessory.Accessory -------------------------------------------------------------------------------- /docs/api/config.md: -------------------------------------------------------------------------------- 1 | # wechaty.config 2 | 3 | :::wechaty.config -------------------------------------------------------------------------------- /docs/api/plugin.md: -------------------------------------------------------------------------------- 1 | # wechaty.plugin 2 | 3 | :::wechaty.plugin.WechatyPlugin -------------------------------------------------------------------------------- /docs/api/types.md: -------------------------------------------------------------------------------- 1 | # types 2 | 3 | :::wechaty.types -------------------------------------------------------------------------------- /docs/api/user/contact.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.contact 2 | 3 | :::wechaty.user.contact.Contact -------------------------------------------------------------------------------- /docs/api/user/contact_self.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.contact_self 2 | 3 | :::wechaty.user.contact_self.ContactSelf -------------------------------------------------------------------------------- /docs/api/user/favorite.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.favorite 2 | :::wechaty.user.favorite.Favorite -------------------------------------------------------------------------------- /docs/api/user/friendship.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.friendship 2 | :::wechaty.user.friendship.Friendship -------------------------------------------------------------------------------- /docs/api/user/image.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.image 2 | :::wechaty.user.image.Image -------------------------------------------------------------------------------- /docs/api/user/message.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.message 2 | :::wechaty.user.message.Message -------------------------------------------------------------------------------- /docs/api/user/mini_program.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.mini_program 2 | :::wechaty.user.mini_program.MiniProgram -------------------------------------------------------------------------------- /docs/api/user/room.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.room 2 | 3 | :::wechaty.user.room.Room -------------------------------------------------------------------------------- /docs/api/user/room_invitation.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.room_invitation 2 | 3 | :::wechaty.user.room_invitation.RoomInvitation -------------------------------------------------------------------------------- /docs/api/user/tag.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.tag 2 | 3 | :::wechaty.user.tag.Tag -------------------------------------------------------------------------------- /docs/api/user/url_link.md: -------------------------------------------------------------------------------- 1 | # wechaty.user.url_link 2 | 3 | :::wechaty.user.url_link.UrlLink -------------------------------------------------------------------------------- /docs/api/utils/async_helper.md: -------------------------------------------------------------------------------- 1 | # wechaty.utils.async_helper 2 | 3 | :::wechaty.utils.async_helper -------------------------------------------------------------------------------- /docs/api/utils/date_util.md: -------------------------------------------------------------------------------- 1 | # wechaty.utils.date_util 2 | 3 | :::wechaty.utils.date_util -------------------------------------------------------------------------------- /docs/api/utils/link.md: -------------------------------------------------------------------------------- 1 | # wechaty.utils.link 2 | 3 | :::wechaty.utils.link -------------------------------------------------------------------------------- /docs/api/utils/qr_code.md: -------------------------------------------------------------------------------- 1 | # wechaty.utils.qr_code 2 | 3 | :::wechaty.utils.qr_code -------------------------------------------------------------------------------- /docs/api/utils/qrcode_terminal.md: -------------------------------------------------------------------------------- 1 | # wechaty.utils.qrcode_terminal 2 | 3 | :::wechaty.utils.qrcode_terminal -------------------------------------------------------------------------------- /docs/api/utils/type_check.md: -------------------------------------------------------------------------------- 1 | # wechaty.utils.type_check 2 | 3 | :::wechaty.utils.type_check -------------------------------------------------------------------------------- /docs/api/wechaty.md: -------------------------------------------------------------------------------- 1 | # wechaty.Wechaty 2 | 3 | :::wechaty.wechaty.Wechaty -------------------------------------------------------------------------------- /docs/explanation/different_protocol.md: -------------------------------------------------------------------------------- 1 | ## 一、支持的协议 2 | 3 | 一个Wechaty实例/派生类就是机器人对象,能够根据`TOKEN`找到连接的服务,获取用户自模块执行搜索,而这些信息都是由Wechaty实例管理。 4 | 5 | > 由于服务的连接信息是保存到实例当中,故用户子模块一定要通过Wechaty实例来获取。例如:bot.Contact.find_all() 6 | 7 | ### 1.1 什么是协议 8 | 9 | 所有实现底层平台对接实现就是一个协议。 10 | 11 | python-wechaty理论上能够对接所有IM平台,目前已经对接微信、微信公众号、钉钉、飞书以及WhatsApp等平台,源码都是基于TypeScript语言,可是通过`wechaty-puppet-service`能够将其服务以gRPC的形式暴露出来,提供给多语言`Wechaty`来连接。例如微信免费Web协议,底层实现是基于TyepScript编写,可是通过社区生态项目,可是都可以使用docker将接口的实现部署成服务。 12 | 13 | 比如[wechaty-puppet-wechat](https://github.com/wechaty/wechaty-puppet-wechat)能够通过[wechaty/wechaty:latest](https://hub.docker.com/r/wechaty/wechaty)镜像将其所有实现接口暴露成gRPC的服务,非常的方便,已然实现`write once, run anywhere`。 14 | 15 | ### 1.2 协议列表 16 | 17 | 目前python-wechaty能够使用wechaty生态中所有IM平台对接协议,协议列表如下所示: 18 | 19 | * [wechaty-puppet-wechaty](https://github.com/wechaty/wechaty-puppet-wechat): 免费微信Web协议 20 | * [wechaty-puppet-](https://github.com/wechaty/wechaty-puppet-macOS): 免费微信MacOs协议 21 | * [wechaty-puppet-padlocal](https://github.com/wechaty/wechaty-puppet-padlocal): 付费微信Pad协议 22 | * [wechaty-puppet-official-account](https://github.com/wechaty/wechaty-puppet-official-account): 微信公众号协议 23 | * [wechaty-puppet-lark](https://github.com/wechaty/wechaty-puppet-lark): 飞书协议 24 | * [wechaty-puppet-dingtalk](https://github.com/wechaty/wechaty-puppet-dingtalk): 钉钉协议 25 | * [wechaty-puppet-teams](https://github.com/wechaty/wechaty-puppet-dingtalk): 微软Teams协议 26 | * ...... 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/explanation/index.md: -------------------------------------------------------------------------------- 1 | > 这里应该是介绍一些核心的设计理念。 2 | -------------------------------------------------------------------------------- /docs/explanation/why_plugin.md: -------------------------------------------------------------------------------- 1 | # 插件系统设计理念介绍 2 | 3 | > explanation中的内容,应该是围绕这一个topic的 深入浅出的解释,不必列出具体的代码 -------------------------------------------------------------------------------- /docs/faq/common.md: -------------------------------------------------------------------------------- 1 | # python-wechaty 能登录多个微信吗? 2 | 3 | 可以。必须保证一个进程内只启动一个wechaty实例,可通过多行命令来启动多个wechaty实例,或在程序当中使用多进程启动多个wechaty实例(不推荐)。 4 | 5 | 6 | # the network is not good, the bot will try to restart after 60 seconds 7 | 8 | 此类问题的出现主要是由于Service(Token Provider Service, Gateway Service)连接不上。 9 | 10 | 你可以从如下方式进行排查: 11 | * 使用latest和0.62.3版本的wechaty docker镜像来都给启动service。 12 | * 查看服务的endpoint(ip、port)是否可连通? 13 | * 查看docker镜像当中是否有connection的记录? 14 | 15 | # 如何自定义插件缓存地址 16 | 17 | wechaty中的插件会自动创建缓存目录,规则如下: `plugin_cache_dir = root_cache_dir / plugin_name`。 18 | 19 | * root_cache_dir: str = os.environ.get("CACHE_DIR", ".wechaty") 20 | * plugin_name: str = plugin.options.name 21 | 22 | 根据以上代码即可看出,插件的缓存地址是由两部分决定,如果想要自定义插件的缓存地址,也是可以从这两部分入手解决。 23 | -------------------------------------------------------------------------------- /docs/faq/faq.md: -------------------------------------------------------------------------------- 1 | # python-wechaty 能登录多个微信吗? 2 | 3 | 可以。必须保证一个进程内只启动一个wechaty实例,可通过多行命令来启动多个wechaty实例,或在程序当中使用多进程启动多个wechaty实例(不推荐)。 4 | 5 | 6 | # the network is not good, the bot will try to restart after 60 seconds 7 | 8 | 此类问题的出现主要是由于Service(Token Provider Service, Gateway Service)连接不上。 9 | 10 | 你可以从如下方式进行排查: 11 | * 使用latest和0.62.3版本的wechaty docker镜像来都给启动service。 12 | * 查看服务的endpoint(ip、port)是否可连通? 13 | * 查看docker镜像当中是否有connection的记录? 14 | 15 | # 什么是Puppet 16 | 17 | The term `Puppet`in Wechaty is an Abstract Class for implementing protocol plugins. The plugins are the component that helps Wechaty to control the Wechat(that's the reason we call it puppet). 18 | 19 | The plugins are named XXXPuppet, like PuppetPuppeteer is using the chrome puppeteer to control the WeChat Web API via a chrome browser, PuppetPadchat is using the WebSocket protocol to connect with a Protocol Server for controlling the iPad Wechat program. -------------------------------------------------------------------------------- /docs/faq/what-is-a-puppet.md: -------------------------------------------------------------------------------- 1 | # 什么是Puppet 2 | 3 | The term `Puppet`in Wechaty is an Abstract Class for implementing protocol plugins. The plugins are the component that helps Wechaty to control the Wechat(that's the reason we call it puppet). 4 | 5 | The plugins are named XXXPuppet, like PuppetPuppeteer is using the chrome puppeteer to control the WeChat Web API via a chrome browser, PuppetPadchat is using the WebSocket protocol to connect with a Protocol Server for controlling the iPad Wechat program. -------------------------------------------------------------------------------- /docs/how-to-contribute-for-docs.md: -------------------------------------------------------------------------------- 1 | # 如何贡献文档 2 | 3 | 非常感谢各位开发者能够查看此章节,python-wechaty团队正在努力编写开发者友好的文档系统,从而减少学习成本。 4 | 5 | ## 一、找到问题 6 | 7 | 在贡献之前,开发者需要明确这是否是有待完善之处,推荐从如下三点查找: 8 | 9 | * 文档系统会在此[issue](https://github.com/wechaty/python-wechaty/issues/201)下不停更新TODO LIST,以提供各位开发者查看未完成的模块 10 | * 开发者在浏览文档的过程中如果发现有任何觉得不合理需要调整地方,请提交PR来优化,同时编写详细的说明来描述。 11 | * 在Issue列表中查看相关Bug或者Feature,确定能否解决 12 | 13 | ## 二、如何贡献 14 | 15 | ### fork & install 16 | 17 | * fork[python-wechaty](https://github.com/wechaty/python-wechaty)项目 18 | 19 | * 安装依赖包 20 | 21 | ```shell 22 | make install 23 | ``` 24 | 25 | or 26 | 27 | ```shell 28 | pip install -r requirements.txt 29 | pip install -r requirements-dev.txt 30 | 31 | # cleanup existing pre-commit configuration (if any) 32 | pre-commit clean 33 | pre-commit gc 34 | # setup pre-commit 35 | # Ensures pre-commit hooks point to latest versions 36 | pre-commit autoupdate 37 | pre-commit install 38 | pre-commit install --hook-type pre-push 39 | ``` 40 | 41 | * 提交文档更新PR 42 | 43 | 有如下要求: 44 | 45 | - [ ] 标题为:Improve Docs: [title-of-your-pr] 46 | - [ ] PR 内容的详细描述 47 | 48 | # 成为 Contributor 49 | 50 | 如果优化了部分文档,即可成为Wechaty的Contribtuor,享受社区提供的长期免费token服务,具体可查看:[contributor-program](https://wechaty.js.org/docs/contributor-program/)。 51 | -------------------------------------------------------------------------------- /docs/how-to/how-to_add_friendship.md: -------------------------------------------------------------------------------- 1 | > TODO: how to add friend -------------------------------------------------------------------------------- /docs/how-to/how-to_auto_reply.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "自动回复" 3 | --- 4 | 5 | ## 自动回复 6 | 7 | 自动回复也是我们日常生活工作中的一些高频使用场景,而回复内容不仅限于文字,还可以是图片,文件,链接以及小程序等等。比如你给机器人发“网易”,它会给你发送一个网易云音乐的小程序;你给它发一个”身份证“,它会给你发送身份证的正反面照片;...... 等等。 8 | 9 | 以上应用场景很常见,而且还有更多的实际应用案例可根据自己的需求来调整。 10 | 11 | 示例代码如下所示: 12 | 13 | ```python 14 | import asyncio 15 | from wechaty import Wechaty, MiniProgram # type: ignore 16 | from wechaty_puppet import ( # type: ignore 17 | FileBox 18 | ) 19 | 20 | from wechaty_plugin_contrib import ( 21 | AutoReplyRule, 22 | AutoReplyPlugin, 23 | AutoReplyOptions, 24 | ) 25 | 26 | from wechaty_plugin_contrib.matchers import ContactMatcher 27 | 28 | async def run(): 29 | """async run method""" 30 | img_url = 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy' \ 31 | '/it/u=1257042014,3164688936&fm=26&gp=0.jpg' 32 | plugin = AutoReplyPlugin(options=AutoReplyOptions( 33 | rules=[ 34 | AutoReplyRule(keyword='ding', reply_content='dong'), 35 | AutoReplyRule(keyword='七龙珠', reply_content='七龙珠'), 36 | AutoReplyRule( 37 | keyword='七龙珠', 38 | reply_content=FileBox.from_url(img_url, name='python.png') 39 | ), 40 | AutoReplyRule( 41 | keyword='网易-李白', 42 | reply_content=MiniProgram.create_from_json({...}) 43 | ) 44 | ], 45 | matchers=[ 46 | ContactMatcher('秋客'), 47 | ] 48 | )) 49 | bot = Wechaty().use(plugin) 50 | await bot.start() 51 | 52 | asyncio.run(run()) 53 | ``` 54 | 55 | 代码非常简单(API设计的很人性化),相信大家一眼就能够看懂,在此我就不做过多解释。 -------------------------------------------------------------------------------- /docs/how-to/how-to_finder.md: -------------------------------------------------------------------------------- 1 | ### 一、Finder 2 | 3 | 插件中的部分业务功能通常是只针对于指定群聊或联系人,于是如何检索到指定对象,就成为开发的第一个问题。在此,我给大家介绍Finder,一个用于检索群聊,联系人的功能模块。 4 | 5 | #### 1.1 Contact Finder 6 | 7 | 有很多种方式筛选联系人,比如最常见的通过`contact_id`、`contact name/alias`、`callback_func`等方法。使用方法如下所示: 8 | 9 | ```python 10 | from __future__ import annotations 11 | from typing import List 12 | import re 13 | 14 | from wechaty import Wechaty 15 | from wechaty.user.contact import Contact 16 | from wechaty.user.room import Room 17 | from wechaty_puppet.schemas.room import RoomQueryFilter 18 | from wechaty_plugin_contrib import ( 19 | RoomFinder, 20 | FinderOption, 21 | ContactFinder 22 | ) 23 | 24 | async def find_wechaty_contacts(bot: Wechaty) -> List[Contact]: 25 | contacts: List[Contact] = await bot.Contact.find_all('Baby') 26 | return contacts 27 | 28 | async def contact_finders(bot: Wechaty) -> List[Contact]: 29 | """Contact Finder Example Code""" 30 | options: List[FinderOption] = [ 31 | # 通过contact-id来筛选指定联系人 32 | 'contact-id', 33 | # 通过Pattern(正则化表达式)来筛选群聊 34 | re.Pattern(r'Baby-\d'), 35 | # 通过回调函数来检索房间 36 | find_wechaty_contacts 37 | ] 38 | contact_finder = ContactFinder(options) 39 | contacts: List[Contact] = await contact_finder.match(bot) 40 | return contacts 41 | ``` 42 | 43 | #### 1.2 Room Finder 44 | 45 | ```python 46 | from __future__ import annotations 47 | from typing import List 48 | import re 49 | 50 | from wechaty import Wechaty 51 | from wechaty.user.contact import Contact 52 | from wechaty.user.room import Room 53 | from wechaty_puppet.schemas.room import RoomQueryFilter 54 | from wechaty_plugin_contrib import ( 55 | RoomFinder, 56 | FinderOption, 57 | ContactFinder 58 | ) 59 | 60 | async def find_wechaty_rooms(bot: Wechaty) -> List[Room]: 61 | return await bot.Room.find_all(RoomQueryFilter(topic='Wechaty Room 1')) 62 | 63 | async def room_finders(bot: Wechaty) -> List[Room]: 64 | """Room Finder Example Code""" 65 | room_finder_options: List[FinderOption] = [ 66 | # 通过room-id来筛选指定群聊 67 | 'room-id', 68 | # 通过Pattern(正则化表达式)来筛选群聊 69 | re.Pattern(r'Wechaty(.*)') 70 | ] 71 | room_finder = RoomFinder(room_finder_options) 72 | rooms: List[Room] = await room_finder.match(bot) 73 | return rooms 74 | ``` 75 | 76 | ### 二、Matcher 77 | 78 | ### 2.1 Contact Matcher 79 | 80 | ### 2.2 Room Matcher 81 | 82 | ### 2.3 Message Matcher -------------------------------------------------------------------------------- /docs/how-to/how-to_github_webhook.md: -------------------------------------------------------------------------------- 1 | > TODO: Github Webhook 插件 -------------------------------------------------------------------------------- /docs/how-to/how-to_gitlab_webhook.md: -------------------------------------------------------------------------------- 1 | > TODO: Gitlab Webhook 插件 -------------------------------------------------------------------------------- /docs/how-to/how-to_introduction.md: -------------------------------------------------------------------------------- 1 | ## 这一节内容为wechaty的进阶使用 2 | 3 | - [如何添加好友](./how-to_add_friendship.md) 4 | - [如何关键字入群](./how-to_room_inviter.md) 5 | - [如何完成自动回复](./how-to_auto_reply.md) 6 | - [如何检索群聊联系人](./how-to_finder.md) 7 | - [如何完成任务调度](./how-to_scheduler.md) 8 | - [如何完成群消息同步](./how-to_message_forward.md) 9 | - [如何使用Rasa Sever](./how-to_rasa.md) 10 | - [如何使用Github Webhook插件](./how-to_github_webhook.md) 11 | - [如何使用Gitlab Webhook插件](./how-to_gitlab_webhook.md) -------------------------------------------------------------------------------- /docs/how-to/how-to_message_forward.md: -------------------------------------------------------------------------------- 1 | > TODO: 使用多群消息同步插件完成 -------------------------------------------------------------------------------- /docs/how-to/how-to_rasa.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Rasa Rest Connector" 3 | author: wj-mcat 4 | categories: tutorial 5 | tags: 6 | - python 7 | - plugin 8 | - rasa 9 | --- 10 | 11 | ## Rasa Plugin 12 | 13 | 用于将Rasa Server对接到Python Wechaty中,让你的Bot拥有智能对话管理的能力。 14 | 15 | ### 一、Quick Start 16 | 17 | #### 1.1 Rasa Server 18 | 19 | 首先你需要启动Rasa Server,推荐的脚本如下所示: 20 | 21 | > 假设rasa模型都已经训练好,能够正常运行,如果对rasa还不是很熟悉的同学,可以参考[rasa-getting-started](https://github.com/BOOOOTBAY/rasa-getting-started) 22 | 23 | ```shell 24 | rasa run --credentials credentials.yml \ 25 | --cors "*" --debug --endpoints endpoints.yml --enable-api 26 | ``` 27 | 28 | #### 1.2 Rasa Plugin 29 | 30 | 如果想要在python-wechaty中使用此插件,可参考以下代码: 31 | 32 | ```shell 33 | pip install wechaty-plugin-contrib 34 | ``` 35 | 36 | ```python 37 | """rasa plugin bot examples""" 38 | from __future__ import annotations 39 | 40 | import asyncio 41 | from wechaty import Wechaty # type: ignore 42 | 43 | from wechaty_plugin_contrib import ( 44 | RasaRestPlugin, 45 | RasaRestPluginOptions 46 | ) 47 | 48 | async def run(): 49 | """async run method""" 50 | options = RasaRestPluginOptions( 51 | endpoint='your-endpoint', 52 | conversation_ids=['room-id', 'contact-id'] 53 | ) 54 | rasa_plugin = RasaRestPlugin(options) 55 | 56 | bot = Wechaty().use(rasa_plugin) 57 | await bot.start() 58 | 59 | asyncio.run(run()) 60 | ``` 61 | 62 | -------------------------------------------------------------------------------- /docs/how-to/how-to_room_inviter.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "自动回复&关键字入群插件" 3 | --- 4 | 5 | 经不可靠统计,大部分聊天机器人的初学者都是以:自动回复和关键字入群这两个基础功能上手,然后才会逐步开发更多更复杂的功能,在此我将介绍如何使用python-wechaty快速实现这两个功能。 6 | 7 | > python-wechaty:一个面向所有IM平台的聊天机器人框架。 8 | 9 | ## 一、背景介绍 10 | 11 | ### 1.1 自动回复 12 | 13 | 有接触到微信公众号的同学都知道,它有一个自动回复的功能:你给它发送一个关键字,它就给你回复指定的内容,可以是纯文字,图片,视频等。 14 | 15 | > 微信公众号的自动回复数量只有200个,虽然能够满足大部分的需求,可是如果扩充的话,便可以选择自定义开发。 16 | 17 | 而在微信中回复的内容又可以是什么呢?可以是:纯文字、带艾特消息@的文字、图片、动图(表情包)、视频,语音、视频、小程序以及好友名片等,这些消息内容在python-wechaty都能够使用简单的配置即可完成此功能。 18 | 19 | ### 1.2 关键字入群 20 | 21 | 目前有很多社区运营者都会面临着一些问题: 22 | 23 | - 群人数一多,就只能够一个一个拉人入群。 24 | - 有好几个用户/开发者群,某些群人数满了,无法动态拉到其他群。 25 | - 一个人管理的活动太多,每次都需要根据用户的意图将其拉到指定的群,大大增加运营同学的工作量。 26 | - 每天就光拉人入群就花了半天的时间,还有半天是在偷着休息,因为太累了。 27 | - ...... 28 | 29 | 以上问题都是我根据身边部分同学和朋友的吐槽中总结而来,为了帮助他们快速完成KPI,提升业绩,我做了以下AutoReplyPlugin和RoomInviterPlugin两个插件。 30 | 31 | 接下来我将介绍如何上手这两个插件,快速解决你们身边的一些问题。 32 | 33 | ## 二、安装 & 配置 34 | 35 | 编程环境要求python3.7+版本,以及一个token两个依赖包。 36 | 37 | ### 1.1 配置Token 38 | 39 | 什么是Token?为什么要配置Token?如何获取Token? 40 | 41 | 这么粗暴的灵魂三问在我们官网上早已有相关的[文档](https://github.com/juzibot/Welcome/wiki/Everything-about-Wechaty),也欢迎各位去挖掘我们潜在的文档链接,说不定你就能找到属于你的One Piece,所以在此章节我就只介绍如何在python-wechaty中配置Token。 42 | 43 | Token的配置可以有多种方式: 44 | 45 | - 方法一:通过环境变量来配置 46 | 47 | ```bash 48 | export WECHATY_PUPPET_SERVICE_TOKEN='your-token' 49 | ``` 50 | 51 | - 方法二:通过python代码来配置 52 | 53 | ```python 54 | import os 55 | os.environ['WECHATY_PUPPET_SERVICE_TOKEN'] = 'your-token' 56 | ``` 57 | 58 | 那如何获取长期Token呢?详细请看:[Everything-about-Wechaty](https://github.com/juzibot/Welcome/wiki/Everything-about-Wechaty) 59 | 60 | ### 1.2 安装依赖包 61 | 62 | 整个依赖包分为两个:`wechaty`以及`wechaty-plugin-contrib`,安装脚本如下所示: 63 | 64 | ```bash 65 | # 安装python-wechaty包 66 | pip install wechaty 67 | # 安装插件库 68 | pip install wechaty-plugin-contrib 69 | ``` 70 | 71 | 前者为使用python-wechaty开发聊天机器人的基础依赖包,里面包含开发过程中的对象,甚至是自定义插件;后者为官方维护的插件库,在里面有很多常见的基础插件,让你快速解决日常学习工作中的自动化问题。同时也欢迎各位来提交PR,贡献属于自己的插件。 72 | 73 | Wechaty社区欢迎各位优秀开发者共建Chatbot领域基础设施 74 | 75 | ## 三、关键字入群 76 | 77 | 关键字入群是很多社区运营同学的日常工作,也是最消耗体力的活儿,并没有很很复杂的脑力活儿。现在都0202年了,居然有同学还没有使用到自动化工具来提升工作效率,更有趣的事儿,他们大部分都有一个程序猿同事/同学/老公/老婆。为了让他们更好的帮助自己身边的人解决问题,关键字入群这个插件你们必须得安利一波儿~ 78 | 79 | ### 3.1 功能介绍 80 | 81 | 当用户私聊你,发送一个关键字,然后聊天机器人会根据关键字寻找到对应的群,比如你给Wechaty官方机器人发送一个“wechaty”的关键字,它会将你拉到Wechaty的开发者群内,并发送欢迎语。 82 | 83 | 功能实际上很简单,如果从零开发的话,会让你无从下说。可是如果你使用python-wechaty的话,只需要几行简单的配置代码即可开发此功能。 84 | 85 | `RoomInviterPlugin` Is All You Need. 86 | 87 | ### 3.2 示例代码 88 | 89 | 最好的代码永远是最简单的代码 90 | 91 | 以下代码接近于人类语言,即使是新手,相信看完也知道如何开发专属聊天机器人: 92 | 93 | ```python 94 | import asyncio 95 | from typing import Dict 96 | from wechaty import Wechaty 97 | from wechaty_plugin_contrib.contrib import ( 98 | RoomInviterOptions, 99 | RoomInviterPlugin 100 | ) 101 | from wechaty_plugin_contrib.matchers import ( 102 | MessageMatcher, 103 | RoomMatcher 104 | ) 105 | 106 | async def run(): 107 | """async run method""" 108 | rules: Dict[MessageMatcher, RoomMatcher] = { 109 | MessageMatcher('wechaty'): RoomMatcher('Wechaty开发者群(1)'), 110 | MessageMatcher('python-wechaty'): RoomMatcher('Python-Wechaty开发者群(2)'), 111 | } 112 | plugin = RoomInviterPlugin(options=RoomInviterOptions( 113 | name='python-wechaty关键字入群插件', 114 | rules=rules, 115 | welcome='欢迎入群 ~' 116 | )) 117 | bot = Wechaty().use(plugin) 118 | await bot.start() 119 | 120 | asyncio.run(run()) 121 | ``` 122 | 123 | 在以上代码中,主要是分为三步:导入对象,注入规则,启动机器人。 124 | 125 | - **导入对象** 126 | 127 | 在`wechaty-plugin-contrib`的插件库中,所有的插件都会存在于`wechaty_plugin_contrib.contrib`下。大家一方面可以从源代码中查看的到最新的插件列表,也可以从README中查看到对应的插件列表。 128 | 129 | > 相信大家能够在这里找到灵感来源,或者一怒之下提交自己的定制插件。 130 | 131 | - 注入**规则** 132 | 133 | 在上述代码中,`rules`是一个规则字典,表示匹配到指定消息后就将消息发送者邀请到指定的群内;`plugin`就是封装核心逻辑组件,处理所有的自动化操作逻辑,开发者不需要关心这部分。 134 | 135 | - 启动机器人 136 | 137 | 启动机器人只需要调用一下start这个函数。 138 | 139 | ## 三、自动回复 140 | 141 | 自动回复也是我们日常生活工作中的一些高频使用场景,而回复内容不仅限于文字,还可以是图片,文件,链接以及小程序等等。比如你给机器人发“网易”,它会给你发送一个网易云音乐的小程序;你给它发一个”身份证“,它会给你发送身份证的正反面照片;...... 等等。 142 | 143 | 以上应用场景很常见,而且还有更多的实际应用案例可根据自己的需求来调整。 144 | 145 | 示例代码如下所示: 146 | 147 | ```python 148 | import asyncio 149 | from wechaty import Wechaty, MiniProgram # type: ignore 150 | from wechaty_puppet import ( # type: ignore 151 | FileBox 152 | ) 153 | 154 | from wechaty_plugin_contrib import ( 155 | AutoReplyRule, 156 | AutoReplyPlugin, 157 | AutoReplyOptions, 158 | ) 159 | 160 | from wechaty_plugin_contrib.matchers import ContactMatcher 161 | 162 | async def run(): 163 | """async run method""" 164 | img_url = 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy' \ 165 | '/it/u=1257042014,3164688936&fm=26&gp=0.jpg' 166 | plugin = AutoReplyPlugin(options=AutoReplyOptions( 167 | rules=[ 168 | AutoReplyRule(keyword='ding', reply_content='dong'), 169 | AutoReplyRule(keyword='七龙珠', reply_content='七龙珠'), 170 | AutoReplyRule( 171 | keyword='七龙珠', 172 | reply_content=FileBox.from_url(img_url, name='python.png') 173 | ), 174 | AutoReplyRule( 175 | keyword='网易-李白', 176 | reply_content=MiniProgram.create_from_json({...}) 177 | ) 178 | ], 179 | matchers=[ 180 | ContactMatcher('秋客'), 181 | ] 182 | )) 183 | bot = Wechaty().use(plugin) 184 | await bot.start() 185 | 186 | asyncio.run(run()) 187 | ``` 188 | 189 | 代码非常简单(API设计的很人性化),相信大家一眼就能够看懂,在此我就不做过多解释。 190 | 191 | ## 四、总结 192 | 193 | python-wechaty有非常人性化的API,同时内置了很多高频功能插件库,提供给开发者能够快速上手开发出自己的小应用。 194 | 195 | 整个wechaty的目标是面向所有IM平台,打造一款通用聊天机器人框架,也欢迎各位关注并使用[python-wechaty](https://github.com/wechaty/python-wechaty)框架。 196 | -------------------------------------------------------------------------------- /docs/how-to/how-to_scheduler.md: -------------------------------------------------------------------------------- 1 | > TODO: 任务调度框架 -------------------------------------------------------------------------------- /docs/how-to/how-to_use_plugin.md: -------------------------------------------------------------------------------- 1 | ## 插件系统 2 | 3 | 插件系统提供了模块化的管理,能够让不同业务的代码隔离开,特别是针对于复杂的业务。 4 | 5 | 在处理不同业务时,通常选择将指定业务封装成一个插件,wechaty社区也欢迎大家贡献自己的插件,从而快速实现某些简单功能。 6 | 7 | ### 一、插件列表 8 | > 以下只是列表,具体的使用方法以及在前几篇文章中进行了说明,此处只是声明可能存在的插件,以及插件的调用方式。 9 | 10 | - [关键字入群插件](./how-to_room_inviter.md) 11 | - [自动回复插件](./how-to_auto_reply.md) 12 | - [任务调度插件](./how-to_scheduler.md) 13 | - [群消息同步插件](./how-to_message_forward.md) 14 | - [Rasa Rest Connector](./how-to_rasa.md) 15 | - [Github Webhook插件](./how-to_github_webhook.md) 16 | - [Gitlab Webhook插件](./how-to_gitlab_webhook.md) -------------------------------------------------------------------------------- /docs/how-to/use-padlocal-protocol.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Python Wechaty如何使用PadLocal Puppet Service 3 | --- 4 | 5 | ## Python Wechaty如何使用PadLocal Puppet Service 6 | 7 | 本文描述Python语言下如何使用iPad协议的PadLocal Token。其他Wechaty多语言开发也能做参考。 8 | 9 | - [wechaty-puppet-padlocal](https://github.com/padlocal/wechaty-puppet-padlocal) 10 | - [TOKEN 申请方法](https://wechaty.js.org/docs/puppet-services/) 11 | 12 | ## 搭建PadLocal Token Gateway 13 | 14 | ```shell 15 | # 设置环境变量 16 | 17 | export WECHATY_LOG="verbose" 18 | export WECHATY_PUPPET="wechaty-puppet-padlocal" 19 | export WECHATY_PUPPET_PADLOCAL_TOKEN="puppet_padlocal_XXXXXX" 20 | 21 | export WECHATY_PUPPET_SERVER_PORT="9001" 22 | export WECHATY_TOKEN="1fe5f846-3cfb-401d-b20c-XXXXX" 23 | 24 | docker run -ti \ 25 | --name wechaty_puppet_service_token_gateway \ 26 | --rm \ 27 | -e WECHATY_LOG \ 28 | -e WECHATY_PUPPET \ 29 | -e WECHATY_PUPPET_PADLOCAL_TOKEN \ 30 | -e WECHATY_PUPPET_SERVER_PORT \ 31 | -e WECHATY_TOKEN \ 32 | -p "$WECHATY_PUPPET_SERVER_PORT:$WECHATY_PUPPET_SERVER_PORT" \ 33 | wechaty/wechaty:0.56 34 | ``` 35 | 36 | - WECHATY_PUPPET_PADLOCAL_TOKEN 申请得到的token代码 37 | - WECHATY_PUPPET_SERVER_PORT 设置对外访问端口,需要保证端口没被占用,没被防火墙匹配 38 | - WECHATY_TOKEN 生成个人随机[TOKEN](https://www.uuidgenerator.net/version4)。WECHATY_TOKEN:个人理解为和远程wechaty服务器做通讯用,通过这个唯一token可以返回当前主机访问地址和端口。所以需要避免和别人重复。 39 | 40 | 可以通过下面代码,确定是否成功。 41 | 42 | ```shell 43 | curl https://api.chatie.io/v0/hosties/$WECHATY_TOKEN (个人随机token) 44 | {"ip":"36.7.XXX.XXX","port":9001} 45 | ``` 46 | 47 | ## python-Wechaty对接GateWay 48 | 49 | 在对接Gateway的时候,这里需要注意下,如果GateWay是部署在公网可以访问的服务器上,按照默认配置就可访问;如果是部署在自己内网服务器上,就会报`Your service token has no available endpoint, is your token correct?`,这个时候需要设置WECHATY_PUPPET_SERVICE_ENDPOINT。 50 | 51 | ```shell 52 | #1 默认配置 53 | export WECHATY_PUPPET="wechaty-puppet-service" 54 | export WECHATY_PUPPET_SERVICE_TOKEN="1fe5f846-3cfb-401d-b20c-XXXXX" 55 | 56 | #2 主机是部署在内网服务器上 57 | export WECHATY_PUPPET="wechaty-puppet-service" 58 | export WECHATY_PUPPET_SERVICE_TOKEN="1fe5f846-3cfb-401d-b20c-XXXXX" 59 | export WECHATY_PUPPET_SERVICE_ENDPOINT="192.168.1.56:9001" 60 | ``` 61 | 62 | WECHATY_PUPPET_SERVICE_ENDPOINT:内网IP地址:端口号 63 | 64 | ### python-wechaty-getting-started 65 | 66 | ```shell 67 | git clone https://github.com/wj-Mcat/python-wechaty-getting-started 68 | cd python-wechaty-getting-started 69 | 70 | export WECHATY_PUPPET="wechaty-puppet-service" 71 | export WECHATY_PUPPET_SERVICE_TOKEN="1fe5f846-3cfb-401d-b20c-XXXXX" 72 | 73 | python examples/ding-dong-bot.py 74 | ``` 75 | 76 | 到此,恭喜你入坑。 77 | 具体的使用可以查看[python-wechaty-getting-started](https://github.com/wechaty/python-wechaty-getting-started) 78 | 79 | ## 参考 80 | 81 | - 如何成为 `Wechaty Contributor` 可以通过该链接查看 [https://wechaty.js.org/docs/contributor-program/](https://wechaty.js.org/docs/contributor-program/) 82 | - [.NET Wechaty 如何使用 PadLocal Puppet Service](https://wechaty.js.org/2021/01/28/csharp-wechaty-for-padlocal-puppet-service/) 83 | - 特别感谢 @huan 的帮助。 84 | -------------------------------------------------------------------------------- /docs/how-to/use-web-protocol.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "教你用python-wecahty和web协议开发机器人" 3 | date: 2021-04-25 4 | --- 5 | 6 | 写这篇文章的原因: go-wechaty作者[dchaofei](https://github.com/dchaofei)抢先写了[web协议复活的博客](https://wechaty.js.org/2021/04/16/go-wechaty-use-web/),作为[python-wechaty](http://github.com/wechaty/python-wechaty)的作者我也需要给大家更加详细的介绍如何使用[python-wechaty](http://github.com/wechaty/python-wechaty)来登陆web版本的微信。 7 | 8 | ## 一、介绍 9 | 10 | 微信版本的机器人种类很多,出现的协议也很多,比如Ipad、Mac以及Windows协议,而最早出现的其实是web版本的协议。在前几年由于腾讯的一些限制,将大部分用户的web登陆的权限给关掉了,导致很多web协议版本的微信机器人直接死掉了,比如著名的itchat。 11 | 12 | 可是自从统信和腾讯共同推出桌面版本的微信之后,web版本的机器人以某种方式复活了,而wechaty便是最早来解决这个事情的开源项目之一,接下来我将详细介绍如何使用[python-wechaty](http://github.com/wechaty/python-wechaty)基于web版本协议开发聊天机器人。 13 | 14 | 整体步骤分为两步: 15 | 16 | * 使用Docker启动web协议服务 17 | * 使用python-wechaty连接服务 18 | 19 | 第一步将web版本的协议以gRPC服务的形式暴露出来,使用过程非常简单,只是需要注意几个配置项;第二步则是使用python-wechaty连接该服务,开发聊天机器人。 20 | 21 | ## 二、启动web协议服务 22 | 23 | 启动web协议服务脚本如下所示: 24 | 25 | ```shell 26 | docker pull wechaty/wechaty:latest 27 | 28 | export WECHATY_LOG="verbose" 29 | export WECHATY_PUPPET="wechaty-puppet-wechat" 30 | export WECHATY_PUPPET_SERVER_PORT="8080" 31 | export WECHATY_TOKEN="python-wechaty-uos-token" 32 | export WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_SERVER="true" 33 | 34 | docker run -ti \ 35 | --name wechaty_puppet_service_token_gateway \ 36 | --rm \ 37 | -e WECHATY_LOG \ 38 | -e WECHATY_PUPPET \ 39 | -e WECHATY_PUPPET_SERVER_PORT \ 40 | -e WECHATY_TOKEN \ 41 | -p "$WECHATY_PUPPET_SERVER_PORT:$WECHATY_PUPPET_SERVER_PORT" \ 42 | wechaty/wechaty:latest 43 | ``` 44 | 45 | 如果是在本地测试时,`WECHATY_PUPPET_SERVER_PORT`和`WECHATY_TOKEN`相对比较随意,大家都可以随时设置,因为下一步中的连接可以设置本地连接。 46 | 47 | 如果是在服务端部署时,`WECHATY_PUPPET_SERVER_PORT`是需要保证所在服务器的该端口是保持开放的,以保证使用`python-wechaty`能够正常连接;此外`WECHATY_TOKEN`将用于在wechaty token中心注册启动的服务,以让[python-wechaty](http://github.com/wechaty/python-wechaty)能够找到该服务的地址,所以必须是修改成唯一标识符,推荐使用`uuid`来代替`python-wechaty-uos-token`。 48 | 49 | ## 三、连接服务 50 | 51 | 使用python开发最简单的聊天机器人,代码如下所示: 52 | 53 | ```python 54 | # bot.py 55 | from wechaty import Wechaty 56 | import os 57 | 58 | import asyncio 59 | async def main(): 60 | bot = Wechaty() 61 | bot.on('scan', lambda status, qrcode, data: print('Scan QR Code to login: {}\nhttps://wechaty.js.org/qrcode/{}'.format(status, qrcode))) 62 | bot.on('login', lambda user: print('User {} logged in'.format(user))) 63 | bot.on('message', lambda message: print('Message: {}'.format(message))) 64 | await bot.start() 65 | 66 | asyncio.run(main()) 67 | ``` 68 | 69 | 当在本地测试时,可以通过设置`WECHATY_PUPPET_SERVICE_ENDPOINT`环境变量让`python-wechaty`直接与本地的web服务连接。例如:`WECHATY_PUPPET_SERVICE_ENDPOINT=127.0.0.1:8080`,运行脚本如下所示: 70 | 71 | ```shell 72 | WECHATY_PUPPET_SERVICE_TOKEN=python-wechaty-uos-token WECHATY_PUPPET_SERVICE_ENDPOINT=127.0.0.1:8080 python bot.py 73 | ``` 74 | 75 | 当在远端服务器部署时,只需要设置`WECHATY_PUPPET_SERVICE_TOKEN`即可连接启动的web服务,运行脚本如下所示: 76 | 77 | ```shell 78 | WECHATY_PUPPET_SERVICE_TOKEN=python-wechaty-uos-token python bot.py 79 | ``` 80 | 81 | ## 四、总结 82 | 83 | python-wechaty是一个非常简单的聊天机器人框架,理论上能够对接任何IM平台,拥有原生与AI对接的能力,能够快速开发出功能强大的Chatbot,欢迎大家关注[python-wechaty](https://github.com/wechaty/python-wechaty) 84 | 85 | ## 五、相关链接 86 | 87 | * [python-wechty](https://github.com/wechaty/python-wechaty) 88 | * [python-wechaty getting started](https://github.com/wechaty/python-wechaty-getting-started ) 89 | * [web协议复活](https://wechaty.js.org/2021/04/13/wechaty-uos-web/) 90 | * [Python Wechaty Getting Started](https://wechaty.js.org/docs/polyglot/python/) 91 | * [puppet-providers](https://wechaty.js.org/docs/puppet-providers/wechat) 92 | -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/python-wechaty/e9a04a98a3b01f287760e2d2a4514e4a80ecd15f/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/getting-started/python-wechaty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/python-wechaty/e9a04a98a3b01f287760e2d2a4514e4a80ecd15f/docs/img/getting-started/python-wechaty.png -------------------------------------------------------------------------------- /docs/img/introduction/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/python-wechaty/e9a04a98a3b01f287760e2d2a4514e4a80ecd15f/docs/img/introduction/cloud.png -------------------------------------------------------------------------------- /docs/img/wechaty-icon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.16, written by Peter Selinger 2001-2019 9 | 10 | 12 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/img/wechaty-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.16, written by Peter Selinger 2001-2019 9 | 10 | 12 | 35 | 64 | 73 | 79 | 87 | 96 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to python-wechaty 2 | 3 | ![](./img/getting-started/python-wechaty.png) 4 | 5 | ## 一、Wechaty 是什么 6 | 7 | Wechaty 是一个开源聊天机器人框架SDK,具有高度封装、高可用的特性,支持NodeJs, Python, Go 和Java 等多语言版本。在过去的4年中,服务了数万名开发者,收获了 Github 的 1w+ Star。同时配置了完整的 DevOps 体系并持续按照 Apache 的方式管理技术社区。 8 | 9 | 目前IM平台众多,为了实现`write once run anlywhere`,Wechaty 将IM平台中通用的消息处理进行高度抽象封装,提供统一的上层接口,让开发者不用关心具体底层实现细节,用简单的代码开发出功能强大的聊天机器人。 10 | 11 | ## 二、Python-Wechaty 是什么 12 | 13 | > 理论上python-wechaty可以对接任何IM平台 14 | 15 | python-wechaty是基于Wechaty生态派生出的Python编程语言客户端,能够让开发者使用少量代码对接到各个即时通讯软件平台。在过去的一年里,python-wechaty致力于提升代码鲁棒性、添加社区开箱即用的工具、以及完善软件开发文档。 16 | 17 | 目前可对接: 18 | 19 | - [微信](https://github.com/wechaty/wechaty-puppet-wechat) 20 | - [微信公众号](https://github.com/wechaty/wechaty-puppet-official-account) 21 | - [钉钉](https://github.com/wechaty/wechaty-puppet-dingtalk) 22 | - [飞书](https://github.com/wechaty/wechaty-puppet-lark) 23 | - [WhatsApp](https://github.com/wechaty/wechaty-puppet-whatsapp) 24 | - [Gitter](https://github.com/wechaty/wechaty-puppet-gitter) 25 | - ... 26 | 27 | ## 三、TOKEN 是什么 28 | 29 | 如果要开发微信聊天机器人时,wechaty会使用token来连接第三方的服务;如果要开发飞书聊天机器人时,wechaty会使用token和secret来连接官方服务接口;如果要将node puppet以服务的形式部署到服务器上时,自定义的token将会是作为服务连接的钥匙。 30 | 31 | ![token gateway](./img/introduction/cloud.png) 32 | 33 | TOKEN是一个用来连接底层服务的密钥,也是开发聊天机器人的第一步;官网有介绍[如何获取TOKEN](https://wechaty.js.org/docs/puppet-services/#get-a-token)。 34 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Welcome to python-wechaty 2 | 3 | ## Wechaty 4 | 5 | Wechaty 是一个开源聊天机器人框架SDK,具有高度封装、高可用的特性,支持NodeJs, Python, Go 和Java 等多语言版本。在过去的4年中,服务了数万名开发者,收获了 Github 的 8000 Star。同时配置了完整的 DevOps 体系并持续按照 Apache 的方式管理技术社区。 6 | 7 | ![]() 8 | 9 | ## Project layout 10 | 11 | mkdocs.yml # The configuration file. 12 | docs/ 13 | index.md # The documentation homepage. 14 | ... # Other markdown pages, images and other files. 15 | -------------------------------------------------------------------------------- /docs/introduction/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "上手视频" 3 | author: wj-mcat 4 | categories: tutorial 5 | tags: 6 | - news 7 | - python 8 | image: /assets/2020/python-wechaty/live-coding.png 9 | --- 10 | 11 | ## Python-Wechaty 12 | 13 | Wechaty 作为一个对话SDK,拥有适配多平台的优秀能力,同时还具备多语言的特性,今天我们将以一个简单的视频来介绍如何开始使用[Python-Wechaty](https://github.com/wechaty/python-wechaty)编写一个最简单的聊天机器人。 14 | 15 | {% include iframe.html src="https://www.youtube.com/watch?v=KSELdGeJIzo" %} 16 | 17 | ## 上手步骤 18 | 19 | ### 1. 安装依赖包 20 | 21 | ```shell 22 | pip install wechaty 23 | ``` 24 | 25 | ### 2. 配置Token 26 | 27 | Token的配置可以有多种方式: 28 | 29 | 方法一:通过环境变量来配置 30 | 31 | ```shell 32 | export WECHATY_PUPPET_SERVICE_TOKEN='your-token' 33 | ``` 34 | 35 | 方法二:通过python代码来配置 36 | 37 | ```python 38 | import os 39 | os.environ['WECHATY_PUPPET_SERVICE_TOKEN'] = 'your-token' 40 | ``` 41 | 42 | 那如何获取长期Token呢?详细请看:[Everything-about-Wechaty](https://github.com/juzibot/Welcome/wiki/Everything-about-Wech aty) 43 | 44 | ### 3. 编写最简单的机器人代码 45 | 46 | > talk is cheep, show you the code 47 | 48 | ```python 49 | import asyncio 50 | from wechaty import Wechaty, Message 51 | 52 | class MyBot(Wechaty): 53 | async def on_message(self, msg: Message): 54 | talker = msg.talker() 55 | await talker.ready() 56 | if msg.text() == "ding": 57 | await talker.say('dong') 58 | elif msg.text() == 'image': 59 | file_box = FileBox.from_url( 60 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 61 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 62 | name='ding-dong.jpg') 63 | await talker.say(file_box) 64 | 65 | async def main(): 66 | bot = MyBot() 67 | await bot.start() 68 | 69 | asyncio.run(main()) 70 | ``` 71 | 72 | 以上代码即可完成一个最简单的`ding-dong`机器人,以及你给他发送一个`image`关键字,它能够给你回复一个图片,代码是不是非常简单呢? 73 | 74 | 这里还有功能更加强大的机器人[示例代码库](https://github.com/wechaty/python-wechaty-getting-started),大家可以在这里来找与自己需求类似的机器人。 75 | 76 | 也欢迎大家持续关注[python-wechaty](https://github.com/wechaty/python-wechaty),未来我们将持续发布一些短视频来介绍相关的用法。 77 | -------------------------------------------------------------------------------- /docs/introduction/use-padlocal-protocol.md: -------------------------------------------------------------------------------- 1 | # 使用Padlocal协议 2 | 3 | ## 一、介绍 4 | 5 | 本节与上一篇章的Web协议相比,区别只是在启动服务的环节:Token Gateway。 6 | 7 | ## 二、为什么需要网关 8 | 9 | ### 2.1 TOKEN 协议 10 | 11 | 在之前的付费微信协议中,通常是需要将服务部署在商家的集群上面,所有的消息接受与发送都是经过商家集群来管理,集中式管理固然能够监控意外bug,可是也带来了一个很严重的问题:所有的消息发送和接受的IP都是来自于一个IP池。 12 | 13 | 这个问题会加重账号的风险系数,毕竟官方是具备有一定的账号风控机制,让用户的账号处于一个危险的境地,担心消息接受的过多,发送的过多会不会让自己封号等等,而且这些因素的考虑也是合理的。 14 | 15 | ### 2.2 PadLocal 16 | 17 | 当然也有一定的解决方案:商家可以换对应的IP池,通过不定期的购买新的IP来减少消息扎堆的问题。这种方法虽然是缓解了这个问题,可是羊毛出在羊身上,维护成本大了,`TOKEN`肯定就贵了,对于双方来说都不太好。于是社区就基于Pad协议提出了`Local`的概念:[wechaty-puppet-padlocal](https://github.com/wechaty/wechaty-puppet-padlocal). 18 | 19 | 要使用此协议,必须在本地开启一个Gateway来发送和接受官方服务器发送过来的消息,这样消息的来源和发送目的地都是由开发者自己决定,就类似于电脑的客户端接受和发送消息,这样就极大程度上减小了账号风险系数,这也是协议中`Local`的原因。 20 | 21 | ### 2.3 使用步骤 22 | 23 | 即使是这样,使用起来与web协议区别不大,整体步骤也分为两步: 24 | 25 | * 使用Docker启动Padlocal网关服务 26 | * 使用python-wechaty连接服务 27 | 28 | ## 三、启动Padlocal网关服务 29 | 30 | ### 3.1 脚本 31 | 32 | - [wechaty-puppet-padlocal](https://github.com/padlocal/wechaty-puppet-padlocal) 33 | - [TOKEN 申请方法](https://wechaty.js.org/docs/puppet-services/) 34 | 35 | ```shell 36 | # 设置环境变量 37 | 38 | export WECHATY_LOG="verbose" 39 | export WECHATY_PUPPET="wechaty-puppet-padlocal" 40 | export WECHATY_PUPPET_PADLOCAL_TOKEN="puppet_padlocal_XXXXXX" 41 | 42 | export WECHATY_PUPPET_SERVER_PORT="9001" 43 | # 可使用代码随机生成UUID:import uuid;print(uuid.uuid4()); 44 | export WECHATY_TOKEN="1fe5f846-3cfb-401d-b20c-XXXXX" 45 | 46 | docker run -ti \ 47 | --name wechaty_puppet_service_token_gateway \ 48 | --rm \ 49 | -e WECHATY_LOG \ 50 | -e WECHATY_PUPPET \ 51 | -e WECHATY_PUPPET_PADLOCAL_TOKEN \ 52 | -e WECHATY_PUPPET_SERVER_PORT \ 53 | -e WECHATY_TOKEN \ 54 | -p "$WECHATY_PUPPET_SERVER_PORT:$WECHATY_PUPPET_SERVER_PORT" \ 55 | wechaty/wechaty:0.65 56 | ``` 57 | 58 | > 在此我默认所有的人都对[Docker](https://www.docker.com)的基本使用已经有了一定的了解,否则可以花几分钟去看看其[文档](https://www.docker.com/get-started)熟悉一下。 59 | 60 | ### 3.2 参数说明 61 | 62 | * **WECHATY_PUPPET**: **标识**使用的哪个协议,一般和`token`类型的一一对应。比如当使用`padlocal`协议的话,那这个就是`wechaty-puppet-padlocal`,如果使用`web`协议的话,那这个就是`wechaty-puppet-wechat`。 63 | * **WECHATY_PUPPET_PADLOCAL_TOKEN**: 这个协议是用来连接Padlocal的服务,目前是付费的。 64 | * **WECHATY_PUPPET_SERVER_PORT**: 网关服务的接口,提供给`python-wechaty`来连接调用,如果服务部署在云服务器上,则需要保证该端口的可访问性。 65 | * **WECHATY_TOKEN**: 当开发者在自己机器上启动一个网关服务时,需要通过`TOEKN`来做身份验证,避免服务被他人窃取。 66 | 67 | 以上代码只需要修改三个参数:`WECHATY_PUPPET_PADLOCAL_TOKEN`, `WECHATY_PUPPET_SERVER_PORT`, `WECHATY_TOKEN` 即可成功启动Token网关服务。 68 | 69 | 那网关服务启动成功之后,只需要编写`python-wechaty`的代码来连接即可。 70 | 71 | ## 四、连接服务 72 | 73 | ### 4.1 本地测试和远端部署 74 | 75 | 当启动网关服务时,`Padlocal`会根据`WECHATY_TOKEN`来在[Wechaty服务接口](https://api.chatie.io/v0/hosties/__TOKEN__)上注册部署机器的`IP`和`端口`,然后python-wechaty会根据`WECHATY_TOKEN`在[Wechaty服务接口](https://api.chatie.io/v0/hosties/__TOKEN__)上获取对应的IP和端口。 76 | 77 | 可是很多小伙伴在实际开发的过程中,通常会出现`endpoint is not invalid`等错误信息,那是因为开发者有可能在本地启动网关服务或者服务器端口没有开放。 78 | 79 | 网关服务的部署通常是分为本地测试和远端部署,前者通常只是为了初学测试,后者是为了生产部署。如果是在生产部署时,只需要设置环境变量: 80 | 81 | ```shell 82 | export WECHATY_PUPPET_SERVICE_TOKEN=1fe5f846-3cfb-401d-b20c-XXXXX 83 | # or 84 | export TOKEN=1fe5f846-3cfb-401d-b20c-XXXXX 85 | # or 86 | export token=1fe5f846-3cfb-401d-b20c-XXXXX 87 | ``` 88 | 89 | 可是如果是在本地测试时,则通过ENDPOINT来找到启动的网关服务。 90 | 91 | ```shell 92 | export WECHATY_PUPPET_SERVICE_TOKEN=1fe5f846-3cfb-401d-b20c-XXXXX 93 | # or 94 | export TOKEN=1fe5f846-3cfb-401d-b20c-XXXXX 95 | # or 96 | export token=1fe5f846-3cfb-401d-b20c-XXXXX 97 | 98 | export WECHATY_PUPPET_SERVICE_ENDPOINT=127.0.0.1:9001 99 | # or 100 | export ENDPOINT=127.0.0.1:9001 101 | # or 102 | export endpoint=127.0.0.1:9001 103 | ``` 104 | 105 | ### 4.2 TOKEN的作用 106 | 107 | 总而言之: 108 | 109 | * 如果是公网环境下,可只需要设置`TOKEN`即可(因为你的token已经注册在chatie server上,故可以获取到目标资源服务器的ip和port) 110 | * 如果是内网环境下,可只需要使用`ENDPOINT`(`localhost:port`)来让python-wechaty连接目标资源服务器。 111 | 112 | > 如果是token是padlocal类型,则在python-wechaty程序内部可直接设置`export endpoint=localhost:port`来连接Gateway Server。 113 | 114 | 当然,以上的写法是使用Bash的方式来设置环境变量,也是可以通过python代码来设置环境变量,详细可看: 115 | 116 | ```python 117 | import os 118 | os.environ['token'] = "1fe5f846-3cfb-401d-b20c-XXXXX" 119 | os.environ['endpoint'] = "127.0.0.1:9001" 120 | ``` 121 | 122 | ## 五、示例代码 123 | 124 | > talke is cheep, show you the code 125 | 126 | ```python 127 | import asyncio, os 128 | from typing import List, Optional, Union 129 | 130 | from wechaty_puppet import FileBox # type: ignore 131 | 132 | from wechaty import Wechaty, Contact 133 | from wechaty.user import Message, Room 134 | 135 | 136 | class MyBot(Wechaty): 137 | 138 | async def on_message(self, msg: Message): 139 | """ 140 | listen for message event 141 | """ 142 | from_contact: Optional[Contact] = msg.talker() 143 | text = msg.text() 144 | room: Optional[Room] = msg.room() 145 | if text == 'ding': 146 | conversation: Union[ 147 | Room, Contact] = from_contact if room is None else room 148 | await conversation.ready() 149 | await conversation.say('dong') 150 | file_box = FileBox.from_url( 151 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 152 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 153 | name='ding-dong.jpg') 154 | await conversation.say(file_box) 155 | 156 | os.environ['TOKEN'] = "1fe5f846-3cfb-401d-b20c-XXXXX" 157 | os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = "127.0.0.1:9001" 158 | asyncio.run(MyBot().start()) 159 | ``` 160 | 161 | 欢迎各位品尝以上代码 🥳 162 | 163 | * **相关链接** 164 | * [python-wechaty](https://github.com/wechaty/python-wechaty) 165 | * [python-wechaty-getting-started](https://github.com/wechaty/python-wechaty-getting-started) 166 | -------------------------------------------------------------------------------- /docs/introduction/use-paimon-protocol.md: -------------------------------------------------------------------------------- 1 | # 使用Paimon协议 2 | 3 | ## 一、介绍 4 | 5 | python原生支持paimon协议,不需要Token Gateway,简单方便。 6 | [免费申请7天试用Token](https://wechaty.js.org/docs/puppet-services/paimon) 7 | 8 | 9 | ## 二、连接服务 10 | 11 | ### 2.1 本地测试和远端部署 12 | 13 | 14 | ```shell 15 | export WECHATY_PUPPET_SERVICE_TOKEN=puppet_paimon_XXXXX 16 | # or 17 | export TOKEN=puppet_paimon_XXXXX 18 | # or 19 | export token=puppet_paimon_XXXXX 20 | ``` 21 | 22 | 23 | 24 | 当然,以上的写法是使用Bash的方式来设置环境变量,也是可以通过python代码来设置环境变量,详细可看: 25 | 26 | ```python 27 | import os 28 | os.environ['token'] = "puppet_paimon_XXXXX" 29 | ``` 30 | 31 | ## 三、示例代码 32 | 33 | > talke is cheep, show you the code 34 | 35 | ```python 36 | import asyncio, os 37 | from typing import List, Optional, Union 38 | 39 | from wechaty_puppet import FileBox # type: ignore 40 | 41 | from wechaty import Wechaty, Contact 42 | from wechaty.user import Message, Room 43 | 44 | 45 | class MyBot(Wechaty): 46 | 47 | async def on_message(self, msg: Message): 48 | """ 49 | listen for message event 50 | """ 51 | from_contact: Optional[Contact] = msg.talker() 52 | text = msg.text() 53 | room: Optional[Room] = msg.room() 54 | if text == 'ding': 55 | conversation: Union[ 56 | Room, Contact] = from_contact if room is None else room 57 | await conversation.ready() 58 | await conversation.say('dong') 59 | file_box = FileBox.from_url( 60 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 61 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 62 | name='ding-dong.jpg') 63 | await conversation.say(file_box) 64 | 65 | os.environ['TOKEN'] = "1fe5f846-3cfb-401d-b20c-XXXXX" 66 | asyncio.run(MyBot().start()) 67 | ``` 68 | 69 | 欢迎各位品尝以上代码 🥳 70 | 71 | * **相关链接** 72 | * [python-wechaty](https://github.com/wechaty/python-wechaty) 73 | * [python-wechaty-getting-started](https://github.com/wechaty/python-wechaty-getting-started) 74 | -------------------------------------------------------------------------------- /docs/introduction/use-web-protocol.md: -------------------------------------------------------------------------------- 1 | # 使用免费Web协议 2 | 3 | ## 一、介绍 4 | 5 | 底层的对接实现是基于TypeScript语言,故无法直接在python-wechaty中使用该服务。可是Wechaty社区能够直接将其转化成对应的服务让多语言调用,从而实现:底层复用的特性。 6 | 7 | 整体步骤分为两步: 8 | 9 | * 使用Docker启动web协议服务 10 | * 使用python-wechaty连接服务 11 | 12 | ## 二、启动Web协议服务 13 | 14 | ```shell 15 | docker pull wechaty/wechaty:0.65 16 | 17 | export WECHATY_LOG="verbose" 18 | export WECHATY_PUPPET="wechaty-puppet-wechat" 19 | export WECHATY_PUPPET_SERVER_PORT="8080" 20 | export WECHATY_TOKEN="python-wechaty-{uuid}" 21 | export WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_SERVER="true" 22 | 23 | # save login session 24 | if [ ! -f "${WECHATY_TOKEN}.memory-card.json" ]; then 25 | touch "${WECHATY_TOKEN}.memory-card.json" 26 | fi 27 | 28 | docker run -ti \ 29 | --name wechaty_puppet_service_token_gateway \ 30 | --rm \ 31 | -v "`pwd`/${WECHATY_TOKEN}.memory-card.json":"/wechaty/${WECHATY_TOKEN}.memory-card.json" \ 32 | -e WECHATY_LOG \ 33 | -e WECHATY_PUPPET \ 34 | -e WECHATY_PUPPET_SERVER_PORT \ 35 | -e WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_SERVER \ 36 | -e WECHATY_TOKEN \ 37 | -p "$WECHATY_PUPPET_SERVER_PORT:$WECHATY_PUPPET_SERVER_PORT" \ 38 | wechaty/wechaty:0.65 39 | ``` 40 | 41 | 注意: 42 | 43 | * WECHATY_TOKEN 必须使用生成的UUID来替换,不然直接使用该token来启动的服务很容易被他人盗窃。 44 | 45 | 小伙伴们可在python解释器中运行以下代码来获得随机TOKEN: 46 | ```python 47 | # 例如:b2ff8fc5-c5a2-4384-b317-3695807e483f 48 | import uuid;print(uuid.uuid4()); 49 | ``` 50 | 51 | ## 三、连接服务 52 | 53 | 当使用docker来启动web服务时,可分为在本地环境测试以及在远端环境中测试,在连接方式上有一些不一样。 54 | 55 | ### 3.1 本地WEB服务 56 | 57 | 当在计算机本地启动web服务后,可直接使用python-wechaty连接本地的服务,不通过token来获取对应的服务连接地址。示例代码如下: 58 | 59 | ```shell 60 | export WECHATY_PUPPET_SERVICE_ENDPOINT=127.0.0.1:8080 61 | ``` 62 | 63 | 或者 64 | 65 | ```python 66 | import os 67 | os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:8080' 68 | ``` 69 | 70 | > 当你的服务和python-wechaty机器人代码都部署在服务器中时,此时也属于本地服务,可使用此方法来配置。 71 | 72 | ### 3.2 远端服务 73 | 74 | 当把服务部署在远端服务器中时,要保证该计算机能够被外网访问到,且对应端口开放。例如在上述示例脚本中,比如保证服务器的`8080`端口开放,而你的服务器IP为:`10.12.123.23`,此时可直接设置服务连接地址: 75 | 76 | ```shell 77 | export WECHATY_PUPPET_SERVICE_ENDPOINT=10.12.123.23:8080 78 | ``` 79 | 80 | 或者 81 | 82 | ```python 83 | import os 84 | os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '10.12.123.23:8080' 85 | ``` 86 | 87 | ## 四、编写代码 88 | 89 | > talk is cheep, show you the code 90 | 91 | ```python 92 | import asyncio 93 | from typing import List, Optional, Union 94 | 95 | from wechaty_puppet import FileBox # type: ignore 96 | 97 | from wechaty import Wechaty, Contact 98 | from wechaty.user import Message, Room 99 | 100 | 101 | class MyBot(Wechaty): 102 | 103 | async def on_message(self, msg: Message): 104 | """ 105 | listen for message event 106 | """ 107 | from_contact: Optional[Contact] = msg.talker() 108 | text = msg.text() 109 | room: Optional[Room] = msg.room() 110 | if text == 'ding': 111 | conversation: Union[ 112 | Room, Contact] = from_contact if room is None else room 113 | await conversation.ready() 114 | await conversation.say('dong') 115 | file_box = FileBox.from_url( 116 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 117 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 118 | name='ding-dong.jpg') 119 | await conversation.say(file_box) 120 | 121 | asyncio.run(MyBot().start()) 122 | ``` 123 | 124 | 欢迎各位品尝以上代码 🥳 125 | -------------------------------------------------------------------------------- /docs/references/contact-self.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ContactSelf 3 | --- 4 | 5 | > 机器人本身被封装为一个`ContactSelf`类。 这个类继承自联系人`Contact`类。 6 | 7 | ::: wechaty.user.contact_self.ContactSelf.avatar 8 | 9 | ### 示例代码 10 | > 获取机器人账号的头像, 返回`FileBox`类型的对象 11 | 12 | ```python 13 | # 保存头像到本地文件, 类似 `1-name.jpg`的格式 14 | import asyncio 15 | from wechaty import Wechaty, FileBox, Contact 16 | 17 | class MyBot(Wechaty): 18 | 19 | async def on_login(self, contact: Contact) -> None: 20 | print(f"用户{contact}登入") 21 | file: FileBox = await contact.avatar() 22 | name = file.name 23 | await file.to_file(name, True) 24 | print(f"保存头像: {contact.name} 和头像文件: {name}") 25 | 26 | 27 | asyncio.run(MyBot().start()) 28 | ``` 29 | > 设置机器人账号的头像 30 | 31 | ```python 32 | import asyncio 33 | from wechaty import Wechaty, FileBox, Contact 34 | 35 | class MyBot(Wechaty): 36 | 37 | async def on_login(self, contact: Contact) -> None: 38 | print(f"用户{contact}登入") 39 | file_box: FileBox = FileBox.from_url('https://wechaty.github.io/wechaty/images/bot-qr-code.png') 40 | await contact.avatar(file_box) 41 | print(f"更改账号头像成功") 42 | 43 | 44 | asyncio.run(MyBot().start()) 45 | ``` 46 | 47 | 48 | 49 | ::: wechaty.user.contact_self.ContactSelf.qr_code 50 | ### 示例代码 51 | > 获取机器人账号的二维码链接 52 | 53 | ```python 54 | import asyncio 55 | from wechaty import Wechaty 56 | from wechaty.user import ContactSelf 57 | from wechaty.utils.qrcode_terminal import qr_terminal_str 58 | 59 | class MyBot(Wechaty): 60 | 61 | async def on_login(self, contact: ContactSelf) -> None: 62 | print(f"用户{contact}登入") 63 | qr_code = await contact.qr_code() # 获取二维码信息 64 | print(qr_terminal_str(qr_code)) # 在控制台打印二维码 65 | 66 | asyncio.run(MyBot().start()) 67 | ``` 68 | 69 | ::: wechaty.user.contact_self.ContactSelf.name 70 | ### 示例代码 71 | > 获取机器人的名字 72 | 73 | ```python 74 | import sys 75 | import asyncio 76 | from datetime import datetime 77 | from wechaty import Wechaty 78 | from wechaty.user import ContactSelf 79 | 80 | 81 | class MyBot(Wechaty): 82 | 83 | async def on_login(self, contact: ContactSelf) -> None: 84 | old_name = contact.name # 获取Bot账号的名字 85 | print(old_name) 86 | 87 | asyncio.run(MyBot().start()) 88 | ``` 89 | 90 | ::: wechaty.user.contact_self.ContactSelf.set_name 91 | ### 示例代码 92 | > 更改机器人的名字 93 | 94 | ```python 95 | import sys 96 | import asyncio 97 | from datetime import datetime 98 | from wechaty import Wechaty 99 | from wechaty.user import ContactSelf 100 | 101 | 102 | class MyBot(Wechaty): 103 | 104 | async def on_login(self, contact: ContactSelf) -> None: 105 | old_name = contact.name # 获取Bot账号的名字 106 | contact.set_name(f"{old_name}{datetime.now()}") # 更改Bot账号的名字 107 | 108 | asyncio.run(MyBot().start()) 109 | ``` 110 | 111 | ::: wechaty.user.contact_self.ContactSelf.signature 112 | ### 示例代码 113 | > 更改机器人账号的签名 114 | 115 | ```python 116 | import sys 117 | import asyncio 118 | from datetime import datetime 119 | from wechaty import Wechaty 120 | from wechaty.user import ContactSelf 121 | 122 | class MyBot(Wechaty): 123 | 124 | async def on_login(self, contact: ContactSelf) -> None: 125 | print(f"用户{contact}登入") 126 | try: 127 | await contact.signature(f"签名被Wechaty更改于{datetime.now()}") 128 | except Exception as e: 129 | print("更改签名失败", e, file=sys.stderr) 130 | 131 | asyncio.run(MyBot().start()) 132 | ``` -------------------------------------------------------------------------------- /docs/references/contact.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contact 3 | --- 4 | 5 | > 所有的微信联系人(朋友)都会被封装成一个`Contact`联系人对象。 6 | 7 | ::: wechaty.user.contact.Contact.__init__ 8 | 9 | ::: wechaty.user.contact.Contact.get_id 10 | 11 | ::: wechaty.user.contact.Contact.load 12 | 13 | ::: wechaty.user.contact.Contact.find 14 | ### 示例代码 15 | ```python 16 | import asyncio 17 | from wechaty import Wechaty, Contact 18 | from wechaty_puppet import ContactQueryFilter 19 | 20 | class MyBot(Wechaty): 21 | 22 | async def on_login(self, contact: Contact) -> None: 23 | contact = await self.Contact.find(ContactQueryFilter(name="lijiarui")) 24 | contact = await self.Contact.find(ContactQueryFilter(alias="ruirui")) 25 | 26 | asyncio.run(MyBot().start()) 27 | ``` 28 | 29 | ::: wechaty.user.contact.Contact.find_all 30 | ### 示例代码 31 | ```python 32 | import asyncio 33 | from wechaty import Wechaty, Contact 34 | from wechaty_puppet import ContactQueryFilter 35 | 36 | class MyBot(Wechaty): 37 | 38 | async def on_login(self, contact: Contact) -> None: 39 | contact = await self.Contact.find_all() # 获取一个列表, 里面包含了Bot所有的联系人 40 | contact = await self.Contact.find_all(ContactQueryFilter(name="lijiarui")) # 获取一个包含所有名字为lijiarui的联系人的列表 41 | contact = await self.Contact.find_all(ContactQueryFilter(alias="ruirui")) # 获取一个包含所有别名(备注)为ruirui的联系人的列表 42 | 43 | asyncio.run(MyBot().start()) 44 | ``` 45 | 46 | 47 | ::: wechaty.user.contact.Contact.ready 48 | 49 | 50 | ::: wechaty.user.contact.Contact.say 51 | ### 示例代码 52 | ```python 53 | import asyncio 54 | from wechaty import Wechaty, Contact, FileBox, UrlLink 55 | from wechaty_puppet import ContactQueryFilter 56 | 57 | 58 | class MyBot(Wechaty): 59 | 60 | async def on_login(self, contact: Contact) -> None: 61 | contact = await self.Contact.find( 62 | ContactQueryFilter(name="lijiarui")) # 把`lijiarui`更改为您在微信中的任意联系人的姓名 63 | 64 | # 1. 发送文字到联系人 65 | await contact.say('welcome to wechaty!') 66 | 67 | # 2. 发送媒体文件到联系人 68 | fileBox1 = FileBox.from_url('https://wechaty.github.io/wechaty/images/bot-qr-code.png', "bot-qr-code.png") 69 | fileBox2 = FileBox.from_file('text.txt', "text.txt") 70 | await contact.say(fileBox1) 71 | await contact.say(fileBox2) 72 | 73 | # 3. 发送名片到联系人 74 | contactCard = self.Contact.load('lijiarui') # 把`lijiarui`更改为您在微信中的任意联系人的姓名 75 | await contact.say(contactCard) 76 | 77 | # 4. 发送链接到联系人 78 | 79 | urlLink = UrlLink.create( 80 | description='WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', 81 | thumbnail_url='https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', 82 | title='Welcome to Wechaty', 83 | url='https://github.com/wechaty/wechaty', 84 | ) 85 | await contact.say(urlLink) 86 | 87 | # 5. 发送小程序 (暂时只有`wechaty-puppet-macpro`支持该服务) 88 | 89 | miniProgram = self.MiniProgram.create_from_json({ 90 | "appid": 'gh_0aa444a25adc', 91 | "title": '我正在使用Authing认证身份,你也来试试吧', 92 | "pagePath": 'routes/explore.html', 93 | "description": '身份管家', 94 | "thumbUrl": '30590201000452305002010002041092541302033d0af802040b30feb602045df0c2c5042b777875706c6f61645f31373533353339353230344063686174726f6f6d3131355f313537363035393538390204010400030201000400', 95 | "thumbKey": '42f8609e62817ae45cf7d8fefb532e83', 96 | }) 97 | 98 | await contact.say(miniProgram) 99 | 100 | asyncio.run(MyBot().start()) 101 | ``` 102 | 103 | ::: wechaty.user.contact.Contact.name 104 | 105 | ::: wechaty.user.contact.Contact.alias 106 | ### 示例代码 107 | ```python 108 | alias = await contact.alias() 109 | if alias is None or alias == "": 110 | print('您还没有为联系人设置任何别名' + contact.name) 111 | else: 112 | print('您已经为联系人设置了别名 ' + contact.name + ':' + alias) 113 | 114 | ``` 115 | 116 | ::: wechaty.user.contact.Contact.is_friend 117 | 118 | ::: wechaty.user.contact.Contact.is_offical 119 | 120 | ::: wechaty.user.contact.Contact.is_personal 121 | 122 | ::: wechaty.user.contact.Contact.type 123 | ### 示例代码 124 | > 注意: ContactType是个枚举类型. 125 | 126 | ```python 127 | import asyncio 128 | from wechaty import Wechaty, Message, ContactType 129 | 130 | class MyBot(Wechaty): 131 | 132 | async def on_message(self, msg: Message) -> None: 133 | contact = msg.talker() 134 | print(contact.type() == ContactType.CONTACT_TYPE_OFFICIAL) 135 | 136 | asyncio.run(MyBot().start()) 137 | ``` 138 | 139 | ::: wechaty.user.contact.Contact.star 140 | 141 | ::: wechaty.user.contact.Contact.gender 142 | ### 示例代码 143 | > 注意: ContactGender是个枚举类型. 144 | 145 | ```python 146 | import asyncio 147 | from wechaty import Wechaty, Message, ContactGender 148 | 149 | 150 | class MyBot(Wechaty): 151 | 152 | async def on_message(self, msg: Message) -> None: 153 | contact = msg.talker() 154 | # 判断联系人是否为男性 155 | print(contact.gender() == ContactGender.CONTACT_GENDER_MALE) 156 | 157 | asyncio.run(MyBot().start()) 158 | ``` 159 | 160 | 161 | ::: wechaty.user.contact.Contact.city 162 | 163 | ::: wechaty.user.contact.Contact.avatar 164 | ### 示例代码 165 | ```python 166 | # 以类似 `1-name.jpg`的格式保存头像图片到本地 167 | import asyncio 168 | from wechaty import Wechaty, Message, FileBox 169 | 170 | class MyBot(Wechaty): 171 | 172 | async def on_message(self, msg: Message) -> None: 173 | contact = msg.talker() 174 | avatar: "FileBox" = await contact.avatar() 175 | name = avatar.name 176 | await avatar.to_file(name, True) 177 | print(f"联系人: {contact.name} 和头像: {name}") 178 | 179 | asyncio.run(MyBot().start()) 180 | ``` 181 | 182 | ::: wechaty.user.contact.Contact.tags 183 | 184 | ::: wechaty.user.contact.Contact.is_self 185 | 186 | ::: wechaty.user.contact.Contact.weixin 187 | 188 | ## Typedefs 类型定义 189 | 190 | ### [ContactQueryFilter](contact.md#ContactQueryFilter) 191 | 192 | 用于搜索联系人对象的一个封装结构 193 | 194 | | **属性名** | 类型 | **描述** | 195 | | ---------- | -------- | ------------------------------------------------------------ | 196 | | name | `str` | 由用户本身(user-self)设置的名字, 叫做name | 197 | | alias | `str` | 由Bot为联系人设置的名字(备注/别名). 该值可以传入正则表达式用于搜索用户, 更多细节详见[issues#365](https://github.com/wechaty/wechaty/issues/365)和[源码](https://github.com/wechaty/python-wechaty-puppet/blob/master/src/wechaty_puppet/schemas/contact.py) | 198 | 199 | -------------------------------------------------------------------------------- /docs/references/filebox.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/python-wechaty/e9a04a98a3b01f287760e2d2a4514e4a80ecd15f/docs/references/filebox.md -------------------------------------------------------------------------------- /docs/references/friendship.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Friendship 3 | --- 4 | 5 | # class Friendship() 6 | > 发送、接收好友请求和好友确认事件。 7 | > [示例/Friend-Bot](https://github.com/wechaty/python-wechaty-getting-started/blob/master/examples/advanced/friendship-bot.py) 8 | 9 | 10 | ::: wechaty.user.friendship.Friendship.search 11 | 12 | ::: wechaty.user.friendship.Friendship.add 13 | ### 示例代码 14 | ```python 15 | memberList = await room.memberList() 16 | for member in memberList: 17 | await bot.Friendship.add(member, 'Nice to meet you! I am wechaty bot!') 18 | ``` 19 | 20 | ::: wechaty.user.friendship.Friendship.contact 21 | ### 示例代码 22 | ```python 23 | import asyncio 24 | from wechaty import Wechaty, Friendship 25 | 26 | 27 | class MyBot(Wechaty): 28 | 29 | async on_friendship(self, friendship: Friendship) -> None: 30 | contact = friendship.contact() 31 | await contact.ready() 32 | log_msg = f'receive "friendship" message from {contact.name}' 33 | print(log_msg) 34 | 35 | 36 | asyncio.run(MyBot().start()) 37 | ``` 38 | 39 | ::: wechaty.user.friendship.Friendship.accept 40 | ### 示例代码 41 | ```python 42 | import asyncio 43 | from wechaty import Wechaty, Friendship 44 | 45 | class MyBot(Wechaty): 46 | 47 | async on_friendship(self, friendship: Friendship) -> None: 48 | contact = friendship.contact() 49 | await contact.ready() 50 | 51 | if friendship.type() == FriendshipType.FRIENDSHIP_TYPE_RECEIVE: 52 | log_msg = 'accepted automatically' 53 | await friendship.accept() 54 | # if want to send msg, you need to delay sometimes 55 | 56 | print('waiting to send message ...') 57 | await asyncio.sleep(3) 58 | await contact.say('hello from wechaty ...') 59 | print('after accept ...') 60 | elif friendship.type() == FriendshipType.FRIENDSHIP_TYPE_CONFIRM: 61 | log_msg = 'friend ship confirmed with ' + contact.name 62 | 63 | print(log_msg) 64 | 65 | asyncio.run(MyBot().start()) 66 | ``` 67 | 68 | ::: wechaty.user.friendship.Friendship.hello 69 | ### 示例代码 70 | > 自动接受好友请求中包含消息为 `ding` 的好友请求 71 | 72 | ```python 73 | import asyncio 74 | from wechaty import Wechaty, Friendship 75 | 76 | 77 | class MyBot(Wechaty): 78 | 79 | async on_friendship(self, friendship: Friendship) -> None: 80 | contact = friendship.contact() 81 | await contact.ready() 82 | 83 | if friendship.type() == FriendshipType.FRIENDSHIP_TYPE_RECEIVE and friendship.hello() == 'ding': 84 | log_msg = 'accepted automatically because verify messsage is "ding"' 85 | await friendship.accept() 86 | # if want to send msg, you need to delay sometimes 87 | 88 | print('waiting to send message ...') 89 | await asyncio.sleep(3) 90 | await contact.say('hello from wechaty ...') 91 | print('after accept ...') 92 | 93 | asyncio.run(MyBot().start()) 94 | ``` 95 | 96 | ::: wechaty.user.friendship.Friendship.type 97 | 98 | ::: wechaty.user.friendship.Friendship.from_json -------------------------------------------------------------------------------- /docs/references/index.md: -------------------------------------------------------------------------------- 1 | 2 | python-wechaty的接口非常人性化和轻量级,通过不同模块不同接口来完成定制化的功能。在这个章节中将会为你详细介绍不同模块下的接口细节。 3 | 4 | ## 模块 5 | 6 | python-wechaty中所有模块都可直接从top-level来导入,例如: 7 | 8 | ```python 9 | from wechaty import Wechaty, Message 10 | ``` 11 | 12 | ### Wechaty 类 13 | 14 | - [Class Wechaty](./wechaty) 15 | 16 | ```python 17 | from wechaty import Wechaty 18 | ``` 19 | 20 | 当开发者想要编写聊天机器人时,可通过面向函数式编程和面向对象编程两种模式: 21 | 22 | * 面向函数编程 23 | 24 | ```python 25 | import os, asyncio 26 | 27 | from wechaty import Message, Wechaty 28 | 29 | async def on_message(msg: Message): 30 | if msg.text() == 'ding': 31 | await msg.say('dong') 32 | 33 | async def main(): 34 | bot = Wechaty() 35 | bot.on('message', on_message) 36 | 37 | await bot.start() 38 | print('[Python Wechaty] Ding Dong Bot started.') 39 | 40 | asyncio.run(main()) 41 | ``` 42 | 43 | * 面向对象编程 44 | 45 | ```python 46 | import asyncio 47 | 48 | from wechaty import ( 49 | Wechaty, Contact, Message 50 | ) 51 | 52 | class MyBot(Wechaty): 53 | async def on_message(self, msg: Message): 54 | if msg.text() == 'ding': 55 | await msg.say('dong') 56 | 57 | asyncio.run(MyBot().start()) 58 | ``` 59 | 60 | 以上两种方式中,各有优劣,可是我推荐使用面向对象编程,这个在封装性和代码提示的角度上都对开发者比较友好。 61 | 62 | ### 用户相关模块 63 | 64 | 当开发者想要搜索联系人,主动给某个联系人发送消息时,此时需要主动加载联系人对象,然后发送消息。模块类型有: 65 | 66 | > 推荐:所有系统初始化相关的任务都需要在ready事件触发之后执行。 67 | 68 | - [Class Message](./message) 69 | - [Class Contact](./contact) 70 | - [Class ContactSelf](./contact-self) 71 | - [Class Room](./room) 72 | - [Class RoomInvitation](./room-invitation) 73 | - [Class Friendship](./friendship) 74 | 75 | ⚠️ 注意:在python-wechaty中加载以上模块的方式: 76 | 77 | * 面向函数式编程 78 | 79 | ```python 80 | # bot:机器人实例对象,函数内可访问的对象,推荐使用单例模式来构建 81 | contacts: List[Contact] = bot.Contact.find_all() 82 | ``` 83 | 84 | * 面向对象编程 85 | 86 | ```python 87 | async def on_ready(self, payload): 88 | # self: 机器人实例对象,而且还有良好的代码自动提示的功能 89 | contacts: List[Contact] = self.Contact.find_all() 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/references/room-invitation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: RoomInvitation 3 | --- 4 | 5 | > 对群聊邀请事件的封装 6 | 7 | ::: wechaty.user.room_invitation.RoomInvitation.load 8 | 9 | ::: wechaty.user.room_invitation.RoomInvitation.accept 10 | ### 示例代码 11 | ```python 12 | import asyncio 13 | from wechaty import Wechaty, RoomInvitation 14 | 15 | 16 | class MyBot(Wechaty): 17 | 18 | async def on_room_invite(self, room_invitation: RoomInvitation) -> None: 19 | try: 20 | print("收到群聊邀请事件") 21 | await room_invitation.accept() 22 | print("已经自动接受") 23 | except Exception as e: 24 | print(e) 25 | 26 | asyncio.run(MyBot().start()) 27 | ``` 28 | 29 | ::: wechaty.user.room_invitation.RoomInvitation.inviter 30 | ### 示例代码 31 | ```python 32 | import asyncio 33 | from wechaty import Wechaty, RoomInvitation 34 | 35 | 36 | class MyBot(Wechaty): 37 | 38 | async def on_room_invite(self, room_invitation: RoomInvitation) -> None: 39 | try: 40 | print("收到群聊邀请事件") 41 | inviter = await room_invitation.inviter() 42 | inviter_name = inviter.name 43 | print(f"收到来自{inviter_name}的群聊邀请") 44 | except Exception as e: 45 | print(e) 46 | 47 | asyncio.run(MyBot().start()) 48 | ``` 49 | 50 | ::: wechaty.user.room_invitation.RoomInvitation.topic 51 | ### 示例代码 52 | ```python 53 | import asyncio 54 | from wechaty import Wechaty, RoomInvitation 55 | 56 | 57 | class MyBot(Wechaty): 58 | 59 | async def on_room_invite(self, room_invitation: RoomInvitation) -> None: 60 | try: 61 | room_name = await room_invitation.topic() 62 | print(f"收到来自{room_name}的群聊邀请") 63 | except Exception as e: 64 | print(e) 65 | 66 | asyncio.run(MyBot().start()) 67 | ``` 68 | 69 | ::: wechaty.user.room_invitation.RoomInvitation.member_count 70 | 71 | ::: wechaty.user.room_invitation.RoomInvitation.member_list 72 | 73 | ::: wechaty.user.room_invitation.RoomInvitation.date 74 | 75 | ::: wechaty.user.room_invitation.RoomInvitation.age 76 | > 举个例子, 有条群聊邀请是`8:43:01`发送的, 而当我们在Wechaty中接收到它的时候时间已经为 `8:43:15`, 那么这时 `age()`返回的值为 `8:43:15 - 8:43:01 = 14 (秒)` 77 | 78 | ::: wechaty.user.room_invitation.RoomInvitation.from_json 79 | 80 | ::: wechaty.user.room_invitation.RoomInvitation.to_json -------------------------------------------------------------------------------- /docs/references/wechaty.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Wechaty 3 | --- 4 | 5 | > `Wechaty`类用来实例化机器人对象,控制机器人的整体逻辑,如:启动、注册监听事件、登录、注销等功能。 6 | ------ 7 | > 一个机器人就是`Wechaty`实例,所有用户相关模块都应该通过实例来获取,这样能保证服务连接的一致性,此外所有的逻辑应该以插件和事件订阅的形式组织,保证不同业务之间的隔离性以及业务内的内聚性。 8 | 9 | ::: wechaty.Wechaty.__init__ 10 | ### 示例代码 11 | (世界上最短的Python ChatBot:9行代码) 12 | ```python 13 | from wechaty import Wechaty 14 | import asyncio 15 | async def main(): 16 | bot = Wechaty() 17 | bot.on('scan', lambda status, qrcode, data: print('Scan QR Code to login: {}\nhttps://wechaty.js.org/qrcode/{}'.format(status, qrcode))) 18 | bot.on('login', lambda user: print('User {} logged in'.format(user))) 19 | bot.on('message', lambda message: print('Message: {}'.format(message))) 20 | await bot.start() 21 | asyncio.run(main()) 22 | ``` 23 | ### WechatyOptions 24 | 25 | 创建一个wechaty实例的可选参数 26 | 27 | **Kind**: global typedef 28 | **Properties** 29 | 30 | | Name | Type | Description | 31 | | :--- | :--- | :--- | 32 | | profile | `string` | Wechaty Name. When you set this: `new Wechaty({profile: 'wechatyName'})` it will generate a file called `wechatyName.memory-card.json`. This file stores the bot's login information. If the file is valid, the bot can auto login so you don't need to scan the qrcode to login again. Also, you can set the environment variable for `WECHATY_PROFILE` to set this value when you start. eg: `WECHATY_PROFILE="your-cute-bot-name" node bot.js`. This field is deprecated, please use `name` instead. [see more](https://github.com/wechaty/wechaty/issues/2049) | 33 | | puppet | `PuppetModuleName` \| `Puppet` | Puppet name or instance | 34 | | puppetOptions | `Partial.` | Puppet TOKEN | 35 | | ioToken | `string` | Io TOKEN | 36 | 37 | ::: wechaty.Wechaty.instance 38 | 39 | ::: wechaty.Wechaty.use 40 | 41 | ::: wechaty.Wechaty.on 42 | ### WechatyEventName 43 | 44 | Wechaty类的事件类型 45 | 46 | **Kind**: global typedef 47 | **Properties** 48 | 49 | | Name | Type | Description | 50 | | :--- | :--- | :--- | 51 | | error | `string` | When the bot get error, there will be a Wechaty error event fired. | 52 | | login | `string` | After the bot login full successful, the event login will be emitted, with a Contact of current logined user. | 53 | | logout | `string` | Logout will be emitted when bot detected log out, with a Contact of the current login user. | 54 | | heartbeat | `string` | Get bot's heartbeat. | 55 | | friendship | `string` | When someone sends you a friend request, there will be a Wechaty friendship event fired. | 56 | | message | `string` | Emit when there's a new message. | 57 | | ready | `string` | Emit when all data has load completed, in wechaty-puppet-padchat, it means it has sync Contact and Room completed | 58 | | room-join | `string` | Emit when anyone join any room. | 59 | | room-topic | `string` | Get topic event, emitted when someone change room topic. | 60 | | room-leave | `string` | Emit when anyone leave the room. | 61 | | room-invite | `string` | Emit when there is a room invitation, see more in [RoomInvitation](room-invitation.md) If someone leaves the room by themselves, wechat will not notice other people in the room, so the bot will never get the "leave" event. | 62 | | scan | `string` | A scan event will be emitted when the bot needs to show you a QR Code for scanning. </br> It is recommend to install qrcode-terminal\(run `npm install qrcode-terminal`\) in order to show qrcode in the terminal. | 63 | 64 | ### WechatyEventFunction 65 | 66 | Wechaty类的事件所绑定的相关函数 67 | 68 | **Kind**: global typedef 69 | **Properties** 70 | 71 | | Name | Type | Description | 72 | | :--- | :--- | :--- | 73 | | error | `function` | \(this: Wechaty, error: Error\) => void callback function | 74 | | login | `function` | \(this: Wechaty, user: ContactSelf\)=> void | 75 | | logout | `function` | \(this: Wechaty, user: ContactSelf\) => void | 76 | | scan | `function` | \(this: Wechaty, url: string, code: number\) => void | 77 | | heartbeat | `function` | \(this: Wechaty, data: any\) => void | 78 | | friendship | `function` | \(this: Wechaty, friendship: Friendship\) => void | 79 | | message | `function` | \(this: Wechaty, message: Message\) => void | 80 | | ready | `function` | \(this: Wechaty\) => void | 81 | | room-join | `function` | \(this: Wechaty, room: Room, inviteeList: Contact\[\], inviter: Contact\) => void | 82 | | room-topic | `function` | \(this: Wechaty, room: Room, newTopic: string, oldTopic: string, changer: Contact\) => void | 83 | | room-leave | `function` | \(this: Wechaty, room: Room, leaverList: Contact\[\]\) => void | 84 | | room-invite | `function` | \(this: Wechaty, room: Room, leaverList: Contact\[\]\) => void see more in [RoomInvitation](room-invitation.md) | 85 | 86 | ::: wechaty.Wechaty.start 87 | ### 示例代码 88 | ```python 89 | from wechaty import Wechaty 90 | import asyncio 91 | 92 | async def main(): 93 | bot = Wechaty() 94 | await bot.start() 95 | 96 | asyncio.run(main()) 97 | ``` 98 | 99 | ::: wechaty.Wechaty.restart 100 | 101 | ::: wechaty.Wechaty.stop 102 | 103 | ::: wechaty.Wechaty.user_self 104 | 105 | ::: wechaty.Wechaty.self -------------------------------------------------------------------------------- /docs/tutorials/getting-started.md: -------------------------------------------------------------------------------- 1 | ## 快速开始 2 | 3 | 1、 安装 4 | 5 | ```shell 6 | pip install --upgrade wechaty 7 | ``` 8 | 9 | 2、 设置TOKEN 10 | 11 | ```shell 12 | export token=your_token_at_here 13 | # or 14 | export WECHATY_PUPPET_SERVICE_TOKEN=your_token_at_here 15 | ``` 16 | 17 | 或者通过代码来设置环境变量: 18 | 19 | ```python 20 | import os 21 | os.environ['token'] = 'your_token_at_here' 22 | # or 23 | os.environ['WECHATY_PUPPET_SERVICE_TOKEN'] = 'your_token_at_here' 24 | ``` 25 | 26 | 3、 聊天机器人 27 | 28 | ```python 29 | import asyncio 30 | 31 | from wechaty import Wechaty 32 | 33 | class MyBot(Wechaty): 34 | async def on_message(self, msg: Message): 35 | from_contact = msg.talker() 36 | text = msg.text() 37 | room = msg.room() 38 | if text == 'ding': 39 | conversation: Union[ 40 | Room, Contact] = from_contact if room is None else room 41 | await conversation.ready() 42 | await conversation.say('dong') 43 | file_box = FileBox.from_url( 44 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 45 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 46 | name='ding-dong.jpg') 47 | await conversation.say(file_box) 48 | 49 | asyncio.run(MyBot().start()) 50 | ``` 51 | 52 | 以上代码展示了基于python-wechaty如何开发聊天机器人的整体步骤:安装、设置TOKEN环境变量以及编写聊天机器人。示例机器人代码可查看:[ding-dong-bot-oop.py](https://github.com/wechaty/python-wechaty-getting-started/blob/master/examples/basic/ding-dong-bot-oop.py) 53 | 54 | ## 快速上手 55 | 56 | - [使用padlocal协议](./use_padlocal_getting_started.md) 57 | - [使用web协议](./use_web_getting_started.md) 58 | - [使用Paimon协议](./use_paimon_getting_started.md) 59 | -------------------------------------------------------------------------------- /docs/tutorials/index.md: -------------------------------------------------------------------------------- 1 | ## 快速开始一个简单的机器人应用 2 | - 在本节内容中 将手把手快速指导你开始运行一个消息机器人。 3 | 4 | ### 运行一个微信机器人 5 | 运行微信机器人有多种协议可以使用,有以下三种不同的协议方式, 每个协议都有不用的特点,如果你不知道选什么,建议使用Padlocal协议来运行你的第一个微信机器人。 6 | 7 | - [使用Padlocal协议(推荐使用)](./use_padlocal_getting_started.md) 8 | - 使用Paimon协议 9 | - 使用免费Web协议(目前已不可用) 10 | - 使用puppet-xp协议(仅windows下可用) 11 | 12 | ### 运行一个微信公众号机器人 13 | - TODO 14 | 15 | ### 运行一个钉钉机器人 16 | - TODO 17 | 18 | ### 运行一个Whatsapp机器人 19 | - TODO 20 | 21 | ### 运行一个Gitter机器人 22 | - TODO -------------------------------------------------------------------------------- /docs/tutorials/use_padlocal_getting_started.md: -------------------------------------------------------------------------------- 1 | # 使用Padlocal协议启动微信机器人 2 | 3 | 底层的对接实现是基于TypeScript语言,故无法直接在python-wechaty中使用该服务。可是Wechaty社区能够直接将其转化成对应的服务让多语言调用,从而实现:底层复用的特性。 4 | 5 | 整体步骤分为两步: 6 | 7 | * 申请一个TOKEN 8 | * 部署模板机器人 9 | 10 | 11 | ## 一、申请一个TOKEN 12 | - 可以通过手机号注册来获得一个7天免费的TOKEN:[申请地址](http://pad-local.com) 13 | - [TOKEN 说明](https://wechaty.js.org/docs/puppet-services/) 14 | - 那如何获取长期Token呢?详细请看:[Everything-about-Wechaty](https://github.com/juzibot/Welcome/wiki/Everything-about-Wech aty) 15 | 16 | 17 | 18 | ## 二、 部署模板机器人 19 | 20 | - 现在的你已经拥有了一个Padlocal Token. 接下来则需要使用它启动我们的第一个机器人了。 21 | - 在这里,我们直接使用[python-wechaty-template](https://github.com/wechaty/python-wechaty-template)项目来快速的部署我们的第一个机器人。 22 | - **在此之前请确保你的机器上已经安装并运行了[docker](https://www.docker.com/get-started)服务** 23 | 24 | ### 1. clone 模板机器人项目 25 | 26 | ```she 27 | git clone https://github.com/wechaty/python-wechaty-template && cd python-wechaty-template 28 | ``` 29 | 30 | ### 2. 启动padlocal网关 31 | 32 | - 参数为申请的token,下面示例中token不可用,请换上你申请的token 33 | 34 | ```shel 35 | ./start_gateway_docker.sh puppet_padlocal_d764cc8d231747b18b9ee5540dadd55c 36 | ``` 37 | 38 | ### 3. 启动机器人 39 | 40 | - 上一步会启动一个docker,并运行Gateway服务,会有持续输出,建议运行在tmux中。 41 | - 开起一个新的terminal,并在模板机器人项目路径下运行以下命令 42 | 43 | ```shell 44 | make bot 45 | ``` 46 | 47 | - 初次登陆时,可能需要扫码多次。 48 | 49 | ### 4. 验证机器人启动成功 50 | 51 | - 机器人发送消息`ding`来测试,如果启动成功,则机器人自动回复`dong`,至此你的第一个微信机器人就启动成功了。 52 | 53 | 54 | 55 | 56 | 57 | ##  三、自定义配置 58 | 59 | 60 | 61 | ### 3.1 使用docker启动的padlocal网关参数说明 62 | 63 | - 这些参数在模板机器人项目中的`start_gateway_docker.sh`脚本中配置 64 | 65 | * **WECHATY_PUPPET**: **标识**使用的哪个协议,一般和`token`类型的一一对应。比如当使用`padlocal`协议的话,那这个就是`wechaty-puppet-padlocal`,如果使用`web`协议的话,那这个就是`wechaty-puppet-wechat`。 66 | * **WECHATY_PUPPET_PADLOCAL_TOKEN**: 这个协议是用来连接Padlocal的服务,目前是付费的。也就是在第一步中申请的。 67 | * **WECHATY_PUPPET_SERVER_PORT**: 网关服务的接口,提供给`python-wechaty`来连接调用,如果服务部署在云服务器上,则需要保证该端口的可访问性。(默认8080) 68 | * **WECHATY_TOKEN**: 当开发者在自己机器上启动一个网关服务时,需要通过`TOEKN`来做身份验证,避免服务被他人窃取。脚本中使用uuid自动生成。具体值可在`.env`文件中查看 69 | 70 | 网关服务启动成功之后,只需要编写`python-wechaty`的代码来连接即可。 71 | 72 | 73 | 74 | ## 四、使用python-wechaty连接远程网关服务 75 | 76 | ### 4.1 本地测试和远端部署 77 | 78 | 当启动网关服务时,`Padlocal`会根据`WECHATY_TOKEN`来在[Wechaty服务接口](https://api.chatie.io/v0/hosties/__TOKEN__)上注册部署机器的`IP`和`端口`,然后python-wechaty会根据`WECHATY_TOKEN`在[Wechaty服务接口](https://api.chatie.io/v0/hosties/__TOKEN__)上获取对应的IP和端口。 79 | 80 | 可是很多小伙伴在实际开发的过程中,通常会出现`endpoint is not invalid`等错误信息,那是因为开发者有可能在本地启动网关服务或者服务器端口没有开放。 81 | 82 | 网关服务的部署通常是分为本地测试和远端部署,前者通常只是为了初学测试,后者是为了生产部署。如果是在生产部署时,只需要将模板机器人项目下的.env文件内容在本地保持一致即可。即使用gateway启动时生成的token即可在本地访问远程网关服务。 83 | 84 | ### 4.2 TOKEN的作用 85 | 86 | 总而言之: 87 | 88 | * 如果是公网环境下,可只需要设置`TOKEN`即可(因为你的token已经注册在chatie server上,故可以获取到目标资源服务器的ip和port) 89 | * 如果是内网环境下,可只需要使用`ENDPOINT`(`localhost:port`)来让python-wechaty连接目标资源服务器。 90 | 91 | > 如果是token是padlocal类型,则在python-wechaty程序内部可直接设置`export endpoint=localhost:port`来连接Gateway Server。 92 | -------------------------------------------------------------------------------- /docs/tutorials/use_paimon_getting_started.md: -------------------------------------------------------------------------------- 1 | # 使用Paimon协议 2 | 3 | ## 一、介绍 4 | 5 | python原生支持paimon协议,不需要Token Gateway,简单方便。 6 | [免费申请7天试用Token](https://wechaty.js.org/docs/puppet-services/paimon) 7 | 8 | 9 | ## 二、连接服务 10 | 11 | ### 2.1 本地测试和远端部署 12 | 13 | 14 | ```shell 15 | export WECHATY_PUPPET_SERVICE_TOKEN=puppet_paimon_XXXXX 16 | # or 17 | export TOKEN=puppet_paimon_XXXXX 18 | # or 19 | export token=puppet_paimon_XXXXX 20 | ``` 21 | 22 | 23 | 24 | 当然,以上的写法是使用Bash的方式来设置环境变量,也是可以通过python代码来设置环境变量,详细可看: 25 | 26 | ```python 27 | import os 28 | os.environ['token'] = "puppet_paimon_XXXXX" 29 | ``` 30 | 31 | ## 三、示例代码 32 | 33 | > talke is cheep, show you the code 34 | 35 | ```python 36 | import asyncio, os 37 | from typing import List, Optional, Union 38 | 39 | from wechaty_puppet import FileBox # type: ignore 40 | 41 | from wechaty import Wechaty, Contact 42 | from wechaty.user import Message, Room 43 | 44 | 45 | class MyBot(Wechaty): 46 | 47 | async def on_message(self, msg: Message): 48 | """ 49 | listen for message event 50 | """ 51 | from_contact: Optional[Contact] = msg.talker() 52 | text = msg.text() 53 | room: Optional[Room] = msg.room() 54 | if text == 'ding': 55 | conversation: Union[ 56 | Room, Contact] = from_contact if room is None else room 57 | await conversation.ready() 58 | await conversation.say('dong') 59 | file_box = FileBox.from_url( 60 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 61 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 62 | name='ding-dong.jpg') 63 | await conversation.say(file_box) 64 | 65 | os.environ['TOKEN'] = "1fe5f846-3cfb-401d-b20c-XXXXX" 66 | asyncio.run(MyBot().start()) 67 | ``` 68 | 69 | 欢迎各位品尝以上代码 🥳 70 | 71 | * **相关链接** 72 | * [python-wechaty](https://github.com/wechaty/python-wechaty) 73 | * [python-wechaty-getting-started](https://github.com/wechaty/python-wechaty-getting-started) 74 | -------------------------------------------------------------------------------- /docs/tutorials/use_web_getting_started.md: -------------------------------------------------------------------------------- 1 | # 使用免费Web协议 2 | 3 | ## 一、介绍 4 | 5 | 底层的对接实现是基于TypeScript语言,故无法直接在python-wechaty中使用该服务。可是Wechaty社区能够直接将其转化成对应的服务让多语言调用,从而实现:底层复用的特性。 6 | 7 | 整体步骤分为两步: 8 | 9 | * 使用Docker启动web协议服务 10 | * 使用python-wechaty连接服务 11 | 12 | ## 二、启动Web协议服务 13 | 14 | ```shell 15 | docker pull wechaty/wechaty:0.65 16 | 17 | export WECHATY_LOG="verbose" 18 | export WECHATY_PUPPET="wechaty-puppet-wechat" 19 | export WECHATY_PUPPET_SERVER_PORT="8080" 20 | export WECHATY_TOKEN="python-wechaty-{uuid}" 21 | export WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_SERVER="true" 22 | 23 | # save login session 24 | if [ ! -f "${WECHATY_TOKEN}.memory-card.json" ]; then 25 | touch "${WECHATY_TOKEN}.memory-card.json" 26 | fi 27 | 28 | docker run -ti \ 29 | --name wechaty_puppet_service_token_gateway \ 30 | --rm \ 31 | -v "`pwd`/${WECHATY_TOKEN}.memory-card.json":"/wechaty/${WECHATY_TOKEN}.memory-card.json" \ 32 | -e WECHATY_LOG \ 33 | -e WECHATY_PUPPET \ 34 | -e WECHATY_PUPPET_SERVER_PORT \ 35 | -e WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_SERVER \ 36 | -e WECHATY_TOKEN \ 37 | -p "$WECHATY_PUPPET_SERVER_PORT:$WECHATY_PUPPET_SERVER_PORT" \ 38 | wechaty/wechaty:0.65 39 | ``` 40 | 41 | 注意: 42 | 43 | * WECHATY_TOKEN 必须使用生成的UUID来替换,不然直接使用该token来启动的服务很容易被他人盗窃。 44 | 45 | 小伙伴们可在python解释器中运行以下代码来获得随机TOKEN: 46 | ```python 47 | # 例如:b2ff8fc5-c5a2-4384-b317-3695807e483f 48 | import uuid;print(uuid.uuid4()); 49 | ``` 50 | 51 | ## 三、连接服务 52 | 53 | 当使用docker来启动web服务时,可分为在本地环境测试以及在远端环境中测试,在连接方式上有一些不一样。 54 | 55 | ### 3.1 本地WEB服务 56 | 57 | 当在计算机本地启动web服务后,可直接使用python-wechaty连接本地的服务,不通过token来获取对应的服务连接地址。示例代码如下: 58 | 59 | ```shell 60 | export WECHATY_PUPPET_SERVICE_ENDPOINT=127.0.0.1:8080 61 | ``` 62 | 63 | 或者 64 | 65 | ```python 66 | import os 67 | os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '127.0.0.1:8080' 68 | ``` 69 | 70 | > 当你的服务和python-wechaty机器人代码都部署在服务器中时,此时也属于本地服务,可使用此方法来配置。 71 | 72 | ### 3.2 远端服务 73 | 74 | 当把服务部署在远端服务器中时,要保证该计算机能够被外网访问到,且对应端口开放。例如在上述示例脚本中,比如保证服务器的`8080`端口开放,而你的服务器IP为:`10.12.123.23`,此时可直接设置服务连接地址: 75 | 76 | ```shell 77 | export WECHATY_PUPPET_SERVICE_ENDPOINT=10.12.123.23:8080 78 | ``` 79 | 80 | 或者 81 | 82 | ```python 83 | import os 84 | os.environ['WECHATY_PUPPET_SERVICE_ENDPOINT'] = '10.12.123.23:8080' 85 | ``` 86 | 87 | ## 四、编写代码 88 | 89 | > talk is cheep, show you the code 90 | 91 | ```python 92 | import asyncio 93 | from typing import List, Optional, Union 94 | 95 | from wechaty_puppet import FileBox # type: ignore 96 | 97 | from wechaty import Wechaty, Contact 98 | from wechaty.user import Message, Room 99 | 100 | 101 | class MyBot(Wechaty): 102 | 103 | async def on_message(self, msg: Message): 104 | """ 105 | listen for message event 106 | """ 107 | from_contact: Optional[Contact] = msg.talker() 108 | text = msg.text() 109 | room: Optional[Room] = msg.room() 110 | if text == 'ding': 111 | conversation: Union[ 112 | Room, Contact] = from_contact if room is None else room 113 | await conversation.ready() 114 | await conversation.say('dong') 115 | file_box = FileBox.from_url( 116 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 117 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 118 | name='ding-dong.jpg') 119 | await conversation.say(file_box) 120 | 121 | asyncio.run(MyBot().start()) 122 | ``` 123 | 124 | 欢迎各位品尝以上代码 🥳 125 | -------------------------------------------------------------------------------- /docs/tutorials/videos.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 介绍视频 3 | summary: . 4 | authors: 5 | - wj-Mcat 6 | date: 2021-04-25 7 | some_url: https://github.com/wj-Mcat 8 | --- 9 | 10 | ## live-coding 11 | 12 | > 原始视频链接:[@吴京京 python-wechaty-live-coding](https://www.bilibili.com/video/BV1dv411k75G?from=search&seid=13874361539953942172) 13 | 14 | 15 | 16 | ## AI情话 17 | 18 | > 原始视频链接:[@Val_傲娇的鸽子 手把手教你做个用AI续写情话的Wechaty聊天机器人](https://www.bilibili.com/video/BV1BB4y1A714?from=search&seid=13874361539953942172) 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/contact-bot.py: -------------------------------------------------------------------------------- 1 | """doc""" 2 | import asyncio 3 | from typing import Optional 4 | 5 | from wechaty_puppet import ContactType 6 | from wechaty_puppet.logger import get_logger 7 | 8 | from wechaty import Wechaty, Contact 9 | from wechaty.utils.qrcode_terminal import qr_terminal_str 10 | 11 | log = get_logger('ContactBot') 12 | 13 | WELCOME_MSG = ''' 14 | =============== Powered by Wechaty =============== 15 | -------- https://github.com/Chatie/wechaty -------- 16 | Hello, 17 | I'm a Wechaty Botie with the following super powers: 18 | 1. List all your contacts with weixn id & name 19 | 2. Dump the avatars of your first 17 contacts 20 | __________________________________________________ 21 | Hope you like it, and you are very welcome to 22 | upgrade me for more super powers! 23 | Please wait... I'm trying to login in... 24 | ''' 25 | 26 | # pylint: disable=W0602 27 | bot: Optional[Wechaty] = None 28 | MAX_CONTACTS = 17 29 | INTERVAL = 7 30 | 31 | 32 | async def display_contacts() -> None: 33 | """Display all the contacts and dump the avatars""" 34 | # pylint: disable=W0603 35 | global bot 36 | assert bot is not None 37 | while True: 38 | contacts = await bot.Contact.find_all() 39 | 40 | log.info('#######################') 41 | log.info('Contact number: %s\n', len(contacts)) 42 | 43 | for index, contact in enumerate(contacts): 44 | if contact.type() == ContactType.CONTACT_TYPE_PERSONAL: 45 | log.info('personal %s: %s : %s', index, contact.name, contact.get_id()) 46 | 47 | for contact in contacts[:MAX_CONTACTS]: 48 | file = await contact.avatar() 49 | name = file.name 50 | await file.to_file(name, True) 51 | 52 | log.info('Contact: "%s" with avatar file: "%s"', contact.name, name) 53 | 54 | if len(contacts) > MAX_CONTACTS: 55 | log.info('Too many contacts. I only show you the first %s ones...', MAX_CONTACTS) 56 | 57 | log.info('I will re-dump contact weixin id & names after %s second... ', INTERVAL) 58 | await asyncio.sleep(INTERVAL) 59 | 60 | 61 | async def handle_login(user: Contact) -> None: 62 | """Handle the login event""" 63 | log.info('%s logged in', user.name) 64 | await user.say('wechaty contact-bot just logged in') 65 | await display_contacts() 66 | 67 | 68 | async def main() -> None: 69 | """The main function for the contact-bot module""" 70 | # pylint: disable=W0603 71 | global bot 72 | print(WELCOME_MSG) 73 | bot = Wechaty()\ 74 | .on('login', handle_login)\ 75 | .on('error', lambda error: log.info('error: %s', error))\ 76 | .on('scan', 77 | lambda qrcode, status: print(f'{qr_terminal_str(qrcode)}\n' 78 | f'[{status}] Scan QR Code in above url to login:')) 79 | await bot.start() 80 | 81 | 82 | asyncio.run(main()) 83 | -------------------------------------------------------------------------------- /examples/ding-dong-bot.py: -------------------------------------------------------------------------------- 1 | """doc""" 2 | import asyncio 3 | import logging 4 | from typing import Optional, Union 5 | 6 | from wechaty_puppet import FileBox 7 | 8 | from wechaty import Wechaty, Contact 9 | from wechaty.user import Message, Room 10 | 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format='%(asctime)s %(levelname)s %(filename)s <%(funcName)s> %(message)s', 14 | datefmt='%Y-%m-%d %H:%M:%S', 15 | ) 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | 20 | async def message(msg: Message) -> None: 21 | """back on message""" 22 | from_contact = msg.talker() 23 | text = msg.text() 24 | room = msg.room() 25 | if text == 'ding': 26 | conversation: Union[ 27 | Room, Contact] = from_contact if room is None else room 28 | await conversation.ready() 29 | await conversation.say('dong') 30 | file_box = FileBox.from_url( 31 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 32 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 33 | name='ding-dong.jpg') 34 | await conversation.say(file_box) 35 | 36 | bot: Optional[Wechaty] = None 37 | 38 | 39 | async def main() -> None: 40 | """doc""" 41 | # pylint: disable=W0603 42 | global bot 43 | bot = Wechaty().on('message', message) 44 | await bot.start() 45 | 46 | 47 | asyncio.run(main()) 48 | -------------------------------------------------------------------------------- /examples/health_check_plugin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/python-wechaty/e9a04a98a3b01f287760e2d2a4514e4a80ecd15f/examples/health_check_plugin.py -------------------------------------------------------------------------------- /examples/plugin-server-bot.py: -------------------------------------------------------------------------------- 1 | """doc""" 2 | import asyncio 3 | import logging 4 | from typing import Optional, Union 5 | from quart import Quart 6 | 7 | from wechaty_puppet import FileBox, PuppetOptions 8 | 9 | from wechaty import Wechaty, Contact, WechatyPlugin, WechatyOptions 10 | from wechaty.user import Message, Room 11 | 12 | logging.basicConfig( 13 | level=logging.INFO, 14 | format='%(asctime)s %(levelname)s %(filename)s <%(funcName)s> %(message)s', 15 | datefmt='%Y-%m-%d %H:%M:%S', 16 | ) 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | 21 | class SimpleServerWechatyPlugin(WechatyPlugin): 22 | """ 23 | simple hello wechaty web server plugin 24 | """ 25 | async def blueprint(self, app: Quart) -> None: 26 | @app.route('/wechaty') 27 | def hello_wechaty() -> str: 28 | """helo blueprint function""" 29 | return 'hello wechaty' 30 | 31 | 32 | async def message(msg: Message) -> None: 33 | """back on message""" 34 | from_contact = msg.talker() 35 | text = msg.text() 36 | room = msg.room() 37 | if text == '#ding': 38 | conversation: Union[ 39 | Room, Contact] = from_contact if room is None else room 40 | await conversation.ready() 41 | await conversation.say('dong') 42 | file_box = FileBox.from_url( 43 | 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/' 44 | 'u=1116676390,2305043183&fm=26&gp=0.jpg', 45 | name='ding-dong.jpg') 46 | await conversation.say(file_box) 47 | 48 | bot: Optional[Wechaty] = None 49 | 50 | 51 | async def main() -> None: 52 | """doc""" 53 | # pylint: disable=W0603 54 | global bot 55 | options = WechatyOptions( 56 | host='127.0.0.1', 57 | port=5005, 58 | puppet_options=PuppetOptions( 59 | token='your-token' 60 | ) 61 | ) 62 | 63 | bot = Wechaty( 64 | options=options 65 | ).on('message', message) 66 | bot.use(SimpleServerWechatyPlugin()) 67 | 68 | await bot.start() 69 | 70 | 71 | asyncio.run(main()) 72 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: python-wechaty 2 | repo_url: http://github.com/wechaty/python-wechaty 3 | repo_name: GitHub 4 | site_description: 'python-wechaty AI Chatbot' 5 | copyright: '@wechaty community' 6 | nav: 7 | - 介绍: 'index.md' 8 | - 快速开始: 9 | - 介绍: 'tutorials/index.md' 10 | - 使用Padlocal协议快速开始一个微信机器人: 'tutorials/use_padlocal_getting_started.md' 11 | - 使用免费web协议快速开始一个微信机器人: 'tutorials/use_web_getting_started.md' 12 | - 使用paimon协议快速开始一个微信机器人: 'tutorials/use_paimon_getting_started.md' 13 | - 视频教程: 'tutorials/videos.md' 14 | - 使用指南: 15 | - 介绍: 'how-to/how-to_introduction.md' 16 | - 如何添加好友: 'how-to/how-to_add_friendship.md' 17 | - 如何关键字入群: 'how-to/how-to_room_inviter.md' 18 | - 如何完成自动回复: 'how-to/how-to_auto_reply.md' 19 | - 如何检索群聊或联系人: 'how-to/how-to_finder.md' 20 | - 如何完成任务调度: 'how-to/how-to_scheduler.md' 21 | - 如何完成群消息同步: 'how-to/how-to_message_forward.md' 22 | - 如何使用Rasa Sever: 'how-to/how-to_rasa.md' 23 | - 如何使用Github Webhook插件: 'how-to/how-to_github_webhook.md' 24 | - 如何使用Gitlab Webhook插件: 'how-to/how-to_gitlab_webhook.md' 25 | - 如何使用插件系统: 'how-to/how-to_use_plugin.md' 26 | - 模块详解: 27 | - 介绍: 'references/index.md' 28 | - 'Wechaty模块': 'references/wechaty.md' 29 | - '消息模块': 'references/message.md' 30 | - '联系人模块': 'references/contact.md' 31 | - '登录人模块': 'references/contact-self.md' 32 | - '好友关系模块': 'references/friendship.md' 33 | - '群聊模块': 'references/room.md' 34 | - '群聊邀请模块': 'references/room-invitation.md' 35 | - 'filebox模块': 'references/filebox.md' 36 | - API文档: 37 | - 'wechaty.Wechaty': 'api/wechaty.md' 38 | - 'wechaty.accessory': 'api/accessory.md' 39 | - 'wechaty.config': 'api/config.md' 40 | - 'wechaty.types': 'api/types.md' 41 | - 'wechaty.plugin': 'api/plugin.md' 42 | - wechaty.utils: 43 | - 'wechaty.utils.date_util': 'api/utils/date_util.md' 44 | - 'wechaty.utils.async_helper': 'api/utils/async_helper.md' 45 | - 'wechaty.utils.link': 'api/utils/link.md' 46 | - 'wechaty.utils.qr_code': 'api/utils/qr_code.md' 47 | - 'wechaty.utils.qrcode_teminal': 'api/utils/qrcode_terminal.md' 48 | - 'wechaty.utils.type_check': 'api/utils/type_check.md' 49 | - wechaty.user: 50 | - 'wechaty.user.contact': 'api/user/contact.md' 51 | - 'wechaty.user.contact_self': 'api/user/contact_self.md' 52 | - 'wechaty.user.favorite': 'api/user/favorite.md' 53 | - 'wechaty.user.friendship': 'api/user/friendship.md' 54 | - 'wechaty.user.image': 'api/user/image.md' 55 | - 'wechaty.user.message': 'api/user/message.md' 56 | - 'wechaty.user.mini_program': 'api/user/mini_program.md' 57 | - 'wechaty.user.room': 'api/user/room.md' 58 | - 'wechaty.user.room_invitation': 'api/user/room_invitation.md' 59 | - 'wechaty.user.tag': 'api/user/tag.md' 60 | - 'wechaty.user.url_link': 'api/user/url_link.md' 61 | - 设计理念: 62 | - 介绍: 'explanation/index.md' 63 | - 不同协议比较: 'explanation/different_protocol.md' 64 | - 插件系统: 'explanation/why_plugin.md' 65 | - FAQ: 66 | - '基础常见问题': 'faq/common.md' 67 | - '什么是Puppet': 'faq/what-is-a-puppet.md' 68 | 69 | theme: 70 | name: material 71 | logo: img/wechaty-icon-white.svg 72 | favicon: img/favicon.ico 73 | 74 | markdown_extensions: 75 | - pymdownx.highlight 76 | - pymdownx.superfences 77 | - toc: 78 | baselevel: 2 79 | 80 | google_analytics: 81 | - G-1TDFTF2BYD 82 | - auto 83 | 84 | plugins: 85 | - search 86 | - mkdocstrings: 87 | handlers: 88 | python: 89 | selection: 90 | filters: 91 | - "!^_" # exlude all members starting with _ 92 | - "^__init__$" # but always include __init__ modules and methods 93 | rendering: 94 | show_root_heading: yes 95 | show_root_full_path: false 96 | members_order: source 97 | heading_level: 2 98 | watch: 99 | - ./src -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.mypy] 2 | disallow_untyped_defs = true 3 | warn_unused_ignores = true 4 | ignore_missing_imports = false 5 | 6 | [[tool.mypy.overrides]] 7 | module = [ 8 | "qrcode.*", 9 | "wechaty_puppet.*", 10 | "pytest.*", 11 | "grpclib.*", 12 | "lxml.*", 13 | "apscheduler.*", 14 | "pyee.*" 15 | ] 16 | ignore_missing_imports = true 17 | 18 | # refer to: https://docs.pytest.org/en/stable/mark.html 19 | [tool.pytest.ini_options] 20 | minversion = "6.0" 21 | addopts = "--cov-report term-missing --cov-report xml --cov=src/" 22 | testpaths = [ 23 | "tests", 24 | ] 25 | markers = [ 26 | "asyncio" 27 | ] 28 | pythonpath='./src' -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # use the latest version of setuptools 2 | setuptools>=63.2.0 3 | flake8 4 | mypy 5 | mypy_extensions 6 | pycodestyle 7 | pylint 8 | pylint-quotes 9 | pytest 10 | 11 | # TODO(wj-Mcat): find out why latest pytest-asyncio will make test failed 12 | pytest-asyncio==0.18.3 13 | pytest-cov 14 | pytype 15 | semver==3.0.0.dev3 16 | pyee 17 | requests 18 | qrcode 19 | apscheduler 20 | lxml 21 | pre-commit 22 | mkdocs 23 | mkdocs-material 24 | types-requests 25 | mkdocstrings 26 | mkdocstrings-python-legacy 27 | yapf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyee 2 | requests 3 | qrcode 4 | lxml 5 | wechaty-puppet>=0.4.19 6 | wechaty-puppet-service>=0.8.9 7 | quart 8 | opengraph_py3 9 | Quart-CORS 10 | APScheduler 11 | SQLAlchemy 12 | PyGithub 13 | urllib3<2 14 | -------------------------------------------------------------------------------- /scripts/build_ui.sh: -------------------------------------------------------------------------------- 1 | # start to build wechaty-ui into wechaty 2 | 3 | make clean 4 | git clone https://github.com/wechaty/wechaty-ui ui 5 | cd ui 6 | npm i && npm run build 7 | 8 | # move the dist files to 9 | echo "starting to copy files to the package ..." 10 | pwd 11 | cp -r dist/ ../src/wechaty/ui -------------------------------------------------------------------------------- /scripts/check_python_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """check version""" 3 | 4 | import re 5 | import sys 6 | from typing import Tuple 7 | 8 | 9 | def version() -> Tuple[int, int, int]: 10 | """version""" 11 | try: 12 | ver = re.findall(r'^\d+\.\d+\.\d+', sys.version)[0] 13 | senior, minor, patch = re.findall(r'\d+', ver) 14 | return (int(senior), int(minor), int(patch)) 15 | 16 | # pylint: disable=W0703 17 | except Exception: 18 | return (0, 0, 0) 19 | 20 | 21 | # major, minor, patch = version() 22 | 23 | 24 | if sys.version_info < (3, 6): 25 | sys.exit('ERROR: Python 3.7 or above is required.') 26 | else: 27 | print('Python %d.%d.%d passed checking.' % sys.version_info[:3]) 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # NOTE: All relative paths are relative to the location of this file. 2 | 3 | [pytype] 4 | # Space-separated list of files or directories to exclude. 5 | exclude = 6 | **/*_test.py 7 | **/test_*.py 8 | 9 | # Space-separated list of files or directories to process. 10 | inputs = 11 | . 12 | 13 | # Keep going past errors to analyze as many files as possible. 14 | keep_going = False 15 | 16 | # Run N jobs in parallel. 17 | jobs = 4 18 | 19 | # All pytype output goes here. 20 | output = .pytype 21 | 22 | # Paths to source code directories, separated by ':'. 23 | pythonpath = 24 | . 25 | 26 | # Python version (major.minor) of the target code. 27 | python_version = 3.7 28 | 29 | # Comma or space separated list of error names to ignore. 30 | disable = 31 | pyi-error 32 | # import-error 33 | 34 | # Don't report errors. 35 | report_errors = True 36 | 37 | # Experimental: Infer precise return types even for invalid function calls. 38 | precise_return = False 39 | 40 | # Experimental: solve unknown types to label with structural types. 41 | protocols = False 42 | 43 | # Experimental: Only load submodules that are explicitly imported. 44 | strict_import = False 45 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | setup 3 | """ 4 | import os 5 | from typing import List 6 | 7 | import semver 8 | import setuptools 9 | 10 | 11 | def versioning(version: str) -> str: 12 | """ 13 | version to specification 14 | Author: Huan (https://github.com/huan) 15 | 16 | X.Y.Z -> X.Y.devZ 17 | 18 | """ 19 | sem_ver = semver.parse(version) 20 | 21 | major = sem_ver['major'] 22 | minor = sem_ver['minor'] 23 | patch = str(sem_ver['patch']) 24 | 25 | if minor % 2: 26 | patch = 'dev' + patch 27 | 28 | fin_ver = '%d.%d.%s' % ( 29 | major, 30 | minor, 31 | patch, 32 | ) 33 | 34 | return fin_ver 35 | 36 | 37 | def get_version() -> str: 38 | """ 39 | read version from VERSION file 40 | """ 41 | version = '0.0.0' 42 | 43 | with open( 44 | os.path.join( 45 | os.path.dirname(__file__), 46 | 'VERSION' 47 | ) 48 | ) as version_fh: 49 | # Get X.Y.Z 50 | version = version_fh.read().strip() 51 | # versioning from X.Y.Z to X.Y.devZ 52 | version = versioning(version) 53 | 54 | return version 55 | 56 | 57 | def get_long_description() -> str: 58 | """get long_description""" 59 | with open('README.md', 'r') as readme_fh: 60 | return readme_fh.read() 61 | 62 | 63 | def get_install_requires() -> List[str]: 64 | """get install_requires""" 65 | with open('requirements.txt', 'r') as requirements_fh: 66 | return requirements_fh.read().splitlines() 67 | 68 | 69 | setuptools.setup( 70 | name='wechaty', 71 | version=get_version(), 72 | author='Jingjing WU (吴京京)', 73 | author_email='wechaty@chatie.io', 74 | description='Wechaty is a Conversational RPA SDK for Chatbot Makers', 75 | long_description=get_long_description(), 76 | long_description_content_type='text/markdown', 77 | license='Apache-2.0', 78 | url='https://github.com/wechaty/python-wechaty', 79 | packages=setuptools.find_packages('src', exclude=['__pycache__', '.mypy_cache']), 80 | include_package_data=True, 81 | package_dir={'wechaty': 'src/wechaty'}, 82 | install_requires=get_install_requires(), 83 | classifiers=[ 84 | 'Programming Language :: Python :: 3.7', 85 | 'License :: OSI Approved :: Apache Software License', 86 | 'Operating System :: OS Independent', 87 | ], 88 | ) 89 | -------------------------------------------------------------------------------- /src/wechaty/__init__.py: -------------------------------------------------------------------------------- 1 | """doc""" 2 | 3 | # 4 | # import types from wechaty_puppet 5 | # 6 | 7 | from wechaty_puppet import ( 8 | FileBox, 9 | MessageType, 10 | MessagePayload, 11 | 12 | # Contact 13 | ContactGender, 14 | ContactType, 15 | ContactPayload, 16 | 17 | # Friendship 18 | FriendshipType, 19 | FriendshipPayload, 20 | 21 | # Room 22 | RoomPayload, 23 | RoomMemberPayload, 24 | 25 | # UrlLink 26 | 27 | # RoomInvitation 28 | RoomInvitationPayload, 29 | 30 | # Image 31 | ImageType, 32 | 33 | # Event 34 | EventType, 35 | EventReadyPayload, 36 | 37 | RoomQueryFilter, 38 | RoomMemberQueryFilter, 39 | FriendshipSearchQueryFilter, 40 | ContactQueryFilter, 41 | MessageQueryFilter, 42 | ScanStatus 43 | ) 44 | 45 | from .config import ( 46 | get_logger, 47 | ) 48 | from .accessory import Accessory 49 | from .plugin import ( 50 | WechatyPlugin, 51 | ) 52 | from .schema import WechatyPluginOptions 53 | 54 | from .wechaty import ( 55 | Wechaty, 56 | WechatyOptions, 57 | ) 58 | from .user import ( 59 | Contact, 60 | Favorite, 61 | Friendship, 62 | Image, 63 | Message, 64 | MiniProgram, 65 | Room, 66 | RoomInvitation, 67 | Tag, 68 | UrlLink, 69 | ) 70 | from .exceptions import ( 71 | WechatyError, 72 | WechatyConfigurationError, 73 | WechatyAccessoryBindingError, 74 | WechatyStatusError, 75 | WechatyPayloadError, 76 | WechatyOperationError, 77 | WechatyPluginError, 78 | ) 79 | 80 | from .version import VERSION 81 | 82 | __version__ = VERSION 83 | 84 | __all__ = [ 85 | 'Accessory', 86 | 'Contact', 87 | 'Favorite', 88 | 'FileBox', 89 | 'Friendship', 90 | 'get_logger', 91 | 'Image', 92 | 'Message', 93 | 'MiniProgram', 94 | 'Room', 95 | 'RoomInvitation', 96 | 'Tag', 97 | 'UrlLink', 98 | 'Wechaty', 99 | 'WechatyOptions', 100 | 101 | 'WechatyPlugin', 102 | 'WechatyPluginOptions', 103 | 104 | 'MessageType', 105 | 'MessagePayload', 106 | 107 | # Contact 108 | 'ContactGender', 109 | 'ContactType', 110 | 'ContactPayload', 111 | 112 | # Friendship 113 | 'FriendshipType', 114 | 'FriendshipPayload', 115 | 116 | # Room 117 | 'RoomPayload', 118 | 'RoomMemberPayload', 119 | 120 | # UrlLink 121 | 122 | # RoomInvitation 123 | 'RoomInvitationPayload', 124 | 125 | # Image 126 | 'ImageType', 127 | 128 | # Event 129 | 'EventType', 130 | 'EventReadyPayload', 131 | 132 | 'ScanStatus', 133 | 134 | 'RoomQueryFilter', 135 | 'RoomMemberQueryFilter', 136 | 'FriendshipSearchQueryFilter', 137 | 'ContactQueryFilter', 138 | 'MessageQueryFilter', 139 | 140 | # Error 141 | 'WechatyError', 142 | 'WechatyConfigurationError', 143 | 'WechatyAccessoryBindingError', 144 | 'WechatyStatusError', 145 | 'WechatyPayloadError', 146 | 'WechatyOperationError', 147 | 'WechatyPluginError', 148 | ] 149 | -------------------------------------------------------------------------------- /src/wechaty/accessory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2018-now @copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import annotations 22 | 23 | from typing import ( 24 | TYPE_CHECKING, 25 | # overload, 26 | # cast, 27 | Optional, 28 | TypeVar, 29 | Generic, 30 | ) 31 | 32 | from wechaty_puppet import ( 33 | get_logger, 34 | Puppet, 35 | ) 36 | 37 | from wechaty.exceptions import WechatyAccessoryBindingError 38 | 39 | # pylint:disable=R0401 40 | if TYPE_CHECKING: 41 | from .wechaty import Wechaty 42 | 43 | log = get_logger('Accessory') 44 | 45 | PayloadType = TypeVar('PayloadType') 46 | 47 | 48 | class Accessory(Generic[PayloadType]): 49 | """ 50 | Translate the function from TypeScript to Python 51 | See: https://github.com/wechaty/wechaty/blob/master/src/accessory.ts 52 | """ 53 | 54 | _puppet: Optional[Puppet] = None 55 | _wechaty: Optional[Wechaty] = None 56 | 57 | abstract: bool = True 58 | 59 | def __init__(self) -> None: 60 | if self.abstract: 61 | raise WechatyAccessoryBindingError( 62 | 'Do not instantiate class {cls} directly, sse with bot.{cls} instead. ' 63 | 'See https://github.com/wechaty/wechaty/issues/1217'.format( 64 | cls=type(self).__name__ 65 | ) 66 | ) 67 | 68 | self._payload: Optional[PayloadType] = None 69 | 70 | @property 71 | def payload(self) -> PayloadType: 72 | """ 73 | get the payload object as a property 74 | :return: 75 | """ 76 | if self._payload is None: 77 | raise ValueError( 78 | f'should ready() the {type(self).__name__} payload before get it, ' 79 | 'please call the method' 80 | ) 81 | return self._payload 82 | 83 | @payload.setter 84 | def payload(self, value: PayloadType) -> None: 85 | """ 86 | :param value: 87 | :return: 88 | """ 89 | if self._payload: 90 | log.warning('<%s> set payload more than once', self) 91 | self._payload = value 92 | 93 | def is_ready(self) -> bool: 94 | """ 95 | check if payload is ready 96 | :return: 97 | """ 98 | return self._puppet is not None and self._payload is not None 99 | 100 | @classmethod 101 | def set_puppet(cls, new_puppet: Puppet) -> None: 102 | """doc""" 103 | if cls._puppet is not None: 104 | raise AttributeError('can not set _puppet twice') 105 | cls._puppet = new_puppet 106 | 107 | @classmethod 108 | def set_wechaty(cls, new_wechaty: Wechaty) -> None: 109 | """doc""" 110 | if cls._wechaty is not None: 111 | raise AttributeError('can not set _wechaty twice') 112 | cls._wechaty = new_wechaty 113 | 114 | @classmethod 115 | def get_puppet(cls) -> Puppet: 116 | """doc""" 117 | if cls._puppet is None: 118 | raise AttributeError('puppet not found') 119 | return cls._puppet 120 | 121 | @classmethod 122 | def get_wechaty(cls) -> Wechaty: 123 | """doc""" 124 | if cls._wechaty is None: 125 | raise AttributeError('wechaty not found') 126 | return cls._wechaty 127 | 128 | @property 129 | def puppet(self) -> Puppet: 130 | """doc""" 131 | if self._puppet is None: 132 | raise AttributeError('puppet not set') 133 | return self._puppet 134 | 135 | @property 136 | def wechaty(self) -> Wechaty: 137 | """ 138 | instance property 139 | """ 140 | if self._wechaty is None: 141 | raise AttributeError('wechaty not set') 142 | return self._wechaty 143 | -------------------------------------------------------------------------------- /src/wechaty/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | import os 22 | import re 23 | from typing import ( 24 | Optional, 25 | Any 26 | ) 27 | 28 | from wechaty_puppet import ( 29 | FileBox, 30 | get_logger 31 | ) 32 | 33 | 34 | log = get_logger('Config') 35 | 36 | # log.debug('test logging debug') 37 | # log.info('test logging info') 38 | 39 | 40 | # TODO(wj-Mcat): there is no reference usage, so need to be removed 41 | _FILE_PATH = os.path.dirname(os.path.realpath(__file__)) 42 | DATA_PATH = os.path.realpath( 43 | os.path.join( 44 | _FILE_PATH, 45 | '../data', 46 | ), 47 | ) 48 | 49 | # http://jkorpela.fi/chars/spaces.html 50 | # String.fromCharCode(8197) 51 | AT_SEPARATOR = chr(0x2005) 52 | 53 | # refer to:https://github.com/wechaty/python-wechaty/issues/285#issuecomment-997441596 54 | PARALLEL_TASK_NUM = 100 55 | 56 | 57 | def global_exception_handler(exception: Exception) -> None: 58 | """ 59 | handle the global exception 60 | :param exception: exception message 61 | :return: 62 | """ 63 | log.error('occur %s %s', exception.__class__.__name__, str(exception.args)) 64 | print(exception) 65 | 66 | 67 | class DefaultSetting(dict): 68 | """ 69 | store global default setting 70 | """ 71 | default_api_host: Optional[str] = None 72 | default_port: Optional[int] = None 73 | default_protocol: Optional[str] = None 74 | 75 | 76 | # pylint: disable=R0903 77 | def valid_api_host(api_host: str) -> bool: 78 | """ 79 | test the validation of the api_host 80 | :param api_host: 81 | :return: 82 | """ 83 | pattern = re.compile( 84 | r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|:?[0-9]*' 85 | r'([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|:?[0-9]*' 86 | r'([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.:?[0-9]*' 87 | r'([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3}):?[0-9]*$' 88 | ) 89 | return bool(pattern.match(api_host)) 90 | 91 | 92 | class Config: 93 | """ 94 | get the configuration from the environment variables 95 | """ 96 | @property 97 | def cache_dir(self) -> str: 98 | """get the cache dir in the lazy loading mode 99 | 100 | Returns: 101 | str: the path of cache dir 102 | """ 103 | path = os.environ.get("CACHE_DIR", '.wechaty') 104 | os.makedirs(path, exist_ok=True) 105 | return path 106 | 107 | @property 108 | def ui_dir(self) -> str: 109 | """get the ui directory 110 | 111 | Returns: 112 | str: the path of the ui dir 113 | """ 114 | default_ui_dir = os.path.join( 115 | os.path.dirname(__file__), 116 | 'ui' 117 | ) 118 | return os.environ.get("UI_DIR", default_ui_dir) 119 | 120 | def get_environment_variable( 121 | self, 122 | name: str, 123 | default_value: Optional[Any] = None 124 | ) -> Optional[Any]: 125 | """get environment variable 126 | 127 | Args: 128 | name (str): the name of environment 129 | default_value (Optional[Any], optional): default Value. Defaults to None. 130 | """ 131 | if name not in os.environ: 132 | return default_value 133 | return os.environ[name] 134 | 135 | @property 136 | def cache_rooms(self) -> bool: 137 | """whether cache all of payloads of rooms 138 | 139 | Returns: 140 | bool: whether cache the paylaod of rooms 141 | """ 142 | env_key = 'CACHE_ROOMS' 143 | true_strings = ['true', '1'] 144 | if env_key not in os.environ: 145 | return True 146 | value = os.environ[env_key] 147 | return value in true_strings 148 | 149 | @property 150 | def cache_room_path(self) -> str: 151 | """get the room pickle path""" 152 | env_key = "CACHE_ROOMS_PATH" 153 | if env_key in os.environ: 154 | return os.environ[env_key] 155 | 156 | default_path = os.path.join( 157 | self.cache_dir, 158 | "room_payloads.pkl" 159 | ) 160 | return default_path 161 | 162 | @property 163 | def cache_contacts(self) -> bool: 164 | """whether cache all of payloads of contact 165 | 166 | Returns: 167 | bool: whether cache the paylaod of contact 168 | """ 169 | 170 | env_key = 'CACHE_CONTACTS' 171 | true_strings = ['true', '1'] 172 | if env_key not in os.environ: 173 | return True 174 | value = os.environ[env_key] 175 | return value in true_strings 176 | 177 | @property 178 | def cache_contact_path(self) -> str: 179 | """get the contact pickle path""" 180 | env_key = "CACHE_CONTACTS_PATH" 181 | if env_key in os.environ: 182 | return os.environ[env_key] 183 | 184 | default_path = os.path.join( 185 | self.cache_dir, 186 | "contact_payloads.pkl" 187 | ) 188 | return default_path 189 | 190 | # export const CHATIE_OFFICIAL_ACCOUNT_ID = 'gh_051c89260e5d' 191 | # chatie_official_account_id = 'gh_051c89260e5d' 192 | # CHATIE_OFFICIAL_ACCOUNT_ID = 'gh_051c89260e5d' 193 | 194 | 195 | def qr_code_for_chatie() -> FileBox: 196 | """ 197 | create QRcode for chatie 198 | :return: 199 | """ 200 | # const CHATIE_OFFICIAL_ACCOUNT_QRCODE = 201 | # 'http://weixin.qq.com/r/qymXj7DEO_1ErfTs93y5' 202 | chatie_official_account_qr_code: str = \ 203 | 'http://weixin.qq.com/r/qymXj7DEO_1ErfTs93y5' 204 | return FileBox.from_qr_code(chatie_official_account_qr_code) 205 | 206 | 207 | config = Config() 208 | -------------------------------------------------------------------------------- /src/wechaty/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Alfred Huang (黃文超) 5 | 6 | 2020-now @ Copyright Wechaty 7 | 8 | Licensed under the Apache License, Version 2.0 (the 'License'); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an 'AS IS' BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | 21 | 22 | from typing import Any 23 | 24 | 25 | class WechatyError(Exception): 26 | """ Wechaty error """ 27 | 28 | def __init__(self, message: str, code: Any = None, params: Any = None): 29 | super().__init__(message, code, params) 30 | 31 | self.message = message 32 | self.code = code 33 | self.params = params 34 | 35 | def __str__(self) -> str: 36 | return repr(self) 37 | 38 | 39 | class WechatyAccessoryBindingError(WechatyError): 40 | """ Raises when using Accessory classes in the wrong way """ 41 | 42 | 43 | class WechatyStatusError(WechatyError, AttributeError): 44 | """ Wechaty method calling o non-proper status (e.g. lack of await ready) """ 45 | 46 | 47 | class WechatyConfigurationError(WechatyError, AttributeError): 48 | """ Raises when configuration out of expected case """ 49 | 50 | 51 | class WechatyOperationError(WechatyError): 52 | """ Logical out of business error occurs when using wechaty """ 53 | 54 | 55 | class WechatyPluginError(WechatyError): 56 | """ Error occurs when using plugin """ 57 | 58 | 59 | class WechatyPayloadError(WechatyError, ValueError): 60 | """ Error occurs when the GRPC service return data out of expected """ 61 | -------------------------------------------------------------------------------- /src/wechaty/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/python-wechaty/e9a04a98a3b01f287760e2d2a4514e4a80ecd15f/src/wechaty/py.typed -------------------------------------------------------------------------------- /src/wechaty/schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import annotations 22 | from enum import Enum 23 | import os 24 | from typing import Any, Optional, List, Dict, Union 25 | from dataclasses import dataclass 26 | 27 | from quart import jsonify, Response 28 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore 29 | from wechaty.config import config 30 | 31 | 32 | @dataclass 33 | class NavMetadata: 34 | """nav metadata""" 35 | view_url: Optional[str] = None 36 | author: Optional[str] = None # name of author 37 | avatar: Optional[str] = None # avatar of author 38 | author_link: Optional[str] = None # introduction link of author 39 | icon: Optional[str] = None # avatar of author 40 | 41 | 42 | @dataclass 43 | class NavDTO: 44 | """the data transfer object of plugin list""" 45 | name: str # name of plugin 46 | status: int # status of plugin: 0 / 1 47 | 48 | view_url: Optional[str] = None 49 | author: Optional[str] = None # name of author 50 | avatar: Optional[str] = None # avatar of author 51 | author_link: Optional[str] = None # introduction link of author 52 | icon: Optional[str] = None # avatar of author 53 | 54 | def update_metadata(self, nav_metadata: NavMetadata) -> None: 55 | """update the field with nav data 56 | """ 57 | self.author = nav_metadata.author 58 | self.author_link = nav_metadata.author_link 59 | self.avatar = nav_metadata.avatar 60 | self.icon = nav_metadata.icon 61 | self.view_url = nav_metadata.view_url 62 | 63 | 64 | def success(data: Any) -> Response: 65 | """make the success response with data 66 | 67 | Args: 68 | data (dict): the data of response 69 | """ 70 | return jsonify(dict( 71 | code=200, 72 | data=data 73 | )) 74 | 75 | 76 | def error(msg: str) -> Response: 77 | """make the error response with msg 78 | 79 | Args: 80 | msg (str): the error msg string of data 81 | """ 82 | return jsonify(dict( 83 | code=500, 84 | msg=msg 85 | )) 86 | 87 | 88 | @dataclass 89 | class WechatyPluginOptions: 90 | """options for wechaty plugin""" 91 | name: Optional[str] = None 92 | metadata: Optional[dict] = None 93 | 94 | 95 | @dataclass 96 | class WechatySchedulerOptions: 97 | """options for wechaty scheduler""" 98 | job_store: Union[str, SQLAlchemyJobStore] = f'sqlite:///{config.cache_dir}/job.db' 99 | job_store_alias: str = 'wechaty-scheduler' 100 | 101 | 102 | class PluginStatus(Enum): 103 | """plugin running status""" 104 | Running = 0 105 | Stopped = 1 106 | 107 | 108 | class StaticFileCacher: 109 | """cache the static file to avoid time-consuming finding and loading 110 | """ 111 | def __init__(self, cache_dirs: Optional[List[str]] = None) -> None: 112 | self.file_maps: Dict[str, str] = {} 113 | 114 | self.cache_dirs = cache_dirs or [] 115 | 116 | def add_dir(self, static_file_dir: Optional[str]) -> None: 117 | """add the static file dir 118 | 119 | Args: 120 | static_file_dir (str): the path of the static file 121 | """ 122 | if not static_file_dir: 123 | return 124 | self.cache_dirs.append(static_file_dir) 125 | 126 | def _find_file_path_recursive(self, base_dir: str, name: str) -> Optional[str]: 127 | """find the file based on the file-name which will & should be union 128 | 129 | Args: 130 | base_dir (str): the root dir of static files for the plugin 131 | name (str): the union name of static file 132 | 133 | Returns: 134 | Optional[str]: the target static file path 135 | """ 136 | if not os.path.exists(base_dir) or os.path.isfile(base_dir): 137 | return None 138 | 139 | for file_name in os.listdir(base_dir): 140 | if file_name == name: 141 | return os.path.join(base_dir, file_name) 142 | file_path = os.path.join(base_dir, file_name) 143 | 144 | target_path = self._find_file_path_recursive(file_path, name) 145 | if target_path: 146 | return target_path 147 | 148 | return None 149 | 150 | def find_file_path(self, name: str) -> Optional[str]: 151 | """find the file based on the file-name which will & should be union 152 | 153 | Args: 154 | name (str): the union name of static file 155 | 156 | Returns: 157 | Optional[str]: the path of the static file 158 | """ 159 | if name in self.file_maps: 160 | return self.file_maps[name] 161 | 162 | for cache_dir in self.cache_dirs: 163 | file_path = self._find_file_path_recursive(cache_dir, name) 164 | if file_path: 165 | return file_path 166 | return None 167 | -------------------------------------------------------------------------------- /src/wechaty/types.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import annotations 22 | 23 | from typing import ( 24 | List, 25 | Optional, 26 | Union, 27 | TYPE_CHECKING, 28 | Tuple 29 | ) 30 | from abc import ABC 31 | 32 | if TYPE_CHECKING: 33 | from .user import ( 34 | Message, 35 | Contact, 36 | ) 37 | 38 | 39 | # pylint: disable=R0903 40 | class Sayable(ABC): 41 | """ 42 | wechaty sayable interface 43 | """ 44 | async def say( 45 | self, text: str, 46 | reply_to: Union[Contact, List[Contact]] 47 | ) -> Optional[Message]: 48 | """ 49 | derived classes must implement this function 50 | """ 51 | raise NotImplementedError 52 | 53 | 54 | # pylint: disable=R0903 55 | class Acceptable(ABC): 56 | """ 57 | wechaty acceptable interface 58 | """ 59 | async def accept(self) -> None: 60 | """ 61 | derived classes must implement this function 62 | """ 63 | raise NotImplementedError 64 | 65 | 66 | EndPoint = Tuple[str, int] 67 | -------------------------------------------------------------------------------- /src/wechaty/user/__init__.py: -------------------------------------------------------------------------------- 1 | """doc""" 2 | from __future__ import annotations 3 | 4 | from .contact import Contact 5 | from .favorite import Favorite 6 | from .friendship import Friendship 7 | from .image import Image 8 | from .message import Message 9 | from .mini_program import MiniProgram 10 | from .room import Room 11 | from .tag import Tag 12 | from .url_link import UrlLink 13 | from .room_invitation import RoomInvitation 14 | from .contact_self import ContactSelf 15 | 16 | # Huan(202003): is that necessary to put "name" to `__all__`? 17 | # name = 'user' 18 | 19 | __all__ = [ 20 | 'Contact', 21 | 'Favorite', 22 | 'Friendship', 23 | 'Image', 24 | 'Message', 25 | 'MiniProgram', 26 | 'Room', 27 | 'Tag', 28 | 'UrlLink', 29 | 'RoomInvitation', 30 | 'ContactSelf' 31 | ] 32 | -------------------------------------------------------------------------------- /src/wechaty/user/contact_self.py: -------------------------------------------------------------------------------- 1 | """ContactSelf""" 2 | 3 | from __future__ import annotations 4 | from typing import Any, Optional, Type 5 | 6 | from wechaty import FileBox, get_logger 7 | from wechaty.exceptions import WechatyOperationError 8 | from wechaty.user.contact import Contact 9 | 10 | log = get_logger('ContactSelf') 11 | 12 | 13 | class ContactSelf(Contact): 14 | """ContactSelf""" 15 | 16 | async def avatar(self, file_box: Optional[FileBox] = None) -> FileBox: 17 | """ 18 | Get or set avatar of ContactSelf. 19 | 20 | Args: 21 | file_box: FileBox object, if not provided, it will return a FileBox object 22 | 23 | Examples: 24 | >>> contact_self = bot.contact_self() 25 | >>> file_box = await contact_self.avatar() 26 | >>> file_box = await contact_self.avatar(file_box) 27 | 28 | Raises: 29 | WechatyOperationError: if the contact is not self, it will not get the avatar 30 | 31 | Returns: 32 | FileBox: file_box 33 | """ 34 | log.info('avatar(%s)' % file_box.name if file_box else '') 35 | if not file_box: 36 | file_box = await super().avatar(None) 37 | return file_box 38 | 39 | if self.contact_id != self.puppet.self_id(): 40 | raise WechatyOperationError('set avatar only available for user self') 41 | 42 | await self.puppet.contact_avatar(self.contact_id, file_box) 43 | 44 | async def qr_code(self) -> str: 45 | """ 46 | Return the qrcode of ContactSelf 47 | 48 | Examples: 49 | >>> contact_self = bot.contact_self() 50 | >>> qr_code = await contact_self.qr_code() 51 | 52 | Raises: 53 | WechatyOperationError: if there is login exception, it will not get the qrcode 54 | WechatyOperationError: if the contact is not self, it will not get the qrcode 55 | 56 | Returns: 57 | str: the content of qrcode 58 | """ 59 | try: 60 | contact_id: str = self.puppet.self_id() 61 | except Exception: 62 | raise WechatyOperationError( 63 | 'Can not get qr_code, user might be either not logged in or already logged out' 64 | ) 65 | 66 | if self.contact_id != contact_id: 67 | raise WechatyOperationError('only can get qr_code for the login user self') 68 | qr_code_value = await self.puppet.contact_self_qr_code() 69 | return qr_code_value 70 | 71 | @property 72 | def name(self) -> str: 73 | """ 74 | Get the name of login contact. 75 | 76 | Examples: 77 | >>> contact_self = bot.contact_self() 78 | >>> name = contact_self.name 79 | 80 | Returns: 81 | str: the name of Login User 82 | """ 83 | return super().name 84 | 85 | async def set_name(self, name: str) -> None: 86 | """ 87 | Set the name of login contact. 88 | 89 | Args: 90 | name: new name 91 | 92 | Examples: 93 | >>> contact_self = bot.contact_self() 94 | >>> await contact_self.set_name('new name') 95 | """ 96 | await self.puppet.contact_self_name(name) 97 | await self.ready(force_sync=True) 98 | 99 | async def signature(self, signature: str) -> Any: 100 | """ 101 | Set the signature of login contact. 102 | 103 | Args: 104 | signature: new signature 105 | 106 | Examples: 107 | >>> contact_self = bot.contact_self() 108 | >>> await contact_self.signature('new signature') 109 | 110 | Raises: 111 | WechatyOperationError: if there is login exception, it will not set the signature 112 | WechatyOperationError: if the contact is not self, it will not set the signature 113 | 114 | Returns: 115 | Any: the signature of login user 116 | """ 117 | puppet_id = self.puppet.self_id() 118 | 119 | if self.contact_id != puppet_id: 120 | raise WechatyOperationError('only can get qr_code for the login user self') 121 | 122 | return self.puppet.contact_signature(signature) 123 | -------------------------------------------------------------------------------- /src/wechaty/user/favorite.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import annotations 22 | 23 | from typing import ( 24 | TYPE_CHECKING, 25 | Any, 26 | List, 27 | ) 28 | from wechaty_puppet import get_logger 29 | 30 | if TYPE_CHECKING: 31 | from .tag import Tag 32 | 33 | log = get_logger('Favorite') 34 | 35 | 36 | # pylint: disable=R 37 | class Favorite: 38 | """ 39 | favorite object which handle the url_link content 40 | """ 41 | def __init__(self, favorite_id: str): 42 | self.favorite_id = favorite_id 43 | 44 | def get_id(self) -> str: 45 | """ 46 | get favorite_id 47 | :return: 48 | """ 49 | log.info('get_id() <%s>', self) 50 | return self.favorite_id 51 | 52 | async def tags(self) -> List[Tag]: 53 | """ 54 | get favorite tags 55 | """ 56 | # TODO -> favorite tags 57 | return [] 58 | 59 | async def find_all(self) -> Any: 60 | """ 61 | get all favorite tags 62 | """ 63 | # TODO -> find_all 64 | return 65 | -------------------------------------------------------------------------------- /src/wechaty/user/image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 2020-now @ Copyright Wechaty 4 | 5 | GitHub: 6 | TypeScript: https://github.com/wechaty/wechaty/blob/master/src/user/image.ts 7 | Python: https://github.com/wechaty/python-wechaty/blob/master/src/wechaty/user/images.py 8 | 9 | Authors: Huan LI (李卓桓) 10 | Jingjing WU (吴京京) 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | """ 24 | from __future__ import annotations 25 | 26 | from typing import ( 27 | Type, 28 | ) 29 | 30 | 31 | from wechaty_puppet import ( 32 | FileBox, ImageType, get_logger 33 | ) 34 | 35 | from ..accessory import Accessory 36 | 37 | log = get_logger('Image') 38 | 39 | 40 | class Image(Accessory): 41 | """ 42 | User Image class 43 | """ 44 | 45 | def __str__(self) -> str: 46 | return 'Image<%s>' % self.image_id 47 | 48 | def __init__( 49 | self, 50 | image_id: str, 51 | ) -> None: 52 | """ 53 | :param image_id: 54 | """ 55 | super().__init__() 56 | log.info('init the message Image object <%s>', image_id) 57 | 58 | self.image_id = image_id 59 | 60 | @classmethod 61 | def create(cls: Type[Image], image_id: str) -> Image: 62 | """ 63 | create image instance by image_id 64 | :param cls: 65 | :param image_id: 66 | :return: 67 | """ 68 | log.info('@classmethod create(%s)', image_id) 69 | return cls(image_id) 70 | 71 | async def thumbnail(self) -> FileBox: 72 | """ 73 | docstring 74 | :return: 75 | """ 76 | log.info('thumbnail() for <%s>', self.image_id) 77 | image_file = await self.puppet.message_image( 78 | message_id=self.image_id, image_type=ImageType.IMAGE_TYPE_HD) 79 | return image_file 80 | 81 | async def hd(self) -> FileBox: 82 | """ 83 | docstring 84 | :return: 85 | """ 86 | log.info('hd() for <%s>', self.image_id) 87 | image_file = await self.puppet.message_image( 88 | message_id=self.image_id, image_type=ImageType.IMAGE_TYPE_HD) 89 | return image_file 90 | 91 | async def artwork(self) -> FileBox: 92 | """ 93 | docstring 94 | :return: 95 | """ 96 | log.info('artwork() for <%s>', self.image_id) 97 | image_file = await self.puppet.message_image( 98 | message_id=self.image_id, image_type=ImageType.IMAGE_TYPE_ARTWORK) 99 | return image_file 100 | -------------------------------------------------------------------------------- /src/wechaty/user/message.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from logging import Logger 3 | from ..accessory import Accessory as Accessory 4 | from .contact import Contact as Contact 5 | from .image import Image as Image 6 | from .mini_program import MiniProgram as MiniProgram 7 | from .room import Room as Room 8 | from .url_link import UrlLink as UrlLink 9 | from _typeshed import Incomplete 10 | from datetime import datetime 11 | from typing import List, Optional, Union, overload 12 | from wechaty.exceptions import WechatyOperationError as WechatyOperationError, WechatyPayloadError as WechatyPayloadError 13 | from wechaty.user.contact_self import ContactSelf as ContactSelf 14 | from wechaty.utils import timestamp_to_date as timestamp_to_date 15 | from wechaty_puppet import FileBox, MessagePayload, MessageType 16 | 17 | log: Logger 18 | SUPPORTED_MESSAGE_FILE_TYPES: List[MessageType] 19 | 20 | class Message(Accessory[MessagePayload]): 21 | Type: MessageType 22 | message_id: str 23 | 24 | def __init__(self, message_id: str) -> None: ... 25 | def message_type(self) -> MessageType: ... 26 | 27 | @overload 28 | async def say(self, msg: str) -> Optional[Message]: ... 29 | @overload 30 | async def say(self, msg: str, mention_ids: List[str] = ...) -> Optional[Message]: ... 31 | @overload 32 | async def say(self, msg: Union[Contact, FileBox, UrlLink, MiniProgram]) -> Optional[Message]: ... 33 | 34 | @classmethod 35 | async def find(cls, talker_id: Optional[str] = ..., message_id: Optional[str] = ..., room_id: Optional[str] = ..., text: Optional[str] = ..., to_id: Optional[str] = ..., message_type: Optional[MessageType] = ...) -> Optional[Message]: ... 36 | @classmethod 37 | async def find_all(cls, talker_id: Optional[str] = ..., message_id: Optional[str] = ..., room_id: Optional[str] = ..., text: Optional[str] = ..., to_id: Optional[str] = ..., message_type: Optional[MessageType] = ...) -> List[Message]: ... 38 | def talker(self) -> Contact: ... 39 | def to(self) -> Optional[Contact]: ... 40 | def room(self) -> Optional[Room]: ... 41 | def chatter(self) -> Union[Room, Contact]: ... 42 | def text(self) -> str: ... 43 | async def to_recalled(self) -> Message: ... 44 | async def recall(self) -> bool: ... 45 | @classmethod 46 | def load(cls, message_id: str) -> Message: ... 47 | def type(self) -> MessageType: ... 48 | def is_self(self) -> bool: ... 49 | async def mention_list(self) -> List[Contact]: ... 50 | async def mention_text(self) -> str: ... 51 | async def mention_self(self) -> bool: ... 52 | payload: Incomplete 53 | async def ready(self) -> None: ... 54 | async def forward(self, to: Union[Room, Contact]) -> None: ... 55 | def date(self) -> datetime: ... 56 | def age(self) -> int: ... 57 | async def to_file_box(self) -> FileBox: ... 58 | def to_image(self) -> Image: ... 59 | async def to_contact(self) -> Contact: ... 60 | async def to_url_link(self) -> UrlLink: ... 61 | async def to_mini_program(self) -> MiniProgram: ... 62 | -------------------------------------------------------------------------------- /src/wechaty/user/mini_program.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import annotations 22 | 23 | from typing import TYPE_CHECKING 24 | from dataclasses import asdict 25 | 26 | from wechaty import Accessory 27 | from wechaty_puppet import MiniProgramPayload, get_logger 28 | from wechaty.utils import default_str 29 | 30 | if TYPE_CHECKING: 31 | from wechaty.user import Message 32 | 33 | 34 | log = get_logger('MiniProgram') 35 | 36 | 37 | class MiniProgram(Accessory[MiniProgramPayload]): 38 | """ 39 | mini_program object which handle the url_link content 40 | """ 41 | def __init__(self, payload: MiniProgramPayload): 42 | """ 43 | initialization for mini_program 44 | :param payload: 45 | """ 46 | super().__init__() 47 | 48 | log.info('MiniProgram created') 49 | self._payload: MiniProgramPayload = payload 50 | 51 | @classmethod 52 | async def create_from_message(cls, message: Message) -> MiniProgram: 53 | """ 54 | static create MiniProgram method 55 | :return: 56 | """ 57 | log.info(f'loading the mini-program from message <{message}>') 58 | 59 | mini_program_payload = await cls.get_puppet().message_mini_program( 60 | message_id=message.message_id) 61 | 62 | mini_program = MiniProgram(mini_program_payload) 63 | return mini_program 64 | 65 | @classmethod 66 | def create_from_json(cls, payload_data: dict) -> MiniProgram: 67 | """ 68 | create the mini_program from json data 69 | """ 70 | log.info(f'loading the mini-program from json data <{payload_data}>') 71 | 72 | payload = MiniProgramPayload(**payload_data) 73 | 74 | mini_program = cls(payload=payload) 75 | return mini_program 76 | 77 | def to_json(self) -> dict: 78 | """ 79 | save the mini-program to dict data 80 | """ 81 | log.info(f'save the mini-program to json data : <{self.payload}>') 82 | mini_program_data = asdict(self.payload) 83 | return mini_program_data 84 | 85 | @property 86 | def app_id(self) -> str: 87 | """ 88 | get mini_program app_id 89 | :return: 90 | """ 91 | return default_str(self._payload.appid) 92 | 93 | @property 94 | def title(self) -> str: 95 | """ 96 | get mini_program title 97 | :return: 98 | """ 99 | return default_str(self._payload.title) 100 | 101 | @property 102 | def icon_url(self) -> str: 103 | """ 104 | get mini_program icon url 105 | """ 106 | return default_str(self._payload.iconUrl) 107 | 108 | @property 109 | def page_path(self) -> str: 110 | """ 111 | get mini_program page_path 112 | :return: 113 | """ 114 | return default_str(self._payload.pagePath) 115 | 116 | @property 117 | def user_name(self) -> str: 118 | """ 119 | get mini_program user_name 120 | :return: 121 | """ 122 | return default_str(self._payload.username) 123 | 124 | @property 125 | def description(self) -> str: 126 | """ 127 | get mini_program description 128 | :return: 129 | """ 130 | return default_str(self._payload.description) 131 | 132 | @property 133 | def thumb_url(self) -> str: 134 | """ 135 | get mini_program thumb_url 136 | :return: 137 | """ 138 | return default_str(self._payload.thumbUrl) 139 | 140 | @property 141 | def thumb_key(self) -> str: 142 | """ 143 | get mini_program thumb_key 144 | :return: 145 | """ 146 | return default_str(self._payload.thumbKey) 147 | -------------------------------------------------------------------------------- /src/wechaty/user/room.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from ..accessory import Accessory as Accessory 3 | from ..config import AT_SEPARATOR as AT_SEPARATOR, PARALLEL_TASK_NUM as PARALLEL_TASK_NUM 4 | from .contact import Contact as Contact 5 | from .message import Message as Message 6 | from .mini_program import MiniProgram as MiniProgram 7 | from .url_link import UrlLink as UrlLink 8 | from _typeshed import Incomplete 9 | from typing import Any, Callable, List, Optional, Union, overload 10 | from wechaty.exceptions import WechatyOperationError as WechatyOperationError, WechatyPayloadError as WechatyPayloadError 11 | from wechaty.user.contact_self import ContactSelf as ContactSelf 12 | from wechaty.utils.async_helper import gather_with_concurrency as gather_with_concurrency 13 | from wechaty_puppet import FileBox, RoomMemberQueryFilter, RoomPayload, RoomQueryFilter 14 | 15 | log: Incomplete 16 | 17 | class Room(Accessory[RoomPayload]): 18 | room_id: Incomplete 19 | def __init__(self, room_id: str) -> None: ... 20 | def on(self, event_name: str, func: Callable[..., Any]) -> None: ... 21 | def emit(self, event_name: str, *args: Any, **kwargs: Any) -> None: ... 22 | @classmethod 23 | async def create(cls, contacts: List[Contact], topic: str) -> Room: ... 24 | @classmethod 25 | async def find_all(cls, query: Optional[Union[str, RoomQueryFilter, Callable[[Contact], bool]]] = ...) -> List[Room]: ... 26 | @classmethod 27 | async def find(cls, query: Union[str, RoomQueryFilter, Callable[[Room], bool]] = ...) -> Optional[Room]: ... 28 | @classmethod 29 | def load(cls, room_id: str) -> Room: ... 30 | payload: Incomplete 31 | async def ready(self, force_sync: bool = ..., load_members: bool = ...) -> None: ... 32 | @overload 33 | async def say(self, msg: str) -> Optional[Message]: ... 34 | @overload 35 | async def say(self, msg: str, mention_ids: List[str] = ...) -> Optional[Message]: ... 36 | @overload 37 | async def say(self, msg: Union[Contact, FileBox, UrlLink, MiniProgram]) -> Optional[Message]: ... 38 | 39 | async def add(self, contact: Contact) -> None: ... 40 | async def delete(self, contact: Contact) -> None: ... 41 | async def quit(self) -> None: ... 42 | async def topic(self, new_topic: str = ...) -> Optional[str]: ... 43 | async def announce(self, announce_text: str = ...) -> Optional[str]: ... 44 | async def qr_code(self) -> str: ... 45 | async def alias(self, member: Contact) -> Optional[str]: ... 46 | async def has(self, contact: Contact) -> bool: ... 47 | async def member_list(self, query: Union[str, RoomMemberQueryFilter] = ...) -> List[Contact]: ... 48 | async def member(self, query: Union[str, RoomMemberQueryFilter] = ...) -> Optional[Contact]: ... 49 | async def owner(self) -> Optional[Contact]: ... 50 | async def avatar(self) -> FileBox: ... 51 | -------------------------------------------------------------------------------- /src/wechaty/user/tag.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tag for Contact Message 3 | """ 4 | from __future__ import annotations 5 | 6 | from typing import ( 7 | Dict, 8 | # Optional, 9 | Union, 10 | TYPE_CHECKING 11 | ) 12 | 13 | from collections import defaultdict 14 | 15 | from wechaty.exceptions import WechatyOperationError 16 | from wechaty_puppet import get_logger 17 | from ..accessory import ( 18 | Accessory, 19 | ) 20 | 21 | if TYPE_CHECKING: 22 | from .contact import Contact 23 | from .favorite import Favorite 24 | 25 | log = get_logger('Tag') 26 | 27 | 28 | class Tag(Accessory): 29 | """ 30 | tag object which handle the url_link content 31 | """ 32 | _pool: Dict[str, 'Tag'] = defaultdict() 33 | 34 | tag_id: str 35 | 36 | def __init__(self, tag_id: str) -> None: 37 | """ 38 | initialization for tag base class 39 | :param tag_id: 40 | """ 41 | super().__init__() 42 | log.info('create tag %s', tag_id) 43 | 44 | self.tag_id = tag_id 45 | 46 | @classmethod 47 | def load(cls, tag_id: str) -> Tag: 48 | """ 49 | load tag instance 50 | """ 51 | if tag_id in cls._pool: 52 | return cls._pool[tag_id] 53 | 54 | new_tag = cls(tag_id) 55 | cls._pool[tag_id] = new_tag 56 | return new_tag 57 | 58 | @classmethod 59 | def get(cls, tag_id: str) -> Tag: 60 | """ 61 | get tag objecr 62 | """ 63 | log.info('load tag object %s', tag_id) 64 | return cls.load(tag_id) 65 | 66 | async def delete(self, target: Union[Contact, Favorite]) -> None: 67 | """ 68 | remove tag from contact or favorite 69 | :param target: 70 | :return: 71 | """ 72 | log.info('delete tag %s', self.tag_id) 73 | 74 | if target is Contact: 75 | await self.puppet.tag_contact_delete(tag_id=self.tag_id) 76 | elif target is Favorite: 77 | # TODO -> tag_favorite_delete not implement 78 | pass 79 | # await self.puppet.tag_contact_delete() 80 | else: 81 | raise WechatyOperationError('target param is required to be Contact or Favorite object') 82 | 83 | async def add(self, to: Union[Contact, Favorite]) -> None: 84 | """ 85 | add tag to contact or favorite 86 | :param to: 87 | :return: 88 | """ 89 | log.info('add tag to %s', str(to)) 90 | if isinstance(to, Contact): 91 | await self.puppet.tag_contact_add( 92 | tag_id=self.tag_id, contact_id=to.contact_id 93 | ) 94 | elif isinstance(to, Favorite): 95 | # TODO -> tag_favorite_add not implement 96 | pass 97 | # self.puppet.tag_favorite_add(self.tag_id, to) 98 | 99 | def remove(self, source: Union[Contact, Favorite]) -> None: 100 | """ 101 | Remove this tag from Contact/Favorite 102 | 103 | tips : This function is depending on the Puppet Implementation, 104 | see [puppet-compatible-table](https://github.com/wechaty/ 105 | wechaty/wiki/Puppet#3-puppet-compatible-table) 106 | :param source: 107 | :return: 108 | """ 109 | log.info('remove tag for %s with %s', 110 | self.tag_id, 111 | str(source)) 112 | try: 113 | if isinstance(source, Contact): 114 | self.puppet.tag_contact_remove( 115 | tag_id=self.tag_id, contact_id=source.contact_id) 116 | elif isinstance(source, Favorite): 117 | # TODO -> tag_favorite_remove not implement 118 | pass 119 | except Exception as e: 120 | log.info('remove exception %s', str(e.args)) 121 | raise WechatyOperationError('remove error') 122 | -------------------------------------------------------------------------------- /src/wechaty/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """doc""" 2 | from .qr_code import qr_terminal 3 | from .type_check import default_str 4 | from .date_util import timestamp_to_date 5 | from .data_util import WechatySetting 6 | # from .type_check import type_check 7 | 8 | __all__ = [ 9 | 'qr_terminal', 10 | 'default_str', 11 | 'timestamp_to_date', 12 | 'WechatySetting' 13 | # 'type_check' 14 | ] 15 | -------------------------------------------------------------------------------- /src/wechaty/utils/async_helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | async helpers 3 | """ 4 | from __future__ import annotations 5 | import asyncio 6 | from asyncio import Task 7 | from typing import ( 8 | List, 9 | Any, 10 | Optional, 11 | Set, 12 | TYPE_CHECKING 13 | ) 14 | import functools 15 | 16 | if TYPE_CHECKING: 17 | from wechaty import Message, WechatyPlugin 18 | 19 | 20 | async def gather_with_concurrency(n_task: int, tasks: List[Task]) -> Any: 21 | """ 22 | gather tasks with the specific number concurrency 23 | Args: 24 | n_task: the number of tasks 25 | tasks: task objects 26 | """ 27 | semaphore = asyncio.Semaphore(n_task) 28 | 29 | async def sem_task(task: Task) -> Any: 30 | async with semaphore: 31 | return await task 32 | return await asyncio.gather(*(sem_task(task) for task in tasks)) 33 | 34 | 35 | class SingleIdContainer: 36 | """Store the Message Id Container""" 37 | _instance: Optional[SingleIdContainer] = None 38 | 39 | def __init__(self) -> None: 40 | self.ids: Set[str] = set() 41 | self.max_size: int = 100000 42 | 43 | def exist(self, message_id: str) -> bool: 44 | """exist if the message has been emitted 45 | 46 | Args: 47 | message_id (str): the identifier of message 48 | 49 | Returns: 50 | bool: if the message is the first message 51 | """ 52 | if message_id in self.ids: 53 | return True 54 | self.ids.add(message_id) 55 | return False 56 | 57 | @classmethod 58 | def instance(cls) -> SingleIdContainer: 59 | """singleton pattern for MessageIdContainer""" 60 | if cls._instance is None or len(cls._instance.ids) > cls._instance.max_size: 61 | cls._instance = SingleIdContainer() 62 | 63 | return cls._instance 64 | 65 | 66 | def single_message(on_message_func): # type: ignore 67 | """single message decorator""" 68 | @functools.wraps(on_message_func) 69 | async def wrapper(plugin: WechatyPlugin, message: Message) -> None: 70 | if not SingleIdContainer.instance().exist(message.message_id): 71 | await on_message_func(plugin, message) 72 | return wrapper 73 | -------------------------------------------------------------------------------- /src/wechaty/utils/data_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import annotations 22 | import json 23 | 24 | import os 25 | from typing import ( 26 | Any, 27 | ) 28 | from collections import UserDict 29 | import pickle 30 | 31 | 32 | def save_pickle_data(obj: object, path: str): 33 | """save pickle data 34 | 35 | Args: 36 | obj (object): the pickled data 37 | path (str): the path of pickle data 38 | """ 39 | with open(path, 'wb') as f: 40 | pickle.dump(obj, f) 41 | 42 | def load_pickle_data(path: str) -> object: 43 | """load pickle data from path 44 | 45 | Args: 46 | path (str): the path of pickle data 47 | 48 | Returns: 49 | object: the final data 50 | """ 51 | with open(path, 'rb') as f: 52 | data = pickle.load(f) 53 | return data 54 | 55 | 56 | class WechatySetting(UserDict): 57 | """save setting into file when changed""" 58 | def __init__( 59 | self, 60 | setting_file: str 61 | ): 62 | """init wechaty setting""" 63 | super().__init__() 64 | self.setting_file = setting_file 65 | self._init_setting() 66 | self.data = self.read_setting() 67 | 68 | def _init_setting(self): 69 | """init setting file""" 70 | # 1. init setting dir 71 | setting_dir = os.path.dirname(self.setting_file).strip() 72 | if setting_dir: 73 | os.makedirs(setting_dir, exist_ok=True) 74 | 75 | # 2. init setting file 76 | if not os.path.exists(self.setting_file): 77 | self.save_setting({}) 78 | 79 | # 3. check the content of setting file 80 | else: 81 | with open(self.setting_file, 'r', encoding='utf-8') as f: 82 | content = f.read().strip() 83 | 84 | if not content: 85 | self.save_setting({}) 86 | 87 | def read_setting(self) -> dict: 88 | """read the setting from file 89 | 90 | Returns: 91 | dict: the data of setting file 92 | """ 93 | with open(self.setting_file, 'r', encoding='utf-8') as f: 94 | data = json.load(f) 95 | return data 96 | 97 | def save_setting(self, value: dict) -> None: 98 | """update the plugin setting""" 99 | with open(self.setting_file, 'w', encoding='utf-8') as f: 100 | json.dump(value, f, ensure_ascii=False) 101 | self.data = value 102 | 103 | def __setitem__(self, key: str, value: Any) -> None: 104 | """triggered by `data[key] = value`""" 105 | self.data[key] = value 106 | self.save_setting(self.data) 107 | 108 | def to_dict(self) -> dict: 109 | """return the dict data""" 110 | return self.read_setting() 111 | 112 | 113 | # class QCloudSetting(WechatySetting): 114 | # """Tencent Cloud Object Storaging""" 115 | # def __init__(self, setting_file: str): 116 | # super().__init__(setting_file) 117 | 118 | # from qcloud_cos import CosConfig 119 | # from qcloud_cos import CosS3Client 120 | 121 | # secret_id = config.get_environment_variable("q_secret_id") 122 | # secret_key = config.get_environment_variable("q_secret_key") 123 | # region = config.get_environment_variable("q_secret_region") 124 | # self.bucket_name = config.get_environment_variable("bucket_name") 125 | # self.bucket_path_prefix: str = config.get_environment_variable("bucket_prefix", "") 126 | 127 | # cos_config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key) 128 | # self.client = CosS3Client(cos_config) 129 | 130 | # def read_setting(self) -> dict: 131 | # """read setting from q-cloud 132 | 133 | # Returns: 134 | # dict: the object of setting 135 | # """ 136 | # remote_path = os.path.join(self.bucket_path_prefix, self.setting_file) 137 | # self.client 138 | -------------------------------------------------------------------------------- /src/wechaty/utils/date_util.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python Wechaty - https://github.com/wechaty/python-wechaty 3 | 4 | Authors: Huan LI (李卓桓) 5 | Jingjing WU (吴京京) 6 | 7 | 2020-now @ Copyright Wechaty 8 | 9 | Licensed under the Apache License, Version 2.0 (the 'License'); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an 'AS IS' BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | from __future__ import annotations 22 | from datetime import datetime 23 | 24 | 25 | def timestamp_to_date(timestamp: float) -> datetime: 26 | """convert different timestamp precision to python format 27 | 28 | Python2.7: https://docs.python.org/2.7/library/datetime.html#datetime.datetime 29 | Python3+ :https://docs.python.org/3.7/library/datetime.html#datetime.datetime 30 | for datetime.fromtimestamp. It’s common for this to be restricted to years from 1970 through 2038. 31 | 2145888000 is 2038-01-01 00:00:00 UTC for second 32 | 2145888000 is 1970-01-26 04:04:48 UTC for millisecond 33 | 34 | Args: 35 | timestamp (float): from different source, so, has different 36 | timestamp precision 37 | """ 38 | if timestamp > 2145888000: 39 | timestamp = timestamp / 1000 40 | return datetime.fromtimestamp(timestamp) 41 | -------------------------------------------------------------------------------- /src/wechaty/utils/link.py: -------------------------------------------------------------------------------- 1 | """link helper function for fetching the friendly meta data from url link""" 2 | from __future__ import annotations 3 | from typing import Any, Dict 4 | import requests 5 | from opengraph_py3 import OpenGraph # type: ignore 6 | 7 | 8 | def get_url_metadata(url: str) -> Dict[str, Any]: 9 | """get open graph meta data open open graph protocol 10 | 11 | The Open Graph Protocol: https://ogp.me/ 12 | 13 | Args: 14 | url (str): the url of link 15 | 16 | Returns: 17 | Dict[str, Any]: the meta data 18 | """ 19 | return OpenGraph(url=url) 20 | 21 | 22 | def fetch_github_user_avatar_url(name: str) -> str: 23 | """fetch_github_user_avatar_url 24 | 25 | refer to: https://docs.github.com/en/rest/users/users#get-a-user 26 | 27 | Args: 28 | name (str): the name of github user 29 | 30 | Returns: 31 | str: the avatar url of github user 32 | """ 33 | source_avatar_url = f'https://api.github.com/users/{name}' 34 | 35 | header = { 36 | "accept": "application/vnd.github.v3+jso" 37 | } 38 | response = requests.get(source_avatar_url, headers=header) 39 | data = response.json() 40 | return data.get('avatar_url', None) -------------------------------------------------------------------------------- /src/wechaty/utils/qr_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | qr_code helper utils 4 | """ 5 | from typing import Any 6 | import qrcode 7 | 8 | 9 | def qr_terminal(data: str, version: Any = None) -> None: 10 | """print the qrcode to the terminal using the python-qrcode tools 11 | 12 | https://github.com/lincolnloop/python-qrcode 13 | 14 | Args: 15 | data (str): the data of the qrcode 16 | version (Any, optional): the qrcode version. Defaults to None. 17 | """ 18 | qr = qrcode.QRCode(version, border=2) 19 | qr.add_data(data) 20 | if version: 21 | qr.make() 22 | else: 23 | qr.make(fit=True) 24 | qr.print_ascii(invert=True) 25 | -------------------------------------------------------------------------------- /src/wechaty/utils/qrcode_terminal.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | import qrcode 3 | import platform 4 | 5 | 6 | def qr_terminal_str(data: Any, version: Any = None) -> str: 7 | """ 8 | 9 | :param data: qrcode data 10 | :param version:1-40 or None 11 | :return: 12 | """ 13 | if platform.system() == "Windows": 14 | white_block = '▇' 15 | black_block = ' ' 16 | new_line = '\n' 17 | else: 18 | white_block = '\033[0;37;47m ' 19 | black_block = '\033[0;37;40m ' 20 | new_line = '\033[0m\n' 21 | 22 | qr = qrcode.QRCode(version) 23 | qr.add_data(data) 24 | if version: 25 | qr.make() 26 | else: 27 | qr.make(fit=True) 28 | output = white_block * (qr.modules_count + 2) + new_line 29 | for mn in qr.modules: 30 | output += white_block 31 | for m in mn: 32 | if m: 33 | output += black_block 34 | else: 35 | output += white_block 36 | output += white_block + new_line 37 | output += white_block * (qr.modules_count + 2) + new_line 38 | return output 39 | 40 | 41 | def draw(data: Any, version: Any = None) -> None: 42 | """doc""" 43 | output = qr_terminal_str(data, version) 44 | print(output) 45 | -------------------------------------------------------------------------------- /src/wechaty/utils/type_check.py: -------------------------------------------------------------------------------- 1 | """add type check utils""" 2 | from typing import Union, Optional 3 | 4 | 5 | def type_check(obj: object, type_name: str) -> bool: 6 | """ 7 | circulation dependency problems can be resolved by TYPE_CHECKING, 8 | but this can not resolve NO type linting problems. eg: 9 | if isinstance(msg, Contact): 10 | pass 11 | in this problem, program don't import Contact at running time. So, it will 12 | throw a Exception, which will not be threw 13 | :param obj: 14 | :param type_name: 15 | :return: 16 | """ 17 | if hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'): 18 | return obj.__class__.__name__ == type_name 19 | return False 20 | 21 | 22 | def default_str(obj: Union[str, Optional[str]]) -> str: 23 | if obj: 24 | return obj 25 | return '' 26 | -------------------------------------------------------------------------------- /src/wechaty/version.py: -------------------------------------------------------------------------------- 1 | """ 2 | Do not edit this file. 3 | This file will be auto-generated before deploy. 4 | """ 5 | VERSION = '0.0.0' 6 | -------------------------------------------------------------------------------- /tests/accessory_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Huan(202003): 3 | Translated from TypeScript to Python 4 | See: https://github.com/wechaty/wechaty/blob/master/src/accessory.spec.ts 5 | """ 6 | from __future__ import annotations 7 | 8 | import os 9 | from uuid import uuid4 10 | from typing import ( 11 | Any, 12 | Generator, 13 | Type, 14 | cast, 15 | ) 16 | import tempfile 17 | import pytest 18 | 19 | from wechaty.accessory import ( 20 | Accessory, 21 | ) 22 | from wechaty import ContactPayload 23 | from wechaty.utils.data_util import save_pickle_data, load_pickle_data 24 | 25 | EXPECTED_PUPPET1 = cast(Any, {'p': 1}) 26 | EXPECTED_PUPPET2 = cast(Any, {'p': 2}) 27 | 28 | EXPECTED_WECHATY1 = cast(Any, {'w': 1}) 29 | EXPECTED_WECHATY2 = cast(Any, {'w': 2}) 30 | 31 | 32 | def get_user_class() -> Type[Accessory]: 33 | """create a fixture""" 34 | 35 | class FixtureClass(Accessory): 36 | """fixture""" 37 | # fish-ball: in order to make a Accessory class properly construct, 38 | # we should instantiate it from a derived class which has abstract 39 | # field to be False 40 | abstract = False 41 | 42 | return FixtureClass 43 | 44 | 45 | @pytest.fixture(name='user_class') 46 | def fixture_user_class() -> Generator[Type[Accessory], None, None]: 47 | """fixture for fixture class""" 48 | yield get_user_class() 49 | 50 | 51 | def test_indenpendent_user_classes() -> None: 52 | """two child class should not be equal""" 53 | user_class1 = get_user_class() 54 | user_class2 = get_user_class() 55 | 56 | assert user_class1 != user_class2, 'two child class should not be equal' 57 | 58 | 59 | def test_user_classes_should_share() -> None: 60 | """doc""" 61 | 62 | user_class = get_user_class() 63 | 64 | user_class.set_wechaty(EXPECTED_WECHATY1) 65 | user_class.set_puppet(EXPECTED_PUPPET1) 66 | 67 | child1 = user_class() 68 | child2 = user_class() 69 | 70 | assert child1.wechaty == EXPECTED_WECHATY1, \ 71 | 'child1 should get the wechaty from static value' 72 | assert child2.wechaty == EXPECTED_WECHATY1, \ 73 | 'child1 should get the wechaty from static value' 74 | 75 | 76 | def test_indenpendent_user_classes_instances() -> None: 77 | """doc""" 78 | 79 | user_class1 = get_user_class() 80 | user_class2 = get_user_class() 81 | 82 | user_class1.set_wechaty(EXPECTED_WECHATY1) 83 | user_class1.set_puppet(EXPECTED_PUPPET1) 84 | 85 | user_class2.set_wechaty(EXPECTED_WECHATY2) 86 | user_class2.set_puppet(EXPECTED_PUPPET2) 87 | 88 | user_class1_instance = user_class1() 89 | user_class2_instance = user_class2() 90 | 91 | assert user_class1_instance.wechaty == EXPECTED_WECHATY1, \ 92 | 'class1 instance should get wechaty1' 93 | assert user_class1_instance.puppet == EXPECTED_PUPPET1, \ 94 | 'class1 instance should get puppet1' 95 | 96 | assert user_class2_instance.wechaty == EXPECTED_WECHATY2, \ 97 | 'class2 instance should get wechaty2' 98 | assert user_class2_instance.puppet == EXPECTED_PUPPET2, \ 99 | 'class2 instance should get puppet2' 100 | 101 | 102 | def test_accessory_read_initialized_class( 103 | user_class: Type[Accessory], 104 | ) -> None: 105 | """ 106 | should read excepted value by reading static wechaty & puppet after init 107 | """ 108 | 109 | # reveal_type(accessory_class.wechaty) 110 | 111 | user_class.set_puppet(EXPECTED_PUPPET1) 112 | user_class.set_wechaty(EXPECTED_WECHATY1) 113 | 114 | accessory_instance = user_class() 115 | 116 | assert \ 117 | accessory_instance.puppet == EXPECTED_PUPPET1, \ 118 | 'should get puppet back by instance from static' 119 | assert \ 120 | accessory_instance.wechaty == EXPECTED_WECHATY1, \ 121 | 'should get wechaty back by instance from static' 122 | 123 | 124 | def test_accessory_read_uninitialized_instance( 125 | user_class: Type[Accessory], 126 | ) -> None: 127 | """should throw if read instance wechaty & puppet before initialization""" 128 | # pytest.skip('tbd') 129 | 130 | instance = user_class() 131 | 132 | with pytest.raises(AttributeError) as exception: 133 | assert instance.puppet 134 | assert str(exception.value) == 'puppet not set' 135 | 136 | with pytest.raises(AttributeError) as exception: 137 | assert instance.wechaty 138 | assert str(exception.value) == 'wechaty not set' 139 | 140 | 141 | def test_accessory_read_initialized_instance( 142 | user_class: Type[Accessory], 143 | ) -> None: 144 | """ 145 | should get expected value by reading instance wechaty & puppet after init 146 | """ 147 | 148 | user_class.set_puppet(EXPECTED_PUPPET1) 149 | user_class.set_wechaty(EXPECTED_WECHATY1) 150 | 151 | # reveal_type(accessory_class) 152 | accessory_instance = user_class() 153 | 154 | assert \ 155 | accessory_instance.puppet == EXPECTED_PUPPET1, \ 156 | 'should get puppet back' 157 | assert \ 158 | accessory_instance.wechaty == EXPECTED_WECHATY1, \ 159 | 'should get wechaty back' 160 | 161 | 162 | def test_accessory_set_twice( 163 | user_class: Type[Accessory], 164 | ) -> None: 165 | """doc""" 166 | user_class.set_puppet(EXPECTED_PUPPET1) 167 | 168 | with pytest.raises(Exception) as exception: 169 | user_class.set_puppet(EXPECTED_PUPPET1) 170 | assert str(exception.value) == 'can not set _puppet twice' 171 | 172 | user_class.set_wechaty(EXPECTED_WECHATY1) 173 | with pytest.raises(Exception) as exception: 174 | user_class.set_wechaty(EXPECTED_WECHATY1) 175 | assert str(exception.value) == 'can not set _wechaty twice' 176 | 177 | 178 | def test_accessory_classmethod_access_puppet() -> None: 179 | """ 180 | docstring 181 | """ 182 | user_class1 = get_user_class() 183 | user_class2 = get_user_class() 184 | 185 | user_class1.set_puppet(EXPECTED_PUPPET1) 186 | user_class2.set_puppet(EXPECTED_PUPPET2) 187 | 188 | assert user_class1.get_puppet() == EXPECTED_PUPPET1, \ 189 | 'user_class1 should get the puppet from static value' 190 | 191 | assert user_class2.get_puppet() == EXPECTED_PUPPET2, \ 192 | 'user_class2 should get the puppet from static value' 193 | 194 | assert user_class1.get_puppet() != user_class2.get_puppet(), \ 195 | 'user_class1 & user_class2 get_puppet() should be different' 196 | 197 | 198 | def test_accessory_classmethod_access_wechaty() -> None: 199 | """ 200 | docstring 201 | """ 202 | user_class1 = get_user_class() 203 | user_class2 = get_user_class() 204 | 205 | user_class1.set_wechaty(EXPECTED_WECHATY1) 206 | user_class2.set_wechaty(EXPECTED_WECHATY2) 207 | 208 | assert user_class1.get_wechaty() == EXPECTED_WECHATY1, \ 209 | 'user_class1 should get the wechaty from static value' 210 | 211 | assert user_class2.get_wechaty() == EXPECTED_WECHATY2, \ 212 | 'user_class2 should get the puppet from static value' 213 | 214 | assert user_class1.get_wechaty() != user_class2.get_wechaty(), \ 215 | 'user_class1 & user_class2 get_wechaty() should be different' 216 | 217 | 218 | def test_payload_pickle(): 219 | """test save/load pickle payload""" 220 | with tempfile.TemporaryDirectory() as tempdir: 221 | contact_payload = ContactPayload( 222 | id=str(uuid4()), 223 | name="fake-name" 224 | ) 225 | cache_path = os.path.join(tempdir, 'contact.pkl') 226 | save_pickle_data(contact_payload, cache_path) 227 | 228 | payload = load_pickle_data(cache_path) 229 | assert payload.id == contact_payload.id 230 | 231 | # save list 232 | save_pickle_data([contact_payload], cache_path) 233 | payloads = load_pickle_data(cache_path) 234 | assert len(payloads) == 1 235 | assert payloads[0].id == contact_payload.id 236 | -------------------------------------------------------------------------------- /tests/config_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | config unit test 3 | """ 4 | from typing import ( 5 | Any, 6 | # Dict, 7 | Iterable, 8 | ) 9 | 10 | import pytest 11 | 12 | from wechaty_puppet import get_logger 13 | 14 | # pylint: disable=C0103 15 | log = get_logger('ConfigTest') 16 | 17 | # pylint: disable=redefined-outer-name 18 | 19 | 20 | # https://stackoverflow.com/a/57015304/1123955 21 | @pytest.fixture(name='data', scope='module') 22 | def fixture_data() -> Iterable[str]: 23 | """ doc """ 24 | yield 'test' 25 | 26 | 27 | def test_config( 28 | data: Any, 29 | ) -> None: 30 | """ 31 | Unit Test for config function 32 | """ 33 | print(data) 34 | 35 | assert data == 'test', 'data should equals test' 36 | 37 | 38 | def test_get_logger() -> None: 39 | """test""" 40 | assert get_logger, 'log should exist' 41 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import sys 3 | from os.path import abspath, dirname, join 4 | 5 | from typing import Dict, List, MutableMapping, Optional, Tuple 6 | from uuid import uuid4 7 | import pytest 8 | from wechaty_grpc.wechaty.puppet import MessageType 9 | from wechaty_puppet.puppet import Puppet 10 | from wechaty_puppet.schemas.message import MessageQueryFilter 11 | from wechaty_puppet.schemas.types import ( 12 | MessagePayload, 13 | RoomPayload, 14 | ContactPayload, 15 | RoomMemberPayload, 16 | ) 17 | from wechaty_puppet.schemas.puppet import PuppetOptions 18 | from wechaty.wechaty import Wechaty, WechatyOptions # noqa 19 | from wechaty.fake_puppet import FakePuppet 20 | 21 | 22 | @pytest.fixture 23 | async def test_bot() -> Wechaty: 24 | """Initialize a Wechaty instance and return it""" 25 | puppet = FakePuppet(options=PuppetOptions()) 26 | puppet.add_fake_contact(ContactPayload("wechaty_user", name="Wechaty User")) 27 | puppet.add_fake_contact(ContactPayload("fake_user", name="Fake User")) 28 | puppet.add_fake_contact(ContactPayload("test_user", name="Test User")) 29 | puppet.add_fake_room( 30 | RoomPayload( 31 | id="test_room", 32 | topic="test_room", 33 | owner_id="wechaty_user", 34 | member_ids=["wechaty_user", "fake_user", "test_user"], 35 | ) 36 | ) 37 | puppet.add_fake_room( 38 | RoomPayload( 39 | id="fake_room", 40 | topic="fake_room", 41 | owner_id="wechaty_user", 42 | member_ids=["wechaty_user", "fake_user", "test_user"], 43 | ) 44 | ) 45 | puppet.add_fake_room_members( 46 | "fake_room", 47 | [ 48 | RoomMemberPayload("wechaty_user"), 49 | RoomMemberPayload("fake_user", room_alias="Fake Alias"), 50 | RoomMemberPayload("test_user") 51 | ] 52 | ) 53 | puppet.add_fake_message( 54 | MessagePayload("no_mention", text="foo bar asd", type=MessageType.MESSAGE_TYPE_TEXT) 55 | ) 56 | puppet.add_fake_message( 57 | MessagePayload( 58 | "room_no_mention", 59 | text="beep", 60 | room_id="fake_room", 61 | type=MessageType.MESSAGE_TYPE_TEXT, 62 | ) 63 | ) 64 | puppet.add_fake_message( 65 | MessagePayload( 66 | "room_with_mentions", 67 | text="@Wechaty User @Test User test message asd", 68 | room_id="fake_room", 69 | type=MessageType.MESSAGE_TYPE_TEXT, 70 | mention_ids=["wechaty_user", "test_user"], 71 | ) 72 | ) 73 | puppet.add_fake_message( 74 | MessagePayload( 75 | "room_with_mentions_and_alias", 76 | text="123123 @Wechaty User @Test User @Fake Alias kkasd", 77 | room_id="fake_room", 78 | type=MessageType.MESSAGE_TYPE_TEXT, 79 | mention_ids=["wechaty_user", "test_user", "fake_user"], 80 | ) 81 | ) 82 | puppet.add_fake_message( 83 | MessagePayload( 84 | "room_with_mentions_and_alias_mismatched", 85 | text="123123@Wechaty User @Test User @Fake User beep", 86 | room_id="fake_room", 87 | type=MessageType.MESSAGE_TYPE_TEXT, 88 | mention_ids=["wechaty_user", "test_user", "fake_user"], 89 | ) 90 | ) 91 | puppet.add_fake_message( 92 | MessagePayload( 93 | "room_with_text_mentions", 94 | text="@Wechaty User @Test User @Fake Alias beep!!", 95 | room_id="fake_room", 96 | type=MessageType.MESSAGE_TYPE_TEXT, 97 | ) 98 | ) 99 | 100 | bot = Wechaty(WechatyOptions(puppet=puppet)) 101 | await bot.init_puppet() 102 | return bot 103 | -------------------------------------------------------------------------------- /tests/plugin_test.py: -------------------------------------------------------------------------------- 1 | """unittest for plugin""" 2 | import json 3 | import os 4 | import tempfile 5 | import unittest 6 | from wechaty import Wechaty, WechatyOptions 7 | from wechaty.plugin import WechatyPlugin 8 | from wechaty.utils.data_util import WechatySetting 9 | from wechaty.fake_puppet import FakePuppet 10 | 11 | 12 | def test_setting(): 13 | with tempfile.TemporaryDirectory() as cache_dir: 14 | os.environ['CACHE_DIR'] = cache_dir 15 | plugin = WechatyPlugin() 16 | 17 | plugin.setting['unk'] = 11 18 | 19 | assert 'count' not in plugin.setting 20 | plugin.setting['count'] = 20 21 | 22 | # load the setting file 23 | assert os.path.exists(plugin.setting_file) 24 | 25 | with open(plugin.setting_file, 'r', encoding='utf-8') as f: 26 | data = json.load(f) 27 | 28 | assert data['unk'] == 11 29 | assert data['count'] == 20 30 | 31 | 32 | class TestWechatySetting(unittest.TestCase): 33 | 34 | def setUp(self) -> None: 35 | self.tempdir = tempfile.TemporaryDirectory() 36 | 37 | def tearDown(self) -> None: 38 | self.tempdir.cleanup() 39 | 40 | 41 | def test_simple_init(self): 42 | setting_file = os.path.join(self.tempdir.name, 'simple_setting.json') 43 | wechaty_setting: WechatySetting = WechatySetting(setting_file) 44 | 45 | assert os.path.exists(setting_file) 46 | 47 | wechaty_setting['a'] = 'a' 48 | assert 'a' in wechaty_setting.read_setting() 49 | assert wechaty_setting.read_setting()['a'] == 'a' 50 | 51 | assert 'b' not in wechaty_setting 52 | 53 | assert wechaty_setting.get("b", "b") == "b" 54 | 55 | wechaty_setting.save_setting({"c": "c"}) 56 | assert 'a' not in wechaty_setting 57 | assert 'c' in wechaty_setting 58 | 59 | def test_sub_setting(self): 60 | setting_file = os.path.join(self.tempdir.name, "sub", 'simple_setting.json') 61 | wechaty_setting: WechatySetting = WechatySetting(setting_file) 62 | 63 | assert os.path.exists(setting_file) 64 | 65 | wechaty_setting['a'] = 'a' 66 | assert 'a' in wechaty_setting.read_setting() 67 | assert wechaty_setting.read_setting()['a'] == 'a' 68 | 69 | assert 'b' not in wechaty_setting 70 | 71 | assert wechaty_setting.get("b", "b") == "b" 72 | 73 | wechaty_setting.save_setting({"c": "c"}) 74 | assert 'a' not in wechaty_setting 75 | assert 'c' in wechaty_setting 76 | 77 | 78 | async def test_finder(self): 79 | fake_puppet = FakePuppet() 80 | bot = Wechaty(options=WechatyOptions(puppet=fake_puppet)) 81 | 82 | contact_id = fake_puppet.add_random_fake_contact() 83 | 84 | contact_payload = await fake_puppet.contact_payload(contact_id) 85 | from wechaty_plugin_contrib.finders.contact_finder import ContactFinder 86 | finder = ContactFinder( 87 | contact_id 88 | ) 89 | contacts = await finder.match(bot) 90 | assert len(contacts) == 1 91 | 92 | contact = contacts[0] 93 | await contact.ready() 94 | 95 | assert contact.payload.name == contact_payload.name 96 | -------------------------------------------------------------------------------- /tests/room_test.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | import pytest 3 | from wechaty.wechaty import Wechaty # noqa 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_room_owner(test_bot: Wechaty) -> None: 8 | owner = await test_bot.Room("fake_room").owner() 9 | await owner.ready() 10 | assert owner.contact_id == "wechaty_user" 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test_room_topic(test_bot: Wechaty) -> None: 15 | topic = await test_bot.Room("fake_room").topic() 16 | assert topic == "fake_room" 17 | 18 | 19 | @pytest.mark.parametrize( 20 | ("room_name", "res"), [("test", "test_room"), ("fake", "fake_room"), ("wechaty", None)] 21 | ) 22 | @pytest.mark.asyncio 23 | async def test_room_find(test_bot: Wechaty, room_name: str, res: Union[str, None]) -> None: 24 | room = await test_bot.Room.find(room_name) 25 | name = room.room_id if room else None 26 | assert name == res 27 | 28 | 29 | @pytest.mark.parametrize( 30 | ("room_name", "res"), [("test", 1), ("fake", 1), ("room", 2), ("wechaty", 0)] 31 | ) 32 | @pytest.mark.asyncio 33 | async def test_room_findall(test_bot: Wechaty, room_name: str, res: int) -> None: 34 | room = await test_bot.Room.find_all(room_name) 35 | assert len(room) == res 36 | -------------------------------------------------------------------------------- /tests/smoke_testing_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit Test 3 | """ 4 | # pylint: disable=W0621 5 | 6 | # from typing import ( 7 | # # Any, 8 | # Iterable, 9 | # ) 10 | 11 | import pytest 12 | 13 | # from agent import Agent 14 | 15 | 16 | def test_smoke_testing() -> None: 17 | """ wechaty """ 18 | assert pytest, 'should True' 19 | -------------------------------------------------------------------------------- /tests/timestamp_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit test 3 | """ 4 | from wechaty.utils import timestamp_to_date 5 | 6 | 7 | def test_timestamp_with_millisecond_precision() -> None: 8 | timestamp = timestamp_to_date(1600849574736) 9 | assert timestamp is not None 10 | 11 | 12 | def test_timestamp_with_microsecond_precision() -> None: 13 | timestamp = timestamp_to_date(1600849792.367416) 14 | assert timestamp is not None 15 | -------------------------------------------------------------------------------- /tests/url_link_test.py: -------------------------------------------------------------------------------- 1 | """unit test for urllink""" 2 | from __future__ import annotations 3 | 4 | from unittest import TestCase 5 | from wechaty.user.url_link import UrlLink, GithubUrlLinkParser 6 | 7 | 8 | 9 | 10 | class TestUrlLink(TestCase): 11 | def setUp(self) -> None: 12 | self.sample_issue_link = 'https://github.com/wechaty/python-wechaty/issues/339' 13 | self.sample_issue_comment_link = 'https://github.com/wechaty/python-wechaty/issues/339' 14 | 15 | def test_create(): 16 | """unit test for creating""" 17 | # UrlLink.create( 18 | # url='https://github.com/wechaty/python-wechaty/issues/339', 19 | # title='title', 20 | # thumbnail_url='thu', 21 | # description='simple desc' 22 | # ) 23 | 24 | 25 | def test_github_payload(): 26 | parser = GithubUrlLinkParser() 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/user_message_test.py: -------------------------------------------------------------------------------- 1 | # import pytest 2 | # from wechaty.wechaty import Wechaty 3 | # import pdb 4 | 5 | 6 | # @pytest.mark.asyncio 7 | # async def test_mention_text_without_mentions(test_bot: Wechaty) -> None: 8 | # """Test extracting mention text from a message without mentions""" 9 | # msg = await test_bot.Message.find(message_id="no_mention") 10 | # await msg.ready() 11 | # text = await msg.mention_text() 12 | # assert text == 'foo bar asd' 13 | 14 | 15 | # @pytest.mark.asyncio 16 | # async def test_mention_text_without_mentions_in_room(test_bot: Wechaty) -> None: 17 | # """Test extracting mention text from a message without mentions""" 18 | # msg = await test_bot.Message.find(message_id="room_no_mention") 19 | # await msg.ready() 20 | # text = await msg.mention_text() 21 | # assert text == 'beep' 22 | 23 | 24 | # @pytest.mark.asyncio 25 | # async def test_mention_text_with_mentions_in_room(test_bot: Wechaty) -> None: 26 | # """Test extracting mention text from a message without mentions""" 27 | # msg = await test_bot.Message.find(message_id="room_with_mentions") 28 | # await msg.ready() 29 | # text = await msg.mention_text() 30 | # assert text == 'test message asd' 31 | 32 | 33 | # @pytest.mark.asyncio 34 | # async def test_mention_text_with_mentions_and_alias_in_room(test_bot: Wechaty) -> None: 35 | # """Test extracting mention text from a message without mentions""" 36 | # msg = await test_bot.Message.find(message_id="room_with_mentions_and_alias") 37 | # await msg.ready() 38 | # text = await msg.mention_text() 39 | # assert text == '123123 kkasd' 40 | 41 | 42 | # @pytest.mark.asyncio 43 | # async def test_mention_text_with_mentions_and_mismatched_alias(test_bot: Wechaty) -> None: 44 | # """Test extracting mention text from a message without mentions""" 45 | # msg = await test_bot.Message.find(message_id="room_with_mentions_and_alias_mismatched") 46 | # await msg.ready() 47 | # text = await msg.mention_text() 48 | # assert text == '123123@Fake User beep' 49 | 50 | 51 | # @pytest.mark.asyncio 52 | # async def test_mention_text_with_mentions_but_not_mention_data(test_bot: Wechaty) -> None: 53 | # """Test extracting mention text from a message without mentions""" 54 | # msg = await test_bot.Message.find(message_id="room_with_text_mentions") 55 | 56 | # await msg.ready() 57 | # text = await msg.mention_text() 58 | # assert text == '@Wechaty User @Test User @Fake Alias beep!!' 59 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | from wechaty.utils.async_helper import gather_with_concurrency 4 | from wechaty.utils.link import fetch_github_user_avatar_url, get_url_metadata 5 | from wechaty.utils.async_helper import SingleIdContainer 6 | from wechaty.utils.data_util import WechatySetting 7 | 8 | 9 | async def number_task(num: int): 10 | """ 11 | just return the original number 12 | """ 13 | return num 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_gather_tasks_with_n_concurrency(): 18 | tasks = [asyncio.create_task(number_task(i)) for i in range(1000)] 19 | sum_value = (0 + 999) * 1000 / 2 20 | result = await gather_with_concurrency(10, tasks) 21 | assert sum_value == sum(result), 'the final sum value is not correct' 22 | 23 | 24 | def test_fetch_metadata(): 25 | metadata = get_url_metadata('https://github.com/') 26 | assert 'title' in metadata 27 | assert 'image' in metadata 28 | 29 | 30 | def test_fetch_github_user_avatar(): 31 | avatar = fetch_github_user_avatar_url('wj-Mcat') 32 | assert avatar is not None 33 | assert 'avatars.githubusercontent.com' in avatar 34 | 35 | 36 | def test_single_id_container(): 37 | assert not SingleIdContainer.instance().exist('-1') 38 | assert SingleIdContainer.instance().exist('-1') 39 | 40 | for index in range(SingleIdContainer.instance().max_size): 41 | assert not SingleIdContainer.instance().exist(index) 42 | 43 | assert len(SingleIdContainer.instance().ids) == 0 44 | 45 | -------------------------------------------------------------------------------- /tests/version_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | version unit test 3 | """ 4 | # import pytest # type: ignore 5 | 6 | from wechaty.version import VERSION 7 | 8 | 9 | def test_version() -> None: 10 | """ 11 | Unit Test for version file 12 | """ 13 | 14 | assert VERSION == '0.0.0', 'version should be 0.0.0' 15 | -------------------------------------------------------------------------------- /tests/wechaty_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Unit test 3 | """ 4 | import pytest 5 | import os 6 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 7 | from wechaty_puppet import WechatyPuppetConfigurationError 8 | from wechaty import Wechaty, WechatyOptions 9 | 10 | def test_constructor(): 11 | # remove environment variables 12 | os.environ.pop("token", None) 13 | os.environ.pop("WECHATY_TOKEN", None) 14 | 15 | with pytest.raises(WechatyPuppetConfigurationError): 16 | bot = Wechaty() 17 | 18 | options = WechatyOptions(token='fake-token', endpoint='127.0.0.1:8080') 19 | bot = Wechaty(options=options) 20 | 21 | assert bot.puppet.options.token == 'fake-token' 22 | assert bot.puppet.options.end_point == '127.0.0.1:8080' 23 | 24 | 25 | def test_scheduler(): 26 | """test add scheduler in the options""" 27 | scheduler = AsyncIOScheduler() 28 | options = WechatyOptions( 29 | scheduler=scheduler 30 | ) 31 | bot = Wechaty(options=options) 32 | assert id(scheduler) == id(bot._plugin_manager.scheduler) 33 | -------------------------------------------------------------------------------- /wip/wechaty/__init__.py: -------------------------------------------------------------------------------- 1 | """doc""" 2 | from __future__ import annotations 3 | 4 | from typing import ( 5 | Optional, 6 | ) 7 | 8 | 9 | class Wechaty: 10 | """Working In Progress""" 11 | 12 | def __init__(self) -> None: 13 | """WIP Warning""" 14 | print(''' 15 | 16 | Dear Python Wechaty user, 17 | 18 | Thank you very much for using Python Wechaty! 19 | 20 | Wechaty is a RPA SDK for Wechat Individual Account that can help you create a chatbot in 6 lines of Python. 21 | Our GitHub is at https://github.com/wechaty/python-wechaty 22 | 23 | Today, we are under the process of translating the TypeScript Wechaty to Python Wechaty, please see issue #11 "From TypeScript to Python in Wechaty Way - Internal Modules" at https://github.com/wechaty/python-wechaty/issues/11 24 | 25 | To stay tuned, watch our repository now! 26 | 27 | Please also feel free to leave comments in our issues if you want to contribute, testers, coders, and doc writers are all welcome! 28 | 29 | Huan 30 | Author of Wechaty 31 | Mar 15, 2020 32 | 33 | ''') 34 | 35 | def version(self) -> str: 36 | """version""" 37 | type(self) 38 | return '0.0.0' 39 | 40 | def ding( 41 | self: Wechaty, 42 | data: Optional[str], 43 | ) -> None: 44 | """ding""" 45 | type(self) 46 | type(data) 47 | 48 | 49 | __all__ = [ 50 | 'Wechaty', 51 | ] 52 | --------------------------------------------------------------------------------