commit d5f55b5f9c5ccc2254d7a723b6ea12692116b35a Author: wehub-resource-sync Date: Mon Jul 13 12:40:42 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100755 index 0000000..ff01227 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,8 @@ +languages: + Ruby: false + JavaScript: true + PHP: false + Python: false +exclude_paths: + - "/doc/" + - "**/*.spec.ts" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8cbe01a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +dist +docs +node_modules +*.memory-card.json +t/ +t.* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..df3e785 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = 0 +trim_trailing_whitespace = false + +# 4 tab indentation +[Makefile] +indent_style = tab +indent_size = 4 diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..a267e49 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,8 @@ +const rules = { + 'no-use-before-define': 'off', +} + +module.exports = { + extends: '@chatie', + rules, +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..6c74374 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# +# https://help.github.com/articles/about-codeowners/ +# + +/ @wechaty/contributors diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..765185f --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1d05f49 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +# patreon: # Replace with a single Patreon username +open_collective: wechaty +# ko_fi: # Replace with a single Ko-fi username +# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +# custom: # Replace with a single custom sponsorship URL diff --git a/.github/ISSUE_TEMPLATE/wechaty-bug-report.md b/.github/ISSUE_TEMPLATE/wechaty-bug-report.md new file mode 100644 index 0000000..30720f7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/wechaty-bug-report.md @@ -0,0 +1,90 @@ +--- +name: Wechaty Bug Report +about: Create a bug report for a bug you found in wechaty +--- + +> Important๏ผšPlease file the issue follow the template, or we won't be able to help you to solve the problem. + +## 0. Report Issue Guide + +1. Please run the following command and check whether the problem has been fixed๏ผš + + ```sh + rm -rf package-lock.json + rm -rf node_modules + npm install + ``` + +2. Please search in [FAQ List](https://wechaty.js.org/faq) first, and make sure your problem has not been solved before. + +3. Please search in the issue first, and make sure your problem had not been reported before + +## 1. Versions + +> What is your wechaty version? + +Answer: + +> Which puppet are you using for wechaty? (puppeteer/padlocal/service...) + +Answer: + +> What is your wechaty-puppet-XXX๏ผˆpadchat/puppeteer/๏ผ‰ version? + +Answer: + +> What is your node version? (run `node --version`) + +Answer: + +> What os are you using + +Answer: + +## 2. Describe the bug + +> Give a clear and concise description of what the bug is. + +## 3. To Reproduce + +> This part is very important: if you can not provide any reproduce steps, then the problem will be very hard to be recognized. + +**[How to create a Minimal, Reproducible Example](https://stackoverflow.com/help/minimal-reproducible-example)** + +Steps to reproduce the behavior: + +1. run '...' +2. ... +3. ... + +## 4. Expected behavior + +> Give a clear and concise description of what you expected to happen. + +## 5. Actual behavior + +> If applicable, add screenshots to help explain your problem. But the log message must be in plain text instead of screenshot. + +## 6. Full Output Logs + +> Set env `WECHATY_LOG=silly` in order to set log level to silly, then we can get the full log (If you dosen't set log env, log level is info as default, we cannot get the full log) + +**We need full log instead of log screenshot or log fragments!** + +
+ +Show Logs + + +```shell +$ WECHATY_LOG=silly node yourbot.js + +-> Make sure to copy/paste the full (instead of only fragments) log message in text format (instead of image/screenshot) at here + +``` + +
+ +## 7. Additional context + +> Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/wechaty-feature-request.md b/.github/ISSUE_TEMPLATE/wechaty-feature-request.md new file mode 100644 index 0000000..fdc3657 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/wechaty-feature-request.md @@ -0,0 +1,19 @@ +--- +name: Wechaty Feature Request +about: Suggest a feature for wechaty + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. + +[enhancement] diff --git a/.github/ISSUE_TEMPLATE/wechaty-question.md b/.github/ISSUE_TEMPLATE/wechaty-question.md new file mode 100644 index 0000000..d35774a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/wechaty-question.md @@ -0,0 +1,11 @@ +--- +name: Wechaty Question +about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/wechaty + +--- + +๐Ÿšจ The issue tracker is not for questions ๐Ÿšจ + +If you have a question, please ask it on https://stackoverflow.com/questions/tagged/wechaty + +[question] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..e86168c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +## Checklist + +- [ ] Commit Messages follow the [Conventional Commits](https://conventionalcommits.org/) pattern + - A feature commit message is prefixed "feat:" + - A bugfix commit message is prefixed "fix:" +- [ ] Tests for the changes have been added +- [ ] CI has been passed. (GitHub actions all turns green) +- [ ] CLA has been signed + +## Description + +> please describe the changes that you are making, with the related issue number. diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..e132f3d --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,8 @@ +template: | + ## Whatโ€™s Changed + + $CHANGES + + ## Contributors + + $CONTRIBUTORS diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..8c8603c --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,26 @@ +name: Coverage + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: | + echo See: https://github.com/wechaty/wechaty/issues/2242 + # - uses: actions/checkout@v2 + # - uses: actions/setup-node@v2 + # - run: npm install + # - run: | + # npm test + # npm run coverage + # env: + # CI: true + # - name: Coveralls + # uses: coverallsapp/github-action@master + # with: + # github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..aa5e1c9 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,55 @@ +name: Docker + +on: [push, pull_request] +# push: +# branches: +# - main +# - 'v[0-9]+*' + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: reviewdog/action-shellcheck@v1 + - name: Install Bats + run: | + curl -sSL -o /tmp/bats_v0.4.0.tar.gz https://github.com/sstephenson/bats/archive/v0.4.0.tar.gz + tar -xf /tmp/bats_v0.4.0.tar.gz + sudo bats-0.4.0/install.sh /usr/local + - name: Build the Docker image + run: ./scripts/docker.sh build + - name: Test the Docker image + run: ./scripts/docker.sh test + + # https://www.prestonlamb.com/blog/creating-a-docker-image-with-github-actions + publish: + name: Publish + needs: [build] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v')) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + version: latest + + - name: Builder instance name + run: echo ${{ steps.buildx.outputs.name }} + - name: Available platforms + run: echo ${{ steps.buildx.outputs.platforms }} + + - name: Login to DockerHub Registry + run: echo ${{ secrets.DOCKER_PERSONAL_ACCESS_TOKEN }} | docker login -u zixia --password-stdin + - name: Deploy to Docker Hub + run: ./scripts/docker.sh deploy diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..629f215 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,36 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + strategy: + matrix: + os: + - macos-latest + - windows-latest + - ubuntu-latest + node-version: + - 16 + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: package.json + - run: npm install + - run: npm run build --if-present + - run: npm test + env: + CI: true diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml new file mode 100644 index 0000000..b174398 --- /dev/null +++ b/.github/workflows/npm.yml @@ -0,0 +1,88 @@ +name: NPM + +on: [push, pull_request] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + node-version: + - 22 + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + cache-dependency-path: package.json + + - name: Install Dependencies + run: | + node --version + npm --version + npm install + + - name: Test + run: npm test + + pack: + name: Pack + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: package.json + + - run: npm install + - name: Generate Package JSON + run: ./scripts/generate-package-json.sh + - run: ./scripts/npm-pack-testing.sh + env: + WECHATY_PUPPET_PADPLUS_TOKEN: ${{ secrets.WECHATY_PUPPET_PADPLUS_TOKEN }} + + publish: + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/v')) + name: Publish + needs: [build, pack] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org/ + cache: npm + cache-dependency-path: package.json + - run: npm install + - name: Generate Package JSON + run: ./scripts/generate-package-json.sh + - run: ./scripts/package-publish-config-tag.sh + - run: npm run dist + + - name: Check Branch + id: check-branch + run: | + if [[ ${{ github.ref }} =~ ^refs/heads/(main|v[0-9]+\.[0-9]+.*)$ ]]; then + echo ::set-output name=match::true + fi # See: https://stackoverflow.com/a/58869470/1123955 + - name: Is A Publish Branch + if: steps.check-branch.outputs.match == 'true' + run: | + NAME=$(npx pkg-jq -r .name) + VERSION=$(npx pkg-jq -r .version) + if npx version-exists "$NAME" "$VERSION" + then echo "$NAME@$VERSION exists on NPM, skipped." + else echo "Publishing..." && npm publish + fi + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Is Not A Publish Branch + if: steps.check-branch.outputs.match != 'true' + run: echo 'Not A Publish Branch' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47a5920 --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directory +node_modules + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +npm-shrinkwrap.json + +.*.swp + +# TAP Coverage +coverage +.nyc_output + +# Wechaty session file +demo.wechaty.json + +# Cloud 9 +.c9/ + +# Other staff +0 +t/ +t.* +.DS_Store +/dist/ +yarn.lock +.yarn/ +.yarn-cache/ +.v8flags.* + +# why? forgot... 2017-05-18 by huan +/tests/fixtures/docker/package.json + +tags +*.memory-card.json +*.wechaty.json +/package +/wechaty-*.*.*.tgz +*.bak +package-lock.json +.babel.json + +.idea +examples/.config +examples/.pki +.config/ +log +/*.jpg diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..32d3a98 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,15 @@ +{ + "default": true, + "no-trailing-punctuation": { + "punctuation": ".,;:!" + }, + "MD013": false, + "MD033": { + "allowed_elements": ["dl", "dt", "dd", "code", "var", "cite"] + }, + "first-line-h1": false, + "no-hard-tabs": true, + "no-trailing-spaces": { + "br_spaces": 2 + } +} diff --git a/.nycrc b/.nycrc new file mode 100644 index 0000000..618c053 --- /dev/null +++ b/.nycrc @@ -0,0 +1,3 @@ +{ + "reporter": ["lcov", "text-summary"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100755 index 0000000..2e43ee1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,99 @@ +{ + "typescript.tsdk": "./node_modules/typescript/lib", + + "editor.fontFamily": "Consolas, 'Courier New', monospace", + "editor.fontLigatures": true, + + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": [ + //following will be in italics (=Pacifico) + "comment", + // "entity.name.type.class", //class names + "keyword", //import, export, returnโ€ฆ + "support.class.builtin.js", //String, Number, Booleanโ€ฆ, this, super + "storage.modifier", //static keyword + "storage.type.class.js", //class keyword + "storage.type.function.js", // function keyword + "storage.type.js", // Variable declarations + "keyword.control.import.js", // Imports + "keyword.control.from.js", // From-Keyword + "entity.name.type.js", // new โ€ฆ Expression + "keyword.control.flow.js", // await + "keyword.control.conditional.js", // if + "keyword.control.loop.js", // for + "keyword.operator.new.js", // new + ], + "settings": { + "fontStyle": "italic", + }, + }, + { + "scope": [ + //following will be excluded from italics (My theme (Monokai dark) has some defaults I don't want to be in italics) + "invalid", + "keyword.operator", + "constant.numeric.css", + "keyword.other.unit.px.css", + "constant.numeric.decimal.js", + "constant.numeric.json", + "entity.name.type.class.js" + ], + "settings": { + "fontStyle": "", + }, + } + ] + }, + + "files.exclude": { + "dist/": true, + "doc/": true, + "node_modules/": true, + "package/": true, + }, + "alignment": { + "operatorPadding": "right", + "indentBase": "firstline", + "surroundSpace": { + "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. + "assignment": [1, 1], // The same as above. + "arrow": [1, 1], // The same as above. + "comment": 2, // Special how much space to add between the trailing comment and the code. + // If this value is negative, it means don't align the trailing comment. + } + }, + "editor.formatOnSave": false, + "python.pythonPath": "python3", + "eslint.validate": [ + "javascript", + "typescript", + ], + "cSpell.words": [ + "Gitter", + "Wechaty", + "appid", + "botie", + "huan", + "ioscat", + "ipad", + "lcov", + "lijiarui", + "padchat", + "padplus", + "padpro", + "pagepath", + "portfinder", + "qrcode", + "removee", + "ruirui", + "thumbnailurl", + "wechat", + "weixin", + "wxwork", + "zbeekman", + "zixia" + ], + "typescript.format.semicolons": "remove", +} diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..912d563 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,2 @@ +Huan https://github.com/huan +Rui https://github.com/lijiarui diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..325fb9e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1191 @@ + +# CHANGELOG + +## WECHATY CONTRIBUTORS +### Active Contributors + +1. @[lijiarui](https://github.com/lijiarui): [\#2118](https://github.com/wechaty/wechaty/pull/2118) [\#1876](https://github.com/wechaty/wechaty/pull/1876) [\#1875](https://github.com/wechaty/wechaty/pull/1875) [\#1859](https://github.com/wechaty/wechaty/pull/1859) [\#1702](https://github.com/wechaty/wechaty/pull/1702) [\#1700](https://github.com/wechaty/wechaty/pull/1700) [\#1692](https://github.com/wechaty/wechaty/pull/1692) [\#1633](https://github.com/wechaty/wechaty/pull/1633) [\#1631](https://github.com/wechaty/wechaty/pull/1631) [\#1615](https://github.com/wechaty/wechaty/pull/1615) [\#1614](https://github.com/wechaty/wechaty/pull/1614) [\#1533](https://github.com/wechaty/wechaty/pull/1533) [\#1514](https://github.com/wechaty/wechaty/pull/1514) [\#1510](https://github.com/wechaty/wechaty/pull/1510) [\#1502](https://github.com/wechaty/wechaty/pull/1502) [\#1498](https://github.com/wechaty/wechaty/pull/1498) [\#1497](https://github.com/wechaty/wechaty/pull/1497) [\#1486](https://github.com/wechaty/wechaty/pull/1486) [\#1482](https://github.com/wechaty/wechaty/pull/1482) [\#1481](https://github.com/wechaty/wechaty/pull/1481) [\#1477](https://github.com/wechaty/wechaty/pull/1477) [\#1408](https://github.com/wechaty/wechaty/pull/1408) [\#1407](https://github.com/wechaty/wechaty/pull/1407) [\#1405](https://github.com/wechaty/wechaty/pull/1405) [\#1402](https://github.com/wechaty/wechaty/pull/1402) [\#1375](https://github.com/wechaty/wechaty/pull/1375) [\#1374](https://github.com/wechaty/wechaty/pull/1374) [\#1373](https://github.com/wechaty/wechaty/pull/1373) [\#1352](https://github.com/wechaty/wechaty/pull/1352) [\#1351](https://github.com/wechaty/wechaty/pull/1351) [\#1348](https://github.com/wechaty/wechaty/pull/1348) [\#1347](https://github.com/wechaty/wechaty/pull/1347) [\#1344](https://github.com/wechaty/wechaty/pull/1344) [\#1341](https://github.com/wechaty/wechaty/pull/1341) [\#1338](https://github.com/wechaty/wechaty/pull/1338) [\#1333](https://github.com/wechaty/wechaty/pull/1333) [\#1331](https://github.com/wechaty/wechaty/pull/1331) [\#1325](https://github.com/wechaty/wechaty/pull/1325) [\#1116](https://github.com/wechaty/wechaty/pull/1116) [\#1086](https://github.com/wechaty/wechaty/pull/1086) [\#816](https://github.com/wechaty/wechaty/pull/816) [\#812](https://github.com/wechaty/wechaty/pull/812) [\#805](https://github.com/wechaty/wechaty/pull/805) [\#798](https://github.com/wechaty/wechaty/pull/798) [\#757](https://github.com/wechaty/wechaty/pull/757) [\#725](https://github.com/wechaty/wechaty/pull/725) [\#440](https://github.com/wechaty/wechaty/pull/440) [\#370](https://github.com/wechaty/wechaty/pull/370) [\#364](https://github.com/wechaty/wechaty/pull/364) [\#362](https://github.com/wechaty/wechaty/pull/362) [\#328](https://github.com/wechaty/wechaty/pull/328) [\#324](https://github.com/wechaty/wechaty/pull/324) [\#323](https://github.com/wechaty/wechaty/pull/323) [\#321](https://github.com/wechaty/wechaty/pull/321) [\#318](https://github.com/wechaty/wechaty/pull/318) [\#303](https://github.com/wechaty/wechaty/pull/303) [\#292](https://github.com/wechaty/wechaty/pull/292) [\#139](https://github.com/wechaty/wechaty/pull/139) [\#112](https://github.com/wechaty/wechaty/pull/112) [\#110](https://github.com/wechaty/wechaty/pull/110) [\#38](https://github.com/wechaty/wechaty/pull/38) +1. @[huan](https://github.com/huan): [\#2124](https://github.com/wechaty/wechaty/pull/2124) [\#2091](https://github.com/wechaty/wechaty/pull/2091) [\#2028](https://github.com/wechaty/wechaty/pull/2028) [\#1931](https://github.com/wechaty/wechaty/pull/1931) [\#1888](https://github.com/wechaty/wechaty/pull/1888) [\#1870](https://github.com/wechaty/wechaty/pull/1870) [\#1782](https://github.com/wechaty/wechaty/pull/1782) [\#1597](https://github.com/wechaty/wechaty/pull/1597) [\#1143](https://github.com/wechaty/wechaty/pull/1143) [\#1131](https://github.com/wechaty/wechaty/pull/1131) [\#1083](https://github.com/wechaty/wechaty/pull/1083) [\#1075](https://github.com/wechaty/wechaty/pull/1075) [\#1074](https://github.com/wechaty/wechaty/pull/1074) [\#1073](https://github.com/wechaty/wechaty/pull/1073) [\#1072](https://github.com/wechaty/wechaty/pull/1072) [\#1071](https://github.com/wechaty/wechaty/pull/1071) [\#860](https://github.com/wechaty/wechaty/pull/860) [\#854](https://github.com/wechaty/wechaty/pull/854) [\#841](https://github.com/wechaty/wechaty/pull/841) [\#831](https://github.com/wechaty/wechaty/pull/831) [\#810](https://github.com/wechaty/wechaty/pull/810) [\#469](https://github.com/wechaty/wechaty/pull/469) [\#462](https://github.com/wechaty/wechaty/pull/462) [\#455](https://github.com/wechaty/wechaty/pull/455) [\#449](https://github.com/wechaty/wechaty/pull/449) [\#396](https://github.com/wechaty/wechaty/pull/396) [\#351](https://github.com/wechaty/wechaty/pull/351) [\#317](https://github.com/wechaty/wechaty/pull/317) [\#316](https://github.com/wechaty/wechaty/pull/316) [\#315](https://github.com/wechaty/wechaty/pull/315) [\#314](https://github.com/wechaty/wechaty/pull/314) [\#313](https://github.com/wechaty/wechaty/pull/313) [\#312](https://github.com/wechaty/wechaty/pull/312) [\#311](https://github.com/wechaty/wechaty/pull/311) [\#168](https://github.com/wechaty/wechaty/pull/168) [\#158](https://github.com/wechaty/wechaty/pull/158) [\#149](https://github.com/wechaty/wechaty/pull/149) [\#146](https://github.com/wechaty/wechaty/pull/146) [\#143](https://github.com/wechaty/wechaty/pull/143) [\#142](https://github.com/wechaty/wechaty/pull/142) [\#141](https://github.com/wechaty/wechaty/pull/141) [\#25](https://github.com/wechaty/wechaty/pull/25) +1. @[windmemory](https://github.com/windmemory): [\#2138](https://github.com/wechaty/wechaty/pull/2138) [\#2078](https://github.com/wechaty/wechaty/pull/2078) [\#2074](https://github.com/wechaty/wechaty/pull/2074) [\#2067](https://github.com/wechaty/wechaty/pull/2067) [\#2051](https://github.com/wechaty/wechaty/pull/2051) [\#2050](https://github.com/wechaty/wechaty/pull/2050) [\#2048](https://github.com/wechaty/wechaty/pull/2048) [\#2043](https://github.com/wechaty/wechaty/pull/2043) [\#2039](https://github.com/wechaty/wechaty/pull/2039) [\#1832](https://github.com/wechaty/wechaty/pull/1832) [\#1770](https://github.com/wechaty/wechaty/pull/1770) [\#1735](https://github.com/wechaty/wechaty/pull/1735) [\#1729](https://github.com/wechaty/wechaty/pull/1729) [\#1662](https://github.com/wechaty/wechaty/pull/1662) [\#1660](https://github.com/wechaty/wechaty/pull/1660) [\#1643](https://github.com/wechaty/wechaty/pull/1643) [\#1630](https://github.com/wechaty/wechaty/pull/1630) [\#1577](https://github.com/wechaty/wechaty/pull/1577) [\#1571](https://github.com/wechaty/wechaty/pull/1571) [\#1557](https://github.com/wechaty/wechaty/pull/1557) [\#1550](https://github.com/wechaty/wechaty/pull/1550) [\#1538](https://github.com/wechaty/wechaty/pull/1538) [\#1526](https://github.com/wechaty/wechaty/pull/1526) [\#1503](https://github.com/wechaty/wechaty/pull/1503) [\#1457](https://github.com/wechaty/wechaty/pull/1457) +1. @[su-chang](https://github.com/su-chang): [\#2147](https://github.com/wechaty/wechaty/pull/2147) [\#2095](https://github.com/wechaty/wechaty/pull/2095) [\#1936](https://github.com/wechaty/wechaty/pull/1936) [\#1921](https://github.com/wechaty/wechaty/pull/1921) [\#1915](https://github.com/wechaty/wechaty/pull/1915) [\#1913](https://github.com/wechaty/wechaty/pull/1913) [\#1910](https://github.com/wechaty/wechaty/pull/1910) [\#1900](https://github.com/wechaty/wechaty/pull/1900) [\#1895](https://github.com/wechaty/wechaty/pull/1895) [\#1883](https://github.com/wechaty/wechaty/pull/1883) [\#1868](https://github.com/wechaty/wechaty/pull/1868) [\#1866](https://github.com/wechaty/wechaty/pull/1866) [\#1864](https://github.com/wechaty/wechaty/pull/1864) [\#1861](https://github.com/wechaty/wechaty/pull/1861) [\#1833](https://github.com/wechaty/wechaty/pull/1833) +1. @[mukaiu](https://github.com/mukaiu): [\#1089](https://github.com/wechaty/wechaty/pull/1089) [\#337](https://github.com/wechaty/wechaty/pull/337) [\#470](https://github.com/wechaty/wechaty/pull/470) [\#438](https://github.com/wechaty/wechaty/pull/438) [\#421](https://github.com/wechaty/wechaty/pull/421) [\#420](https://github.com/wechaty/wechaty/pull/420) [\#415](https://github.com/wechaty/wechaty/pull/415) [\#376](https://github.com/wechaty/wechaty/pull/376) +1. @[JasLin](https://github.com/JasLin): [\#404](https://github.com/wechaty/wechaty/pull/404) [\#358](https://github.com/wechaty/wechaty/pull/358) [\#105](https://github.com/wechaty/wechaty/pull/105) [\#100](https://github.com/wechaty/wechaty/pull/100) [\#78](https://github.com/wechaty/wechaty/pull/78) [\#76](https://github.com/wechaty/wechaty/pull/76) +1. @[xinbenlv](https://github.com/xinbenlv): [\#1814](https://github.com/wechaty/wechaty/pull/1814) [\#1017](https://github.com/wechaty/wechaty/pull/1017) [\#935](https://github.com/wechaty/wechaty/pull/935) [\#388](https://github.com/wechaty/wechaty/pull/388) [\#361](https://github.com/wechaty/wechaty/pull/361) +1. @[binsee](https://github.com/binsee): [\#844](https://github.com/wechaty/wechaty/pull/844) [\#811](https://github.com/wechaty/wechaty/pull/811) [\#771](https://github.com/wechaty/wechaty/pull/771) [\#744](https://github.com/wechaty/wechaty/pull/744) [\#727](https://github.com/wechaty/wechaty/pull/727) +1. @[kis87988](https://github.com/kis87988): [\#1908](https://github.com/wechaty/wechaty/pull/1908) [\#1623](https://github.com/wechaty/wechaty/pull/1623) [\#1607](https://github.com/wechaty/wechaty/pull/1607) [\#1570](https://github.com/wechaty/wechaty/pull/1570) +1. @[linyimin-bupt](https://github.com/linyimin-bupt): [\#1757](https://github.com/wechaty/wechaty/pull/1757) [\#1752](https://github.com/wechaty/wechaty/pull/1752) [\#1750](https://github.com/wechaty/wechaty/pull/1750) [\#1749](https://github.com/wechaty/wechaty/pull/1749) +1. @[TbhT](https://github.com/TbhT): [\#1713](https://github.com/wechaty/wechaty/pull/1713) [\#1583](https://github.com/wechaty/wechaty/pull/1583) [\#1582](https://github.com/wechaty/wechaty/pull/1582) +1. @[suntong](https://github.com/suntong): [\#1677](https://github.com/wechaty/wechaty/pull/1677) [\#1129](https://github.com/wechaty/wechaty/pull/1129) [\#1123](https://github.com/wechaty/wechaty/pull/1123) +1. @[Gcaufy](https://github.com/Gcaufy): [\#1625](https://github.com/wechaty/wechaty/pull/1625) [\#1620](https://github.com/wechaty/wechaty/pull/1620) [\#310](https://github.com/wechaty/wechaty/pull/310) +1. @[SilentQianyi](https://github.com/SilentQianyi): [\#1891](https://github.com/wechaty/wechaty/pull/1891) [\#1886](https://github.com/wechaty/wechaty/pull/1886) +1. @[LinuxSuRen](https://github.com/LinuxSuRen): [\#1838](https://github.com/wechaty/wechaty/pull/1838) [\#1836](https://github.com/wechaty/wechaty/pull/1836) + +### Contributors + +1. @[profthecopyright](https://github.com/profthecopyright): [\#2104](https://github.com/wechaty/wechaty/pull/2104) +1. @[yesxin](https://github.com/yesxin): [\#2079](https://github.com/wechaty/wechaty/pull/2079) +1. @[satouriko](https://github.com/satouriko): [\#1904](https://github.com/wechaty/wechaty/pull/1904) +1. @[LanceZhu](https://github.com/LanceZhu): [\#1854](https://github.com/wechaty/wechaty/pull/1854) +1. @[zhaoic](https://github.com/zhaoic): [\#1822](https://github.com/wechaty/wechaty/pull/1822) +1. @[coderwhocode](https://github.com/coderwhocode): [\#1819](https://github.com/wechaty/wechaty/pull/1819) +1. @[gengchen528](https://github.com/gengchen528): [\#1818](https://github.com/wechaty/wechaty/pull/1818) +1. @[monkeywithacupcake](https://github.com/monkeywithacupcake): [\#1759](https://github.com/wechaty/wechaty/pull/1759) +1. @[lhr0909](https://github.com/lhr0909): [\#1666](https://github.com/wechaty/wechaty/pull/1666) +1. @[jzj1993](https://github.com/jzj1993): [\#1661](https://github.com/wechaty/wechaty/pull/1661) +1. @[bitwater](https://github.com/bitwater): [\#1532](https://github.com/wechaty/wechaty/pull/1532) +1. @[IdiosApps](https://github.com/IdiosApps): [\#1087](https://github.com/wechaty/wechaty/pull/1087) +1. @[hiwanz](https://github.com/hiwanz): [\#1036](https://github.com/wechaty/wechaty/pull/1036) +1. @[htoooth](https://github.com/htoooth): [\#1014](https://github.com/wechaty/wechaty/pull/1014) +1. @[zhenyong](https://github.com/zhenyong): [\#770](https://github.com/wechaty/wechaty/pull/770) +1. @[xjchengo](https://github.com/xjchengo): [\#416](https://github.com/wechaty/wechaty/pull/416) +1. @[ax4](https://github.com/ax4): [\#380](https://github.com/wechaty/wechaty/pull/380) +1. @[cherry-geqi](https://github.com/cherry-geqi): [\#97](https://github.com/wechaty/wechaty/pull/97) + +# Changelog + +## [Unreleased](https://github.com/wechaty/wechaty/tree/HEAD) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.56...HEAD) + +**Implemented enhancements:** + +- Change default puppet for Wechaty: puppet-puppeteer -\> puppet-service [\#2127](https://github.com/wechaty/wechaty/issues/2127) +- Move HotImport related code to a Plugin [\#1962](https://github.com/wechaty/wechaty/issues/1962) + +**Closed issues:** + +- ็™ปๅฝ•ๆˆๅŠŸไน‹ๅŽ็›‘ๅฌๅˆฐ้”™่ฏฏๅ‘็”Ÿ๏ผŒๆœๅŠกไธ€็›ด้‡ๅฏ๏ผŒไฝฟ็”จๅŽ็ปญๅŠŸ่ƒฝๅผ‚ๅธธ [\#2170](https://github.com/wechaty/wechaty/issues/2170) +- Use Pure JavaScript gRPC [\#2159](https://github.com/wechaty/wechaty/issues/2159) +- why customer/list return empty list [\#2155](https://github.com/wechaty/wechaty/issues/2155) +- https://hub.juzibot.com/api/v1/customer/list?token= [\#2154](https://github.com/wechaty/wechaty/issues/2154) +- ่Žทๅ–ๅพฎไฟกๅท้”™่ฏฏ [\#2146](https://github.com/wechaty/wechaty/issues/2146) +- ๆทปๅŠ ็พคๆˆๅ‘˜ไธบๅฅฝๅ‹ [\#2142](https://github.com/wechaty/wechaty/issues/2142) +- How to set tag for a Contact [\#2139](https://github.com/wechaty/wechaty/issues/2139) +- ไธ‹่ฝฝๅผ•็”จ็š„ๅ›พ็‰‡ [\#2134](https://github.com/wechaty/wechaty/issues/2134) +- Login restricted after one login with wechaty [\#2133](https://github.com/wechaty/wechaty/issues/2133) +- PuppetPad Keep logging in and out [\#1488](https://github.com/wechaty/wechaty/issues/1488) + +**Merged pull requests:** + +- Add room member [\#2147](https://github.com/wechaty/wechaty/pull/2147) ([su-chang](https://github.com/su-chang)) +- pass custom server host to io server [\#2138](https://github.com/wechaty/wechaty/pull/2138) ([windmemory](https://github.com/windmemory)) + +## [v0.56](https://github.com/wechaty/wechaty/tree/v0.56) (2021-01-25) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.38...v0.56) + +**Implemented enhancements:** + +- Switch from `wechaty-puppet-hostie` to `wechaty-puppet-service` [\#2122](https://github.com/wechaty/wechaty/issues/2122) +- Add wechaty-puppet-padlocal to our puppet config [\#2102](https://github.com/wechaty/wechaty/issues/2102) +- Use `message.talker\(\)` to replace `message.from\(\)` [\#2094](https://github.com/wechaty/wechaty/issues/2094) +- Create a `looseInstanceOfClass` to check `FileBox` and `Puppet` instances [\#2090](https://github.com/wechaty/wechaty/issues/2090) +- Issue with batch operation on room loading process [\#2068](https://github.com/wechaty/wechaty/issues/2068) +- Support more properties on contact [\#2036](https://github.com/wechaty/wechaty/issues/2036) +- Refactoring Multi-instance Wechaty Design: Try to remove the Accessory class and related codes [\#2027](https://github.com/wechaty/wechaty/issues/2027) +- Use Typed-Emitter in Wechaty [\#2014](https://github.com/wechaty/wechaty/issues/2014) +- Support WECHATY\_HOSTIE\_PORT environment variable [\#1984](https://github.com/wechaty/wechaty/issues/1984) +- Wechaty v0.23 PadPro Testing, an enhanced pad puppet implementation! [\#1668](https://github.com/wechaty/wechaty/issues/1668) +- Using wechaty to start a wechatOA account [\#1016](https://github.com/wechaty/wechaty/issues/1016) + +**Fixed bugs:** + +- FileBox instance type checking problem [\#2035](https://github.com/wechaty/wechaty/issues/2035) +- Wechaty.off\(\) not work: can not remove listeners. [\#2019](https://github.com/wechaty/wechaty/issues/2019) +- friendship.contact\(\) will load Contact only, contact.ready\(\) is wanted. [\#1954](https://github.com/wechaty/wechaty/issues/1954) + +**Closed issues:** + +- ไธ่ƒฝ็™ปๅฝ•๏ผŒไนŸๆฒกๆ˜พ็คบไบŒ็ปด็ ๅฐฑ็›ดๆŽฅ้€€ๅ‡บ [\#2099](https://github.com/wechaty/wechaty/issues/2099) +- Can't login after scan the qrcode [\#2092](https://github.com/wechaty/wechaty/issues/2092) +- ็ฝ‘็ปœไธๅฅฝ็จ‹ๅบ่‡ชๅŠจ้‡ๅฏๅŽๆŽจ้€ๅผ‚ๅธธ [\#2088](https://github.com/wechaty/wechaty/issues/2088) +- Message.forward\(\) does not support returning a message [\#2073](https://github.com/wechaty/wechaty/issues/2073) +- bot.Friendship.add\(\) ๆ€ปๆ˜ฏๆŠฅ้”™ [\#2060](https://github.com/wechaty/wechaty/issues/2060) +- ๅœจไฝฟ็”จFileBox ๅ‘้€่ง†้ข‘ๆ—ถๅ€™ๅ‡บ็Žฐ้—ฎ้ข˜ [\#2059](https://github.com/wechaty/wechaty/issues/2059) +- The AWS Access Key Id you provided does not exist in our records. [\#2058](https://github.com/wechaty/wechaty/issues/2058) +- When setting up hostie token gateway, Account was locked after 5 getContactInfo call within a second. [\#2040](https://github.com/wechaty/wechaty/issues/2040) +- Support phone related operations [\#2037](https://github.com/wechaty/wechaty/issues/2037) +- BREAKING CHANGES: remove hotImport support from wechaty [\#1997](https://github.com/wechaty/wechaty/issues/1997) +- ERR GRPC\_GATEWAY GRPC SERVER ERROR [\#1996](https://github.com/wechaty/wechaty/issues/1996) +- ๅพฎไฟกๅฎ‰่ฃ… [\#1990](https://github.com/wechaty/wechaty/issues/1990) +- Cannot read property 'QQ' of undefined [\#1982](https://github.com/wechaty/wechaty/issues/1982) +- Need upgrade wechaty-puppet@0.25.7 version for wechaty [\#1980](https://github.com/wechaty/wechaty/issues/1980) +- The qrcode all the time timeout [\#1977](https://github.com/wechaty/wechaty/issues/1977) +- Upgrade wechaty-puppet-hostie@0.7.10 for fix the bug of friendship.accpet\(\) [\#1966](https://github.com/wechaty/wechaty/issues/1966) +- 23:42:53 SILL GrpcGateway callback type:ใ€invalid-tokenใ€‘ [\#1959](https://github.com/wechaty/wechaty/issues/1959) +- ๅ‡†ๅค‡ๆ”ฏๆŒไผไธšๅพฎไฟก็พคๅ— [\#1958](https://github.com/wechaty/wechaty/issues/1958) +- node-pre-gyp WARN [\#1953](https://github.com/wechaty/wechaty/issues/1953) +- How to filter official account numbers [\#1951](https://github.com/wechaty/wechaty/issues/1951) +- Update wechaty-puppet-hostie version for wechaty [\#1948](https://github.com/wechaty/wechaty/issues/1948) +- Is that you? [\#1942](https://github.com/wechaty/wechaty/issues/1942) +- ipad WeChat login will automatically log out, The returned QR code is invalid, you cannot log in again, you must restart [\#1940](https://github.com/wechaty/wechaty/issues/1940) +- New version release notes for wechaty 0.38 [\#1937](https://github.com/wechaty/wechaty/issues/1937) +- Can the receive the recall " room-leave". [\#1745](https://github.com/wechaty/wechaty/issues/1745) +- Refactor message.ts file to fit the requirement in CodeClimate [\#1663](https://github.com/wechaty/wechaty/issues/1663) +- Want to get information about sent-out message [\#1501](https://github.com/wechaty/wechaty/issues/1501) + +**Merged pull requests:** + +- rename wechaty-puppet-hostie -\> wechaty-puppet-service [\#2124](https://github.com/wechaty/wechaty/pull/2124) ([huan](https://github.com/huan)) +- Update README.md [\#2118](https://github.com/wechaty/wechaty/pull/2118) ([lijiarui](https://github.com/lijiarui)) +- Update room-invitation.ts [\#2104](https://github.com/wechaty/wechaty/pull/2104) ([profthecopyright](https://github.com/profthecopyright)) +- Update contact.ts [\#2095](https://github.com/wechaty/wechaty/pull/2095) ([su-chang](https://github.com/su-chang)) +- create looseInstanceOfClass \(\#2090\) [\#2091](https://github.com/wechaty/wechaty/pull/2091) ([huan](https://github.com/huan)) +- Update Dockerfile [\#2079](https://github.com/wechaty/wechaty/pull/2079) ([yesxin](https://github.com/yesxin)) +- Bump minor 49 [\#2078](https://github.com/wechaty/wechaty/pull/2078) ([windmemory](https://github.com/windmemory)) +- fix \#2073 [\#2074](https://github.com/wechaty/wechaty/pull/2074) ([windmemory](https://github.com/windmemory)) +- Split to trunks when making requests of room.findAll\(\) and room.ready\(\) [\#2067](https://github.com/wechaty/wechaty/pull/2067) ([windmemory](https://github.com/windmemory)) +- bump hostie version to be the stable one [\#2051](https://github.com/wechaty/wechaty/pull/2051) ([windmemory](https://github.com/windmemory)) +- 0.48.0 [\#2050](https://github.com/wechaty/wechaty/pull/2050) ([windmemory](https://github.com/windmemory)) +- feat: add more methods into contact class [\#2048](https://github.com/wechaty/wechaty/pull/2048) ([windmemory](https://github.com/windmemory)) +- feat: add scoped wxwork puppet into puppet config [\#2043](https://github.com/wechaty/wechaty/pull/2043) ([windmemory](https://github.com/windmemory)) +- add phone method in contact class [\#2039](https://github.com/wechaty/wechaty/pull/2039) ([windmemory](https://github.com/windmemory)) +- remove Accessories by wechatify user classes [\#2028](https://github.com/wechaty/wechaty/pull/2028) ([huan](https://github.com/huan)) + +## [v0.38](https://github.com/wechaty/wechaty/tree/v0.38) (2020-04-08) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.30...v0.38) + +**Implemented enhancements:** + +- BREAKING CHANGE: Remove `MessageUserQueryFilter` [\#1929](https://github.com/wechaty/wechaty/issues/1929) +- Simplify the Accessory class logic [\#1924](https://github.com/wechaty/wechaty/issues/1924) +- Update Docker Base Image form Ubuntu:18 to Ubuntu:19 [\#1920](https://github.com/wechaty/wechaty/issues/1920) +- wechaty dependency modules deep cleaning [\#1917](https://github.com/wechaty/wechaty/issues/1917) +- FileBox version incompatible across wechaty wechaty-puppet and wechaty-puppet-x [\#1914](https://github.com/wechaty/wechaty/issues/1914) +- Publish Wechaty Document Site to https://wechaty.js.org [\#1912](https://github.com/wechaty/wechaty/issues/1912) +- Support Encode & Decode QR Code functions from file-box@0.10 [\#1907](https://github.com/wechaty/wechaty/issues/1907) +- Design an new `Image` class for receive images with different sizes. [\#1871](https://github.com/wechaty/wechaty/issues/1871) +- Integrate Wechat with Matrix with the power of Wechaty [\#1737](https://github.com/wechaty/wechaty/issues/1737) + +**Fixed bugs:** + +- Can not create Image class. [\#1922](https://github.com/wechaty/wechaty/issues/1922) + +**Closed issues:** + +- ๅพฎไฟก้€€ๅ‡บ็™ปๅฝ•ๅŽ๏ผŒ้‡ๆ–ฐ็™ปๅฝ•ไธไบ†ipad [\#1839](https://github.com/wechaty/wechaty/issues/1839) +- ๅŸบไบŽpadproๅ่ฎฎไธ่ƒฝๅ‘้€ๅพฎไฟกๅฐ็จ‹ๅบ [\#1837](https://github.com/wechaty/wechaty/issues/1837) + +**Merged pull requests:** + +- 0.38.0 [\#1936](https://github.com/wechaty/wechaty/pull/1936) ([su-chang](https://github.com/su-chang)) +- Remove MessageUserQueryFilter [\#1931](https://github.com/wechaty/wechaty/pull/1931) ([huan](https://github.com/huan)) +- Image bug [\#1921](https://github.com/wechaty/wechaty/pull/1921) ([su-chang](https://github.com/su-chang)) +- File box [\#1915](https://github.com/wechaty/wechaty/pull/1915) ([su-chang](https://github.com/su-chang)) +- Support message.toImage\(\) method. [\#1913](https://github.com/wechaty/wechaty/pull/1913) ([su-chang](https://github.com/su-chang)) +- Support delay to accept room invitation [\#1910](https://github.com/wechaty/wechaty/pull/1910) ([su-chang](https://github.com/su-chang)) + +## [v0.30](https://github.com/wechaty/wechaty/tree/v0.30) (2020-02-08) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.28...v0.30) + +**Implemented enhancements:** + +- Support room.say`hi ${contact} ${num++}` [\#1899](https://github.com/wechaty/wechaty/issues/1899) +- Support new event named "message" on Room [\#1897](https://github.com/wechaty/wechaty/issues/1897) +- Support search new friends by phone number or user name. [\#1892](https://github.com/wechaty/wechaty/issues/1892) +- {Room,Contact}.qrcode\(\) should return QR Code Value instead of Image [\#1889](https://github.com/wechaty/wechaty/issues/1889) +- Implmented UrlLink.create\(url\) with Open Graph support [\#1887](https://github.com/wechaty/wechaty/issues/1887) +- ไธปๅŠจๆ’คๅ›žๆถˆๆฏ [\#1885](https://github.com/wechaty/wechaty/issues/1885) +- Implement Label in Wechaty [\#1856](https://github.com/wechaty/wechaty/issues/1856) +- Would like to have the timestamp on the room/friendship events [\#1829](https://github.com/wechaty/wechaty/issues/1829) +- Wechaty v0.26 iosBird Testing, an iOS hook puppet implementation! [\#1775](https://github.com/wechaty/wechaty/issues/1775) +- Missing example code [\#1756](https://github.com/wechaty/wechaty/issues/1756) +- BREAKING CHANGE v0.25 Room.say\(text: string, mention: Contact\[\]\) deprecated [\#1730](https://github.com/wechaty/wechaty/issues/1730) +- New Puppet: Ioscat \(iPhone Wechat App Hook\) [\#1528](https://github.com/wechaty/wechaty/issues/1528) + +**Fixed bugs:** + +- bot.logonoff\(\)ๆŠฅ้”™ใ€static puppet not found for Wechatyใ€‘ [\#1878](https://github.com/wechaty/wechaty/issues/1878) +- JSDoc publishing problem: missed the Room class [\#1872](https://github.com/wechaty/wechaty/issues/1872) +- Wechaty can not get room info when the bot has been removed from one room. [\#1834](https://github.com/wechaty/wechaty/issues/1834) +- bot.Contact.find\(\) error message misleading [\#1812](https://github.com/wechaty/wechaty/issues/1812) +- ๆœบๅ™จไบบไผš้š”ไธ€ๆฎตๆ—ถ้—ด่‡ชๅŠจ้€€ๅ‡บ [\#1810](https://github.com/wechaty/wechaty/issues/1810) + +**Closed issues:** + +- room.announce\(\)ไธญๅฝ“ๅ‚ๆ•ฐไธบ็ฉบๅญ—็ฌฆไธฒๆ—ถ, ่ฐƒ็”จ่Žทๅ–็พคๅ…ฌๅ‘Š่€Œไธๆ˜ฏ่ฎพ็ฝฎ็พคๅ…ฌๅ‘Šไธบ็ฉบ [\#1902](https://github.com/wechaty/wechaty/issues/1902) +- ๅปถๆœŸ้€š่ฟ‡ๅฅฝๅ‹ [\#1890](https://github.com/wechaty/wechaty/issues/1890) +- I want wechaty puppet padpro token to create a wechat bot [\#1882](https://github.com/wechaty/wechaty/issues/1882) +- msg.mentionSelf\(\)ๆ–นๆณ•ไธๅฏน๏ผŒๅง‹็ปˆ่ฟ”ๅ›žfalse [\#1877](https://github.com/wechaty/wechaty/issues/1877) +- ้œ€่ฆๅ–ๅˆฐContactidๆˆ–่€…ๅพฎไฟกๅท [\#1873](https://github.com/wechaty/wechaty/issues/1873) +- Function message `toContact\(\)` should to be implemented. [\#1855](https://github.com/wechaty/wechaty/issues/1855) +- install error [\#1853](https://github.com/wechaty/wechaty/issues/1853) +- wechaty-puppet can not install in electron [\#1851](https://github.com/wechaty/wechaty/issues/1851) +- wechaty-puppet-macpro alpha test [\#1846](https://github.com/wechaty/wechaty/issues/1846) +- Action required: Greenkeeper could not be activated ๐Ÿšจ [\#1781](https://github.com/wechaty/wechaty/issues/1781) +- ็ปๅธธๆŠฅไปฅไธ‹warning [\#1634](https://github.com/wechaty/wechaty/issues/1634) +- Doesn't work with UK Android account/device [\#1556](https://github.com/wechaty/wechaty/issues/1556) + +**Merged pull requests:** + +- fix: wechaty-puppet-dll temporary unavailable [\#1908](https://github.com/wechaty/wechaty/pull/1908) ([kis87988](https://github.com/kis87988)) +- docs: improve ding-dong-bot example [\#1904](https://github.com/wechaty/wechaty/pull/1904) ([satouriko](https://github.com/satouriko)) +- Add reason for logout event [\#1900](https://github.com/wechaty/wechaty/pull/1900) ([su-chang](https://github.com/su-chang)) +- Friend search [\#1895](https://github.com/wechaty/wechaty/pull/1895) ([su-chang](https://github.com/su-chang)) +- Delay friendship [\#1891](https://github.com/wechaty/wechaty/pull/1891) ([SilentQianyi](https://github.com/SilentQianyi)) +- Create url link [\#1888](https://github.com/wechaty/wechaty/pull/1888) ([huan](https://github.com/huan)) +- Recall msg [\#1886](https://github.com/wechaty/wechaty/pull/1886) ([SilentQianyi](https://github.com/SilentQianyi)) +- Change Mini Program payload in wechaty [\#1883](https://github.com/wechaty/wechaty/pull/1883) ([su-chang](https://github.com/su-chang)) +- Update README.md [\#1876](https://github.com/wechaty/wechaty/pull/1876) ([lijiarui](https://github.com/lijiarui)) +- add kaiyuanshe bot [\#1875](https://github.com/wechaty/wechaty/pull/1875) ([lijiarui](https://github.com/lijiarui)) +- chore\(package\): update @types/node to version 12.12.3 [\#1870](https://github.com/wechaty/wechaty/pull/1870) ([huan](https://github.com/huan)) +- Implement toContact\(\) method [\#1868](https://github.com/wechaty/wechaty/pull/1868) ([su-chang](https://github.com/su-chang)) +- feat: change say\(\) method response type from void to Message [\#1866](https://github.com/wechaty/wechaty/pull/1866) ([su-chang](https://github.com/su-chang)) +- Implement Label related methods [\#1864](https://github.com/wechaty/wechaty/pull/1864) ([su-chang](https://github.com/su-chang)) +- Update contact.ts [\#1861](https://github.com/wechaty/wechaty/pull/1861) ([su-chang](https://github.com/su-chang)) +- fix opencollective Sponsors style [\#1859](https://github.com/wechaty/wechaty/pull/1859) ([lijiarui](https://github.com/lijiarui)) +- fix typo [\#1854](https://github.com/wechaty/wechaty/pull/1854) ([LanceZhu](https://github.com/LanceZhu)) +- Update wechaty.ts [\#1833](https://github.com/wechaty/wechaty/pull/1833) ([su-chang](https://github.com/su-chang)) + +## [v0.28](https://github.com/wechaty/wechaty/tree/v0.28) (2019-09-02) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.26.0...v0.28) + +**Implemented enhancements:** + +- Room announcement is not working properly for some puppet implementation [\#1816](https://github.com/wechaty/wechaty/issues/1816) +- \[Feature\] Send Mini-Program [\#1806](https://github.com/wechaty/wechaty/issues/1806) +- Upgrade the repo to use Chatie DevOps toolset [\#1793](https://github.com/wechaty/wechaty/issues/1793) +- Easy install for wechaty-puppet-puppeteer [\#1792](https://github.com/wechaty/wechaty/issues/1792) +- ๆทปๅŠ ็”จ id ๅฏปๆ‰พRoom RoomQueryFilter [\#1785](https://github.com/wechaty/wechaty/issues/1785) +- Generate version.ts before publish to NPM [\#1780](https://github.com/wechaty/wechaty/issues/1780) +- Use `@chatie/tsconfig` as the tsconfig.json base configuration [\#1777](https://github.com/wechaty/wechaty/issues/1777) +- Could you please upgrade dependency for wechaty-puppet-puppeteer [\#1758](https://github.com/wechaty/wechaty/issues/1758) +- Missing comment example code in room.ts file [\#1751](https://github.com/wechaty/wechaty/issues/1751) + +**Fixed bugs:** + +- missing puppets in latest docker image [\#1820](https://github.com/wechaty/wechaty/issues/1820) +- Cannot `Contact.say\(imageAsFileBox\)` with puppeteer [\#1795](https://github.com/wechaty/wechaty/issues/1795) +- Document bug in README [\#1690](https://github.com/wechaty/wechaty/issues/1690) +- Fix comment, in replacing \#1784 [\#1814](https://github.com/wechaty/wechaty/pull/1814) ([xinbenlv](https://github.com/xinbenlv)) + +**Closed issues:** + +- ็™ปๅฝ•ไธไบ†ไธ€ไธช้”™่ฏฏๆŠฅๅ‘Š [\#1841](https://github.com/wechaty/wechaty/issues/1841) +- ๆƒณ่Žทๅ–ๅพฎไฟกๆถˆๆฏๅˆ—่กจ๏ผŒMessage.findAll\(\)่ฟ”ๅ›žไธบ็ฉบ๏ผŸ [\#1825](https://github.com/wechaty/wechaty/issues/1825) +- ็”จไบ†ไธ€ไธชๆœˆitchat็›ธๅฎ‰ๆ— ไบ‹๏ผŒ็”จไบ†2ๅฐๆ—ถwechaty... [\#1815](https://github.com/wechaty/wechaty/issues/1815) +- Wechaty process.exit\(1\) if token is not valid [\#1811](https://github.com/wechaty/wechaty/issues/1811) +- TSError: โจฏ Unable to compile TypeScript: [\#1791](https://github.com/wechaty/wechaty/issues/1791) +- padpro ๆ— ๆณ•่ฟžๆŽฅๆœๅŠกๅ™จ [\#1789](https://github.com/wechaty/wechaty/issues/1789) +- Action required: Greenkeeper could not be activated ๐Ÿšจ [\#1765](https://github.com/wechaty/wechaty/issues/1765) +- Mention api add @ automatically no matter it is already exists in the text [\#1718](https://github.com/wechaty/wechaty/issues/1718) + +**Merged pull requests:** + +- Update the ipad protocol environment [\#1838](https://github.com/wechaty/wechaty/pull/1838) ([LinuxSuRen](https://github.com/LinuxSuRen)) +- Update link for wechaty-puppet-padchat [\#1836](https://github.com/wechaty/wechaty/pull/1836) ([LinuxSuRen](https://github.com/LinuxSuRen)) +- Add timestamp to events [\#1832](https://github.com/wechaty/wechaty/pull/1832) ([windmemory](https://github.com/windmemory)) +- Send MiniProgram using PadPro [\#1822](https://github.com/wechaty/wechaty/pull/1822) ([zhaoic](https://github.com/zhaoic)) +- Add Readme - powered by wechaty example [\#1819](https://github.com/wechaty/wechaty/pull/1819) ([coderwhocode](https://github.com/coderwhocode)) +- ๆทปๅŠ ้กน็›ฎๆกˆไพ‹ๅพฎไฟกๅฐๅŠฉๆ‰‹ [\#1818](https://github.com/wechaty/wechaty/pull/1818) ([gengchen528](https://github.com/gengchen528)) +- chore\(package\): update dependencies [\#1782](https://github.com/wechaty/wechaty/pull/1782) ([huan](https://github.com/huan)) + +## [v0.26.0](https://github.com/wechaty/wechaty/tree/v0.26.0) (2019-05-11) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.22.4...v0.26.0) + +**Implemented enhancements:** + +- Recalled message design [\#1728](https://github.com/wechaty/wechaty/issues/1728) +- Message.toString format discussion [\#1676](https://github.com/wechaty/wechaty/issues/1676) +- Upgrade wechaty-puppet-padchat stable version [\#1642](https://github.com/wechaty/wechaty/issues/1642) +- New Puppet - wechaty-puppet-padpro [\#1629](https://github.com/wechaty/wechaty/issues/1629) +- room mention contact should using roomAlias [\#1604](https://github.com/wechaty/wechaty/issues/1604) +- for Pull Request: Disable puppet unit tests that requires a token [\#1580](https://github.com/wechaty/wechaty/issues/1580) +- Refresh contact self when updating self name or signature [\#1576](https://github.com/wechaty/wechaty/issues/1576) +- Pass mention list down to puppets [\#1560](https://github.com/wechaty/wechaty/issues/1560) +- Wechaty Puppet Ioscat Testing: iPhone\(iOS\) Wechat Hooking Support are comming! [\#1530](https://github.com/wechaty/wechaty/issues/1530) + +**Fixed bugs:** + +- Room.owner\(\) function is printing log in info level [\#1769](https://github.com/wechaty/wechaty/issues/1769) +- Build is failing caused by promisify missing arg3 [\#1761](https://github.com/wechaty/wechaty/issues/1761) +- Comment: Example code error [\#1746](https://github.com/wechaty/wechaty/issues/1746) +- Documentation็ฝ‘็ซ™ๆŒ‚ๆމไบ† [\#1739](https://github.com/wechaty/wechaty/issues/1739) +- ๆž„ๅปบdocker้•œๅƒๆŠฅ้”™ [\#1593](https://github.com/wechaty/wechaty/issues/1593) +- memberAll\(\) not running in docker after v0.21.27 [\#1573](https://github.com/wechaty/wechaty/issues/1573) +- ERR PuppetPuppeteer roomRawPayload [\#1547](https://github.com/wechaty/wechaty/issues/1547) + +**Closed issues:** + +- invalid processingToken:make the account logout.. [\#1766](https://github.com/wechaty/wechaty/issues/1766) +- Action required: Greenkeeper could not be activated ๐Ÿšจ [\#1764](https://github.com/wechaty/wechaty/issues/1764) +- Action required: Greenkeeper could not be activated ๐Ÿšจ [\#1763](https://github.com/wechaty/wechaty/issues/1763) +- \[ๅทฒ่งฃๅ†ณ\]ๆ— ๆณ•่‡ชๅŠจ้€š่ฟ‡ๅฅฝๅ‹่ฏทๆฑ‚ [\#1755](https://github.com/wechaty/wechaty/issues/1755) +- Missing comment example code in contact.ts file [\#1748](https://github.com/wechaty/wechaty/issues/1748) +- Is there a way to send message to a contact instead of making a reply using say\(\)? [\#1734](https://github.com/wechaty/wechaty/issues/1734) +- await message.mentionSelf\(\) is invalid [\#1725](https://github.com/wechaty/wechaty/issues/1725) +- message.mentionSelf\(\) [\#1724](https://github.com/wechaty/wechaty/issues/1724) +- wechaty-puppet-padpro ่Žทๅพ—็š„่ฏญ้Ÿณๆถˆๆฏๆ–‡ไปถๆ˜ฏslkๆ ผๅผ [\#1720](https://github.com/wechaty/wechaty/issues/1720) +- ๆœ‰ๆฒกๆœ‰้’ˆๅฏนไผไธšๅพฎไฟกApp็š„็™ปๅฝ•ๅ’Œๆ”ถๅ‘ๆถˆๆฏ็š„ๆ–นๆกˆๅ•Š๏ผŸ [\#1717](https://github.com/wechaty/wechaty/issues/1717) +- ๅ…ณไบŽwin7ๅฎ‰่ฃ… npm install wechaty-puppet-padpro ็š„้—ฎ้ข˜ [\#1716](https://github.com/wechaty/wechaty/issues/1716) +- ๅฐๆก”ๆœบๅ™จไบบๆŒบๅฅฝ็”จ๏ผŒๅธŒๆœ›่ฏฆ็ป†ๅฏนๆฏ”WeTools๏ผŒๅนถๅˆ†ไบซๅผ€ๅ‘Roadmap๏ผŒ่ฐข่ฐข [\#1712](https://github.com/wechaty/wechaty/issues/1712) +- npm start ๅˆฐ puppeteer@1.12.2 ๆŠฅ้”™ใ€‚ใ€‚ใ€‚ [\#1708](https://github.com/wechaty/wechaty/issues/1708) +- ่ฟ™ๆ˜ฏไป€ไนˆ [\#1703](https://github.com/wechaty/wechaty/issues/1703) +- ไฝฟ็”จ่‡ชๅŠจๆ‹‰ไบบๅ…ฅ็พคๅŠŸ่ƒฝ [\#1697](https://github.com/wechaty/wechaty/issues/1697) +- docker compose [\#1688](https://github.com/wechaty/wechaty/issues/1688) +- Errors occur sometimes when bot running [\#1675](https://github.com/wechaty/wechaty/issues/1675) +- msg.say ้‡ๅคๆ‰ง่กŒ [\#1674](https://github.com/wechaty/wechaty/issues/1674) +- WECHATY\_PUPPET=mock failed, TypeError \[ERR\_INVALID\_ARG\_TYPE\]: The "request" argument must be of type string [\#1673](https://github.com/wechaty/wechaty/issues/1673) +- Room.say method is not working [\#1665](https://github.com/wechaty/wechaty/issues/1665) +- ่ฟ™ๆ˜ฏๅŸบไบŽไป€ไนˆๅ่ฎฎ็š„web pc ipad [\#1664](https://github.com/wechaty/wechaty/issues/1664) +- Adjust data sync order when message event triggered [\#1659](https://github.com/wechaty/wechaty/issues/1659) +- ไนฑ็  [\#1656](https://github.com/wechaty/wechaty/issues/1656) +- ไนฑ็  [\#1655](https://github.com/wechaty/wechaty/issues/1655) +- ipad [\#1650](https://github.com/wechaty/wechaty/issues/1650) +- \[RFC\] custom id on Contact [\#1647](https://github.com/wechaty/wechaty/issues/1647) +- ่ฏท้—ฎ๏ผŒ ๆ€Žไนˆ่ฎฉๆœบๅ™จไบบ้€š่ฟ‡ๆ‰‹ๆœบๅทๆทปๅŠ ๅฅฝๅ‹ [\#1639](https://github.com/wechaty/wechaty/issues/1639) +- ่ฏท้—ฎwechatyๅฏไปฅๅ…ณๆމๆ—ฅๅฟ—่พ“ๅ‡บๅ—๏ผŸ [\#1638](https://github.com/wechaty/wechaty/issues/1638) +- ubuntu 18.04ไธ‹ๆ— ๆณ•ๆญฃๅธธๅฏๅŠจchrome [\#1637](https://github.com/wechaty/wechaty/issues/1637) +- ๅœจdockerๅฎนๅ™จไธญ๏ผŒnpm install wechaty๏ผŒ่ฟ่กŒmybotๆŠฅ้”™๏ผšFailed to launch chrome! [\#1636](https://github.com/wechaty/wechaty/issues/1636) +- Why I stuck here? [\#1624](https://github.com/wechaty/wechaty/issues/1624) +- can robot join room by qrcode๏ผŸ [\#1622](https://github.com/wechaty/wechaty/issues/1622) +- ่ฐƒ็”จ็งๅ‘/็พคๅ†…ๅ‘ๆถˆๆฏๆŽฅๅฃ๏ผŒemitๅ‡บ็š„ๆถˆๆฏไบ‹ไปถไธญๆถˆๆฏๆ—ถ้—ดๆœ‰่ฏฏ [\#1619](https://github.com/wechaty/wechaty/issues/1619) +- ่ฐƒ็”จๅ‘ๆถˆๆฏๆŽฅๅฃๅŽ๏ผŒemit ๅ‡บ็š„ๆถˆๆฏไบ‹ไปถๅฏนๅบ”็š„ๆถˆๆฏๆ—ถ้—ดไธๆญฃ็กฎ [\#1617](https://github.com/wechaty/wechaty/issues/1617) +- room.on\('join',function\(room, inviteeList, inviter\)\), can not get the inviteeList i.e. the new member [\#1613](https://github.com/wechaty/wechaty/issues/1613) +- cgggg [\#1610](https://github.com/wechaty/wechaty/issues/1610) +- c [\#1609](https://github.com/wechaty/wechaty/issues/1609) +- I use this function keyroom.add\(contact\),it doesn't work [\#1601](https://github.com/wechaty/wechaty/issues/1601) +- use "wechaty-puppet-padchat" cannot login successfully [\#1600](https://github.com/wechaty/wechaty/issues/1600) +- wechatyๆ— ๆณ•ๆŸฅๆ‰พๅˆฐ็พคๅ็งฐไธญๆœ‰~็š„็พค [\#1592](https://github.com/wechaty/wechaty/issues/1592) +- Sending messages with MessageType=1 is not supported [\#1588](https://github.com/wechaty/wechaty/issues/1588) +- ไธ่ƒฝ้€š่ฟ‡็พคๆˆๅ‘˜ๆทปๅŠ ๅฅฝๅ‹ [\#1578](https://github.com/wechaty/wechaty/issues/1578) +- \[RFC\] Launch broken when using wechaty-puppet-padchat [\#1575](https://github.com/wechaty/wechaty/issues/1575) +- Delete the `breaking change` tag in issue [\#1565](https://github.com/wechaty/wechaty/issues/1565) + +**Merged pull requests:** + +- Change room.owner\(\) log from info level to verbose level [\#1770](https://github.com/wechaty/wechaty/pull/1770) ([windmemory](https://github.com/windmemory)) +- Activating Open Collective [\#1759](https://github.com/wechaty/wechaty/pull/1759) ([monkeywithacupcake](https://github.com/monkeywithacupcake)) +- add example code [\#1757](https://github.com/wechaty/wechaty/pull/1757) ([linyimin-bupt](https://github.com/linyimin-bupt)) +- add send urlLink example code in room class [\#1752](https://github.com/wechaty/wechaty/pull/1752) ([linyimin-bupt](https://github.com/linyimin-bupt)) +- add send urlLink example code [\#1750](https://github.com/wechaty/wechaty/pull/1750) ([linyimin-bupt](https://github.com/linyimin-bupt)) +- fix comment error [\#1749](https://github.com/wechaty/wechaty/pull/1749) ([linyimin-bupt](https://github.com/linyimin-bupt)) +- Add `Message.recalled\(\)` [\#1735](https://github.com/wechaty/wechaty/pull/1735) ([windmemory](https://github.com/windmemory)) +- revise room.say\(\) mention function [\#1729](https://github.com/wechaty/wechaty/pull/1729) ([windmemory](https://github.com/windmemory)) +- fix document presentation bugs [\#1713](https://github.com/wechaty/wechaty/pull/1713) ([TbhT](https://github.com/TbhT)) +- code clean for issue template [\#1702](https://github.com/wechaty/wechaty/pull/1702) ([lijiarui](https://github.com/lijiarui)) +- add issue template [\#1700](https://github.com/wechaty/wechaty/pull/1700) ([lijiarui](https://github.com/lijiarui)) +- Change chatie org to wechaty org [\#1692](https://github.com/wechaty/wechaty/pull/1692) ([lijiarui](https://github.com/lijiarui)) +- Update the Message.toString format, close \#1676 [\#1677](https://github.com/wechaty/wechaty/pull/1677) ([suntong](https://github.com/suntong)) +- Fix room mention [\#1666](https://github.com/wechaty/wechaty/pull/1666) ([lhr0909](https://github.com/lhr0909)) +- Pass mention list down to puppet and use mentionIdList from puppet if possible [\#1662](https://github.com/wechaty/wechaty/pull/1662) ([windmemory](https://github.com/windmemory)) +- fix: Message.mention\(\) [\#1661](https://github.com/wechaty/wechaty/pull/1661) ([jzj1993](https://github.com/jzj1993)) +- Adjust data sync order for message ready [\#1660](https://github.com/wechaty/wechaty/pull/1660) ([windmemory](https://github.com/windmemory)) +- Upgrade wechaty-puppet-padchat version in puppet config [\#1643](https://github.com/wechaty/wechaty/pull/1643) ([windmemory](https://github.com/windmemory)) +- Docs [\#1633](https://github.com/wechaty/wechaty/pull/1633) ([lijiarui](https://github.com/lijiarui)) +- Docs [\#1631](https://github.com/wechaty/wechaty/pull/1631) ([lijiarui](https://github.com/lijiarui)) +- new Puppet - wechaty-puppet-padpro [\#1630](https://github.com/wechaty/wechaty/pull/1630) ([windmemory](https://github.com/windmemory)) +- docs: fixed typo [\#1625](https://github.com/wechaty/wechaty/pull/1625) ([Gcaufy](https://github.com/Gcaufy)) +- fix typo [\#1623](https://github.com/wechaty/wechaty/pull/1623) ([kis87988](https://github.com/kis87988)) +- docs: Fixed FriendshipType enum typo [\#1620](https://github.com/wechaty/wechaty/pull/1620) ([Gcaufy](https://github.com/Gcaufy)) +- README typo [\#1615](https://github.com/wechaty/wechaty/pull/1615) ([lijiarui](https://github.com/lijiarui)) +- Contribute [\#1614](https://github.com/wechaty/wechaty/pull/1614) ([lijiarui](https://github.com/lijiarui)) +- fix: room mention contact should using roomAlias https://github.com/Chatie/wechaty/issues/1604 [\#1607](https://github.com/wechaty/wechaty/pull/1607) ([kis87988](https://github.com/kis87988)) +- Greenkeeper/monorepo.babel7 7.0.1 [\#1597](https://github.com/wechaty/wechaty/pull/1597) ([huan](https://github.com/huan)) +- add the methods' doc link [\#1583](https://github.com/wechaty/wechaty/pull/1583) ([TbhT](https://github.com/TbhT)) +- update readme doc [\#1582](https://github.com/wechaty/wechaty/pull/1582) ([TbhT](https://github.com/TbhT)) +- sync contactSelf after updating bot info [\#1577](https://github.com/wechaty/wechaty/pull/1577) ([windmemory](https://github.com/windmemory)) + +## [v0.22.4](https://github.com/wechaty/wechaty/tree/v0.22.4) (2018-08-29) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.20.0...v0.22.4) + +**Implemented enhancements:** + +- Closing wechaty-puppet-padchat beta test [\#1572](https://github.com/wechaty/wechaty/issues/1572) +- Add API List to README [\#1566](https://github.com/wechaty/wechaty/issues/1566) +- Add support for send url rich media message [\#718](https://github.com/wechaty/wechaty/issues/718) +- contact.stranger\(\) still returns true after adding friends between bot and user [\#293](https://github.com/wechaty/wechaty/issues/293) +- be careful about the `unofficial client` check and report \(in browser\) [\#29](https://github.com/wechaty/wechaty/issues/29) + +**Fixed bugs:** + +- ReadError: Database is not open \[object Promise\] - FlashStoreSync was closed when need writing [\#1433](https://github.com/wechaty/wechaty/issues/1433) +- The latest docker get `ContactGetter` error when get wechat OA message [\#974](https://github.com/wechaty/wechaty/issues/974) +- "Chromium revision is not downloaded." [\#954](https://github.com/wechaty/wechaty/issues/954) +- Room.memberAll\(FilterString\) if FilterString are all with emojis [\#704](https://github.com/wechaty/wechaty/issues/704) +- `Room.find\(\)` can find the room when the bot is removed from the room [\#254](https://github.com/wechaty/wechaty/issues/254) + +**Closed issues:** + +- bot logined, but `scan` event still. [\#1567](https://github.com/wechaty/wechaty/issues/1567) +- Wechaty v0.20 & Puppet Padchat v0.14 Released. Padchat Alpha Testing Closed. [\#1296](https://github.com/wechaty/wechaty/issues/1296) +- Session closed when running for a long time [\#1052](https://github.com/wechaty/wechaty/issues/1052) + +**Merged pull requests:** + +- Bump wechaty-puppet-padchat version [\#1571](https://github.com/wechaty/wechaty/pull/1571) ([windmemory](https://github.com/windmemory)) +- Edit room doc [\#1570](https://github.com/wechaty/wechaty/pull/1570) ([kis87988](https://github.com/kis87988)) + +## [v0.20.0](https://github.com/wechaty/wechaty/tree/v0.20.0) (2018-08-18) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.18.0...v0.20.0) + +**Implemented enhancements:** + +- \[RFC\] Suggest to have more detailed error message when doing ContactSelf related operations [\#1549](https://github.com/wechaty/wechaty/issues/1549) +- feat: add receive and send link [\#1539](https://github.com/wechaty/wechaty/issues/1539) +- Feat: add `Message.find` to search in cache. [\#1534](https://github.com/wechaty/wechaty/issues/1534) +- Get Rid of the Dependences which requires build when `npm install` [\#1529](https://github.com/wechaty/wechaty/issues/1529) +- Puppet Padchat Not Supported Attachment File Type in Message. [\#1524](https://github.com/wechaty/wechaty/issues/1524) +- BREAKING CHANGE: v0.20 `Contact.alias` changed from Sync to Async [\#1500](https://github.com/wechaty/wechaty/issues/1500) +- Pre-install all official supported Puppets when build docker image [\#1493](https://github.com/wechaty/wechaty/issues/1493) +- ใ€Proposalใ€‘New 'room-invite' event [\#1492](https://github.com/wechaty/wechaty/issues/1492) +- Message type: mini program for Message.type\(\) [\#1444](https://github.com/wechaty/wechaty/issues/1444) +- Promote PuppetPadchat to SOLO NPM Module wechaty-puppet-padchat [\#1372](https://github.com/wechaty/wechaty/issues/1372) +- PuppetPadchat: support message type of `StatusNotify` with `op id` push update information [\#1310](https://github.com/wechaty/wechaty/issues/1310) +- Make tslint.json to be simple with `extends` standard. [\#1303](https://github.com/wechaty/wechaty/issues/1303) +- feat: PuppetPadchat supports get QrCode for userSelf\(can be scan by others for adding friend\) [\#1301](https://github.com/wechaty/wechaty/issues/1301) +- New Puppet: PuppetPadchat [\#1249](https://github.com/wechaty/wechaty/issues/1249) +- feat: add `Wechaty.unref\(\)` support to unref all the underlying resources. [\#1197](https://github.com/wechaty/wechaty/issues/1197) +- Disable HotImport in PRODUCTION Environment [\#907](https://github.com/wechaty/wechaty/issues/907) +- Reconstruction PuppeteerMessage Class [\#724](https://github.com/wechaty/wechaty/issues/724) +- \[New Puppet\] PuppetWechat4u - enable by `WECHATY\_PUPPET=wechat4u` [\#69](https://github.com/wechaty/wechaty/issues/69) + +**Fixed bugs:** + +- TypeError: Cannot read property 'type' of undefined at AnotherOriginalClass.type [\#1545](https://github.com/wechaty/wechaty/issues/1545) +- contact alias not sync after call contact.alias\(string\) to set alias [\#1517](https://github.com/wechaty/wechaty/issues/1517) +- Fix Puppet Memory Multiplex Problem with Multi-Instance Wechaty and Pre-Instanced Puppet [\#1516](https://github.com/wechaty/wechaty/issues/1516) +- Question: How to pre-set puppet module to be used by wechaty with docker [\#1478](https://github.com/wechaty/wechaty/issues/1478) +- JSDoc bugs [\#1475](https://github.com/wechaty/wechaty/issues/1475) +- PuppetPadChat:Contact.avatar\(\) Error [\#1473](https://github.com/wechaty/wechaty/issues/1473) +- PuppetWeb loses event listeners when resetting [\#1470](https://github.com/wechaty/wechaty/issues/1470) +- Check the puppet version to satisfy the Wechaty requirement [\#1453](https://github.com/wechaty/wechaty/issues/1453) +- Split the `xxxIdList` to chunks when map it to payload to prevent block the event loop [\#1450](https://github.com/wechaty/wechaty/issues/1450) +- `puppetPadchat.reset\(\)` should call `start\(\)` after `stop\(\)` [\#1385](https://github.com/wechaty/wechaty/issues/1385) +- cannot run `speech-to-text-bot` [\#1350](https://github.com/wechaty/wechaty/issues/1350) +- Error: Cannot stub non-existent own property [\#1247](https://github.com/wechaty/wechaty/issues/1247) +- How to run hot-import-bot example [\#1222](https://github.com/wechaty/wechaty/issues/1222) +- PuppetWebEvent onLogin: browser not fully loaded\(ttl=30\), retry later [\#970](https://github.com/wechaty/wechaty/issues/970) + +**Closed issues:** + +- appear bug when logout use padchat [\#1559](https://github.com/wechaty/wechaty/issues/1559) +- \[RFC\] Shall we clean up room information when we receive room events? [\#1552](https://github.com/wechaty/wechaty/issues/1552) +- Want to create UrlLink with control on all properties [\#1541](https://github.com/wechaty/wechaty/issues/1541) +- memory-card cannot save successfully [\#1537](https://github.com/wechaty/wechaty/issues/1537) +- Error: Cannot find module 'babel-plugin-transform-runtime' from '/bot' [\#1536](https://github.com/wechaty/wechaty/issues/1536) +- contact name not sync after call sync\(\) or restart docker \(RPC\) [\#1531](https://github.com/wechaty/wechaty/issues/1531) +- Why wechaty need express [\#1523](https://github.com/wechaty/wechaty/issues/1523) +- calling await bot.stop\(\) can't stop the bot [\#1519](https://github.com/wechaty/wechaty/issues/1519) +- @ ไบบๆฒกๆ็คบ [\#1504](https://github.com/wechaty/wechaty/issues/1504) +- Puppet Name use full npm name instead of alias: padchat =\> wechaty-puppet-padchat [\#1496](https://github.com/wechaty/wechaty/issues/1496) +- why friendship.contact\(\).name\(\) is "" [\#1490](https://github.com/wechaty/wechaty/issues/1490) +- why the padchat always restart\(login\)? [\#1485](https://github.com/wechaty/wechaty/issues/1485) +- sending a custom emotic led to an error on the server [\#1483](https://github.com/wechaty/wechaty/issues/1483) +- PuppetPadChat: The server always restart on pm2. [\#1472](https://github.com/wechaty/wechaty/issues/1472) +- PuppetPadchat Not response after run several hours [\#1443](https://github.com/wechaty/wechaty/issues/1443) +- PuppetPadchat: make contact has a function with real wechat ID [\#1423](https://github.com/wechaty/wechaty/issues/1423) +- Maybe we shouldn't syncContactsAndRooms per 3 hours [\#1414](https://github.com/wechaty/wechaty/issues/1414) +- PuppetPadchat: Cannot read property 'user\_name' of undefined [\#1392](https://github.com/wechaty/wechaty/issues/1392) +- I changed my operating system๏ผŒbut when run dev ,still output restarting [\#1382](https://github.com/wechaty/wechaty/issues/1382) +- Room-join event show ERR PuppetWebFirer fireRoomJoin\(\) not found\(yet\) error \(Bug\) [\#1169](https://github.com/wechaty/wechaty/issues/1169) +- `Room.findAll\(\)` get error when rooms more than 400 [\#1112](https://github.com/wechaty/wechaty/issues/1112) +- emit error event when listen the page has been closed [\#1110](https://github.com/wechaty/wechaty/issues/1110) + +**Merged pull requests:** + +- sync room when receive room events [\#1557](https://github.com/wechaty/wechaty/pull/1557) ([windmemory](https://github.com/windmemory)) +- better error message for contact self operations [\#1550](https://github.com/wechaty/wechaty/pull/1550) ([windmemory](https://github.com/windmemory)) +- add receive and send link prototype [\#1538](https://github.com/wechaty/wechaty/pull/1538) ([windmemory](https://github.com/windmemory)) +- add ready in jsdoc [\#1533](https://github.com/wechaty/wechaty/pull/1533) ([lijiarui](https://github.com/lijiarui)) +- add check isMentionMe method [\#1532](https://github.com/wechaty/wechaty/pull/1532) ([bitwater](https://github.com/bitwater)) +- Add method in ContactSelf to update name and signature [\#1526](https://github.com/wechaty/wechaty/pull/1526) ([windmemory](https://github.com/windmemory)) +- remove useless param type in example code [\#1514](https://github.com/wechaty/wechaty/pull/1514) ([lijiarui](https://github.com/lijiarui)) +- aad room invitation jsdoc [\#1510](https://github.com/wechaty/wechaty/pull/1510) ([lijiarui](https://github.com/lijiarui)) +- feat: \[Prototype\] adding new data-ready event [\#1503](https://github.com/wechaty/wechaty/pull/1503) ([windmemory](https://github.com/windmemory)) +- Contact get alias from sync to async [\#1502](https://github.com/wechaty/wechaty/pull/1502) ([lijiarui](https://github.com/lijiarui)) +- add Contact self class [\#1498](https://github.com/wechaty/wechaty/pull/1498) ([lijiarui](https://github.com/lijiarui)) +- remove a outdated file-box function [\#1497](https://github.com/wechaty/wechaty/pull/1497) ([lijiarui](https://github.com/lijiarui)) +- Add api to explain which belongs to web API, which belongs to padchat API, which belongsโ€ฆ [\#1486](https://github.com/wechaty/wechaty/pull/1486) ([lijiarui](https://github.com/lijiarui)) +- move wechaty-getting-started from lijiarui to chatie [\#1482](https://github.com/wechaty/wechaty/pull/1482) ([lijiarui](https://github.com/lijiarui)) +- file\(\) desperate, use toFileBox instead [\#1481](https://github.com/wechaty/wechaty/pull/1481) ([lijiarui](https://github.com/lijiarui)) +- fix \#1475 bug [\#1477](https://github.com/wechaty/wechaty/pull/1477) ([lijiarui](https://github.com/lijiarui)) + +## [v0.18.0](https://github.com/wechaty/wechaty/tree/v0.18.0) (2018-07-11) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.16.0...v0.18.0) + +**Implemented enhancements:** + +- PuppetPadchat Upgrade TODO Lists [\#1442](https://github.com/wechaty/wechaty/issues/1442) +- Dynamic install puppet implementations instead of pre-install [\#1437](https://github.com/wechaty/wechaty/issues/1437) +- Split PuppetWechat4u as a NPM module [\#1419](https://github.com/wechaty/wechaty/issues/1419) +- PuppetPadchat: The contact.star\(\) doesn't work [\#1413](https://github.com/wechaty/wechaty/issues/1413) +- Promote PuppetPuppeteer to SOLO NPM Module wechaty-puppet-puppeteer [\#1371](https://github.com/wechaty/wechaty/issues/1371) +- Promote Puppet to SOLO NPM Module wechaty-puppet [\#1370](https://github.com/wechaty/wechaty/issues/1370) +- New Puppet: PuppetMock for Testing & Starter [\#1177](https://github.com/wechaty/wechaty/issues/1177) + +**Fixed bugs:** + +- Cannot detect the the environment WECHATY\_PUPPET v0.17.118 [\#1456](https://github.com/wechaty/wechaty/issues/1456) +- PuppetPadchat Server logout and login cycle average 3-5mins [\#1446](https://github.com/wechaty/wechaty/issues/1446) +- WARN PuppetPuppeteer initWatchdogForPuppet\(\) dog.on\(reset\) last food:inited, timeout:120000 [\#1439](https://github.com/wechaty/wechaty/issues/1439) +- ts-node 7.0 breaking change: Skip `files` by default [\#1383](https://github.com/wechaty/wechaty/issues/1383) +- Can not find room after add member to the room [\#1380](https://github.com/wechaty/wechaty/issues/1380) +- PuppetPadchat: `friend` value of `ContactPayload` is undefined [\#1359](https://github.com/wechaty/wechaty/issues/1359) +- WXGetContact cannot get user\_name [\#1358](https://github.com/wechaty/wechaty/issues/1358) +- leveldown::Database::Close: Program terminated with signal SIGSEGV, Segmentation fault. [\#1355](https://github.com/wechaty/wechaty/issues/1355) + +**Closed issues:** + +- QR Code problem under screen/xterm [\#1455](https://github.com/wechaty/wechaty/issues/1455) +- How to get the room title in v0.17 wechaty? [\#1454](https://github.com/wechaty/wechaty/issues/1454) +- should not download chromium by default [\#1451](https://github.com/wechaty/wechaty/issues/1451) +- PuppetPadChat:Send media file. [\#1436](https://github.com/wechaty/wechaty/issues/1436) +- PuppetPadchat:My wechat accou can not login. [\#1416](https://github.com/wechaty/wechaty/issues/1416) +- It reports errors when the room delete one member. [\#1415](https://github.com/wechaty/wechaty/issues/1415) +- `cannot get user\_name from raw payload: {} \[object Promise\]` Error [\#1399](https://github.com/wechaty/wechaty/issues/1399) +- Where can I see the complete API documentation of puppet-padchat, such as createRoom and Moment [\#1391](https://github.com/wechaty/wechaty/issues/1391) +- still restart [\#1378](https://github.com/wechaty/wechaty/issues/1378) +- WARN PuppetPuppeteer [\#1376](https://github.com/wechaty/wechaty/issues/1376) +- m.say\(\) repeat many many times [\#1216](https://github.com/wechaty/wechaty/issues/1216) +- room.say mention is not work [\#1185](https://github.com/wechaty/wechaty/issues/1185) +- ่Žทๅ–ๆ‰€ๆœ‰็พค [\#1020](https://github.com/wechaty/wechaty/issues/1020) +- Any way to keep login for days? [\#988](https://github.com/wechaty/wechaty/issues/988) + +**Merged pull requests:** + +- update version of puppet-padchat [\#1457](https://github.com/wechaty/wechaty/pull/1457) ([windmemory](https://github.com/windmemory)) +- catch error when get undifined user\_name in room [\#1408](https://github.com/wechaty/wechaty/pull/1408) ([lijiarui](https://github.com/lijiarui)) +- add a more suitable time to sync Contact and Room [\#1407](https://github.com/wechaty/wechaty/pull/1407) ([lijiarui](https://github.com/lijiarui)) +- mock self bot when WXGetContact\(\) return null user\_name [\#1405](https://github.com/wechaty/wechaty/pull/1405) ([lijiarui](https://github.com/lijiarui)) +- sync contact and room per hour [\#1402](https://github.com/wechaty/wechaty/pull/1402) ([lijiarui](https://github.com/lijiarui)) + +## [v0.16.0](https://github.com/wechaty/wechaty/tree/v0.16.0) (2018-06-21) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.14.0...v0.16.0) + +**Implemented enhancements:** + +- Unable to start multiple instances with padchat puppet [\#1367](https://github.com/wechaty/wechaty/issues/1367) +- Prevent the Floating Promise in the Async/Await Code [\#1346](https://github.com/wechaty/wechaty/issues/1346) +- BREAKING CHANGES v0.16: FriendRequest class will be replaced with Friendship [\#1312](https://github.com/wechaty/wechaty/issues/1312) +- feat: PuppetPadchat can set avatar for userself support. [\#1298](https://github.com/wechaty/wechaty/issues/1298) +- BREAKING CHANGE: v0.16 `Room.topic\(\)` change from Sycn to Async [\#1295](https://github.com/wechaty/wechaty/issues/1295) +- BREAKING CHANGE: v0.16 `Room.alias\(contact\)` change from Sycn to Async [\#1293](https://github.com/wechaty/wechaty/issues/1293) +- BREAKING CHANGE: v0.16 `Room.memberList\(\)` change from Sycn to Async [\#1290](https://github.com/wechaty/wechaty/issues/1290) +- BREAKING CHANGE: v0.16 `Room.has\(contact\)` change from Sycn to Async [\#1289](https://github.com/wechaty/wechaty/issues/1289) +- BREAKING CHANGE: v0.16 `scan` event args will be different! [\#1262](https://github.com/wechaty/wechaty/issues/1262) +- BREAKING CHANGE: v0.16 `Message.mention\(\)` change from `sync` to `async` [\#1259](https://github.com/wechaty/wechaty/issues/1259) +- BREAKING CHANGES: v0.16 `Room.member\(\)` from `sync` to `async` [\#1258](https://github.com/wechaty/wechaty/issues/1258) +- Promote `Profile` class to a solo NPM module: `MemoryCard` [\#1257](https://github.com/wechaty/wechaty/issues/1257) +- rewrite roomFindAll\(\) [\#1255](https://github.com/wechaty/wechaty/issues/1255) +- function friendRequestAccept [\#1254](https://github.com/wechaty/wechaty/issues/1254) +- read messageRawPayload by id [\#1253](https://github.com/wechaty/wechaty/issues/1253) +- function friendRequestSend [\#1252](https://github.com/wechaty/wechaty/issues/1252) +- rewrite contactFindAll\(\) [\#1251](https://github.com/wechaty/wechaty/issues/1251) +- Upgrade Docker Base Image from Ubuntu 17.10 to 18.04 [\#1239](https://github.com/wechaty/wechaty/issues/1239) +- NPM Switch: `promise-retry` to replace `retry-promise` [\#1235](https://github.com/wechaty/wechaty/issues/1235) +- Add unit test to puppet accessory [\#1219](https://github.com/wechaty/wechaty/issues/1219) +- Use browser implementation of Node.js' stream library [\#1201](https://github.com/wechaty/wechaty/issues/1201) +- feat: Add `for await \(const contact of room\) {}` support by ES6 iterators override [\#1198](https://github.com/wechaty/wechaty/issues/1198) +- BREAKING CHANGE: v0.16 on\('friend`\) arguments changed! [\#1196](https://github.com/wechaty/wechaty/issues/1196) +- TypeScript Strict Mode: set `noImplicitAny` to `true` [\#1180](https://github.com/wechaty/wechaty/issues/1180) +- Generic for Return Child Class Type in Abstract Class Implementation [\#1178](https://github.com/wechaty/wechaty/issues/1178) +- BREAKING CHANGE: v0.16 Message.ext\(\) return '.ext' instead of 'ext' before [\#1168](https://github.com/wechaty/wechaty/issues/1168) +- Encapsulated `Contact`, `Messag`, `FriendRequest`, and `Room` into `PuppetWeb` [\#1166](https://github.com/wechaty/wechaty/issues/1166) +- BREAKING CHANGE: v0.16 will remove `MediaMessage` class [\#1164](https://github.com/wechaty/wechaty/issues/1164) +- BREAKING CHANGE: v0.16 will replace `Message.content\(\)` with `Message.text\(\)` [\#1163](https://github.com/wechaty/wechaty/issues/1163) +- Continious Deploy to NPM with @next tag when the MINOR version number is odd\(in developing branch\) [\#1158](https://github.com/wechaty/wechaty/issues/1158) +- BREAKING CHANGE: first arg of `room-leave` event licener changed from `Contact` to `Contact\[\]` [\#723](https://github.com/wechaty/wechaty/issues/723) +- Should throw Exception when there have API Error. [\#683](https://github.com/wechaty/wechaty/issues/683) +- delay time for all function\(method\) that calls Tencent API [\#596](https://github.com/wechaty/wechaty/issues/596) +- \[todo\] allow Wechaty to be multi-instance [\#518](https://github.com/wechaty/wechaty/issues/518) + +**Fixed bugs:** + +- When bot quit the room, bot still thought it in the room. [\#1345](https://github.com/wechaty/wechaty/issues/1345) +- When the bot remove one out of the group, room data didn't refresh [\#1343](https://github.com/wechaty/wechaty/issues/1343) +- Room Event cannot work as expect after create a new room [\#1342](https://github.com/wechaty/wechaty/issues/1342) +- cannot refresh room data when execute the code again [\#1339](https://github.com/wechaty/wechaty/issues/1339) +- can't run demo [\#1337](https://github.com/wechaty/wechaty/issues/1337) +- room-leave error [\#1334](https://github.com/wechaty/wechaty/issues/1334) +- room-join event, when run `room.say`, it actually run `contact.say` [\#1330](https://github.com/wechaty/wechaty/issues/1330) +- room-leave event cannot get leaver member [\#1329](https://github.com/wechaty/wechaty/issues/1329) +- should refresh room data when there is a room event [\#1328](https://github.com/wechaty/wechaty/issues/1328) +- \[room topic event\] throw error: no changerId found [\#1326](https://github.com/wechaty/wechaty/issues/1326) +- room-join cannot get member [\#1324](https://github.com/wechaty/wechaty/issues/1324) +- `contact.avatar\(\)` cannot work as expect [\#1321](https://github.com/wechaty/wechaty/issues/1321) +- run contact-bot throw error [\#1319](https://github.com/wechaty/wechaty/issues/1319) +- Padchat: WXAutoLogin result is faild, but I still receive message [\#1316](https://github.com/wechaty/wechaty/issues/1316) +- Fix the `+` in data for PuppetPadchat [\#1302](https://github.com/wechaty/wechaty/issues/1302) +- get fromId not right for room invitation sys message [\#1297](https://github.com/wechaty/wechaty/issues/1297) +- Error: The command "echo $TRAVIS\_OS\_NAME" exited with 1. [\#1236](https://github.com/wechaty/wechaty/issues/1236) +- TravisCI Conditional Deployment [\#1211](https://github.com/wechaty/wechaty/issues/1211) +- Update the peerDependencies of `rx-queue`: rxjs@6 from rxjs@5 [\#1205](https://github.com/wechaty/wechaty/issues/1205) +- Cannot send image message on v0.15.21 [\#1175](https://github.com/wechaty/wechaty/issues/1175) +- cannot refresh room topic or contact name [\#1157](https://github.com/wechaty/wechaty/issues/1157) +- How to avoid the memory leak [\#981](https://github.com/wechaty/wechaty/issues/981) +- Puppeteer Navigation Timeout Exceeded: 30000ms exceeded [\#870](https://github.com/wechaty/wechaty/issues/870) +- SyntaxError: Unexpected end of JSON input [\#846](https://github.com/wechaty/wechaty/issues/846) +- function `Message.mention\(\)` should recognize both magic code and blank [\#813](https://github.com/wechaty/wechaty/issues/813) + +**Closed issues:** + +- BREAKING CHANGE v0.16 Wechaty.self\(\) eprecated, use Wechaty.userSelf\(\) instead [\#1369](https://github.com/wechaty/wechaty/issues/1369) +- BREAKING CHANGE v0.16 Contact.personal\(\) and Contact.official\(\) deprecated, use Contact.type\(\) instead [\#1366](https://github.com/wechaty/wechaty/issues/1366) +- no encodedText error in `padchat-decode.ts` [\#1365](https://github.com/wechaty/wechaty/issues/1365) +- BREAKING CHANGE v0.16 room.add return Promise\ instead of return Promise\ [\#1362](https://github.com/wechaty/wechaty/issues/1362) +- `media-file-bot` cannot save xlsx file [\#1349](https://github.com/wechaty/wechaty/issues/1349) +- room-leave-error [\#1335](https://github.com/wechaty/wechaty/issues/1335) +- room-leave event throw error, cannot get leaver contact [\#1323](https://github.com/wechaty/wechaty/issues/1323) +- `friendship` cannot accept friend request automatically [\#1322](https://github.com/wechaty/wechaty/issues/1322) +- PadchatRpc WXCheckQRCode result: {"message":"WS่ฏทๆฑ‚้”™่ฏฏ","status":-19} [\#1315](https://github.com/wechaty/wechaty/issues/1315) +- m.forward ๆ˜ฏ undefined ๏ผŸ [\#1272](https://github.com/wechaty/wechaty/issues/1272) +- Navigation Timeout Exceeded: 30000ms exceeded [\#1248](https://github.com/wechaty/wechaty/issues/1248) +- profile.set can only set 'cookies' instead of other keys [\#1240](https://github.com/wechaty/wechaty/issues/1240) +- Create a websocket ipad demo [\#1228](https://github.com/wechaty/wechaty/issues/1228) +- Proper wechaty and its dependency installation [\#1225](https://github.com/wechaty/wechaty/issues/1225) +- can't run the typescript examples [\#1221](https://github.com/wechaty/wechaty/issues/1221) +- Scan QR Code not shown on terminal, wechaty@0.14.4 [\#1220](https://github.com/wechaty/wechaty/issues/1220) +- ่ฏท้—ฎๆ€ŽไนˆๆทปๅŠ ๅพฎไฟก็พคไธญ็š„ไบบๅฝ“ๅš่‡ชๅทฑ็š„ๅฅฝๅ‹ๅ‘ข ๆœ‰ไพ‹ๅญๅฏไปฅๅ‚่€ƒๅ— [\#1207](https://github.com/wechaty/wechaty/issues/1207) +- room-bot.ts error [\#1199](https://github.com/wechaty/wechaty/issues/1199) +- TypeScript 2.9 with trailing comma after rest parameters. [\#1188](https://github.com/wechaty/wechaty/issues/1188) +- code example 'media-file-bot' not working [\#1183](https://github.com/wechaty/wechaty/issues/1183) +- QrCode `scan` event not refresh on v0.15.21 \#1175 [\#1176](https://github.com/wechaty/wechaty/issues/1176) +- Version 10 of node.js has been released [\#1170](https://github.com/wechaty/wechaty/issues/1170) +- ่‡ชๅŠจๅŠ ๅฅฝๅ‹๏ผŒๅŠ ๅฅฝๅ‹ๆˆๅŠŸๅŽ๏ผŒๅ‘ๅฏนๆ–นๅ‘ไฟกๆฏๆŠฅ้”™ [\#1165](https://github.com/wechaty/wechaty/issues/1165) +- Use `injection-js` for Wechaty v1.0 provide the resolvers of the Wechaty Puppet [\#1146](https://github.com/wechaty/wechaty/issues/1146) +- findAll ,WARN Room parse\(\) on a empty rawObj [\#1141](https://github.com/wechaty/wechaty/issues/1141) +- Rename all `find\(\)` method to `search\(\)` [\#1132](https://github.com/wechaty/wechaty/issues/1132) +- ERR PuppetWebBridge init\(\) exception: Error: connect ECONNREFUSED 127.0.0.1:35493 [\#1113](https://github.com/wechaty/wechaty/issues/1113) +- Feature request: sending file with a stream \(creating media message with a stream\) [\#1092](https://github.com/wechaty/wechaty/issues/1092) +- node\_modules/\_wechaty@0.13.36@wechaty/dist/src/config.d.ts\(1,24\): error TS2307: Cannot find module 'raven'. [\#1035](https://github.com/wechaty/wechaty/issues/1035) + +**Merged pull requests:** + +- add await for promise [\#1375](https://github.com/wechaty/wechaty/pull/1375) ([lijiarui](https://github.com/lijiarui)) +- Fix room.add\(\) failed when room member more than 40 [\#1374](https://github.com/wechaty/wechaty/pull/1374) ([lijiarui](https://github.com/lijiarui)) +- call randam server for stable [\#1373](https://github.com/wechaty/wechaty/pull/1373) ([lijiarui](https://github.com/lijiarui)) +- check room valid by id [\#1352](https://github.com/wechaty/wechaty/pull/1352) ([lijiarui](https://github.com/lijiarui)) +- fixed cannot find room by topic after bot create room [\#1351](https://github.com/wechaty/wechaty/pull/1351) ([lijiarui](https://github.com/lijiarui)) +- fix warnings when run `npm run lint` [\#1348](https://github.com/wechaty/wechaty/pull/1348) ([lijiarui](https://github.com/lijiarui)) +- test `room.quit\(\)` in room-bot [\#1347](https://github.com/wechaty/wechaty/pull/1347) ([lijiarui](https://github.com/lijiarui)) +- add log as \#1342 [\#1344](https://github.com/wechaty/wechaty/pull/1344) ([lijiarui](https://github.com/lijiarui)) +- Bug compatible WXCreateChatRoom [\#1341](https://github.com/wechaty/wechaty/pull/1341) ([lijiarui](https://github.com/lijiarui)) +- add room-bot test code [\#1338](https://github.com/wechaty/wechaty/pull/1338) ([lijiarui](https://github.com/lijiarui)) +- save room join sys message to cache [\#1333](https://github.com/wechaty/wechaty/pull/1333) ([lijiarui](https://github.com/lijiarui)) +- add function in self-testing-bot.ts [\#1331](https://github.com/wechaty/wechaty/pull/1331) ([lijiarui](https://github.com/lijiarui)) +- Room bot example [\#1325](https://github.com/wechaty/wechaty/pull/1325) ([lijiarui](https://github.com/lijiarui)) + +## [v0.14.0](https://github.com/wechaty/wechaty/tree/v0.14.0) (2018-04-15) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.12.0...v0.14.0) + +**Implemented enhancements:** + +- Rename {test,fixture,example,...} to {tests,fixtures,examples,...} etc. [\#1127](https://github.com/wechaty/wechaty/issues/1127) +- Trigger Deploy on Branch /v\d+\.\d+\$/ [\#956](https://github.com/wechaty/wechaty/issues/956) +- Wechaty Version Numbering [\#905](https://github.com/wechaty/wechaty/issues/905) +- \[New Puppet\] Puppeteer [\#790](https://github.com/wechaty/wechaty/issues/790) +- \[enhancement\] Add timezone in to Dockerfile [\#594](https://github.com/wechaty/wechaty/issues/594) +- \[docker\] to prevent docker users install a local dependence of wechaty [\#281](https://github.com/wechaty/wechaty/issues/281) + +**Fixed bugs:** + +- When run CI inside the container: openBinaryFile: does not exist \(No such file or directory\) [\#1137](https://github.com/wechaty/wechaty/issues/1137) +- Try to use ~~Wercker~~ Shippable CI to replace Circle CI [\#1134](https://github.com/wechaty/wechaty/issues/1134) +- Pupeteer not work after upgrade to v1.1 [\#1103](https://github.com/wechaty/wechaty/issues/1103) +- ๅ…ณไบŽMsgType ็š„ไป‹็ปๆ–‡ๆกฃ404ไบ† [\#1079](https://github.com/wechaty/wechaty/issues/1079) +- Docker test fail because ts-node behavior changed between v3 and v4 [\#1050](https://github.com/wechaty/wechaty/issues/1050) +- fix description [\#1027](https://github.com/wechaty/wechaty/issues/1027) +- ERR Profile save\(\) exception: Error: EACCES: permission denied, open '/bot/demo.wechaty.json' [\#982](https://github.com/wechaty/wechaty/issues/982) +- Dockerfile.onbuild build error.Directory permissions wrong [\#961](https://github.com/wechaty/wechaty/issues/961) +- \[docker\] onbuild failed to start when we put `wechaty` as dependency in package.json [\#500](https://github.com/wechaty/wechaty/issues/500) + +**Closed issues:** + +- How to send file [\#1150](https://github.com/wechaty/wechaty/issues/1150) +- Signals \(e.g. ^C\) handling [\#1122](https://github.com/wechaty/wechaty/issues/1122) +- OK to add a .eslintrc.js file? [\#1120](https://github.com/wechaty/wechaty/issues/1120) +- Q: All the media types [\#1115](https://github.com/wechaty/wechaty/issues/1115) +- `TTL expired` error when login [\#1114](https://github.com/wechaty/wechaty/issues/1114) +- `Cannot read property 'MemberList' of undefined ` still exisit in the latest docker version [\#1111](https://github.com/wechaty/wechaty/issues/1111) +- emoji suggestion [\#1108](https://github.com/wechaty/wechaty/issues/1108) +- Regenerate docs/index.md by run `npm run doc` [\#1105](https://github.com/wechaty/wechaty/issues/1105) +- hot-reload-bot adding hot-reloading config [\#1100](https://github.com/wechaty/wechaty/issues/1100) +- Third options to run wechaty [\#1085](https://github.com/wechaty/wechaty/issues/1085) +- issue [\#1076](https://github.com/wechaty/wechaty/issues/1076) +- Session last only for 1 hour in Azure [\#1059](https://github.com/wechaty/wechaty/issues/1059) +- ๅ‡บ็Žฐ no Url ้”™่ฏฏๅŽ๏ผŒ็จ‹ๅบๅดฉๆบƒ [\#1055](https://github.com/wechaty/wechaty/issues/1055) +- ๅ›พ็‰‡ๅ‘้€ไธ€ๆฌกๅŽ,ๅ›พ็‰‡ๆŸๅ [\#1040](https://github.com/wechaty/wechaty/issues/1040) +- ERR PuppetWebBridge init\(\) exception [\#1018](https://github.com/wechaty/wechaty/issues/1018) +- ๆˆ‘ๅฎžๅœจ่งฃๅ†ณไธไบ†่ฟ™ไธช้—ฎ้ข˜ไบ†~่œ้ธŸไธ€ๆžš [\#998](https://github.com/wechaty/wechaty/issues/998) +- TypeError: Cannot read property 'MemberList' of undefined [\#984](https://github.com/wechaty/wechaty/issues/984) +- Using hot-import cannot quit wechaty when quit the code [\#978](https://github.com/wechaty/wechaty/issues/978) +- Error: Navigation Timeout Exceeded: 30000ms exceeded [\#966](https://github.com/wechaty/wechaty/issues/966) +- hot-reload does not work [\#958](https://github.com/wechaty/wechaty/issues/958) +- \[docker\] Login error [\#950](https://github.com/wechaty/wechaty/issues/950) +- Invalid audio output parameters received; using fake audio path. [\#939](https://github.com/wechaty/wechaty/issues/939) +- Keep getting "can not found bot file: src/main.ts" [\#937](https://github.com/wechaty/wechaty/issues/937) +- glSetDrawRectangleCHROMIUM: failed on surface [\#934](https://github.com/wechaty/wechaty/issues/934) +- \[FR\] Create an example for using hot-import for listener and other modules [\#923](https://github.com/wechaty/wechaty/issues/923) +- Bot log out frequently and got some strange error between it logout and relogin automatically [\#612](https://github.com/wechaty/wechaty/issues/612) + +**Merged pull requests:** + +- chore\(package\): update ws to version 5.1.0 [\#1143](https://github.com/wechaty/wechaty/pull/1143) ([huan](https://github.com/huan)) +- chore\(package\): update puppeteer to version 1.2.0 [\#1131](https://github.com/wechaty/wechaty/pull/1131) ([huan](https://github.com/huan)) +- - \[+\] add one more test case for digestEmoji\(\), close \#1108 [\#1129](https://github.com/wechaty/wechaty/pull/1129) ([suntong](https://github.com/suntong)) +- hot-reload-bot adding hot-reloading config close \#1066, close \#1100 [\#1123](https://github.com/wechaty/wechaty/pull/1123) ([suntong](https://github.com/suntong)) +- cannot recognize room event [\#1116](https://github.com/wechaty/wechaty/pull/1116) ([lijiarui](https://github.com/lijiarui)) +- fix chown: invalid user: โ€˜botโ€™ [\#1089](https://github.com/wechaty/wechaty/pull/1089) ([mukaiu](https://github.com/mukaiu)) +- Add troubleshooting for hidden .txt files \(Windows\) [\#1087](https://github.com/wechaty/wechaty/pull/1087) ([IdiosApps](https://github.com/IdiosApps)) +- fix \#1079 [\#1086](https://github.com/wechaty/wechaty/pull/1086) ([lijiarui](https://github.com/lijiarui)) +- chore\(package\): update @types/node to version 9.4.0 [\#1083](https://github.com/wechaty/wechaty/pull/1083) ([huan](https://github.com/huan)) +- chore\(package\): update finis to version 0.4.1 [\#1075](https://github.com/wechaty/wechaty/pull/1075) ([huan](https://github.com/huan)) +- chore\(package\): update ts-node to version 4.1.0 [\#1074](https://github.com/wechaty/wechaty/pull/1074) ([huan](https://github.com/huan)) +- chore\(package\): update mime to version 2.2.0 [\#1073](https://github.com/wechaty/wechaty/pull/1073) ([huan](https://github.com/huan)) +- chore\(package\): update tslint to version 5.9.0 [\#1072](https://github.com/wechaty/wechaty/pull/1072) ([huan](https://github.com/huan)) +- chore\(package\): update sinon to version 4.2.0 [\#1071](https://github.com/wechaty/wechaty/pull/1071) ([huan](https://github.com/huan)) +- fail to install node dependencies [\#1036](https://github.com/wechaty/wechaty/pull/1036) ([hiwanz](https://github.com/hiwanz)) +- Change `init` to `start` in demo [\#1017](https://github.com/wechaty/wechaty/pull/1017) ([xinbenlv](https://github.com/xinbenlv)) +- Update tuling123-bot.ts [\#1014](https://github.com/wechaty/wechaty/pull/1014) ([htoooth](https://github.com/htoooth)) +- Fix \#923 [\#935](https://github.com/wechaty/wechaty/pull/935) ([xinbenlv](https://github.com/xinbenlv)) + +## [v0.12.0](https://github.com/wechaty/wechaty/tree/v0.12.0) (2017-10-30) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.9.0...v0.12.0) + +**Implemented enhancements:** + +- Promote `rx-queue` to SOLO NPM Module [\#938](https://github.com/wechaty/wechaty/issues/938) +- Add Wechaty.logonoff\(\) method [\#926](https://github.com/wechaty/wechaty/issues/926) +- Registe Wechaty Listeners with a try {} catch {} block in order to prevent listener function crash the framework. [\#878](https://github.com/wechaty/wechaty/issues/878) +- Upgrade CircleCI from 1.0 to 2.0 [\#863](https://github.com/wechaty/wechaty/issues/863) +- Switch Docker Node Image from `alphin` to official `node:7` [\#862](https://github.com/wechaty/wechaty/issues/862) +- Click Web Wechat `Switch Account` Automatically to get qrcode immediately when bot logout [\#636](https://github.com/wechaty/wechaty/issues/636) +- Upgrade docker image from Node.js v7 to v8 [\#577](https://github.com/wechaty/wechaty/issues/577) +- \[todo\] switch unit test tool from AVA to TAPE [\#513](https://github.com/wechaty/wechaty/issues/513) + +**Fixed bugs:** + +- \[CI\] Homebrew must be run under Ruby 2.3! You're running 2.0.0. \(RuntimeError\) [\#936](https://github.com/wechaty/wechaty/issues/936) +- "PromiseRejectionHandledWarning: Promise rejection was handled asynchronously" when Wechat says "ๅฝ“ๅ‰็™ปๅฝ•็Žฏๅขƒๅผ‚ๅธธ" [\#925](https://github.com/wechaty/wechaty/issues/925) +- TypeError: cookieList.filter is not a function [\#919](https://github.com/wechaty/wechaty/issues/919) +- TypeError: Cannot read property 'error' of null [\#918](https://github.com/wechaty/wechaty/issues/918) +- ERR PuppetWebBridge init\(\) initPage\(\) onLoad\(\) exception: undefined [\#917](https://github.com/wechaty/wechaty/issues/917) +- Sometimes Wechaty can't login \(with puppeteer\) [\#899](https://github.com/wechaty/wechaty/issues/899) +- \[ci\] WebDriver Error: "no such session" [\#756](https://github.com/wechaty/wechaty/issues/756) +- \[ci\] execute proxyWechaty\(init\) error: 503, init\(\) without a ready angular env [\#329](https://github.com/wechaty/wechaty/issues/329) +- \[ci log\] watchdog reset after 120 seconds [\#195](https://github.com/wechaty/wechaty/issues/195) +- Selenium WebDriver driver.getSession\(\) wait a long time [\#86](https://github.com/wechaty/wechaty/issues/86) + +**Closed issues:** + +- ๅœจ็™ปๅฝ•ๅคฑ่ดฅๆ—ถ็š„ๅผ‚ๅธธๆ็คบไผ˜ๅŒ– [\#898](https://github.com/wechaty/wechaty/issues/898) +- CANT RUN THE WECHATY-GETTING-STARTED, PUPPETWEBROWSER valid\(\) getSession\(\) [\#891](https://github.com/wechaty/wechaty/issues/891) +- Error after restart vps invalid driver at ttl 0 [\#788](https://github.com/wechaty/wechaty/issues/788) +- webdriver.executeScript wait a long time\(26s\) before page load [\#2](https://github.com/wechaty/wechaty/issues/2) + +**Merged pull requests:** + +- Replace WebDriver by Puppeteer \(\#790\) [\#860](https://github.com/wechaty/wechaty/pull/860) ([huan](https://github.com/huan)) +- chore\(package\): update coveralls to version 3.0.0 [\#854](https://github.com/wechaty/wechaty/pull/854) ([huan](https://github.com/huan)) + +## [v0.9.0](https://github.com/wechaty/wechaty/tree/v0.9.0) (2017-10-04) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.8.2...v0.9.0) + +**Implemented enhancements:** + +- Support hot-reload for Wechaty events listeners [\#820](https://github.com/wechaty/wechaty/issues/820) +- \[jsdoc\] additional optimizations [\#774](https://github.com/wechaty/wechaty/issues/774) +- \[bug\] Support for send 25Mb+ size files [\#766](https://github.com/wechaty/wechaty/issues/766) +- \[Announcement\] Drop support for phantomjs [\#763](https://github.com/wechaty/wechaty/issues/763) +- chrome-headless support [\#739](https://github.com/wechaty/wechaty/issues/739) +- Add Transpond Message [\#726](https://github.com/wechaty/wechaty/issues/726) +- Cannot send pdf file using MediaMessage [\#710](https://github.com/wechaty/wechaty/issues/710) +- Use Sentry.io to report exceptions [\#580](https://github.com/wechaty/wechaty/issues/580) +- \[enhancement\] Add pdf, docx etc support to MediaMessage\(now only picture is supported\) [\#538](https://github.com/wechaty/wechaty/issues/538) +- use babel-node to run javascript\(.js\) file inside docker [\#507](https://github.com/wechaty/wechaty/issues/507) +- \[todo\] Change to use native Dom Websocket instead of socket.io library [\#502](https://github.com/wechaty/wechaty/issues/502) +- License Change: from ISC to Apache-2.0 [\#474](https://github.com/wechaty/wechaty/issues/474) +- requesting a new QR code cost more than 2 minutes [\#434](https://github.com/wechaty/wechaty/issues/434) +- Send\(upload\) an image twice will cause error. [\#422](https://github.com/wechaty/wechaty/issues/422) +- Keep emoji characters from user name and room name [\#207](https://github.com/wechaty/wechaty/issues/207) +- \[Feature request\] Sending Stickers [\#156](https://github.com/wechaty/wechaty/issues/156) +- \[Feature request\] @mention support? [\#153](https://github.com/wechaty/wechaty/issues/153) +- \[Docker\] add a `onbuild` image to Wechaty [\#147](https://github.com/wechaty/wechaty/issues/147) +- \[doc\] To Embed Document in Wechaty Code for Generating Automaticly [\#73](https://github.com/wechaty/wechaty/issues/73) +- it seems RECALLED: 10002 message dose't trigger on\('message'\) event [\#8](https://github.com/wechaty/wechaty/issues/8) +- Support Message Type of Image/Video [\#4](https://github.com/wechaty/wechaty/issues/4) + +**Fixed bugs:** + +- Cannot send image after restart [\#777](https://github.com/wechaty/wechaty/issues/777) +- \[bug\]Cannot read property 'getUserName' of undefined [\#772](https://github.com/wechaty/wechaty/issues/772) +- \[tslint\] stuck on v5.3 and can not upgrade [\#762](https://github.com/wechaty/wechaty/issues/762) +- CI, green keeper and package-lock under npm 5 [\#656](https://github.com/wechaty/wechaty/issues/656) +- watchDogReset\(\) watchdog reset after 60 seconds \(phantomjs head\) [\#633](https://github.com/wechaty/wechaty/issues/633) +- \[test\] Unit Test for `mentioned` feature does not run at all [\#623](https://github.com/wechaty/wechaty/issues/623) +- error TS2345: Argument of type 'string | MemberQueryFilter' is not assignable to parameter of type 'MemberQueryFilter' [\#622](https://github.com/wechaty/wechaty/issues/622) +- \[Doc\] Add `say\(new MediaMessage\('/tmp/mediafile.gif'\)\)` documentation [\#587](https://github.com/wechaty/wechaty/issues/587) +- Node Typing BUG: `process.env: any` [\#582](https://github.com/wechaty/wechaty/issues/582) +- \[ci\] Appveyor error: `should get cookies after loadSession\(\)` [\#579](https://github.com/wechaty/wechaty/issues/579) +- wechaty v0.8.54 does not install all required component [\#522](https://github.com/wechaty/wechaty/issues/522) +- message.mentioned\(\) does not work as expected [\#512](https://github.com/wechaty/wechaty/issues/512) +- Some types of media file is saved as a 0 byte file. [\#504](https://github.com/wechaty/wechaty/issues/504) +- ts-node commond not found after update docker image [\#492](https://github.com/wechaty/wechaty/issues/492) +- may be not need .vscode folder, need .editorconfig [\#489](https://github.com/wechaty/wechaty/issues/489) +- `Room.findAll\(\)` should always return a `ready\(\)`-ed instance [\#477](https://github.com/wechaty/wechaty/issues/477) +- MediaMessage\#filename\(\) should not use timestamp as part of the filename [\#465](https://github.com/wechaty/wechaty/issues/465) +- \[ci\] ร— src ยป message ยป ready\(\) contact ready for ToNickName [\#445](https://github.com/wechaty/wechaty/issues/445) +- Build Docker image from zixia/wechaty:onbuild,/bot/node\_modules does not exist. [\#436](https://github.com/wechaty/wechaty/issues/436) +- Concat.avatar\(\) faild ,when hostname changed from https://wx.qq.com to https://wx2.qq.com [\#418](https://github.com/wechaty/wechaty/issues/418) +- \[test\] Unit Tests under Linux by TravisCI keep failing [\#384](https://github.com/wechaty/wechaty/issues/384) +- `checkRoomJoin\(\)` cannot get inviteeList sometimes [\#248](https://github.com/wechaty/wechaty/issues/248) +- \[ci log\] no driver process after quit [\#210](https://github.com/wechaty/wechaty/issues/210) +- \[ci\] no new tests completed within the last 180000ms of inactivity [\#175](https://github.com/wechaty/wechaty/issues/175) +- how to NOT scan the QRCode on each opening [\#151](https://github.com/wechaty/wechaty/issues/151) +- Unhandled Rejection: checkLegacyResponse\(\) at error.js:505 [\#122](https://github.com/wechaty/wechaty/issues/122) +- WebDriverError: unknown error: r.isBrandContact is not a function [\#81](https://github.com/wechaty/wechaty/issues/81) +- Can't get wechaty up and running using phantomjs [\#60](https://github.com/wechaty/wechaty/issues/60) +- `accountFactory` of angularjs in wxapp work not right under Phantomjs, but it work without problem with chrome [\#28](https://github.com/wechaty/wechaty/issues/28) +- Fixed: replace `document.domain` with `location.hostname` since front one sometimes lose subdomain name [\#770](https://github.com/wechaty/wechaty/pull/770) ([zhenyong](https://github.com/zhenyong)) + +**Closed issues:** + +- vscode setting config error [\#843](https://github.com/wechaty/wechaty/issues/843) +- add static method `Message.findAll\(\)` [\#765](https://github.com/wechaty/wechaty/issues/765) +- cannot find Chrome binary [\#746](https://github.com/wechaty/wechaty/issues/746) +- UnhandledPromiseRejectionWarning: Unhandled promise rejection \(rejection id: 2\): Error: no puppet instance [\#738](https://github.com/wechaty/wechaty/issues/738) +- MediaMessage.filename\(\) cannot get correct img name. [\#722](https://github.com/wechaty/wechaty/issues/722) +- MediaMessage.ext\(\) cannot work as expect [\#721](https://github.com/wechaty/wechaty/issues/721) +- the latest docker version 139 cannot send file [\#720](https://github.com/wechaty/wechaty/issues/720) +- what I need is just a lib instead of a product [\#709](https://github.com/wechaty/wechaty/issues/709) +- How to link it with personal account. [\#693](https://github.com/wechaty/wechaty/issues/693) +- Sending Images [\#690](https://github.com/wechaty/wechaty/issues/690) +- windows 10 throws PuppetWeb initBrowser\(\) exception: got invalid driver at ttl 0 [\#688](https://github.com/wechaty/wechaty/issues/688) +- Group game [\#675](https://github.com/wechaty/wechaty/issues/675) +- if wechaty cannot get inviteeList when emit `room-join` , suggest it emit room-fire and get warning info. [\#671](https://github.com/wechaty/wechaty/issues/671) +- ๅœจ websocket ็š„ๅ›ž่ฐƒไธญๆ— ๆณ•ไฝฟ็”จ wechaty ๅ— [\#665](https://github.com/wechaty/wechaty/issues/665) +- Room.find\(\) ๅ‘้€ๆถˆๆฏๆ็คบ say is not a function [\#664](https://github.com/wechaty/wechaty/issues/664) +- ่Žทๅ–ไธๅˆฐavatar [\#645](https://github.com/wechaty/wechaty/issues/645) +- Get ECONNREFUSED when bot logout and cannot re-login [\#617](https://github.com/wechaty/wechaty/issues/617) +- Modify Function `Room.create` return type, from `Promise\` to `Promise\` [\#616](https://github.com/wechaty/wechaty/issues/616) +- init ๅŽไผšๆ‰“ๅผ€ๆ‰ซ็ ็š„็ฝ‘้กต๏ผŸ [\#601](https://github.com/wechaty/wechaty/issues/601) +- Error: ENOENT: no such file or directory, stat '/wechaty/dist/.git' [\#581](https://github.com/wechaty/wechaty/issues/581) +- ๅœจ Windows Server ไธŠๅˆๅง‹ๅŒ–็š„ๆ—ถๅ€™๏ผŒchromedriver ๆŠฅ้”™ใ€‚ [\#574](https://github.com/wechaty/wechaty/issues/574) +- ๅฏๅŠจๅŽๅกไฝ้—ฎ้ข˜ [\#566](https://github.com/wechaty/wechaty/issues/566) +- whatever [\#543](https://github.com/wechaty/wechaty/issues/543) +- \[enhancement\] handle room name change event [\#532](https://github.com/wechaty/wechaty/issues/532) +- got \[aq.qq.com\] domain [\#526](https://github.com/wechaty/wechaty/issues/526) +- some strange session error [\#523](https://github.com/wechaty/wechaty/issues/523) +- static Contact.find\(\) / static Contact.findAll\(\) throws exception [\#520](https://github.com/wechaty/wechaty/issues/520) +- Cannot set alias of Contact Object getting from `message.from\(\)` method when Contact is not a friend [\#509](https://github.com/wechaty/wechaty/issues/509) +- room.member\(\) can not return right result [\#437](https://github.com/wechaty/wechaty/issues/437) +- windows run program send images throw out error [\#427](https://github.com/wechaty/wechaty/issues/427) +- group names have HTML in them [\#382](https://github.com/wechaty/wechaty/issues/382) +- jsdoc2md may flush some pieces of the embedded doc [\#378](https://github.com/wechaty/wechaty/issues/378) +- Secure WebSocket\(wss\) do not work with Self Signed Certificate in PhantomJS [\#12](https://github.com/wechaty/wechaty/issues/12) + +**Merged pull requests:** + +- fix: fix vscode setting, and close autoFixOnSave [\#844](https://github.com/wechaty/wechaty/pull/844) ([binsee](https://github.com/binsee)) +- Add Hot Listener Support \(\#820\) [\#841](https://github.com/wechaty/wechaty/pull/841) ([huan](https://github.com/huan)) +- Revert "Update express to the latest version ๐Ÿš€" [\#831](https://github.com/wechaty/wechaty/pull/831) ([huan](https://github.com/huan)) +- add \n after wiki [\#816](https://github.com/wechaty/wechaty/pull/816) ([lijiarui](https://github.com/lijiarui)) +- Friendrequest doc [\#812](https://github.com/wechaty/wechaty/pull/812) ([lijiarui](https://github.com/lijiarui)) +- Fix switch account [\#811](https://github.com/wechaty/wechaty/pull/811) ([binsee](https://github.com/binsee)) +- fix\(package\): update brolog to version 1.2.6 [\#810](https://github.com/wechaty/wechaty/pull/810) ([huan](https://github.com/huan)) +- change readme doc [\#805](https://github.com/wechaty/wechaty/pull/805) ([lijiarui](https://github.com/lijiarui)) +- change doc order \# 774 [\#798](https://github.com/wechaty/wechaty/pull/798) ([lijiarui](https://github.com/lijiarui)) +- fix\(\*\): Support for send 25Mb+ files [\#771](https://github.com/wechaty/wechaty/pull/771) ([binsee](https://github.com/binsee)) +- Readme [\#757](https://github.com/wechaty/wechaty/pull/757) ([lijiarui](https://github.com/lijiarui)) +- fix\(wechaty-bro\): resolved emit RECALLED type msg \(fix \#8\) [\#744](https://github.com/wechaty/wechaty/pull/744) ([binsee](https://github.com/binsee)) +- add: Message.forward\(\) forward message [\#727](https://github.com/wechaty/wechaty/pull/727) ([binsee](https://github.com/binsee)) +- add wechaty document [\#725](https://github.com/wechaty/wechaty/pull/725) ([lijiarui](https://github.com/lijiarui)) + +## [v0.8.2](https://github.com/wechaty/wechaty/tree/v0.8.2) (2017-05-03) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.7.0...v0.8.2) + +**Implemented enhancements:** + +- Promote StateMonitor to a solo NPM module: StateSwitch [\#466](https://github.com/wechaty/wechaty/issues/466) +- Display detailed error trace when an error is caught in async [\#360](https://github.com/wechaty/wechaty/issues/360) +- Room.find\({topic: topic}\) should allowed to return null [\#291](https://github.com/wechaty/wechaty/issues/291) +- add `room-bot-leave` event [\#250](https://github.com/wechaty/wechaty/issues/250) +- Prepare to rename the nick/remark/display for contact/room [\#217](https://github.com/wechaty/wechaty/issues/217) +- \[new feature\] add function message.mention\(\) [\#216](https://github.com/wechaty/wechaty/issues/216) +- \[new feature\] set bot's nickname in the group [\#201](https://github.com/wechaty/wechaty/issues/201) +- \[feature request\] fire `room-join` when someone joins from a QR Code [\#155](https://github.com/wechaty/wechaty/issues/155) +- \#4 send image/video [\#337](https://github.com/wechaty/wechaty/pull/337) ([mukaiu](https://github.com/mukaiu)) + +**Fixed bugs:** + +- Reuse MediaMessage upload fail.Can be allowed MediaMessage reuse๏ผŸ [\#439](https://github.com/wechaty/wechaty/issues/439) +- Room.member\(\) cannot find contact correctly [\#365](https://github.com/wechaty/wechaty/issues/365) +- Room.alias\(\) should return null if we have not set the alias in the room [\#283](https://github.com/wechaty/wechaty/issues/283) +- Cannot read property 'Symbol\(Symbol.iterator\)' of undefined [\#273](https://github.com/wechaty/wechaty/issues/273) +- add sys message in FriendRequest Event [\#260](https://github.com/wechaty/wechaty/issues/260) +- \[docker\] chromium-browser fail to start after upgrading chromium from v53 to v56 [\#235](https://github.com/wechaty/wechaty/issues/235) +- typo in Wiki [\#205](https://github.com/wechaty/wechaty/issues/205) +- doc bug [\#196](https://github.com/wechaty/wechaty/issues/196) +- Linting Error from PR@lijiarui [\#181](https://github.com/wechaty/wechaty/issues/181) +- \[document\] should list only the public/stable API to users [\#174](https://github.com/wechaty/wechaty/issues/174) + +**Closed issues:** + +- Always getSession timeout [\#463](https://github.com/wechaty/wechaty/issues/463) +- how to create more bots at once [\#460](https://github.com/wechaty/wechaty/issues/460) +- how do we get avatar link? [\#424](https://github.com/wechaty/wechaty/issues/424) +- can't run the example [\#423](https://github.com/wechaty/wechaty/issues/423) +- ๆœ‰ๆฒกๆœ‰ๆŸฅๆ‰พๅฅฝๅ‹็š„ๆ–นๆณ•๏ผŸ [\#411](https://github.com/wechaty/wechaty/issues/411) +- ding-dong-bot-ts cannot run normally on Mac [\#410](https://github.com/wechaty/wechaty/issues/410) +- Failed due to EAI\_AGAIN registry.yarnpkg.com:443 [\#408](https://github.com/wechaty/wechaty/issues/408) +- cannot remark friend in centos system [\#406](https://github.com/wechaty/wechaty/issues/406) +- MediaMessage in ding-dong-bot example can not be create [\#399](https://github.com/wechaty/wechaty/issues/399) +- wechaty can auto receive money\(red envolop/transfer\) on account. [\#398](https://github.com/wechaty/wechaty/issues/398) +- \[bug\] room.say\(\) return contact's alias when bot set alias for some one [\#394](https://github.com/wechaty/wechaty/issues/394) +- `Room.fresh\(\)`not work; `Room.alias\(\)`returns null [\#391](https://github.com/wechaty/wechaty/issues/391) +- should add`phantomjs-prebuilt` in package.json [\#385](https://github.com/wechaty/wechaty/issues/385) +- error on room join: TypeError: room.topic is not a function [\#383](https://github.com/wechaty/wechaty/issues/383) +- problem starting docker container . SyntaxError: Unexpected token function [\#352](https://github.com/wechaty/wechaty/issues/352) +- \[discuss\] Rename Wechaty to Chatie? [\#346](https://github.com/wechaty/wechaty/issues/346) +- cannot send images / this.puppet.getBaseRequest is not a function [\#338](https://github.com/wechaty/wechaty/issues/338) +- Some strange log warn [\#336](https://github.com/wechaty/wechaty/issues/336) +- run bot in server,about 1 hour ago ,the process will be killed [\#330](https://github.com/wechaty/wechaty/issues/330) +- wechaty 0.7.21 works but 0.7.24 failed with Argument of type 'string | Promise\' is not assignable to parameter of type 'string'. [\#282](https://github.com/wechaty/wechaty/issues/282) +- how i can save avatar without await keywords? [\#278](https://github.com/wechaty/wechaty/issues/278) +- ๅฆ‚ไฝ•่Žทๅ–MsgTypeไธบAPP็ฑปๅž‹็š„ไฟกๆฏ,่งฃๆžไธๆˆxml [\#262](https://github.com/wechaty/wechaty/issues/262) +- \[linting\] fix needed for new tslint rule: trailing-comma [\#251](https://github.com/wechaty/wechaty/issues/251) +- Avatar return empty image in example/contact-bot.ts [\#246](https://github.com/wechaty/wechaty/issues/246) +- Room&Contact.find\(\) should throw exception when it get more than one value [\#229](https://github.com/wechaty/wechaty/issues/229) +- Contact.findAll\(\) return contactList includes oa account [\#222](https://github.com/wechaty/wechaty/issues/222) +- timeouts when running unattended [\#184](https://github.com/wechaty/wechaty/issues/184) +- room.member\(\) cannot find contact when contact set whose alias in the room [\#173](https://github.com/wechaty/wechaty/issues/173) + +**Merged pull requests:** + +- Lazy to create a stream [\#470](https://github.com/wechaty/wechaty/pull/470) ([mukaiu](https://github.com/mukaiu)) +- chore\(package\): update state-switch to version 0.1.7 [\#469](https://github.com/wechaty/wechaty/pull/469) ([huan](https://github.com/huan)) +- chore\(package\): update bl to version 1.2.1 [\#462](https://github.com/wechaty/wechaty/pull/462) ([huan](https://github.com/huan)) +- fix\(package\): update brolog to version 1.0.13 [\#455](https://github.com/wechaty/wechaty/pull/455) ([huan](https://github.com/huan)) +- chore\(package\): update fluent-ffmpeg to version 2.1.2 [\#449](https://github.com/wechaty/wechaty/pull/449) ([huan](https://github.com/huan)) +- add magic code for room.say\(\) when `@bot ` happen [\#440](https://github.com/wechaty/wechaty/pull/440) ([lijiarui](https://github.com/lijiarui)) +- \#3 support send gif [\#438](https://github.com/wechaty/wechaty/pull/438) ([mukaiu](https://github.com/mukaiu)) +- Limit video file size [\#421](https://github.com/wechaty/wechaty/pull/421) ([mukaiu](https://github.com/mukaiu)) +- add room.say\(MediaMessage\) [\#420](https://github.com/wechaty/wechaty/pull/420) ([mukaiu](https://github.com/mukaiu)) +- Fix chrome driver path problem in Windows [\#416](https://github.com/wechaty/wechaty/pull/416) ([xjchengo](https://github.com/xjchengo)) +- fix upload media url error [\#415](https://github.com/wechaty/wechaty/pull/415) ([mukaiu](https://github.com/mukaiu)) +- support brand checking of contact [\#404](https://github.com/wechaty/wechaty/pull/404) ([JasLin](https://github.com/JasLin)) +- chore\(package\): update chromedriver to version 2.29.0 [\#396](https://github.com/wechaty/wechaty/pull/396) ([huan](https://github.com/huan)) +- Add missing %s content for leaver not found error [\#388](https://github.com/wechaty/wechaty/pull/388) ([xinbenlv](https://github.com/xinbenlv)) +- fix jsdoc flush issue \#378 and minor fix on the doc examples [\#380](https://github.com/wechaty/wechaty/pull/380) ([ax4](https://github.com/ax4)) +- Limit the size of the sending file [\#376](https://github.com/wechaty/wechaty/pull/376) ([mukaiu](https://github.com/mukaiu)) +- add room-leave event [\#370](https://github.com/wechaty/wechaty/pull/370) ([lijiarui](https://github.com/lijiarui)) +- room.memberAll\(\) & change room.member\(\) query to 3 types [\#364](https://github.com/wechaty/wechaty/pull/364) ([lijiarui](https://github.com/lijiarui)) +- Add mention [\#362](https://github.com/wechaty/wechaty/pull/362) ([lijiarui](https://github.com/lijiarui)) +- Printout entire error trace when unhandledRejection was caught [\#361](https://github.com/wechaty/wechaty/pull/361) ([xinbenlv](https://github.com/xinbenlv)) +- first item of memberList as owner is confusion [\#358](https://github.com/wechaty/wechaty/pull/358) ([JasLin](https://github.com/JasLin)) +- chore\(package\): update ts-node to version 3.0.2 [\#351](https://github.com/wechaty/wechaty/pull/351) ([huan](https://github.com/huan)) +- fix room test [\#328](https://github.com/wechaty/wechaty/pull/328) ([lijiarui](https://github.com/lijiarui)) +- remove blank [\#324](https://github.com/wechaty/wechaty/pull/324) ([lijiarui](https://github.com/lijiarui)) +- remove m.send\(\) fucntion [\#323](https://github.com/wechaty/wechaty/pull/323) ([lijiarui](https://github.com/lijiarui)) +- Add JsDoc for Class Contact [\#321](https://github.com/wechaty/wechaty/pull/321) ([lijiarui](https://github.com/lijiarui)) +- 291 [\#318](https://github.com/wechaty/wechaty/pull/318) ([lijiarui](https://github.com/lijiarui)) +- chore\(package\): update yarn to version 0.21.3 [\#317](https://github.com/wechaty/wechaty/pull/317) ([huan](https://github.com/huan)) +- chore\(package\): update nyc to version 10.1.2 [\#316](https://github.com/wechaty/wechaty/pull/316) ([huan](https://github.com/huan)) +- chore\(package\): update tslint to version 4.5.1 [\#315](https://github.com/wechaty/wechaty/pull/315) ([huan](https://github.com/huan)) +- chore\(package\): update check-node-version to version 2.0.1 [\#314](https://github.com/wechaty/wechaty/pull/314) ([huan](https://github.com/huan)) +- chore\(package\): update @types/ws to version 0.0.38 [\#313](https://github.com/wechaty/wechaty/pull/313) ([huan](https://github.com/huan)) +- chore\(package\): update @types/node to version 7.0.7 [\#312](https://github.com/wechaty/wechaty/pull/312) ([huan](https://github.com/huan)) +- fix\(package\): update @types/selenium-webdriver to version 3.0.0 [\#311](https://github.com/wechaty/wechaty/pull/311) ([huan](https://github.com/huan)) +- added hot load bots [\#310](https://github.com/wechaty/wechaty/pull/310) ([Gcaufy](https://github.com/Gcaufy)) +- \#283 [\#303](https://github.com/wechaty/wechaty/pull/303) ([lijiarui](https://github.com/lijiarui)) +- \#291 change `throw error` to `return null` [\#292](https://github.com/wechaty/wechaty/pull/292) ([lijiarui](https://github.com/lijiarui)) + +## [v0.7.0](https://github.com/wechaty/wechaty/tree/v0.7.0) (2016-12-29) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.6.0...v0.7.0) + +**Implemented enhancements:** + +- ่ฏท้—ฎๅฏไปฅ่Žทๅ–่”็ณปไบบๆˆ–็พคๆˆๅ‘˜็š„ๆ€งๅˆซใ€ๆ‰€ๅฑžๅœฐๅŸŸใ€ๅคดๅƒๅ—๏ผŸ [\#121](https://github.com/wechaty/wechaty/issues/121) +- Function Room.add\(\) should return Promise\ [\#119](https://github.com/wechaty/wechaty/issues/119) +- Could you add api to find contact by remark [\#117](https://github.com/wechaty/wechaty/issues/117) +- Need to support AppMsgType: 100001 with MsgType: 49 [\#114](https://github.com/wechaty/wechaty/issues/114) +- get room owner on event 'room-join','room-topic' [\#105](https://github.com/wechaty/wechaty/pull/105) ([JasLin](https://github.com/JasLin)) + +**Fixed bugs:** + +- to silence all the output from webdriver\(chromedriver\) for log level INFO [\#150](https://github.com/wechaty/wechaty/issues/150) +- `tsc` compiling error: Cannot find namespace 'webdriver' [\#136](https://github.com/wechaty/wechaty/issues/136) +- remark\(null\) doesn't work [\#130](https://github.com/wechaty/wechaty/issues/130) +- Cannot identify \['\] in room topic [\#116](https://github.com/wechaty/wechaty/issues/116) +- room.member\(\) cannot get member when bot set remark for friend [\#104](https://github.com/wechaty/wechaty/issues/104) +- contactFind\(function \(c\) { return /.\*/.test\(c\) }\) rejected: javascript error: Unexpected token [\#98](https://github.com/wechaty/wechaty/issues/98) +- Error: Chrome failed to start: was killed [\#95](https://github.com/wechaty/wechaty/issues/95) +- Function `message.to\(\): Contact|Room` bug [\#88](https://github.com/wechaty/wechaty/issues/88) +- Session Cookies not loaded correctly? [\#31](https://github.com/wechaty/wechaty/issues/31) + +**Closed issues:** + +- too many levels of symbolic links [\#165](https://github.com/wechaty/wechaty/issues/165) +- node dist/example/ding-dong-bot.js example่ฟ่กŒๅผ‚ๅธธ [\#159](https://github.com/wechaty/wechaty/issues/159) +- deploying to server problems \(running headless\) [\#154](https://github.com/wechaty/wechaty/issues/154) +- wechaty mybot.js start error [\#126](https://github.com/wechaty/wechaty/issues/126) +- Room-join' para inviteeList\[\] cannot always work well when contain emoji [\#125](https://github.com/wechaty/wechaty/issues/125) +- \[help\] install wechaty and its types [\#124](https://github.com/wechaty/wechaty/issues/124) +- ERR Message ready\(\) exception: Error: Contact.load\(\): id not found [\#123](https://github.com/wechaty/wechaty/issues/123) +- enhance request.hello function [\#120](https://github.com/wechaty/wechaty/issues/120) +- ๆ— ๆณ•่‡ชๅŠจ้€š่ฟ‡ๅฅฝๅ‹่ฏทๆฑ‚ [\#115](https://github.com/wechaty/wechaty/issues/115) +- \[EVENT INVITATION\] Welcome to join Beijing Node Party 18: Wechaty & ChatBot on 11th Dec. [\#107](https://github.com/wechaty/wechaty/issues/107) +- another problem about docker run [\#103](https://github.com/wechaty/wechaty/issues/103) +- Error: Server terminated early with status 127 [\#102](https://github.com/wechaty/wechaty/issues/102) +- failed run demo in docker under centos [\#101](https://github.com/wechaty/wechaty/issues/101) +- Wechaty.send\(\) error when send message to the room [\#89](https://github.com/wechaty/wechaty/issues/89) +- ๅŸบ็ก€่ฟ่กŒๆŠฅ้”™.....ๆˆ‘้ƒฝๆœ‰็‚นไธๅฅฝๆ„ๆ€้—ฎไบ†.....ๆฑ—.... [\#82](https://github.com/wechaty/wechaty/issues/82) + +**Merged pull requests:** + +- chore\(package\): update @types/node to version 6.0.54 [\#168](https://github.com/wechaty/wechaty/pull/168) ([huan](https://github.com/huan)) +- chore\(package\): update tslint to version 4.2.0 [\#158](https://github.com/wechaty/wechaty/pull/158) ([huan](https://github.com/huan)) +- chore\(package\): update @types/selenium-webdriver to version 2.53.37 [\#149](https://github.com/wechaty/wechaty/pull/149) ([huan](https://github.com/huan)) +- chore\(package\): update tslint to version 4.1.1 [\#146](https://github.com/wechaty/wechaty/pull/146) ([huan](https://github.com/huan)) +- chore\(package\): update @types/sinon to version 1.16.33 [\#143](https://github.com/wechaty/wechaty/pull/143) ([huan](https://github.com/huan)) +- chore\(package\): update @types/node to version 6.0.52 [\#142](https://github.com/wechaty/wechaty/pull/142) ([huan](https://github.com/huan)) +- chore\(package\): update tslint to version 4.1.0 [\#141](https://github.com/wechaty/wechaty/pull/141) ([huan](https://github.com/huan)) +- Update README.md [\#139](https://github.com/wechaty/wechaty/pull/139) ([lijiarui](https://github.com/lijiarui)) +- qrcode [\#112](https://github.com/wechaty/wechaty/pull/112) ([lijiarui](https://github.com/lijiarui)) +- Update README.md [\#110](https://github.com/wechaty/wechaty/pull/110) ([lijiarui](https://github.com/lijiarui)) +- fixed javascript error: attempt is not defined [\#100](https://github.com/wechaty/wechaty/pull/100) ([JasLin](https://github.com/JasLin)) +- convert wechaty-bro.js to plain old javascript syntax \#60 [\#97](https://github.com/wechaty/wechaty/pull/97) ([cherry-geqi](https://github.com/cherry-geqi)) + +## [v0.6.0](https://github.com/wechaty/wechaty/tree/v0.6.0) (2016-11-11) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.5.22...v0.6.0) + +**Fixed bugs:** + +- \[Docker\] Config.isDocker is not right in some Docker version / Linux distribution [\#84](https://github.com/wechaty/wechaty/issues/84) + +## [v0.5.22](https://github.com/wechaty/wechaty/tree/v0.5.22) (2016-11-10) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.5.9...v0.5.22) + +**Implemented enhancements:** + +- To Disable WebDriverJS promise manager for Selenium v3.0 [\#72](https://github.com/wechaty/wechaty/issues/72) +- \[Upgrade to v0.5\] Convert code base to Typescript from Javascript [\#40](https://github.com/wechaty/wechaty/issues/40) + +**Closed issues:** + +- can't run demo in docker under mac [\#80](https://github.com/wechaty/wechaty/issues/80) +- ๅœจwindowsไธ‹่ฟ่กŒไพ‹ๅญ,npm ็Žฏๅขƒไธญ,ๆŠฅ้”™ๅ…ณไบŽgetChromeDriver\(\) [\#77](https://github.com/wechaty/wechaty/issues/77) + +## [v0.5.9](https://github.com/wechaty/wechaty/tree/v0.5.9) (2016-11-07) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.5.1...v0.5.9) + +**Closed issues:** + +- run on windows.error [\#75](https://github.com/wechaty/wechaty/issues/75) + +**Merged pull requests:** + +- fix: memberList Method have no 'name' argument defined ,it'will cause a undefined error. [\#78](https://github.com/wechaty/wechaty/pull/78) ([JasLin](https://github.com/JasLin)) +- fix issue \#70 [\#76](https://github.com/wechaty/wechaty/pull/76) ([JasLin](https://github.com/JasLin)) + +## [v0.5.1](https://github.com/wechaty/wechaty/tree/v0.5.1) (2016-11-03) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.4.0...v0.5.1) + +**Implemented enhancements:** + +- Dockerize Wechaty for easy start [\#66](https://github.com/wechaty/wechaty/issues/66) +- Wechatๅธๅท็•Œ้ข่ฏญ่จ€่ฎพไธบไธญๆ–‡็Žฏๅขƒไธ‹๏ผšwechaty็š„room-join room-leave room-topic ไบ‹ไปถๆ— ๆณ•่งฆๅ‘ [\#52](https://github.com/wechaty/wechaty/issues/52) +- test/fix Watchdog with browser dead & timeout conditions [\#47](https://github.com/wechaty/wechaty/issues/47) +- use StateMonitor to record&check wechaty/puppet/bridge/browser state change [\#46](https://github.com/wechaty/wechaty/issues/46) +- \[New Feature\] send message by branding new method: say\(\) [\#41](https://github.com/wechaty/wechaty/issues/41) +- \[New Feature\] Contact.{tag,star,remark,find,findAll} [\#34](https://github.com/wechaty/wechaty/issues/34) +- \[New Feature\] FriendRequest class and event [\#33](https://github.com/wechaty/wechaty/issues/33) +- \[New Feature\] Room.{create,addMember,delMember,quit,modTopic} support [\#32](https://github.com/wechaty/wechaty/issues/32) + +**Fixed bugs:** + +- Just have a try as example of tuling bot. But method message.self\(\) seems work improper. [\#68](https://github.com/wechaty/wechaty/issues/68) +- ๅœจcloud9ไธญ่ฟ่กŒwechatyๆŠฅ้”™ [\#67](https://github.com/wechaty/wechaty/issues/67) +- ๅฝ“็”จๆˆทๆ˜ต็งฐไธญๅซๆœ‰่กจๆƒ…ๆ—ถ๏ผŒๆ— ๆณ•่งฆๅ‘room-join ไบ‹ไปถ [\#64](https://github.com/wechaty/wechaty/issues/64) +- room-join ไบ‹ไปถไธ‹๏ผŒๆ— ๆณ•้€š่ฟ‡contact.id ๆ–นๆณ•่Žทๅ–contact\_id [\#54](https://github.com/wechaty/wechaty/issues/54) +- FriendRequest is not export to npm module [\#50](https://github.com/wechaty/wechaty/issues/50) + +**Closed issues:** + +- Run wechaty occurs chromedriver is still running and the solution [\#62](https://github.com/wechaty/wechaty/issues/62) +- Can't run wechaty with error log [\#61](https://github.com/wechaty/wechaty/issues/61) +- \[design\] new class: BrowserCookie [\#59](https://github.com/wechaty/wechaty/issues/59) +- ๅœจroomไธญ้€š่ฟ‡room.topic\(\)่Žทๅ–ไธๅˆฐroom็š„topic [\#55](https://github.com/wechaty/wechaty/issues/55) +- ่ฟ‘ๆœŸwechatyๅฏๅŠจๅคฑ่ดฅๆฌกๆ•ฐ่พƒๅคš [\#53](https://github.com/wechaty/wechaty/issues/53) +- TSError: โจฏ Unable to compile TypeScript src/puppet-web/event.ts \(120,12\): Type 'PuppetWeb' is not assignable to type 'void'. \(2322\) [\#51](https://github.com/wechaty/wechaty/issues/51) +- demo ๆ— ๆณ•่ฟ่กŒ [\#49](https://github.com/wechaty/wechaty/issues/49) +- Suggest give an api to get url [\#45](https://github.com/wechaty/wechaty/issues/45) +- element\_wrong----contact.get\('name'\) got room name not contact name [\#43](https://github.com/wechaty/wechaty/issues/43) +- webdrive login always occur error, for one success login always cost 4-5 log trys [\#42](https://github.com/wechaty/wechaty/issues/42) + +## [v0.4.0](https://github.com/wechaty/wechaty/tree/v0.4.0) (2016-10-08) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.2.0...v0.4.0) + +**Implemented enhancements:** + +- \[Feature Request\] Add friend\(Contact\) to a group\(Room\) [\#14](https://github.com/wechaty/wechaty/issues/14) +- Support Friend Request / Contact Add & Del [\#6](https://github.com/wechaty/wechaty/issues/6) + +**Fixed bugs:** + +- Wechaty account logout unexpectedly [\#37](https://github.com/wechaty/wechaty/issues/37) +- google-chrome fails to start in docker [\#26](https://github.com/wechaty/wechaty/issues/26) +- wx.qq.com detect phantomjs and disabled it [\#21](https://github.com/wechaty/wechaty/issues/21) + +**Closed issues:** + +- get rid of `PuppetWeb.initAttach` [\#35](https://github.com/wechaty/wechaty/issues/35) +- webdriver fail in docker when use ava \(parallel tests mode\) [\#27](https://github.com/wechaty/wechaty/issues/27) +- WARN PuppetWebBridge init\(\) inject FINAL fail [\#22](https://github.com/wechaty/wechaty/issues/22) +- node-tap strange behaviour cause CircleCI & Travis-CI keep failing [\#11](https://github.com/wechaty/wechaty/issues/11) + +**Merged pull requests:** + +- add hubot introduction in readme [\#38](https://github.com/wechaty/wechaty/pull/38) ([lijiarui](https://github.com/lijiarui)) +- Ava [\#25](https://github.com/wechaty/wechaty/pull/25) ([huan](https://github.com/huan)) + +## [v0.2.0](https://github.com/wechaty/wechaty/tree/v0.2.0) (2016-06-28) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.1.1...v0.2.0) + +## [v0.1.1](https://github.com/wechaty/wechaty/tree/v0.1.1) (2016-06-09) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.0.6...v0.1.1) + +**Fixed bugs:** + +- ding-dong bot broken due to typo [\#5](https://github.com/wechaty/wechaty/issues/5) + +## [v0.0.6](https://github.com/wechaty/wechaty/tree/v0.0.6) (2016-05-15) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/v0.0.5...v0.0.6) + +**Closed issues:** + +- selenium-webdriver & phantomjs-prebuilt not work together under win32 [\#1](https://github.com/wechaty/wechaty/issues/1) + +**Merged pull requests:** + +- Add a Gitter chat badge to README.md [\#3](https://github.com/wechaty/wechaty/pull/3) ([gitter-badger](https://github.com/gitter-badger)) + +## [v0.0.5](https://github.com/wechaty/wechaty/tree/v0.0.5) (2016-05-11) + +[Full Changelog](https://github.com/wechaty/wechaty/compare/292f50456e3bd6cb9849218348ee053c76b1d6b3...v0.0.5) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ed6d18f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Guidelines for Contributing + +Thank you for your time on Wechaty. + +## How to [contribute](https://github.com/wechaty/wechaty/blob/master/CONTRIBUTING.md) + +* Write a blog about Wechaty +* Create a tutorial for Wechaty +* Help expand the [wiki](https://github.com/wechaty/wechaty/wiki) +* Answer questions on the [issue](https://github.com/wechaty/wechaty/issues) +* Review [pull requests](https://github.com/wechaty/wechaty/pulls) +* Start [hacking on Wechaty](https://github.com/wechaty/wechaty/blob/master/CONTRIBUTING.md) + +### Why contribute + +As an open source product, Chatie thrives from contributions of community members. Whatever your skill set is, there is a lot you can do to help us make Chatie better! +So start forking! + +### Not sure where to start? + +It's a myth that writing code is the only way to contribute to open source. Chatie community is open to new ideas and there are so many different ways to make valuable contributions. We have some ideas of how you can get started! + +#### 1. Contribute Code + +- Check out GitHub issues with the tags `good first issue`, `pull request welcome` or `help wanted` +- Write code examples for documentation +- Report a bug and work on resolving it +- Collaborate with others on building new features + +**Tips:** +If you want to add new features or change the API, please submit an issue first to make sure no one else is already working on the same thing and discuss the implementation and API details with maintainers and users by creating an issue. When everything is settled down, you can submit a pull request. + +Make sure to add tests for your features and bugfixes and update the documentation (see below) before submitting your code! + +#### 2. Contribute Support + +- Fix typos in documentation +- Translate documentation to your local language +- Write tutorials and blog posts, see more: https://github.com/chatie/blog +- Answer questions on the Wechaty Developers Home or Github issues +- Organize Chatie meetups or user groups in your area + +Contact rui@chatie.io to learn more + +## Link to Wechaty + +Contribute by marketing: Add **Powered by Wechaty** Badge to your Project Homepage: + +[![Powered by Wechaty](https://img.shields.io/badge/Powered%20By-Wechaty-green.svg)](https://github.com/wechaty/wechaty) + +### Markdown + +```markdown +[![Powered by Wechaty](https://img.shields.io/badge/Powered%20By-Wechaty-green.svg)](https://github.com/wechaty/wechaty) +``` + +### Html + +```html + + Powered by Wechaty + +``` + +## Bug Report + +Contribute by feedback, make code robust. + +## Feature Request + +Contribute by make product suggestion. + +## Pull Request + +Please: + +1. submit issue to describe the problem to get start +1. then send pull request as you need(with unit test & pass linting) + + ```bash + $ npm run lint + $ npm run test + ``` + +1. and do not forget to reference the issue + +I built wechaty with pleasure because it can help others. help from you for wechaty will be very appreciated by the community. + +Cheers! + +Huan diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..edefb48 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,28 @@ +# DEVELOPMENT + +Tips: + +- [Mashup Font of FiraCode and Script12](https://github.com/kencrocken/FiraCodeiScript) +- [Monospaced font with programming ligatures](https://github.com/tonsky/FiraCode) +- [Combined programming mono font with italics and font ligatures made from Fira Code and Pacifico for Italics](https://github.com/kosimst/Firicico) + +## FiraCode + +`Font Book` -> New Collection -> Downloaded -> Save Fira Code TTFs in it. + +`.vscode/settings.json` + +```json + , "editor.fontFamily": "'Fira Code iScript', 'Fira Code', 'Courier New', Consolas, monospace" + , "editor.fontLigatures": true +``` + +## Generate Changelog + +```shell +npm run changelog +``` + +1. `403 - You have triggered an abuse detection mechanism. Please wait a few minutes before you try again. // See: https://developer.github.com/v3/#abuse-rate-limits'` + +See: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..16d80a0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,104 @@ +FROM debian:bullseye +LABEL maintainer="Huan LI (ๆŽๅ“ๆก“) " + +ENV DEBIAN_FRONTEND noninteractive +ENV WECHATY_DOCKER 1 +ENV LC_ALL C.UTF-8 +ENV NODE_ENV $NODE_ENV +ENV NPM_CONFIG_LOGLEVEL warn + +# Instal the 'apt-utils' package to solve the error 'debconf: delaying package configuration, since apt-utils is not installed' +# https://peteris.rocks/blog/quiet-and-unattended-installation-with-apt-get/ +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + apt-utils \ + autoconf \ + automake \ + bash \ + build-essential \ + ca-certificates \ + chromium \ + coreutils \ + curl \ + ffmpeg \ + figlet \ + git \ + gnupg2 \ + jq \ + libgconf-2-4 \ + libtool \ + libxtst6 \ + moreutils \ + python-dev \ + shellcheck \ + sudo \ + tzdata \ + vim \ + wget \ + && apt-get purge --auto-remove \ + && rm -rf /tmp/* /var/lib/apt/lists/* + +RUN curl -sL https://deb.nodesource.com/setup_16.x | bash - \ + && apt-get update && apt-get install -y --no-install-recommends nodejs \ + && apt-get purge --auto-remove \ + && rm -rf /tmp/* /var/lib/apt/lists/* + +WORKDIR /wechaty + +COPY package.json . +RUN npm install \ + && rm -fr /tmp/* ~/.npm + +COPY . . + +RUN ./scripts/generate-package-json.sh && rm -f src/package-json.spec.ts +RUN npm test \ + && npm run dist \ + && npm link + +# Pre-install all puppets. +# Must be placed after `npm link`, or it will be all deleted by `npm link` +RUN npm run puppet-install \ + && sudo rm -fr /tmp/* ~/.npm + +# +# Enable ES Modules support #2232 +# See: https://github.com/wechaty/wechaty/issues/2232 +# +RUN echo '{"type": "module"}' > /package.json + +# Loading from node_modules Folders: https://nodejs.org/api/modules.html +# If it is not found there, then it moves to the parent directory, and so on, until the root of the file system is reached. +RUN mkdir /node_modules \ + && ln -sfv /usr/lib/node_modules/* /node_modules/ \ + && ln -sfv /wechaty/node_modules/* /node_modules/ \ + && /wechaty/bin/clean-json.js /wechaty/tsconfig.json \ + | jq 'del(."ts-node")' > /tsconfig.json \ + && echo 'Linked Wechaty & tsconfig.json to Global' + +WORKDIR /bot + +ENTRYPOINT [ "/wechaty/bin/entrypoint.sh" ] +CMD [ "" ] + +# +# https://docs.docker.com/docker-cloud/builds/advanced/ +# http://label-schema.org/rc1/ +# +LABEL \ + org.label-schema.license="Apache-2.0" \ + org.label-schema.build-date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + org.label-schema.version="$DOCKER_TAG" \ + org.label-schema.schema-version="$(wechaty-version)" \ + org.label-schema.name="Wechaty" \ + org.label-schema.description="Wechat for Bot" \ + org.label-schema.usage="https://github.com/wechaty/wechaty/wiki/Docker" \ + org.label-schema.url="https://www.chatie.io" \ + org.label-schema.vendor="Chatie" \ + org.label-schema.vcs-ref="$SOURCE_COMMIT" \ + org.label-schema.vcs-url="https://github.com/wechaty/wechaty" \ + org.label-schema.docker.cmd="docker run -ti --rm wechaty/wechaty " \ + org.label-schema.docker.cmd.test="docker run -ti --rm wechaty/wechaty test" \ + org.label-schema.docker.cmd.help="docker run -ti --rm wechaty/wechaty help" \ + org.label-schema.docker.params="WECHATY_TOKEN=token token from https://www.chatie.io, WECHATY_LOG=verbose Set Verbose Log, TZ='Asia/Shanghai' TimeZone" + diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 0000000..a6e7f0a --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,85 @@ +# +# Wechaty Docker +# https://github.com/wechaty/wechaty +# +# FROM alpine +# +# Docker image for Alpine Linux with latest ShellCheck, a static analysis tool for shell scripts. +# https://hub.docker.com/r/nlknguyen/alpine-shellcheck/ +# FROM nlknguyen/alpine-shellcheck +FROM mhart/alpine-node:7 +LABEL maintainer="Huan LI " + +RUN apk update && apk upgrade \ + && apk add \ + bash \ + ca-certificates \ + chromium-chromedriver \ + chromium \ + coreutils \ + curl \ + ffmpeg \ + figlet \ + jq \ + moreutils \ + ttf-freefont \ + udev \ + vim \ + xauth \ + xvfb \ + && rm -rf /tmp/* /var/cache/apk/* + +RUN mkdir /wechaty +WORKDIR /wechaty + +# npm `chromedriver` not support alpine linux +# https://github.com/giggio/node-chromedriver/issues/70 +COPY package.json . +RUN sed -i '/chromedriver/d' package.json \ + && npm --silent --progress=false install > /dev/null \ + && rm -fr /tmp/* ~/.npm + +# Loading from node_modules Folders: https://nodejs.org/api/modules.html +# If it is not found there, then it moves to the parent directory, and so on, until the root of the file system is reached. +COPY . . +RUN sed -i '/chromedriver/d' package.json \ + && npm run dist \ + && npm --silent --progress=false link \ + \ + && mkdir /bot \ + \ + && ( mkdir /node_modules && cd /node_modules \ + && ln -s /wechaty . \ + && ln -s /wechaty/node_modules/* . \ + ) \ + && ln -s /wechaty/tsconfig.json / \ + && echo "export * from 'wechaty'" > /index.ts \ + \ + && echo 'Linked wechaty to global' + +VOLUME [ "/bot" ] + +ENTRYPOINT [ "/wechaty/bin/entrypoint.sh" ] +CMD [ "" ] + +# +# https://docs.docker.com/docker-cloud/builds/advanced/ +# http://label-schema.org/rc1/ +# +LABEL org.label-schema.license="ISC" \ + org.label-schema.build-date="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ + org.label-schema.version="$DOCKER_TAG" \ + org.label-schema.schema-version="$(wechaty-version)" \ + org.label-schema.name="Wechaty" \ + org.label-schema.description="Wechat for Bot" \ + org.label-schema.usage="https://github.com/wechaty/wechaty/wiki/Docker" \ + org.label-schema.url="https://www.chatie.io" \ + org.label-schema.vendor="AKA Mobi" \ + org.label-schema.vcs-ref="$SOURCE_COMMIT" \ + org.label-schema.vcs-url="https://github.com/wechaty/wechaty" \ + org.label-schema.docker.cmd="docker run -ti --rm wechaty/wechaty " \ + org.label-schema.docker.cmd.test="docker run -ti --rm wechaty/wechaty test" \ + org.label-schema.docker.cmd.help="docker run -ti --rm wechaty/wechaty help" \ + org.label-schema.docker.params="WECHATY_TOKEN=token token from https://www.chatie.io" + +#RUN npm test diff --git a/Dockerfile.onbuild b/Dockerfile.onbuild new file mode 100644 index 0000000..7b0c5c6 --- /dev/null +++ b/Dockerfile.onbuild @@ -0,0 +1,15 @@ +FROM wechaty/wechaty:next + +ONBUILD ARG NODE_ENV +ONBUILD ENV NODE_ENV $NODE_ENV + +ONBUILD WORKDIR /bot + +ONBUILD COPY package.json . +ONBUILD RUN jq 'del(.dependencies.wechaty)' package.json | sponge package.json \ + && npm install \ + && sudo rm -fr /tmp/* ~/.npm +ONBUILD COPY . . +ONBUILD RUN npm run build --if-present + +CMD [ "npm", "start" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..27f56e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016 Huan LI (ๆŽๅ“ๆก“) and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..4b8f8d6 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +Wechaty is a Conversational SDK for Chatbot Makers. +Copyright 2016-now Huan (ๆŽๅ“ๆก“) and Wechaty Community Contributors. + +This product includes software developed at +The Wechaty Organization (https://github.com/wechaty). + +This software contains code derived from the Stackoverflow, +including various modifications by GitHub. diff --git a/README.md b/README.md new file mode 100644 index 0000000..446424e --- /dev/null +++ b/README.md @@ -0,0 +1,569 @@ +# Wechaty [![Join Wechaty Discord Developer Community](https://img.shields.io/discord/916984413944967180?logo=discord&style=flat)](https://discord.gg/7q8NBZbQzt) [![NPM Version](https://img.shields.io/npm/v/wechaty?color=brightgreen)](https://www.npmjs.com/package/wechaty) [![NPM](https://github.com/wechaty/wechaty/workflows/NPM/badge.svg)](https://github.com/wechaty/wechaty/actions?query=workflow%3ANPM) [![Docker](https://github.com/wechaty/wechaty/workflows/Docker/badge.svg)](https://github.com/wechaty/wechaty/actions?query=workflow%3ADocker) + +[![Wechaty](https://wechaty.js.org/img/wechaty-logo.svg)](https://wechaty.js.org) + +[Ship.Fail](https://ship.fail) [PreAngel](https://PreAngel.AI) +[![Downloads](https://img.shields.io/npm/dm/wechaty.svg?style=flat-square)](https://www.npmjs.com/package/wechaty) +[![GitHub stars](https://img.shields.io/github/stars/wechaty/wechaty.svg?label=github%20stars)](https://github.com/wechaty/wechaty) +[![Docker Pulls](https://img.shields.io/docker/pulls/wechaty/wechaty.svg?maxAge=2592000)](https://hub.docker.com/r/wechaty/wechaty/) +[![ES Modules](https://img.shields.io/badge/ES-Modules-orange)](https://github.com/Chatie/tsconfig/issues/16) + +[![TypeScript](https://img.shields.io/badge/--3178C6?logo=typescript&logoColor=ffffff)](https://wechaty.js.org/docs/polyglot/typescript/) +[![JavaScript](https://img.shields.io/badge/--F7DF1E?logo=javascript&logoColor=000)](https://wechaty.js.org/docs/polyglot/typescript/) +[![Python](https://img.shields.io/badge/--3670A0?logo=python&logoColor=ffdd54)](https://wechaty.js.org/docs/polyglot/python/) +[![Go](https://img.shields.io/badge/--00ADD8?logo=go&logoColor=white)](https://wechaty.js.org/docs/polyglot/go/) +[![Java](https://img.shields.io/badge/--red?logo=java&logoColor=white)](https://wechaty.js.org/docs/polyglot/java/) +[![.NET](https://img.shields.io/badge/--5C2D91?logo=dotnet&logoColor=white)](https://wechaty.js.org/docs/polyglot/dotnet/) +[![PHP](https://img.shields.io/badge/--777BB4?logo=php&logoColor=white)](https://wechaty.js.org/docs/polyglot/php/) +[![Rust](https://img.shields.io/badge/--000000?logo=rust&logoColor=white)](https://wechaty.js.org/docs/polyglot/rust/) +[![Scala](https://img.shields.io/badge/--DC322F?logo=scala&logoColor=white)](https://wechaty.js.org/docs/polyglot/scala/) + +[![WeChat](https://img.shields.io/badge/--07C160?logo=wechat&logoColor=white)](https://wechaty.js.org/docs/puppet-providers/wechat) +[![Whatsapp](https://img.shields.io/badge/--25D366?logo=whatsapp&logoColor=white)](https://wechaty.js.org/docs/puppet-providers/whatsapp) + + + +## Elevator Pitch + +Wechaty is a conversational RPA SDK that simplifies the process of building chatbots. It's like a Swiss Army knife for chatbot development, providing a universal interface to various messaging platforms such as WhatsApp, WeChat, and more. With Wechaty, you write your bot code once, and it runs on any of the supported platforms. This means you can focus on creating engaging conversational experiences without worrying about the underlying complexities of each platform's API. It's open-source, easy to use, and backed by a vibrant community that's ready to help you bring your chatbot ideas to life. + +## Connecting Chatbots + +Wechaty is an RPA (Robotic Process Automation) SDK for Chatbot Makers which can help you create a bot in 6 lines of [JavaScript](https://GitHub.com/Wechaty/wechaty), [Python](https://GitHub.com/Wechaty/python-wechaty/), [Go](https://GitHub.com/Wechaty/go-wechaty/), and [Java](https://GitHub.com/Wechaty/java-wechaty/), with cross-platform support including [Linux, Windows, MacOS](https://github.com/wechaty/wechaty/actions?query=workflow%3ANPM), and [Docker](https://github.com/wechaty/wechaty/actions?query=workflow%3ADocker). + +:spider_web: +:octocat: +:beetle: +:book: +:whale: + +## Breaking News + +- [้‡็ฃ…๏ผš็ป•่ฟ‡็™ปๅฝ•้™ๅˆถ๏ผŒwechatyๅ…่ดน็‰ˆwebๅ่ฎฎ้‡ๆ”พ่ฃๅ…‰, @gengchen528, Apr 13, 2021](https://wechaty.js.org/2021/04/13/wechaty-uos-web/) + +## Voice of Developers + +> "Wechaty is a great solution; I believe there would be much more users who recognize it." [link](https://github.com/Wechaty/wechaty/pull/310#issuecomment-285574472) +> — @Gcaufy, Tencent Engineer, Author of [WePY](https://github.com/Tencent/wepy) +> +> "ๅคชๅฅฝ็”จ๏ผŒๅฅฝ็”จ็š„ๆƒณๅ“ญ" +> — @xinbenlv, Google Engineer, Founder of HaoShiYou.org +> +> โ€ๅฅฝ็”จๅˆฐๅ“ญโ€œโ€”โ€”ไฝ ไปฌๅฏนๅพ—่ตท่ฟ™ไธช่ฏ„ไปท๏ผ [link](https://github.com/bigbrother666sh/shezhangbujianle#%E8%87%B4%E6%95%AC) +> — [@bigbrother666sh](https://github.com/bigbrother666sh), creator ofใ€Š็คพ้•ฟไธ่งไบ†ใ€‹ๅ‰งๆœฌๆ€ NPC DM +> +> "ๆœ€ๅฅฝ็š„ๅพฎไฟกๅผ€ๅ‘ๅบ“" [link](http://weibo.com/3296245513/Ec4iNp9Ld?type=comment) +> — @Jarvis, Baidu Engineer +> +> "Wechaty่ฎฉ่ฟ่ฅไบบๅ‘˜ๆ›ดๅคš็š„ๆ—ถ้—ดๆ€่€ƒๅฆ‚ไฝ•่ฟ›่กŒๆดปๅŠจ็ญ–ๅˆ’ใ€็•™ๅญ˜็”จๆˆท๏ผŒๅ•†ไธšๅ˜็Žฐ" [link](http://mp.weixin.qq.com/s/dWHAj8XtiKG-1fIS5Og79g) +> — @lijiarui, Founder & CEO of Juzi.BOT. +> +> "If you know js ... try Wechaty. It's easy to use." +> — @Urinx Uri Lee, Author of [WeixinBot(Python)](https://github.com/Urinx/WeixinBot) +> +> "Wechaty is a good project; I hope it can continue! Therefore, I became a contributor in open collective." +> — [@Simple](https://github.com/mrwhh) + +See more at [Wiki:Voice Of Developer](https://github.com/Wechaty/wechaty/wiki/Voice%20Of%20Developer) + +### Join Us on Discord + +[![Join Wechaty Discord Developer Community](https://img.shields.io/discord/916984413944967180?logo=discord&style=flat)](https://discord.gg/7q8NBZbQzt) +[![Wechaty Discord](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fdiscord.com%2Fapi%2Finvites%2F7q8NBZbQzt%3Fwith_counts%3Dtrue&query=%24.approximate_member_count&logo=discord&logoColor=white&label=Users%20Total&color=green&style=flat)](https://discord.gg/7q8NBZbQzt) + +Wechaty is used in many ChatBot projects by thousands of developers. To talk with other developers, scan the QR Code below and join our **Wechaty Developer Community**. + +![Wechaty Discord Community QR Code](https://wechaty.js.org/img/discord-qrcode.svg) + +Scan now because other Wechaty developers want to talk with you, too! + +### Resource + +Wechaty has already held lots of talks and got a lot of blogs in the past years; here are all of the wechaty resources: + +- :video_camera: [Youtube Playlist: Watch all of the talk videos related with Wechaty](https://www.youtube.com/playlist?list=PL8hd9KDTdarDXf_Rxtr8meKhxtgcXMInh) +- :page_with_curl: [Full Docs](https://wechaty.js.org/) +- :bulb: [Blog: See how developers use wechaty to build fantastic projects!](https://wechaty.github.io/) +- :beginner: [Wechaty-Getting-Started:Wechaty Starter Project Template that Works Out-of-the-Box](https://github.com/wechaty/wechaty-getting-started) +- :tada: [Wechaty Contributor List: Thanks for their contribution!](https://github.com/wechaty/wechaty/wiki/Contributors) +- :gift: [Juzibot Support Wechaty: Know everything about Wechaty](https://github.com/juzibot/Welcome/wiki/Everything-about-Wechaty) + +## :rocket: The World's Shortest ChatBot Code: 6 lines of JavaScript + +```javascript + +import { WechatyBuilder } from 'wechaty' + +const wechaty = WechatyBuilder.build() // get a Wechaty instance +wechaty + .on('scan', (qrcode, status) => console.log(`Scan QR Code to login: ${status}\nhttps://wechaty.js.org/qrcode/${encodeURIComponent(qrcode)}`)) + .on('login', user => console.log(`User ${user} logged in`)) + .on('message', message => console.log(`Message: ${message}`)) +wechaty.start() +``` + +> **Notice: Wechaty requires Node.js version >= 16** + +This bot can log all messages to the console after login by the scan. + +You can find Wechaty's Official Examples at [examples/ding-dong-bot.ts](examples/ding-dong-bot.ts) and more from our [Example Directory](https://github.com/Wechaty/wechaty-getting-started/blob/main/examples/). + +## :checkered_flag: Requirements + +1. Node.js version 16+ +1. NPM version 7+ +1. TypeScript version 4.4+ + +## Getting Started + +[![node](https://img.shields.io/node/v/wechaty.svg?maxAge=604800)](https://nodejs.org/) + +- Wechaty Starter Repository - + +We have a Wechaty starter repository for beginners with the simplest setting. It will be **work** out-of-the-box after you `clone` & `npm install` & `npm start`. + +If you are new to Wechaty and want to try it the first time, we'd like to strong recommend you starting from this repository, and using it as your starter template for your project. + +Otherwise, please saved the above _The World's Shortest ChatBot Code: 6 lines of JavaScript_ example to a file named `bot.js` before you can use either NPM or Docker to run it. + +### 1. Npm + +[![NPM Version](https://img.shields.io/npm/v/wechaty?color=brightgreen&label=wechaty%40latest)](https://www.npmjs.com/package/wechaty) +[![npm (tag)](https://img.shields.io/npm/v/wechaty/next?color=yellow&label=wechaty%40next)](https://www.npmjs.com/package/wechaty?activeTab=versions) + +[![Downloads](https://img.shields.io/npm/dm/wechaty.svg?style=flat-square)](https://www.npmjs.com/package/wechaty) +[![install size](https://packagephobia.now.sh/badge?p=wechaty)](https://packagephobia.now.sh/result?p=wechaty) + +```shell +npm init +npm install wechaty + +# create your first bot.js file, you can copy/paste from the above "The World's Shortest ChatBot Code: 6 lines of JavaScript" +# then: +node bot.js +``` + +### 2. Docker + +[![Docker Pulls](https://img.shields.io/docker/pulls/wechaty/wechaty.svg?maxAge=2592000)](https://hub.docker.com/r/wechaty/wechaty/) +[![Docker Layers](https://images.microbadger.com/badges/image/wechaty/wechaty.svg)](https://microbadger.com/#/images/wechaty/wechaty) + +- Wechaty Starter Repository for Docker - + +> Wechaty Docker supports both JavaScript and TypeScript. To use TypeScript just write in TypeScript and save with extension name `.ts`, no need to compile because we use `ts-node` to run it. + +2.1. Run JavaScript + +```shell +# for JavaScript +docker run -ti --rm --volume="$(pwd)":/bot wechaty/wechaty bot.js +``` + +2.2. Run TypeScript + +```shell +# for TypeScript +docker run -ti --rm --volume="$(pwd)":/bot wechaty/wechaty bot.ts +``` + +> Learn more about Wechaty Docker at [Wiki:Docker](https://github.com/Wechaty/wechaty/wiki/Docker). + +### 3. Switch Protocol(Puppet) + +Wechaty is very powerful that it can run over different protocols. You can specify the protocol by set the environment variable `WECHATY_PUPPET` to different puppet provider. + +If you cannot use Web protocol, you can apply other protocols following the instruction here: We provide free token to support developers build a valuable WeChat chatbot. + +Currently we support the following [puppet providers](https://wechaty.js.org/docs/puppet-services/) : + +| Protocol | Puppet Provider | Environment Variable | +| --- | --- | --- | +| Web | [PuppetPuppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer) | `export WECHATY_PUPPET=wechaty-puppet-puppeteer` | +| Windows | [PuppetWorkPro](https://github.com/RPAChat/workpro-getting-started) | `export WECHATY_PUPPET=wechaty-puppet-service` | +| Mock | [PuppetMock](https://github.com/wechaty/wechaty-puppet-mock) | `export WECHATY_PUPPET=wechaty-puppet-mock` | +| Web | [PuppetWechat4u](https://github.com/wechaty/wechaty-puppet-wechat4u) | `export WECHATY_PUPPET=wechaty-puppet-wechat4u` | +| iPad | [PuppetRock](https://github.com/wechaty/puppet-service-providers) | `export WECHATY_PUPPET=wechaty-puppet-service` | +| iPad | [PuppetPadLocal](https://github.com/wechaty/puppet-service-providers) | `export WECHATY_PUPPET=wechaty-puppet-service` | +| Windows | [PuppetDonut](https://github.com/wechaty/puppet-service-providers) | `export WECHATY_PUPPET=wechaty-puppet-service` | +| iPad | ~~PuppetPadpro~~ **DEPRECATED** | `export WECHATY_PUPPET=wechaty-puppet-padpro` | +| iPad | ~~PuppetPadchat~~ **DEPRECATED** | `export WECHATY_PUPPET=wechaty-puppet-padchat` | +| iPad | ~~PuppetPadplus~~ **DEPRECATED** | `export WECHATY_PUPPET=wechaty-puppet-padplus` | +| Mac | ~~PuppetMacpro~~ **DEPRECATED** | `export WECHATY_PUPPET=wechaty-puppet-macpro` | +| Windows | ~~PuppetWxwork~~ **DEPRECATED** | `export WECHATY_PUPPET=wechaty-puppet-service` | + +Learn more about Wechaty Puppet from the Puppet Wiki: + +1. Puppet Directory: +1. Puppet Compatibility: + +## :guitar: API + +Read the Full Documentation at [Wechaty Official API Reference](https://wechaty.github.io/wechaty/) + +### 1 Class `Wechaty` + +Main bot class. + +A `Bot` is a Wechaty instance that control a specific [wechaty-puppet](https://wechaty.js.org/docs/specifications/puppet/). + +- `new Wechaty(options?: WechatyOptions)` + 1. `options.name?: string` the name of this bot(optional) + 2. `options.puppet?: string` select which puppet provider we want to use. must be one of the: + 1. [wechaty-puppet-wechat](https://github.com/Wechaty/wechaty-puppet-wechat) - Angular Hook for Web Wechat + 2. [wechaty-puppet-wechat4u](https://github.com/Wechaty/wechaty-puppet-wechat4u) - HTTP API for Web Wechat + 3. [wechaty-puppet-padpro](https://github.com/botorange/wechaty-puppet-padpro) - iPad App Protocol + 4. [wechaty-puppet-ioscat](https://github.com/linyimin-bupt/wechaty-puppet-ioscat) - iPhone App Hook + 5. [wechaty-puppet-mock](https://github.com/Wechaty/wechaty-puppet-mock) - Mock for Testing + 3. `options.puppetOptions?: PuppetOptions` options for the puppet provider. + +| Wechaty | API | Description | +| :--- | :--- | :--- | +| event | [`login`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit after bot login full successful | +| event | [`logout`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit after the bot log out | +| event | [`friendship`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit when someone sends bot a friend request| +| event | [`message`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit when there's a new message | +| event | [`room-join`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit when anyone join any room | +| event | [`room-topic`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit when someone change room topic | +| event | [`room-leave`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit when anyone leave the room | +| event | [`room-invite`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit when there is a room invitation | +| event | [`scan`](https://wechaty.github.io/wechaty/#Wechaty+on) | emit when the bot needs to show you a QR Code for scanning | +| method | [`start(): Promise`](https://wechaty.github.io/wechaty/#Wechaty+start) | start the bot | +| method | [`stop(): Promise`](https://wechaty.github.io/wechaty/#Wechaty+stop) | stop the bot | +| method | [`logonoff(): boolean`](https://wechaty.github.io/wechaty/#Wechaty+logonoff) | bot login status | +| method | [`logout(): Promise`](https://wechaty.github.io/wechaty/#Wechaty+logout) | logout the bot | +| method | [`currentUser(): ContactSelf`](https://wechaty.github.io/wechaty/#Wechaty+currentUser) | get the login-ed bot contact | +| method | [`say(text: string): Promise`](https://wechaty.github.io/wechaty/#Wechaty+say) | let bot say `text` to itself | + +### 2 Class `Contact` + +All wechat contacts(friends/non-friends) will be encapsulated as a Contact. + +| Contact | API | Description | +| :--- | :--- | :--- | +| static | [`find(query: string): Promise`](https://wechaty.github.io/wechaty/#Contact.find) | find contact by name or alias, if the result more than one, return the first one. | +| static | [`findAll(query: string): Promise`](https://wechaty.github.io/wechaty/#Contact.findAll) | find contact by `name` or `alias` | +| static | [`load(query: string): Contact`](https://wechaty.github.io/wechaty/#Contact.load) | get contact by id | +| property | `id: readonly string` | get contact id | +| method | [`sync(): Promise`](https://wechaty.github.io/wechaty/#Contact+sync) | force reload data for contact , sync data from lowlevel API again| +| method | [`say(text: string): Promise`](https://wechaty.github.io/wechaty/#Contact+say) | send text, Contact, or file to contact, return the message which the bot sent (only `puppet-padplus` supported). | +| method | [`self(): boolean`](https://wechaty.github.io/wechaty/#Contact+self) | check if contact is self | +| method | [`name(): string`](https://wechaty.github.io/wechaty/#Contact+name) | get the name from a contact | +| method | [`alias(): Promise`](https://wechaty.github.io/wechaty/#Contact+alias) | get the alias for a contact | +| method | [`alias(newAlias: string): Promise`](https://wechaty.github.io/wechaty/#Contact+alias) | set or delete the alias for a contact | +| method | [`friend(): boolean`](https://wechaty.github.io/wechaty/#Contact+friend) | check if contact is friend | +| method | [`type(): ContactType`](https://wechaty.github.io/wechaty/#Contact+type) | return the type of the Contact | +| method | [`province(): string`](https://wechaty.github.io/wechaty/#Contact+province) | get the region 'province' from a contact | +| method | [`city(): string`](https://wechaty.github.io/wechaty/#Contact+city) | get the region 'city' from a contact | +| method | [`avatar(): Promise`](https://wechaty.github.io/wechaty/#Contact+avatar) | get avatar picture file stream | +| method | [`gender(): ContactGender`](https://wechaty.github.io/wechaty/#Contact+gender) | get gender from a contact | + +#### 2.1 Class `ContactSelf` + +Class `ContactSelf` is extended from `Contact`. + +| ContactSelf | API | Description | +| :--- | :--- | :--- | +| method | [`avatar(file: FileBox): Promise`](https://wechaty.github.io/wechaty/#ContactSelf+avatar) | set avatar for bot | +| method | [`qrcode(): Promise`](https://wechaty.github.io/wechaty/#ContactSelf+qrcode) | get qrcode for bot | +| method | [`signature(text: string): Promise`](https://wechaty.github.io/wechaty/#ContactSelf+signature) | set signature for bot | + +#### 2.2 Class `Friendship` + +Send, receive friend request, and friend confirmation events. + +| Friendship | API | Description | +| :--- | :--- | :--- | +| static | [`add(contact: Contact, hello?: string): Promise`](https://wechaty.github.io/wechaty/#Friendship.add) | send a friend invitation to contact | +| method | [`accept(): Promise`](https://wechaty.github.io/wechaty/#Friendship+accept) | accept Friend Request | +| method | [`hello(): string`](https://wechaty.github.io/wechaty/#Friendship+hello) | get the hello string from a friendship invitation | +| method | [`contact(): Contact`](https://wechaty.github.io/wechaty/#Friendship+contact) | get the contact from friendship | +| method | [`type(): FriendshipType`](https://wechaty.github.io/wechaty/#Friendship+type) | return the Friendship Type(unknown, confirm, receive, verify) | + +### 3 Class `Message` + +All wechat messages will be encapsulated as a Message. + +| Message | API | Description | +| :--- | :--- | :--- | +| static | [`find(query: string): Promise`](https://wechaty.github.io/wechaty/#Message.find) | find message in cache and return the first one | +| static | [`findAll(query: string): Promise`](https://wechaty.github.io/wechaty/#Message.findAll) | find messages in cache, return a message list | +| method | [`from(): Contact`](https://wechaty.github.io/wechaty/#Message+from) | get the sender from a message | +| method | [`to(): Contact`](https://wechaty.github.io/wechaty/#Message+to) | get the destination of the message | +| method | [`room(): null \| Room`](https://wechaty.github.io/wechaty/#Message+room) | get the room from the message.(If the message is not in a room, then will return `null`) | +| method | [`text(): string`](https://wechaty.github.io/wechaty/#Message+text) | get the text content of the message | +| method | [`say(text: string): Promise`](https://wechaty.github.io/wechaty/#Message+say) | reply a Text, Media File , or contact message to the sender, return the message which the bot sent (only `puppet-padplus` supported). | +| method | [`type(): MessageType`](https://wechaty.github.io/wechaty/#Message+type) | get the type from the message | +| method | [`self(): boolean`](https://wechaty.github.io/wechaty/#Message+self) | check if a message is sent by self | +| method | [`mention(): Contact[]`](https://wechaty.github.io/wechaty/#Message+mention) | get message mentioned contactList. | +| method | [`mentionSelf(): boolean`](https://wechaty.github.io/wechaty/#Message+mentionSelf) | check if a message is mention self | +| method | [`forward(to: Contact): Promise`](https://wechaty.github.io/wechaty/#Message+forward) | Forward the received message | +| method | [`age(): number`](https://wechaty.github.io/wechaty/#Message+age) | the number of seconds since it has been created | +| method | [`date(): Date`](https://wechaty.github.io/wechaty/#Message+date) | the time it was created | +| method | [`toFileBox(): Promise`](https://wechaty.github.io/wechaty/#Message+toFileBox) | extract the Media File from the Message, and put it into the FileBox. | +| method | [`toContact(): Promise`](https://wechaty.github.io/wechaty/#Message+toContact) | get Share Card of the Message | + +### 4 Class `Room` + +All wechat rooms(groups) will be encapsulated as a Room. + +| Room | API | Description | +| :--- | :--- | :--- | +| static | [`create(contactList: Contact[], topic?: string): Promise`](https://wechaty.github.io/wechaty/#Room.create) | create a new room | +| static | [`find(query: string): Promise`](https://wechaty.github.io/wechaty/#Room.find) | Try to find a room by filter. If get many, return the first one. | +| static | [`findAll(query: string): Promise`](https://wechaty.github.io/wechaty/#Room.findAll) | Find all contacts in a room | +| static | [`load(query: string): Room`](https://wechaty.github.io/wechaty/#Room.load) | load room by room id | +| property | `id: readonly string` | | +| event | [`join`](https://wechaty.github.io/wechaty/#Room+on) | emit when anyone join any room | +| event | [`topic`](https://wechaty.github.io/wechaty/#Room+on) | emit when someone change room topic | +| event | [`leave`](https://wechaty.github.io/wechaty/#Room+on) | emit when anyone leave the room | +| event | [`invite`](https://wechaty.github.io/wechaty/#Room+on) | emit when receive a room invitation | +| method | [`sync(): `](https://wechaty.github.io/wechaty/#Room+sync) | force reload data for room, sync data from lowlevel API again. +| method | [`say(text: string): Promise`](https://wechaty.github.io/wechaty/#Room+say) | Send text,media file, contact card, or text with mention @mention contact inside Room, return the message which the bot sent (only `puppet-padplus` supported). | +| method | [`add(contact: Contact): Promise`](https://wechaty.github.io/wechaty/#Room+add) | Add contact in a room | +| method | [`del(contact: Contact): Promise`](https://wechaty.github.io/wechaty/#Room+del) | Delete a contact from the room | +| method | [`quit(): Promise`](https://wechaty.github.io/wechaty/#Room+quit) | Bot quit the room itself | +| method | [`topic(): Promise`](https://wechaty.github.io/wechaty/#Room+topic) | GET topic from the room | +| method | [`topic(newTopic: string): Promise`](https://wechaty.github.io/wechaty/#Room+topic) | SET topic from the room | +| method | [`announce(text: string): Promise`](https://wechaty.github.io/wechaty/#Room+announce) | SET/GET announce from the room | +| method | [`qrcode(): Promise`](https://wechaty.github.io/wechaty/#Room+qrcode) | Get QR Code of the Room from the room, which can be used as scan and join the room. | +| method | [`alias(contact: Contact): Promise`](https://wechaty.github.io/wechaty/#Room+alias) | Return contact's roomAlias in the room | +| method | [`roomAlias(contact: Contact): Promise`](https://wechaty.github.io/wechaty/#Room+roomAlias) | Same as function alias | +| method | [`has(contact: Contact): Promise`](https://wechaty.github.io/wechaty/#Room+has) | Check if the room has member `contact` | +| method | [`memberAll(query?: string): Promise`](https://wechaty.github.io/wechaty/#Room+memberAll) | Find all contacts or with specific name in a room | +| method | [`member(query: string): Promise`](https://wechaty.github.io/wechaty/#Room+member) | Find all contacts in a room, if get many, return the first one. | +| method | [`memberList():Promise`](https://wechaty.github.io/wechaty/#Room+memberList) | get all room member from the room | +| method | [`owner(): null \| Contact`](https://wechaty.github.io/wechaty/#Room+owner) | Get room's owner from the room. | + +#### 4.1 Class `RoomInvitation` + +Accept room invitation + +| RoomInvitation | API | Description | +| :--- | :--- | :--- | +| method | [`accept(): Promise`](https://wechaty.github.io/wechaty/#RoomInvitation+accept) | accept Room Invitation | +| method | [`inviter(): Contact`](https://wechaty.github.io/wechaty/#RoomInvitation+inviter) | get the inviter from room invitation | +| method | [`roomTopic(): Promise`](https://wechaty.github.io/wechaty/#RoomInvitation+inviter) | get the room topic from room invitation | +| method | [`date(): Promise`](https://wechaty.github.io/wechaty/#RoomInvitation+date) | the time it was created | +| method | [`age(): Promise`](https://wechaty.github.io/wechaty/#RoomInvitation+age) | the number of seconds since it has been created | + +## TEST + +[![NPM](https://github.com/wechaty/wechaty/workflows/NPM/badge.svg)](https://github.com/wechaty/wechaty/actions?query=workflow%3ANPM) +[![Docker](https://github.com/wechaty/wechaty/workflows/Docker/badge.svg)](https://github.com/wechaty/wechaty/actions?query=workflow%3ADocker) +[![Coverage Status](https://coveralls.io/repos/github/wechaty/wechaty/badge.svg?branch=main)](https://coveralls.io/github/wechaty/wechaty?branch=main) + +[![Known Vulnerabilities](https://snyk.io/test/github/Wechaty/wechaty/badge.svg)](https://snyk.io/test/github/Wechaty/wechaty) + +Wechaty is fully automatically tested by unit and integration tests, with Continuous Integration & Continuous Delivery (CI/CD) support powered by CI like Travis, Shippable and Appveyor. + +To test Wechaty, run: + +```shell +npm test +``` + +Get to know more about the tests from [Wiki:Tests](https://github.com/Wechaty/wechaty/wiki/Tests) + +## CREATING WECHATY PLUGIN + +Creating and publishing a Wechaty Plugin is simple. Simply expose your module as a function that takes 1 parameter: wechaty. When your plugin is imported by Wechaty, it will pass itself in as the argument, and so you are free to add any configuration that Wechaty supports. + +```ts +import { WechatyPlugin } from 'wechaty' + +export default const MyPlugin: WechatyPlugin = (wechaty: Wechaty) => { + // ... +} +``` + +The `config` exist so the user can pass in customizations to your Plugin. In documenting your Wechaty Plugin, you would lay out your supported config for the user. + +See: + +1. [Wechaty Plugin Support with KickOut Example #1939](https://github.com/wechaty/wechaty/issues/1939) +1. [Wechaty Plugins Contrib](https://github.com/wechaty/wechaty-plugin-contrib) + +## :pencil: RELEASE NOTES + +- [Latest Release](https://github.com/Wechaty/wechaty/releases/latest)(All releases [here](https://github.com/Wechaty/wechaty/releases)) +- [Changelog](https://github.com/Wechaty/wechaty/blob/main/CHANGELOG.md) + +### :saxophone: Views Since Feb 15, 2019 + +[![HitCount](http://hits.dwyl.io/wechaty/wechaty.svg)](http://hits.dwyl.io/wechaty/wechaty) + +## :sparkling_heart: POWERED BY WECHATY + +[![Powered by Wechaty](https://img.shields.io/badge/Powered%20By-Wechaty-brightgreen.svg)](https://wechaty.js.org) + +### :sparkles: Wechaty Badge + +```markdown +[![Powered by Wechaty](https://img.shields.io/badge/Powered%20By-Wechaty-brightgreen.svg)](https://wechaty.js.org) +``` + +Get more embed html/markdown code from [Wiki:Badge](https://github.com/wechaty/wechaty/wiki/Badge) + +### :star2: Projects Using Wechaty + +1. [ไธ€ไธช็”จCNNๆทฑๅบฆ็ฅž็ป็ฝ‘็ปœ็ป™ๅ›พ็‰‡่ฏ„ๅˆ†็š„wechaty้กน็›ฎ](https://github.com/huyingxi/wechaty_selfie) +2. [Relay between Telegram and WeChat](https://github.com/Firaenix/TeleChatRelay) +3. [A chat bot managing the HaoShiYou wechat groups run by volunteers of haoshiyou.org](https://github.com/xinbenlv/haoshiyou-bot) +4. [An interactive chat bot to manage a TODO list](https://github.com/coderbunker/candobot) +5. [Forward WeChat messages to telegram](https://github.com/luosheng/Wegram) +6. [koaไธŽwechatyๅฎž็Žฐ็š„ๅพฎไฟกๅฐๅŠฉๆ‰‹๏ผŒๅฏๅฎšๆ—ถๆ้†’ไธŽๅ‘ๆถˆๆฏ่ฎพๅฎšๅฎšๆ—ถไปปๅŠก](https://github.com/gengchen528/wechat-assistant) +7. [Wechaty Pay - ่ฎฉ็บฟไธŠๆฒกๆœ‰้šพๅš็š„็”Ÿๆ„](https://github.com/coderwhocode/wechaty-pay) +8. [ๅผ€ๆบ็คพ็š„ๅพฎไฟกๆœบๅ™จไบบ้กน็›ฎ](https://github.com/kaiyuanshe/wechat-robot) + +Pull Request is welcome to add yours! + +Learn more about Projects Using Wechaty at [Wiki:Projects Using Wechaty](https://github.com/wechaty/wechaty/wiki/Projects-Using-Wechaty) + +## :innocent: Find a Good Server + +The best practice for running Wechaty Docker/NPM is using a VPS(Virtual Private Server) outside of China, which can save you hours of time because `npm install` and `docker pull` will run smoothly without any problem. + +The following VPS providers are used by the Wechaty team, and they worked perfectly in production. You can use the following link to get one in minutes. Also, doing this can support Wechaty because you are referred by us. + +| Location | Price | Ram | Payment | Provider | +| --- | --- | --- | --- | --- | +| Singapore | $5 | 512MB | Paypal | [DigitalOcean](https://m.do.co/c/01a54778df5c) | +| Japan | $5 | 1GB | Paypal | [Linode](https://www.linode.com/?r=5fd2b713d711746bb5451111df0f2b6d863e9f63) | +| Korea | $10 | 1GB | Alipay, Paypal | [Netdedi](https://www.netdedi.com/?affid=35) | +| Singapore | $3.5 | 512MB | Alipay, Wechat | [Vultr](https://www.vultr.com/?ref=6986613) | + +## :notes: See Also + +- [RelatedProject](https://github.com/Wechaty/wechaty/wiki/RelatedProject) + +## :poop: The Story + +In 2017 ... + +Huan's daily life/work depends on too much chat on wechat. + +- Almost 14,000 wechat friends in May 2014, before wechat restricts a total number of friends to 5,000. +- Almost 400 wechat rooms, and most of them have more than 400 members. + +Can you imagine that? He was dying... + +So a tireless bot working for me 24x7 on wechat, monitoring/filtering the most important message is badly needed. For example, it highlights discussion which contains the KEYWORDS which he want to follow up(especially in a noisy room). ;-) + +At last, It's built for huan's personal study purpose of Automatically Testing. + +## Stargazers over time + +[![Stargazers over time](https://starchart.cc/wechaty/wechaty.svg)](https://starchart.cc/wechaty/wechaty) + +## :two_hearts: Contributors + +[![GitHub issues](https://img.shields.io/github/issues/wechaty/wechaty.svg)](https://github.com/Wechaty/wechaty/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/wechaty/wechaty.svg)](https://github.com/Wechaty/wechaty/pulls) +[![Open Collective Backers](https://opencollective.com/wechaty/backer/badge.svg?label=open%20collective%20backers&color=blue)](https://opencollective.com/wechaty/) +[![Open Collective Sponsors](https://opencollective.com/wechaty/sponsors/badge.svg?label=open%20collective%20sponsors&color=blue)](https://opencollective.com/wechaty/) + +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/0)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/0) +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/1)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/1) +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/2)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/2) +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/3)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/3) +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/4)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/4) +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/5)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/5) +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/6)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/6) +[![contributor](https://sourcerer.io/fame/huan/wechaty/wechaty/images/7)](https://sourcerer.io/fame/huan/wechaty/wechaty/links/7) + +This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. + +----- + +[![Contribute](https://opencollective.com/wechaty/contributors.svg?width=890&button=false)](https://github.com/Wechaty/wechaty/graphs/contributors) + +## :sunglasses: Backers + +[![Backers on Open Collective](https://opencollective.com/wechaty/backers/badge.svg)](#sunglasses-backers) + +Thank you to all our backers! ๐Ÿ™ [[Become a backer](https://opencollective.com/wechaty#backer)] + +[![Open Collective Wechaty](https://opencollective.com/wechaty/backers.svg?width=890)](https://opencollective.com/wechaty#backers) + +## :smirk: Sponsors + +[![Sponsors on Open Collective](https://opencollective.com/wechaty/sponsors/badge.svg)](#smirk-sponsors) + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/wechaty#sponsor)] + +[![Wechaty Sponsor](https://opencollective.com/wechaty/sponsor.svg?width=890)](https://opencollective.com/wechaty/#sponsor) + +## Multi-language Wechaty + +[![Python Wechaty](https://img.shields.io/badge/Wechaty-Python-blue)](https://github.com/wechaty/python-wechaty) +[![Go Wechaty](https://img.shields.io/badge/Wechaty-Go-7de)](https://github.com/wechaty/go-wechaty) +[![Java(Kotlin) Wechaty](https://img.shields.io/badge/Wechaty-Java-f80)](https://github.com/wechaty/java-wechaty) +[![Scala Wechaty](https://img.shields.io/badge/Wechaty-Scala-890)](https://github.com/wechaty/scala-wechaty) +[![PHP Wechaty](https://img.shields.io/badge/Wechaty-PHP-99c)](https://github.com/wechaty/php-wechaty) +[![.NET(C#) Wechatyin](https://img.shields.io/badge/Wechaty-.NET-629)](https://github.com/wechaty/dotnet-wechaty) + +- [Wechaty](https://github.com/wechaty/wechaty) - RPA SDK for Chatot Makers (TypeScript) +- [Python Wechaty](https://github.com/wechaty/python-wechaty) - RPA SDK for Chatbot Makers written in Python +- [Go Wechaty](https://github.com/wechaty/go-wechaty) - RPA SDK for Chatbot Makers written in Go +- [Java Wechaty](https://github.com/wechaty/java-wechaty) - RPA SDK for Chatbot Makers written in Java(Kotlin) +- [Scala Wechaty](https://github.com/wechaty/scala-wechaty) - RPA SDK for Chatbot Makers written in Scala +- [PHP Wechaty](https://github.com/wechaty/php-wechaty) - RPA SDK for Chatbot Makers written in PHP +- [.Net(C#) Wechaty](https://github.com/wechaty/dotnet-wechaty) - RPA SDK for Chatbot Makers written in .NET(C#) + +## History + +### main v1.11 (Nov 22, 2021) + +Working on reduxify the Wechaty ecosystem for applying the CQRS pattern. + +### v1.10 (Nov 21, 2021) + +Second beta release of Wechaty, with all ecosystem npm modules with version v1.10 (wechaty-puppet, wechaty-puppet-service, etc) + +### v1.0 (Sep 2021) + +- Release v1.0 of Wechaty is the first beta release of Wechaty. + +### v0.69 + +1. v0.69: Supports ES Modules (with CJS dual support) ([#2232](https://github.com/wechaty/wechaty/issues/2232)) + +### v0.68 (Aug 27, 2021) + +1. TLS support ([#2231](https://github.com/wechaty/wechaty/issues/2231)) +1. The latest CommonJS version + +## Creators + +1. [Huan](https://github.com/huan) [(LinkedIn)](http://linkedin.com/in/huan42), ๐Ÿง Tencent TVP of Chatbotใƒป๐Ÿค– Chatie Architectใƒปโญ๏ธ GitHub Starใƒป๐Ÿš€ YC W19ใƒป๐ŸŒ Microsoft RD & AI MVPใƒป๐Ÿฆพ Google ML GDE ใƒป๐Ÿค  Serial Entrepreneurใƒป๐Ÿ”ฅ Burner +1. [Rui (ๆŽไฝณ่Šฎ)](https://pre-angel.com/peoples/jiarui-li/), Microsoft AI MVP & RD, Co-founder & CEO of [Juzi.BOT](https://www.juzibot.com/) (YC W19 Alumni) + +[![Profile of Huan Li on StackOverflow](https://stackoverflow.com/users/flair/1123955.png)](https://stackoverflow.com/users/1123955/huan) + +## Cite Wechaty + +To cite this project in publications: + +```bibtex +@misc{Wechaty, + author = {Huan Li, Rui Li}, + title = {Wechaty: Conversational SDK for Chatbot Makers}, + year = {2016}, + publisher = {GitHub}, + journal = {GitHub Repository}, + howpublished = {\url{https://github.com/wechaty/wechaty}}, +} +``` + +## Copyright & License + +- Code & Docs ยฉ 2016-now Huan, Rui, and Wechaty Community Contributors +- Code released under the Apache-2.0 License +- Docs released under Creative Commons diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..a6682a0 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub ๆฅๆบ่ฏดๆ˜Ž + +- ๅŽŸๅง‹้กน็›ฎ๏ผš`wechaty/wechaty` +- ๅŽŸๅง‹ไป“ๅบ“๏ผšhttps://github.com/wechaty/wechaty +- ๅฏผๅ…ฅๆ–นๅผ๏ผšไธŠๆธธ้ป˜่ฎคๅˆ†ๆ”ฏ็š„ๆœ€ๆ–ฐๅฟซ็…ง +- ๅŽŸไฝœ่€…ใ€็‰ˆๆƒๅ’Œ่ฎธๅฏ่ฏไฟกๆฏไปฅๅŽŸๅง‹ไป“ๅบ“ๅŠๆœฌไป“ๅบ“ LICENSE ไธบๅ‡† +- ๆœฌๆ–‡ไปถไป…็”จไบŽ่ฎฐๅฝ•ๆฅๆบ๏ผŒไธไปฃ่กจ WeHub ๆ˜ฏๅŽŸ้กน็›ฎไฝœ่€… diff --git a/app.json b/app.json new file mode 100644 index 0000000..e448794 --- /dev/null +++ b/app.json @@ -0,0 +1,29 @@ +{ + "name": "wechaty", + "description": "Wechat for Bot. Get your Heroku cloud bot by deploy me", + "repository": "https://github.com/wechaty/wechaty", + "logo": "https://raw.githubusercontent.com/chatie/wechaty/master/image/wechaty-icon.png", + "keywords": ["wechaty", "wechat", "bot", "chatbot", "framework", "cloudbot"], + "env": { + "WECHATY_LOG": { + "description": "Don't touch this. unless you know what this mean", + "value": "info" + }, + "WECHATY_PUPPET": { + "description": "Puppet Name", + "value": "default" + }, + "WECHATY_TOKEN": { + "description": "Cloud access token for Wechaty.io", + "value": "wechaty" + }, + }, + "engines": { + "node": "10" + }, + "buildpacks": [ + { + "url": "https://github.com/heroku/heroku-buildpack-google-chrome" + } + ] +} diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..3ed9d23 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,29 @@ +# http://www.appveyor.com/docs/appveyor-yml + +# Test against these versions of Io.js and Node.js. +environment: + matrix: + - nodejs_version: "10" + +# Install scripts. (runs after repo cloning) +install: + # Get the latest stable version of Node.js or io.js + - ps: Install-Product node $env:nodejs_version + # chocolatey install jq - https://chocolatey.org/packages/jq + - choco install jq + # install modules + - npm install + +# Post-install test scripts. +test_script: + - node --version + - npm --version + # https://bugs.chromium.org/p/chromium/issues/detail?id=158372#c6 + - wmic datafile where name="C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe" get Version /value + - npm run test:unit + +# Don't actually build. +build: off + +# Set build version format here instead of in the admin panel. +version: "{build}" diff --git a/bin/clean-json.js b/bin/clean-json.js new file mode 100755 index 0000000..76eda98 --- /dev/null +++ b/bin/clean-json.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +import fs from 'fs' +import JSON5 from 'json5' + +const file = process.argv[2] +// console.error('Converting file: ', file) + +const json = JSON5.parse( + fs.readFileSync(file, 'utf8') +) + +console.info( + JSON.stringify(json, null, ' ') +) diff --git a/bin/cli.ts b/bin/cli.ts new file mode 100755 index 0000000..c36e804 --- /dev/null +++ b/bin/cli.ts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** + * https://github.com/wechaty/wechaty + * + * Author: Huan + * License: Apache-2.0 + * + * CLI Apps in TypeScript with `cmd-ts` (Part 1) + * Using `cmd-ts` to easily build a type-safe TypeScript CLI app + * + * https://gal.hagever.com/posts/type-safe-cli-apps-in-typescript-with-cmd-ts-part-1/ + */ +/* eslint-disable sort-keys */ +import 'dotenv/config.js' + +import { + binary, + run, + subcommands, +} from 'cmd-ts' + +import { VERSION } from '../src/config.js' + +import * as cmds from '../src/cli/mod.js' + +const wechatyCli = subcommands({ + name: 'wechaty', + description: 'Wechaty CLI Utility', + version: VERSION, + cmds, +}) + +run( + binary(wechatyCli), + process.argv, +).catch(console.error) diff --git a/bin/doctor.ts b/bin/doctor.ts new file mode 100644 index 0000000..fa7ed5e --- /dev/null +++ b/bin/doctor.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env node +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import os from 'os' + +import { config } from '../src/config.js' +import { Doctor } from '../src/doctor.js' +import { WechatyBuilder } from '../src/wechaty-builder.js' + +const wechaty = WechatyBuilder.singleton() +const doctor = new Doctor() + +async function main () { + let ipcTestResult: string + try { + await doctor.testTcp() + ipcTestResult = 'PASS' + } catch (err) { + console.info(err) + ipcTestResult = 'FAIL. Please check your tcp network, Wechaty need to listen on localhost and connect to it.' + } + + console.info(` + #### Wechaty Doctor + + 1. Wechaty version: ${wechaty.version()} + 2. ${os.type()} ${os.arch()} version ${os.release()} memory ${Math.floor(os.freemem() / 1024 / 1024)}/${Math.floor(os.totalmem() / 1024 / 1024)} MB + 3. Docker: ${config.docker} + 4. Node version: ${process.version} + 5. Tcp IPC TEST: ${ipcTestResult} + + `) + +} + +main() + .catch(err => console.error('main() exception: %s', err)) diff --git a/bin/entrypoint.sh b/bin/entrypoint.sh new file mode 100755 index 0000000..af231ef --- /dev/null +++ b/bin/entrypoint.sh @@ -0,0 +1,338 @@ +#!/usr/bin/env bash +# +# Wechaty - Connect ChatBots +# +# https://github.com/wechaty/wechaty +# +set -e + +export HOME=/bot +export PATH=$PATH:/wechaty/bin:/wechaty/node_modules/.bin + +function wechaty::banner() { + echo + figlet " Wechaty " + echo ____________________________________________________ + echo " https://www.chatie.io" +} + +function wechaty::errorBotNotFound() { + local file=$1 + echo "Container ERROR: can not found bot file: $HOME/$file" + + echo "Container PWD: $(pwd)" + echo "Container HOME: $HOME" + echo "Container LS $HOME: $(ls -l $HOME)" + + figlet " Troubleshooting " + cat <<'TROUBLESHOOTING' + + Troubleshooting: + + 1. Did you bind the current directory into container? + + check your `docker run ...` command, if there's no `volumn` arg, + then you need to add it so that we can bind the volume of /bot: + + `--volume="$(pwd)":/bot` + + this will let the container visit your current directory. + + 2. Are you sure your .js/.ts files aren't .js.txt/.ts.txt? + + this could be a problem on new Windows installs (file + extensions hidden by default). + + if you still have issue, please have a look at + https://github.com/wechaty/wechaty/issues/66 + and do a search in issues, that might be help. + +TROUBLESHOOTING +} + +function wechaty::printEnv () { + num=$(env | grep -c WECHATY) + echo "WECHATY Environment Variables: $num" + env | grep WECHATY +} + +function wechaty::errorCtrlC () { + # http://www.tldp.org/LDP/abs/html/exitcodes.html + # 130 Script terminated by Control-C Ctl-C Control-C is fatal error signal 2, (130 = 128 + 2, see above) + echo ' Script terminated by Control-C ' + figlet ' Ctrl + C ' +} + +function wechaty::pressEnterToContinue() { + local -i timeoutSecond=${1:-30} + local message=${2:-'Press ENTER to continue ... '} + + read -r -t "$timeoutSecond" -p "$message" || true + echo +} + +function wechaty::diagnose() { + local -i ret=$1 && shift + local file=$1 && shift + + echo "ERROR: Bot exited with code $ret" + + figlet ' BUG REPORT ' + wechaty::pressEnterToContinue 30 + + echo + echo "### 1. source code of $file" + echo + cat "$HOME/$file" || echo "ERROR: file not found" + echo + + echo + echo "### 2. directory structor of $HOME" + echo + ls -l "$HOME" + + echo + echo '### 3. package.json' + echo + cat "$HOME"/package.json || echo "No package.json" + + echo + echo "### 4. directory structor inside $HOME/node_modules" + echo + ls "$HOME"/node_modules || echo "No node_modules" + + echo + echo '### 5. wechaty doctor' + echo + wechaty-doctor + + figlet " Submit a ISSUE " + echo _____________________________________________________________ + echo '####### please paste all the above diagnose messages #######' + echo + echo 'Wechaty Issue https://github.com/wechaty/wechaty/issues' + echo + + wechaty::pressEnterToContinue +} + +function wechaty::runBot() { + local botFile=$1 + + if [ ! -f "$HOME/$botFile" ]; then + wechaty::errorBotNotFound "$botFile" + return 1 + fi + + echo "Working directory: $HOME" + cd "$HOME" + + [ -f package.json ] && { + # echo "Install dependencies modules ..." + + # + # NPM module install will have problem in China. + # i.e. chromedriver need to visit a google host to download binarys. + # + echo "Please make sure you had installed all the NPM modules which is depended on your bot script." + # yarn < /dev/null || return $? # yarn will close stdin??? cause `read` command fail after yarn + + # + # Issue https://github.com/wechaty/wechaty/issues/1478 + # As a conclusion: we should better not to link the local node_modules to the Docker global. + # + # wechaty::linkBotNodeModules + } + + # echo -n "Linking Wechaty module to bot ... " + # npm link wechaty < /dev/null > /dev/null 2>&1 + # echo "linked. " + + # npm --progress=false install @types/node > /dev/null + + # + # Huan(202108) node_arg: --es-module-specifier-resolution=node + # + # Make .js extension default for ESM imports + # See: https://github.com/nodejs/node/issues/30927#issuecomment-575998045 \ + # + local -i ret=0 + case "$botFile" in + *.js | *.cjs | *.mjs) + echo "Executing node $*" + node \ + --es-module-specifier-resolution=node \ + "$@" \ + & + ;; + *.ts) + echo "Executing node --loader=ts-node/esm $*" + node \ + --es-module-specifier-resolution=node \ + --no-warnings \ + --loader=ts-node/esm \ + "$@" \ + & + ;; + *) + echo "ERROR: wechaty::runBot() neither .{js,cjs,mjs} nor .ts" + exit 1 & + esac + + wait "$!" || ret=$? # fix `can only `return' from a function or sourced script` error + + case "$ret" in + 0) + ;; + 130) + wechaty::errorCtrlC + ;; + *) + wechaty::diagnose "$ret" "$@" + ;; + esac + + return "$ret" +} + +# Issue https://github.com/wechaty/wechaty/issues/1478 +# To Be Tested: +function wechaty::linkBotNodeModules() { + for localModule in /bot/node_modules/*; do + [ -e "$localModule" ] || continue + + module=${localModule//\/bot\/node_modules\//} + + globalModule="/node_modules/$module" + + if [ ! -e "$globalModule" ]; then + ln -sf "$localModule" /node_modules/ + # else + # echo "$globalModule exists" + fi + done +} + +function wechaty::io-client() { + figlet " Chatie.io " + figlet " Authing By:" + echo + echo "WECHATY_TOKEN=$WECHATY_TOKEN " + echo + + pushd /wechaty + npm run io-client + popd +} + +function wechaty::help() { + figlet " Docker Usage: " + cat <, or a . + + : + mybot.js: a JavaScript program for your bot. + mybot.ts: a TypeScript program for your bot. + + : + demo Run Wechaty DEMO + doctor Print Diagnose Report + test Run Unit Test + + Learn more at: + https://github.com/wechaty/wechaty/wiki/Docker + + + +HELP +} + +function main() { + # issue #84 + echo -e 'nameserver 114.114.114.114\nnameserver 1.1.1.1\nnameserver 8.8.8.8' | sudo tee -a /etc/resolv.conf > /dev/null + + wechaty::banner + figlet Connecting + figlet ChatBots + + wechaty::printEnv + + VERSION=$(WECHATY_LOG=WARN wechaty-version 2>/dev/null || echo '0.0.0(unknown)') + + echo + echo -n "Starting Docker Container for Wechaty v$VERSION with " + echo -n "Node.js $(node --version) ..." + echo + + local -i ret=0 + + local defaultArg=help + if [ -n "$WECHATY_TOKEN" ]; then + defaultArg=io-client + fi + + case "${1:-${defaultArg}}" in + # + # 1. Get a shell + # + shell | sh | bash) + /bin/bash -s || ret=$? + ;; + + # + # 2. Run a bot + # + *.ts | *.js | *.cjs | *.mjs) + # set -e will not work inside wechaty::runBot because of + # http://stackoverflow.com/a/4073372/1123955 + wechaty::runBot "$@" || ret=$? + ;; + + # + # 3. If there's additional `npm` arg... + # + npm) + shift + npm "$@" || ret=$? + ;; + + help|version) + wechaty::help + ;; + + io-client) + wechaty::io-client + ;; + + test) + # WECHATY_LOG=silent npm run test:unit + if [ -f "$HOME"/package.json ]; then + pushd "$HOME" + else + pushd /wechaty + fi + WECHATY_LOG=silent npm run test + popd + ;; + + # + # 4. Default to execute npm run ... + # + *) + [ "$1" = "run" ] && shift + npm run "$@" || ret=$? + ;; + esac + + wechaty::banner + figlet " Exit $ret " + return $ret +} + +main "$@" + diff --git a/bin/io-client.ts b/bin/io-client.ts new file mode 100644 index 0000000..16a9dea --- /dev/null +++ b/bin/io-client.ts @@ -0,0 +1,95 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import 'dotenv/config.js' + +import { + log, +} from 'wechaty-puppet' +import { + config, +} from '../src/config.js' + +import { + IoClient, + IoClientOptions, +} from '../src/io-client.js' +import { WechatyBuilder } from '../src/wechaty-builder.js' + +const welcome = ` +| __ __ _ _ +| \\ \\ / /__ ___| |__ __ _| |_ _ _ +| \\ \\ /\\ / / _ \\/ __| '_ \\ / _\` | __| | | | +| \\ V V / __/ (__| | | | (_| | |_| |_| | +| \\_/\\_/ \\___|\\___|_| |_|\\__,_|\\__|\\__, | +| |___/ + +=============== Powered by Wechaty =============== + -------- https://www.chatie.io -------- + +My super power: download cloud bot from www.chatie.io + +__________________________________________________ + +` + +async function main () { + const token = config.token + + if (!token) { + throw new Error('token not found: please set WECHATY_TOKEN in environment before run io-client') + } + + console.info(welcome) + log.info('Client', 'Starting for WECHATY_TOKEN: %s', token) + + const wechaty = WechatyBuilder.build({ name: token }) + + let port + if (process.env['WECHATY_PUPPET_SERVER_PORT']) { + port = parseInt(process.env['WECHATY_PUPPET_SERVER_PORT']) + } + + const options: IoClientOptions = { + token, + wechaty, + } + if (port) { + options.port = port + } + + const client = new IoClient(options) + + client.start() + .catch(onError.bind(client)) +} + +async function onError ( + this : IoClient, + e : Error, +) { + log.error('Client', 'start() fail: %s', e) + await this.quit() + process.exit(-1) +} + +main() + .catch(console.error) diff --git a/bin/providers-trends.ts b/bin/providers-trends.ts new file mode 100644 index 0000000..16a5730 --- /dev/null +++ b/bin/providers-trends.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env node +import { OFFICIAL_PUPPET_DEPENDENCIES } from '../src/puppet-config.js' + +async function main () { + const puppetNameList = Object.keys(OFFICIAL_PUPPET_DEPENDENCIES) + const publicNameList = puppetNameList.filter(name => /^[^@]/.test(name)) + const urlPath = publicNameList.join('-vs-') + const url = 'https://www.npmtrends.com/' + urlPath + console.info(url) +} + +main() + .catch(console.error) diff --git a/bin/puppet-install.ts b/bin/puppet-install.ts new file mode 100644 index 0000000..335010f --- /dev/null +++ b/bin/puppet-install.ts @@ -0,0 +1,69 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import spawn from 'cross-spawn' + +import { log } from 'wechaty-puppet' + +import { + OFFICIAL_PUPPET_DEPENDENCIES, + OfficialPuppetNpmName, +} from '../src/puppet-config.js' + +/** + * Install all `wechaty-puppet-*` modules from `puppet-config.ts` + */ +async function main (): Promise { + log.info('PuppetInstall', 'main() installing ...') + + const skipList = [ + '@juzibot/wechaty-puppet-donut', // windows puppet + '@juzibot/wechaty-puppet-wxwork', // wxwork puppet + ] + + const moduleList: string[] = [] + + for (const puppetModuleName of Object.keys(OFFICIAL_PUPPET_DEPENDENCIES)) { + const version = OFFICIAL_PUPPET_DEPENDENCIES[puppetModuleName as OfficialPuppetNpmName] + + if (version === '0.0.0' || skipList.includes(puppetModuleName)) { + continue + } + + moduleList.push(`${puppetModuleName}@${version}`) + } + + const args = [ + 'install', + '--no-save', + ...moduleList, + ] + log.info('PuppetInstall', `installing ... Shell: "npm install ${args.join(' ')}"\n`) + + const result = spawn('npm', args, { stdio: 'inherit' }) + await new Promise(resolve => result.once('exit', resolve)) + + log.info('PuppetInstall', 'main() installing ... done') +} + +main().catch(e => { + console.error(e) + process.exit(1) +}) diff --git a/bin/version.ts b/bin/version.ts new file mode 100644 index 0000000..1e0e0ea --- /dev/null +++ b/bin/version.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { VERSION } from '../src/config.js' + +console.info(VERSION) diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..2f7efbe --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-minimal \ No newline at end of file diff --git a/docs/images/blessed-twins-bot.png b/docs/images/blessed-twins-bot.png new file mode 100644 index 0000000..f4ee1ac Binary files /dev/null and b/docs/images/blessed-twins-bot.png differ diff --git a/docs/images/bot-qr-code.png b/docs/images/bot-qr-code.png new file mode 100644 index 0000000..4983427 Binary files /dev/null and b/docs/images/bot-qr-code.png differ diff --git a/docs/images/friday-qrcode.svg b/docs/images/friday-qrcode.svg new file mode 100644 index 0000000..4a8443a --- /dev/null +++ b/docs/images/friday-qrcode.svg @@ -0,0 +1,218 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/qrcode_for_chatie.jpg b/docs/images/qrcode_for_chatie.jpg new file mode 100644 index 0000000..b85fb90 Binary files /dev/null and b/docs/images/qrcode_for_chatie.jpg differ diff --git a/docs/images/screenshot-ding-dong-bot-win32-qrcode.gif b/docs/images/screenshot-ding-dong-bot-win32-qrcode.gif new file mode 100644 index 0000000..5b9c053 Binary files /dev/null and b/docs/images/screenshot-ding-dong-bot-win32-qrcode.gif differ diff --git a/docs/images/wechaty-ascii-art.txt b/docs/images/wechaty-ascii-art.txt new file mode 100644 index 0000000..2048453 --- /dev/null +++ b/docs/images/wechaty-ascii-art.txt @@ -0,0 +1,6 @@ +__ __ _ _ +\ \ / /__ ___| |__ __ _| |_ _ _ + \ \ /\ / / _ \/ __| '_ \ / _` | __| | | | + \ V V / __/ (__| | | | (_| | |_| |_| | + \_/\_/ \___|\___|_| |_|\__,_|\__|\__, | + |___/ diff --git a/docs/images/wechaty-banner-white.svg b/docs/images/wechaty-banner-white.svg new file mode 100644 index 0000000..17425a8 --- /dev/null +++ b/docs/images/wechaty-banner-white.svg @@ -0,0 +1,104 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + diff --git a/docs/images/wechaty-banner.svg b/docs/images/wechaty-banner.svg new file mode 100644 index 0000000..7cd1d77 --- /dev/null +++ b/docs/images/wechaty-banner.svg @@ -0,0 +1,104 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + diff --git a/docs/images/wechaty-icon-green.png b/docs/images/wechaty-icon-green.png new file mode 100644 index 0000000..cb19968 Binary files /dev/null and b/docs/images/wechaty-icon-green.png differ diff --git a/docs/images/wechaty-icon.png b/docs/images/wechaty-icon.png new file mode 100644 index 0000000..8bab05d Binary files /dev/null and b/docs/images/wechaty-icon.png differ diff --git a/docs/images/wechaty-logo-en.png b/docs/images/wechaty-logo-en.png new file mode 100644 index 0000000..6011613 Binary files /dev/null and b/docs/images/wechaty-logo-en.png differ diff --git a/docs/images/wechaty-logo-green-en-transparent.png b/docs/images/wechaty-logo-green-en-transparent.png new file mode 100644 index 0000000..a3b096a Binary files /dev/null and b/docs/images/wechaty-logo-green-en-transparent.png differ diff --git a/docs/images/wechaty-logo-green-en.png b/docs/images/wechaty-logo-green-en.png new file mode 100644 index 0000000..88a4c2d Binary files /dev/null and b/docs/images/wechaty-logo-green-en.png differ diff --git a/docs/images/wechaty-logo-zh.png b/docs/images/wechaty-logo-zh.png new file mode 100644 index 0000000..3a75d62 Binary files /dev/null and b/docs/images/wechaty-logo-zh.png differ diff --git a/docs/images/wechaty-white-transparent.png b/docs/images/wechaty-white-transparent.png new file mode 100644 index 0000000..48d16c3 Binary files /dev/null and b/docs/images/wechaty-white-transparent.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..f00a024 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,2215 @@ +# Wechaty v0.62.1 Documentation + +- Website - +- Docs Site - +- API References - + +## Classes + +
+
Wechaty
+

Main bot class.

+

A Bot is a WeChat client depends on which puppet you use. +It may equals

+ +

See more:

+ +
+

If you want to know how to send message, see Message
+If you want to know how to get contact, see Contact

+
+
+
Room
+

All WeChat rooms(groups) will be encapsulated as a Room.

+

Examples/Room-Bot

+
+
Contact
+

All wechat contacts(friend) will be encapsulated as a Contact. +Examples/Contact-Bot

+
+
ContactSelf
+

Bot itself will be encapsulated as a ContactSelf.

+
+

Tips: this class is extends Contact

+
+
+
Friendship
+

Send, receive friend request, and friend confirmation events.

+
    +
  1. send request
  2. +
  3. receive request(in friend event)
  4. +
  5. confirmation friendship(friend event)
  6. +
+

Examples/Friend-Bot

+
+
Message
+

All wechat messages will be encapsulated as a Message.

+

Examples/Ding-Dong-Bot

+
+
RoomInvitation
+

accept room invitation

+
+
+ +## Constants + +
+
cuid_1
+

Wechaty Chatbot SDK - https://github.com/wechaty/wechaty

+
+
clone_class_1
+

Wechaty Chatbot SDK - https://github.com/wechaty/wechaty

+
+
clone_class_1
+

Wechaty Chatbot SDK - https://github.com/wechaty/wechaty

+
+
wechaty_puppet_1
+

Wechaty Chatbot SDK - https://github.com/wechaty/wechaty

+
+
events_1
+

Wechaty Chatbot SDK - https://github.com/wechaty/wechaty

+
+
events_1
+

Wechaty Chatbot SDK - https://github.com/wechaty/wechaty

+
+
clone_class_1
+

Wechaty Chatbot SDK - https://github.com/wechaty/wechaty

+
+
+ +## Functions + +
+
guardWechatify()
+

Huan(202008): we will bind the wechaty puppet with user modules (Contact, Room, etc) together inside the start() method

+
+
+ +## Typedefs + +
+
PuppetModuleName
+

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). +The plugins are named XXXPuppet, for example:

+ +
+
WechatyOptions
+

The option parameter to create a wechaty instance

+
+
RoomQueryFilter
+

The filter to find the room: {topic: string | RegExp}

+
+
RoomEventName
+

Room Class Event Type

+
+
RoomEventFunction
+

Room Class Event Function

+
+
RoomMemberQueryFilter
+

The way to search member by Room.member()

+
+
ContactQueryFilter
+

The way to search Contact

+
+
+ + + +## Wechaty +Main bot class. + +A `Bot` is a WeChat client depends on which puppet you use. +It may equals +- web-WeChat, when you use: [puppet-puppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer)/[puppet-wechat4u](https://github.com/wechaty/wechaty-puppet-wechat4u) +- ipad-WeChat, when you use: [puppet-padchat](https://github.com/wechaty/wechaty-puppet-padchat) +- ios-WeChat, when you use: puppet-ioscat + +See more: +- [What is a Puppet in Wechaty](https://github.com/wechaty/getting-started/wiki/FAQ#31-what-is-a-puppet-in-wechaty) + +> If you want to know how to send message, see [Message](#Message)
+> If you want to know how to get contact, see [Contact](#Contact) + +**Kind**: global class + +* [Wechaty](#Wechaty) + * [new Wechaty([options])](#new_Wechaty_new) + * _instance_ + * [.wechatifiedContact](#Wechaty+wechatifiedContact) + * [.name()](#Wechaty+name) + * [.use(...plugins)](#Wechaty+use) โ‡’ [Wechaty](#Wechaty) + * [.start()](#Wechaty+start) โ‡’ Promise.<void> + * [.stop()](#Wechaty+stop) โ‡’ Promise.<void> + * [.logout()](#Wechaty+logout) โ‡’ Promise.<void> + * [.logonoff()](#Wechaty+logonoff) โ‡’ boolean + * [.userSelf()](#Wechaty+userSelf) โ‡’ [ContactSelf](#ContactSelf) + * [.say(something)](#Wechaty+say) โ‡’ Promise.<void> + * _static_ + * [.instance([options])](#Wechaty.instance) + * [.use(...plugins)](#Wechaty.use) โ‡’ [Wechaty](#Wechaty) + + + +### new Wechaty([options]) +Creates an instance of Wechaty. + + +| Param | Type | Default | +| --- | --- | --- | +| [options] | [WechatyOptions](#WechatyOptions) | {} | + +**Example** *(The World's Shortest ChatBot Code: 6 lines of JavaScript)* +```js +import { Wechaty } from 'wechaty' +const bot = new Wechaty() +bot.on('scan', (qrCode, status) => console.log('https://wechaty.js.org/qrcode/' + encodeURIComponent(qrcode))) +bot.on('login', user => console.log(`User ${user} logged in`)) +bot.on('message', message => console.log(`Message: ${message}`)) +bot.start() +``` + + +### wechaty.wechatifiedContact +1. Setup Wechaty User Classes + +**Kind**: instance property of [Wechaty](#Wechaty) + + +### wechaty.name() +Wechaty bot name set by `options.name` +default: `wechaty` + +**Kind**: instance method of [Wechaty](#Wechaty) + + +### wechaty.use(...plugins) โ‡’ [Wechaty](#Wechaty) +For wechaty ecosystem, allow user to define a 3rd party plugin for the current wechaty instance. + +**Kind**: instance method of [Wechaty](#Wechaty) +**Returns**: [Wechaty](#Wechaty) - - this for chaining, + +| Param | Type | Description | +| --- | --- | --- | +| ...plugins | Array.<WechatyPlugin> | The plugins you want to use | + +**Example** +```js +// The same usage with Wechaty.use(). +``` + + +### wechaty.start() โ‡’ Promise.<void> +When you start the bot, bot will begin to login, need you WeChat scan qrcode to login +> Tips: All the bot operation needs to be triggered after start() is done + +**Kind**: instance method of [Wechaty](#Wechaty) +**Example** +```js +await bot.start() +// do other stuff with bot here +``` + + +### wechaty.stop() โ‡’ Promise.<void> +Stop the bot + +**Kind**: instance method of [Wechaty](#Wechaty) +**Example** +```js +await bot.stop() +``` + + +### wechaty.logout() โ‡’ Promise.<void> +Logout the bot + +**Kind**: instance method of [Wechaty](#Wechaty) +**Example** +```js +await bot.logout() +``` + + +### wechaty.logonoff() โ‡’ boolean +Get the logon / logoff state + +**Kind**: instance method of [Wechaty](#Wechaty) +**Example** +```js +if (bot.logonoff()) { + console.log('Bot logged in') +} else { + console.log('Bot not logged in') +} +``` + + +### wechaty.userSelf() โ‡’ [ContactSelf](#ContactSelf) +Get current user + +**Kind**: instance method of [Wechaty](#Wechaty) +**Example** +```js +const contact = bot.userSelf() +console.log(`Bot is ${contact.name()}`) +``` + + +### wechaty.say(something) โ‡’ Promise.<void> +Send message to userSelf, in other words, bot send message to itself. +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Wechaty](#Wechaty) + +| Param | Type | Description | +| --- | --- | --- | +| something | string \| [Contact](#Contact) \| FileBox \| UrlLink \| MiniProgram | send text, Contact, or file to bot.
You can use [FileBox](https://www.npmjs.com/package/file-box) to send file | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +// after logged in + +// 1. send text to bot itself +await bot.say('hello!') + +// 2. send Contact to bot itself +const contact = await bot.Contact.find() +await bot.say(contact) + +// 3. send Image to bot itself from remote url +import { FileBox } from 'wechaty' +const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') +await bot.say(fileBox) + +// 4. send Image to bot itself from local file +import { FileBox } from 'wechaty' +const fileBox = FileBox.fromFile('/tmp/text.jpg') +await bot.say(fileBox) + +// 5. send Link to bot itself +const linkPayload = new UrlLink ({ + description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + title : 'Welcome to Wechaty', + url : 'https://github.com/wechaty/wechaty', +}) +await bot.say(linkPayload) + +// 6. send MiniProgram to bot itself +const miniPayload = new MiniProgram ({ + username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + appid : '', //optional, get from mp.weixin.qq.com + title : '', //optional + pagepath : '', //optional + description : '', //optional + thumbnailurl : '', //optional +}) +await bot.say(miniPayload) +``` + + +### Wechaty.instance([options]) +Get the global instance of Wechaty + +**Kind**: static method of [Wechaty](#Wechaty) + +| Param | Type | Default | +| --- | --- | --- | +| [options] | [WechatyOptions](#WechatyOptions) | {} | + +**Example** *(The World's Shortest ChatBot Code: 6 lines of JavaScript)* +```js +const { Wechaty } = require('wechaty') + +Wechaty.instance() // Global instance +.on('scan', (url, status) => console.log(`Scan QR Code to login: ${status}\n${url}`)) +.on('login', user => console.log(`User ${user} logged in`)) +.on('message', message => console.log(`Message: ${message}`)) +.start() +``` + + +### Wechaty.use(...plugins) โ‡’ [Wechaty](#Wechaty) +For wechaty ecosystem, allow user to define a 3rd party plugin for the all wechaty instances + +**Kind**: static method of [Wechaty](#Wechaty) +**Returns**: [Wechaty](#Wechaty) - - this for chaining, + +| Param | Type | Description | +| --- | --- | --- | +| ...plugins | Array.<WechatyPlugin> | The plugins you want to use | + +**Example** +```js +// Report all chat message to my server. + +function WechatyReportPlugin(options: { url: string }) { + return function (this: Wechaty) { + this.on('message', message => http.post(options.url, { data: message })) + } +} + +bot.use(WechatyReportPlugin({ url: 'http://somewhere.to.report.your.data.com' }) +``` + + +## Room +All WeChat rooms(groups) will be encapsulated as a Room. + +[Examples/Room-Bot](https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/room-bot.ts) + +**Kind**: global class +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| id | string | Room id. This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) | + + +* [Room](#Room) + * _instance_ + * [.sync()](#Room+sync) โ‡’ Promise.<void> + * [.say(textOrContactOrFileOrUrlOrMini, [mention])](#Room+say) โ‡’ Promise.<(void\|Message)> + * [.add(contact)](#Room+add) โ‡’ Promise.<void> + * [.remove(contact)](#Room+remove) โ‡’ Promise.<void> + * ~~[.del()](#Room+del)~~ + * [.quit()](#Room+quit) โ‡’ Promise.<void> + * [.topic([newTopic])](#Room+topic) โ‡’ Promise.<(string\|void)> + * [.announce([text])](#Room+announce) โ‡’ Promise.<(void\|string)> + * [.qrCode()](#Room+qrCode) โ‡’ Promise.<string> + * [.alias(contact)](#Room+alias) โ‡’ Promise.<(string\|null)> + * [.has(contact)](#Room+has) โ‡’ Promise.<boolean> + * [.memberAll([query])](#Room+memberAll) โ‡’ Promise.<Array.<Contact>> + * [.member(queryArg)](#Room+member) โ‡’ Promise.<(null\|Contact)> + * [.owner()](#Room+owner) โ‡’ [Contact](#Contact) \| null + * [.avatar()](#Room+avatar) โ‡’ FileBox + * _static_ + * [.create(contactList, [topic])](#Room.create) โ‡’ [Promise.<Room>](#Room) + * [.findAll([query])](#Room.findAll) โ‡’ Promise.<Array.<Room>> + * [.find(query)](#Room.find) โ‡’ Promise.<(Room\|null)> + + + +### room.sync() โ‡’ Promise.<void> +Force reload data for Room, Sync data from puppet API again. + +**Kind**: instance method of [Room](#Room) +**Example** +```js +await room.sync() +``` + + +### room.say(textOrContactOrFileOrUrlOrMini, [mention]) โ‡’ Promise.<(void\|Message)> +Send message inside Room, if set [replyTo], wechaty will mention the contact as well. +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Room](#Room) + +| Param | Type | Description | +| --- | --- | --- | +| textOrContactOrFileOrUrlOrMini | string \| [Contact](#Contact) \| FileBox | Send `text` or `media file` inside Room.
You can use [FileBox](https://www.npmjs.com/package/file-box) to send file | +| [mention] | [Contact](#Contact) \| [Array.<Contact>](#Contact) | Optional parameter, send content inside Room, and mention @replyTo contact or contactList. | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const room = await bot.Room.find({topic: 'wechaty'}) + +// 1. Send text inside Room + +await room.say('Hello world!') +const msg = await room.say('Hello world!') // only supported by puppet-padplus + +// 2. Send media file inside Room +import { FileBox } from 'wechaty' +const fileBox1 = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') +const fileBox2 = FileBox.fromLocal('/tmp/text.txt') +await room.say(fileBox1) +const msg1 = await room.say(fileBox1) // only supported by puppet-padplus +await room.say(fileBox2) +const msg2 = await room.say(fileBox2) // only supported by puppet-padplus + +// 3. Send Contact Card in a room +const contactCard = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member +await room.say(contactCard) +const msg = await room.say(contactCard) // only supported by puppet-padplus + +// 4. Send text inside room and mention @mention contact +const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member +await room.say('Hello world!', contact) +const msg = await room.say('Hello world!', contact) // only supported by puppet-padplus + +// 5. Send text inside room and mention someone with Tagged Template +const contact2 = await bot.Contact.find({name: 'zixia'}) // change 'zixia' to any of the room member +await room.say`Hello ${contact}, here is the world ${contact2}` +const msg = await room.say`Hello ${contact}, here is the world ${contact2}` // only supported by puppet-padplus + +// 6. send url link in a room + +const urlLink = new UrlLink ({ + description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + title : 'Welcome to Wechaty', + url : 'https://github.com/wechaty/wechaty', +}) +await room.say(urlLink) +const msg = await room.say(urlLink) // only supported by puppet-padplus + +// 7. send mini program in a room + +const miniProgram = new MiniProgram ({ + username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + appid : '', //optional, get from mp.weixin.qq.com + title : '', //optional + pagepath : '', //optional + description : '', //optional + thumbnailurl : '', //optional +}) +await room.say(miniProgram) +const msg = await room.say(miniProgram) // only supported by puppet-padplus +``` + + +### room.add(contact) โ‡’ Promise.<void> +Add contact in a room + +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) +> +> see [Web version of WeChat closed group interface](https://github.com/wechaty/wechaty/issues/1441) + +**Kind**: instance method of [Room](#Room) + +| Param | Type | +| --- | --- | +| contact | [Contact](#Contact) | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any contact in your WeChat +const room = await bot.Room.find({topic: 'WeChat'}) // change 'WeChat' to any room topic in your WeChat +if (room) { + try { + await room.add(contact) + } catch(e) { + console.error(e) + } +} +``` + + +### room.remove(contact) โ‡’ Promise.<void> +Remove a contact from the room +It works only when the bot is the owner of the room + +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) +> +> see [Web version of WeChat closed group interface](https://github.com/wechaty/wechaty/issues/1441) + +**Kind**: instance method of [Room](#Room) + +| Param | Type | +| --- | --- | +| contact | [Contact](#Contact) | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const room = await bot.Room.find({topic: 'WeChat'}) // change 'WeChat' to any room topic in your WeChat +const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any room member in the room you just set +if (room) { + try { + await room.remove(contact) + } catch(e) { + console.error(e) + } +} +``` + + +### ~~room.del()~~ +***Deprecated*** + +**Kind**: instance method of [Room](#Room) + + +### room.quit() โ‡’ Promise.<void> +Bot quit the room itself + +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Room](#Room) +**Example** +```js +await room.quit() +``` + + +### room.topic([newTopic]) โ‡’ Promise.<(string\|void)> +SET/GET topic from the room + +**Kind**: instance method of [Room](#Room) + +| Param | Type | Description | +| --- | --- | --- | +| [newTopic] | string | If set this para, it will change room topic. | + +**Example** *(When you say anything in a room, it will get room topic. )* +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const room = m.room() + if (room) { + const topic = await room.topic() + console.log(`room topic is : ${topic}`) + } +}) +.start() +``` +**Example** *(When you say anything in a room, it will change room topic. )* +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const room = m.room() + if (room) { + const oldTopic = await room.topic() + await room.topic('change topic to wechaty!') + console.log(`room topic change from ${oldTopic} to ${room.topic()}`) + } +}) +.start() +``` + + +### room.announce([text]) โ‡’ Promise.<(void\|string)> +SET/GET announce from the room +> Tips: It only works when bot is the owner of the room. +> +> This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Room](#Room) + +| Param | Type | Description | +| --- | --- | --- | +| [text] | string | If set this para, it will change room announce. | + +**Example** *(When you say anything in a room, it will get room announce. )* +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const room = await bot.Room.find({topic: 'your room'}) +const announce = await room.announce() +console.log(`room announce is : ${announce}`) +``` +**Example** *(When you say anything in a room, it will change room announce. )* +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const room = await bot.Room.find({topic: 'your room'}) +const oldAnnounce = await room.announce() +await room.announce('change announce to wechaty!') +console.log(`room announce change from ${oldAnnounce} to ${room.announce()}`) +``` + + +### room.qrCode() โ‡’ Promise.<string> +Get QR Code Value of the Room from the room, which can be used as scan and join the room. +> Tips: +1. This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) +2. The return should be the QR Code Data, instead of the QR Code Image. (the data should be less than 8KB. See: https://stackoverflow.com/a/12764370/1123955 ) + +**Kind**: instance method of [Room](#Room) + + +### room.alias(contact) โ‡’ Promise.<(string\|null)> +Return contact's roomAlias in the room + +**Kind**: instance method of [Room](#Room) +**Returns**: Promise.<(string\|null)> - - If a contact has an alias in room, return string, otherwise return null + +| Param | Type | +| --- | --- | +| contact | [Contact](#Contact) | + +**Example** +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const room = m.room() + const contact = m.from() + if (room) { + const alias = await room.alias(contact) + console.log(`${contact.name()} alias is ${alias}`) + } +}) +.start() +``` + + +### room.has(contact) โ‡’ Promise.<boolean> +Check if the room has member `contact`, the return is a Promise and must be `await`-ed + +**Kind**: instance method of [Room](#Room) +**Returns**: Promise.<boolean> - Return `true` if has contact, else return `false`. + +| Param | Type | +| --- | --- | +| contact | [Contact](#Contact) | + +**Example** *(Check whether 'lijiarui' is in the room 'wechaty')* +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of contact in your WeChat +const room = await bot.Room.find({topic: 'wechaty'}) // change 'wechaty' to any of the room in your WeChat +if (contact && room) { + if (await room.has(contact)) { + console.log(`${contact.name()} is in the room wechaty!`) + } else { + console.log(`${contact.name()} is not in the room wechaty!`) + } +} +``` + + +### room.memberAll([query]) โ‡’ Promise.<Array.<Contact>> +Find all contacts in a room + +#### definition +- `name` the name-string set by user-self, should be called name, equal to `Contact.name()` +- `roomAlias` the name-string set by user-self in the room, should be called roomAlias +- `contactAlias` the name-string set by bot for others, should be called alias, equal to `Contact.alias()` + +**Kind**: instance method of [Room](#Room) + +| Param | Type | Description | +| --- | --- | --- | +| [query] | [RoomMemberQueryFilter](#RoomMemberQueryFilter) \| string | Optional parameter, When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias | + +**Example** +```js +const roomList:Contact[] | null = await room.findAll() +if(roomList) + console.log(`room all member list: `, roomList) +const memberContactList: Contact[] | null =await room.findAll(`abc`) +console.log(`contact list with all name, room alias, alias are abc:`, memberContactList) +``` + + +### room.member(queryArg) โ‡’ Promise.<(null\|Contact)> +Find all contacts in a room, if get many, return the first one. + +**Kind**: instance method of [Room](#Room) + +| Param | Type | Description | +| --- | --- | --- | +| queryArg | [RoomMemberQueryFilter](#RoomMemberQueryFilter) \| string | When use member(name:string), return all matched members, including name, roomAlias, contactAlias | + +**Example** *(Find member by name)* +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const room = await bot.Room.find({topic: 'wechaty'}) // change 'wechaty' to any room name in your WeChat +if (room) { + const member = await room.member('lijiarui') // change 'lijiarui' to any room member in your WeChat + if (member) { + console.log(`wechaty room got the member: ${member.name()}`) + } else { + console.log(`cannot get member in wechaty room!`) + } +} +``` +**Example** *(Find member by MemberQueryFilter)* +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const room = await bot.Room.find({topic: 'wechaty'}) // change 'wechaty' to any room name in your WeChat +if (room) { + const member = await room.member({name: 'lijiarui'}) // change 'lijiarui' to any room member in your WeChat + if (member) { + console.log(`wechaty room got the member: ${member.name()}`) + } else { + console.log(`cannot get member in wechaty room!`) + } +} +``` + + +### room.owner() โ‡’ [Contact](#Contact) \| null +Get room's owner from the room. +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Room](#Room) +**Example** +```js +const owner = room.owner() +``` + + +### room.avatar() โ‡’ FileBox +Get avatar from the room. + +**Kind**: instance method of [Room](#Room) +**Example** +```js +const fileBox = await room.avatar() +const name = fileBox.name +fileBox.toFile(name) +``` + + +### Room.create(contactList, [topic]) โ‡’ [Promise.<Room>](#Room) +Create a new room. + +**Kind**: static method of [Room](#Room) + +| Param | Type | +| --- | --- | +| contactList | [Array.<Contact>](#Contact) | +| [topic] | string | + +**Example** *(Creat a room with 'lijiarui' and 'huan', the room topic is 'ding - created')* +```js +const helperContactA = await Contact.find({ name: 'lijiarui' }) // change 'lijiarui' to any contact in your WeChat +const helperContactB = await Contact.find({ name: 'huan' }) // change 'huan' to any contact in your WeChat +const contactList = [helperContactA, helperContactB] +console.log('Bot', 'contactList: %s', contactList.join(',')) +const room = await Room.create(contactList, 'ding') +console.log('Bot', 'createDingRoom() new ding room created: %s', room) +await room.topic('ding - created') +await room.say('ding - created') +``` + + +### Room.findAll([query]) โ‡’ Promise.<Array.<Room>> +Find room by by filter: {topic: string | RegExp}, return all the matched room + +**Kind**: static method of [Room](#Room) + +| Param | Type | +| --- | --- | +| [query] | [RoomQueryFilter](#RoomQueryFilter) | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +// after logged in +const roomList = await bot.Room.findAll() // get the room list of the bot +const roomList = await bot.Room.findAll({topic: 'wechaty'}) // find all of the rooms with name 'wechaty' +``` + + +### Room.find(query) โ‡’ Promise.<(Room\|null)> +Try to find a room by filter: {topic: string | RegExp}. If get many, return the first one. + +**Kind**: static method of [Room](#Room) +**Returns**: Promise.<(Room\|null)> - If can find the room, return Room, or return null + +| Param | Type | +| --- | --- | +| query | [RoomQueryFilter](#RoomQueryFilter) | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +// after logged in... +const roomList = await bot.Room.find() +const roomList = await bot.Room.find({topic: 'wechaty'}) +``` + + +## Contact +All wechat contacts(friend) will be encapsulated as a Contact. +[Examples/Contact-Bot](https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/contact-bot.ts) + +**Kind**: global class +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| id | string | Get Contact id. This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) | + + +* [Contact](#Contact) + * _instance_ + * [.say(something)](#Contact+say) โ‡’ Promise.<(void\|Message)> + * [.name()](#Contact+name) โ‡’ string + * [.alias(newAlias)](#Contact+alias) โ‡’ Promise.<(null\|string\|void)> + * [.friend()](#Contact+friend) โ‡’ boolean \| null + * [.type()](#Contact+type) โ‡’ ContactType.Unknown \| ContactType.Personal \| ContactType.Official + * [.gender()](#Contact+gender) โ‡’ ContactGender.Unknown \| ContactGender.Male \| ContactGender.Female + * [.province()](#Contact+province) โ‡’ string \| null + * [.city()](#Contact+city) โ‡’ string \| null + * [.avatar()](#Contact+avatar) โ‡’ Promise.<FileBox> + * [.tags()](#Contact+tags) โ‡’ Promise.<Array.<Tag>> + * [.sync()](#Contact+sync) โ‡’ Promise.<this> + * [.self()](#Contact+self) โ‡’ boolean + * _static_ + * [.find(query)](#Contact.find) โ‡’ Promise.<(Contact\|null)> + * [.findAll([queryArg])](#Contact.findAll) โ‡’ Promise.<Array.<Contact>> + * [.tags()](#Contact.tags) โ‡’ Promise.<Array.<Tag>> + + + +### contact.say(something) โ‡’ Promise.<(void\|Message)> +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Contact](#Contact) + +| Param | Type | Description | +| --- | --- | --- | +| something | string \| [Contact](#Contact) \| FileBox \| UrlLink \| MiniProgram | send text, Contact, or file to contact.
You can use [FileBox](https://www.npmjs.com/package/file-box) to send file | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of your contact name in wechat + +// 1. send text to contact + +await contact.say('welcome to wechaty!') +const msg = await contact.say('welcome to wechaty!') // only supported by puppet-padplus + +// 2. send media file to contact + +import { FileBox } from 'wechaty' +const fileBox1 = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') +const fileBox2 = FileBox.fromFile('/tmp/text.txt') +await contact.say(fileBox1) +const msg1 = await contact.say(fileBox1) // only supported by puppet-padplus +await contact.say(fileBox2) +const msg2 = await contact.say(fileBox2) // only supported by puppet-padplus + +// 3. send contact card to contact + +const contactCard = bot.Contact.load('contactId') +const msg = await contact.say(contactCard) // only supported by puppet-padplus + +// 4. send url link to contact + +const urlLink = new UrlLink ({ + description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + title : 'Welcome to Wechaty', + url : 'https://github.com/wechaty/wechaty', +}) +await contact.say(urlLink) +const msg = await contact.say(urlLink) // only supported by puppet-padplus + +// 5. send mini program to contact + +const miniProgram = new MiniProgram ({ + username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + appid : '', //optional, get from mp.weixin.qq.com + title : '', //optional + pagepath : '', //optional + description : '', //optional + thumbnailurl : '', //optional +}) +await contact.say(miniProgram) +const msg = await contact.say(miniProgram) // only supported by puppet-padplus +``` + + +### contact.name() โ‡’ string +Get the name from a contact + +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +const name = contact.name() +``` + + +### contact.alias(newAlias) โ‡’ Promise.<(null\|string\|void)> +GET / SET / DELETE the alias for a contact + +Tests show it will failed if set alias too frequently(60 times in one minute). + +**Kind**: instance method of [Contact](#Contact) + +| Param | Type | +| --- | --- | +| newAlias | none \| string \| null | + +**Example** *( GET the alias for a contact, return {(Promise<string | null>)})* +```js +const alias = await contact.alias() +if (alias === null) { + console.log('You have not yet set any alias for contact ' + contact.name()) +} else { + console.log('You have already set an alias for contact ' + contact.name() + ':' + alias) +} +``` +**Example** *(SET the alias for a contact)* +```js +try { + await contact.alias('lijiarui') + console.log(`change ${contact.name()}'s alias successfully!`) +} catch (e) { + console.log(`failed to change ${contact.name()} alias!`) +} +``` +**Example** *(DELETE the alias for a contact)* +```js +try { + const oldAlias = await contact.alias(null) + console.log(`delete ${contact.name()}'s alias successfully!`) + console.log('old alias is ${oldAlias}`) +} catch (e) { + console.log(`failed to delete ${contact.name()}'s alias!`) +} +``` + + +### contact.friend() โ‡’ boolean \| null +Check if contact is friend + +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Contact](#Contact) +**Returns**: boolean \| null -
True for friend of the bot
+False for not friend of the bot, null for unknown. +**Example** +```js +const isFriend = contact.friend() +``` + + +### contact.type() โ‡’ ContactType.Unknown \| ContactType.Personal \| ContactType.Official +Return the type of the Contact +> Tips: ContactType is enum here.
+ +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +const bot = new Wechaty() +await bot.start() +const isOfficial = contact.type() === bot.Contact.Type.Official +``` + + +### contact.gender() โ‡’ ContactGender.Unknown \| ContactGender.Male \| ContactGender.Female +Contact gender +> Tips: ContactGender is enum here.
+ +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +const gender = contact.gender() === bot.Contact.Gender.Male +``` + + +### contact.province() โ‡’ string \| null +Get the region 'province' from a contact + +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +const province = contact.province() +``` + + +### contact.city() โ‡’ string \| null +Get the region 'city' from a contact + +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +const city = contact.city() +``` + + +### contact.avatar() โ‡’ Promise.<FileBox> +Get avatar picture file stream + +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +// Save avatar to local file like `1-name.jpg` + +const file = await contact.avatar() +const name = file.name +await file.toFile(name, true) +console.log(`Contact: ${contact.name()} with avatar file: ${name}`) +``` + + +### contact.tags() โ‡’ Promise.<Array.<Tag>> +Get all tags of contact + +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +const tags = await contact.tags() +``` + + +### contact.sync() โ‡’ Promise.<this> +Force reload data for Contact, Sync data from low-level API again. + +**Kind**: instance method of [Contact](#Contact) +**Example** +```js +await contact.sync() +``` + + +### contact.self() โ‡’ boolean +Check if contact is self + +**Kind**: instance method of [Contact](#Contact) +**Returns**: boolean - True for contact is self, False for contact is others +**Example** +```js +const isSelf = contact.self() +``` + + +### Contact.find(query) โ‡’ Promise.<(Contact\|null)> +Try to find a contact by filter: {name: string | RegExp} / {alias: string | RegExp} + +Find contact by name or alias, if the result more than one, return the first one. + +**Kind**: static method of [Contact](#Contact) +**Returns**: Promise.<(Contact\|null)> - If can find the contact, return Contact, or return null + +| Param | Type | +| --- | --- | +| query | [ContactQueryFilter](#ContactQueryFilter) | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +const contactFindByName = await bot.Contact.find({ name:"ruirui"} ) +const contactFindByAlias = await bot.Contact.find({ alias:"lijiarui"} ) +``` + + +### Contact.findAll([queryArg]) โ‡’ Promise.<Array.<Contact>> +Find contact by `name` or `alias` + +If use Contact.findAll() get the contact list of the bot. + +#### definition +- `name` the name-string set by user-self, should be called name +- `alias` the name-string set by bot for others, should be called alias + +**Kind**: static method of [Contact](#Contact) + +| Param | Type | +| --- | --- | +| [queryArg] | [ContactQueryFilter](#ContactQueryFilter) | + +**Example** +```js +const bot = new Wechaty() +await bot.start() +const contactList = await bot.Contact.findAll() // get the contact list of the bot +const contactList = await bot.Contact.findAll({ name: 'ruirui' }) // find all of the contacts whose name is 'ruirui' +const contactList = await bot.Contact.findAll({ alias: 'lijiarui' }) // find all of the contacts whose alias is 'lijiarui' +``` + + +### Contact.tags() โ‡’ Promise.<Array.<Tag>> +Get tags for all contact + +**Kind**: static method of [Contact](#Contact) +**Example** +```js +const tags = await wechaty.Contact.tags() +``` + + +## ContactSelf +Bot itself will be encapsulated as a ContactSelf. + +> Tips: this class is extends Contact + +**Kind**: global class + +* [ContactSelf](#ContactSelf) + * [.avatar([file])](#ContactSelf+avatar) โ‡’ Promise.<(void\|FileBox)> + * [.qrcode()](#ContactSelf+qrcode) โ‡’ Promise.<string> + * [.signature(signature)](#ContactSelf+signature) + + + +### contactSelf.avatar([file]) โ‡’ Promise.<(void\|FileBox)> +GET / SET bot avatar + +**Kind**: instance method of [ContactSelf](#ContactSelf) + +| Param | Type | +| --- | --- | +| [file] | FileBox | + +**Example** *( GET the avatar for bot, return {Promise<FileBox>})* +```js +// Save avatar to local file like `1-name.jpg` + +bot.on('login', (user: ContactSelf) => { + console.log(`user ${user} login`) + const file = await user.avatar() + const name = file.name + await file.toFile(name, true) + console.log(`Save bot avatar: ${contact.name()} with avatar file: ${name}`) +}) +``` +**Example** *(SET the avatar for a bot)* +```js +import { FileBox } from 'wechaty' +bot.on('login', (user: ContactSelf) => { + console.log(`user ${user} login`) + const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + await user.avatar(fileBox) + console.log(`Change bot avatar successfully!`) +}) +``` + + +### contactSelf.qrcode() โ‡’ Promise.<string> +Get bot qrcode + +**Kind**: instance method of [ContactSelf](#ContactSelf) +**Example** +```js +import { generate } from 'qrcode-terminal' +bot.on('login', (user: ContactSelf) => { + console.log(`user ${user} login`) + const qrcode = await user.qrcode() + console.log(`Following is the bot qrcode!`) + generate(qrcode, { small: true }) +}) +``` + + +### contactSelf.signature(signature) +Change bot signature + +**Kind**: instance method of [ContactSelf](#ContactSelf) + +| Param | Description | +| --- | --- | +| signature | The new signature that the bot will change to | + +**Example** +```js +bot.on('login', async user => { + console.log(`user ${user} login`) + try { + await user.signature(`Signature changed by wechaty on ${new Date()}`) + } catch (e) { + console.error('change signature failed', e) + } +}) +``` + + +## Friendship +Send, receive friend request, and friend confirmation events. + +1. send request +2. receive request(in friend event) +3. confirmation friendship(friend event) + +[Examples/Friend-Bot](https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/friend-bot.ts) + +**Kind**: global class + +* [Friendship](#Friendship) + * _instance_ + * [.accept()](#Friendship+accept) โ‡’ Promise.<void> + * [.hello()](#Friendship+hello) โ‡’ string + * [.contact()](#Friendship+contact) โ‡’ [Contact](#Contact) + * [.type()](#Friendship+type) โ‡’ FriendshipType + * [.toJSON()](#Friendship+toJSON) โ‡’ FriendshipPayload + * _static_ + * [.search(condition)](#Friendship.search) โ‡’ [Promise.<Contact>](#Contact) + * [.add(contact, options)](#Friendship.add) โ‡’ Promise.<void> + * [.fromJSON()](#Friendship.fromJSON) + + + +### friendship.accept() โ‡’ Promise.<void> +Accept Friend Request + +**Kind**: instance method of [Friendship](#Friendship) +**Example** +```js +const bot = new Wechaty() +bot.on('friendship', async friendship => { + try { + console.log(`received friend event.`) + switch (friendship.type()) { + + // 1. New Friend Request + + case Friendship.Type.Receive: + await friendship.accept() + break + + // 2. Friend Ship Confirmed + + case Friendship.Type.Confirm: + console.log(`friend ship confirmed`) + break + } + } catch (e) { + console.error(e) + } +} +.start() +``` + + +### friendship.hello() โ‡’ string +Get verify message from + +**Kind**: instance method of [Friendship](#Friendship) +**Example** *(If request content is `ding`, then accept the friendship)* +```js +const bot = new Wechaty() +bot.on('friendship', async friendship => { + try { + console.log(`received friend event from ${friendship.contact().name()}`) + if (friendship.type() === Friendship.Type.Receive && friendship.hello() === 'ding') { + await friendship.accept() + } + } catch (e) { + console.error(e) + } +} +.start() +``` + + +### friendship.contact() โ‡’ [Contact](#Contact) +Get the contact from friendship + +**Kind**: instance method of [Friendship](#Friendship) +**Example** +```js +const bot = new Wechaty() +bot.on('friendship', async friendship => { + const contact = friendship.contact() + const name = contact.name() + console.log(`received friend event from ${name}`) +} +.start() +``` + + +### friendship.type() โ‡’ FriendshipType +Return the Friendship Type +> Tips: FriendshipType is enum here.
+- FriendshipType.Unknown
+- FriendshipType.Confirm
+- FriendshipType.Receive
+- FriendshipType.Verify
+ +**Kind**: instance method of [Friendship](#Friendship) +**Example** *(If request content is `ding`, then accept the friendship)* +```js +const bot = new Wechaty() +bot.on('friendship', async friendship => { + try { + if (friendship.type() === Friendship.Type.Receive && friendship.hello() === 'ding') { + await friendship.accept() + } + } catch (e) { + console.error(e) + } +} +.start() +``` + + +### friendship.toJSON() โ‡’ FriendshipPayload +get friendShipPayload Json + +**Kind**: instance method of [Friendship](#Friendship) +**Example** +```js +const bot = new Wechaty() +bot.on('friendship', async friendship => { + try { + // JSON.stringify(friendship) as well. + const payload = await friendship.toJSON() + } catch (e) { + console.error(e) + } +} +.start() +``` + + +### Friendship.search(condition) โ‡’ [Promise.<Contact>](#Contact) +Search a Friend by phone or weixin. + +The best practice is to search friend request once per minute. +Remember not to do this too frequently, or your account may be blocked. + +**Kind**: static method of [Friendship](#Friendship) + +| Param | Type | Description | +| --- | --- | --- | +| condition | FriendshipSearchCondition | Search friend by phone or weixin. | + +**Example** +```js +const friend_phone = await bot.Friendship.search({phone: '13112341234'}) +const friend_weixin = await bot.Friendship.search({weixin: 'weixin_account'}) + +console.log(`This is the new friend info searched by phone : ${friend_phone}`) +await bot.Friendship.add(friend_phone, 'hello') +``` + + +### Friendship.add(contact, options) โ‡’ Promise.<void> +Send a Friend Request to a `contact` with message `hello`. + +The best practice is to send friend request once per minute. +Remeber not to do this too frequently, or your account may be blocked. + +**Kind**: static method of [Friendship](#Friendship) + +| Param | Type | Description | +| --- | --- | --- | +| contact | [Contact](#Contact) | Send friend request to contact | +| options | FriendshipAddOptions | The friend request content | + +**Example** +```js +const contact = await bot.Friendship.search({phone: '13112341234'}) +await bot.Friendship.add(contact, 'Nice to meet you! I am wechaty bot!') + +const memberList = await room.memberList() +for (let i = 0; i < memberList.length; i++) { + await bot.Friendship.add(member, { + room: room, + hello: `Nice to meet you! I am wechaty bot from room: ${await room.topic()}!`, + }) +} +``` + + +### Friendship.fromJSON() +create friendShip by friendshipJson + +**Kind**: static method of [Friendship](#Friendship) +**Example** +```js +const bot = new Wechaty() +bot.start() + +const payload = '{...}' // your saved JSON payload +const friendship = bot.FriendShip.fromJSON(friendshipFromDisk) +await friendship.accept() +``` + + +## Message +All wechat messages will be encapsulated as a Message. + +[Examples/Ding-Dong-Bot](https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts) + +**Kind**: global class + +* [Message](#Message) + * _instance_ + * [.talker()](#Message+talker) โ‡’ [Contact](#Contact) + * [.from()](#Message+from) + * [.to()](#Message+to) โ‡’ [Contact](#Contact) \| null + * [.room()](#Message+room) โ‡’ [Room](#Room) \| null + * [.text()](#Message+text) โ‡’ string + * [.toRecalled()](#Message+toRecalled) + * [.say(textOrContactOrFile, [mention])](#Message+say) โ‡’ Promise.<(void\|Message)> + * [.recall()](#Message+recall) โ‡’ Promise.<boolean> + * [.type()](#Message+type) โ‡’ MessageType + * [.self()](#Message+self) โ‡’ boolean + * [.mentionList()](#Message+mentionList) โ‡’ Promise.<Array.<Contact>> + * ~~[.mention()](#Message+mention)~~ + * [.mentionSelf()](#Message+mentionSelf) โ‡’ Promise.<boolean> + * [.forward(to)](#Message+forward) โ‡’ Promise.<void> + * [.date()](#Message+date) + * [.age()](#Message+age) โ‡’ number + * [.toFileBox()](#Message+toFileBox) โ‡’ Promise.<FileBox> + * [.toImage()](#Message+toImage) โ‡’ Image + * [.toContact()](#Message+toContact) โ‡’ [Promise.<Contact>](#Contact) + * _static_ + * [.find()](#Message.find) + * [.findAll()](#Message.findAll) + + + +### message.talker() โ‡’ [Contact](#Contact) +Get the talker of a message. + +**Kind**: instance method of [Message](#Message) +**Example** +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const talker = msg.talker() + const text = msg.text() + const room = msg.room() + if (room) { + const topic = await room.topic() + console.log(`Room: ${topic} Contact: ${talker.name()} Text: ${text}`) + } else { + console.log(`Contact: ${talker.name()} Text: ${text}`) + } +}) +.start() +``` + + +### message.from() +**Kind**: instance method of [Message](#Message) +**Depreacated**: Use `message.talker()` to replace `message.from()` + https://github.com/wechaty/wechaty/issues/2094 + + +### message.to() โ‡’ [Contact](#Contact) \| null +Get the destination of the message +Message.to() will return null if a message is in a room, use Message.room() to get the room. + +**Kind**: instance method of [Message](#Message) + + +### message.room() โ‡’ [Room](#Room) \| null +Get the room from the message. +If the message is not in a room, then will return `null` + +**Kind**: instance method of [Message](#Message) +**Example** +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const contact = msg.from() + const text = msg.text() + const room = msg.room() + if (room) { + const topic = await room.topic() + console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`) + } else { + console.log(`Contact: ${contact.name()} Text: ${text}`) + } +}) +.start() +``` + + +### message.text() โ‡’ string +Get the text content of the message + +**Kind**: instance method of [Message](#Message) +**Example** +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const contact = msg.from() + const text = msg.text() + const room = msg.room() + if (room) { + const topic = await room.topic() + console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`) + } else { + console.log(`Contact: ${contact.name()} Text: ${text}`) + } +}) +.start() +``` + + +### message.toRecalled() +Get the recalled message + +**Kind**: instance method of [Message](#Message) +**Example** +```js +const bot = new Wechaty() +bot +.on('message', async m => { + if (m.type() === MessageType.Recalled) { + const recalledMessage = await m.toRecalled() + console.log(`Message: ${recalledMessage} has been recalled.`) + } +}) +.start() +``` + + +### message.say(textOrContactOrFile, [mention]) โ‡’ Promise.<(void\|Message)> +Reply a Text or Media File message to the sender. +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Message](#Message) +**See**: [Examples/ding-dong-bot](https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts) + +| Param | Type | Description | +| --- | --- | --- | +| textOrContactOrFile | string \| [Contact](#Contact) \| FileBox \| UrlLink \| MiniProgram | send text, Contact, or file to bot.
You can use [FileBox](https://www.npmjs.com/package/file-box) to send file | +| [mention] | [Contact](#Contact) \| [Array.<Contact>](#Contact) | If this is a room message, when you set mention param, you can `@` Contact in the room. | + +**Example** +```js +import { FileBox } from 'wechaty' +const bot = new Wechaty() +bot +.on('message', async m => { + +// 1. send Image + + if (/^ding$/i.test(m.text())) { + const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + await msg.say(fileBox) + const message = await msg.say(fileBox) // only supported by puppet-padplus + } + +// 2. send Text + + if (/^dong$/i.test(m.text())) { + await msg.say('ding') + const message = await msg.say('ding') // only supported by puppet-padplus + } + +// 3. send Contact + + if (/^lijiarui$/i.test(m.text())) { + const contactCard = await bot.Contact.find({name: 'lijiarui'}) + if (!contactCard) { + console.log('not found') + return + } + await msg.say(contactCard) + const message = await msg.say(contactCard) // only supported by puppet-padplus + } + +// 4. send Link + + if (/^link$/i.test(m.text())) { + const linkPayload = new UrlLink ({ + description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + title : 'Welcome to Wechaty', + url : 'https://github.com/wechaty/wechaty', + }) + await msg.say(linkPayload) + const message = await msg.say(linkPayload) // only supported by puppet-padplus + } + +// 5. send MiniProgram + + if (/^link$/i.test(m.text())) { + const miniProgramPayload = new MiniProgram ({ + username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + appid : '', //optional, get from mp.weixin.qq.com + title : '', //optional + pagepath : '', //optional + description : '', //optional + thumbnailurl : '', //optional + }) + await msg.say(miniProgramPayload) + const message = await msg.say(miniProgramPayload) // only supported by puppet-padplus + } + +}) +.start() +``` + + +### message.recall() โ‡’ Promise.<boolean> +Recall a message. +> Tips: + +**Kind**: instance method of [Message](#Message) +**Example** +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const recallMessage = await msg.say('123') + if (recallMessage) { + const isSuccess = await recallMessage.recall() + } +}) +``` + + +### message.type() โ‡’ MessageType +Get the type from the message. +> Tips: MessageType is Enum here.
+- MessageType.Unknown
+- MessageType.Attachment
+- MessageType.Audio
+- MessageType.Contact
+- MessageType.Emoticon
+- MessageType.Image
+- MessageType.Text
+- MessageType.Video
+- MessageType.Url
+ +**Kind**: instance method of [Message](#Message) +**Example** +```js +const bot = new Wechaty() +if (message.type() === bot.Message.Type.Text) { + console.log('This is a text message') +} +``` + + +### message.self() โ‡’ boolean +Check if a message is sent by self. + +**Kind**: instance method of [Message](#Message) +**Returns**: boolean - - Return `true` for send from self, `false` for send from others. +**Example** +```js +if (message.self()) { + console.log('this message is sent by myself!') +} +``` + + +### message.mentionList() โ‡’ Promise.<Array.<Contact>> +Get message mentioned contactList. + +Message event table as follows + +| | Web | Mac PC Client | iOS Mobile | android Mobile | +| :--- | :--: | :----: | :---: | :---: | +| [You were mentioned] tip ([ๆœ‰ไบบ@ๆˆ‘]็š„ๆ็คบ) | โœ˜ | โˆš | โˆš | โˆš | +| Identify magic code (8197) by copy & paste in mobile | โœ˜ | โˆš | โˆš | โœ˜ | +| Identify magic code (8197) by programming | โœ˜ | โœ˜ | โœ˜ | โœ˜ | +| Identify two contacts with the same roomAlias by [You were mentioned] tip | โœ˜ | โœ˜ | โˆš | โˆš | + +**Kind**: instance method of [Message](#Message) +**Returns**: Promise.<Array.<Contact>> - - Return message mentioned contactList +**Example** +```js +const contactList = await message.mentionList() +console.log(contactList) +``` + + +### ~~message.mention()~~ +***Deprecated*** + +**Kind**: instance method of [Message](#Message) + + +### message.mentionSelf() โ‡’ Promise.<boolean> +Check if a message is mention self. + +**Kind**: instance method of [Message](#Message) +**Returns**: Promise.<boolean> - - Return `true` for mention me. +**Example** +```js +if (await message.mentionSelf()) { + console.log('this message were mentioned me! [You were mentioned] tip ([ๆœ‰ไบบ@ๆˆ‘]็š„ๆ็คบ)') +} +``` + + +### message.forward(to) โ‡’ Promise.<void> +Forward the received message. + +**Kind**: instance method of [Message](#Message) + +| Param | Type | Description | +| --- | --- | --- | +| to | Sayable \| Array.<Sayable> | Room or Contact The recipient of the message, the room, or the contact | + +**Example** +```js +const bot = new Wechaty() +bot +.on('message', async m => { + const room = await bot.Room.find({topic: 'wechaty'}) + if (room) { + await m.forward(room) + console.log('forward this message to wechaty room!') + } +}) +.start() +``` + + +### message.date() +Message sent date + +**Kind**: instance method of [Message](#Message) + + +### message.age() โ‡’ number +Returns the message age in seconds.
+ +For example, the message is sent at time `8:43:01`, +and when we received it in Wechaty, the time is `8:43:15`, +then the age() will return `8:43:15 - 8:43:01 = 14 (seconds)` + +**Kind**: instance method of [Message](#Message) +**Returns**: number - message age in seconds. + + +### message.toFileBox() โ‡’ Promise.<FileBox> +Extract the Media File from the Message, and put it into the FileBox. +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Message](#Message) +**Example** *(Save media file from a message)* +```js +const fileBox = await message.toFileBox() +const fileName = fileBox.name +fileBox.toFile(fileName) +``` + + +### message.toImage() โ‡’ Image +Extract the Image File from the Message, so that we can use different image sizes. +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Message](#Message) +**Example** *(Save image file from a message)* +```js +const image = message.toImage() +const fileBox = await image.artwork() +const fileName = fileBox.name +fileBox.toFile(fileName) +``` + + +### message.toContact() โ‡’ [Promise.<Contact>](#Contact) +Get Share Card of the Message +Extract the Contact Card from the Message, and encapsulate it into Contact class +> Tips: +This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + +**Kind**: instance method of [Message](#Message) + + +### Message.find() +Find message in cache + +**Kind**: static method of [Message](#Message) + + +### Message.findAll() +Find messages in cache + +**Kind**: static method of [Message](#Message) + + +## RoomInvitation +accept room invitation + +**Kind**: global class + +* [RoomInvitation](#RoomInvitation) + * _instance_ + * [.accept()](#RoomInvitation+accept) โ‡’ Promise.<void> + * [.inviter()](#RoomInvitation+inviter) โ‡’ [Contact](#Contact) + * [.topic()](#RoomInvitation+topic) โ‡’ string + * [.date()](#RoomInvitation+date) โ‡’ Promise.<Date> + * [.age()](#RoomInvitation+age) โ‡’ number + * [.toJSON()](#RoomInvitation+toJSON) โ‡’ string + * _static_ + * [.fromJSON()](#RoomInvitation.fromJSON) โ‡’ [RoomInvitation](#RoomInvitation) + + + +### roomInvitation.accept() โ‡’ Promise.<void> +Accept Room Invitation + +**Kind**: instance method of [RoomInvitation](#RoomInvitation) +**Example** +```js +const bot = new Wechaty() +bot.on('room-invite', async roomInvitation => { + try { + console.log(`received room-invite event.`) + await roomInvitation.accept() + } catch (e) { + console.error(e) + } +} +.start() +``` + + +### roomInvitation.inviter() โ‡’ [Contact](#Contact) +Get the inviter from room invitation + +**Kind**: instance method of [RoomInvitation](#RoomInvitation) +**Example** +```js +const bot = new Wechaty() +bot.on('room-invite', async roomInvitation => { + const inviter = await roomInvitation.inviter() + const name = inviter.name() + console.log(`received room invitation event from ${name}`) +} +.start() +``` + + +### roomInvitation.topic() โ‡’ string +Get the room topic from room invitation + +**Kind**: instance method of [RoomInvitation](#RoomInvitation) +**Example** +```js +const bot = new Wechaty() +bot.on('room-invite', async roomInvitation => { + const topic = await roomInvitation.topic() + console.log(`received room invitation event from room ${topic}`) +} +.start() +``` + + +### roomInvitation.date() โ‡’ Promise.<Date> +Get the invitation time + +**Kind**: instance method of [RoomInvitation](#RoomInvitation) + + +### roomInvitation.age() โ‡’ number +Returns the roopm invitation age in seconds.
+ +For example, the invitation is sent at time `8:43:01`, +and when we received it in Wechaty, the time is `8:43:15`, +then the age() will return `8:43:15 - 8:43:01 = 14 (seconds)` + +**Kind**: instance method of [RoomInvitation](#RoomInvitation) + + +### roomInvitation.toJSON() โ‡’ string +Get the room invitation info when listened on room-invite event + +**Kind**: instance method of [RoomInvitation](#RoomInvitation) +**Example** +```js +const bot = new Wechaty() +bot.on('room-invite', async roomInvitation => { + const roomInvitation = bot.RoomInvitation.load(roomInvitation.id) + const jsonData = await roomInvitation.toJSON(roomInvitation.id) + // save the json data to disk, and we can use it by RoomInvitation.fromJSON() +} +.start() +``` + + +### RoomInvitation.fromJSON() โ‡’ [RoomInvitation](#RoomInvitation) +Load the room invitation info from disk + +**Kind**: static method of [RoomInvitation](#RoomInvitation) +**Example** +```js +const bot = new Wechaty() +const dataFromDisk // get the room invitation info data from disk +const roomInvitation = await bot.RoomInvitation.fromJSON(dataFromDisk) +await roomInvitation.accept() +``` + + +## cuid\_1 +Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + +**Kind**: global constant +**Copyright**: 2016 Huan LI (ๆŽๅ“ๆก“) , and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## clone\_class\_1 +Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + +**Kind**: global constant +**Copyright**: 2016 Huan LI (ๆŽๅ“ๆก“) , and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## clone\_class\_1 +Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + +**Kind**: global constant +**Copyright**: 2016 Huan LI (ๆŽๅ“ๆก“) , and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## wechaty\_puppet\_1 +Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + +**Kind**: global constant +**Copyright**: 2016 Huan LI (ๆŽๅ“ๆก“) , and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## events\_1 +Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + +**Kind**: global constant +**Copyright**: 2016 Huan LI (ๆŽๅ“ๆก“) , and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## events\_1 +Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + +**Kind**: global constant +**Copyright**: 2016 Huan LI (ๆŽๅ“ๆก“) , and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## clone\_class\_1 +Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + +**Kind**: global constant +**Copyright**: 2016 Huan LI (ๆŽๅ“ๆก“) , and + Wechaty Contributors . + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +## guardWechatify() +Huan(202008): we will bind the wechaty puppet with user modules (Contact, Room, etc) together inside the start() method + +**Kind**: global function + + +## PuppetModuleName +The term [Puppet](https://github.com/wechaty/wechaty/wiki/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). +The plugins are named XXXPuppet, for example: +- [PuppetPuppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer): +- [PuppetPadchat](https://github.com/wechaty/wechaty-puppet-padchat) + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| PUPPET_DEFAULT | string | The default puppet. | +| wechaty-puppet-wechat4u | string | The default puppet, using the [wechat4u](https://github.com/nodeWechat/wechat4u) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser. | +| wechaty-puppet-padchat | string | - Using the WebSocket protocol to connect with a Protocol Server for controlling the iPad WeChat program. | +| wechaty-puppet-puppeteer | string | - Using the [google puppeteer](https://github.com/GoogleChrome/puppeteer) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser. | +| wechaty-puppet-mock | string | - Using the mock data to mock wechat operation, just for test. | + + + +## WechatyOptions +The option parameter to create a wechaty instance + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| name | string | Wechaty Name.
When you set this:
`new Wechaty({name: 'wechaty-name'}) `
it will generate a file called `wechaty-name.memory-card.json`.
This file stores the login information for bot.
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_NAME` to set this value when you start.
eg: `WECHATY_NAME="your-cute-bot-name" node bot.js` | +| puppet | [PuppetModuleName](#PuppetModuleName) \| Puppet | Puppet name or instance | +| puppetOptions | Partial.<PuppetOptions> | Puppet TOKEN | +| ioToken | string | Io TOKEN | + + + +## RoomQueryFilter +The filter to find the room: {topic: string | RegExp} + +**Kind**: global typedef +**Properties** + +| Name | Type | +| --- | --- | +| topic | string | + + + +## RoomEventName +Room Class Event Type + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| join | string | Emit when anyone join any room. | +| topic | string | Get topic event, emitted when someone change room topic. | +| leave | string | Emit when anyone leave the room.
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. | + + + +## RoomEventFunction +Room Class Event Function + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| room-join | function | (this: Room, inviteeList: Contact[] , inviter: Contact) => void | +| room-topic | function | (this: Room, topic: string, oldTopic: string, changer: Contact) => void | +| room-leave | function | (this: Room, leaver: Contact) => void | + + + +## RoomMemberQueryFilter +The way to search member by Room.member() + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| name | string | Find the contact by WeChat name in a room, equal to `Contact.name()`. | +| roomAlias | string | Find the contact by alias set by the bot for others in a room. | +| contactAlias | string | Find the contact by alias set by the contact out of a room, equal to `Contact.alias()`. [More Detail](https://github.com/wechaty/wechaty/issues/365) | + + + +## ContactQueryFilter +The way to search Contact + +**Kind**: global typedef +**Properties** + +| Name | Type | Description | +| --- | --- | --- | +| name | string | The name-string set by user-self, should be called name | +| alias | string | The name-string set by bot for others, should be called alias [More Detail](https://github.com/wechaty/wechaty/issues/365) | + + + diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..8b5c371 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,7 @@ +# EXAMPLES + +The Wechaty Official Example List Directory has been move to: + + + +Please use [wechaty-getting-started](https://github.com/wechaty/getting-started) repository as the starter template for your project because it can work out-of-the-box with lots of examples. diff --git a/examples/ding-dong-bot.ts b/examples/ding-dong-bot.ts new file mode 100644 index 0000000..b9cd734 --- /dev/null +++ b/examples/ding-dong-bot.ts @@ -0,0 +1,205 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + WechatyBuilder, + ScanStatus, + Message, + Contact, +} from '../src/mods/mod.js' // from 'wechaty' + +import qrTerm from 'qrcode-terminal' +import { FileBox } from 'file-box' + +/** + * + * 1. Declare your Bot! + * + */ +const options = { + name : 'ding-dong-bot', + + /** + * You can specify different puppet for different IM protocols. + * Learn more from https://wechaty.js.org/docs/puppet-providers/ + */ + // puppet: 'wechaty-puppet-whatsapp' + + /** + * You can use wechaty puppet provider 'wechaty-puppet-service' + * which can connect to Wechaty Puppet Services + * for using more powerful protocol. + * Learn more about services (and TOKEN)from https://wechaty.js.org/docs/puppet-services/ + */ + // puppet: 'wechaty-puppet-service' + // puppetOptions: { + // token: 'xxx', + // } +} + +const bot = WechatyBuilder.build(options) + +/** + * + * 2. Register event handlers for Bot + * + */ +bot + .on('logout', onLogout) + .on('login', onLogin) + .on('scan', onScan) + .on('error', onError) + .on('message', onMessage) +/** + * + * 3. Start the bot! + * + */ + .start() + .catch(async e => { + console.error('Bot start() fail:', e) + await bot.stop() + process.exit(-1) + }) + +/** + * + * 4. You are all set. ;-] + * + */ + +/** + * + * 5. Define Event Handler Functions for: + * `scan`, `login`, `logout`, `error`, and `message` + * + */ +function onScan (qrcode: string, status: ScanStatus) { + if (status === ScanStatus.Waiting || status === ScanStatus.Timeout) { + qrTerm.generate(qrcode) + + const qrcodeImageUrl = [ + 'https://wechaty.js.org/qrcode/', + encodeURIComponent(qrcode), + ].join('') + + console.info('onScan: %s(%s) - %s', ScanStatus[status], status, qrcodeImageUrl) + } else { + console.info('onScan: %s(%s)', ScanStatus[status], status) + } + + // console.info(`[${ScanStatus[status]}(${status})] ${qrcodeImageUrl}\nScan QR Code above to log in: `) +} + +function onLogin (user: Contact) { + console.info(`${user.name()} login`) +} + +function onLogout (user: Contact) { + console.info(`${user.name()} logged out`) +} + +function onError (e: Error) { + console.error('Bot error:', e) + /* + if (bot.isLoggedIn) { + bot.say('Wechaty error: ' + e.message).catch(console.error) + } + */ +} + +/** + * + * 6. The most important handler is for: + * dealing with Messages. + * + */ +async function onMessage (msg: Message) { + console.info(msg.toString()) + + if (msg.self()) { + console.info('Message discarded because its outgoing') + return + } + + if (msg.age() > 2 * 60) { + console.info('Message discarded because its TOO OLD(than 2 minutes)') + return + } + + if (msg.type() !== bot.Message.Type.Text + || !/^(ding|ping|bing|code)$/i.test(msg.text()) + ) { + console.info('Message discarded because it does not match ding/ping/bing/code') + return + } + + /** + * 1. reply 'dong' + */ + await msg.say('dong') + console.info('REPLY: dong') + + /** + * 2. reply image(qrcode image) + */ + const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + + await msg.say(fileBox) + console.info('REPLY: %s', fileBox.toString()) + + /** + * 3. reply 'scan now!' + */ + await msg.say([ + 'Join Wechaty Developers Community\n\n', + 'Scan now, because other Wechaty developers want to talk with you too!\n\n', + '(secret code: wechaty)', + ].join('')) +} + +/** + * + * 7. Output the Welcome Message + * + */ +const welcome = ` +| __ __ _ _ +| \\ \\ / /__ ___| |__ __ _| |_ _ _ +| \\ \\ /\\ / / _ \\/ __| '_ \\ / _\` | __| | | | +| \\ V V / __/ (__| | | | (_| | |_| |_| | +| \\_/\\_/ \\___|\\___|_| |_|\\__,_|\\__|\\__, | +| |___/ + +=============== Powered by Wechaty =============== +-------- https://github.com/wechaty/wechaty -------- + Version: ${bot.version()} + +I'm a bot, my superpower is talk in Wechat. + +If you send me a 'ding', I will reply you a 'dong'! +__________________________________________________ + +Hope you like it, and you are very welcome to +upgrade me to more superpowers! + +Please wait... I'm trying to login in... + +` +console.info(welcome) diff --git a/examples/video-post.ts b/examples/video-post.ts new file mode 100644 index 0000000..0ffeaf6 --- /dev/null +++ b/examples/video-post.ts @@ -0,0 +1,92 @@ +import PUPPET from 'wechaty-puppet' +import { FileBox } from 'file-box' + +import { + WechatyBuilder, +} from '../src/mods/mod.js' + +const bot = WechatyBuilder.build({ + name : 'video-post-bot', +}) + +async function testVideoPost () { + const contact = await bot.Contact.find({ id: 'xxx' }) + const room = await bot.Room.find({ id: 'xxx' }) + + if (!contact || !room) { + return + } + + const post = await bot.Post.builder() + .add('hello, world') + .add(FileBox.fromQRCode('qr')) + .add(await bot.UrlLink.create('https://wechaty.js.org')) + .build() + + await bot.say(post) + + bot.on('message', async message => { + + // if (message.type() !== message.type.Post) { + // return + // } + + // forward video post + await message.forward(contact) + + /** + * Video Post + */ + const post2 = await message.toPost() + + const counter = post2.counter() + console.info('total tap(like) number:', counter.taps && counter.taps[PUPPET.types.Tap.Like]) + console.info('total children number:', counter.descendant) + + /** + * Comment + */ + // reply comment + await post2.reply('xxxx') + await post2.reply(FileBox.fromQRCode('qrimage')) + + for await (const descendant of post.descendants({ contactId: message.wechaty.currentUser.id })) { + console.info(descendant) + } + + /** + * Like + */ + + // like message + await post.like(true) + // await post.tap(PostTapType.Like, true) + + // check whether we have liked this post + const liked = await post.like() + console.info('liked date:', liked) + // const liked = await post.tap(PostTapType.Like) + + // cancel like + await post.like(false) + // await post.tap(PostTapType.Like, false) + + // list all likers + for await (const tap of post.taps()) { + console.info('Taper -------') + console.info('taper:', tap.contact) + console.info('date:', tap.date) + console.info('type:', tap.type) + } + + for await (const like of post.likes()) { + console.info('-------') + console.info('liker:', like.contact) + console.info('date:', like.date) + console.info('type:', like.type) + } + }) + +} + +void testVideoPost() diff --git a/package.json b/package.json new file mode 100644 index 0000000..314b497 --- /dev/null +++ b/package.json @@ -0,0 +1,177 @@ +{ + "name": "wechaty", + "version": "2.0.0-alpha.1", + "description": "Wechaty is a RPA SDK for Chatbot Makers.", + "type": "module", + "exports": { + ".": { + "import": "./dist/esm/src/mods/mod.js", + "require": "./dist/cjs/src/mods/mod.js" + }, + "./impls": { + "import": "./dist/esm/src/mods/impls.js", + "require": "./dist/cjs/src/mods/impls.js" + }, + "./users": { + "import": "./dist/esm/src/mods/users.js", + "require": "./dist/cjs/src/mods/users.js" + } + }, + "typesVersions": { + "*": { + "users": [ + "./dist/esm/src/mods/users.d.ts" + ], + "impls": [ + "./dist/esm/src/mods/impls.d.ts" + ] + } + }, + "types": "./dist/esm/src/mods/mod.d.ts", + "engines": { + "node": ">=16", + "npm": ">=7" + }, + "wechaty": { + "DEFAULT_PORT": 8080, + "DEFAULT_PROTOCOL": "io|0.0.1", + "DEFAULT_APIHOST": "api.chatie.io" + }, + "scripts": { + "build": "tsc && tsc -p tsconfig.cjs.json", + "changelog": "docker run -it --rm -e CHANGELOG_GITHUB_TOKEN -v \"$(pwd)\":/usr/local/src/your-app ferrarimarco/github-changelog-generator -u wechaty -p wechaty && sed -i'.bak' /greenkeeper/d CHANGELOG.md && sed -i'.bak' /Snyk/d CHANGELOG.md && sed -i'.bak' '/An in-range update of/d' CHANGELOG.md && node --no-warnings --loader=ts-node/esm scripts/sort-contributiveness.ts < CHANGELOG.md > CHANGELOG.new.md 2>/dev/null && cat CHANGELOG.md >> CHANGELOG.new.md && mv CHANGELOG.new.md CHANGELOG.md", + "clean": "shx rm -fr dist/*", + "coverage": "nyc report --reporter=lcov", + "cli": "node --no-warnings --loader ts-node/esm bin/cli.ts", + "demo": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" node examples/ding-dong-bot.ts", + "dist": "npm-run-all clean build dist:commonjs", + "dist:commonjs": "jq -n \"{ type: \\\"commonjs\\\" }\" > dist/cjs/package.json", + "docs": "bash -x scripts/generate-docs.sh", + "doctor": "node --no-warnings --loader=ts-node/esm bin/doctor.ts", + "io-client": "node --no-warnings --loader=ts-node/esm bin/io-client.ts", + "lint:es": "eslint --ignore-pattern node_modules/ --ignore-pattern fixtures/ \"{bin,examples,src,scripts,tests}/**/*.ts\"", + "lint:md": "markdownlint README.md", + "lint:sh": "bash -n bin/*.sh", + "lint:ts": "tsc --isolatedModules --noEmit", + "lint": "npm-run-all lint:es lint:ts lint:sh lint:md", + "puppet-install": "node --no-warnings --loader=ts-node/esm bin/puppet-install.ts", + "sloc": "sloc bin examples scripts src tests --details --format cli-table --keys total,source,comment && sloc bin examples scripts src tests", + "start": "npm run demo", + "test:pack": "bash -x scripts/npm-pack-testing.sh", + "test:shell": "shellcheck bin/*.sh scripts/*.sh", + "test:unit": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" TAP_TIMEOUT=600 tap \"src/**/*.spec.ts\" \"tests/**/*.spec.ts\"", + "test": "npm run lint && nyc npm run test:unit", + "typedoc": "bash scripts/typedoc.sh" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/wechaty/wechaty.git" + }, + "bin": { + "wechaty": "dist/esm/bin/cli.js", + "wechaty-doctor": "dist/esm/bin/doctor.js", + "wechaty-io-client": "dist/esm/bin/io-client.js", + "wechaty-puppet-install": "dist/esm/bin/puppet-install.js", + "wechaty-version": "dist/esm/bin/version.js" + }, + "keywords": [ + "ai", + "bot", + "chatbot", + "chatie", + "conversational", + "framework", + "robotics", + "robot", + "rpa", + "sdk", + "wechat", + "wecom", + "wxwork", + "lark", + "TikTok", + "weixin", + "whatsapp", + "ๅพฎไฟก" + ], + "author": { + "name": "Huan LI (ๆŽๅ“ๆก“)", + "email": "zixia@zixia.net", + "url": "https://linkedin.com/in/zixia/" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/wechaty/wechaty/issues" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/wechaty" + }, + "homepage": "https://github.com/wechaty/", + "dependencies": { + "clone-class": "^1.1.1", + "cmd-ts": "^0.10.0", + "cockatiel": "^2.0.2", + "cross-spawn": "^7.0.3", + "dotenv": "^16.0.0", + "file-box": "^1.4.15", + "fp-ts": "^2.11.9", + "gerror": "^1.0.16", + "get-port": "^6.1.2", + "json-rpc-peer": "^0.17.0", + "memory-card": "^1.1.2", + "open-graph": "^0.2.6", + "rx-queue": "^1.0.5", + "state-switch": "^1.6.3", + "uuid": "^8.3.2", + "wechaty-puppet": "^1.20.1", + "wechaty-puppet-service": "^1.19.8", + "wechaty-puppet-wechat4u": "^1.13.15", + "wechaty-token": "^1.1.1", + "ws": "^8.5.0" + }, + "devDependencies": { + "@chatie/eslint-config": "^1.0.4", + "@chatie/git-scripts": "^0.6.2", + "@chatie/semver": "^0.6.0", + "@chatie/tsconfig": "^4.6.3", + "@types/cross-spawn": "^6.0.2", + "@types/dotenv": "^8.2.0", + "@types/glob": "^7.2.0", + "@types/open-graph": "^0.2.2", + "@types/qrcode-terminal": "^0.12.0", + "@types/raven": "^2.5.4", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.5.3", + "coveralls": "^3.1.1", + "glob": "^7.2.0", + "jsdoc-to-markdown": "^7.1.1", + "nyc": "^15.1.0", + "qrcode-terminal": "^0.12.0", + "raven": "^2.6.4", + "regenerator-runtime": "^0.13.9", + "sloc": "^0.2.1", + "typed-emitter": "^1.5.0-from-event", + "typedoc": "^1.0.0-dev.4 ", + "wechaty-puppet-mock": "^1.18.2", + "wechaty-puppet-padlocal": "^1.11.13" + }, + "files": [ + "bin/", + "dist/", + "src/" + ], + "publishConfig": { + "access": "public", + "tag": "next" + }, + "collective": { + "type": "opencollective", + "url": "https://opencollective.com/wechaty" + }, + "git": { + "scripts": { + "pre-push": "npx git-scripts-pre-push" + } + } +} diff --git a/scripts/docker.sh b/scripts/docker.sh new file mode 100755 index 0000000..407670d --- /dev/null +++ b/scripts/docker.sh @@ -0,0 +1,108 @@ +#!/bin/bash -x + +set -eo pipefail + +export ARTIFACT_IMAGE='wechaty/wechaty:artifact' + +function dockerBuild () { + platform=$1 # `all` for all platforms, `linux/amd64`, `linux/arm64`, etc. + tag=${2} # set the tag for building image + deploy=$3 # `true` for deploy, not set for local build + + # Shellcheck - https://github.com/koalaman/shellcheck/wiki/SC2086 + options=('--rm') + [ -n "$NO_CACHE" ] && options+=('--no-cache') + + [ "$platform" == "all" ] && platform='linux/amd64,linux/arm64,linux/arm/v7' + [ -n "$deploy" ] && options+=('--push') + + cat <<_CMD_ + docker buildx build \ + "${options[@]}" \ + --platform "$platform" \ + --tag "wechaty/wechaty:$tag" \ + . +_CMD_ + + docker buildx build \ + "${options[@]}" \ + --platform "$platform" \ + --tag "wechaty/wechaty:$tag" \ + . +} + +function deployOnbuild () { + export tag=$1 + + echo "Building & Deploying 'wechaty/onbuild:$tag' based on 'wechaty/wechaty:$tag'" + + sed "s#FROM wechaty/wechaty:next#FROM wechaty/wechaty:$tag#" < Dockerfile.onbuild \ + | docker buildx build \ + --platform 'linux/amd64,linux/arm64,linux/arm/v7' \ + --tag "wechaty/onbuild:$tag" \ + --push \ + - +} + +function main () { +declare -i ret=0 + + case "$1" in + build | '') + docker build \ + --rm \ + --tag "$ARTIFACT_IMAGE" \ + . + + ret=$? + ;; + + test) + echo "Testing the docker image behaviors to make sure it works as expected..." + echo "bats tests/" + IMAGE_NAME="$ARTIFACT_IMAGE" bats tests/ + + echo + echo + echo docker run -i --rm -v /dev/shm:/dev/shm "$ARTIFACT_IMAGE" test + exec docker run -i --rm -v /dev/shm:/dev/shm "$ARTIFACT_IMAGE" test + ret=$? + ;; + + deploy) + version=$(npx pkg-jq -r .version) + majorVer=$(echo "$version" | cut -d. -f1) + minorVer=$(echo "$version" | cut -d. -f2) + versionTag="${majorVer}.${minorVer}" + + dockerBuild all "$versionTag" deploy + deployOnbuild "$versionTag" + + if npx --package @chatie/semver semver-is-prod "$version"; then + dockerBuild all latest deploy + deployOnbuild latest + else + dockerBuild all next deploy + deployOnbuild next + fi + ;; + + clean) + docker ps -a | grep Exited | awk '{print $1}' | xargs docker rm || true + docker images | grep none | awk '{print $3}' | xargs docker rmi + ;; + + *) + echo docker run -ti --rm -v /dev/shm:/dev/shm "$ARTIFACT_IMAGE" "$@" + exec docker run -ti --rm -v /dev/shm:/dev/shm "$ARTIFACT_IMAGE" "$@" + ;; + esac + + [ "$ret" -ne 0 ] && { + echo "ERROR: exec return $ret ???" + } + + return $ret +} + +main "$@" diff --git a/scripts/generate-docs.sh b/scripts/generate-docs.sh new file mode 100755 index 0000000..28bbacc --- /dev/null +++ b/scripts/generate-docs.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -e + +VERSION=$(npx pkg-jq -r .version) +echo "Version $VERSION" + +if ! npx --package @chatie/semver semver-is-prod "$VERSION"; then + echo "Current release is a development release, please only update the docs when there's a stable release." + + if [ "$1" != "-f" ]; then + echo "Or use -f to force generating." + exit 1 + else + echo "-f found. Forcing generating ..." + fi +fi + +echo "Generating docs ..." +npm run dist + +DOCS_INDEX_MD=docs/index.md + +cat <<_EOF_ > "$DOCS_INDEX_MD" +# Wechaty v$(jq -r .version package.json) Documentation + +- Website - +- Docs Site - +- API References - + +_EOF_ + +jsdoc2md \ + dist/src/wechaty.js \ + dist/src/user/{room,contact,contact-self,friendship,message,room-invitation}.js \ + >> "$DOCS_INDEX_MD" + +# +# https://sidecar.gitter.im/ +# +cat <<_EOF_ >> "$DOCS_INDEX_MD" + + +_EOF_ diff --git a/scripts/generate-package-json.sh b/scripts/generate-package-json.sh new file mode 100755 index 0000000..0297905 --- /dev/null +++ b/scripts/generate-package-json.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -e + +SRC_PACKAGE_JSON_TS_FILE='src/package-json.ts' + +[ -f ${SRC_PACKAGE_JSON_TS_FILE} ] || { + echo ${SRC_PACKAGE_JSON_TS_FILE}" not found" + exit 1 +} + +GIT_COMMIT_HASH=$(git rev-parse HEAD) + +cat <<_SRC_ > ${SRC_PACKAGE_JSON_TS_FILE} +/* eslint-disable */ +/** + * This file was auto generated from scripts/generate-version.sh + */ +import type { PackageJsonWechaty } from './config' + +export const GIT_COMMIT_HASH: string = '${GIT_COMMIT_HASH}' +export const packageJson: PackageJsonWechaty = $(cat package.json) as any +_SRC_ diff --git a/scripts/git-push-tag.sh b/scripts/git-push-tag.sh new file mode 100755 index 0000000..bae9bb1 --- /dev/null +++ b/scripts/git-push-tag.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +[ -z "$1" ] && { + echo + echo "Usage: git-push-tag.sh " + echo + exit 1 +} + +NO_HOOK=1 git push origin "$1" diff --git a/scripts/install-chrome-linux.sh b/scripts/install-chrome-linux.sh new file mode 100755 index 0000000..adb7128 --- /dev/null +++ b/scripts/install-chrome-linux.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +[ "$(command -v google-chrome)" = "" ] && { + [ "$(grep chrome /etc/apt/sources.list.d/*)" -gt 0 ] && { + wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo sh -c 'echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' + } + sudo apt-get update + sudo apt-get install -y google-chrome-stable +} + +google-chrome --version diff --git a/scripts/npm-deprecate-version.sh b/scripts/npm-deprecate-version.sh new file mode 100755 index 0000000..4fdd7c5 --- /dev/null +++ b/scripts/npm-deprecate-version.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -e + +# https://docs.npmjs.com/cli/deprecate +# $ npm deprecate [@] + +# npm semver calculator +# - https://semver.npmjs.com + +if [ $# -eq 0 ]; then + echo + echo "Usage:" + echo + echo " deprecate all the 0.15.* versions." + echo " \$ $0 0.15" + echo " \$ $0 0.15.*" + echo " \$ $0 ^0.15.1" + echo + +else + message="${2:-WARNING: this version(odd number) is coming from a developing branch which means it is very possible UNSTABLE. Please use the latest version with even number if you are in production. You can know more about the Wechaty version numbering design at https://github.com/wechaty/wechaty/issues/905}" + + npm deprecate "wechaty@$1" "$message" + +fi diff --git a/scripts/npm-pack-testing.sh b/scripts/npm-pack-testing.sh new file mode 100755 index 0000000..be81a55 --- /dev/null +++ b/scripts/npm-pack-testing.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -e + +VERSION=$(npx pkg-jq -r .version) + +if npx --package @chatie/semver semver-is-prod "$VERSION"; then + NPM_TAG=latest +else + NPM_TAG=next +fi + +npm run dist +npm pack + +TMPDIR="/tmp/npm-pack-testing.$$" +mkdir "$TMPDIR" +# trap "rm -fr $TMPDIR" EXIT + +mv ./*-*.*.*.tgz "$TMPDIR" +cp tests/fixtures/smoke-testing.ts "$TMPDIR" + +cd $TMPDIR + +npm init -y +npm install --production ./*-*.*.*.tgz \ + @types/node \ + typescript@latest \ + pkg-jq \ + file-box@"$NPM_TAG" \ + wechaty-puppet-mock@"$NPM_TAG" \ + wechaty-puppet-padlocal@"$NPM_TAG" \ + wechaty-puppet-service@"$NPM_TAG" \ + +# +# CommonJS +# +npx tsc \ + --target esnext \ + --module CommonJS \ + \ + --moduleResolution node \ + --esModuleInterop \ + --lib esnext \ + --noEmitOnError \ + --noImplicitAny \ + --skipLibCheck \ + smoke-testing.ts + +echo +echo "CommonJS: pack testing..." +node smoke-testing.js + +# +# ES Modules +# +npx pkg-jq -i '.type="module"' + +npx tsc \ + --target esnext \ + --module esnext \ + \ + --moduleResolution node \ + --esModuleInterop \ + --lib esnext \ + --noEmitOnError \ + --noImplicitAny \ + --skipLibCheck \ + smoke-testing.ts + +echo +echo "ES Module: pack testing..." +node smoke-testing.js diff --git a/scripts/npm-set-latest.sh b/scripts/npm-set-latest.sh new file mode 100755 index 0000000..0bccff2 --- /dev/null +++ b/scripts/npm-set-latest.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# +# Author: Huan LI +# https://github.com/zixia +# License: Apache-2.0 +# + +[ -z "$1" ] && { + echo + echo "Usage: npm-set-latest.sh " + echo + exit 1 +} + +npm dist-tag add "wechaty@$1" latest diff --git a/scripts/package-publish-config-tag.sh b/scripts/package-publish-config-tag.sh new file mode 100755 index 0000000..19e6d21 --- /dev/null +++ b/scripts/package-publish-config-tag.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +VERSION=$(npx pkg-jq -r .version) + +if npx --package @chatie/semver semver-is-prod "$VERSION"; then + npx pkg-jq -i '.publishConfig.tag="latest"' + echo "production release: publicConfig.tag set to latest." +else + npx pkg-jq -i '.publishConfig.tag="next"' + echo 'development release: publicConfig.tag set to next.' +fi + diff --git a/scripts/retry-unit-tests.ts b/scripts/retry-unit-tests.ts new file mode 100644 index 0000000..7098392 --- /dev/null +++ b/scripts/retry-unit-tests.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { spawn } from 'child_process' + +const MAX_RETRY_NUM = 3 + +async function main (): Promise { + console.info('Safe Test: starting...') + + let round = 0 + let succ = false + do { + console.info(`Safe Test: running for round #${round}`) + succ = await unitTest() + if (succ) { // success! + console.info(`Safe Test: successed at round #${round}!`) + return 0 + } + } while (round++ < MAX_RETRY_NUM) + return 1 // fail finally :( +} + +async function unitTest () { + const child = spawn( + 'npm', + [ + 'run', + 'test:unit', + ], + { + shell: true, // https://stackoverflow.com/a/39682805/1123955 + stdio: 'inherit', + }, + ) + return new Promise((resolve, reject) => { + child.once('exit', code => + code === 0 ? resolve(true) : resolve(false), + ) + child.once('error', reject) + }) +} + +main() + .then(process.exit) + .catch(e => { + console.error(e) + process.exit(1) + }) diff --git a/scripts/sort-contributiveness.ts b/scripts/sort-contributiveness.ts new file mode 100644 index 0000000..dd5574d --- /dev/null +++ b/scripts/sort-contributiveness.ts @@ -0,0 +1,133 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import readline from 'readline' + +const contributeMap: { + [contributor: string]: string[], +} = {} + +function parseLine (line: string): string[] | null { + // [\#264](https://github.com/wechaty/wechaty/pull/264) ([lijiarui](https://github.com/lijiarui)) + // const regex = /(\[\\#\d+\]\([^\)]+\))\s+(\(\[[^]]+\]\([^)]+\)))/i + const regex = /(\[\\#\d+\])(\([^)]+\))\s+\((\[[^\]]+\]\([^)]+\))/ + const matches = regex.exec(line) + if (!matches) { + return null + } + // console.info('match!') + // console.info(matches[1]) // [\#264] + // console.info(matches[2]) // (https://github.com/wechaty/wechaty/pull/264) + // console.info(matches[3]) // ([lijiarui](https://github.com/lijiarui) + return matches +} + +function processLine (line: string): void { + const matches = parseLine(line) + if (matches) { + // console.info('match:', line) + // console.info(matches) + const link = matches[1]! + matches[2] + const contributor = matches[3]! + // console.info('link:', link) + // console.info('contributor:', contributor) + if (!(contributor in contributeMap)) { + contributeMap[contributor] = [] + } + contributeMap[contributor]!.push(link) + // console.info(contributiveness) + } else { + console.error('NO match:', line) + } +} + +function outputContributorMd () { + const MIN_MAINTAINER_COMMIT_NUM = 2 + function isMaintainer (committer: string): boolean { + return contributeMap[committer]!.length >= MIN_MAINTAINER_COMMIT_NUM + } + + const activeContributorList = Object + .keys(contributeMap) + .filter(isMaintainer) + .sort(desc) + + function desc (committerA: string, committerB: string): number { + return contributeMap[committerB]!.length - contributeMap[committerA]!.length + } + + console.info([ + '', + '# CHANGELOG', + '', + '## WECHATY CONTRIBUTORS', + '### Active Contributors', + '', + ].join('\n')) + + for (const contributor of activeContributorList) { + console.info(`1. @${contributor}: ${contributeMap[contributor]!.join(' ')}`) + } + + console.info([ + '', + '### Contributors', + '', + ].join('\n')) + + const SKIP_NAME_LIST = [ + 'snyk-bot', + 'gitter-badger', + ] + const SKIP_REGEX = new RegExp(SKIP_NAME_LIST.join('|'), 'i') + for (const contributor of Object.keys(contributeMap).sort(desc)) { + if (SKIP_REGEX.test(contributor)) { + continue + } + if (!activeContributorList.includes(contributor)) { + console.info(`1. @${contributor}: ${contributeMap[contributor]!.join(' ')}`) + } + } + console.info() + +} + +async function main () { + // https://stackoverflow.com/a/20087094/1123955 + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, + }) + + rl.on('line', processLine) + await new Promise(resolve => rl.on('close', resolve)) + + outputContributorMd() + + return 0 +} + +main() + .then(process.exit) + .catch(e => { + console.error(e) + process.exit(1) + }) diff --git a/scripts/typedoc.sh b/scripts/typedoc.sh new file mode 100755 index 0000000..0fdc9f9 --- /dev/null +++ b/scripts/typedoc.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +typedoc \ + --exclude \"src/*.spec.ts\" \ + --excludeExternals \ + --excludePrivate \ + --excludeProtected \ + --externalPattern ./**/lib/** \ + --mode modules \ + --module commonjs \ + --target ES6 \ + --name "Wechaty Documentation" \ + --out dist/docs/ \ + src/ diff --git a/scripts/update-license.ts b/scripts/update-license.ts new file mode 100644 index 0000000..91eced4 --- /dev/null +++ b/scripts/update-license.ts @@ -0,0 +1,179 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + createReadStream, + createWriteStream, + promises as fsPromises, + // link as linkCallback, + // unlink as unlinkCallback, +} from 'fs' +import { + Transform, + // TransformOptions, +} from 'stream' +import { promisify } from 'util' + +import globCallback from 'glob' + +const LICENSE = `/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */` + +class LicenseTransformer extends Transform { + + private lineBuf = '' + private lineNum = 0 + + private updating = false + private updated = false + + // constructor (options?: TransformOptions) { + // super(options) + // } + + override _transform (chunk: any, _: string /* encoding: string */, done: () => void) { + if (this.updated) { + this.push(chunk) + } else { + const updatedChunk = this.updateChunk(chunk) + this.push(updatedChunk) + } + + done() + } + + private updateChunk (chunk: any): string { + const buffer = this.lineBuf + chunk.toString() + this.lineBuf = '' + + if (!buffer) { + console.error('no data') + return '' + } + + const updatedLineList: string[] = [] + + buffer + .split(/\n/) + .forEach(line => { + if (this.lineNum === 0 && line.startsWith('#!')) { + updatedLineList.push(line) + } else if (this.updated) { + updatedLineList.push(line) + } else if (this.updating) { + if (/\*\//.test(line)) { + updatedLineList.push(line.replace(/.*\*\//, LICENSE)) + this.updating = false + this.updated = true + } else { + // drop the old comments + } + } else { // not updating and not updated. searching... + if (!line) { + updatedLineList.push(line) + } else if (/\s*\/\*\*/.test(line)) { // comment start + if (/\*\//.test(line)) { // comment end at the same line with start + updatedLineList.push(line.replace(/\/\*\*.*\*\//, LICENSE)) + this.updated = true + } else { + this.updating = true + } + } else { // not a comment. INSERT here + updatedLineList.push(LICENSE) + updatedLineList.push(line) + this.updated = true + } + } + + this.lineBuf = line + this.lineNum++ + }) + + return updatedLineList.join('\n') + } + + override _flush (done: () => void) { + if (this.lineBuf) { + this.push(this.lineBuf) + this.lineBuf = '' + } + done() + } + +} + +async function updateLicense (file: string): Promise { + const tmpFile = file + `.${process.pid}.tmp` + const readStream = createReadStream(file) + const writeStream = createWriteStream(tmpFile) + const tranStream = new LicenseTransformer() + + console.info(`Updating LICENSE for file ${file}...`) + await new Promise((resolve, reject) => { + readStream + .pipe(tranStream) + .pipe(writeStream) + .on('close', resolve) + .on('error', reject) + }) + // await promisify(unlinkCallback) + await fsPromises.unlink(file) + // await promisify(linkCallback)(tmpFile, file) + await fsPromises.link(tmpFile, file) + // await promisify(unlinkCallback)(tmpFile) + await fsPromises.unlink(tmpFile) +} + +async function glob (pattern: string): Promise { + return promisify(globCallback as any)(pattern) +} + +async function main (): Promise { + const pattern = '{bin/**/*.ts,examples/**/*.{js,ts},scripts/**/*.{ts,js},src/**/*.{ts,js},tests/**/*.ts}' + // const pattern = 't.ts' + const srcFileList = await glob(pattern) + const promiseList = srcFileList.map(updateLicense) + await Promise.all(promiseList) + return 0 +} + +main() + .then(process.exit) + .catch(e => { + console.error(e) + process.exit(1) + }) diff --git a/shippable.yml b/shippable.yml new file mode 100644 index 0000000..063582c --- /dev/null +++ b/shippable.yml @@ -0,0 +1,50 @@ +# Language setting http://docs.shippable.com/ci/languages/node/ + +language: python + +python: + - 3.6 + +# use this to control what branches get built. +# http://docs.shippable.com/ci/advancedOptions/branches/ +branches: + only: + - master + - v*.* + +env: + global: + - secure: x0+gSTZx9QXVhhnEu+7iRXBEE0O/nGTXAvSJVgc+wx/qPocyB2CQqAf7+UTq3jGsrcdZYKGe8MCgxFOmT2abcv8TQn02LT9LQVsSBo9I72U4tisJcpcbFad4XkjwUXHOMZfndubGgy58GfDTNl/x1v7DB/Olhwu6nlJRYnedj22l8kdEKBpabMmrnogrWesoKKpYbkhR9Vi3XHeZ3FA5hu14wbhq8A2jpcGqh35bDpViAL7f447nPdn20SAFGOrqF3yC/Ws8keG0ChQ2IhQ4cx/ucpGmswUw+ZLJEUCQL0ZlUgkXAAUVeAUAMMywE+yWYLnfDRqnVshRdb6+lHz6VA== + +build: + pre_ci_boot: + options: "-v /root/src:/root/src" + + # http://docs.shippable.com/ci/shippableyml/#ci + ci: + - curl -sSL -o /tmp/bats_v0.4.0.tar.gz https://github.com/sstephenson/bats/archive/v0.4.0.tar.gz + - tar -xf /tmp/bats_v0.4.0.tar.gz + - sudo bats-0.4.0/install.sh /usr/local + - pwd + - ls -l + - docker run -v "$(pwd)":/mnt nlknguyen/alpine-shellcheck bin/*.sh + - ./scripts/docker.sh build + - ./scripts/docker.sh test + on_success: + - echo $BRANCH + - echo $IS_PULL_REQUEST + - if [ "$BRANCH" == "master" ] && [ "$IS_PULL_REQUEST" == false ]; then curl -X POST -d '{"from":"shippable"}' "$DOCKER_REBUILD_URL"; fi + + +# Integrations are used to connect external resources to CI +# http://docs.shippable.com/integrations/overview/ +integrations: + # http://docs.shippable.com/ci/shippableyml/#notifications + notifications: + # turning of email for PR builds, get notified only on failure and change in status + # http://docs.shippable.com/integrations/notifications/email/ + - integrationName: email + type: email + on_success: change + on_failure: change + on_pull_request: never diff --git a/src/cli/friday.ts b/src/cli/friday.ts new file mode 100644 index 0000000..5cfaafb --- /dev/null +++ b/src/cli/friday.ts @@ -0,0 +1,20 @@ +/* eslint-disable sort-keys */ +import { + command, +} from 'cmd-ts' + +async function handler (_args: any) { + console.info('this command can help you contact Friday BOT and Talk to him.') + console.info('Not implemented yet, please stay tuned and contribute is welcome!') + console.info('GitHub repo(source code): https://github.com/wechaty/wechaty/blob/main/src/cli/friday-bot.ts') +} + +const friday = command({ + name: 'friday', + description: 'Contact Friday BOT and talk to him!', + args: { + }, + handler, +}) + +export { friday } diff --git a/src/cli/gateway.ts b/src/cli/gateway.ts new file mode 100644 index 0000000..402b09c --- /dev/null +++ b/src/cli/gateway.ts @@ -0,0 +1,190 @@ +/* eslint-disable sort-keys */ +import { + command, + option, + optional, + string, + number, +} from 'cmd-ts' + +import { + log, +} from 'wechaty-puppet' +import { WechatyToken } from 'wechaty-token' +import getPort from 'get-port' + +import { + VERSION, +} from '../config.js' +import { + IoClient, + IoClientOptions, +} from '../io-client.js' +import { WechatyBuilder } from '../wechaty-builder.js' +import { + isPuppetModuleName, + OFFICIAL_PUPPET_DEPENDENCIES, + OFFICIAL_PUPPET_DEFAULT, +} from '../puppet-config.js' + +async function onError ( + this : IoClient, + e : Error, +) { + log.error('Gateway', 'Error: %s', e) + await this.quit() + process.exit(-1) +} + +const gateway = command({ + name: 'gateway', + description: 'Publish your DIY token as a Wechaty Puppet Service. (you can generate a token by running `wechaty token generate`)', + args: { + token: option({ + description: 'Specify a TOKEN for your DIY Wechaty Puppet Service (WPS). It is the standard token service for TypeScript and Polyglot Wechaty (like Python, Rust, Go, etc.). If it not specified, it will be generated automatically.', + long: 'token', + short: 't', + type: optional(string), + }), + port: option({ + description: 'Specify a Port for your DIY Wechaty Puppet Service (WPS). If it not specified, it will select a free port automatically. This port MUST be public accessiable via the internet.', + long: 'port', + short: 'p', + type: optional(number), + }), + puppet: option({ + description: 'Specify a Wechaty Puppet Provider (WPP). If it not specified, it will use the environment variable `WECHATY_PUPPET`.', + long: 'puppet', + short: 'P', + type: optional(string), + }), + puppetToken: option({ + description: 'Specify the WEchaty Puppet Provider TOKEN. If it not specified, it will use the environment variable `WECHATY_PUPPET_XXX_TOKEN` (`XXX` is your WPP name).', + long: 'puppet-token', + short: 'T', + type: optional(string), + }), + }, + handler: async args => { + log.info('Gateway', 'Wechaty version %s', VERSION) + + let { + token, + port, + puppet, + puppetToken, + } = args + + /** + * Token + */ + if (!token) { + log.info('Gateway', 'no `--token` specified, generating random Wechaty Puppet Service TOKEN ...') + token = WechatyToken.generate('insecure') + } + log.info('Gateway', 'Wechaty Puppet Service TOKEN: %s', token) + + /** + * Puppet Service Server Port + */ + if (!port) { + log.info('Gateway', 'no `--port` specified, selecting random free Wechaty Puppet Service Server Port ...') + port = await getPort({ + port: 8788, + }) + + } + log.info('Gateway', 'Wechaty Puppet Service Server Port: %d', port) + + /** + * Puppet Provider + */ + if (!puppet) { + log.info('Gateway', 'no `--puppet` specified, reading from environment variable `WECHATY_PUPPET` for Wechaty Puppet Provider ...') + puppet = process.env['WECHATY_PUPPET'] + if (!puppet) { + log.info('Gateway', `environment variable "WECHATY_PUPPET" is not available. Using system default puppet "${OFFICIAL_PUPPET_DEFAULT}"`) + puppet = OFFICIAL_PUPPET_DEFAULT + } + } + if (!isPuppetModuleName(puppet)) { + console.error(`The puppet name "${puppet}" is not a valid puppet name.`) + console.error(`Valid puppet names are: ${Object.keys(OFFICIAL_PUPPET_DEPENDENCIES).join(', ')}`) + return 1 + } + log.info('Gateway', 'Wechaty Puppet Provider: %s', puppet) + + /** + * Puppet Token + */ + if (!puppetToken) { + log.info('Gateway', 'no `--puppet-token` specified, reading from environment variable `WECHATY_PUPPET_TOKEN` for your Wechaty Puppet Provider ...') + puppetToken = process.env['WECHATY_PUPPET_TOKEN'] + if (!puppetToken) { + log.info('Gateway', 'WECHATY_PUPPET_TOKEN environment variable not available. Token disabled.') + } + } + log.info('Gateway', 'Wechaty Puppet Provider Token: %s', puppetToken ? `"${puppetToken}"` : 'disabled') + + const wechaty = WechatyBuilder.build({ + name: 'Gateway', + puppet, + puppetOptions: { + token: puppetToken, + }, + }) + + const options: IoClientOptions = { + port, + token, + wechaty, + } + + const client = new IoClient(options) + + client.start() + .catch(onError.bind(client)) + + const wechatyToken = new WechatyToken(token) + let address + while (true) { + log.info('Gateway', 'Registering Wechaty Puppet Token Gateway Service Discovery ...') + await new Promise(resolve => setTimeout(resolve, 1000)) + address = await wechatyToken.discover() + if (address) { + log.info('Gateway', 'Registering Wechaty Puppet Token Gateway Service Discovery ... success') + break + } + log.info('Gateway', 'Registering Wechaty Puppet Token Gateway Service Discovery ... timeout') + } + + console.info(` + + Your have successfully ran Wechaty Token Gateway with the below settings: + 1. token: ${token} + 2. port: ${port} + 3. puppet: ${puppet} + 4. puppetToken: ${puppetToken || ''} + + The Wechaty Puppet Token Gateway Service Address is ${address.host}:${address.port}, please make sure it can be public accessed via the internet. + + You can save the below command to restore the token gateway in future: + + $ wechaty gateway --token ${token} --port ${port} --puppet ${puppet} --puppet-token ${puppetToken || ''} + + You can set the below environment variables for your Wechaty program to use the DIY token provided by this gateway: + __________________________________________________________________________ + $ export WECHATY_PUPPET=wechaty-puppet-service + $ export WECHATY_PUPPET_SERVICE_TOKEN=${token} + $ npm start + + Please file an issue if you found any bug or have feature request at https://github.com/wechaty/wechaty/issues + Join the Wechaty Community Gitter Channel at https://gitter.im/wechaty/wechaty if you have any questions. + + `) + + return 0 + }, +}) + +export { gateway } diff --git a/src/cli/mod.ts b/src/cli/mod.ts new file mode 100644 index 0000000..8773d15 --- /dev/null +++ b/src/cli/mod.ts @@ -0,0 +1,5 @@ +export { friday } from './friday.js' +export { gateway } from './gateway.js' +export { provider } from './provider.js' +export { service } from './service.js' +export { token } from './token.js' diff --git a/src/cli/provider.ts b/src/cli/provider.ts new file mode 100644 index 0000000..4d573b1 --- /dev/null +++ b/src/cli/provider.ts @@ -0,0 +1,20 @@ +/* eslint-disable sort-keys */ +import { + command, +} from 'cmd-ts' + +async function handler (_args: any) { + console.info('this command can help you manage Wechaty Puppet Provider (WPP).') + console.info('Not implemented yet, please stay tuned and contribute is welcome!') + console.info('GitHub repo(source code): https://github.com/wechaty/wechaty/blob/main/src/cli/puppet-provider.ts') +} + +const provider = command({ + name: 'provider', + description: 'Manage Wechaty Puppet Provider (WPP)', + args: { + }, + handler, +}) + +export { provider } diff --git a/src/cli/service.ts b/src/cli/service.ts new file mode 100644 index 0000000..659db74 --- /dev/null +++ b/src/cli/service.ts @@ -0,0 +1,20 @@ +/* eslint-disable sort-keys */ +import { + command, +} from 'cmd-ts' + +async function handler (_args: any) { + console.info('this command can help you manage Wechaty Puppet Servivce (WPS).') + console.info('Not implemented yet, please stay tuned and contribute is welcome!') + console.info('GitHub repo(source code): https://github.com/wechaty/wechaty/blob/main/src/cli/puppet-service.ts') +} + +const service = command({ + name: 'service', + description: 'Manage Wechaty Puppet Service (WPS)', + args: { + }, + handler, +}) + +export { service } diff --git a/src/cli/token.ts b/src/cli/token.ts new file mode 100644 index 0000000..3c0c43d --- /dev/null +++ b/src/cli/token.ts @@ -0,0 +1,19 @@ +/* eslint-disable sort-keys */ +import { + subcommands, +} from 'cmd-ts' + +import { cmds } from 'wechaty-token' + +import { + VERSION, +} from '../config.js' + +const token = subcommands({ + name: 'Token', + description: 'Generate & Discover TOKEN for Wechaty', + version: VERSION, + cmds, +}) + +export { token } diff --git a/src/config.spec.ts b/src/config.spec.ts new file mode 100755 index 0000000..ac68249 --- /dev/null +++ b/src/config.spec.ts @@ -0,0 +1,97 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { test } from 'tstest' + +import { config } from './config.js' +// import { Puppet } from './puppet' + +test('important variables', async t => { + // t.ok('puppet' in config, 'should exist `puppet` in Config') + t.ok('apihost' in config, 'should exist `apihost` in Config') + t.ok('token' in config, 'should exist `token` in Config') + + // t.ok(config.default.DEFAULT_PUPPET , 'should export DEFAULT_PUPPET') + // t.ok(config.default.DEFAULT_PROFILE , 'should export DEFAULT_PROFILE') + t.ok(config.default.DEFAULT_PROTOCOL, 'should export DEFAULT_PROTOCOL') + t.ok(config.default.DEFAULT_APIHOST, 'should export DEFAULT_APIHOST') +}) + +test('validApiHost()', async t => { + const OK_APIHOSTS = [ + 'api.chatie.io', + 'chatie.io:8080', + ] + const ERR_APIHOSTS = [ + 'https://api.chatie.io', + 'chatie.io/', + ] + OK_APIHOSTS.forEach(apihost => { + t.doesNotThrow(() => { + config.validApiHost(apihost) + }) + }, 'should not row for right apihost') + ERR_APIHOSTS.forEach(apihost => { + t.throws(() => { + config.validApiHost(apihost) + }) + }, 'should throw for error apihost') + +}) + +// test('puppetInstance()', async t => { +// // BUG Compitable with Win32 CI +// // global instance infected across unit tests... :( +// const bak = config.puppetInstance() + +// config.puppetInstance(null) +// t.throws(() => { +// config.puppetInstance() +// }, Error, 'should throw when not initialized') +// config.puppetInstance(bak) + +// const EXPECTED: Puppet = {userId: 'test'} as any +// const mockPuppet = EXPECTED + +// config.puppetInstance(mockPuppet) +// const instance = config.puppetInstance() +// t.same(instance, EXPECTED, 'should equal with initialized data') + +// config.puppetInstance(null) +// t.throws(() => { +// config.puppetInstance() +// }, Error, 'should throw after set to null') + +// config.puppetInstance(bak) +// }) + +test('systemPuppetName ()', async t => { + const WECHATY_PUPPET_ORIG = process.env['WECHATY_PUPPET'] + + delete process.env['WECHATY_PUPPET'] + t.equal(config.systemPuppetName(), 'wechaty-puppet-wechat4u', 'should get wechaty-puppet-wechat4u as default puppet name') + + process.env['WECHATY_PUPPET'] = 'wechaty-puppet-mock' + t.equal(config.systemPuppetName(), 'wechaty-puppet-mock', 'should get puppet name from process.env') + + // restore the original value + process.env['WECHATY_PUPPET'] = WECHATY_PUPPET_ORIG +}) diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..61c6bdd --- /dev/null +++ b/src/config.ts @@ -0,0 +1,165 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +/// + +import { + log, +} from 'wechaty-puppet' +import { FileBox } from 'file-box' +import type { + FileBoxInterface, +} from 'file-box' +import type { + PackageJson, +} from 'type-fest' + +import { + OfficialPuppetNpmName, + OFFICIAL_PUPPET_DEFAULT, +} from './puppet-config.js' +import { + packageJson, + GIT_COMMIT_HASH, +} from './package-json.js' + +type PackageJsonWechaty = PackageJson & { + wechaty: { + DEFAULT_PORT: number + DEFAULT_PROTOCOL: string + DEFAULT_APIHOST: string + } +} + +const VERSION = packageJson.version || '0.0.0' + +/** + * to handle unhandled exceptions + */ +if (log.level() === 'verbose' || log.level() === 'silly') { + log.info('Config', 'registering process.on("unhandledRejection") for development/debug') + + /** + * Refer to https://nodejs.org/api/process.html#process_event_unhandledrejection + * the reason is in type: Error | any + */ + process.on('unhandledRejection', (reason: Error | any, promise) => { + log.error('Config', '###########################') + log.error('Config', 'Wechaty unhandledRejection: %s %s', reason.stack || reason, promise) + log.error('Config', '###########################') + promise.catch(err => { + log.error('Config', 'process.on(unhandledRejection) promise.catch(%s)', err.message) + console.error('Config', err) // I don't know if log.error has similar full trace print support like console.error + }) + }) + + process.on('uncaughtException', function (error) { + const origin = arguments[1] // to compatible with node 12 or below version typings + + log.error('Config', '###########################') + log.error('Config', 'Wechaty uncaughtException: %s %s', error.stack, origin) + log.error('Config', '###########################') + }) +} + +export interface DefaultSetting { + DEFAULT_PORT : number, + DEFAULT_APIHOST : string, + DEFAULT_PROTOCOL : string, +} + +const DEFAULT_SETTING = packageJson['wechaty'] as DefaultSetting + +export class Config { + + default = DEFAULT_SETTING + + apihost = process.env['WECHATY_APIHOST'] || DEFAULT_SETTING['DEFAULT_APIHOST'] + + serviceIp = process.env['WECHATY_PUPPET_SERVICE_IP'] || '' + + systemPuppetName (): OfficialPuppetNpmName { + return ( + process.env['WECHATY_PUPPET'] || OFFICIAL_PUPPET_DEFAULT + ).toLowerCase() as OfficialPuppetNpmName + } + + name = process.env['WECHATY_NAME'] + + // DO NOT set DEFAULT, because sometimes user do not want to connect to io cloud service + token = process.env['WECHATY_TOKEN'] + + debug = !!(process.env['WECHATY_DEBUG']) + + httpPort = process.env['PORT'] + || process.env['WECHATY_PORT'] + || DEFAULT_SETTING['DEFAULT_PORT'] + + docker = !!(process.env['WECHATY_DOCKER']) + + constructor () { + log.verbose('Config', 'constructor()') + this.validApiHost(this.apihost) + } + + validApiHost (apihost: string): boolean { + if (/^[a-zA-Z0-9.\-_]+:?[0-9]*$/.test(apihost)) { + return true + } + throw new Error('validApiHost() fail for ' + apihost) + } + +} + +export const CHATIE_OFFICIAL_ACCOUNT_ID = 'gh_051c89260e5d' + +export function qrCodeForChatie (): FileBoxInterface { + const CHATIE_OFFICIAL_ACCOUNT_QRCODE = 'http://weixin.qq.com/r/qymXj7DEO_1ErfTs93y5' + return FileBox.fromQRCode(CHATIE_OFFICIAL_ACCOUNT_QRCODE) +} + +// http://jkorpela.fi/chars/spaces.html +// String.fromCharCode(8197) +export const FOUR_PER_EM_SPACE = String.fromCharCode(0x2005) +// mobile: \u2005, PCใ€mac: \u0020 +export const AT_SEPARATOR_REGEX = /[\u2005\u0020]/ + +export function qrcodeValueToImageUrl (qrcodeValue: string): string { + return [ + 'https://wechaty.js.org/qrcode/', + encodeURIComponent(qrcodeValue), + ].join('') +} + +export function isProduction (): boolean { + return process.env['NODE_ENV'] === 'production' + || process.env['NODE_ENV'] === 'prod' +} + +const config = new Config() + +export type { + PackageJsonWechaty, +} +export { + log, + config, + GIT_COMMIT_HASH, + VERSION, +} diff --git a/src/doctor.ts b/src/doctor.ts new file mode 100644 index 0000000..7a13be1 --- /dev/null +++ b/src/doctor.ts @@ -0,0 +1,92 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + createServer, + Socket, +} from 'net' +import { spawnSync as spawn } from 'child_process' + +import { + log, +} from 'wechaty-puppet' + +export class Doctor { + + constructor () { + log.verbose('Doctor', 'constructor()') + } + + public chromedriverVersion (): string { + let version: string + try { + const cmd = spawn('chromedriver', [ '--version' ]) + version = String(cmd.error) || cmd.stdout.toString() || cmd.stderr.toString() + } catch (e) { + version = (e as Error).message + } + return version + } + + /** + * https://gist.github.com/tedmiston/5935757 + */ + public testTcp (): Promise { + log.verbose('Doctor', 'testTcp()') + + return new Promise((resolve, reject) => { + /** + * Server + */ + const server = createServer(socket => socket.pipe(socket)) + /** + * Promise Reject + */ + server.on('error', reject) + server.on('close', () => log.silly('Doctor', 'testTcp() server closed')) + + server.listen(8788, 'localhost', () => { + /** + * Client + */ + const client = new Socket() + client.connect(8788, 'localhost', () => { + log.silly('Doctor', 'testTcp() client connected') + client.write('ding') + }) + + client.on('data', () => { + /** + * Promise Resolve + */ + resolve(true) + + client.destroy() // kill client after server's response + }) + /** + * Promise Reject + */ + client.on('error', reject) + + client.on('close', () => server.close()) + }) + }) + } + +} diff --git a/src/helper-functions/open-graph.ts b/src/helper-functions/open-graph.ts new file mode 100644 index 0000000..6062069 --- /dev/null +++ b/src/helper-functions/open-graph.ts @@ -0,0 +1,34 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import og from 'open-graph' + +export async function openGraph (url: string): Promise { + return new Promise((resolve, reject) => { + og(url, (err, meta) => { + if (err) { + reject(err) + } else if (!meta) { + reject(new Error('meta is undefined')) + } else { + resolve(meta) + } + }) + }) +} diff --git a/src/helper-functions/stringify-filter.ts b/src/helper-functions/stringify-filter.ts new file mode 100644 index 0000000..a08b640 --- /dev/null +++ b/src/helper-functions/stringify-filter.ts @@ -0,0 +1,13 @@ +// https://stackoverflow.com/a/38251445/1123955 + +function stringifyFilter (_key: string, value: any) { + if (value instanceof RegExp) { + return value.toString() + } + + return value +} + +export { + stringifyFilter, +} diff --git a/src/io-client.ts b/src/io-client.ts new file mode 100644 index 0000000..252bb70 --- /dev/null +++ b/src/io-client.ts @@ -0,0 +1,276 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { StateSwitch } from 'state-switch' +import { + PuppetServer, + PuppetServerOptions, +} from 'wechaty-puppet-service' + +import type { MessageInterface } from './user-modules/mod.js' +import type { WechatyInterface } from './wechaty/mod.js' + +import { + log, +} from './config.js' +import { Io } from './io.js' + +export interface IoClientOptions { + token : string, + wechaty : WechatyInterface, + port?: number +} + +const DEFAULT_IO_CLIENT_OPTIONS: Partial = { + port: 8788, +} + +export class IoClient { + + /** + * Huan(20161026): keep io `null-able` or not? + * Huan(202002): make it optional. + */ + private io?: Io + private puppetServer?: PuppetServer + + private state: StateSwitch + + protected options: Required + + constructor ( + options: IoClientOptions, + ) { + log.verbose('IoClient', 'constructor({%s})', + Object.keys(options) + .map(key => { + return `${key}:${(options as any)[key]}` + }) + .reduce((acc, cur) => `${acc}, ${cur}`), + ) + + const normalizedOptions = { + ...DEFAULT_IO_CLIENT_OPTIONS, + ...options, + } as Required + + this.options = normalizedOptions + + this.state = new StateSwitch('IoClient', { log }) + } + + private async startPuppetServer () { + log.verbose('IoClient', 'startPuppetServer()') + + if (this.puppetServer) { + throw new Error('puppet server exists') + } + + const options: PuppetServerOptions = { + endpoint : '0.0.0.0:' + this.options.port, + /** + * Huan(202110): FIXME: remove the any + * by updating the puppet-service server code + * to use PuppetInterface + */ + puppet : this.options.wechaty.puppet as any, + token : this.options.token, + } + this.puppetServer = new PuppetServer(options) + await this.puppetServer.start() + } + + private async stopPuppetServer () { + log.verbose('IoClient', 'stopPuppetServer()') + + if (!this.puppetServer) { + log.error('IoClient', 'stopPuppetServer() this.puppetServer is `undefined`') + return + } + + await this.puppetServer.stop() + this.puppetServer = undefined + } + + public async start (): Promise { + log.verbose('IoClient', 'start()') + + if (this.state.active()) { + log.warn('IoClient', 'start() with a on state, wait and return') + await this.state.stable('active') + return + } + + this.state.active('pending') + + try { + await this.hookWechaty(this.options.wechaty) + + await this.startIo() + + await this.options.wechaty.start() + + await this.startPuppetServer() + + this.state.active(true) + + } catch (e) { + log.error('IoClient', 'start() exception: %s', (e as Error).message) + this.state.inactive(true) + throw e + } + } + + private async hookWechaty (wechaty: WechatyInterface): Promise { + log.verbose('IoClient', 'hookWechaty()') + + if (this.state.inactive()) { + const e = new Error('state.off() is true, skipped') + log.warn('IoClient', 'initWechaty() %s', e.message) + throw e + } + + wechaty + .on('login', user => log.info('IoClient', `${user.name()} logged in`)) + .on('logout', user => log.info('IoClient', `${user.name()} logged out`)) + .on('message', msg => this.onMessage(msg)) + .on('scan', (url, code) => { + log.info('IoClient', [ + `[${code}] ${url}`, + `Online QR Code Image: https://wechaty.js.org/qrcode/${encodeURIComponent(url)}`, + ].join('\n')) + }) + } + + private async startIo (): Promise { + log.verbose('IoClient', 'startIo() with token %s', this.options.token) + + if (this.state.inactive()) { + const e = new Error('startIo() state.off() is true, skipped') + log.warn('IoClient', e.message) + throw e + } + + if (this.io) { + throw new Error('io exists') + } + + this.io = new Io({ + servicePort : this.options.port, + token : this.options.token, + wechaty : this.options.wechaty, + }) + + try { + await this.io.start() + } catch (e) { + log.verbose('IoClient', 'startIo() init fail: %s', (e as Error).message) + throw e + } + } + + private async stopIo () { + log.verbose('IoClient', 'stopIo()') + + if (!this.io) { + log.warn('IoClient', 'stopIo() io does not exist') + return + } + + await this.io.stop() + this.io = undefined + } + + private async onMessage (msg: MessageInterface) { + log.verbose('IoClient', 'onMessage(%s)', msg) + + // const from = m.from() + // const to = m.to() + // const content = m.toString() + // const room = m.room() + + // log.info('Bot', '%s<%s>:%s' + // , (room ? '['+room.topic()+']' : '') + // , from.name() + // , m.toStringDigest() + // ) + + // if (/^wechaty|chatie|botie/i.test(m.text()) && !m.self()) { + // await m.say('https://www.chatie.io') + // .then(_ => log.info('Bot', 'REPLIED to magic word "chatie"')) + // } + } + + public async stop (): Promise { + log.verbose('IoClient', 'stop()') + + this.state.inactive('pending') + + await this.stopIo() + await this.stopPuppetServer() + await this.options.wechaty.stop() + + this.state.inactive(true) + + // XXX 20161026 + // this.io = null + } + + public async restart (): Promise { + log.verbose('IoClient', 'restart()') + + try { + await this.stop() + await this.start() + } catch (e) { + log.error('IoClient', 'restart() exception %s', (e as Error).message) + throw e + } + } + + public async quit (): Promise { + log.verbose('IoClient', 'quit()') + + if (this.state.inactive() === 'pending') { + log.warn('IoClient', 'quit() with state.off() = `pending`, skipped') + throw new Error('quit() with state.off() = `pending`') + } + + this.state.inactive('pending') + + try { + // if (this.options.wechaty) { + await this.options.wechaty.stop() + // this.wechaty = null + // } else { log.warn('IoClient', 'quit() no this.wechaty') } + + if (this.io) { + await this.io.stop() + // this.io = null + } else { log.warn('IoClient', 'quit() no this.io') } + + } catch (e) { + log.error('IoClient', 'exception: %s', (e as Error).message) + throw e + } finally { + this.state.inactive(true) + } + } + +} diff --git a/src/io-peer/io-peer.spec.ts b/src/io-peer/io-peer.spec.ts new file mode 100755 index 0000000..e265a9d --- /dev/null +++ b/src/io-peer/io-peer.spec.ts @@ -0,0 +1,64 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from 'tstest' + +import jsonRpcPeer from 'json-rpc-peer' + +import { + getPeer, +} from './io-peer.js' + +test('getPeer()', async t => { + const EXPECTED_PORT = 8788 + const server = getPeer({ + serviceGrpcPort: EXPECTED_PORT, + }) + const client = new jsonRpcPeer.Peer() + /** + * FIXME: Huan(202108): remove `any` to fix the typing + */ + server.pipe(client as any).pipe(server) + + /** + * Huan(202101) Need to be fixed by new IO Bus system. + * See: https://github.com/wechaty/wechaty-puppet-service/issues/118 + */ + const port = await client.request('getHostieGrpcPort') + t.equal(port, EXPECTED_PORT, 'should get the right port') +}) + +test('exec()', async t => { + const EXPECTED_PORT = 8788 + const server = getPeer({ + serviceGrpcPort: EXPECTED_PORT, + }) + + /** + * Huan(202101) Need to be fixed by new IO Bus system. + * See: https://github.com/wechaty/wechaty-puppet-service/issues/118 + */ + const request = jsonRpcPeer.format.request(42, 'getHostieGrpcPort') + const response = await server.exec(request) as string + // console.info('response: ', response) + + const obj = jsonRpcPeer.parse(response) as jsonRpcPeer.JsonRpcPayloadResponse + t.equal(obj.result, EXPECTED_PORT, 'should get the right port from payload') +}) diff --git a/src/io-peer/io-peer.ts b/src/io-peer/io-peer.ts new file mode 100644 index 0000000..783d62f --- /dev/null +++ b/src/io-peer/io-peer.ts @@ -0,0 +1,79 @@ +/// +import jsonRpcPeer from 'json-rpc-peer' + +// // https://stackoverflow.com/a/50375286/1123955 +// type UnionToIntersection = +// (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never + +// type UnknownJsonRpcPayload = Partial> + +const isJsonRpcRequest = (payload: jsonRpcPeer.JsonRpcPayload): payload is jsonRpcPeer.JsonRpcPayloadRequest => ('method' in payload) +const isJsonRpcNotification = (payload: jsonRpcPeer.JsonRpcPayload): payload is jsonRpcPeer.JsonRpcPayloadNotification => isJsonRpcRequest(payload) && (!('id' in payload)) +const isJsonRpcResponse = (payload: jsonRpcPeer.JsonRpcPayload): payload is jsonRpcPeer.JsonRpcPayloadResponse => ('result' in payload) +const isJsonRpcError = (payload: jsonRpcPeer.JsonRpcPayload): payload is jsonRpcPeer.JsonRpcPayloadError => ('error' in payload) + +interface IoPeerOptions { + serviceGrpcPort: number, +} + +const getPeer = (options: IoPeerOptions) => { + const getServiceGrpcPort = () => options.serviceGrpcPort + + const serviceImpl = { + /** + * Huan(202101) Need to be fixed by new IO Bus system. + * See: https://github.com/wechaty/wechaty-puppet-service/issues/118 + */ + getHostieGrpcPort: getServiceGrpcPort, + } + + const onMessage = async (message: jsonRpcPeer.JsonRpcPayload): Promise => { + if (isJsonRpcRequest(message)) { + const { + // id, + method, + // params, + } = message + + if (!(method in serviceImpl)) { + console.error('serviceImpl does not contain method: ' + method) + return + } + + const serviceMethodName = method as keyof typeof serviceImpl + switch (serviceMethodName) { + /** + * Huan(202101) Need to be fixed by new IO Bus system. + * See: https://github.com/wechaty/wechaty-puppet-service/issues/118 + */ + case 'getHostieGrpcPort': + return serviceImpl[serviceMethodName]() + + default: + throw new jsonRpcPeer.MethodNotFound(serviceMethodName) + } + } else if (isJsonRpcResponse(message)) { + // NOOP: we are server + } else if (isJsonRpcNotification(message)) { + // NOOP: we are server + } else if (isJsonRpcError(message)) { + // NOOP: we are server + } else { + throw new Error('unknown json-rpc message: ' + JSON.stringify(message)) + } + console.info(JSON.stringify(message)) + } + + const ioPeer = new jsonRpcPeer.Peer(onMessage) + + return ioPeer +} + +export { + getPeer, + + isJsonRpcError, + isJsonRpcNotification, + isJsonRpcRequest, + isJsonRpcResponse, +} diff --git a/src/io-peer/json-rpc-peer.d.ts b/src/io-peer/json-rpc-peer.d.ts new file mode 100644 index 0000000..5a6a124 --- /dev/null +++ b/src/io-peer/json-rpc-peer.d.ts @@ -0,0 +1,68 @@ +/** + * Add typescript definitions #60 + * https://github.com/JsCommunity/json-rpc-peer/pull/60 + */ +declare module 'json-rpc-peer' { + + /// + /// + import { Writable } from 'stream' + import { EventEmitter } from 'events' + import { + JsonRpcPayload, + JsonRpcParamsSchema, + } from 'json-rpc-protocol' + + // export as namespace JsonRpcPeer + + export * from 'json-rpc-protocol' + + export class Peer extends EventEmitter implements Writable { + + constructor(onmessage?: (message: JsonRpcPayload, data: any) => Promise) + + public exec( + message: string | object, + data?: any + ): Promise + + /** + * Fails all pending requests. + */ + public failPendingRequests(reason?: string) + + /** + * This function should be called to send a request to the other end. + */ + public request(method: string, params?: JsonRpcParamsSchema): Promise + + /** + * This function should be called to send a notification to the other end. + */ + public notify(method: string, params?: JsonRpcParamsSchema) + + public push(chunk: any, encoding?: string) + + public pipe(writable: T): T + + // NodeJS.WritableStream + + writable: boolean + write( + buffer: Uint8Array | string, + cb?: (err?: Error | null) => void + ): boolean + + write( + str: string, + encoding?: string, + cb?: (err?: Error | null) => void + ): boolean + + end(cb?: () => void): void + end(data: string | Uint8Array, cb?: () => void): void + end(str: string, encoding?: string, cb?: () => void): void + + } + +} diff --git a/src/io.spec.ts b/src/io.spec.ts new file mode 100755 index 0000000..95e1f08 --- /dev/null +++ b/src/io.spec.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from 'tstest' +import { WechatyBuilder } from './wechaty-builder.js' + +import { Io } from './io.js' + +test('Io restart without problem', async t => { + const io = new Io({ + // token must not contain any white spaces + servicePort : 8788, + token : 'mock_00000000-0000-0000-0000-000000000000', + wechaty : WechatyBuilder.build(), + }) + + try { + for (let i = 0; i < 2; i++) { + await io.start() + await io.stop() + t.pass('start/stop-ed at #' + i) + } + t.pass('start/restart successed.') + } catch (e) { + t.fail(e as any) + } +}) diff --git a/src/io.ts b/src/io.ts new file mode 100644 index 0000000..46d1037 --- /dev/null +++ b/src/io.ts @@ -0,0 +1,585 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import WebSocket from 'ws' + +import type * as PUPPET from 'wechaty-puppet' +import { StateSwitch } from 'state-switch' + +import * as jsonRpcPeer from 'json-rpc-peer' + +import type { + MessageInterface, +} from './user-modules/mod.js' +import type { WechatyInterface } from './wechaty/mod.js' + +import { + log, + config, +} from './config.js' + +import { + getPeer, + isJsonRpcRequest, +} from './io-peer/io-peer.js' + +export interface IoOptions { + wechaty : WechatyInterface, + token : string, + apihost? : string, + protocol? : string, + servicePort? : number, +} + +export const IO_EVENT_DICT = { + botie : 'tbw', + error : 'tbw', + heartbeat : 'tbw', + jsonrpc : 'JSON RPC', + login : 'tbw', + logout : 'tbw', + message : 'tbw', + raw : 'tbw', + reset : 'tbw', + scan : 'tbw', + shutdown : 'tbw', + sys : 'tbw', + update : 'tbw', +} + +type IoEventName = keyof typeof IO_EVENT_DICT + +interface IoEventScan { + name : 'scan', + payload : PUPPET.payloads.EventScan, +} + +interface IoEventJsonRpc { + name : 'jsonrpc', + payload : jsonRpcPeer.JsonRpcPayload, +} + +interface IoEventAny { + name: IoEventName, + payload: any, +} + +type IoEvent = IoEventScan | IoEventJsonRpc | IoEventAny + +/** + * https://github.com/Chatie/botie/issues/2 + * https://github.com/actions/github-script/blob/f035cea4677903b153fa754aa8c2bba66f8dc3eb/src/async-function.ts#L6 + */ +const AsyncFunction = Object.getPrototypeOf(async () => null).constructor + +// function callAsyncFunction ( +// args: U, +// source: string +// ): Promise { +// const fn = new AsyncFunction(...Object.keys(args), source) +// return fn(...Object.values(args)) +// } + +export class Io { + + protected readonly id : string + protected readonly protocol : string + protected eventBuffer : IoEvent[] = [] + protected ws : undefined | WebSocket + + protected readonly state = new StateSwitch('Io', { log }) + + protected reconnectTimer? : ReturnType + protected reconnectTimeout? : number + + protected lifeTimer? : ReturnType + + protected onMessage: undefined | Function + + protected scanPayload?: PUPPET.payloads.EventScan + + protected jsonRpc?: jsonRpcPeer.Peer + + constructor ( + protected options: IoOptions, + ) { + options.apihost = options.apihost || config.apihost + options.protocol = options.protocol || config.default.DEFAULT_PROTOCOL + + this.id = options.wechaty.id + + this.protocol = options.protocol + '|' + options.wechaty.id + '|' + config.serviceIp + '|' + options.servicePort + log.verbose('Io', 'instantiated with apihost[%s], token[%s], protocol[%s], cuid[%s]', + options.apihost, + options.token, + options.protocol, + this.id, + ) + + if (options.servicePort) { + this.jsonRpc = getPeer({ + serviceGrpcPort: this.options.servicePort!, + }) + } + + } + + public toString () { + return `Io<${this.options.token}>` + } + + private connected () { + return this.ws && this.ws.readyState === WebSocket.OPEN + } + + public async start (): Promise { + log.verbose('Io', 'start()') + + if (this.lifeTimer) { + throw new Error('lifeTimer exist') + } + + this.state.active('pending') + + try { + this.initEventHook() + + this.ws = await this.initWebSocket() + + this.options.wechaty.on('login', () => { this.scanPayload = undefined }) + this.options.wechaty.on('scan', (qrcode, status) => { + this.scanPayload = { + ...this.scanPayload, + qrcode, + status, + } + }) + + this.lifeTimer = setInterval(() => { + if (this.ws && this.connected()) { + log.silly('Io', 'start() setInterval() ws.ping()') + // TODO: check 'pong' event on ws + this.ws.ping() + } + }, 1000 * 10) + + this.state.active(true) + + } catch (e) { + log.warn('Io', 'start() exception: %s', (e as Error).message) + this.state.inactive(true) + throw e + } + } + + private initEventHook () { + log.verbose('Io', 'initEventHook()') + const wechaty = this.options.wechaty + + wechaty.on('error', error => this.send({ name: 'error', payload: error })) + wechaty.on('heartbeat', data => this.send({ name: 'heartbeat', payload: { cuid: this.id, data } })) + wechaty.on('login', user => this.send({ name: 'login', payload: wechaty.puppet.contactPayload(user.id) })) + wechaty.on('logout', user => this.send({ name: 'logout', payload: wechaty.puppet.contactPayload(user.id) })) + wechaty.on('message', message => this.ioMessage(message)) + + // FIXME: payload schema need to be defined universal + // wechaty.on('scan', (url, code) => this.send({ name: 'scan', payload: { url, code } })) + wechaty.on('scan', (qrcode, status) => this.send({ name: 'scan', payload: { qrcode, status } } as IoEventScan)) + } + + private async initWebSocket (): Promise { + log.verbose('Io', 'initWebSocket()') + // this.state.current('on', false) + + // const auth = 'Basic ' + new Buffer(this.setting.token + ':X').toString('base64') + const auth = 'Token ' + this.options.token + const headers = { Authorization: auth } + + if (!this.options.apihost) { + throw new Error('no apihost') + } + let endpoint = 'wss://' + this.options.apihost + '/v0/websocket' + + // XXX quick and dirty: use no ssl for API_HOST other than official + // FIXME: use a configurable VARIABLE for the domain name at here: + if (!/api\.chatie\.io/.test(this.options.apihost)) { + endpoint = 'ws://' + this.options.apihost + '/v0/websocket' + } + + const ws = this.ws = new WebSocket(endpoint, this.protocol, { headers }) + + ws.on('open', () => this.wsOnOpen(ws)) + ws.on('message', data => this.wsOnMessage(data)) + ws.on('error', e => this.wsOnError(e)) + ws.on('close', (code, reason) => this.wsOnClose(ws, code, reason.toString())) + + await new Promise((resolve, reject) => { + ws.once('open', resolve) + ws.once('error', reject) + ws.once('close', reject) + }) + + return ws + } + + private wsOnOpen (ws: WebSocket): void { + if (this.protocol !== ws.protocol) { + log.error('Io', 'wsOnOpen() require protocol[%s] failed', this.protocol) + // XXX deal with error? + } + log.verbose('Io', 'wsOnOpen() connected with protocol [%s]', ws.protocol) + // this.currentState('connected') + // this.state.current('on') + + // FIXME: how to keep alive??? + // ws._socket.setKeepAlive(true, 100) + + this.reconnectTimeout = undefined + + const name = 'sys' + const payload = 'Wechaty version ' + this.options.wechaty.version() + ` with CUID: ${this.id}` + + const initEvent: IoEvent = { + name, + payload, + } + this.send(initEvent).catch(console.error) + } + + private wsOnMessage (data: WebSocket.Data): void { + log.silly('Io', 'wsOnMessage() ws.on(message): %s', data) + this.wsOnMessageAsync(data).catch(console.error) + } + + private async wsOnMessageAsync (data: WebSocket.Data): Promise { + log.silly('Io', 'wsOnMessageAsync() ws.on(message): %s', data) + + // flags.binary will be set if a binary data is received. + // flags.masked will be set if the data was masked. + let payload: string + + if (Array.isArray(data)) { + payload = data[0]?.toString() ?? 'NODATA' + } else { + payload = data.toString() + } + + const ioEvent: IoEvent = { + name : 'raw', + payload : data, + } + + try { + const obj = JSON.parse(payload) + ioEvent.name = obj.name + ioEvent.payload = obj.payload + } catch (e) { + log.verbose('Io', 'on(message) recv a non IoEvent data[%s]', data) + } + + switch (ioEvent.name) { + case 'botie': + { + const payload = ioEvent.payload + + const args = payload.args + const source = payload.source + + try { + if (args[0] === 'message' && args.length === 1) { + const fn = new AsyncFunction(...args, source) + this.onMessage = fn + } else { + log.warn('Io', 'server pushed function is invalid. args: %s', JSON.stringify(args)) + } + } catch (e) { + log.warn('Io', 'server pushed function exception: %s', e) + this.options.wechaty.emitError(e) + } + } + break + + case 'reset': + log.verbose('Io', 'on(reset): %s', ioEvent.payload) + this.options.wechaty.emitError( + new Error( + 'reset by server: ' + + JSON.stringify(ioEvent.payload), + ), + ) + await this.options.wechaty.reset() + break + + case 'shutdown': + log.info('Io', 'on(shutdown): %s', ioEvent.payload) + process.exit(0) + // eslint-disable-next-line + break + + case 'update': + log.verbose('Io', 'on(update): %s', ioEvent.payload) + { + const wechaty = this.options.wechaty + if (wechaty.isLoggedIn) { + const loginEvent: IoEvent = { + name : 'login', + payload : await wechaty.puppet.contactPayload( + wechaty.puppet.currentUserId, + ), + } + await this.send(loginEvent) + } + + if (this.scanPayload) { + const scanEvent: IoEventScan = { + name: 'scan', + payload: this.scanPayload, + } + await this.send(scanEvent) + } + } + + break + + case 'sys': + // do nothing + break + + case 'logout': + log.info('Io', 'on(logout): %s', ioEvent.payload) + await this.options.wechaty.logout() + break + + case 'jsonrpc': + log.info('Io', 'on(jsonrpc): %s', ioEvent.payload) + + try { + const request = (ioEvent as IoEventJsonRpc).payload + if (!isJsonRpcRequest(request)) { + log.warn('Io', 'on(jsonrpc) payload is not a jsonrpc request: %s', JSON.stringify(request)) + return + } + + if (!this.jsonRpc) { + throw new Error('jsonRpc not initialized!') + } + + const response = await this.jsonRpc.exec(request) + if (!response) { + log.warn('Io', 'on(jsonrpc) response is undefined.') + return + } + const payload = jsonRpcPeer.parse(response) as jsonRpcPeer.JsonRpcPayloadResponse + + const jsonrpcEvent: IoEventJsonRpc = { + name: 'jsonrpc', + payload, + } + + log.verbose('Io', 'on(jsonrpc) send(%s)', response) + await this.send(jsonrpcEvent) + + } catch (e) { + log.error('Io', 'on(jsonrpc): %s', e) + } + + break + + default: + log.warn('Io', 'UNKNOWN on(%s): %s', ioEvent.name, ioEvent.payload) + break + } + } + + // FIXME: it seems the parameter `e` might be `undefined`. + // @types/ws might has bug for `ws.on('error', e => this.wsOnError(e))` + private wsOnError (e?: Error) { + log.warn('Io', 'wsOnError() error event[%s]', e && e.message) + if (!e) { + return + } + + if (!this.ws) { + log.error('Io', 'wsOnError() ws.on(error) this.ws is `undefined`', e.message) + return + } + + if (this.ws.readyState === WebSocket.CONNECTING) { + log.error('Io', 'wsOnError() ws.on(error) ws.readyState is CONNECTING: %s', e.message) + return + } + + if (this.ws.readyState === WebSocket.CLOSING) { + log.error('Io', 'wsOnError() ws.on(error) ws.readyState is CLOSING: %s', e.message) + return + } + + this.options.wechaty.emitError(e) + + // when `error`, there must have already a `close` event + // we should not call this.reconnect() again + // + // this.close() + // this.reconnect() + } + + private wsOnClose ( + ws : WebSocket, + code : number, + message : string, + ): void { + if (this.state.active()) { + log.warn('Io', 'wsOnClose() close event[%d: %s]', code, message) + ws.close() + this.reconnect() + } + } + + private reconnect () { + log.verbose('Io', 'reconnect()') + + if (this.state.inactive()) { + log.warn('Io', 'reconnect() canceled because state.target() === offline') + return + } + + if (this.connected()) { + log.warn('Io', 'reconnect() on a already connected io') + return + } + if (this.reconnectTimer) { + log.warn('Io', 'reconnect() on a already re-connecting io') + return + } + + if (!this.reconnectTimeout) { + this.reconnectTimeout = 1 + } else if (this.reconnectTimeout < 10 * 1000) { + this.reconnectTimeout *= 3 + } + + log.warn('Io', 'reconnect() will reconnect after %d s', Math.floor(this.reconnectTimeout / 1000)) + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = undefined + this.initWebSocket().catch(console.error) + }, this.reconnectTimeout)// as any as NodeJS.Timer + } + + private async send (ioEvent?: IoEvent): Promise { + if (!this.ws) { + throw new Error('no ws') + } + + const ws = this.ws + + if (ioEvent) { + log.silly('Io', 'send(%s)', JSON.stringify(ioEvent)) + this.eventBuffer.push(ioEvent) + } else { log.silly('Io', 'send()') } + + if (!this.connected()) { + log.verbose('Io', 'send() without a connected websocket, eventBuffer.length = %d', this.eventBuffer.length) + return + } + + const list: Array> = [] + while (this.eventBuffer.length) { + const data = JSON.stringify( + this.eventBuffer.shift(), + ) + const p = new Promise((resolve, reject) => ws.send( + data, + (err: undefined | Error) => { + if (err) { + reject(err) + } else { + resolve() + } + }, + )) + list.push(p) + } + + try { + await Promise.all(list) + } catch (e) { + log.error('Io', 'send() exception: %s', (e as Error).stack) + throw e + } + } + + public async stop (): Promise { + log.verbose('Io', 'stop()') + + if (!this.ws) { + throw new Error('no ws') + } + + this.state.inactive('pending') + + // try to send IoEvents in buffer + await this.send() + this.eventBuffer = [] + + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer) + this.reconnectTimer = undefined + } + + if (this.lifeTimer) { + clearInterval(this.lifeTimer) + this.lifeTimer = undefined + } + + this.ws.close() + await new Promise(resolve => { + if (this.ws) { + this.ws.once('close', resolve) + } else { + resolve() + } + }) + this.ws = undefined + + this.state.inactive(true) + } + + /** + * + * Prepare to be overwritten by server setting + * + */ + private async ioMessage (m: MessageInterface): Promise { + log.silly('Io', 'ioMessage() is a nop function before be overwritten from cloud') + if (typeof this.onMessage === 'function') { + await this.onMessage(m) + } + } + + protected async syncMessage (m: MessageInterface): Promise { + log.silly('Io', 'syncMessage(%s)', m) + + const messageEvent: IoEvent = { + name : 'message', + payload : await m.wechaty.puppet.messagePayload(m.id), + } + await this.send(messageEvent) + } + +} diff --git a/src/mods/helpers.ts b/src/mods/helpers.ts new file mode 100644 index 0000000..b8962a9 --- /dev/null +++ b/src/mods/helpers.ts @@ -0,0 +1,5 @@ +export { + messageToSayable, + payloadToSayableWechaty, + sayableToPayload, +} from '../sayable/mod.js' diff --git a/src/mods/impls.spec.ts b/src/mods/impls.spec.ts new file mode 100755 index 0000000..cfc9b91 --- /dev/null +++ b/src/mods/impls.spec.ts @@ -0,0 +1,13 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' + +import * as impls from './impls.js' + +test('Export of the Framework', async t => { + t.ok(impls.ContactImpl, 'should export Contact') + t.ok(impls.FriendshipImpl, 'should export Friendship') + t.ok(impls.MessageImpl, 'should export Message') + t.ok(impls.RoomImpl, 'should export Room') + t.ok(impls.WechatyImpl, 'should export Wechaty') +}) diff --git a/src/mods/impls.ts b/src/mods/impls.ts new file mode 100644 index 0000000..14e1986 --- /dev/null +++ b/src/mods/impls.ts @@ -0,0 +1,115 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * Huan(202111): We export all names with specific postfix to make it easier to be recongnized + * with less misunderstandings. + * + * Postfixes: + * - Interface: type, the `interface + * - Constructor: type, the `Constructor` + * - Impl: value, the `class` + * + * - no postfix: value, the `class` + */ + +export { + ContactImpl, + ContactSelfImpl, + FavoriteImpl, + FriendshipImpl, + ImageImpl, + LocationImpl, + MessageImpl, + MiniProgramImpl, + MomentImpl, + MoneyImpl, + PostImpl, + RoomImpl, + RoomInvitationImpl, + DelayImpl, + TagImpl, + UrlLinkImpl, +} from '../user-modules/mod.js' + +// export { +// ContactImpl as Contact, +// ContactSelfImpl as ContactSelf, +// FavoriteImpl as Favorite, +// FriendshipImpl as Friendship, +// ImageImpl as Image, +// LocationImpl as Location, +// MessageImpl as Message, +// MiniProgramImpl as MiniProgram, +// MomentImpl as Moment, +// MoneyImpl as Money, +// PostImpl as Post, +// RoomImpl as Room, +// RoomInvitationImpl as RoomInvitation, +// DelayImpl as Delay, +// TagImpl as Tag, +// UrlLinkImpl as UrlLink, +// } from '../user-modules/mod.js' + +export type { + ContactInterface, + ContactSelfInterface, + DelayInterface, + FavoriteInterface, + FriendshipInterface, + ImageInterface, + LocationInterface, + MessageInterface, + MiniProgramInterface, + MomentInterface, + MoneyInterface, + PostInterface, + RoomInterface, + RoomInvitationInterface, + TagInterface, + UrlLinkInterface, +} from '../user-modules/mod.js' + +export type { + ContactConstructor, + ContactSelfConstructor, + DelayConstructor, + FavoriteConstructor, + FriendshipConstructor, + ImageConstructor, + LocationConstructor, + MessageConstructor, + MiniProgramConstructor, + MomentConstructor, + MoneyConstructor, + PostConstructor, + RoomConstructor, + RoomInvitationConstructor, + TagConstructor, + UrlLinkConstructor, +} from '../user-modules/mod.js' + +export { + WechatyImpl, +} from '../wechaty/mod.js' +export type { + WechatyInterface, + WechatyConstructor, +} from '../wechaty/mod.js' diff --git a/src/mods/mod.spec.ts b/src/mods/mod.spec.ts new file mode 100755 index 0000000..375e2d9 --- /dev/null +++ b/src/mods/mod.spec.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' + +import * as WECHATY from './mod.js' + +test('Export of the Framework', async t => { + t.ok(WECHATY.impls, 'should export impls.*') + t.ok(WECHATY.types, 'should export types.*') + t.ok(WECHATY.IoClient, 'should export IoClient') + t.ok(WECHATY.log, 'should export log') +}) + +test('Config setting', async t => { + t.ok(WECHATY.config, 'should export Config') + // t.ok(config.default.DEFAULT_PUPPET , 'should has DEFAULT_PUPPET') +}) diff --git a/src/mods/mod.ts b/src/mods/mod.ts new file mode 100644 index 0000000..d0f772a --- /dev/null +++ b/src/mods/mod.ts @@ -0,0 +1,74 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * We need to put `Wechaty` at the beginning of this file for import + * because we have circular dependencies between `Puppet` & `Wechaty` + */ +export type { + WechatyOptions, + WechatyInterface as Wechaty, +} from '../wechaty/mod.js' +export { + WechatyBuilder, + type BuilderInterface, +} from '../wechaty-builder.js' + +export { + type SayableSayer, + type Sayable, +} from '../sayable/mod.js' + +export * as types from 'wechaty-puppet/types' +export * as payloads from 'wechaty-puppet/payloads' + +export * from './users.js' +export * as users from './users.js' +export * as impls from './impls.js' +export * as helpers from './helpers.js' +export { + ScanStatus, +} from 'wechaty-puppet/types' + +export { + log, + config, + qrcodeValueToImageUrl, + VERSION, +} from '../config.js' + +export type { + WechatyEventName, +} from '../schemas/mod.js' + +export type { + OfficialPuppetNpmName, + PuppetModuleName, // DEPRECATED +} from '../puppet-config.js' +export type { + WechatyPlugin, + WechatyPluginUninstaller, +} from '../plugin.js' +export type { + IoClientOptions, +} from '../io-client.js' +export { + IoClient, +} from '../io-client.js' diff --git a/src/mods/users.ts b/src/mods/users.ts new file mode 100644 index 0000000..e3d080d --- /dev/null +++ b/src/mods/users.ts @@ -0,0 +1,44 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * Huan(202111): we export all `interface` from this file, as the most convenience class name. + * + * since they are `interface`s, so they are `type`-only. + */ + +export type { + ContactInterface as Contact, + ContactSelfInterface as ContactSelf, + DelayInterface as Delay, + FavoriteInterface as Favorite, + FriendshipInterface as Friendship, + ImageInterface as Image, + LocationInterface as Location, + MessageInterface as Message, + MiniProgramInterface as MiniProgram, + MomentInterface as Moment, + MoneyInterface as Money, + PostInterface as Post, + RoomInterface as Room, + RoomInvitationInterface as RoomInvitation, + TagInterface as Tag, + UrlLinkInterface as UrlLink, +} from '../user-modules/mod.js' diff --git a/src/package-json.spec.ts b/src/package-json.spec.ts new file mode 100755 index 0000000..4299a9a --- /dev/null +++ b/src/package-json.spec.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { test } from 'tstest' + +import { packageJson } from './package-json.js' + +test('Make sure the packageJson is fresh in source code', async t => { + const json = JSON.parse(JSON.stringify(packageJson)) + /** + * Delete test data (keys), which will + * be overwrite + * by the real data before deploy + */ + delete json['wechaty'] + + const keyNum = Object.keys(json).length + t.equal(keyNum, 0, 'packageJson should be empty in source code, only updated before publish to NPM') +}) diff --git a/src/package-json.ts b/src/package-json.ts new file mode 100644 index 0000000..6df005f --- /dev/null +++ b/src/package-json.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ + +/** + * This file will be overwrite when we publish NPM module + * by scripts/generate_version.ts + */ +import type { PackageJsonWechaty } from './config' + +/** + * Huan(202108): + * The below default values is only for unit testing + */ + +export const packageJson: PackageJsonWechaty = { + wechaty: { + DEFAULT_APIHOST : 'api.chatie.io', + DEFAULT_PORT : 8080, + DEFAULT_PROTOCOL : 'io|0.0.1', + }, +} +export const GIT_COMMIT_HASH = 'GIT_COMMIT_HASH' diff --git a/src/plugin.spec.ts b/src/plugin.spec.ts new file mode 100755 index 0000000..0ed8325 --- /dev/null +++ b/src/plugin.spec.ts @@ -0,0 +1,87 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + test, + sinon, +} from 'tstest' + +import { PuppetMock } from 'wechaty-puppet-mock' + +import type { Wechaty } from './mods/mod.js' +import { WechatyBuilder } from './wechaty-builder.js' + +import { + WechatyPlugin, + isWechatyPluginUninstaller, +} from './plugin.js' + +/** + * + * Huan(202111): we are not remove plugins in the `stop()` method. + * @see https://github.com/wechaty/wechaty/issues/2282#issuecomment-966008175 + * + * TODO: make sure to not remove or remote, then remove this comment + * + */ +test('Wechaty Plugin uninstaller should be called after wechaty.stop()', async t => { + const spyPluginInstall = sinon.spy() + const spyPluginUninstall = sinon.spy() + + const bot = WechatyBuilder.build({ puppet: new PuppetMock() }) + + const plugin: WechatyPlugin = (_bot: Wechaty) => { + spyPluginInstall() + return () => { + spyPluginUninstall() + } + } + + t.ok(spyPluginInstall.notCalled, 'should be clean for install spy') + t.ok(spyPluginUninstall.notCalled, 'should be clean for uninstall spy') + + const uninstaller = bot.use(plugin) + t.ok(spyPluginInstall.calledOnce, 'should called install spy right after use() before start()') + t.ok(spyPluginUninstall.notCalled, 'should not call uninstall spy after use()') + + await bot.start() + t.ok(spyPluginInstall.calledOnce, 'should called install spy after start()') + t.ok(spyPluginUninstall.notCalled, 'should not call uninstall spy after start()') + + spyPluginInstall.resetHistory() + await bot.stop() + t.ok(spyPluginInstall.notCalled, 'should not called with stop()') + await new Promise(setImmediate) // clean the event loop + t.ok(spyPluginUninstall.notCalled, 'should not call uninstall spy after stop()') + + uninstaller() + t.ok(spyPluginUninstall.calledOnce, 'should called uninstall spy after call uninstaller()') +}) + +test('isWechatyPluginUninstaller()', async t => { + const FIXTURES = [ + [ undefined, false ], + [ () => {}, true ], + ] as const + + for (const [ uninstaller, expected ] of FIXTURES) { + t.equal(isWechatyPluginUninstaller(uninstaller), expected, `isWechatyPluginUninstaller(${uninstaller}) === ${expected}`) + } +}) diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..0394aa0 --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,19 @@ +import type { WechatyInterface } from './wechaty/mod.js' + +export type WechatyPluginUninstaller = () => void + +export type WechatyPluginReturn = void | WechatyPluginUninstaller + +export interface WechatyPlugin { + (bot: WechatyInterface): WechatyPluginReturn +} + +function isWechatyPluginUninstaller ( + pluginReturn: WechatyPluginReturn, +): pluginReturn is WechatyPluginUninstaller { + return typeof pluginReturn === 'function' +} + +export { + isWechatyPluginUninstaller, +} diff --git a/src/puppet-config.ts b/src/puppet-config.ts new file mode 100644 index 0000000..1d93415 --- /dev/null +++ b/src/puppet-config.ts @@ -0,0 +1,104 @@ +/* eslint-disable sort-keys */ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const OFFICIAL_PUPPET_DEPENDENCIES = { + /** + * The following puppets were DEPRECATED before 2020 + */ + // 'wechaty-puppet-ioscat' : '^0.5.22', // https://www.npmjs.com/package/wechaty-puppet-ioscat + // 'wechaty-puppet-padchat' : '^0.19.3', // https://www.npmjs.com/package/wechaty-puppet-padchat + // 'wechaty-puppet-padpro' : '^0.3.21', // https://www.npmjs.com/package/wechaty-puppet-padpro + + /** + * Deprecated History: + * https://github.com/wechaty/puppet-service-providers/issues/11 + */ + // Dec 2020 + // - https://github.com/wechaty/puppet-services/issues/61 + // 'wechaty-puppet-padplus' : '^0.7.30', // https://www.npmjs.com/package/wechaty-puppet-padplus + // Sep 2021: + // 'wechaty-puppet-hostie' : '*', // https://www.npmjs.com/package/wechaty-puppet-hostie + + /** + * Wechaty Internal Puppets: dependency by package.json + * + * Huan(202108): DO NOT REMOVE THE SPECIFIC VERSIONS BELOW + * All the internal puppets which have already depended in package.json + * MUST be listed below with their correct version number (ranges) + * because the `bin/puppet-install.ts` will use those version + * to install all them inside the Docker + */ + 'wechaty-puppet-service' : '>=1.19.8', // https://www.npmjs.com/package/wechaty-puppet-service + 'wechaty-puppet-mock' : '>=1.10.2', // https://www.npmjs.com/package/wechaty-puppet-mock + + /** + * WeChat Puppets + */ + 'wechaty-puppet-wechat' : '>=1.11.8', // https://www.npmjs.com/package/wechaty-puppet-wechat + 'wechaty-puppet-wechat4u' : '>=1.11.1', // https://www.npmjs.com/package/wechaty-puppet-wechat4u + 'wechaty-puppet-padlocal' : '>=1.11.13', // https://www.npmjs.com/package/wechaty-puppet-padlocal + 'wechaty-puppet-xp' : '>=1.10.2', // https://www.npmjs.com/package/wechaty-puppet-xp + 'wechaty-puppet-oicq' : '>=1.10.2', // https://www.npmjs.com/package/wechaty-puppet-oicq + 'wechaty-puppet-official-account' : '>=1.10.2', // https://www.npmjs.com/package/wechaty-puppet-official-account + + /** + * Non-WeChat External Puppets + */ + 'wechaty-puppet-gitter' : '>=1.10.1', // https://www.npmjs.com/package/wechaty-puppet-gitter + 'wechaty-puppet-lark' : '>=0.4.5', // https://www.npmjs.com/package/wechaty-puppet-lark + 'wechaty-puppet-whatsapp' : '>=1.10.4', // https://www.npmjs.com/package/wechaty-puppet-whatsapp + 'wechaty-puppet-walnut' : '>=0.1.41', // https://www.npmjs.com/package/wechaty-puppet-walnut + + /** + * Scoped puppets (private) + */ + '@juzibot/wechaty-puppet-donut' : '*', // https://www.npmjs.com/package/wechaty-puppet-donut (to be published) + '@juzibot/wechaty-puppet-wxwork' : '*', // https://www.npmjs.com/package/wechaty-puppet-wxwork (to be published) +} + +type OfficialPuppetNpmName = keyof typeof OFFICIAL_PUPPET_DEPENDENCIES + +const isPuppetModuleName = (name: string): name is OfficialPuppetNpmName => name in OFFICIAL_PUPPET_DEPENDENCIES + +/** + * Updates: + * - Huan(202004): we change default puppet from puppet-service -> puppet-wechat (with UOS support) + * - Huan(202009): use puppet service as default + * - Huan(202201): use puppet-wechat4u as default + */ +const OFFICIAL_PUPPET_DEFAULT: OfficialPuppetNpmName = 'wechaty-puppet-wechat4u' + +// i.e. @juzibot/wechaty-puppet-donut +type PuppetNpmScope = `@${string}/` | '' +type PuppetNpmName = `${PuppetNpmScope}wechaty-puppet-${string}` + +/** + * @deprecated: use `OfficialPuppetNpmName` instead. will be removed after Dec 31, 2022 + */ +type PuppetModuleName = OfficialPuppetNpmName + +export { + type OfficialPuppetNpmName, + type PuppetNpmName, + type PuppetModuleName, // DEPRECATED + OFFICIAL_PUPPET_DEPENDENCIES, + OFFICIAL_PUPPET_DEFAULT, + isPuppetModuleName, +} diff --git a/src/pure-functions/escape-regexp.ts b/src/pure-functions/escape-regexp.ts new file mode 100644 index 0000000..d7f39b0 --- /dev/null +++ b/src/pure-functions/escape-regexp.ts @@ -0,0 +1,22 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +export function escapeRegExp (text: string) { + return text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') +} diff --git a/src/pure-functions/guard-qr-code-value.ts b/src/pure-functions/guard-qr-code-value.ts new file mode 100644 index 0000000..01553c2 --- /dev/null +++ b/src/pure-functions/guard-qr-code-value.ts @@ -0,0 +1,27 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +const MAX_LEN = 7089 + +export function guardQrCodeValue (value: string): string { + if (value.length > MAX_LEN) { + throw new Error('QR Code Value is larger than the max len. Did you return the image base64 text by mistake? See: https://github.com/wechaty/wechaty/issues/1889') + } + return value +} diff --git a/src/pure-functions/is-template-string-array.spec.ts b/src/pure-functions/is-template-string-array.spec.ts new file mode 100755 index 0000000..a28e78a --- /dev/null +++ b/src/pure-functions/is-template-string-array.spec.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from 'tstest' +import { isTemplateStringArray } from './is-template-string-array.js' + +test('isTemplateStringArray', async t => { + function test ( + s: string | TemplateStringsArray, + ...varList : unknown[] + ) { + void varList + if (isTemplateStringArray(s)) { + return true + } + return false + } + + const n = 42 + const obj = {} + + t.ok(test`foo`, 'should return true for template string') + t.ok(test`bar${n}`, 'should return true for template string with one var') + t.ok(test`obj${obj}`, 'should return true for template string with one obj') + + t.notOk(test('xixi'), 'should return false for (string) call') +}) diff --git a/src/pure-functions/is-template-string-array.ts b/src/pure-functions/is-template-string-array.ts new file mode 100644 index 0000000..76fc701 --- /dev/null +++ b/src/pure-functions/is-template-string-array.ts @@ -0,0 +1,5 @@ +function isTemplateStringArray (tsa: any): tsa is TemplateStringsArray { + return Array.isArray(tsa.raw) && Array.isArray(tsa) +} + +export { isTemplateStringArray } diff --git a/src/pure-functions/mod.ts b/src/pure-functions/mod.ts new file mode 100644 index 0000000..09d5045 --- /dev/null +++ b/src/pure-functions/mod.ts @@ -0,0 +1,28 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +export { retryPolicy } from './retry-policy.js' +export { + digestEmoji, + plainText, + stripEmoji, + stripHtml, + unescapeHtml, + unifyEmoji, +} from './xml.js' diff --git a/src/pure-functions/retry-policy.ts b/src/pure-functions/retry-policy.ts new file mode 100644 index 0000000..f18a314 --- /dev/null +++ b/src/pure-functions/retry-policy.ts @@ -0,0 +1,45 @@ +import { + Policy, + RetryPolicy, +} from 'cockatiel' +import { + log, +} from 'wechaty-puppet' + +/** + * Create a retry policy that'll try whatever function we execute 3 + * times with a randomized exponential backoff. + * + * https://github.com/connor4312/cockatiel#policyretry + */ +const retryPolicy = getRetryPolicy() + +function getRetryPolicy (): RetryPolicy { + const policy = Policy + .handleAll() + .retry() + .attempts(3) + .exponential({ + /** + * ExponentialBackoff + * https://github.com/connor4312/cockatiel#exponentialbackoff + */ + initialDelay : 1000, + maxAttempts : 5, + maxDelay : 10 * 1000, + }) + + policy.onRetry(reason => log.silly('wechaty', + 'retry-policy getRetryPolicy policy.onRetry() reason: "%s"', + JSON.stringify(reason), + )) + policy.onSuccess(({ duration }) => log.silly('wechaty', + 'retry-policy getRetryPolicy policy.onSuccess(): retry call ran in %s ms', + duration, + )) + return policy +} + +export { + retryPolicy, +} diff --git a/src/pure-functions/timestamp-to-date.spec.ts b/src/pure-functions/timestamp-to-date.spec.ts new file mode 100755 index 0000000..f37103d --- /dev/null +++ b/src/pure-functions/timestamp-to-date.spec.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from 'tstest' +import { timestampToDate } from './timestamp-to-date.js' + +/** + * 1e11 + * in milliseconds: Sat Mar 03 1973 09:46:39 UTC + * in seconds: Wed Nov 16 5138 9:46:40 UTC + */ +test('timestampToDate() for dealing with seconds', async t => { + const SECONDS = 1e11 - 1 + const EXPECTED_DATE_NOW = 'Wed, 16 Nov 5138 09:46:39 GMT' + + const date = timestampToDate(SECONDS) + t.equal(date.toUTCString(), EXPECTED_DATE_NOW, 'should parse seconds to right date') +}) + +test('timestampToDate() for dealing with milliseconds', async t => { + const MILLISECONDS = 1e11 + 1 + const EXPECTED_DATE_UTC = 'Sat, 03 Mar 1973 09:46:40 GMT' + + const date = timestampToDate(MILLISECONDS) + t.equal(date.toUTCString(), EXPECTED_DATE_UTC, 'should parse milliseconds to right date') +}) diff --git a/src/pure-functions/timestamp-to-date.ts b/src/pure-functions/timestamp-to-date.ts new file mode 100644 index 0000000..afbc026 --- /dev/null +++ b/src/pure-functions/timestamp-to-date.ts @@ -0,0 +1,31 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +export function timestampToDate (timestamp: number): Date { + /** + * 1e11: + * in milliseconds: Sat Mar 03 1973 09:46:39 UTC + * in seconds: Wed Nov 16 5138 9:46:40 UTC + */ + if (timestamp < 1e11) { + timestamp *= 1000 // turn seconds to milliseconds + } + + return new Date(timestamp) +} diff --git a/src/pure-functions/xml.spec.ts b/src/pure-functions/xml.spec.ts new file mode 100755 index 0000000..9fcd2fe --- /dev/null +++ b/src/pure-functions/xml.spec.ts @@ -0,0 +1,116 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { test } from 'tstest' + +import { + digestEmoji, + plainText, + stripEmoji, + stripHtml, + unescapeHtml, + unifyEmoji, +} from './xml.js' + +test('stripHtml()', async t => { + const HTML_BEFORE_STRIP = 'OuterInner' + const HTML_AFTER_STRIP = 'OuterInner' + + const strippedHtml = stripHtml(HTML_BEFORE_STRIP) + t.equal(strippedHtml, HTML_AFTER_STRIP, 'should strip html as expected') +}) + +test('unescapeHtml()', async t => { + const HTML_BEFORE_UNESCAPE = ''|"|>|<|&' + // eslint-disable-next-line + const HTML_AFTER_UNESCAPE = `'|"|>|<|&` + + const unescapedHtml = unescapeHtml(HTML_BEFORE_UNESCAPE) + t.equal(unescapedHtml, HTML_AFTER_UNESCAPE, 'should unescape html as expected') +}) + +test('plainText()', async t => { + const PLAIN_BEFORE = '&&&' + const PLAIN_AFTER = '&&&[ๆตๆฑ—]' + + const text = plainText(PLAIN_BEFORE) + t.equal(text, PLAIN_AFTER, 'should convert plain text as expected') + +}) + +test('digestEmoji()', async t => { + const EMOJI_XML = [ + '', + '', + '', + '', + ] + const EMOJI_AFTER_DIGEST = [ + '[ๆตๆฑ—]', + '[ๅ‘ฒ็‰™]', + '๎€Ž', + '[emoji1f334]', + ] + + for (let i = 0; i < EMOJI_XML.length; i++) { + const emojiDigest = digestEmoji(EMOJI_XML[i]) + t.equal(emojiDigest, EMOJI_AFTER_DIGEST[i], 'should digest emoji string ' + i + ' as expected') + } +}) + +test('unifyEmoji()', async t => { + const ORIGNAL_XML_LIST: Array<[string[], string]> = [ + [ + [ + '', + '', + ], + '', + ], + ] + + ORIGNAL_XML_LIST.forEach(([ xmlList, expectedEmojiXml ]) => { + xmlList.forEach(xml => { + const unifiedXml = unifyEmoji(xml) + t.equal(unifiedXml, expectedEmojiXml, 'should convert the emoji xml to the expected unified xml') + }) + }) +}) + +test('stripEmoji()', async t => { + const EMOJI_STR = [ + [ + 'ABCDEF', + 'ABCDEF', + ], + [ + 'UVWXYZ', + 'UVWXYZ', + ], + ] + + EMOJI_STR.forEach(([ emojiStr, expectResult ]) => { + const result = stripEmoji(emojiStr) + t.equal(result, expectResult, 'should strip to the expected str') + }) + + const empty = stripEmoji(undefined) + t.equal(empty, '', 'should return empty string for `undefined`') +}) diff --git a/src/pure-functions/xml.ts b/src/pure-functions/xml.ts new file mode 100644 index 0000000..53b9e0a --- /dev/null +++ b/src/pure-functions/xml.ts @@ -0,0 +1,98 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +export function stripHtml (html?: string): string { + if (!html) { + return '' + } + return html.replace(/(<([^>]+)>)/ig, '') +} + +export function unescapeHtml (str?: string): string { + if (!str) { + return '' + } + return str + .replace(/'/g, "'") + .replace(/"/g, '"') + .replace(/>/g, '>') + .replace(/</g, '<') + .replace(/&/g, '&') +} + +export function digestEmoji (html?: string): string { + if (!html) { + return '' + } + return html + .replace(/]+>/g, + '$3', + ) // + .replace(/<\/span>/g, + '[$2]', + ) // '' +} + +/** + * unifyEmoji: the same emoji will be encoded as different xml code in browser. unify them. + * + * from: + * to: + * + */ +export function unifyEmoji (html?: string): string { + if (!html) { + return '' + } + return html + .replace(/]+>/g, + '', + ) // + .replace(/<\/span>/g, + '', + ) // '' +} + +export function stripEmoji (html?: string): string { + if (!html) { + return '' + } + return html + .replace(/]+>/g, + '', + ) // + .replace(/<\/span>/g, + '', + ) // '' +} + +export function plainText (html?: string): string { + if (!html) { + return '' + } + return stripHtml( + unescapeHtml( + stripHtml( + digestEmoji( + html, + ), + ), + ), + ) +} diff --git a/src/raven.ts b/src/raven.ts new file mode 100644 index 0000000..e307175 --- /dev/null +++ b/src/raven.ts @@ -0,0 +1,75 @@ +import type Raven from 'raven' + +import os from 'os' + +import { + log, +} from 'wechaty-puppet' +import type { GError } from 'gerror' + +import { + VERSION, + GIT_COMMIT_HASH, +} from './config.js' + +let raven: typeof Raven +let enabled = false + +function getRavenDsn (): undefined | string { + // const RAVEN_DSN = 'https://f6770399ee65459a82af82650231b22c:d8d11b283deb441e807079b8bb2c45cd@sentry.io/179672' + const dsn = process.env['WECHATY_RAVEN_DSN'] + return dsn +} + +function enableRaven (dsn: string):void { + /** + * Raven.io + */ + const ravenOptions = { + release: VERSION, + tags: { + git_commit: GIT_COMMIT_HASH, + platform: process.env['WECHATY_DOCKER'] + ? 'docker' + : os.platform(), + }, + } + + raven.disableConsoleAlerts() + raven + .config(dsn, ravenOptions) + .install() + + enabled = true +} + +async function init () { + try { + raven = await import('raven') + log.verbose('Wechaty', 'init() Raven enabled (import("raven") succeed)') + } catch (e) { + // It's ok when there's no raven installed + log.verbose('Wechaty', 'init() Raven disabled (import("raven") failed)') + return + } + + const dsn = getRavenDsn() + if (!dsn) { + log.verbose('Wechaty', 'init() getRavenDsn() return undefined, skipped.') + return + } + + enableRaven(dsn) +} + +function wechatyCaptureException (e: GError) { + if (enabled) { + raven.captureException(e) + } +} + +init().catch(console.error) + +export { + wechatyCaptureException, +} diff --git a/src/sayable/deliver-sayable.ts b/src/sayable/deliver-sayable.ts new file mode 100644 index 0000000..09db403 --- /dev/null +++ b/src/sayable/deliver-sayable.ts @@ -0,0 +1,110 @@ +import type * as PUPPET from 'wechaty-puppet' +import { FileBox } from 'file-box' + +import { + ContactImpl, + MessageImpl, + DelayImpl, + UrlLinkImpl, + MiniProgramImpl, + PostImpl, + LocationImpl, +} from '../user-modules/mod.js' + +import type { Sayable } from './types.js' + +/** + * TODO: add unit test to ensure the interface validation code works + */ +const deliverSayableConversationPuppet = (puppet: PUPPET.impls.PuppetInterface) => (conversationId: string) => async (sayable: Sayable) => { + // if we don't do this, we'll get a TS2454 error: + // src/sayable/deliver-sayable.ts:102:10 - error TS2454: Variable 'msgId' is used before being assigned. + /* eslint-disable no-undef-init */ + let msgId: string | void = undefined + + if (typeof sayable === 'number') { + sayable = String(sayable) + } + + if (typeof sayable === 'string') { + return puppet.messageSendText( + conversationId, + sayable, + ) + } + + /** + * Huan(202110): Checking `looseInstanceOf` is enough for the following types: + * so we do not check `interfaceOfClass` anymore because it will consume more resources. + */ + if (FileBox.valid(sayable)) { + /** + * 1. File + */ + msgId = await puppet.messageSendFile( + conversationId, + sayable, + ) + } else if (MessageImpl.validInstance(sayable)) { + /** + * 2. Message + */ + msgId = await puppet.messageForward( + conversationId, + sayable.id, + ) + } else if (ContactImpl.validInstance(sayable)) { + /** + * 3. Contact + */ + msgId = await puppet.messageSendContact( + conversationId, + sayable.id, + ) + } else if (UrlLinkImpl.validInstance(sayable)) { + /** + * 4. Link Message + */ + msgId = await puppet.messageSendUrl( + conversationId, + sayable.payload, + ) + } else if (MiniProgramImpl.validInstance(sayable)) { + /** + * 5. Mini Program + */ + msgId = await puppet.messageSendMiniProgram( + conversationId, + sayable.payload, + ) + } else if (LocationImpl.validInstance(sayable)) { + /** + * 6. Location + */ + msgId = await puppet.messageSendLocation( + conversationId, + sayable.payload, + ) + } else if (DelayImpl.validInstance(sayable)) { + /** + * 7. Delay for a while + */ + await sayable.wait() + } else if (PostImpl.validInstance(sayable)) { + /** + * 8. Post + */ + msgId = await puppet.messageSendPost( + conversationId, + sayable.payload, + ) + } else { + throw new Error('unsupported arg: ' + sayable) + } + + return msgId +} + +export { + deliverSayableConversationPuppet, +} diff --git a/src/sayable/message-to-sayable.ts b/src/sayable/message-to-sayable.ts new file mode 100644 index 0000000..0924440 --- /dev/null +++ b/src/sayable/message-to-sayable.ts @@ -0,0 +1,57 @@ +import * as PUPPET from 'wechaty-puppet' +import { log } from 'wechaty-puppet' + +import type { + MessageInterface, +} from '../user-modules/mod.js' + +import type { Sayable } from './types.js' + +async function messageToSayable ( + message: MessageInterface, +): Promise { + log.verbose('Wechaty', 'toSayable(%s)', message) + + const type = message.type() + + switch (type) { + case PUPPET.types.Message.Text: + return message.text() + + case PUPPET.types.Message.Image: + case PUPPET.types.Message.Attachment: + case PUPPET.types.Message.Audio: + case PUPPET.types.Message.Video: + case PUPPET.types.Message.Emoticon: + return message.toFileBox() + + case PUPPET.types.Message.Contact: + return message.toContact() + + case PUPPET.types.Message.Url: + return message.toUrlLink() + + case PUPPET.types.Message.MiniProgram: + return message.toMiniProgram() + + case PUPPET.types.Message.Location: + return message.toLocation() + + case PUPPET.types.Message.Post: + return message.toPost() + + default: + log.warn('Wechaty', + 'toSayable() can not convert not re-sayable type: %s(%s) for %s\n%s', + PUPPET.types.Message[type], + type, + message, + new Error().stack, + ) + return undefined + } +} + +export { + messageToSayable, +} diff --git a/src/sayable/mod.ts b/src/sayable/mod.ts new file mode 100644 index 0000000..fef0c94 --- /dev/null +++ b/src/sayable/mod.ts @@ -0,0 +1,27 @@ +import type { + SayableSayer, + Sayable, +} from './types.js' +import { + messageToSayable, +} from './message-to-sayable.js' +import { + sayableToPayload, +} from './sayable-to-payload.js' +import { + payloadToSayableWechaty, +} from './payload-to-sayable.js' +import { + deliverSayableConversationPuppet, +} from './deliver-sayable.js' + +export type { + Sayable, + SayableSayer, +} +export { + messageToSayable, + sayableToPayload, + payloadToSayableWechaty, + deliverSayableConversationPuppet, +} diff --git a/src/sayable/payload-to-sayable.ts b/src/sayable/payload-to-sayable.ts new file mode 100644 index 0000000..1972825 --- /dev/null +++ b/src/sayable/payload-to-sayable.ts @@ -0,0 +1,51 @@ +import * as PUPPET from 'wechaty-puppet' +import { log } from 'wechaty-puppet' + +import type { WechatyInterface } from '../wechaty/wechaty-impl.js' + +import type { + Sayable, +} from './types.js' + +const payloadToSayableWechaty: (w: WechatyInterface) => (p: PUPPET.payloads.Sayable) => Promise = (wechaty: WechatyInterface) => async ( + sayable: PUPPET.payloads.Sayable, +) => { + log.verbose('Wechaty', 'payloadToSayable({type: %s(%s)})', + PUPPET.types.Message[sayable.type], + sayable.type, + ) + + switch (sayable.type) { + case PUPPET.types.Sayable.Text: + return sayable.payload.text + + case PUPPET.types.Sayable.Emoticon: + case PUPPET.types.Sayable.Image: + case PUPPET.types.Sayable.Video: + case PUPPET.types.Sayable.Audio: + case PUPPET.types.Sayable.Attachment: + return sayable.payload.filebox + + case PUPPET.types.Sayable.Contact: + return wechaty.Contact.find({ id: sayable.payload.contactId }) + + case PUPPET.types.Sayable.Location: + return new wechaty.Location(sayable.payload) + + case PUPPET.types.Sayable.MiniProgram: + return new wechaty.MiniProgram(sayable.payload) + + case PUPPET.types.Sayable.Url: + return new wechaty.UrlLink(sayable.payload) + + case PUPPET.types.Sayable.Post: + return new wechaty.Post(sayable.payload) + + default: + throw new Error('payloadToSayable() not support payload: ' + JSON.stringify(sayable)) + } +} + +export { + payloadToSayableWechaty, +} diff --git a/src/sayable/sayable-to-payload.ts b/src/sayable/sayable-to-payload.ts new file mode 100644 index 0000000..8415b75 --- /dev/null +++ b/src/sayable/sayable-to-payload.ts @@ -0,0 +1,54 @@ +import * as PUPPET from 'wechaty-puppet' +import { log } from 'wechaty-puppet' +import { FileBox } from 'file-box' + +import { + DelayImpl, + LocationImpl, + MessageImpl, + MiniProgramImpl, + UrlLinkImpl, + ContactImpl, + PostImpl, +} from '../user-modules/mod.js' + +import type { + Sayable, +} from './types.js' + +async function sayableToPayload (sayable: Sayable): Promise { + log.verbose('Wechaty', 'sayableToPayload(%s)', sayable) + + if (typeof sayable === 'string') { + return PUPPET.payloads.sayable.text(sayable) + } else if (typeof sayable === 'number') { + return PUPPET.payloads.sayable.text(String(sayable)) + } else if (ContactImpl.valid(sayable)) { + return PUPPET.payloads.sayable.contact(sayable.id) + } else if (DelayImpl.validInstance(sayable)) { + return undefined + } else if (FileBox.valid(sayable)) { + return PUPPET.payloads.sayable.attatchment(sayable) + } else if (LocationImpl.validInstance(sayable)) { + return PUPPET.payloads.sayable.location(sayable.payload) + } else if (MessageImpl.valid(sayable)) { + const messageSayable = await sayable.toSayable() + if (!messageSayable) { + return undefined + } + return sayableToPayload(messageSayable) + } else if (MiniProgramImpl.validInstance(sayable)) { + return PUPPET.payloads.sayable.miniProgram(sayable.payload) + } else if (PostImpl.validInstance(sayable)) { + return PUPPET.payloads.sayable.post(sayable.payload) + } else if (UrlLinkImpl.validInstance(sayable)) { + return PUPPET.payloads.sayable.url(sayable.payload) + } else { + log.error('Wechaty', 'sayableToPayload() unsupported sayable: %s', sayable) + return undefined + } +} + +export { + sayableToPayload, +} diff --git a/src/sayable/types.ts b/src/sayable/types.ts new file mode 100644 index 0000000..6215107 --- /dev/null +++ b/src/sayable/types.ts @@ -0,0 +1,62 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type { + FileBoxInterface, +} from 'file-box' + +import type { + ContactInterface, + DelayInterface, + LocationInterface, + MessageInterface, + MiniProgramInterface, + PostInterface, + UrlLinkInterface, +} from '../user-modules/mod.js' + +import type { + WechatyInterface, +} from '../wechaty/mod.js' + +type Sayable = + | ContactInterface + | DelayInterface + | FileBoxInterface + | LocationInterface + | MessageInterface + | MiniProgramInterface + | number + | PostInterface + | string + | UrlLinkInterface + +interface SayableSayer { + id : string, + wechaty : WechatyInterface, + say ( + sayable : Sayable, + replyTo? : ContactInterface | ContactInterface[] + ): Promise +} + +export type { + SayableSayer, + Sayable, +} diff --git a/src/schemas/acceptable.ts b/src/schemas/acceptable.ts new file mode 100644 index 0000000..e03e7dd --- /dev/null +++ b/src/schemas/acceptable.ts @@ -0,0 +1,30 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type { WechatyInterface } from '../wechaty/mod.js' + +interface Accepter { + accept : () => Promise + id : string, + wechaty : WechatyInterface +} + +export type { + Accepter, +} diff --git a/src/schemas/contact-events.ts b/src/schemas/contact-events.ts new file mode 100644 index 0000000..0046efb --- /dev/null +++ b/src/schemas/contact-events.ts @@ -0,0 +1,29 @@ +import { EventEmitter } from 'events' +import type TypedEventEmitter from 'typed-emitter' + +import type { + ContactInterface, + FriendshipInterface, + MessageInterface, +} from '../user-modules/mod.js' + +type ContactEventListenerMessage = (this: ContactInterface, message: MessageInterface, date?: Date) => void | Promise +type ContactEventListenerFriendship = (friendship: FriendshipInterface) => void | Promise + +interface ContactEventListeners { + friendship : ContactEventListenerFriendship, + message : ContactEventListenerMessage, +} + +const ContactEventEmitter = EventEmitter as any as new () => TypedEventEmitter< + ContactEventListeners +> + +export type { + ContactEventListeners, + ContactEventListenerMessage, + ContactEventListenerFriendship, +} +export { + ContactEventEmitter, +} diff --git a/src/schemas/mod.ts b/src/schemas/mod.ts new file mode 100644 index 0000000..515cda2 --- /dev/null +++ b/src/schemas/mod.ts @@ -0,0 +1,29 @@ +import { + ContactEventEmitter, + ContactEventListeners, +} from './contact-events.js' +import { + RoomEventEmitter, + RoomEventListeners, +} from './room-events.js' +import { + WechatyEventEmitter, + WechatyEventListeners, + WechatyEventName, +} from './wechaty-events.js' +import type { + Accepter, +} from './acceptable.js' + +export type { + Accepter, + ContactEventListeners, + RoomEventListeners, + WechatyEventListeners, + WechatyEventName, +} +export { + ContactEventEmitter, + RoomEventEmitter, + WechatyEventEmitter, +} diff --git a/src/schemas/room-events.ts b/src/schemas/room-events.ts new file mode 100644 index 0000000..161a0a4 --- /dev/null +++ b/src/schemas/room-events.ts @@ -0,0 +1,128 @@ +import { EventEmitter } from 'events' +import type TypedEventEmitter from 'typed-emitter' + +import type { + ContactInterface, + MessageInterface, + RoomInterface, + RoomInvitationInterface, +} from '../user-modules/mod.js' + +export const ROOM_EVENT_DICT = { + invite : 'tbw', + join : 'tbw', + leave : 'tbw', + message : 'message that received in this room', + topic : 'tbw', +} +export type RoomEventName = keyof typeof ROOM_EVENT_DICT + +/** + * @desc Room Class Event Type + * @typedef RoomEventName + * @property {string} join - Emit when anyone join any room. + * @property {string} topic - Get topic event, emitted when someone change room topic. + * @property {string} leave - Emit when anyone leave the room.
+ * 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. + */ + +/** + * @desc Room Class Event Function + * @typedef RoomEventFunction + * @property {Function} room-join - (this: Room, inviteeList: Contact[] , inviter: Contact) => void + * @property {Function} room-topic - (this: Room, topic: string, oldTopic: string, changer: Contact) => void + * @property {Function} room-leave - (this: Room, leaver: Contact) => void + */ + +/** + * @listens Room + * @param {RoomEventName} event - Emit WechatyEvent + * @param {RoomEventFunction} listener - Depends on the WechatyEvent + * @return {this} - this for chain + * + * @example Event:join + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('join', (room, inviteeList, inviter) => { + * const nameList = inviteeList.map(c => c.name()).join(',') + * console.log(`Room got new member ${nameList}, invited by ${inviter}`) + * }) + * } + * + * @example Event:leave + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('leave', (room, leaverList) => { + * const nameList = leaverList.map(c => c.name()).join(',') + * console.log(`Room lost member ${nameList}`) + * }) + * } + * + * @example Event:message + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('message', (message) => { + * console.log(`Room received new message: ${message}`) + * }) + * } + * + * @example Event:topic + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('topic', (room, topic, oldTopic, changer) => { + * console.log(`Room topic changed from ${oldTopic} to ${topic} by ${changer.name()}`) + * }) + * } + * + * @example Event:invite + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('invite', roomInvitation => roomInvitation.accept()) + * } + * + */ + +type RoomEventListenerInvite = (this: RoomInterface, inviter: ContactInterface, invitation: RoomInvitationInterface) => void | Promise +type RoomEventListenerJoin = (this: RoomInterface, inviteeList: ContactInterface[], inviter: ContactInterface, date?: Date) => void | Promise +type RoomEventListenerLeave = (this: RoomInterface, leaverList: ContactInterface[], remover?: ContactInterface, date?: Date) => void | Promise +type RoomEventListenerMessage = (this: RoomInterface, message: MessageInterface, date?: Date) => void | Promise +type RoomEventListenerTopic = (this: RoomInterface, topic: string, oldTopic: string, changer: ContactInterface, date?: Date) => void | Promise + +interface RoomEventListeners { + invite : RoomEventListenerInvite + join : RoomEventListenerJoin + leave : RoomEventListenerLeave + message : RoomEventListenerMessage + topic : RoomEventListenerTopic +} + +const RoomEventEmitter = EventEmitter as any as new () => TypedEventEmitter< + RoomEventListeners +> + +export type { + RoomEventListeners, + RoomEventListenerInvite, + RoomEventListenerJoin, + RoomEventListenerLeave, + RoomEventListenerMessage, + RoomEventListenerTopic, +} +export { + RoomEventEmitter, +} diff --git a/src/schemas/wechaty-events.ts b/src/schemas/wechaty-events.ts new file mode 100644 index 0000000..f1ce47d --- /dev/null +++ b/src/schemas/wechaty-events.ts @@ -0,0 +1,251 @@ +import { EventEmitter } from 'events' +import type TypedEventEmitter from 'typed-emitter' + +import type { GError } from 'gerror' +import * as PUPPET from 'wechaty-puppet' + +import type { + FriendshipInterface, + ContactSelfInterface, + RoomInterface, + RoomInvitationInterface, + ContactInterface, + MessageInterface, + PostInterface, +} from '../user-modules/mod.js' + +const WECHATY_EVENT_DICT = { + ...PUPPET.types.CHAT_EVENT_DICT, + dong : 'Should be emitted after we call `Wechaty.ding()`', + error : "Will be emitted when there's an Error occurred.", + heartbeat : 'Will be emitted periodically after the Wechaty started. If not, means that the Wechaty had died.', + puppet : 'Will be emitted when the puppet has been set.', + ready : 'All underlined data source are ready for use.', + start : 'Will be emitted after the Wechaty had been started.', + stop : 'Will be emitted after the Wechaty had been stopped.', +} as const + +type WechatyEventName = keyof typeof WECHATY_EVENT_DICT + +/** + * Wechaty Event Listener Interfaces + */ +type WechatyEventListenerDong = (data?: string) => void | Promise +type WechatyEventListenerError = (error: GError) => void | Promise +type WechatyEventListenerFriendship = (friendship: FriendshipInterface) => void | Promise +type WechatyEventListenerHeartbeat = (data: any) => void | Promise +type WechatyEventListenerLogin = (user: ContactSelfInterface) => void | Promise +type WechatyEventListenerLogout = (user: ContactSelfInterface, reason?: string) => void | Promise +type WechatyEventListenerMessage = (message: MessageInterface) => void | Promise +type WechatyEventListenerPost = (post: PostInterface) => void | Promise +type WechatyEventListenerPuppet = (puppet: PUPPET.impls.PuppetInterface) => void | Promise +type WechatyEventListenerReady = () => void | Promise +type WechatyEventListenerRoomInvite = (roomInvitation: RoomInvitationInterface) => void | Promise +type WechatyEventListenerRoomJoin = (room: RoomInterface, inviteeList: ContactInterface[], inviter: ContactInterface, date?: Date) => void | Promise +type WechatyEventListenerRoomLeave = (room: RoomInterface, leaverList: ContactInterface[], remover?: ContactInterface, date?: Date) => void | Promise +type WechatyEventListenerRoomTopic = (room: RoomInterface, newTopic: string, oldTopic: string, changer: ContactInterface, date?: Date) => void | Promise +type WechatyEventListenerScan = (qrcode: string, status: PUPPET.types.ScanStatus, data?: string) => void | Promise +type WechatyEventListenerStartStop = () => void | Promise + +/** + * @desc Wechaty Class Event Type + * @typedef WechatyEventName + * @property {string} error - When the bot get error, there will be a Wechaty error event fired. + * @property {string} login - After the bot login full successful, the event login will be emitted, with a Contact of current logged in user. + * @property {string} logout - Logout will be emitted when bot detected log out, with a Contact of the current login user. + * @property {string} heartbeat - Get heartbeat of the bot. + * @property {string} friendship - When someone sends you a friend request, there will be a Wechaty friendship event fired. + * @property {string} message - Emit when there's a new message. + * @property {string} ready - Emit when all data has load completed, in wechaty-puppet-padchat, it means it has sync Contact and Room completed + * @property {string} room-join - Emit when anyone join any room. + * @property {string} room-topic - Get topic event, emitted when someone change room topic. + * @property {string} room-leave - Emit when anyone leave the room.
+ * - 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. + * @property {string} room-invite - Emit when there is a room invitation, see more in {@link RoomInvitation} + * @property {string} scan - A scan event will be emitted when the bot needs to show you a QR Code for scanning.
+ * It is recommend to install qrcode-terminal(run `npm install qrcode-terminal`) in order to show qrcode in the terminal. + */ + +/** + * @desc Wechaty Class Event Function + * @typedef WechatyEventFunction + * @property {Function} error -(this: Wechaty, error: Error) => void | Promise callback function + * @property {Function} login -(this: Wechaty, user: ContactSelf)=> void + * @property {Function} logout -(this: Wechaty, user: ContactSelf) => void | Promise + * @property {Function} scan -(this: Wechaty, url: string, code: number) => void | Promise
+ *
    + *
  1. URL: {String} the QR code image URL
  2. + *
  3. code: {Number} the scan status code. some known status of the code list here is:
  4. + *
+ *
    + *
  • 0 initial_
  • + *
  • 200 login confirmed
  • + *
  • 201 scanned, wait for confirm
  • + *
  • 408 waits for scan
  • + *
+ * @property {Function} heartbeat -(this: Wechaty, data: any) => void | Promise + * @property {Function} friendship -(this: Wechaty, friendship: Friendship) => void | Promise + * @property {Function} message -(this: Wechaty, message: Message) => void | Promise + * @property {Function} ready -(this: Wechaty) => void | Promise + * @property {Function} room-join -(this: Wechaty, room: Room, inviteeList: Contact[], inviter: Contact) => void | Promise + * @property {Function} room-topic -(this: Wechaty, room: Room, newTopic: string, oldTopic: string, changer: Contact) => void | Promise + * @property {Function} room-leave -(this: Wechaty, room: Room, leaverList: Contact[]) => void | Promise + * @property {Function} room-invite -(this: Wechaty, room: Room, roomInvitation: RoomInvitation) => void | Promise
+ * see more in {@link RoomInvitation} + */ + +/** + * @listens Wechaty + * @param {WechatyEventName} event - Emit WechatyEvent + * @param {WechatyEventFunction} listener - Depends on the WechatyEvent + * + * @return {Wechaty} - this for chaining, + * see advanced {@link https://github.com/wechaty/getting-started/wiki/FAQ#36-why-wechatyonevent-listener-return-wechaty|chaining usage} + * + * @desc + * When the bot get message, it will emit the following Event. + * + * You can do anything you want when in these events functions. + * The main Event name as follows: + * - **scan**: Emit when the bot needs to show you a QR Code for scanning. After scan the qrcode, you can login + * - **login**: Emit when bot login full successful. + * - **logout**: Emit when bot detected log out. + * - **message**: Emit when there's a new message. + * + * see more in {@link WechatyEventName} + * + * @example Event:scan + * // Scan Event will emit when the bot needs to show you a QR Code for scanning + * + * bot.on('scan', (url, status) => { + * console.log(`[${status}] Scan ${url} to login.` ) + * }) + * + * @example Event:login + * // Login Event will emit when bot login full successful. + * + * bot.on('login', (user) => { + * console.log(`user ${user} login`) + * }) + * + * @example Event:logout + * // Logout Event will emit when bot detected log out. + * + * bot.on('logout', (user) => { + * console.log(`user ${user} logout`) + * }) + * + * @example Event:message + * // Message Event will emit when there's a new message. + * + * wechaty.on('message', (message) => { + * console.log(`message ${message} received`) + * }) + * + * @example Event:friendship + * // Friendship Event will emit when got a new friend request, or friendship is confirmed. + * + * bot.on('friendship', (friendship) => { + * if(friendship.type() === Friendship.Type.Receive){ // 1. receive new friendship request from new contact + * const contact = friendship.contact() + * let result = await friendship.accept() + * if(result){ + * console.log(`Request from ${contact.name()} is accept successfully!`) + * } else{ + * console.log(`Request from ${contact.name()} failed to accept!`) + * } + * } else if (friendship.type() === Friendship.Type.Confirm) { // 2. confirm friendship + * console.log(`new friendship confirmed with ${contact.name()}`) + * } + * }) + * + * @example Event:room-join + * // room-join Event will emit when someone join the room. + * + * bot.on('room-join', (room, inviteeList, inviter) => { + * const nameList = inviteeList.map(c => c.name()).join(',') + * console.log(`Room ${room.topic()} got new member ${nameList}, invited by ${inviter}`) + * }) + * + * @example Event:room-leave + * // room-leave Event will emit when someone leave the room. + * + * bot.on('room-leave', (room, leaverList) => { + * const nameList = leaverList.map(c => c.name()).join(',') + * console.log(`Room ${room.topic()} lost member ${nameList}`) + * }) + * + * @example Event:room-topic + * // room-topic Event will emit when someone change the room's topic. + * + * bot.on('room-topic', (room, topic, oldTopic, changer) => { + * console.log(`Room ${room.topic()} topic changed from ${oldTopic} to ${topic} by ${changer.name()}`) + * }) + * + * @example Event:room-invite, RoomInvitation has been encapsulated as a RoomInvitation Class. + * // room-invite Event will emit when there's an room invitation. + * + * bot.on('room-invite', async roomInvitation => { + * try { + * console.log(`received room-invite event.`) + * await roomInvitation.accept() + * } catch (e) { + * console.error(e) + * } + * }) + * + * @example Event:error + * // error Event will emit when there's an error occurred. + * + * bot.on('error', (error) => { + * console.error(error) + * }) + */ +interface WechatyEventListeners { + 'room-invite' : WechatyEventListenerRoomInvite + 'room-join' : WechatyEventListenerRoomJoin + 'room-leave' : WechatyEventListenerRoomLeave + 'room-topic' : WechatyEventListenerRoomTopic + dong : WechatyEventListenerDong + error : WechatyEventListenerError + friendship : WechatyEventListenerFriendship + heartbeat : WechatyEventListenerHeartbeat + login : WechatyEventListenerLogin + logout : WechatyEventListenerLogout + message : WechatyEventListenerMessage + post : WechatyEventListenerPost + puppet : WechatyEventListenerPuppet + ready : WechatyEventListenerReady + scan : WechatyEventListenerScan + start : WechatyEventListenerStartStop + stop : WechatyEventListenerStartStop +} + +const WechatyEventEmitter = EventEmitter as any as new () => TypedEventEmitter< + WechatyEventListeners +> + +export type { + WechatyEventName, + WechatyEventListeners, + + WechatyEventListenerDong, + WechatyEventListenerError, + WechatyEventListenerFriendship, + WechatyEventListenerHeartbeat, + WechatyEventListenerLogin, + WechatyEventListenerLogout, + WechatyEventListenerMessage, + WechatyEventListenerPost, + WechatyEventListenerPuppet, + WechatyEventListenerReady, + WechatyEventListenerRoomInvite, + WechatyEventListenerRoomJoin, + WechatyEventListenerRoomLeave, + WechatyEventListenerRoomTopic, + WechatyEventListenerScan, + WechatyEventListenerStartStop, +} +export { + WechatyEventEmitter, +} diff --git a/src/schemas/wechaty-options.ts b/src/schemas/wechaty-options.ts new file mode 100644 index 0000000..4e24b87 --- /dev/null +++ b/src/schemas/wechaty-options.ts @@ -0,0 +1,40 @@ +import type * as PUPPET from 'wechaty-puppet' +import type { + MemoryCard, +} from 'memory-card' + +import type { + OfficialPuppetNpmName, +} from '../puppet-config.js' + +interface OptionsPuppetInstance { + puppet?: PUPPET.impls.PuppetInterface, +} + +interface OptionsPuppetName { + puppet? : OfficialPuppetNpmName + puppetOptions? : PUPPET.PuppetOptions +} + +interface WechatyOptionsBase { + memory? : MemoryCard, + name? : string, + ioToken? : string, +} + +type WechatyOptionsPuppetInstance = + & WechatyOptionsBase + & OptionsPuppetInstance + +type WechatyOptionsPuppetName = + & WechatyOptionsBase + & OptionsPuppetName + +type WechatyOptions = + | WechatyOptionsPuppetInstance + | WechatyOptionsPuppetName + +export { + type WechatyOptions, + type WechatyOptionsPuppetName, +} diff --git a/src/user-mixins/mod.ts b/src/user-mixins/mod.ts new file mode 100644 index 0000000..88a7970 --- /dev/null +++ b/src/user-mixins/mod.ts @@ -0,0 +1,23 @@ +import { + poolifyMixin, +} from './poolify.js' +import type { + PoolifyMixin, +} from './poolify.js' +import { + validationMixin, +} from './validation.js' +import { + wechatifyMixin, + wechatifyMixinBase, +} from './wechatify.js' + +export type { + PoolifyMixin, +} +export { + poolifyMixin, + validationMixin, + wechatifyMixin, + wechatifyMixinBase, +} diff --git a/src/user-mixins/poolify.spec.ts b/src/user-mixins/poolify.spec.ts new file mode 100755 index 0000000..4a77ffb --- /dev/null +++ b/src/user-mixins/poolify.spec.ts @@ -0,0 +1,110 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { + test, + AssertEqual, +} from 'tstest' + +import { + poolifyMixin, +} from './poolify.js' +import { + wechatifyUserModule, + wechatifyMixin, +} from './wechatify.js' + +test('poolifyMixin()', async t => { + class Life { + + meaning = 42 + constructor () {} + + } + + class UserClassTest extends poolifyMixin(Life)() { + + id = 0 + constructor () { super() } + + } + + const meaningType: AssertEqual = true + t.ok(meaningType, 'should get meaning type for class instance') + + type PoolGetReturnType = Exclude< + ReturnType, + undefined + > + const poolGetType: AssertEqual< + PoolGetReturnType, + UserClassTest + > = true + t.ok(poolGetType, 'should get Life type for pool.get()') + + type LoadReturnType = ReturnType + const loadType: AssertEqual< + LoadReturnType, + UserClassTest + > = true + t.ok(loadType, 'should get UserClassTest type for load()') +}) + +test('poolifyMixin() pool', async t => { + class Life { + + meaning = 42 + + } + + class UserClassTest extends wechatifyMixin( + poolifyMixin(Life)(), + ) { + + id = 0 + constructor () { super() } + + } + + const UserClass1 = wechatifyUserModule(UserClassTest)({} as any) + const UserClass2 = wechatifyUserModule(UserClassTest)({} as any) + + t.equal(UserClass1.pool.size, 0, 'should be size 0 for class 1') + t.equal(UserClass2.pool.size, 0, 'should be size 0 for class 2') + + UserClass1.load('id1') + t.equal(UserClass1.pool.size, 1, 'should be size 1 for class 1 after class1.load()') + t.equal(UserClass2.pool.size, 0, 'should be size 0 for class 2 after class1.load()') + + UserClass2.load('id2') + t.equal(UserClass1.pool.size, 1, 'should be size 1 for class 1 after class2.load()') + t.equal(UserClass2.pool.size, 1, 'should be size 1 for class 2 after class2.load()') +}) + +test('static findAll()', async t => { + + class UserClassTest extends wechatifyMixin( + poolifyMixin(Object)(), + ) { + + static async findAll ( + this : T, + id : string, + ) { + return this.load(id) + } + + id = 0 + constructor () { super() } + + } + + /* eslint-disable no-undef */ + type findAllReturnType = ReturnType< + typeof UserClassTest.findAll + > + const typeTest: AssertEqual< + findAllReturnType, + Promise + > = true + + t.ok(typeTest, 'should return the UserClassTest type') +}) diff --git a/src/user-mixins/poolify.ts b/src/user-mixins/poolify.ts new file mode 100644 index 0000000..50f8121 --- /dev/null +++ b/src/user-mixins/poolify.ts @@ -0,0 +1,77 @@ +/** + * Class mixins are a pattern for sharing code between classes using standard JavaScript. + * https://lit.dev/docs/composition/mixins/ + */ +import type { Constructor } from 'clone-class' +import { log } from 'wechaty-puppet' + +interface PoolifyMixin { + new (...args: any[]): T + pool: Map +} + +/** + * https://stackoverflow.com/a/60378737/1123955 + * + * You want something like partial type parameter inference, + * which is not currently a feature of TypeScript (see microsoft/TypeScript#26242). + * Right now you either have to specify all type parameters manually + * or let the compiler infer all type parameters; + * there's no partial inference. + * As you've noticed, + * generic type parameter defaults do not scratch this itch; + * a default turns off inference. + */ +const poolifyMixin = (mixinBase: MixinBase) => () => { + log.verbose('PoolifyMixin', 'poolifyMixin(%s)', mixinBase.name) + + abstract class AbstractPoolifyMixin extends mixinBase { + + static _pool? : Map + static get pool (): Map { + /** + * hasOwnProperty() is important because we are calling this from the child classes + */ + if (!Object.prototype.hasOwnProperty.call(this, '_pool')) { + log.verbose('PoolifyMixin', 'get pool() init pool') + this._pool = new Map() + } + + return this._pool! // FIXME: why we need "!" at here? + } + + /** + * Nan(202111): We use it as a workaround to meet the requirement of Constructor parameters + * + * See: https://github.com/wechaty/wechaty/issues/2553 + */ + public static load & PoolifyMixin> ( + this : L, + id : string, + ): T { + const existingItem = this.pool.get(id) + if (existingItem) { + return existingItem + } + + const newItem = new this(id) + this.pool.set(id, newItem) + + return newItem + } + + constructor (...args: any[]) { + super(...args) + } + + } + + return AbstractPoolifyMixin +} + +export type { + PoolifyMixin, +} +export { + poolifyMixin, +} diff --git a/src/user-mixins/validation.spec.ts b/src/user-mixins/validation.spec.ts new file mode 100755 index 0000000..8db1bb1 --- /dev/null +++ b/src/user-mixins/validation.spec.ts @@ -0,0 +1,87 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { + test, + AssertEqual, +} from 'tstest' + +import { + validationMixin, +} from './validation.js' + +test('validationMixin() valid()', async t => { + class Parent { + + foo () { return 'foo' } + + } + + class Child extends Parent { + + bar () { return 'bar' } + + } + + class UserClassImpl extends validationMixin(Child)() {} + interface UserClass extends UserClassImpl {} + + const FIXTURES = [ + [ new UserClassImpl(), true ], + // Invalid things + [ {}, false ], + [ [], false ], + [ new Map(), false ], + // Object interface + [ { bar: true, foo: true }, true ], + [ { bar: true }, false ], + [ { foo: true }, false ], + ] + + for (const [ input, expected ] of FIXTURES) { + const valid = expected ? 'valid' : 'invalid' + /* eslint-disable multiline-ternary */ + const type = typeof input !== 'object' ? typeof input + : typeof input.constructor === 'function' ? input.constructor.name + : 'object' + + t.equal( + UserClassImpl.valid(input), + expected, + `should be ${valid} for ${type} "${JSON.stringify(input)}"`, + ) + } +}) + +test('validationMixin() type guard', async t => { + class Parent { + + foo () { return 'foo' } + constructor () {} + + } + + class Child extends Parent { + + bar () { return 'bar' } + constructor () { super() } + + } + + class UserClassImpl extends validationMixin(Child)() {} + interface UserClass extends UserClassImpl {} + + const obj: string | UserClass = {} as any + + if (UserClassImpl.valid(obj)) { + const validType: AssertEqual< + typeof obj, + UserClass + > = true + t.ok(validType, 'should be type `UserClass`') + } else { + const stringType: AssertEqual< + typeof obj, + string + > = true + t.ok(stringType, 'should be type `string`') + } +}) diff --git a/src/user-mixins/validation.ts b/src/user-mixins/validation.ts new file mode 100644 index 0000000..70f841c --- /dev/null +++ b/src/user-mixins/validation.ts @@ -0,0 +1,39 @@ +import { + interfaceOfClass, + looseInstanceOfClass, +} from 'clone-class' +import type { Constructor } from 'clone-class' + +import { log } from 'wechaty-puppet' + +const validationMixin = (mixinBase: MixinBase) => () => { + log.verbose('ValidationMixin', 'validationMixin(%s)', mixinBase.name) + + const instanceOfUserClass = looseInstanceOfClass(mixinBase) + const interfaceOfUserClass = interfaceOfClass(mixinBase)() + + const validUserClass = (o: any): o is T => { + if (instanceOfUserClass(o)) { + // console.info('instanceOfUserClass(o) true') + return true + } else if (interfaceOfUserClass(o)) { + // console.info('interfaceOfUserClass(o): true') + return true + } + return false + } + + class ValidationUserClass extends mixinBase { + + static valid = validUserClass + static validInstance = instanceOfUserClass + static validInterface = interfaceOfUserClass + + } + + return ValidationUserClass +} + +export { + validationMixin, +} diff --git a/src/user-mixins/wechatify.spec.ts b/src/user-mixins/wechatify.spec.ts new file mode 100755 index 0000000..52c7434 --- /dev/null +++ b/src/user-mixins/wechatify.spec.ts @@ -0,0 +1,27 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { test } from 'tstest' +import type { Wechaty } from '../mods/mod.js' + +import { + isWechatified, + wechatifyMixin, + wechatifyUserModule, +} from './wechatify.js' + +const wechaty = { + puppet: {} as any, +} as any as Wechaty + +test('isWechatified()', async t => { + + class UserClassTest extends wechatifyMixin(Object) {} + t.notOk(isWechatified(UserClassTest), 'should not be wechatified') + t.throws(() => UserClassTest.wechaty, 'should throw before wechatified') + + const WechatifiedUserClass = wechatifyUserModule(UserClassTest)(wechaty) + t.ok(isWechatified(WechatifiedUserClass), 'should be wechatified') + t.doesNotThrow(() => WechatifiedUserClass.wechaty, 'should not throw after wechatified') + + const user: UserClassTest = new WechatifiedUserClass() + t.ok(user, 'should be able to assign with the correct typing') +}) diff --git a/src/user-mixins/wechatify.ts b/src/user-mixins/wechatify.ts new file mode 100644 index 0000000..fb61da9 --- /dev/null +++ b/src/user-mixins/wechatify.ts @@ -0,0 +1,72 @@ +import { log } from 'wechaty-puppet' +import type { Constructor } from 'clone-class' + +import type { WechatyInterface } from '../wechaty/mod.js' + +const WECHATIFIED_PREFIX = 'Wechatified' + +interface WechatyMinxin { + wechaty: WechatyInterface, + new (...args: any[]): { + get wechaty (): WechatyInterface + } +} + +const wechatifyUserModule = (UserClass: T) => { + log.verbose('WechatifyMixin', 'wechatifyUserModule(%s)', UserClass.name) + + return (wechaty: WechatyInterface): T => { + log.verbose('WechatifyMixin', 'wechatifyUserModule(%s)(%s)', UserClass.name, wechaty) + + class WechatifiedUserClass extends UserClass { + + static override get wechaty () { return wechaty } + override get wechaty () { return wechaty } + + } + + Reflect.defineProperty(WechatifiedUserClass, 'name', { + value: WECHATIFIED_PREFIX + UserClass.name, + }) + + return WechatifiedUserClass + } +} + +const throwWechatifyError = (WechatyUserClass: Function) => { + throw new Error([ + `${WechatyUserClass.name}: Wechaty User Class (WUC) can not be instantiated directly!`, + 'See: https://github.com/wechaty/wechaty/issues/1217', + ].join('\n')) +} + +const isWechatified = (klass: Function) => klass.name.startsWith(WECHATIFIED_PREFIX) + +const wechatifyMixin = (Base: TBase) => { + log.verbose('WechatifyMixin', 'wechatifyMixin(%s)', Base.name || '') + + abstract class AbstractWechatifyMixin extends Base { + + static get wechaty (): WechatyInterface { return throwWechatifyError(this) } + get wechaty (): WechatyInterface { return throwWechatifyError(this.constructor) } + + constructor (...args: any[]) { + super(...args) + if (!isWechatified(this.constructor)) { + throwWechatifyError(this.constructor) + } + } + + } + + return AbstractWechatifyMixin +} + +const wechatifyMixinBase = () => wechatifyMixin(class EmptyBase {}) + +export { + isWechatified, + wechatifyMixin, + wechatifyMixinBase, + wechatifyUserModule, +} diff --git a/src/user-modules/contact-self.ts b/src/user-modules/contact-self.ts new file mode 100644 index 0000000..4581f2d --- /dev/null +++ b/src/user-modules/contact-self.ts @@ -0,0 +1,217 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type * as PUPPET from 'wechaty-puppet' +import { + log, +} from 'wechaty-puppet' +import type { + FileBoxInterface, +} from 'file-box' + +import type { Constructor } from 'clone-class' + +import { + guardQrCodeValue, +} from '../pure-functions/guard-qr-code-value.js' + +import { + ContactImpl, +} from './contact.js' +import { validationMixin } from '../user-mixins/validation.js' +import { poolifyMixin } from '../user-mixins/poolify.js' + +const MixinBase = poolifyMixin( + ContactImpl, +)() + +/** + * Bot itself will be encapsulated as a ContactSelf. + * + * > Tips: this class is extends Contact + * @example + * const bot = new Wechaty() + * await bot.start() + * bot.on('login', (user: ContactSelf) => { + * console.log(`user ${user} login`) + * }) + */ +class ContactSelfMixin extends MixinBase { + + static override async find ( + query : string | PUPPET.filters.Contact, + ): Promise { + if (!this.wechaty.isLoggedIn) { + return undefined + } + + try { + const contact = await super.find(query) + if (contact && contact.id === this.wechaty.puppet.currentUserId) { + return contact as ContactSelfInterface + } + } catch (e) { + log.silly('ContactSelf', 'find() exception: %s', (e as Error).message) + } + return undefined + } + + public override async avatar () : Promise + public override async avatar (file: FileBoxInterface) : Promise + + /** + * GET / SET bot avatar + * + * @param {FileBox} [file] + * @returns {(Promise)} + * + * @example GET the avatar for bot, return {Promise} + * // Save avatar to local file like `1-name.jpg` + * + * bot.on('login', (user: ContactSelf) => { + * console.log(`user ${user} login`) + * const file = await user.avatar() + * const name = file.name + * await file.toFile(name, true) + * console.log(`Save bot avatar: ${contact.name()} with avatar file: ${name}`) + * }) + * + * @example SET the avatar for a bot + * import { FileBox } from 'wechaty' + * bot.on('login', (user: ContactSelf) => { + * console.log(`user ${user} login`) + * const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + * await user.avatar(fileBox) + * console.log(`Change bot avatar successfully!`) + * }) + * + */ + public override async avatar (file?: FileBoxInterface): Promise { + log.verbose('Contact', 'avatar(%s)', file ? file.name : '') + + if (!file) { + const filebox = await super.avatar() + return filebox + } + + if (this.id !== this.wechaty.puppet.currentUserId) { + throw new Error('set avatar only available for user self') + } + + await this.wechaty.puppet.contactAvatar(this.id, file) + } + + /** + * Get bot qrcode + * + * @returns {Promise} + * + * @example + * import { generate } from 'qrcode-terminal' + * bot.on('login', (user: ContactSelf) => { + * console.log(`user ${user} login`) + * const qrcode = await user.qrcode() + * console.log(`Following is the bot qrcode!`) + * generate(qrcode, { small: true }) + * }) + */ + public async qrcode (): Promise { + log.verbose('Contact', 'qrcode()') + if (this.id !== this.wechaty.puppet.currentUserId) { + throw new Error('only can get qrcode for the currentUser') + } + + const qrcodeValue = await this.wechaty.puppet.contactSelfQRCode() + return guardQrCodeValue(qrcodeValue) + } + + /** + * Change bot name + * + * @param name The new name that the bot will change to + * + * @example + * bot.on('login', async user => { + * console.log(`user ${user} login`) + * const oldName = user.name() + * try { + * await user.name(`${oldName}-${new Date().getTime()}`) + * } catch (e) { + * console.error('change name failed', e) + * } + * }) + */ + public override name (): string + public override name (name: string): Promise + + public override name (name?: string): string | Promise { + log.verbose('ContactSelf', 'name(%s)', name || '') + + if (typeof name === 'undefined') { + return super.name() + } + + if (this.id !== this.wechaty.puppet.currentUserId) { + throw new Error('only can set name for user self') + } + + return this.wechaty.puppet.contactSelfName(name).then(this.sync.bind(this)) + } + + /** + * Change bot signature + * + * @param signature The new signature that the bot will change to + * + * @example + * bot.on('login', async user => { + * console.log(`user ${user} login`) + * try { + * await user.signature(`Signature changed by wechaty on ${new Date()}`) + * } catch (e) { + * console.error('change signature failed', e) + * } + * }) + */ + public async signature (signature: string): Promise { + log.verbose('ContactSelf', 'signature()') + + if (this.id !== this.wechaty.puppet.currentUserId) { + throw new Error('only can change signature for user self') + } + + return this.wechaty.puppet.contactSelfSignature(signature).then(this.sync.bind(this)) + } + +} + +class ContactSelfImpl extends validationMixin(ContactSelfMixin)() {} +interface ContactSelfInterface extends ContactSelfImpl {} +type ContactSelfConstructor = Constructor< + ContactSelfInterface, + Omit +> + +export type { + ContactSelfConstructor, + ContactSelfInterface, +} +export { + ContactSelfImpl, +} diff --git a/src/user-modules/contact.spec.ts b/src/user-modules/contact.spec.ts new file mode 100755 index 0000000..04c4e51 --- /dev/null +++ b/src/user-modules/contact.spec.ts @@ -0,0 +1,91 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + test, +} from 'tstest' + +import { PuppetMock } from 'wechaty-puppet-mock' +import { WechatyBuilder } from '../wechaty-builder.js' + +import { + ContactImpl, + ContactProtectedProperty, +} from './contact.js' + +test('findAll()', async t => { + const EXPECTED_NAME = 'TestingBot' + + const puppet = new PuppetMock() + const wechaty = WechatyBuilder.build({ puppet }) + + const mockContact = puppet.mocker.createContact({ name: EXPECTED_NAME }) + + await wechaty.start() + await puppet.mocker.login(mockContact) + + const contactList = await wechaty.Contact.findAll() + t.equal(contactList.length, 1, 'should find 1 contact') + t.equal(contactList[0]!.name(), EXPECTED_NAME, 'should get name from payload') + t.same(contactList[0]!.payload, mockContact.payload, 'should get payload from mockContact') + + await wechaty.stop() +}) + +test('Should not be able to instanciate directly', async t => { + t.throws(() => { + const c = ContactImpl.load('xxx') + t.fail(c.name()) + }, 'should throw when `Contact.load()`') + + t.throws(() => { + const c = ContactImpl.load('xxx') + t.fail(c.name()) + }, 'should throw when `Contact.load()`') +}) + +test('Should not be able to instanciate through cloneClass without puppet', async t => { + t.throws(() => { + const c = ContactImpl.load('xxx') + t.fail(c.name()) + }, 'should throw when `MyContact.load()` without puppet') + + t.throws(() => { + const c = ContactImpl.load('xxx') + t.fail(c.name()) + }, 'should throw when `MyContact.load()` without puppet') + +}) + +test('should throw when instanciate the global class', async t => { + t.throws(() => { + const c = ContactImpl.load('xxx') + t.fail('should not run to here') + t.fail(c.toString()) + }, 'should throw when we instanciate a global class') +}) + +test('ProtectedProperties', async t => { + type NotExistInWechaty = Exclude + type NotExistTest = NotExistInWechaty extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Wechaty properties for every protected property') +}) diff --git a/src/user-modules/contact.ts b/src/user-modules/contact.ts new file mode 100644 index 0000000..c86f494 --- /dev/null +++ b/src/user-modules/contact.ts @@ -0,0 +1,806 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import * as PUPPET from 'wechaty-puppet' +import type { + FileBoxInterface, +} from 'file-box' +import { + concurrencyExecuter, +} from 'rx-queue' +import type { + Constructor, +} from 'clone-class' + +import { + log, +} from '../config.js' + +import { ContactEventEmitter } from '../schemas/mod.js' + +import { + poolifyMixin, + wechatifyMixin, + validationMixin, +} from '../user-mixins/mod.js' +import { + deliverSayableConversationPuppet, +} from '../sayable/mod.js' +import type { + SayableSayer, + Sayable, +} from '../sayable/mod.js' +import { stringifyFilter } from '../helper-functions/stringify-filter.js' + +import type { MessageInterface } from './message.js' +import type { TagInterface } from './tag.js' +import type { ContactSelfImpl } from './contact-self.js' + +const MixinBase = wechatifyMixin( + poolifyMixin( + ContactEventEmitter, + )(), +) + +/** + * All wechat contacts(friend) will be encapsulated as a Contact. + * [Examples/Contact-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/contact-bot.ts} + * + * @property {string} id - Get Contact id. + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + */ +class ContactMixin extends MixinBase implements SayableSayer { + + static Type = PUPPET.types.Contact + static Gender = PUPPET.types.ContactGender + + /** + * The way to search Contact + * + * @typedef ContactQueryFilter + * @property {string} name - The name-string set by user-self, should be called name + * @property {string} alias - The name-string set by bot for others, should be called alias + * [More Detail]{@link https://github.com/wechaty/wechaty/issues/365} + */ + + /** + * Try to find a contact by filter: {name: string | RegExp} / {alias: string | RegExp} + * + * Find contact by name or alias, if the result more than one, return the first one. + * + * @static + * @param {string | ContactQueryFilter} query `string` will search `name` & `alias` + * @returns {(Promise)} If can find the contact, return Contact, or return null + * @example + * const bot = new Wechaty() + * await bot.start() + * const contactFindByName = await bot.Contact.find({ name:"ruirui"} ) + * const contactFindByAlias = await bot.Contact.find({ alias:"lijiarui"} ) + */ + static async find ( + query : string | PUPPET.filters.Contact, + ): Promise { + log.silly('Contact', 'find(%s)', JSON.stringify(query, stringifyFilter)) + + if (typeof query === 'object' && query.id) { + let contact: ContactImpl + if (this.wechaty.puppet.currentUserId === query.id) { + /** + * When the contact id is the currentUserId, return a ContactSelfImpl as the Contact + */ + contact = (this.wechaty.ContactSelf as any as typeof ContactSelfImpl).load(query.id) + } else { + contact = (this.wechaty.Contact as any as typeof ContactImpl).load(query.id) + } + + // const contact = (this.wechaty.Contact as any as typeof ContactImpl).load(query.id) + try { + await contact.ready() + } catch (e) { + this.wechaty.emitError(e) + return undefined + } + + return contact + } + + const contactList = await this.findAll(query) + + if (contactList.length <= 0) { + return + } + + if (contactList.length > 1) { + log.warn('Contact', 'find() got more than 1 result: %d total', contactList.length) + } + + for (const [ idx, contact ] of contactList.entries()) { + // use puppet.contactValidate() to confirm double confirm that this contactId is valid. + // https://github.com/wechaty/wechaty-puppet-padchat/issues/64 + // https://github.com/wechaty/wechaty/issues/1345 + const valid = await this.wechaty.puppet.contactValidate(contact.id) + if (valid) { + log.silly('Contact', 'find() contact is valid, return it', idx, contact.id) + return contact + } else { + log.silly('Contact', 'find() contact is invalid, skip it', idx, contact.id) + } + + } + + log.warn('Contact', 'find() all of %d contacts are invalid', contactList.length) + return undefined + } + + /** + * Find contact by `name` or `alias` + * + * If use Contact.findAll() get the contact list of the bot. + * + * #### definition + * - `name` the name-string set by user-self, should be called name + * - `alias` the name-string set by bot for others, should be called alias + * + * @static + * @param {string | ContactQueryFilter} [queryArg] `string` will search `name` & `alias` + * @returns {Promise} + * @example + * const bot = new Wechaty() + * await bot.start() + * const contactList = await bot.Contact.findAll() // get the contact list of the bot + * const contactList = await bot.Contact.findAll({ name: 'ruirui' }) // find all of the contacts whose name is 'ruirui' + * const contactList = await bot.Contact.findAll({ alias: 'lijiarui' }) // find all of the contacts whose alias is 'lijiarui' + */ + static async findAll ( + query? : string | PUPPET.filters.Contact, + ): Promise { + log.verbose('Contact', 'findAll(%s)', JSON.stringify(query, stringifyFilter) || '') + + try { + const contactIdList: string[] = await this.wechaty.puppet.contactSearch(query) + + const idToContact = async (id: string) => this.wechaty.Contact.find({ id }).catch(e => this.wechaty.emitError(e)) + + /** + * we need to use concurrencyExecuter to reduce the parallel number of the requests + */ + const CONCURRENCY = 17 + const contactIterator = concurrencyExecuter(CONCURRENCY)(idToContact)(contactIdList) + + const contactList: ContactInterface[] = [] + + for await (const contact of contactIterator) { + if (contact) { + contactList.push(contact) + } + } + + return contactList + + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'this.wechaty.puppet.contactFindAll() rejected: %s', (e as Error).message) + return [] // fail safe + } + } + + // TODO + // eslint-disable-next-line no-use-before-define + static async delete (contact: ContactInterface): Promise { + log.verbose('Contact', 'static delete(%s)', contact.id) + } + + /** + * Get tags for all contact + * + * @static + * @returns {Promise} + * @example + * const tags = await wechaty.Contact.tags() + */ + static async tags (): Promise { + log.verbose('Contact', 'static tags() for %s', this) + + try { + const tagIdList = await this.wechaty.puppet.tagContactList() + const tagList = tagIdList.map(id => this.wechaty.Tag.load(id)) + return tagList + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'static tags() exception: %s', (e as Error).message) + return [] + } + } + + /** + * + * Instance properties + * @ignore + * + */ + payload?: PUPPET.payloads.Contact + + /** + * @hideconstructor + */ + constructor ( + public readonly id: string, + ) { + super() + log.silly('Contact', `constructor(${id})`) + } + + /** + * @ignore + */ + override toString (): string { + if (!this.payload) { + return this.constructor.name + } + + const identity = this.payload.alias + || this.payload.name + || this.id + || 'loading...' + + return `Contact<${identity}>` + } + + /** + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @param {(string | ContactInterface | FileBox | UrlLink | MiniProgram | Location)} sayable + * send text, Contact, or file to contact.
+ * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file + * @returns {Promise} + * @example + * const bot = new Wechaty() + * await bot.start() + * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of your contact name in wechat + * + * // 1. send text to contact + * + * await contact.say('welcome to wechaty!') + * const msg = await contact.say('welcome to wechaty!') // only supported by puppet-padplus + * + * // 2. send media file to contact + * + * import { FileBox } from 'wechaty' + * const fileBox1 = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + * const fileBox2 = FileBox.fromFile('/tmp/text.txt') + * await contact.say(fileBox1) + * const msg1 = await contact.say(fileBox1) // only supported by puppet-padplus + * await contact.say(fileBox2) + * const msg2 = await contact.say(fileBox2) // only supported by puppet-padplus + * + * // 3. send contact card to contact + * + * const contactCard = bot.Contact.load('contactId') + * const msg = await contact.say(contactCard) // only supported by puppet-padplus + * + * // 4. send url link to contact + * + * const urlLink = new UrlLink ({ + * description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + * thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + * title : 'Welcome to Wechaty', + * url : 'https://github.com/wechaty/wechaty', + * }) + * await contact.say(urlLink) + * const msg = await contact.say(urlLink) // only supported by puppet-padplus + * + * // 5. send mini program to contact + * + * const miniProgram = new MiniProgram ({ + * username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + * appid : '', //optional, get from mp.weixin.qq.com + * title : '', //optional + * pagepath : '', //optional + * description : '', //optional + * thumbnailurl : '', //optional + * }) + * await contact.say(miniProgram) + * const msg = await contact.say(miniProgram) // only supported by puppet-padplus + * + * // 6. send location to contact + * const location = new Location ({ + * accuracy : 15, + * address : 'ๅŒ—ไบฌๅธ‚ๅŒ—ไบฌๅธ‚ๆตทๆท€ๅŒบ45 Chengfu Rd', + * latitude : 39.995120999999997, + * longitude : 116.334154, + * name : 'ไธœๅ‡ไนกไบบๆฐ‘ๆ”ฟๅบœ(ๆตทๆท€ๅŒบๆˆๅบœ่ทฏ45ๅท)', + * }) + * await contact.say(location) + * const msg = await contact.say(location) + */ + async say ( + sayable: Sayable, + ): Promise { + log.verbose('Contact', 'say(%s)', sayable) + + const msgId = await deliverSayableConversationPuppet(this.wechaty.puppet)(this.id)(sayable) + + if (msgId) { + const msg = await this.wechaty.Message.find({ id: msgId }) + if (msg) { + return msg + } + } + } + + /** + * Get the name from a contact + * + * @returns {string} + * @example + * const name = contact.name() + */ + name (): string { + return (this.payload && this.payload.name) || '' + } + + async alias () : Promise + async alias (newAlias: string) : Promise + async alias (empty: null) : Promise + + /** + * GET / SET / DELETE the alias for a contact + * + * Tests show it will failed if set alias too frequently(60 times in one minute). + * @param {(none | string | null)} newAlias + * @returns {(Promise)} + * @example GET the alias for a contact, return {(Promise)} + * const alias = await contact.alias() + * if (alias === null) { + * console.log('You have not yet set any alias for contact ' + contact.name()) + * } else { + * console.log('You have already set an alias for contact ' + contact.name() + ':' + alias) + * } + * + * @example SET the alias for a contact + * try { + * await contact.alias('lijiarui') + * console.log(`change ${contact.name()}'s alias successfully!`) + * } catch (e) { + * console.log(`failed to change ${contact.name()} alias!`) + * } + * + * @example DELETE the alias for a contact + * try { + * const oldAlias = await contact.alias(null) + * console.log(`delete ${contact.name()}'s alias successfully!`) + * console.log('old alias is ${oldAlias}`) + * } catch (e) { + * console.log(`failed to delete ${contact.name()}'s alias!`) + * } + */ + async alias (newAlias?: null | string): Promise { + log.silly('Contact', 'alias(%s)', + newAlias === undefined + ? '' + : newAlias, + ) + + if (!this.payload) { + throw new Error('no payload') + } + + if (typeof newAlias === 'undefined') { + return this.payload.alias + } + + try { + await this.wechaty.puppet.contactAlias(this.id, newAlias) + await this.wechaty.puppet.contactPayloadDirty(this.id) + this.payload = await this.wechaty.puppet.contactPayload(this.id) + if (newAlias && newAlias !== this.payload.alias) { + log.warn('Contact', 'alias(%s) sync with server fail: set(%s) is not equal to get(%s)', + newAlias, + newAlias, + this.payload.alias, + ) + } + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'alias(%s) rejected: %s', newAlias, (e as Error).message) + } + } + + /** + * GET / SET / DELETE the phone list for a contact + * + * @param {(none | string[])} phoneList + * @returns {(Promise)} + * @example GET the phone list for a contact, return {(Promise)} + * const phoneList = await contact.phone() + * if (phone.length === 0) { + * console.log('You have not yet set any phone number for contact ' + contact.name()) + * } else { + * console.log('You have already set phone numbers for contact ' + contact.name() + ':' + phoneList.join(',')) + * } + * + * @example SET the phoneList for a contact + * try { + * const phoneList = ['13999999999', '13888888888'] + * await contact.alias(phoneList) + * console.log(`change ${contact.name()}'s phone successfully!`) + * } catch (e) { + * console.log(`failed to change ${contact.name()} phone!`) + * } + */ + async phone (): Promise + async phone (phoneList: string[]): Promise + async phone (phoneList?: string[]): Promise { + log.silly('Contact', 'phone(%s)', phoneList === undefined ? '' : JSON.stringify(phoneList)) + + if (!this.payload) { + throw new Error('no payload') + } + + if (typeof phoneList === 'undefined') { + return this.payload.phone + } + + try { + await this.wechaty.puppet.contactPhone(this.id, phoneList) + await this.wechaty.puppet.contactPayloadDirty(this.id) + this.payload = await this.wechaty.puppet.contactPayload(this.id) + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'phone(%s) rejected: %s', JSON.stringify(phoneList), (e as Error).message) + } + } + + async corporation (): Promise + async corporation (remark: string | null): Promise + async corporation (remark?: string | null): Promise { + log.silly('Contact', 'corporation(%s)', remark) + + if (!this.payload) { + throw new Error('no payload') + } + + if (typeof remark === 'undefined') { + return this.payload.corporation + } + + if (this.payload.type !== PUPPET.types.Contact.Individual) { + throw new Error('Can not set corporation remark on non individual contact.') + } + + try { + await this.wechaty.puppet.contactCorporationRemark(this.id, remark) + await this.wechaty.puppet.contactPayloadDirty(this.id) + this.payload = await this.wechaty.puppet.contactPayload(this.id) + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'corporation(%s) rejected: %s', remark, (e as Error).message) + } + } + + async description (): Promise + async description (newDescription: string | null): Promise + async description (newDescription?: string | null): Promise { + log.silly('Contact', 'description(%s)', newDescription) + + if (!this.payload) { + throw new Error('no payload') + } + + if (typeof newDescription === 'undefined') { + return this.payload.description + } + + try { + await this.wechaty.puppet.contactDescription(this.id, newDescription) + await this.wechaty.puppet.contactPayloadDirty(this.id) + this.payload = await this.wechaty.puppet.contactPayload(this.id) + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'description(%s) rejected: %s', newDescription, (e as Error).message) + } + } + + title (): string | null { + if (!this.payload) { + throw new Error('no payload') + } + + return this.payload.title || null + } + + coworker (): boolean { + if (!this.payload) { + throw new Error('no payload') + } + + return !!this.payload.coworker + } + + /** + * Check if contact is friend + * + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @returns {boolean | null} + * + *
True for friend of the bot
+ * False for not friend of the bot, null for unknown. + * @example + * const isFriend = contact.friend() + */ + friend (): undefined | boolean { + log.verbose('Contact', 'friend()') + return this.payload?.friend + } + + /** + * Enum for ContactType + * @enum {number} + * @property {number} Unknown - ContactType.Unknown (0) for Unknown + * @property {number} Personal - ContactType.Personal (1) for Personal + * @property {number} Official - ContactType.Official (2) for Official + */ + + /** + * Return the type of the Contact + * > Tips: ContactType is enum here.
+ * @returns {ContactType.Unknown | ContactType.Personal | ContactType.Official} + * + * @example + * const bot = new Wechaty() + * await bot.start() + * const isOfficial = contact.type() === bot.Contact.Type.Official + */ + type (): PUPPET.types.Contact { + if (!this.payload) { + throw new Error('no payload') + } + return this.payload.type + } + + /** + * @ignore + * TODO: Check if the contact is star contact. + * + * @returns {boolean | null} - True for star friend, False for no star friend. + * @example + * const isStar = contact.star() + */ + star (): undefined | boolean { + return this.payload?.star + } + + /** + * Contact gender + * > Tips: ContactGender is enum here.
+ * + * @returns {ContactGender.Unknown | ContactGender.Male | ContactGender.Female} + * @example + * const gender = contact.gender() === bot.Contact.Gender.Male + */ + gender (): PUPPET.types.ContactGender { + return this.payload + ? this.payload.gender + : PUPPET.types.ContactGender.Unknown + } + + /** + * Get the region 'province' from a contact + * + * @returns {string | null} + * @example + * const province = contact.province() + */ + province (): undefined | string { + return this.payload?.province + } + + /** + * Get the region 'city' from a contact + * + * @returns {string | null} + * @example + * const city = contact.city() + */ + city (): undefined | string { + return this.payload?.city + } + + /** + * Get avatar picture file stream + * + * @returns {Promise} + * @example + * // Save avatar to local file like `1-name.jpg` + * + * const file = await contact.avatar() + * const name = file.name + * await file.toFile(name, true) + * console.log(`Contact: ${contact.name()} with avatar file: ${name}`) + */ + async avatar (): Promise { + log.verbose('Contact', 'avatar()') + + const fileBox = await this.wechaty.puppet.contactAvatar(this.id) + return fileBox + } + + /** + * Get all tags of contact + * + * @returns {Promise} + * @example + * const tags = await contact.tags() + */ + async tags (): Promise { + log.verbose('Contact', 'tags() for %s', this) + + try { + const tagIdList = await this.wechaty.puppet.tagContactList(this.id) + const tagList = tagIdList.map(id => this.wechaty.Tag.load(id)) + return tagList + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'tags() exception: %s', (e as Error).message) + return [] + } + } + + /** + * Force reload data for Contact, Sync data from low-level API again. + * + * @returns {Promise} + * @example + * await contact.sync() + */ + async sync (): Promise { + await this.wechaty.puppet.contactPayloadDirty(this.id) + await this.ready(true) + } + + /** + * `ready()` is For FrameWork ONLY! + * + * Please not to use `ready()` at the user land. + * If you want to sync data, use `sync()` instead. + * + * @ignore + */ + async ready ( + forceSync = false, + ): Promise { + log.silly('Contact', 'ready() @ %s with id="%s"', this.wechaty.puppet, this.id) + + if (!forceSync && this.isReady()) { // already ready + log.silly('Contact', 'ready() isReady() true') + return + } + + try { + this.payload = await this.wechaty.puppet.contactPayload(this.id) + // log.silly('Contact', `ready() this.wechaty.puppet.contactPayload(%s) resolved`, this) + + } catch (e) { + this.wechaty.emitError(e) + log.verbose('Contact', 'ready() this.wechaty.puppet.contactPayload(%s) exception: %s', + this.id, + (e as Error).message, + ) + throw e + } + } + + async readMark (hasRead: boolean): Promise + async readMark (): Promise + + /** + * Mark the conversation as read + * @param { undefined | boolean } hasRead + * + * @example + * const bot = new Wechaty() + * const contact = await bot.Contact.find({name: 'xxx'}) + * await contact.readMark() + */ + + async readMark (hasRead?: boolean): Promise { + try { + if (typeof hasRead === 'undefined') { + return this.wechaty.puppet.conversationReadMark(this.id) + } else { + await this.wechaty.puppet.conversationReadMark(this.id, hasRead) + } + } catch (e) { + this.wechaty.emitError(e) + log.error('Contact', 'readMark() exception: %s', (e as Error).message) + } + } + + /** + * @ignore + */ + isReady (): boolean { + return !!(this.payload && this.payload.name) + } + + /** + * Check if contact is self + * + * @returns {boolean} True for contact is self, False for contact is others + * @example + * const isSelf = contact.self() + */ + self (): boolean { + return this.id === this.wechaty.puppet.currentUserId + } + + /** + * Get the handle from a contact. + * + * > A Twitter handle is the username that appears at the end of your unique Twitter URL. + * + * Sometimes cannot get handle due to the puppet implementation. + * + * @ignore + * @returns {string | null} + * @example + * const handle = contact.handle() + */ + handle (): undefined | string { + return this.payload?.handle + } + + /** + * Huan(202203): `weixin()` will be removed in v2.0 + * @link https://github.com/wechaty/puppet/issues/181 + * @deprecated use `handle()` instead + */ + weixin (): undefined | string { + log.warn('Contact', 'weixin() is deprecated, use `handle()` instead.') + console.error(new Error().stack) + return this.payload?.weixin + } + +} + +class ContactImplBase extends validationMixin(ContactMixin)() {} +interface ContactImplInterface extends ContactImplBase {} + +type ContactProtectedProperty = + | 'ready' + +type ContactInterface = Omit +class ContactImpl extends validationMixin(ContactImplBase)() {} + +type ContactConstructor = Constructor< + ContactImplInterface, + Omit +> + +export type { + ContactConstructor, + ContactProtectedProperty, + ContactInterface, +} +export { + ContactImpl, +} diff --git a/src/user-modules/delay.ts b/src/user-modules/delay.ts new file mode 100644 index 0000000..1628f1e --- /dev/null +++ b/src/user-modules/delay.ts @@ -0,0 +1,61 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type { Constructor } from 'clone-class' +import { validationMixin } from '../user-mixins/validation.js' + +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' + +class DelayMixin extends wechatifyMixinBase() { + + static create (milliseconds: number): DelayInterface { + return new DelayImpl(milliseconds) + } + + constructor ( + public milliseconds: number, + ) { + super() + } + + wait (): Promise { + return new Promise(resolve => { + setTimeout(resolve, this.milliseconds) + }) + } + +} + +class DelayImpl extends validationMixin(DelayMixin)() {} +interface DelayInterface extends DelayImpl {} + +type DelayConstructor = Constructor< + DelayInterface, + typeof DelayImpl +> + +export type { + DelayConstructor, + DelayInterface, +} +export { + DelayImpl, +} diff --git a/src/user-modules/favorite.ts b/src/user-modules/favorite.ts new file mode 100644 index 0000000..0765e68 --- /dev/null +++ b/src/user-modules/favorite.ts @@ -0,0 +1,89 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { log } from 'wechaty-puppet' +import type { Constructor } from 'clone-class' +import { validationMixin } from '../user-mixins/validation.js' + +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' +import type { TagInterface } from './tag.js' + +class FavoriteMixin extends wechatifyMixinBase() { + + static list (): FavoriteInterface[] { + return [] + } + + /** + * Get tags for all favorites + * + * @static + * @returns {Promise} + * @example + * const tags = await wechaty.Favorite.tags() + */ + static async tags (): Promise { + log.verbose('Favorite', 'static tags() for %s', this) + + // TODO: + // try { + // const tagIdList = await this.puppet.tagFavoriteList() + // const tagList = tagIdList.map(id => this.wechaty.Tag.load(id)) + // return tagList + // } catch (e) { + // log.error('Favorite', 'static tags() exception: %s', e.message) + // return [] + // } + return [] + } + + /* + * @hideconstructor + */ + constructor () { + super() + } + + async tags (): Promise { + // TODO: implmente this method + return [] + } + + async findAll () { + // + } + +} + +class FavoriteImpl extends validationMixin(FavoriteMixin)() {} +interface FavoriteInterface extends FavoriteImpl {} +type FavoriteConstructor = Constructor< + FavoriteInterface, + typeof FavoriteImpl +> + +export type { + FavoriteConstructor, + FavoriteInterface, +} +export { + FavoriteImpl, +} diff --git a/src/user-modules/friendship.ts b/src/user-modules/friendship.ts new file mode 100644 index 0000000..aa40f83 --- /dev/null +++ b/src/user-modules/friendship.ts @@ -0,0 +1,441 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { EventEmitter } from 'events' +import * as PUPPET from 'wechaty-puppet' +import { log } from 'wechaty-puppet' +import type { Constructor } from 'clone-class' + +import { + retryPolicy, +} from '../pure-functions/mod.js' + +import type { + Accepter, +} from '../schemas/acceptable.js' + +import type { + ContactInterface, + ContactImpl, +} from './contact.js' +import type { + RoomInterface, +} from './room.js' +import { + wechatifyMixin, +} from '../user-mixins/wechatify.js' +import { validationMixin } from '../user-mixins/validation.js' + +interface FriendshipAddOptionsObject { + room?: RoomInterface, + contact?: ContactInterface, + hello?: string, +} + +type FriendshipAddOptions = string | FriendshipAddOptionsObject + +const MixinBase = wechatifyMixin( + EventEmitter, +) + +/** + * Send, receive friend request, and friend confirmation events. + * + * 1. send request + * 2. receive request(in friend event) + * 3. confirmation friendship(friend event) + * + * [Examples/Friend-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/friend-bot.ts} + */ +class FriendshipMixin extends MixinBase implements Accepter { + + static Type = PUPPET.types.Friendship + + /** + * @ignore + */ + static load ( + id : string, + ): FriendshipInterface { + const newFriendship = new this(id) + return newFriendship + } + + /** + * Search a Friend by phone or weixin. + * + * The best practice is to search friend request once per minute. + * Remember not to do this too frequently, or your account may be blocked. + * + * @param {FriendshipSearchCondition} condition - Search friend by phone or weixin. + * @returns {Promise} + * + * @example + * const friend_phone = await bot.Friendship.search({phone: '13112341234'}) + * const friend_weixin = await bot.Friendship.search({weixin: 'weixin_account'}) + * + * console.log(`This is the new friend info searched by phone : ${friend_phone}`) + * await bot.Friendship.add(friend_phone, 'hello') + * + */ + static async search ( + queryFilter : PUPPET.filters.Friendship, + ): Promise { + log.verbose('Friendship', 'static search("%s")', + JSON.stringify(queryFilter), + ) + const contactId = await this.wechaty.puppet.friendshipSearch(queryFilter) + + if (!contactId) { + return undefined + } + + const contact = await this.wechaty.Contact.find({ id: contactId }) + return contact + } + + /** + * Send a Friend Request to a `contact` with message `hello`. + * + * The best practice is to send friend request once per minute. + * Remeber not to do this too frequently, or your account may be blocked. + * + * @param {ContactInterface} contact - Send friend request to contact + * @param {FriendshipAddOptions} options - The friend request content + * @returns {Promise} + * + * @example + * const contact = await bot.Friendship.search({phone: '13112341234'}) + * await bot.Friendship.add(contact, 'Nice to meet you! I am wechaty bot!') + * + * const memberList = await room.memberList() + * for (let i = 0; i < memberList.length; i++) { + * await bot.Friendship.add(member, { + * room: room, + * hello: `Nice to meet you! I am wechaty bot from room: ${await room.topic()}!`, + * }) + * } + * + */ + static async add ( + contact : ContactInterface, + options : FriendshipAddOptions, + ): Promise { + log.verbose('Friendship', 'static add(%s, %s)', + contact.id, + typeof options === 'string' ? options : options.hello, + ) + + if (typeof options === 'string') { + log.warn('Friendship', 'the params hello is deprecated in the next version, please put the attr hello into options object, e.g. { hello: "xxxx" }') + await this.wechaty.puppet.friendshipAdd(contact.id, { hello: options }) + } else { + const friendOption: PUPPET.types.FriendshipAddOptions = { + contactId: options.contact?.id, + hello: options.hello, + roomId: options.room && options.room.id, + } + await this.wechaty.puppet.friendshipAdd(contact.id, friendOption) + } + } + + static async del ( + contact: ContactInterface, + ): Promise { + log.verbose('Friendship', 'static del(%s)', contact.id) + throw new Error('to be implemented') + } + + /** + * + * Instance Properties + * + */ + + /** + * @ignore + */ + payload?: PUPPET.payloads.Friendship + + /* + * @hideconstructor + */ + constructor ( + public readonly id: string, + ) { + super() + log.verbose('Friendship', 'constructor(id=%s)', id) + } + + override toString () { + if (!this.payload) { + return this.constructor.name + } + + return [ + 'Friendship#', + PUPPET.types.Friendship[this.payload.type], + '<', + this.payload.contactId, + '>', + ].join('') + } + + isReady (): boolean { + return !!this.payload && (Object.keys(this.payload).length > 0) + } + + /** + * no `dirty` support because Friendship has no rawPayload(yet) + * @ignore + */ + async ready (): Promise { + if (this.isReady()) { + return + } + + this.payload = await this.wechaty.puppet.friendshipPayload(this.id) + + // if (!this.#payload) { + // throw new Error('no payload') + // } + + await (this.contact() as ContactImpl).ready() + } + + /** + * Accept Friend Request + * + * @returns {Promise} + * + * @example + * const bot = new Wechaty() + * bot.on('friendship', async friendship => { + * try { + * console.log(`received friend event.`) + * switch (friendship.type()) { + * + * // 1. New Friend Request + * + * case Friendship.Type.Receive: + * await friendship.accept() + * break + * + * // 2. Friend Ship Confirmed + * + * case Friendship.Type.Confirm: + * console.log(`friend ship confirmed`) + * break + * } + * } catch (e) { + * console.error(e) + * } + * } + * .start() + */ + async accept (): Promise { + log.verbose('Friendship', 'accept()') + + if (!this.payload) { + throw new Error('no payload') + } + + if (this.payload.type !== PUPPET.types.Friendship.Receive) { + throw new Error('accept() need type to be FriendshipType.Receive, but it got a ' + FriendshipImpl.Type[this.payload.type]) + } + + log.silly('Friendship', 'accept() to %s', this.payload.contactId) + + await this.wechaty.puppet.friendshipAccept(this.id) + + const contact = this.contact() + + try { + const doSync = async () => { + await (contact as ContactImpl).ready() + if (!contact.isReady()) { + throw new Error('Friendship.accept() contact.ready() not ready') + } + log.verbose('Friendship', 'accept() with contact %s ready()', contact.name()) + } + + await retryPolicy.execute(doSync) + + } catch (e) { + this.wechaty.emitError(e) + log.warn('Friendship', 'accept() contact %s not ready because of %s', contact, (e && (e as Error).message) || e) + // console.error(e) + } + + // try to fix issue #293 - https://github.com/wechaty/wechaty/issues/293 + await contact.sync() + } + + /** + * Get verify message from + * + * @returns {string} + * @example If request content is `ding`, then accept the friendship + * const bot = new Wechaty() + * bot.on('friendship', async friendship => { + * try { + * console.log(`received friend event from ${friendship.contact().name()}`) + * if (friendship.type() === Friendship.Type.Receive && friendship.hello() === 'ding') { + * await friendship.accept() + * } + * } catch (e) { + * console.error(e) + * } + * } + * .start() + */ + hello (): string { + if (!this.payload) { + throw new Error('no payload') + } + return this.payload.hello || '' + } + + /** + * Get the contact from friendship + * + * @returns {ContactInterface} + * @example + * const bot = new Wechaty() + * bot.on('friendship', async friendship => { + * const contact = friendship.contact() + * const name = contact.name() + * console.log(`received friend event from ${name}`) + * } + * .start() + */ + contact (): ContactInterface { + if (!this.payload) { + throw new Error('no payload') + } + + const contact = (this.wechaty.Contact as typeof ContactImpl).load(this.payload.contactId) + return contact + } + + /** + * Return the Friendship Type + * > Tips: FriendshipType is enum here.
+ * - FriendshipType.Unknown
+ * - FriendshipType.Confirm
+ * - FriendshipType.Receive
+ * - FriendshipType.Verify
+ * + * @returns {FriendshipType} + * + * @example If request content is `ding`, then accept the friendship + * const bot = new Wechaty() + * bot.on('friendship', async friendship => { + * try { + * if (friendship.type() === Friendship.Type.Receive && friendship.hello() === 'ding') { + * await friendship.accept() + * } + * } catch (e) { + * console.error(e) + * } + * } + * .start() + */ + type (): PUPPET.types.Friendship { + return this.payload + ? this.payload.type + : PUPPET.types.Friendship.Unknown + } + + /** + * get friendShipPayload Json + * @returns {FriendshipPayload} + * + * @example + * const bot = new Wechaty() + * bot.on('friendship', async friendship => { + * try { + * // JSON.stringify(friendship) as well. + * const payload = await friendship.toJSON() + * } catch (e) { + * console.error(e) + * } + * } + * .start() + */ + toJSON (): string { + log.verbose('Friendship', 'toJSON()') + + if (!this.isReady()) { + throw new Error(`Friendship<${this.id}> needs to be ready. Please call ready() before toJSON()`) + } + return JSON.stringify(this.payload) + } + + /** + * create friendShip by friendshipJson + * @example + * const bot = new Wechaty() + * bot.start() + * + * const payload = '{...}' // your saved JSON payload + * const friendship = bot.FriendShip.fromJSON(friendshipFromDisk) + * await friendship.accept() + */ + static async fromJSON ( + payload: string | PUPPET.payloads.Friendship, + ): Promise { + log.verbose('Friendship', 'static fromJSON(%s)', + typeof payload === 'string' + ? payload + : JSON.stringify(payload), + ) + + if (typeof payload === 'string') { + payload = JSON.parse(payload) as PUPPET.payloads.Friendship + } + + /** + * Set the payload back to the puppet for future use + */ + await this.wechaty.puppet.friendshipPayload(payload.id, payload) + + const instance = this.wechaty.Friendship.load(payload.id) + await instance.ready() + + return instance + } + +} + +class FriendshipImpl extends validationMixin(FriendshipMixin)() {} +interface FriendshipInterface extends FriendshipImpl {} + +type FriendshipConstructor = Constructor< + FriendshipInterface, + typeof FriendshipImpl +> + +export type { + FriendshipConstructor, + FriendshipInterface, +} +export { + FriendshipImpl, +} diff --git a/src/user-modules/image.ts b/src/user-modules/image.ts new file mode 100644 index 0000000..8fa9556 --- /dev/null +++ b/src/user-modules/image.ts @@ -0,0 +1,91 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import * as PUPPET from 'wechaty-puppet' +import type { + FileBoxInterface, +} from 'file-box' +import type { Constructor } from 'clone-class' +import { validationMixin } from '../user-mixins/validation.js' +import { log } from '../config.js' + +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' + +class ImageMixin extends wechatifyMixinBase() { + + static create (id: string): ImageInterface { + log.verbose('Image', 'static create(%s)', id) + + const image = new this(id) + return image + } + + constructor ( + public id: string, + ) { + super() + log.verbose('Image', 'constructor(%s)', id) + } + + async thumbnail (): Promise { + log.verbose('Image', 'thumbnail() for id: "%s"', this.id) + const fileBox = await this.wechaty.puppet.messageImage( + this.id, + PUPPET.types.Image.Thumbnail, + ) + return fileBox + } + + async hd (): Promise { + log.verbose('Image', 'hd() for id: "%s"', this.id) + const fileBox = await this.wechaty.puppet.messageImage( + this.id, + PUPPET.types.Image.HD, + ) + return fileBox + } + + async artwork (): Promise { + log.verbose('Image', 'artwork() for id: "%s"', this.id) + const fileBox = await this.wechaty.puppet.messageImage( + this.id, + PUPPET.types.Image.Artwork, + ) + return fileBox + } + +} + +class ImageImpl extends validationMixin(ImageMixin)() {} +interface ImageInterface extends ImageImpl { } + +type ImageConstructor = Constructor< + ImageInterface, + typeof ImageImpl +> + +export type { + ImageConstructor, + ImageInterface, +} +export { + ImageImpl, +} diff --git a/src/user-modules/location.ts b/src/user-modules/location.ts new file mode 100644 index 0000000..3fe1997 --- /dev/null +++ b/src/user-modules/location.ts @@ -0,0 +1,103 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type * as PUPPET from 'wechaty-puppet' +import type { Constructor } from 'clone-class' +import { log } from '../config.js' + +import { validationMixin } from '../user-mixins/validation.js' +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' + +class LocationMixin extends wechatifyMixinBase() { + + /** + * + * Create + * @param poi string A point of interest (POI) is a specific point location that someone may find useful or interesting. + * See: https://en.wikipedia.org/wiki/Point_of_interest + */ + static async create (poi: string): Promise { + log.verbose('Location', 'create(%s)', poi) + + const payload: PUPPET.payloads.Location = { + accuracy : 15, // in meters + address : 'ๅŒ—ไบฌๅธ‚ๅŒ—ไบฌๅธ‚ๆตทๆท€ๅŒบ45 Chengfu Rd', + latitude : 39.995120999999997, + longitude : 116.334154, + name : poi, // Huan(202109): FIXME: generate payload by poi + } + + return new this(payload) + } + + /* + * @hideconstructor + */ + constructor ( + public readonly payload: PUPPET.payloads.Location, + ) { + super() + log.verbose('Location', 'constructor()') + // Huan(202110): it is ok to create a raw one without wechaty instance + // guardWechatifyClass.call(this, Location) + } + + override toString (): string { + return `Location<${this.payload.name}>` + } + + address (): string { + return this.payload.address + } + + latitude (): number { + return this.payload.latitude + } + + longitude (): number { + return this.payload.longitude + } + + name (): string { + return this.payload.name + } + + accuracy (): number { + return this.payload.accuracy + } + +} + +class LocationImpl extends validationMixin(LocationMixin)() {} +interface LocationInterface extends LocationImpl {} + +type LocationConstructor = Constructor< + LocationInterface, + typeof LocationImpl +> + +export type { + LocationConstructor, + LocationInterface, +} +export { + LocationImpl, +} diff --git a/src/user-modules/message.spec.ts b/src/user-modules/message.spec.ts new file mode 100755 index 0000000..7bcedf8 --- /dev/null +++ b/src/user-modules/message.spec.ts @@ -0,0 +1,124 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { + test, + sinon, +} from 'tstest' + +import * as PUPPET from 'wechaty-puppet' +import { PuppetMock } from 'wechaty-puppet-mock' +import { WechatyBuilder } from '../wechaty-builder.js' +import type { + MessageImpl, + MessageProtectedProperty, +} from './message.js' + +test('recalled()', async t => { + + const EXPECTED_RECALL_MESSAGE_ID = 'message-id-1' + const EXPECTED_RECALLED_MESSAGE_ID = 'message-id-2' + const EXPECTED_MESSAGE_TIMESTAMP = new Date().getTime() + const EXPECTED_ROOM_TOPIC = 'topic' + const EXPECTED_ROOM_ID = 'room-id' + const EXPECTED_TALKER_CONTACT_ID = 'talker-contact-id' + const EXPECTED_LISTENER_CONTACT_ID = 'listener-contact-id' + + const sandbox = sinon.createSandbox() + + const puppet = new PuppetMock() + const wechaty = WechatyBuilder.build({ puppet }) + + await wechaty.start() + + sandbox.stub(puppet, 'messagePayload').callsFake(async (id) => { + await new Promise(resolve => setImmediate(resolve)) + if (id === EXPECTED_RECALL_MESSAGE_ID) { + return { + id: EXPECTED_RECALL_MESSAGE_ID, + talkerId: EXPECTED_TALKER_CONTACT_ID, + text: EXPECTED_RECALLED_MESSAGE_ID, + timestamp: EXPECTED_MESSAGE_TIMESTAMP, + type: PUPPET.types.Message.Recalled, + } as PUPPET.payloads.Message + } else { + return { + id: EXPECTED_RECALLED_MESSAGE_ID, + listenerId: EXPECTED_LISTENER_CONTACT_ID, + roomId: EXPECTED_ROOM_ID, + talkerId: EXPECTED_TALKER_CONTACT_ID, + text: '', + timestamp: EXPECTED_MESSAGE_TIMESTAMP, + type: PUPPET.types.Message.Text, + } as PUPPET.payloads.Message + } + }) + sandbox.stub(puppet, 'roomPayload').callsFake(async () => { + await new Promise(resolve => setImmediate(resolve)) + return { + topic: EXPECTED_ROOM_TOPIC, + } as PUPPET.payloads.Room + }) + + sandbox.stub(puppet, 'roomMemberList').callsFake(async () => { + await new Promise((resolve) => setImmediate(resolve)) + return [ EXPECTED_TALKER_CONTACT_ID, EXPECTED_LISTENER_CONTACT_ID ] + }) + + sandbox.stub(puppet, 'contactPayload').callsFake(async (id: string) => { + await new Promise(setImmediate) + return { + id, + name: id, + } as PUPPET.payloads.Contact + }) + + const fakeIdSearcher = async (...args: any[]) => { + await new Promise(setImmediate) + return [ args[0].id ] + } + + sandbox.stub(puppet, 'messageSearch').callsFake(fakeIdSearcher) + sandbox.stub(puppet, 'contactSearch').callsFake(fakeIdSearcher) + + await puppet.login(EXPECTED_LISTENER_CONTACT_ID) + + const message = await wechaty.Message.find({ id: EXPECTED_RECALL_MESSAGE_ID }) + if (!message) { + throw new Error('no message for id: ' + EXPECTED_RECALL_MESSAGE_ID) + } + const recalledMessage = await message.toRecalled() + t.ok(recalledMessage, 'recalled message should exist.') + t.equal(recalledMessage!.id, EXPECTED_RECALLED_MESSAGE_ID, 'Recalled message should have the right id.') + t.equal(recalledMessage!.talker().id, EXPECTED_TALKER_CONTACT_ID, 'Recalled message should have the right from contact id.') + t.equal(recalledMessage!.listener()!.id, EXPECTED_LISTENER_CONTACT_ID, 'Recalled message should have the right to contact id.') + t.equal(recalledMessage!.room()!.id, EXPECTED_ROOM_ID, 'Recalled message should have the right room id.') + + await wechaty.stop() +}) + +test('ProtectedProperties', async t => { + type NotExistInWechaty = Exclude + type NotExistTest = NotExistInWechaty extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Wechaty properties for every protected property') +}) diff --git a/src/user-modules/message.ts b/src/user-modules/message.ts new file mode 100644 index 0000000..f2bb04a --- /dev/null +++ b/src/user-modules/message.ts @@ -0,0 +1,1113 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { EventEmitter } from 'events' +import * as PUPPET from 'wechaty-puppet' +import type { + FileBoxInterface, +} from 'file-box' + +import type { Constructor } from 'clone-class' + +import { escapeRegExp } from '../pure-functions/escape-regexp.js' +import { timestampToDate } from '../pure-functions/timestamp-to-date.js' + +import { + log, + AT_SEPARATOR_REGEX, +} from '../config.js' +import type { + SayableSayer, + Sayable, +} from '../sayable/mod.js' +import { + messageToSayable, +} from '../sayable/mod.js' + +import { + wechatifyMixin, +} from '../user-mixins/wechatify.js' + +import type { + ContactInterface, + ContactImpl, +} from './contact.js' +import type { + RoomInterface, + RoomImpl, +} from './room.js' +import type { + UrlLinkInterface, +} from './url-link.js' +import type { + MiniProgramInterface, +} from './mini-program.js' +import type { + ImageInterface, +} from './image.js' +import { + PostInterface, + PostImpl, +} from './post.js' +import { + LocationInterface, + LocationImpl, +} from './location.js' + +import { validationMixin } from '../user-mixins/validation.js' +import type { ContactSelfImpl } from './contact-self.js' + +const MixinBase = wechatifyMixin( + EventEmitter, +) + +/** + * All wechat messages will be encapsulated as a Message. + * + * [Examples/Ding-Dong-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts} + */ +class MessageMixin extends MixinBase implements SayableSayer { + + /** + * + * Static Properties + * + */ + + /** + * @ignore + */ + static readonly Type = PUPPET.types.Message + + /** + * Find message in cache + */ + static async find ( + query : string | PUPPET.filters.Message, + ): Promise { + log.verbose('Message', 'find(%s)', JSON.stringify(query)) + + if (typeof query === 'string') { + query = { text: query } + } + + const messageList = await this.findAll(query) + if (messageList.length < 1) { + return undefined + } + + if (messageList.length > 1) { + log.warn('Message', 'findAll() got more than one(%d) result', messageList.length) + } + + return messageList[0]! + } + + /** + * Find messages in cache + */ + static async findAll ( + query? : PUPPET.filters.Message, + ): Promise { + log.verbose('Message', 'findAll(%s)', JSON.stringify(query) || '') + + // Huan(202111): { id } query has been optimized in the PuppetAbstract class + + const invalidDict: { [id: string]: true } = {} + + try { + const MessageIdList = await this.wechaty.puppet.messageSearch(query) + const messageList = MessageIdList.map(id => this.load(id)) + await Promise.all( + messageList.map( + message => message.ready() + .catch(e => { + log.warn('Room', 'findAll() message.ready() rejection: %s', e) + invalidDict[message.id] = true + }), + ), + ) + + return messageList.filter(message => !invalidDict[message.id]) + + } catch (e) { + this.wechaty.emitError(e) + log.warn('Message', 'findAll() rejected: %s', (e as Error).message) + return [] // fail safe + } + } + + /** + * Create a Mobile Terminated Message + * @ignore + * "mobile originated" or "mobile terminated" + * https://www.tatango.com/resources/video-lessons/video-mo-mt-sms-messaging/ + */ + static load (id: string): MessageImplInterface { + log.verbose('Message', 'static load(%s)', id) + + /** + * Must NOT use `Message` at here + * MUST use `this` at here + * + * because the class will be `cloneClass`-ed + */ + const msg = new this(id) + + return msg + } + + /** + * + * Instance Properties + * @hidden + * + */ + payload?: PUPPET.payloads.Message + + /** + * @hideconstructor + */ + constructor ( + public readonly id: string, + ) { + super() + log.verbose('Message', 'constructor(%s) for class %s', + id || '', + this.constructor.name, + ) + } + + /** + * @ignore + */ + override toString () { + if (!this.payload) { + return this.constructor.name + } + + let talker + try { + talker = this.talker() + } catch (e) { + talker = (e as Error).message + } + + const msgStrList = [ + 'Message', + `#${PUPPET.types.Message[this.type()]}`, + '[', + '๐Ÿ—ฃ', + talker, + this.room() + ? '@๐Ÿ‘ฅ' + this.room() + : '', + ']', + ] + + if (this.type() === PUPPET.types.Message.Text + || this.type() === PUPPET.types.Message.Unknown + ) { + msgStrList.push(`\t${this.text().substr(0, 70)}`) + } else { + log.silly('Message', 'toString() for message type: %s(%s)', PUPPET.types.Message[this.type()], this.type()) + // if (!this.#payload) { + // throw new Error('no payload') + // } + } + + return msgStrList.join('') + } + + conversation (): ContactInterface | RoomInterface { + if (this.room()) { + return this.room()! + } else { + return this.talker() + } + } + + /** + * Get the talker of a message. + * @returns {ContactInterface} + * @example + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const talker = msg.talker() + * const text = msg.text() + * const room = msg.room() + * if (room) { + * const topic = await room.topic() + * console.log(`Room: ${topic} Contact: ${talker.name()} Text: ${text}`) + * } else { + * console.log(`Contact: ${talker.name()} Text: ${text}`) + * } + * }) + * .start() + */ + talker (): ContactInterface { + if (!this.payload) { + throw new Error('no payload') + } + + let talkerId = this.payload.talkerId + if (!talkerId) { + /** + * `fromId` is deprecated, this code block will be removed in v2.0 + */ + if (this.payload.fromId) { + talkerId = this.payload.fromId + } else { + throw new Error('no talkerId found for talker') + } + + log.warn('Message', 'talker() payload.talkerId not exist! See: https://github.com/wechaty/puppet/issues/187') + console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) + console.error(new Error().stack) + } + + let talker + if (this.wechaty.isLoggedIn && talkerId === this.wechaty.puppet.currentUserId) { + talker = (this.wechaty.ContactSelf as typeof ContactSelfImpl).load(talkerId) + } else { + talker = (this.wechaty.Contact as typeof ContactImpl).load(talkerId) + } + return talker + } + + /** + * @depreacated Use `message.talker()` to replace `message.from()` + * https://github.com/wechaty/wechaty/issues/2094 + */ + from (): undefined | ContactInterface { + log.warn('Message', 'from() is deprecated, use talker() instead. Call stack: %s', + new Error().stack, + ) + try { + return this.talker() + } catch (e) { + this.wechaty.emitError(e) + return undefined + } + } + + /** + * Get the destination of the message + * Message.to() will return null if a message is in a room, use Message.room() to get the room. + * @returns {(ContactInterface|null)} + * @deprecated use `listener()` instead + */ + to (): undefined | ContactInterface { + // Huan(202108): I want to deprecate this method name in the future, + // and use `message.listener()` to replace it. + return this.listener() + } + + /** + * Get the destination of the message + * Message.listener() will return null if a message is in a room, + * use Message.room() to get the room. + * @returns {(undefined | ContactInterface)} + */ + listener (): undefined | ContactInterface { + if (!this.payload) { + throw new Error('no payload') + } + + let listenerId = this.payload.listenerId + + if (!listenerId && this.payload.toId) { + /** + * `toId` is deprecated, this code block will be removed in v2.0 + */ + listenerId = this.payload.toId + + log.warn('Message', 'listener() payload.listenerId should be set! See: https://github.com/wechaty/puppet/issues/187') + console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) + console.error(new Error().stack) + } + + if (!listenerId) { + return undefined + } + + let listener + if (listenerId === this.wechaty.puppet.currentUserId) { + listener = (this.wechaty.ContactSelf as typeof ContactSelfImpl).load(listenerId) + } else { + listener = (this.wechaty.Contact as typeof ContactImpl).load(listenerId) + } + return listener + } + + /** + * Get the room from the message. + * If the message is not in a room, then will return `null` + * + * @returns {(RoomInterface | null)} + * @example + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const contact = msg.from() + * const text = msg.text() + * const room = msg.room() + * if (room) { + * const topic = await room.topic() + * console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`) + * } else { + * console.log(`Contact: ${contact.name()} Text: ${text}`) + * } + * }) + * .start() + */ + room (): undefined | RoomInterface { + if (!this.payload) { + throw new Error('no payload') + } + const roomId = this.payload.roomId + if (!roomId) { + return undefined + } + + const room = (this.wechaty.Room as typeof RoomImpl).load(roomId) + return room + } + + /** + * Get the text content of the message + * + * @returns {string} + * @example + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const contact = msg.from() + * const text = msg.text() + * const room = msg.room() + * if (room) { + * const topic = await room.topic() + * console.log(`Room: ${topic} Contact: ${contact.name()} Text: ${text}`) + * } else { + * console.log(`Contact: ${contact.name()} Text: ${text}`) + * } + * }) + * .start() + */ + text (): string { + if (!this.payload) { + throw new Error('no payload') + } + + return this.payload.text || '' + } + + /** + * Get the recalled message + * + * @example + * const bot = WechatyBuilder.build() + * bot + * .on('message', async m => { + * if (m.type() === PUPPET.types.Message.Recalled) { + * const recalledMessage = await m.toRecalled() + * console.log(`Message: ${recalledMessage} has been recalled.`) + * } + * }) + * .start() + */ + async toRecalled (): Promise { + if (this.type() !== PUPPET.types.Message.Recalled) { + throw new Error('Can not call toRecalled() on message which is not recalled type.') + } + /** + * Huan(202501): cannot recall why using `this.text()` as `messageId` here... + * however there should be a reason... + */ + const originalMessageId = this.text() + if (!originalMessageId) { + throw new Error('Can not find recalled message') + } + try { + const message = await this.wechaty.Message.find({ id: originalMessageId }) + if (message) { + return message + } + + } catch (e) { + this.wechaty.emitError(e) + log.verbose(`Can not retrieve the recalled message with id ${originalMessageId}.`) + } + return undefined + } + + /** + * Reply a Text or Media File message to the sender. + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @see {@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/ding-dong-bot.ts|Examples/ding-dong-bot} + * @param {(string | ContactInterface | FileBox | UrlLinkInterface | MiniProgramInterface | LocationInterface)} textOrContactOrFile + * send text, Contact, or file to bot.
+ * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file + * @param {(ContactInterface|ContactInterface[])} [mention] + * If this is a room message, when you set mention param, you can `@` Contact in the room. + * @returns {Promise} + * + * @example + * import { FileBox } from 'wechaty' + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * + * // 1. send Image + * + * if (/^ding$/i.test(m.text())) { + * const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + * await msg.say(fileBox) + * const message = await msg.say(fileBox) // only supported by puppet-padplus + * } + * + * // 2. send Text + * + * if (/^dong$/i.test(m.text())) { + * await msg.say('ding') + * const message = await msg.say('ding') // only supported by puppet-padplus + * } + * + * // 3. send Contact + * + * if (/^lijiarui$/i.test(m.text())) { + * const contactCard = await bot.Contact.find({name: 'lijiarui'}) + * if (!contactCard) { + * console.log('not found') + * return + * } + * await msg.say(contactCard) + * const message = await msg.say(contactCard) // only supported by puppet-padplus + * } + * + * // 4. send Link + * + * if (/^link$/i.test(m.text())) { + * const linkPayload = new UrlLink ({ + * description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + * thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + * title : 'Welcome to Wechaty', + * url : 'https://github.com/wechaty/wechaty', + * }) + * await msg.say(linkPayload) + * const message = await msg.say(linkPayload) // only supported by puppet-padplus + * } + * + * // 5. send MiniProgram + * + * if (/^miniProgram$/i.test(m.text())) { + * const miniProgramPayload = new MiniProgram ({ + * username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + * appid : '', //optional, get from mp.weixin.qq.com + * title : '', //optional + * pagepath : '', //optional + * description : '', //optional + * thumbnailurl : '', //optional + * }) + * await msg.say(miniProgramPayload) + * const message = await msg.say(miniProgramPayload) // only supported by puppet-padplus + * } + * + * // 6. send Location + * if (/^location$/i.test(m.text())) { + * const location = new Location ({ + * accuracy : 15, + * address : 'ๅŒ—ไบฌๅธ‚ๅŒ—ไบฌๅธ‚ๆตทๆท€ๅŒบ45 Chengfu Rd', + * latitude : 39.995120999999997, + * longitude : 116.334154, + * name : 'ไธœๅ‡ไนกไบบๆฐ‘ๆ”ฟๅบœ(ๆตทๆท€ๅŒบๆˆๅบœ่ทฏ45ๅท)', + * }) + * await contact.say(location) + * const msg = await msg.say(location) + * } + * }) + * .start() + */ + async say ( + sayable: Sayable, + ): Promise { + log.verbose('Message', 'say(%s)', sayable) + + const talker = this.talker() + const room = this.room() + + if (room) { + return room.say(sayable) + } else { + return talker.say(sayable) + } + } + + /** + * Recall a message. + * > Tips: + * @returns {Promise} + * + * @example + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const recallMessage = await msg.say('123') + * if (recallMessage) { + * const isSuccess = await recallMessage.recall() + * } + * }) + */ + + async recall (): Promise { + log.verbose('Message', 'recall()') + const isSuccess = await this.wechaty.puppet.messageRecall(this.id) + return isSuccess + } + + /** + * Get the type from the message. + * > Tips: PUPPET.types.Message is Enum here.
+ * - PUPPET.types.Message.Unknown
+ * - PUPPET.types.Message.Attachment
+ * - PUPPET.types.Message.Audio
+ * - PUPPET.types.Message.Contact
+ * - PUPPET.types.Message.Emoticon
+ * - PUPPET.types.Message.Image
+ * - PUPPET.types.Message.Text
+ * - PUPPET.types.Message.Video
+ * - PUPPET.types.Message.Url
+ * @returns {PUPPET.types.Message} + * + * @example + * const bot = new Wechaty() + * if (message.type() === bot.Message.Type.Text) { + * console.log('This is a text message') + * } + */ + type (): PUPPET.types.Message { + if (!this.payload) { + throw new Error('no payload') + } + return this.payload.type || PUPPET.types.Message.Unknown + } + + /** + * Check if a message is sent by self. + * + * @returns {boolean} - Return `true` for send from self, `false` for send from others. + * @example + * if (message.self()) { + * console.log('this message is sent by myself!') + * } + */ + self (): boolean { + try { + const talker = this.talker() + + return talker.id === this.wechaty.puppet.currentUserId + } catch (e) { + this.wechaty.emitError(e) + log.error('Message', 'self() rejection: %s', (e as Error).message) + return false + } + } + + /** + * + * Get message mentioned contactList. + * + * Message event table as follows + * + * | | Web | Mac PC Client | iOS Mobile | android Mobile | + * | :--- | :--: | :----: | :---: | :---: | + * | [You were mentioned] tip ([ๆœ‰ไบบ@ๆˆ‘]็š„ๆ็คบ) | โœ˜ | โˆš | โˆš | โˆš | + * | Identify magic code (8197) by copy & paste in mobile | โœ˜ | โˆš | โˆš | โœ˜ | + * | Identify magic code (8197) by programming | โœ˜ | โœ˜ | โœ˜ | โœ˜ | + * | Identify two contacts with the same roomAlias by [You were mentioned] tip | โœ˜ | โœ˜ | โˆš | โˆš | + * + * @returns {Promise} - Return message mentioned contactList + * + * @example + * const contactList = await message.mentionList() + * console.log(contactList) + */ + async mentionList (): Promise { + log.verbose('Message', 'mentionList()') + + const room = this.room() + if (this.type() !== PUPPET.types.Message.Text || !room) { + return [] + } + + /** + * 1. Use mention list if mention list is available + */ + if (this.payload + && 'mentionIdList' in this.payload + && Array.isArray(this.payload.mentionIdList) + ) { + const idToContact = (id: string) => this.wechaty.Contact.find({ id }) + const allContact = await Promise.all( + this.payload.mentionIdList + .map(idToContact), + ) + // remove `undefined` types because we use a `filter(Boolean)` + return allContact.filter(Boolean) as ContactInterface[] + } + + /** + * 2. Otherwise, process the message and get the mention list + */ + + /** + * define magic code `8197` to identify @xxx + * const AT_SEPARATOR = String.fromCharCode(8197) + */ + const atList = this.text().split(AT_SEPARATOR_REGEX) + // console.log('atList: ', atList) + if (atList.length === 0) return [] + + // Using `filter(e => e.indexOf('@') > -1)` to filter the string without `@` + const rawMentionList = atList + .filter(str => str.includes('@')) + .map(str => multipleAt(str)) + + // convert 'hello@a@b@c' to [ 'c', 'b@c', 'a@b@c' ] + function multipleAt (str: string) { + str = str.replace(/^.*?@/, '@') + let name = '' + const nameList: string[] = [] + str.split('@') + .filter(mentionName => !!mentionName) + .reverse() + .forEach(mentionName => { + // console.log('mentionName: ', mentionName) + name = mentionName + '@' + name + nameList.push(name.slice(0, -1)) // get rid of the `@` at beginning + }) + return nameList + } + + let mentionNameList: string[] = [] + // Flatten Array + // see http://stackoverflow.com/a/10865042/1123955 + mentionNameList = mentionNameList.concat.apply([], rawMentionList) + // filter blank string + mentionNameList = mentionNameList.filter(s => !!s) + + log.verbose('Message', 'mentionList() text = "%s", mentionNameList = "%s"', + this.text(), + JSON.stringify(mentionNameList), + ) + + const contactListNested = await Promise.all( + mentionNameList.map( + name => room.memberAll(name), + ), + ) + + let contactList: ContactInterface[] = [] + contactList = contactList.concat.apply([], contactListNested) + + if (contactList.length === 0) { + log.silly('Message', `message.mentionList() can not found member using room.member() from mentionList, mention string: ${JSON.stringify(mentionNameList)}`) + } + return contactList + } + + /** + * @deprecated mention() DEPRECATED. use mentionList() instead. + */ + async mention (): Promise { + log.warn('Message', 'mention() DEPRECATED. use mentionList() instead. Call stack: %s', + new Error().stack, + ) + return this.mentionList() + } + + async mentionText (): Promise { + const text = this.text() + const room = this.room() + + const mentionList = await this.mentionList() + + if (!room || mentionList.length === 0) { + return text + } + + const toAliasName = async (member: ContactInterface) => { + const alias = await room.alias(member) + const name = member.name() + return alias || name + } + + const mentionNameList = await Promise.all(mentionList.map(toAliasName)) + + const textWithoutMention = mentionNameList.reduce((prev, cur) => { + const escapedCur = escapeRegExp(cur) + const regex = new RegExp(`@${escapedCur}(\u2005|\u0020|$)`) + return prev.replace(regex, '') + }, text) + + return textWithoutMention.trim() + } + + /** + * Check if a message is mention self. + * + * @returns {Promise} - Return `true` for mention me. + * @example + * if (await message.mentionSelf()) { + * console.log('this message were mentioned me! [You were mentioned] tip ([ๆœ‰ไบบ@ๆˆ‘]็š„ๆ็คบ)') + * } + */ + async mentionSelf (): Promise { + const currentUserId = this.wechaty.puppet.currentUserId + const mentionList = await this.mentionList() + return mentionList.some(contact => contact.id === currentUserId) + } + + /** + * @ignore + */ + isReady (): boolean { + return !!this.payload + } + + /** + * @ignore + */ + async ready (): Promise { + log.verbose('Message', 'ready()') + + if (this.isReady()) { + return + } + + this.payload = await this.wechaty.puppet.messagePayload(this.id) + + let talkerId = this.payload.talkerId + if (!talkerId) { + /** + * `fromId` is deprecated: this code block will be removed in v2.0 + */ + if (this.payload.fromId) { + talkerId = this.payload.fromId + } else { + throw new Error('no talkerId found for talker') + } + + log.warn('Message', 'ready() payload.talkerId not exist! See: https://github.com/wechaty/puppet/issues/187') + console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) + console.error(new Error().stack) + } + + const roomId = this.payload.roomId + + let listenerId = this.payload.listenerId + if (!listenerId && this.payload.toId) { + /** + * `fromId` is deprecated: this code block will be removed in v2.0 + */ + listenerId = this.payload.toId + + log.warn('Message', 'ready() payload.listenerId should be set! See: https://github.com/wechaty/puppet/issues/187') + console.error('Puppet: %s@%s', this.wechaty.puppet.name(), this.wechaty.puppet.version()) + console.error(new Error().stack) + } + + if (roomId) { + await this.wechaty.Room.find({ id: roomId }) + } + if (talkerId) { + await this.wechaty.Contact.find({ id: talkerId }) + } + if (listenerId) { + await this.wechaty.Contact.find({ id: listenerId }) + } + } + + // case WebMsgType.APP: + // if (!this.rawObj) { + // throw new Error('no rawObj') + // } + // switch (this.typeApp()) { + // case WebAppMsgType.ATTACH: + // if (!this.rawObj.MMAppMsgDownloadUrl) { + // throw new Error('no MMAppMsgDownloadUrl') + // } + // // had set in Message + // // url = this.rawObj.MMAppMsgDownloadUrl + // break + + // case WebAppMsgType.URL: + // case WebAppMsgType.READER_TYPE: + // if (!this.rawObj.Url) { + // throw new Error('no Url') + // } + // // had set in Message + // // url = this.rawObj.Url + // break + + // default: + // const e = new Error('ready() unsupported typeApp(): ' + this.typeApp()) + // log.warn('PuppeteerMessage', e.message) + // throw e + // } + // break + + // case WebMsgType.TEXT: + // if (this.typeSub() === WebMsgType.LOCATION) { + // url = await puppet.bridge.getMsgPublicLinkImg(this.id) + // } + // break + + /** + * Forward the received message. + * + * @param {(Sayable | Sayable[])} to Room or Contact + * The recipient of the message, the room, or the contact + * @returns {Promise} + * @example + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const room = await bot.Room.find({topic: 'wechaty'}) + * if (room) { + * await m.forward(room) + * console.log('forward this message to wechaty room!') + * } + * }) + * .start() + */ + async forward (to: RoomInterface | ContactInterface): Promise { + log.verbose('Message', 'forward(%s)', to) + + // let roomId + // let contactId + + try { + const msgId = await this.wechaty.puppet.messageForward( + to.id, + this.id, + ) + if (msgId) { + const msg = await this.wechaty.Message.find({ id: msgId }) + return msg + } + } catch (e) { + log.error('Message', 'forward(%s) exception: %s', to, e) + throw e + } + } + + /** + * Message sent date + */ + date (): Date { + if (!this.payload) { + throw new Error('no payload') + } + + const timestamp = this.payload.timestamp + return timestampToDate(timestamp) + } + + /** + * Returns the message age in seconds.
+ * + * For example, the message is sent at time `8:43:01`, + * and when we received it in Wechaty, the time is `8:43:15`, + * then the age() will return `8:43:15 - 8:43:01 = 14 (seconds)` + * + * @returns {number} message age in seconds. + */ + age (): number { + const ageMilliseconds = Date.now() - this.date().getTime() + const ageSeconds = Math.floor(ageMilliseconds / 1000) + return ageSeconds + } + + /** + * Extract the Media File from the Message, and put it into the FileBox. + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @returns {Promise} + * + * @example Save media file from a message + * const fileBox = await message.toFileBox() + * const fileName = fileBox.name + * fileBox.toFile(fileName) + */ + async toFileBox (): Promise { + log.verbose('Message', 'toFileBox()') + if (this.type() === PUPPET.types.Message.Text) { + throw new Error('text message no file') + } + const fileBox = await this.wechaty.puppet.messageFile(this.id) + return fileBox + } + + /** + * Extract the Image File from the Message, so that we can use different image sizes. + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @returns {ImageInterface} + * + * @example Save image file from a message + * const image = message.toImage() + * const fileBox = await image.artwork() + * const fileName = fileBox.name + * fileBox.toFile(fileName) + */ + toImage (): ImageInterface { + log.verbose('Message', 'toImage() for message id: %s', this.id) + if (this.type() !== PUPPET.types.Message.Image) { + throw new Error(`not a image type message. type: ${this.type()}`) + } + return this.wechaty.Image.create(this.id) + } + + /** + * Get Share Card of the Message + * Extract the Contact Card from the Message, and encapsulate it into Contact class + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * @returns {Promise} + */ + async toContact (): Promise { + log.verbose('Message', 'toContact()') + + if (this.type() !== PUPPET.types.Message.Contact) { + throw new Error('message not a ShareCard') + } + + const contactId = await this.wechaty.puppet.messageContact(this.id) + + if (!contactId) { + throw new Error(`can not get Contact id by message: ${contactId}`) + } + + const contact = await this.wechaty.Contact.find({ id: contactId }) + if (!contact) { + throw new Error(`can not get Contact payload by from id: ${contactId}`) + } + + return contact + } + + async toUrlLink (): Promise { + log.verbose('Message', 'toUrlLink()') + + if (!this.payload) { + throw new Error('no payload') + } + + if (this.type() !== PUPPET.types.Message.Url) { + throw new Error('message not a Url Link') + } + + const urlPayload = await this.wechaty.puppet.messageUrl(this.id) + + return new this.wechaty.UrlLink(urlPayload) + } + + async toMiniProgram (): Promise { + log.verbose('Message', 'toMiniProgram()') + + if (!this.payload) { + throw new Error('no payload') + } + + if (this.type() !== PUPPET.types.Message.MiniProgram) { + throw new Error('message not a MiniProgram') + } + + const miniProgramPayload = await this.wechaty.puppet.messageMiniProgram(this.id) + + return new this.wechaty.MiniProgram(miniProgramPayload) + } + + async toLocation (): Promise { + log.verbose('Message', 'toLocation()') + + if (!this.payload) { + throw new Error('no payload') + } + + if (this.type() !== PUPPET.types.Message.Location) { + throw new Error('message not a Location') + } + + const locationPayload = await this.wechaty.puppet.messageLocation(this.id) + + return new LocationImpl(locationPayload) + } + + public async toPost (): Promise { + log.verbose('Message', 'toPost()') + + if (!this.payload) { + throw new Error('no payload') + } + + if (this.type() !== PUPPET.types.Message.Post) { + throw new Error('message type not a Post') + } + + const post = PostImpl.load(this.id) + await post.ready() + return post + } + + async toSayable (): Promise { + log.verbose('Message', 'toSayable()') + return messageToSayable(this) + } + +} + +class MessageImplBase extends validationMixin(MessageMixin)() {} +interface MessageImplInterface extends MessageImplBase {} + +type MessageProtectedProperty = + | 'ready' + +type MessageInterface = Omit +class MessageImpl extends validationMixin(MessageImplBase)() {} + +type MessageConstructor = Constructor< + MessageInterface, + Omit +> + +export type { + MessageInterface, + MessageProtectedProperty, + MessageConstructor, +} +export { + MessageImpl, +} diff --git a/src/user-modules/mini-program.ts b/src/user-modules/mini-program.ts new file mode 100644 index 0000000..a1fea4f --- /dev/null +++ b/src/user-modules/mini-program.ts @@ -0,0 +1,111 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type * as PUPPET from 'wechaty-puppet' + +import type { Constructor } from 'clone-class' +import { log } from '../config.js' + +import { validationMixin } from '../user-mixins/validation.js' + +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' + +class MiniProgramMixin extends wechatifyMixinBase() { + + /** + * + * Create + * + */ + static async create (): Promise { + log.verbose('MiniProgram', 'create()') + + // TODO: get appid and username from wechat + const payload: PUPPET.payloads.MiniProgram = { + appid : 'todo', + description : 'todo', + pagePath : 'todo', + thumbKey : 'todo', + thumbUrl : 'todo', + title : 'todo', + username : 'todo', + } + + return new this(payload) + } + + /* + * @hideconstructor + */ + constructor ( + public readonly payload: PUPPET.payloads.MiniProgram, + ) { + super() + log.verbose('MiniProgram', 'constructor()') + // Huan(202110): it is ok to create a raw one without wechaty instance + // guardWechatifyClass.call(this, MiniProgram) + } + + appid (): undefined | string { + return this.payload.appid + } + + title (): undefined | string { + return this.payload.title + } + + pagePath (): undefined | string { + return this.payload.pagePath + } + + username (): undefined | string { + return this.payload.username + } + + description (): undefined | string { + return this.payload.description + } + + thumbUrl (): undefined | string { + return this.payload.thumbUrl + } + + thumbKey (): undefined | string { + return this.payload.thumbKey + } + +} + +class MiniProgramImpl extends validationMixin(MiniProgramMixin)() {} +interface MiniProgramInterface extends MiniProgramImpl {} + +type MiniProgramConstructor = Constructor< + MiniProgramInterface, + typeof MiniProgramImpl +> + +export type { + MiniProgramConstructor, + MiniProgramInterface, +} +export { + MiniProgramImpl, +} diff --git a/src/user-modules/mod.ts b/src/user-modules/mod.ts new file mode 100644 index 0000000..9908aa1 --- /dev/null +++ b/src/user-modules/mod.ts @@ -0,0 +1,160 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + ContactImpl, + ContactInterface, + ContactConstructor, +} from './contact.js' +import { + ContactSelfImpl, + ContactSelfInterface, + ContactSelfConstructor, +} from './contact-self.js' +import { + FavoriteImpl, + FavoriteInterface, + FavoriteConstructor, +} from './favorite.js' +import { + FriendshipImpl, + FriendshipInterface, + FriendshipConstructor, +} from './friendship.js' +import { + ImageImpl, + ImageInterface, + ImageConstructor, +} from './image.js' +import { + LocationImpl, + LocationInterface, + LocationConstructor, +} from './location.js' +import { + MessageImpl, + MessageInterface, + MessageConstructor, +} from './message.js' +import { + MiniProgramImpl, + MiniProgramInterface, + MiniProgramConstructor, +} from './mini-program.js' +import { + MomentImpl, + MomentInterface, + MomentConstructor, +} from './moment.js' +import { + MoneyImpl, + MoneyInterface, + MoneyConstructor, +} from './money.js' +import { + PostImpl, + PostInterface, + PostConstructor, +} from './post.js' +import { + RoomImpl, + RoomInterface, + RoomConstructor, +} from './room.js' +import { + RoomInvitationImpl, + RoomInvitationInterface, + RoomInvitationConstructor, +} from './room-invitation.js' +import { + TagImpl, + TagInterface, + TagConstructor, +} from './tag.js' +import { + UrlLinkImpl, + UrlLinkInterface, + UrlLinkConstructor, +} from './url-link.js' +import { + DelayImpl, + DelayInterface, + DelayConstructor, +} from './delay.js' + +import { wechatifyUserModule } from '../user-mixins/wechatify.js' + +export type { + ContactInterface, + ContactSelfInterface, + FavoriteInterface, + FriendshipInterface, + ImageInterface, + LocationInterface, + MessageInterface, + MiniProgramInterface, + MomentInterface, + MoneyInterface, + PostInterface, + RoomInterface, + RoomInvitationInterface, + TagInterface, + DelayInterface, + UrlLinkInterface, +} + +export type { + ContactConstructor, + ContactSelfConstructor, + FavoriteConstructor, + FriendshipConstructor, + ImageConstructor, + LocationConstructor, + MessageConstructor, + MiniProgramConstructor, + MomentConstructor, + MoneyConstructor, + PostConstructor, + RoomConstructor, + RoomInvitationConstructor, + DelayConstructor, + TagConstructor, + UrlLinkConstructor, +} + +export { + wechatifyUserModule, + + ContactImpl, + ContactSelfImpl, + FavoriteImpl, + FriendshipImpl, + ImageImpl, + LocationImpl, + MessageImpl, + MiniProgramImpl, + MomentImpl, + MoneyImpl, + PostImpl, + RoomImpl, + RoomInvitationImpl, + DelayImpl, + TagImpl, + UrlLinkImpl, +} diff --git a/src/user-modules/moment.ts b/src/user-modules/moment.ts new file mode 100644 index 0000000..c0c0fad --- /dev/null +++ b/src/user-modules/moment.ts @@ -0,0 +1,64 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { log } from 'wechaty-puppet' +import type { Constructor } from 'clone-class' + +import type { ContactInterface } from './contact.js' +import { validationMixin } from '../user-mixins/validation.js' +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' + +class MomentMixin extends wechatifyMixinBase() { + + static post () { + // post new moment + } + + static timeline (contact: ContactInterface): MomentImpl[] { + // list all moment + void contact + return [] + } + + /* + * @hideconstructor + */ + constructor () { + super() + log.verbose('Moment', 'constructor()') + } + +} + +class MomentImpl extends validationMixin(MomentMixin)() {} +interface MomentInterface extends MomentImpl {} +type MomentConstructor = Constructor< + MomentInterface, + typeof MomentImpl +> + +export type { + MomentConstructor, + MomentInterface, +} +export { + MomentImpl, +} diff --git a/src/user-modules/money.ts b/src/user-modules/money.ts new file mode 100644 index 0000000..02c0cc8 --- /dev/null +++ b/src/user-modules/money.ts @@ -0,0 +1,49 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type { Constructor } from 'clone-class' +import { validationMixin } from '../user-mixins/validation.js' + +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' + +class MoneyMixin extends wechatifyMixinBase() { + + constructor () { + super() + } + +} + +class MoneyImpl extends validationMixin(MoneyMixin)() {} +interface MoneyInterface extends MoneyImpl {} + +type MoneyConstructor = Constructor< + MoneyInterface, + typeof MoneyImpl +> + +export type { + MoneyConstructor, + MoneyInterface, +} +export { + MoneyImpl, +} diff --git a/src/user-modules/post.spec.ts b/src/user-modules/post.spec.ts new file mode 100755 index 0000000..85adcb0 --- /dev/null +++ b/src/user-modules/post.spec.ts @@ -0,0 +1,77 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + test, + sinon, +} from 'tstest' + +import * as PUPPET from 'wechaty-puppet' +import PuppetMock from 'wechaty-puppet-mock' +import { FileBox } from 'file-box' + +import { WechatyBuilder } from '../wechaty-builder.js' + +test.skip('Post smoke testing', async t => { + void sinon + + const puppet = new PuppetMock() + const wechaty = WechatyBuilder.build({ puppet }) + await wechaty.start() + const bot = puppet.mocker.createContact({ name: 'Bot' }) + puppet.mocker.login(bot) + + const post = await wechaty.Post.builder() + .add('Hello, world!') + .add(FileBox.fromQRCode('qr')) + .add(await wechaty.UrlLink.create('https://yahoo.com')) + .build() + + await wechaty.say(post) + + await post.reply('Thanks for sharing!') + await post.like(true) + await post.tap(PUPPET.types.Tap.Like, false) + + const pagination = { + pageSize: 10, + pageToken: '', + } + + for await (const sayable of post) { + t.ok(sayable, 'tbw') + } + + for await (const descendantPost of post.descendants()) { + t.ok(descendantPost, 'tbw') + } + + const [ descendantList, _nextPageToken2 ] = await wechaty.Post.findAll({}, pagination) + t.ok(descendantList, 'tbw') + + for await (const liker of post.taps({ type: PUPPET.types.Tap.Like })) { + t.ok(liker, 'tbw') + } + + const [ tapList, _nextPageToken3 ] = await post.tapFind({ type: PUPPET.types.Tap.Like }, pagination) + t.ok(tapList, 'tbw') + + await wechaty.stop() +}) diff --git a/src/user-modules/post.ts b/src/user-modules/post.ts new file mode 100644 index 0000000..7d80148 --- /dev/null +++ b/src/user-modules/post.ts @@ -0,0 +1,549 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * Issue #2245 - New Wechaty User Module (WUM): + * `Post` for supporting Moments, Channel, Tweet, Weibo, Facebook feeds, etc. + * + * @see https://github.com/wechaty/wechaty/issues/2245#issuecomment-914886835 + */ +import * as PUPPET from 'wechaty-puppet' +import { log } from 'wechaty-puppet' + +import type { + Constructor, +} from 'clone-class' + +import { + validationMixin, + wechatifyMixinBase, +} from '../user-mixins/mod.js' + +import type { Sayable } from '../sayable/mod.js' +import { + sayableToPayload, + payloadToSayableWechaty, +} from '../sayable/mod.js' + +import { ContactImpl } from './contact.js' +import type { ContactInterface } from './contact.js' +import { concurrencyExecuter } from 'rx-queue' + +interface Tap { + contact: ContactInterface + type: PUPPET.types.Tap, + date: Date +} + +class PostBuilder { + + payload: PUPPET.payloads.PostClient + + /** + * Wechaty Sayable List + */ + sayableList: Sayable[] = [] + + /** + * Huan(202201): why use Impl as a parameter? + */ + static new (Impl: typeof PostMixin) { return new this(Impl) } + protected constructor ( + protected Impl: typeof PostMixin, + ) { + this.payload = { + sayableList: [], // Puppet Sayable Payload List + type: PUPPET.types.Post.Unspecified, + } + } + + add (sayable: Sayable): this { + this.sayableList.push(sayable) + return this + } + + type (type: PUPPET.types.Post): this { + this.payload.type = type + return this + } + + reply (post: PostInterface): this { + if (!post.id) { + throw new Error('can not link to a post without id: ' + JSON.stringify(post)) + } + + this.payload.parentId = post.payload.id + this.payload.rootId = post.payload.rootId + + return this + } + + async build (): Promise { + const sayablePayloadListNested = await Promise.all( + this.sayableList.map(sayableToPayload), + ) + this.payload.sayableList = sayablePayloadListNested.filter(Boolean) as PUPPET.payloads.Sayable[] + + return this.Impl.create(this.payload) + } + +} + +class PostMixin extends wechatifyMixinBase() { + + static builder (): PostBuilder { return PostBuilder.new(this) } + + /** + * + * Create + * + */ + static create ( + payload: PUPPET.payloads.PostClient, + ): PostInterface { + log.verbose('Post', 'create()') + + return new this(payload) + } + + static load (id: string): PostInterface { + log.verbose('Post', 'static load(%s)', id) + + /** + * Must NOT use `Post` at here + * MUST use `this` at here + * + * because the class will be `cloneClass`-ed + */ + const post = new this(id) + + return post + } + + static async find ( + filter: PUPPET.filters.Post, + ): Promise { + log.verbose('Post', 'find(%s)', + JSON.stringify(filter), + ) + + if (filter.id) { + const post = this.wechaty.Post.load(filter.id) + await post.ready() + return post + } + + const [ postList ] = await this.findAll(filter, { pageSize: 1 }) + if (postList.length > 0) { + return postList[0] + } + return undefined + } + + static async findAll ( + filter : PUPPET.filters.Post, + pagination? : PUPPET.filters.PaginationRequest, + ): Promise<[ + postList : PostInterface[], + nextPageToken? : string, + ]> { + log.verbose('Post', 'findAll(%s%s)', + JSON.stringify(filter), + pagination ? ', ' + JSON.stringify(pagination) : '', + ) + + const { + nextPageToken, + response: postIdList, + } = await this.wechaty.puppet.postSearch( + filter, + pagination, + ) + + const idToPost = async (id: string) => + this.wechaty.Post.find({ id }) + .catch(e => this.wechaty.emitError(e)) + + /** + * we need to use concurrencyExecuter to reduce the parallel number of the requests + */ + const CONCURRENCY = 17 + const postIterator = concurrencyExecuter(CONCURRENCY)(idToPost)(postIdList) + + const postList: PostInterface[] = [] + for await (const post of postIterator) { + if (post) { + postList.push(post) + } + } + + return [ postList, nextPageToken ] + } + + protected _payload?: PUPPET.payloads.Post + get payload (): PUPPET.payloads.Post { + if (!this._payload) { + throw new Error('no payload, need to call `ready()` first.') + } + return this._payload + } + + readonly id?: string + + /* + * @hideconstructor + */ + constructor ( + idOrPayload: string | PUPPET.payloads.Post, + ) { + super() + log.verbose('Post', 'constructor(%s)', + typeof idOrPayload === 'string' + ? idOrPayload + : JSON.stringify(idOrPayload.id), + ) + + if (typeof idOrPayload === 'string') { + this.id = idOrPayload + } else { + this._payload = idOrPayload + this.id = idOrPayload.id + } + } + + counter (): PUPPET.payloads.PostServer['counter'] { + return { + children : 0, + descendant : 0, + taps : {}, + ...(PUPPET.payloads.isPostServer(this.payload) && this.payload.counter), + } + } + + async author (): Promise { + log.silly('Post', 'author()') + + if (PUPPET.payloads.isPostClient(this.payload)) { + return this.wechaty.currentUser + } + + const author = await ContactImpl.find(this.payload.contactId) + if (!author) { + throw new Error('no author for id: ' + this.payload.contactId) + } + return author + } + + async root (): Promise { + log.silly('Post', 'root()') + + if (!this.payload.rootId) { + return undefined + } + + const post = this.wechaty.Post.load(this.payload.rootId) + await post.ready() + return post + } + + async parent (): Promise { + log.silly('Post', 'parent()') + if (!this.payload.parentId) { + return undefined + } + + const post = this.wechaty.Post.load(this.payload.parentId) + await post.ready() + return post + } + + async sync (): Promise { + log.silly('Post', 'sync()') + + if (!this.id) { + throw new Error('no post id found') + } + + this._payload = await this.wechaty.puppet.postPayload(this.id) + } + + async ready (): Promise { + log.silly('Post', 'ready()') + + if (!this.id) { + throw new Error('no post id found') + } + + if (this._payload) { + return + } + + await this.sync() + } + + async * [Symbol.asyncIterator] (): AsyncIterableIterator { + log.verbose('Post', '[Symbol.asyncIterator]()') + + if (PUPPET.payloads.isPostServer(this.payload)) { + for (const messageId of this.payload.sayableList) { + const message = await this.wechaty.Message.find({ id: messageId }) + if (message) { + const sayable = await message.toSayable() + if (sayable) { + yield sayable + } + } + } + + } else { // client + const payloadToSayable = payloadToSayableWechaty(this.wechaty) + for (const sayablePayload of this.payload.sayableList) { + const sayable = await payloadToSayable(sayablePayload) + if (sayable) { + yield sayable + } + } + + } + } + + async * children ( + filter: PUPPET.filters.Post = {}, + ): AsyncIterableIterator { + log.verbose('Post', '*children(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') + + const pagination: PUPPET.filters.PaginationRequest = { + pageSize: 100, + } + + const parentIdFilter = { + ...filter, + parentId: this.id, + } + + let [ postList, nextPageToken ] = await this.wechaty.Post.findAll( + parentIdFilter, + pagination, + ) + + while (true) { + yield * postList + postList.length = 0 + + if (!nextPageToken) { + break + } + + [ postList, nextPageToken ] = await this.wechaty.Post.findAll( + parentIdFilter, + { + ...pagination, + pageToken: nextPageToken, + }, + ) + } + } + + async * descendants ( + filter: PUPPET.filters.Post = {}, + ): AsyncIterableIterator { + log.verbose('Post', '*descendants(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') + + for await (const post of this.children(filter)) { + yield post + yield * post.descendants(filter) + } + } + + async * likes ( + filter: PUPPET.filters.Post = {}, + ): AsyncIterableIterator { + log.verbose('Post', '*likes(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') + return this.taps({ + ...filter, + type: PUPPET.types.Tap.Like, + }) + } + + async * taps ( + filter: PUPPET.filters.Tap = {}, + ): AsyncIterableIterator { + log.verbose('Post', '*taps(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') + + const pagination: PUPPET.filters.PaginationRequest = {} + + let [ tapList, nextPageToken ] = await this.tapFind( + filter, + pagination, + ) + + while (true) { + yield * tapList + tapList.length = 0 + + if (!nextPageToken) { + break + } + + [ tapList, nextPageToken ] = await this.tapFind( + filter, + { ...pagination, pageToken: nextPageToken }, + ) + } + } + + async reply ( + sayable: + | Exclude + | Exclude[], + ): Promise { + log.verbose('Post', 'reply(%s)', sayable) + + if (!this.id) { + console.error('You can only call `reply()` on received posts, but it seems that you are trying to call reply on a post created from local.') + throw new Error('no post id found') + } + + const builder = this.wechaty.Post.builder() + + if (Array.isArray(sayable)) { + sayable.forEach(s => builder.add(s)) + } else { + builder.add(sayable) + } + + const post = await builder + .reply(this) + .build() + + const postId = await this.wechaty.puppet.postPublish(post.payload) + + if (postId) { + const newPost = this.wechaty.Post.load(postId) + await newPost.ready() + return newPost + } + } + + async like (status: boolean) : Promise + async like () : Promise + + async like (status?: boolean): Promise { + log.verbose('Post', 'like(%s)', typeof status === 'undefined' ? '' : status) + + if (typeof status === 'undefined') { + return this.tap( + PUPPET.types.Tap.Like, + ) + } else { + return this.tap( + PUPPET.types.Tap.Like, + status, + ) + } + } + + /** + * Return Date if the bot has tapped the post, otherwise return undefined + */ + async tap (type: PUPPET.types.Tap) : Promise + async tap (type: PUPPET.types.Tap, status: boolean) : Promise + + async tap ( + type : PUPPET.types.Tap, + status? : boolean, + ): Promise { + log.verbose('Post', 'tap(%s%s)', + PUPPET.types.Tap[type], + typeof status === 'undefined' + ? '' + : ', ' + status, + ) + + if (!this.id) { + throw new Error('can not tap for post without id') + } + + return this.wechaty.puppet.tap(this.id, type, status) + } + + async tapFind ( + filter : PUPPET.filters.Tap, + pagination? : PUPPET.filters.PaginationRequest, + ): Promise<[ + tapList : Tap[], + nextPageToken? : string, + ]> { + log.verbose('Post', 'tapFind()') + + if (!this.id) { + throw new Error('can not get tapFind for client created post') + } + + const { + nextPageToken, + response, + } = await this.wechaty.puppet.tapSearch( + this.id, + filter, + pagination, + ) + + const tapList: Tap[] = [] + for (const [ type, data ] of Object.entries(response)) { + for (const [ i, contactId ] of data.contactId.entries()) { + const contact = await this.wechaty.Contact.find({ id: contactId }) + if (!contact) { + log.warn('Post', 'tapFind() contact not found for id: %s', contactId) + continue + } + + const timestamp = data.timestamp[i] + const date = timestamp ? new Date(timestamp) : new Date() + + tapList.push({ + contact, + date, + type: Number(type) as PUPPET.types.Tap, + }) + } + } + + return [ tapList, nextPageToken ] + } + +} + +class PostImpl extends validationMixin(PostMixin)() {} +interface PostInterface extends PostImpl {} + +type PostConstructor = Constructor< + PostInterface, + typeof PostImpl +> + +export type { + PostConstructor, + PostInterface, +} +export { + PostBuilder, + PostImpl, +} diff --git a/src/user-modules/room-invitation.ts b/src/user-modules/room-invitation.ts new file mode 100644 index 0000000..a8c346e --- /dev/null +++ b/src/user-modules/room-invitation.ts @@ -0,0 +1,290 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type * as PUPPET from 'wechaty-puppet' + +import { log } from '../config.js' +import type { Constructor } from 'clone-class' + +import type { + Accepter, +} from '../schemas/acceptable.js' +import { + timestampToDate, +} from '../pure-functions/timestamp-to-date.js' + +import type { + ContactInterface, +} from './contact.js' +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' +import { validationMixin } from '../user-mixins/validation.js' + +/** + * + * accept room invitation + */ +class RoomInvitationMixin extends wechatifyMixinBase() implements Accepter { + + static load ( + id : string, + ): RoomInvitationInterface { + const newRoomInvitation = new this(id) + return newRoomInvitation + } + + /** + * @hideconstructor + * Instance Properties + * + */ + constructor ( + public readonly id: string, + ) { + super() + log.verbose('RoomInvitation', 'constructor(id=%s)', id) + } + + override toString () { + return [ + 'RoomInvitation#', + this.id || 'loading', + ].join('') + } + + /** + * @ignore + */ + async toStringAsync (): Promise { + const payload = await this.wechaty.puppet.roomInvitationPayload(this.id) + return [ + 'RoomInvitation#', + this.id, + '<', + payload.topic, + ',', + payload.inviterId, + '>', + ].join('') + } + + /** + * Accept Room Invitation + * + * @returns {Promise} + * + * @example + * const bot = new Wechaty() + * bot.on('room-invite', async roomInvitation => { + * try { + * console.log(`received room-invite event.`) + * await roomInvitation.accept() + * } catch (e) { + * console.error(e) + * } + * } + * .start() + */ + async accept (): Promise { + log.verbose('RoomInvitation', 'accept()') + + try { + await this.wechaty.puppet.roomInvitationAccept(this.id) + + const inviter = await this.inviter() + const topic = await this.topic() + + log.verbose('RoomInvitation', 'accept() with room(%s) & inviter(%s) ready()', + topic, + inviter, + ) + } catch (e) { + this.wechaty.emitError(e) + log.warn('RoomInvitation', 'accept() rejection: %s', + (e && (e as Error).message) || e, + ) + } + } + + /** + * Get the inviter from room invitation + * + * @returns {ContactInterface} + * @example + * const bot = new Wechaty() + * bot.on('room-invite', async roomInvitation => { + * const inviter = await roomInvitation.inviter() + * const name = inviter.name() + * console.log(`received room invitation event from ${name}`) + * } + * .start() + */ + async inviter (): Promise { + log.verbose('RoomInvitation', 'inviter()') + + const payload = await this.wechaty.puppet.roomInvitationPayload(this.id) + const inviter = await this.wechaty.Contact.find({ id: payload.inviterId }) + + if (!inviter) { + throw new Error('can not found inviter with id: ' + payload.inviterId) + } + return inviter + } + + /** + * Get the room topic from room invitation + * + * @returns {string} + * @example + * const bot = new Wechaty() + * bot.on('room-invite', async roomInvitation => { + * const topic = await roomInvitation.topic() + * console.log(`received room invitation event from room ${topic}`) + * } + * .start() + */ + async topic (): Promise { + const payload = await this.wechaty.puppet.roomInvitationPayload(this.id) + + return payload.topic || payload.topic || '' + } + + async memberCount (): Promise { + log.verbose('RoomInvitation', 'memberCount()') + + const payload = await this.wechaty.puppet.roomInvitationPayload(this.id) + + return payload.memberCount || payload.memberCount || 0 + } + + /** + * List of Room Members that you known(is friend) + * @ignore + */ + async memberList (): Promise { + log.verbose('RoomInvitation', 'roomMemberList()') + + const payload = await this.wechaty.puppet.roomInvitationPayload(this.id) + + const contactIdList = payload.memberIdList + + const contactListAll = await Promise.all( + contactIdList.map( + id => this.wechaty.Contact.find({ id }), + ), + ) + + const contactList = contactListAll.filter(c => !!c) as ContactInterface[] + return contactList + } + + /** + * Get the invitation time + * + * @returns {Promise} + */ + async date (): Promise { + log.verbose('RoomInvitation', 'date()') + + const payload = await this.wechaty.puppet.roomInvitationPayload(this.id) + return timestampToDate(payload.timestamp) + } + + /** + * Returns the roopm invitation age in seconds.
+ * + * For example, the invitation is sent at time `8:43:01`, + * and when we received it in Wechaty, the time is `8:43:15`, + * then the age() will return `8:43:15 - 8:43:01 = 14 (seconds)` + * @returns {number} + */ + async age (): Promise { + const recvDate = await this.date() + + const ageMilliseconds = Date.now() - recvDate.getTime() + const ageSeconds = Math.floor(ageMilliseconds / 1000) + + return ageSeconds + } + + /** + * Load the room invitation info from disk + * + * @returns {RoomInvitationInterface} + * @example + * const bot = new Wechaty() + * const dataFromDisk // get the room invitation info data from disk + * const roomInvitation = await bot.RoomInvitation.fromJSON(dataFromDisk) + * await roomInvitation.accept() + */ + static async fromJSON ( + payload: string | PUPPET.payloads.RoomInvitation, + ): Promise { + log.verbose('RoomInvitation', 'fromJSON(%s)', + typeof payload === 'string' + ? payload + : JSON.stringify(payload), + ) + + if (typeof payload === 'string') { + payload = JSON.parse(payload) as PUPPET.payloads.RoomInvitation + } + + await this.wechaty.puppet.roomInvitationPayload(payload.id, payload) + + return this.wechaty.RoomInvitation.load(payload.id) + } + + /** + * Get the room invitation info when listened on room-invite event + * + * @returns {string} + * @example + * const bot = new Wechaty() + * bot.on('room-invite', async roomInvitation => { + * const roomInvitation = bot.RoomInvitation.load(roomInvitation.id) + * const jsonData = await roomInvitation.toJSON(roomInvitation.id) + * // save the json data to disk, and we can use it by RoomInvitation.fromJSON() + * } + * .start() + */ + async toJSON (): Promise { + log.verbose('RoomInvitation', 'toJSON()') + const payload = await this.wechaty.puppet.roomInvitationPayload(this.id) + return JSON.stringify(payload) + } + +} + +class RoomInvitationImpl extends validationMixin(RoomInvitationMixin)() {} +interface RoomInvitationInterface extends RoomInvitationImpl {} + +type RoomInvitationConstructor = Constructor< + RoomInvitationInterface, + typeof RoomInvitationImpl +> + +export type { + RoomInvitationConstructor, + RoomInvitationInterface, +} +export { + RoomInvitationImpl, +} diff --git a/src/user-modules/room.spec.ts b/src/user-modules/room.spec.ts new file mode 100755 index 0000000..992f7f1 --- /dev/null +++ b/src/user-modules/room.spec.ts @@ -0,0 +1,170 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + test, + sinon, +} from 'tstest' + +import type * as PUPPET from 'wechaty-puppet' +import { PuppetMock } from 'wechaty-puppet-mock' + +import { WechatyBuilder } from '../wechaty-builder.js' +import type { + RoomImpl, + RoomProtectedProperty, +} from './room.js' + +test('findAll()', async t => { + const EXPECTED_ROOM_ID = 'test-id' + const EXPECTED_ROOM_TOPIC = 'test-topic' + const EXPECTED_ROOM_ID_LIST = [ EXPECTED_ROOM_ID ] + + const sandbox = sinon.createSandbox() + + const puppet = new PuppetMock() + const wechaty = WechatyBuilder.build({ puppet }) + + await wechaty.start() + + sandbox.stub(puppet, 'roomSearch').resolves(EXPECTED_ROOM_ID_LIST) + sandbox.stub(puppet, 'roomPayload').callsFake(async () => { + await new Promise(resolve => setImmediate(resolve)) + return { + topic: EXPECTED_ROOM_TOPIC, + } as PUPPET.payloads.Room + }) + + const roomList = await wechaty.Room.findAll() + t.equal(roomList.length, 1, 'should find 1 room') + t.equal(await roomList[0]!.topic(), EXPECTED_ROOM_TOPIC, 'should get topic from payload') + + await wechaty.stop() +}) + +test('room.say() smoke testing', async () => { + + const sandbox = sinon.createSandbox() + const callback = sinon.spy() + + const puppet = new PuppetMock() + const wechaty = WechatyBuilder.build({ puppet }) + + const bot = puppet.mocker.createContact() + puppet.mocker.login(bot) + + await wechaty.start() + + const EXPECTED_ROOM_ID = 'roomId' + const EXPECTED_ROOM_TOPIC = 'test-topic' + const EXPECTED_CONTACT_1_ID = 'contact1' + const EXPECTED_CONTACT_1_ALIAS = 'little1' + const EXPECTED_CONTACT_2_ID = 'contact2' + const EXPECTED_CONTACT_2_ALIAS = 'big2' + const CONTACT_MAP: { [contactId: string]: string } = {} + CONTACT_MAP[EXPECTED_CONTACT_1_ID] = EXPECTED_CONTACT_1_ALIAS + CONTACT_MAP[EXPECTED_CONTACT_2_ID] = EXPECTED_CONTACT_2_ALIAS + + sandbox.stub(puppet, 'roomMemberPayload').callsFake(async (_, contactId) => { + await new Promise(resolve => setImmediate(resolve)) + return { + id: contactId, + roomAlias: CONTACT_MAP[contactId], + } as PUPPET.payloads.RoomMember + }) + sandbox.stub(puppet, 'roomPayload').callsFake(async () => { + await new Promise(resolve => setImmediate(resolve)) + return { + topic: EXPECTED_ROOM_TOPIC, + } as PUPPET.payloads.Room + }) + sandbox.stub(puppet, 'contactPayload').callsFake(async (contactId) => { + await new Promise(resolve => setImmediate(resolve)) + return { + id: contactId, + } as PUPPET.payloads.Contact + }) + // sandbox.spy(puppet, 'messageSendText') + sandbox.stub(puppet, 'messageSendText').callsFake(callback) + + const fakeIdSearcher = async (...args: any[]) => { + await new Promise(setImmediate) + return [ args[0].id ] + } + sandbox.stub(puppet, 'contactSearch').callsFake(fakeIdSearcher) + sandbox.stub(puppet, 'roomSearch').callsFake(fakeIdSearcher) + + const room = await wechaty.Room.find({ id: EXPECTED_ROOM_ID }) + const contact1 = await wechaty.Contact.find({ id: EXPECTED_CONTACT_1_ID }) + const contact2 = await wechaty.Contact.find({ id: EXPECTED_CONTACT_2_ID }) + + if (!room || !contact1 || !contact2) { + throw new Error('find by id: not found') + } + // await contact1.sync() + // await contact2.sync() + // await room.sync() + + test('say with Tagged Template', async t => { + callback.resetHistory() + await room.say`To be ${contact1} or not to be ${contact2}` + + t.same(callback.getCall(0).args, [ + // { contactId: EXPECTED_CONTACT_1_ID, roomId: EXPECTED_ROOM_ID }, + EXPECTED_ROOM_ID, + 'To be @little1 or not to be @big2', + [ EXPECTED_CONTACT_1_ID, EXPECTED_CONTACT_2_ID ], + ], 'Tagged Template say should be matched') + }) + + test('say with regular mention contact', async t => { + callback.resetHistory() + await room.say('Yo', contact1) + + t.same(callback.getCall(0).args, [ + // { contactId: EXPECTED_CONTACT_1_ID, roomId: EXPECTED_ROOM_ID }, + EXPECTED_ROOM_ID, + '@little1 Yo', + [ EXPECTED_CONTACT_1_ID ], + ], 'Single mention should work with old ways') + }) + + test('say with multiple mention contact', async t => { + callback.resetHistory() + await room.say('hey buddies, let\'s party', contact1, contact2) + + t.same(callback.getCall(0).args, [ + // { contactId: EXPECTED_CONTACT_1_ID, roomId: EXPECTED_ROOM_ID }, + EXPECTED_ROOM_ID, + '@little1โ€…@big2 hey buddies, let\'s party', + [ EXPECTED_CONTACT_1_ID, EXPECTED_CONTACT_2_ID ], + ], 'Multiple mention should work with new way') + }) + + await wechaty.stop() +}) + +test('ProtectedProperties', async t => { + type NotExistInWechaty = Exclude + type NotExistTest = NotExistInWechaty extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Wechaty properties for every protected property') +}) diff --git a/src/user-modules/room.ts b/src/user-modules/room.ts new file mode 100644 index 0000000..0213a2c --- /dev/null +++ b/src/user-modules/room.ts @@ -0,0 +1,1158 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import type * as PUPPET from 'wechaty-puppet' +import type { FileBoxInterface } from 'file-box' +import { concurrencyExecuter } from 'rx-queue' +import type { + Constructor, +} from 'clone-class' + +import { + FOUR_PER_EM_SPACE, + log, +} from '../config.js' +import { + wechatyCaptureException, +} from '../raven.js' + +import { + guardQrCodeValue, +} from '../pure-functions/guard-qr-code-value.js' +import { + isTemplateStringArray, +} from '../pure-functions/is-template-string-array.js' + +import { RoomEventEmitter } from '../schemas/mod.js' +import { + poolifyMixin, + wechatifyMixin, + validationMixin, +} from '../user-mixins/mod.js' +import { + deliverSayableConversationPuppet, +} from '../sayable/mod.js' +import type { + SayableSayer, + Sayable, +} from '../sayable/mod.js' +import { stringifyFilter } from '../helper-functions/stringify-filter.js' + +import { + ContactInterface, + ContactImpl, +} from './contact.js' +import type { + MessageInterface, +} from './message.js' + +const MixinBase = wechatifyMixin( + poolifyMixin( + RoomEventEmitter, + )(), +) + +/** + * All WeChat rooms(groups) will be encapsulated as a Room. + * + * [Examples/Room-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/room-bot.ts} + * + */ +class RoomMixin extends MixinBase implements SayableSayer { + + /** + * Create a new room. + * + * @static + * @param {ContactInterface[]} contactList + * @param {string} [topic] + * @returns {Promise} + * @example Creat a room with 'lijiarui' and 'huan', the room topic is 'ding - created' + * const helperContactA = await Contact.find({ name: 'lijiarui' }) // change 'lijiarui' to any contact in your WeChat + * const helperContactB = await Contact.find({ name: 'huan' }) // change 'huan' to any contact in your WeChat + * const contactList = [helperContactA, helperContactB] + * console.log('Bot', 'contactList: %s', contactList.join(',')) + * const room = await Room.create(contactList, 'ding') + * console.log('Bot', 'createDingRoom() new ding room created: %s', room) + * await room.topic('ding - created') + * await room.say('ding - created') + */ + static async create ( + contactList : ContactInterface[], + topic? : string, + ): Promise { + log.verbose('Room', 'create(%s, %s)', contactList.join(','), topic) + + if (contactList.length < 2) { + throw new Error('contactList need at least 2 contact to create a new room') + } + + try { + const contactIdList = contactList.map(contact => contact.id) + const roomId = await this.wechaty.puppet.roomCreate(contactIdList, topic) + const room = this.load(roomId) + return room + } catch (e) { + this.wechaty.emitError(e) + log.error('Room', 'create() exception: %s', (e && (e as Error).stack) || (e as Error).message || (e as Error)) + throw e + } + } + + /** + * The filter to find the room: {topic: string | RegExp} + * + * @typedef RoomQueryFilter + * @property {string} topic + */ + + /** + * Find room by filter: {topic: string | RegExp}, return all the matched room. + * + * NOTE: The returned list would be limited by the underlying puppet + * implementation of `puppet.roomList`. Some implementation (i.e. + * wechaty-puppet-wechat) would only return rooms which have received messges + * after a log-in. + * + * @static + * @param {RoomQueryFilter} [query] + * @returns {Promise} + * @example + * const bot = new Wechaty() + * await bot.start() + * // after logged in + * const roomList = await bot.Room.findAll() // get the room list of the bot + * const roomList = await bot.Room.findAll({topic: 'wechaty'}) // find all of the rooms with name 'wechaty' + */ + static async findAll ( + query? : PUPPET.filters.Room, + ): Promise { + log.verbose('Room', 'findAll(%s)', JSON.stringify(query, stringifyFilter) || '') + + try { + const roomIdList = await this.wechaty.puppet.roomSearch(query) + + const idToRoom = async (id: string) => + this.wechaty.Room.find({ id }) + .catch(e => this.wechaty.emitError(e)) + + /** + * we need to use concurrencyExecuter to reduce the parallel number of the requests + */ + const CONCURRENCY = 17 + const roomIterator = concurrencyExecuter(CONCURRENCY)(idToRoom)(roomIdList) + + const roomList: RoomInterface[] = [] + + for await (const room of roomIterator) { + if (room) { + roomList.push(room) + } + } + + return roomList + + } catch (e) { + this.wechaty.emitError(e) + log.verbose('Room', 'findAll() rejected: %s', (e as Error).message) + return [] as RoomInterface[] // fail safe + } + } + + /** + * Try to find a room by filter: {topic: string | RegExp}. If get many, return the first one. + * + * NOTE: The search space is limited by the underlying puppet + * implementation of `puppet.roomList`. Some implementation (i.e. + * wechaty-puppet-wechat) would only return rooms which have received messges + * after a log-in. + * + * @param {RoomQueryFilter} query + * @returns {Promise} If can find the room, return Room, or return null + * @example + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const roomList = await bot.Room.find() + * const roomList = await bot.Room.find({topic: 'wechaty'}) + */ + + static async find ( + query : string | PUPPET.filters.Room, + ): Promise { + log.silly('Room', 'find(%s)', JSON.stringify(query, stringifyFilter)) + + if (typeof query === 'string') { + query = { topic: query } + } + + if (query.id) { + const room = (this.wechaty.Room as any as typeof RoomImpl).load(query.id) + try { + await room.ready() + } catch (e) { + this.wechaty.emitError(e) + return undefined + } + + return room + } + + const roomList = await this.findAll(query) + // if (!roomList) { + // return null + // } + if (roomList.length < 1) { + return undefined + } + + if (roomList.length > 1) { + log.warn('Room', 'find() got more than one(%d) result', roomList.length) + } + + for (const [ idx, room ] of roomList.entries()) { + // use puppet.roomValidate() to confirm double confirm that this roomId is valid. + // https://github.com/wechaty/wechaty-puppet-padchat/issues/64 + // https://github.com/wechaty/wechaty/issues/1345 + const valid = await this.wechaty.puppet.roomValidate(room.id) + if (valid) { + log.verbose('Room', 'find() room is valid: return it', idx, room.id) + return room + } else { + log.verbose('Room', 'find() room is invalid: skip it', idx, room.id) + } + } + log.warn('Room', 'find() all %d rooms are invalid', roomList.length) + return undefined + } + + /** const roomList: RoomInterface[] = [] + + * @ignore + * + * Instance Properties + * + * + */ + payload?: PUPPET.payloads.Room + + /** + * @hideconstructor + * @property {string} id - Room id. + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + */ + constructor ( + public readonly id: string, + ) { + super() + log.silly('Room', `constructor(${id})`) + } + + /** + * @ignore + */ + override toString () { + if (!this.payload) { + return this.constructor.name + } + + return `Room<${this.payload.topic || 'loading...'}>` + } + + async * [Symbol.asyncIterator] (): AsyncIterableIterator { + const memberList = await this.memberList() + for (const contact of memberList) { + yield contact + } + } + + /** + * Proposal: add a handle field to RoomPayload #181 + * @link https://github.com/wechaty/puppet/issues/181 + */ + handle (): undefined | string { + return this.payload?.handle + } + + /** + * Force reload data for Room, Sync data from puppet API again. + * + * @returns {Promise} + * @example + * await room.sync() + */ + async sync (): Promise { + await this.wechaty.puppet.roomPayloadDirty(this.id) + await this.wechaty.puppet.roomMemberPayloadDirty(this.id) + await this.ready(true) + } + + /** + * Warning: `ready()` is for the framework internally use ONLY! + * + * Please not to use `ready()` at the user land. + * If you want to sync data, use `sync()` instead. + * + * @ignore + */ + async ready ( + forceSync = false, + ): Promise { + log.silly('Room', 'ready()') + + if (!forceSync && this.isReady()) { + return + } + + this.payload = await this.wechaty.puppet.roomPayload(this.id) + + /** + * Sync all room member contacts + */ + const memberIdList = await this.wechaty.puppet.roomMemberList(this.id) + + const doReady = async (id: string): Promise => { + try { + await this.wechaty.Contact.find({ id }) + } catch (e) { + this.wechaty.emitError(e) + } + } + + /** + * we need to use concurrencyExecuter to reduce the parallel number of the requests + */ + const CONCURRENCY = 17 + const contactIterator = concurrencyExecuter(CONCURRENCY)(doReady)(memberIdList) + + for await (const contact of contactIterator) { + void contact // just a empty loop to wait all iterator finished + } + + } + + /** + * @ignore + */ + isReady (): boolean { + return !!(this.payload) + } + + say (sayable: Sayable) : Promise + say (text: string, ...mentionList: ContactInterface[]) : Promise + say (textList: TemplateStringsArray, ...varList: any[]) : Promise + + // Huan(202006): allow fall down to the defination to get more flexibility. + // public say (...args: never[]): never + + /** + * Send message inside Room, if set [replyTo], wechaty will mention the contact as well. + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @param {(string | ContactInterface | FileBox)} textOrContactOrFileOrUrlOrMini - Send `text` or `media file` inside Room.
+ * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file + * @param {(ContactInterface | ContactInterface[])} [mention] - Optional parameter, send content inside Room, and mention @replyTo contact or contactList. + * @returns {Promise} + * + * @example + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'wechaty'}) + * + * // 1. Send text inside Room + * + * await room.say('Hello world!') + * const msg = await room.say('Hello world!') // only supported by puppet-padplus + * + * // 2. Send media file inside Room + * import { FileBox } from 'wechaty' + * const fileBox1 = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + * const fileBox2 = FileBox.fromLocal('/tmp/text.txt') + * await room.say(fileBox1) + * const msg1 = await room.say(fileBox1) // only supported by puppet-padplus + * await room.say(fileBox2) + * const msg2 = await room.say(fileBox2) // only supported by puppet-padplus + * + * // 3. Send Contact Card in a room + * const contactCard = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member + * await room.say(contactCard) + * const msg = await room.say(contactCard) // only supported by puppet-padplus + * + * // 4. Send text inside room and mention @mention contact + * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member + * await room.say('Hello world!', contact) + * const msg = await room.say('Hello world!', contact) // only supported by puppet-padplus + * + * // 5. Send text inside room and mention someone with Tagged Template + * const contact2 = await bot.Contact.find({name: 'zixia'}) // change 'zixia' to any of the room member + * await room.say`Hello ${contact}, here is the world ${contact2}` + * const msg = await room.say`Hello ${contact}, here is the world ${contact2}` // only supported by puppet-padplus + * + * // 6. send url link in a room + * + * const urlLink = new UrlLink ({ + * description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + * thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + * title : 'Welcome to Wechaty', + * url : 'https://github.com/wechaty/wechaty', + * }) + * await room.say(urlLink) + * const msg = await room.say(urlLink) // only supported by puppet-padplus + * + * // 7. send mini program in a room + * + * const miniProgram = new MiniProgram ({ + * username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + * appid : '', //optional, get from mp.weixin.qq.com + * title : '', //optional + * pagepath : '', //optional + * description : '', //optional + * thumbnailurl : '', //optional + * }) + * await room.say(miniProgram) + * const msg = await room.say(miniProgram) // only supported by puppet-padplus + * + * // 8. send location in a room + * const location = new Location ({ + * accuracy : 15, + * address : 'ๅŒ—ไบฌๅธ‚ๅŒ—ไบฌๅธ‚ๆตทๆท€ๅŒบ45 Chengfu Rd', + * latitude : 39.995120999999997, + * longitude : 116.334154, + * name : 'ไธœๅ‡ไนกไบบๆฐ‘ๆ”ฟๅบœ(ๆตทๆท€ๅŒบๆˆๅบœ่ทฏ45ๅท)', + * }) + * await room.say(location) + * const msg = await room.say(location) + */ + async say ( + sayable : Sayable | TemplateStringsArray, + ...varList : unknown[] + ): Promise { + + log.verbose('Room', 'say(%s, %s)', + sayable, + varList.join(', '), + ) + + let msgId + let text: string + + if (isTemplateStringArray(sayable)) { + msgId = await this.sayTemplateStringsArray( + sayable as TemplateStringsArray, + ...varList, + ) + } else if (typeof sayable === 'string') { + /** + * 1. string + */ + let mentionList: ContactInterface[] = [] + + if (varList.length > 0) { + const allIsContact = varList.every(c => ContactImpl.valid(c)) + if (!allIsContact) { + throw new Error('mentionList must be contact when not using TemplateStringsArray function call.') + } + + mentionList = [ ...varList as any ] + + const AT_SEPARATOR = FOUR_PER_EM_SPACE + const mentionAlias = await Promise.all(mentionList.map(async contact => + '@' + (await this.alias(contact) || contact.name()), + )) + const mentionText = mentionAlias.join(AT_SEPARATOR) + + text = mentionText + ' ' + sayable + } else { + text = sayable + } + // const receiver = { + // contactId : (mentionList.length && mentionList[0].id) || undefined, + // roomId : this.id, + // } + msgId = await this.wechaty.puppet.messageSendText( + this.id, + text, + mentionList.map(c => c.id), + ) + } else { + msgId = await deliverSayableConversationPuppet(this.wechaty.puppet)(this.id)(sayable) + } + + if (msgId) { + const msg = await this.wechaty.Message.find({ id: msgId }) + return msg + } + } + + private async sayTemplateStringsArray ( + textList: TemplateStringsArray, + ...varList: unknown[] + ) { + const mentionList = varList.filter(c => ContactImpl.valid(c)) as ContactInterface[] + + // const receiver = { + // contactId : (mentionList.length && mentionList[0].id) || undefined, + // roomId : this.id, + // } + if (varList.length === 0) { + /** + * No mention in the string + */ + return this.wechaty.puppet.messageSendText( + this.id, + textList[0]!, + ) + // TODO(huan) 20191222 it seems the following code will not happen, + // because it's equal the mentionList.length === 0 situation? + // + // XXX(huan) 20200101: See issue https://github.com/wechaty/wechaty/issues/1893 + // This is an anti-pattern usage. + // + // } else if (textList.length === 1) { + // /** + // * Constructed mention string, skip inserting @ signs + // */ + // return this.wechaty.puppet.messageSendText( + // receiver, + // textList[0], + // mentionList.map(c => c.id), + // ) + } else { // mentionList.length > 0 + /** + * Mention in the string + */ + const textListLength = textList.length + const varListLength = varList.length + if (textListLength - varListLength !== 1) { + throw new Error('Can not say message, invalid Template String Array.') + } + let finalText = '' + + let i = 0 + for (; i < varListLength; i++) { + if (ContactImpl.valid(varList[i])) { + const mentionContact: ContactInterface = varList[i] as any + const mentionName = await this.alias(mentionContact) || mentionContact.name() + finalText += textList[i] + '@' + mentionName + } else { + finalText += textList[i]! + varList[i]! + } + } + finalText += textList[i] + + return this.wechaty.puppet.messageSendText( + this.id, + finalText, + mentionList.map(c => c.id), + ) + } + } + + /** + * @desc Room Class Event Type + * @typedef RoomEventName + * @property {string} join - Emit when anyone join any room. + * @property {string} topic - Get topic event, emitted when someone change room topic. + * @property {string} leave - Emit when anyone leave the room.
+ * 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. + */ + + /** + * @desc Room Class Event Function + * @typedef RoomEventFunction + * @property {Function} room-join - (this: Room, inviteeList: Contact[] , inviter: Contact) => void + * @property {Function} room-topic - (this: Room, topic: string, oldTopic: string, changer: Contact) => void + * @property {Function} room-leave - (this: Room, leaver: Contact) => void + */ + + /** + * @listens Room + * @param {RoomEventName} event - Emit WechatyEvent + * @param {RoomEventFunction} listener - Depends on the WechatyEvent + * @return {this} - this for chain + * + * @example Event:join + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('join', (room, inviteeList, inviter) => { + * const nameList = inviteeList.map(c => c.name()).join(',') + * console.log(`Room got new member ${nameList}, invited by ${inviter}`) + * }) + * } + * + * @example Event:leave + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('leave', (room, leaverList) => { + * const nameList = leaverList.map(c => c.name()).join(',') + * console.log(`Room lost member ${nameList}`) + * }) + * } + * + * @example Event:message + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('message', (message) => { + * console.log(`Room received new message: ${message}`) + * }) + * } + * + * @example Event:topic + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('topic', (room, topic, oldTopic, changer) => { + * console.log(`Room topic changed from ${oldTopic} to ${topic} by ${changer.name()}`) + * }) + * } + * + * @example Event:invite + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your WeChat + * if (room) { + * room.on('invite', roomInvitation => roomInvitation.accept()) + * } + * + */ + // public on (event: RoomEventName, listener: (...args: any[]) => any): this { + // log.verbose('Room', 'on(%s, %s)', event, typeof listener) + + // super.on(event, listener) // Room is `Sayable` + // return this + // } + + /** + * Add contact in a room + * + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * > + * > see {@link https://github.com/wechaty/wechaty/issues/1441|Web version of WeChat closed group interface} + * + * @param {ContactInterface} contact + * @returns {Promise} + * @example + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any contact in your WeChat + * const room = await bot.Room.find({topic: 'WeChat'}) // change 'WeChat' to any room topic in your WeChat + * if (room) { + * try { + * await room.add(contact) + * } catch(e) { + * console.error(e) + * } + * } + */ + async add (contact: ContactInterface): Promise { + log.verbose('Room', 'add(%s)', contact) + await this.wechaty.puppet.roomAdd(this.id, contact.id) + } + + /** + * Remove a contact from the room + * It works only when the bot is the owner of the room + * + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * > + * > see {@link https://github.com/wechaty/wechaty/issues/1441|Web version of WeChat closed group interface} + * + * @param {ContactInterface} contact + * @returns {Promise} + * @example + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'WeChat'}) // change 'WeChat' to any room topic in your WeChat + * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any room member in the room you just set + * if (room) { + * try { + * await room.remove(contact) + * } catch(e) { + * console.error(e) + * } + * } + */ + async remove (contact: ContactInterface): Promise { + log.verbose('Room', 'del(%s)', contact) + await this.wechaty.puppet.roomDel(this.id, contact.id) + // this.delLocal(contact) + } + + /** + * Huan(202106): will be removed after Dec 31, 2023 + * @deprecated use remove(contact) instead. + */ + async del (contact: ContactImpl): Promise { + log.warn('Room', 'del() is DEPRECATED, use remove() instead.\n%s', new Error().stack) + return this.remove(contact) + } + + // private delLocal(contact: Contact): void { + // log.verbose('Room', 'delLocal(%s)', contact) + + // const memberIdList = this.payload && this.payload.memberIdList + // if (memberIdList && memberIdList.length > 0) { + // for (let i = 0; i < memberIdList.length; i++) { + // if (memberIdList[i] === contact.id) { + // memberIdList.splice(i, 1) + // break + // } + // } + // } + // } + + /** + * Bot quit the room itself + * + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @returns {Promise} + * @example + * await room.quit() + */ + async quit (): Promise { + log.verbose('Room', 'quit() %s', this) + await this.wechaty.puppet.roomQuit(this.id) + } + + async topic () : Promise + async topic (newTopic: string): Promise + + /** + * SET/GET topic from the room + * + * @param {string} [newTopic] If set this para, it will change room topic. + * @returns {Promise} + * + * @example When you say anything in a room, it will get room topic. + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const room = m.room() + * if (room) { + * const topic = await room.topic() + * console.log(`room topic is : ${topic}`) + * } + * }) + * .start() + * + * @example When you say anything in a room, it will change room topic. + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const room = m.room() + * if (room) { + * const oldTopic = await room.topic() + * await room.topic('change topic to wechaty!') + * console.log(`room topic change from ${oldTopic} to ${room.topic()}`) + * } + * }) + * .start() + */ + async topic (newTopic?: string): Promise { + log.verbose('Room', 'topic(%s)', newTopic || '') + if (!this.isReady()) { + log.warn('Room', 'topic() room not ready') + throw new Error('not ready') + } + + if (typeof newTopic === 'undefined') { + if (this.payload && this.payload.topic) { + return this.payload.topic + } else { + const memberIdList = await this.wechaty.puppet.roomMemberList(this.id) + const memberListFuture = memberIdList + .filter(id => id !== this.wechaty.puppet.currentUserId) + .map(id => this.wechaty.Contact.find({ id })) + + const memberList = (await Promise.all(memberListFuture)) + .filter(Boolean) as ContactInterface[] + + let defaultTopic = (memberList[0] && memberList[0].name()) || '' + for (let i = 1; i < 3 && memberList[i]; i++) { + defaultTopic += ',' + memberList[i]!.name() + } + return defaultTopic + } + } + + const future = this.wechaty.puppet + .roomTopic(this.id, newTopic) + .catch(e => { + log.warn('Room', 'topic(newTopic=%s) exception: %s', + newTopic, (e && e.message) || e, + ) + wechatyCaptureException(e) + }) + + return future + } + + async announce () : Promise + async announce (text: string) : Promise + + /** + * SET/GET announce from the room + * > Tips: It only works when bot is the owner of the room. + * > + * > This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @param {string} [text] If set this para, it will change room announce. + * @returns {(Promise)} + * + * @example When you say anything in a room, it will get room announce. + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'your room'}) + * const announce = await room.announce() + * console.log(`room announce is : ${announce}`) + * + * @example When you say anything in a room, it will change room announce. + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'your room'}) + * const oldAnnounce = await room.announce() + * await room.announce('change announce to wechaty!') + * console.log(`room announce change from ${oldAnnounce} to ${room.announce()}`) + */ + async announce (text?: string): Promise { + log.verbose('Room', 'announce(%s)', + typeof text === 'undefined' + ? '' + : `"${text || ''}"`, + ) + + if (typeof text === 'undefined') { + const announcement = await this.wechaty.puppet.roomAnnounce(this.id) + return announcement + } else { + await this.wechaty.puppet.roomAnnounce(this.id, text) + } + } + + /** + * Get QR Code Value of the Room from the room, which can be used as scan and join the room. + * > Tips: + * 1. This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * 2. The return should be the QR Code Data, instead of the QR Code Image. (the data should be less than 8KB. See: https://stackoverflow.com/a/12764370/1123955 ) + * @returns {Promise} + */ + async qrCode (): Promise { + log.verbose('Room', 'qrCode()') + const qrcodeValue = await this.wechaty.puppet.roomQRCode(this.id) + return guardQrCodeValue(qrcodeValue) + } + + /** + * Return contact's roomAlias in the room + * @param {ContactInterface} contact + * @returns {Promise} - If a contact has an alias in room, return string, otherwise return null + * @example + * const bot = new Wechaty() + * bot + * .on('message', async m => { + * const room = m.room() + * const contact = m.from() + * if (room) { + * const alias = await room.alias(contact) + * console.log(`${contact.name()} alias is ${alias}`) + * } + * }) + * .start() + */ + async alias (contact: ContactInterface): Promise { + const memberPayload = await this.wechaty.puppet.roomMemberPayload(this.id, contact.id) + + if (memberPayload.roomAlias) { + return memberPayload.roomAlias + } + + return undefined + } + + async readMark (hasRead: boolean): Promise + async readMark (): Promise + + /** + * Mark the conversation as read + * @param { undefined | boolean } hasRead + * + * @example + * const bot = new Wechaty() + * const room = await bot.Room.find({topic: 'xxx'}) + * await room.readMark() + */ + async readMark (hasRead?: boolean): Promise { + try { + if (typeof hasRead === 'undefined') { + return this.wechaty.puppet.conversationReadMark(this.id) + } else { + await this.wechaty.puppet.conversationReadMark(this.id, hasRead) + } + } catch (e) { + this.wechaty.emitError(e) + log.error('Room', 'readMark() exception: %s', (e as Error).message) + } + } + + /** + * Check if the room has member `contact`, the return is a Promise and must be `await`-ed + * + * @param {ContactInterface} contact + * @returns {Promise} Return `true` if has contact, else return `false`. + * @example Check whether 'lijiarui' is in the room 'wechaty' + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of contact in your WeChat + * const room = await bot.Room.find({topic: 'wechaty'}) // change 'wechaty' to any of the room in your WeChat + * if (contact && room) { + * if (await room.has(contact)) { + * console.log(`${contact.name()} is in the room wechaty!`) + * } else { + * console.log(`${contact.name()} is not in the room wechaty!`) + * } + * } + */ + async has (contact: ContactInterface): Promise { + const memberIdList = await this.wechaty.puppet.roomMemberList(this.id) + + // if (!memberIdList) { + // return false + // } + + return memberIdList + .filter(id => id === contact.id) + .length > 0 + } + + async memberAll () : Promise + async memberAll (name: string) : Promise + async memberAll (filter: PUPPET.filters.RoomMember) : Promise + + /** + * The way to search member by Room.member() + * + * @typedef RoomMemberQueryFilter + * @property {string} name -Find the contact by WeChat name in a room, equal to `Contact.name()`. + * @property {string} roomAlias -Find the contact by alias set by the bot for others in a room. + * @property {string} contactAlias -Find the contact by alias set by the contact out of a room, equal to `Contact.alias()`. + * [More Detail]{@link https://github.com/wechaty/wechaty/issues/365} + */ + + /** + * Find all contacts in a room + * + * #### definition + * - `name` the name-string set by user-self, should be called name, equal to `Contact.name()` + * - `roomAlias` the name-string set by user-self in the room, should be called roomAlias + * - `contactAlias` the name-string set by bot for others, should be called alias, equal to `Contact.alias()` + * @param {(RoomMemberQueryFilter | string)} [query] -Optional parameter, When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias + * @returns {Promise} + * @example + * const roomList:Contact[] | null = await room.findAll() + * if(roomList) + * console.log(`room all member list: `, roomList) + * const memberContactList: Contact[] | null =await room.findAll(`abc`) + * console.log(`contact list with all name, room alias, alias are abc:`, memberContactList) + */ + async memberAll ( + query?: string | PUPPET.filters.RoomMember, + ): Promise { + log.silly('Room', 'memberAll(%s)', + JSON.stringify(query) || '', + ) + + if (!query) { + return this.memberList() + } + + const contactIdList = await this.wechaty.puppet.roomMemberSearch(this.id, query) + const contactListAll = await Promise.all( + contactIdList.map(id => this.wechaty.Contact.find({ id })), + ) + + const contactList = contactListAll.filter(c => !!c) as ContactInterface[] + return contactList + } + + async member (name : string) : Promise + async member (filter: PUPPET.filters.RoomMember): Promise + + /** + * Find all contacts in a room, if get many, return the first one. + * + * @param {(RoomMemberQueryFilter | string)} queryArg -When use member(name:string), return all matched members, including name, roomAlias, contactAlias + * @returns {Promise} + * + * @example Find member by name + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'wechaty'}) // change 'wechaty' to any room name in your WeChat + * if (room) { + * const member = await room.member('lijiarui') // change 'lijiarui' to any room member in your WeChat + * if (member) {load + * @example Find member by MemberQueryFilter + * const bot = new Wechaty() + * await bot.start() + * // after logged in... + * const room = await bot.Room.find({topic: 'wechaty'}) // change 'wechaty' to any room name in your WeChat + * if (room) { + * const member = await room.member({name: 'lijiarui'}) // change 'lijiarui' to any room member in your WeChat + * if (member) { + * console.log(`wechaty room got the member: ${member.name()}`) + * } else { + * console.log(`cannot get member in wechaty room!`) + * } + * } + */ + async member ( + queryArg: string | PUPPET.filters.RoomMember, + ): Promise { + log.verbose('Room', 'member(%s)', JSON.stringify(queryArg)) + + let memberList: ContactInterface[] + // ISSUE #622 + // error TS2345: Argument of type 'string | MemberQueryFilter' is not assignable to parameter of type 'MemberQueryFilter' #622 + if (typeof queryArg === 'string') { + memberList = await this.memberAll(queryArg) + } else { + memberList = await this.memberAll(queryArg) + } + + if (memberList.length <= 0) { + return undefined + } + + if (memberList.length > 1) { + log.warn('Room', 'member(%s) get %d contacts, use the first one by default', JSON.stringify(queryArg), memberList.length) + } + return memberList[0]! + } + + /** + * Huan(202110): + * - Q: why this method marked as `privated` before? + * - A: it is for supporting the `memberAll()` API + * + * Get all room member from the room + * + * @returns {Promise} + * @example + * await room.memberList() + */ + protected async memberList (): Promise { + log.verbose('Room', 'memberList()') + + const memberIdList = await this.wechaty.puppet.roomMemberList(this.id) + + // if (!memberIdList) { + // log.warn('Room', 'memberList() not ready') + // return [] + // } + + const contactListAll = await Promise.all( + memberIdList.map( + id => this.wechaty.Contact.find({ id }), + ), + ) + + const contactList = contactListAll.filter(c => !!c) as ContactInterface[] + return contactList + } + + /** + * Get room's owner from the room. + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * @returns {(ContactInterface | undefined)} + * @example + * const owner = room.owner() + */ + owner (): undefined | ContactInterface { + log.verbose('Room', 'owner()') + + const ownerId = this.payload && this.payload.ownerId + if (!ownerId) { + return undefined + } + + const owner = (this.wechaty.Contact as typeof ContactImpl).load(ownerId) + return owner + } + + /** + * Get avatar from the room. + * @returns {FileBox} + * @example + * const fileBox = await room.avatar() + * const name = fileBox.name + * fileBox.toFile(name) + */ + async avatar (): Promise { + log.verbose('Room', 'avatar()') + + return this.wechaty.puppet.roomAvatar(this.id) + } + +} + +class RoomImpl extends validationMixin(RoomMixin)() {} +interface RoomImplInterface extends RoomImpl {} + +type RoomProtectedProperty = + | 'ready' + +type RoomInterface = Omit + +type RoomConstructor = Constructor< + RoomImplInterface, + Omit +> + +export type { + RoomConstructor, + RoomProtectedProperty, + RoomInterface, +} +export { + RoomImpl, +} diff --git a/src/user-modules/tag.ts b/src/user-modules/tag.ts new file mode 100644 index 0000000..aa0bc8a --- /dev/null +++ b/src/user-modules/tag.ts @@ -0,0 +1,202 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + log, +} from 'wechaty-puppet' + +import type { Constructor } from 'clone-class' + +import { ContactInterface, ContactImpl } from './contact.js' +import { FavoriteInterface, FavoriteImpl } from './favorite.js' + +import { + poolifyMixin, +} from '../user-mixins/poolify.js' +import { validationMixin } from '../user-mixins/validation.js' +import { + wechatifyMixinBase, +} from '../user-mixins/wechatify.js' + +const MixinBase = poolifyMixin( + wechatifyMixinBase(), +)() + +class TagMixin extends MixinBase { + + /** + * @hideconstructor + */ + constructor ( + public readonly id: string, + ) { + super() + log.silly('Tag', `constructor(${id})`) + } + + /** + * Get a Tag instance for "tag" + * + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @static + * @param {string} [tag] the tag name which want to create + * @returns {Promise} + * @example + * const bot = new Wechaty() + * await bot.Tag.get('TagName') + */ + static async get ( + tag: string, + ): Promise { + log.verbose('Tag', 'get(%s)', tag) + return this.load(tag) + } + + /** + * Delete a tag from Wechat + * + * If you want to delete a tag, please make sure there's no more Contact/Favorite(s) are using this tag. + * If this tag is be used by any Contact/Favorite, then it can not be deleted. + * (This is for protecting the tag being deleted by mistake) + * + * @static + * @returns {Promise} + * @example + * const tag = wechaty.Tag.get('tag') + * await wechaty.Tag.delete(tag) + */ + + /** + * TODO: refactoring the target: do not use ContactIml or FavoriteImpl + */ + static async delete ( + tag: TagInterface, + target?: typeof ContactImpl | typeof FavoriteImpl, + ): Promise { + log.verbose('Tag', 'static delete(%s)', tag) + + /** + * Huan(202110) TODO: refactory this design: + * 1. we should not pass `typeof ContactImpl` as argument + * 2. find a better way to manage tag. + */ + + try { + + /** + * TODO(huan): add tag check code here for checking if this tag is still being used. + */ + + if (!target || target === ContactImpl || target === this.wechaty.Contact) { + await this.wechaty.puppet.tagContactDelete(tag.id) + // TODO: + // } else if (!target || target === Favorite || target === this.wechaty.Favorite) { + // await this.wechaty.puppet.tagFavoriteDelete(tag.id) + } + } catch (e) { + this.wechaty.emitError(e) + log.error('Tag', 'static delete() exception: %s', (e as Error).message) + } + } + + /** + * Add tag for contact + * + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @param {ContactInterface} [to] the contact which need to add tag + * @returns {Promise} + * @example + * await tag.add(contact) + */ + async add ( + to: ContactInterface | FavoriteInterface, + ): Promise { + log.verbose('Tag', 'add(%s) for %s', to, this.id) + + /** + * Huan(202110): TODO: refactory this design: + * 1. we should not pass `typeof ContactImpl` as argument + * 2. use instanceof to check the type of `to` + */ + try { + if (ContactImpl.valid(to)) { + await this.wechaty.puppet.tagContactAdd(this.id, to.id) + } else if (FavoriteImpl.valid(to)) { + // TODO: await this.wechaty.puppet.tagAddFavorite(this.tag, to.id) + } + } catch (e) { + this.wechaty.emitError(e) + log.error('Tag', 'add() exception: %s', (e as Error).message) + throw new Error(`add error : ${(e as Error)}`) + } + } + + /** + * Remove this tag from Contact/Favorite + * + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @returns {Promise} + * @example + * await tag.remove(contact) + */ + async remove (from: ContactInterface | FavoriteInterface): Promise { + log.verbose('Tag', 'remove(%s) for %s', from, this.id) + + /** + * Huan(202110): TODO: refactory this design: + * 1. we should not pass `typeof ContactImpl` as argument + * 2. use instanceof to check the type of `to` + */ + + try { + if (from instanceof ContactImpl) { + await this.wechaty.puppet.tagContactRemove(this.id, from.id) + } else if (from instanceof FavoriteImpl) { + // TODO await this.wechaty.puppet.tagRemoveFavorite(this.tag, from.id) + } + } catch (e) { + this.wechaty.emitError(e) + log.error('Tag', 'remove() exception: %s', (e as Error).message) + throw new Error(`remove error : ${e}`) + } + } + +} + +class TagImpl extends validationMixin(TagMixin)() {} +interface TagInterface extends TagImpl {} + +type TagConstructor = Constructor< + TagInterface, + typeof TagImpl +> + +export type { + TagConstructor, + TagInterface, +} +export { + TagImpl, +} diff --git a/src/user-modules/url-link.spec.ts b/src/user-modules/url-link.spec.ts new file mode 100755 index 0000000..5ed41dd --- /dev/null +++ b/src/user-modules/url-link.spec.ts @@ -0,0 +1,35 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { test } from 'tstest' +import type { Wechaty } from '../mods/mod.js' +import { MessageImpl } from './message.js' + +import { wechatifyUserModule } from './mod.js' +import { UrlLinkImpl } from './url-link.js' + +const wechaty = { + puppet: {} as any, +} as any as Wechaty + +const UrlLinkTest = wechatifyUserModule(UrlLinkImpl)(wechaty) + +test('UrlLink.create() for wechaty.js.org', async t => { + const URL = 'https://wechaty.js.org/2020/07/02/wechat-bot-in-ten-minutes' + const EXPECTED_PAYLOAD = { + description: 'Conversational RPA SDK for Chatbot Makers', + thumbnailUrl: 'https://wechaty.js.org/assets/contributors/luweicn/avatar.webp', + title: 'ๅๅˆ†้’Ÿๅฎž็Žฐไธ€ไธชๆ™บ่ƒฝ้—ฎ็ญ”ๅพฎไฟก่Šๅคฉๆœบๅ™จไบบ', + url: 'https://wechaty.js.org/2020/07/02/wechat-bot-in-ten-minutes', + } + + const urlLink = await UrlLinkTest.create(URL) + t.equal(urlLink.title(), EXPECTED_PAYLOAD.title, 'should have title') + t.equal(urlLink.description(), EXPECTED_PAYLOAD.description, 'should have description') + t.equal(urlLink.url(), EXPECTED_PAYLOAD.url, 'should have url') + t.equal(urlLink.thumbnailUrl(), EXPECTED_PAYLOAD.thumbnailUrl, 'should have thumbnailUrl') +}) + +test('UrlLink.valid()', async t => { + const urlLink = new UrlLinkTest({} as any) + t.ok(UrlLinkImpl.valid(urlLink), 'should pass the validation of UrlLinkImpl') + t.notOk(MessageImpl.valid(urlLink), 'should not pass the validation of MessageImpl') +}) diff --git a/src/user-modules/url-link.ts b/src/user-modules/url-link.ts new file mode 100644 index 0000000..1faf843 --- /dev/null +++ b/src/user-modules/url-link.ts @@ -0,0 +1,145 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import Url from 'url' + +import type * as PUPPET from 'wechaty-puppet' +import type { Constructor } from 'clone-class' + +import { openGraph } from '../helper-functions/open-graph.js' +import { validationMixin } from '../user-mixins/validation.js' +import { wechatifyMixinBase } from '../user-mixins/wechatify.js' +import { log } from '../config.js' + +class UrlLinkMixin extends wechatifyMixinBase() { + + /** + * + * Create from URL + * + */ + static async create ( + url: string, + fallback?: Partial< + Omit< + PUPPET.payloads.UrlLink, + 'url' + > + >, + ): Promise { + log.verbose('UrlLink', 'create(%s)', url) + + const meta = await openGraph(url) + + let description : undefined | string + let thumbnailUrl : undefined | string + let title : string + + if (meta.image) { + if (typeof meta.image === 'string') { + thumbnailUrl = meta.image + } else if (Array.isArray(meta.image)) { + thumbnailUrl = meta.image[0] + } else { + if (Array.isArray(meta.image.url)) { + thumbnailUrl = meta.image.url[0] + } else if (meta.image.url) { + thumbnailUrl = meta.image.url + } + } + } + + if (Array.isArray(meta.title)) { + title = meta.title[0]! + } else { + title = meta.title + } + + if (Array.isArray(meta.description)) { + description = meta.description[0] + } else if (meta.description) { + description = meta.description + } else { + description = title + } + + if (thumbnailUrl && !thumbnailUrl.startsWith('http')) { + const resolvedUrl = new Url.URL(thumbnailUrl, url) + thumbnailUrl = resolvedUrl.toString() + } + + const payload: PUPPET.payloads.UrlLink = { + description : description || fallback?.description || '', + thumbnailUrl : thumbnailUrl || fallback?.thumbnailUrl || '', + title : title || fallback?.title || '', + url, + } + + return new this(payload) + } + + /* + * @hideconstructor + */ + constructor ( + public readonly payload: PUPPET.payloads.UrlLink, + ) { + super() + log.verbose('UrlLink', 'constructor()') + // Huan(202110): it is ok to create a raw one without wechaty instance + // guardWechatifyClass.call(this, UrlLink) + } + + override toString (): string { + return `UrlLink<${this.payload.url}>` + } + + url (): string { + return this.payload.url + } + + title (): string { + return this.payload.title + } + + thumbnailUrl (): undefined | string { + return this.payload.thumbnailUrl + } + + description (): undefined | string { + return this.payload.description + } + +} + +class UrlLinkImpl extends validationMixin(UrlLinkMixin)() {} +interface UrlLinkInterface extends UrlLinkImpl {} + +type UrlLinkConstructor = Constructor< + UrlLinkInterface, + typeof UrlLinkImpl +> + +export type { + UrlLinkConstructor, + UrlLinkInterface, +} +export { + UrlLinkImpl, +} diff --git a/src/wechaty-builder.spec.ts b/src/wechaty-builder.spec.ts new file mode 100755 index 0000000..d2668c9 --- /dev/null +++ b/src/wechaty-builder.spec.ts @@ -0,0 +1,79 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { test } from 'tstest' + +import { PuppetMock } from 'wechaty-puppet-mock' + +import { WechatyBuilder } from './wechaty-builder.js' + +test('WechatyBuilder class', async t => { + const wechaty1 = WechatyBuilder.build() + const wechaty2 = WechatyBuilder.build() + t.not(wechaty1, wechaty2, 'should build two different Wechaty instance') + + const singleton1 = WechatyBuilder.singleton() + const singleton2 = WechatyBuilder.singleton() + t.equal(singleton1, singleton2, 'should get the same singleton instance') + + const wechaty = WechatyBuilder.build({ puppet: 'wechaty-puppet-mock' }) + await wechaty.start() + t.ok(PuppetMock.validInstance(wechaty.puppet), 'should set options.puppet to mock') + await wechaty.stop() +}) + +test('throw when set options twice', async t => { + class WechatyBuilderTest extends WechatyBuilder { + + constructor () { super() } + + static override new () { return new this() } + + override options (...args: any[]) { + super.options(...args) + return this + } + + } + + const builder = WechatyBuilderTest.new() + t.doesNotThrow(() => builder.options({}), 'should not throw for the first time') + t.doesNotThrow(() => builder.options({}), 'should not throw as long as the `options` is an empty object') + + const options = { name: 'bot' } + t.doesNotThrow(() => builder.options(options), 'should not throw for setting non-empty `options` for the first time') + t.throws(() => builder.options(options), 'should throw for setting non-empty `options` again') +}) + +test('WechatyBuilder class static', async t => { + const wechaty1 = WechatyBuilder.build() + const wechaty2 = WechatyBuilder.build() + t.not(wechaty1, wechaty2, 'should build two different Wechaty instance') + + const singleton1 = WechatyBuilder.singleton() + const singleton2 = WechatyBuilder.singleton() + t.equal(singleton1, singleton2, 'should get the same singleton instance') + + const wechaty = WechatyBuilder.build({ puppet: 'wechaty-puppet-mock' }) + await wechaty.start() + t.ok(PuppetMock.validInstance(wechaty.puppet), 'should set options.puppet to mock') + await wechaty.stop() +}) diff --git a/src/wechaty-builder.ts b/src/wechaty-builder.ts new file mode 100644 index 0000000..9a0983b --- /dev/null +++ b/src/wechaty-builder.ts @@ -0,0 +1,123 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { log } from 'wechaty-puppet' + +import { + WechatyInterface, + WechatyImpl, + type WechatyOptions, +} from './wechaty/mod.js' + +interface BuilderInterface { + build (options?: WechatyOptions): WechatyInterface +} + +class WechatyBuilder implements BuilderInterface { + + private static _instance?: WechatyInterface + + static valid (target: any): target is WechatyInterface { + return WechatyImpl.valid(target) + } + + /** + * Create an instance of Wechaty + * + * @param {WechatyOptions} [options={}] + * + * @example The World's Shortest ChatBot Code: 6 lines of JavaScript + * import { WechatyBuilder } from 'wechaty' + * + * WechatyBuilder.build(options) // instance() for singleton mode + * .on('scan', (url, status) => console.log(`Scan QR Code to login: ${status}\n${url}`)) + * .on('login', user => console.log(`User ${user} logged in`)) + * .on('message', message => console.log(`Message: ${message}`)) + * .start() + */ + static build (options?: WechatyOptions): WechatyInterface { + return WechatyBuilder.new().options(options).build() + } + + /** + * @param options is a `WechatyOptions` object, it can only be set once + */ + static singleton (options?: WechatyOptions): WechatyInterface { + const builder = new WechatyBuilder() + if (options) { + builder.options(options) + } + return builder.singleton().build() + } + + protected static new (): WechatyBuilder { return new this() } + protected constructor () {} + + protected _singleton = false + protected _options: WechatyOptions = {} + + protected singleton (): WechatyBuilder { + log.verbose('WechatyBuilder', 'singleton()') + this._singleton = true + return this + } + + protected options (options?: WechatyOptions): WechatyBuilder { + log.verbose('WechatyBuilder', 'singleton()') + if (Object.keys(this._options).length > 0) { + throw new Error([ + 'WechatyBuilder options() can only be set once', + 'Create a new WechatyBuilder if you need different options', + ].join('\n')) + } + + if (options) { + this._options = options + } + + return this + } + + build (): WechatyInterface { + if (this._singleton) { + return this.singletonInstance() + } + return this.newInstance() + } + + protected singletonInstance (): WechatyInterface { + log.verbose('WechatyBuilder', 'singletonInstance()') + if (!WechatyBuilder._instance) { + WechatyBuilder._instance = this.newInstance() + } + return WechatyBuilder._instance + } + + protected newInstance (): WechatyInterface { + log.verbose('WechatyBuilder', 'newInstance()') + return new WechatyImpl(this._options) + } + +} + +export { + type WechatyOptions, + type BuilderInterface, + WechatyBuilder, +} diff --git a/src/wechaty-mixins/gerror-mixin.spec.ts b/src/wechaty-mixins/gerror-mixin.spec.ts new file mode 100755 index 0000000..d14a9be --- /dev/null +++ b/src/wechaty-mixins/gerror-mixin.spec.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { test } from 'tstest' + +import type { + GErrorMixin, + ProtectedPropertyGErrorMixin, +} from './gerror-mixin.js' + +test('ProtectedPropertyGErrorMixin', async t => { + type NotExistInMixin = Exclude> + type NotExistTest = NotExistInMixin extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Mixin properties for every protected property') +}) diff --git a/src/wechaty-mixins/gerror-mixin.ts b/src/wechaty-mixins/gerror-mixin.ts new file mode 100644 index 0000000..652afe2 --- /dev/null +++ b/src/wechaty-mixins/gerror-mixin.ts @@ -0,0 +1,78 @@ +import { log } from 'wechaty-puppet' +import { + GError, + WrapAsync, + wrapAsyncError, +} from 'gerror' + +import { wechatyCaptureException } from '../raven.js' + +import type { WechatySkeleton } from '../wechaty/mod.js' + +const gErrorMixin = (mixinBase: MixinBase) => { + log.verbose('WechatyGErrorMixin', 'gErrorMixin(%s)', mixinBase.name) + + abstract class GErrorMixin extends mixinBase { + + constructor (...args: any[]) { + super(...args) + } + + /** + * Wrap promise in sync way (catch error by emitting it) + * 1. convert a async callback function to be sync function + * by catcing any errors and emit them to error event + * 2. wrap a Promise by catcing any errors and emit them to error event + */ + wrapAsync: WrapAsync = wrapAsyncError((e: any) => this.emit('error', e)) + + /** + * Wechaty internally can use `emit('error' whatever)` to emit any error + * But the external call can only emit GError. + * That's the reason why we need the below `emitError(e: any) + */ + emitError (e: any): void { + this.emit('error', e) + } + + /** + * Convert any error to GError, + * and emit `error` event with GError + */ + override emit (event: any, ...args: any) { + if (event !== 'error') { + return super.emit(event, ...args) + } + + /** + * Dealing with the `error` event + */ + const arg0 = args[0] + let gerror: GError + + if (arg0 instanceof GError) { + gerror = arg0 + } else { + gerror = GError.from(arg0) + } + + wechatyCaptureException(gerror) + return super.emit('error', gerror) + } + + } + + return GErrorMixin +} + +type GErrorMixin = ReturnType + +type ProtectedPropertyGErrorMixin = never + +export type { + GErrorMixin, + ProtectedPropertyGErrorMixin, +} +export { + gErrorMixin, +} diff --git a/src/wechaty-mixins/io-mixin.ts b/src/wechaty-mixins/io-mixin.ts new file mode 100644 index 0000000..d27cb59 --- /dev/null +++ b/src/wechaty-mixins/io-mixin.ts @@ -0,0 +1,117 @@ +import { log } from 'wechaty-puppet' +import { Io } from '../io.js' +import type { WechatyOptions } from '../wechaty-builder.js' + +import type { WechatySkeleton } from '../wechaty/mod.js' +import type { GErrorMixin } from './gerror-mixin.js' + +/** + * Huan(202111): we should not include the IO logic internally + * + * TODO: remove all IO related logics from Wechaty internal + */ +const ioMixin = (mixinBase: MixinBase) => { + log.verbose('WechatyIoMixin', 'ioMixin(%s)', mixinBase.name) + + abstract class IoMixin extends mixinBase { + + __io?: Io + get io (): Io { + if (!this.__io) { + throw new Error('NO IO') + } + return this.__io + } + + __ioToken?: string + + constructor (...args: any[]) { + log.verbose('WechatyIoMixin', 'constructor()') + super(...args) + + const options = args[0] || {} as WechatyOptions + if (options.ioToken) { + this.__ioToken = options.ioToken + } + } + + override async start (): Promise { + log.verbose('WechatyIoMixin', 'start()') + + await super.start() + + if (!this.__ioToken) { + return + } + + /** + * Clean the memory leak-ed io (?) + */ + if (this.__io) { + log.error('WechatyIoMixin', 'start() found existing io instance: stopping...') + try { + await this.__io.stop() + } catch (e) { + this.emitError(e) + } + log.error('WechatyIoMixin', 'start() found existing io instance: stopping... done') + this.__io = undefined + } + + /** + * Initialize IO instance + */ + this.__io = new Io({ + token : this.__ioToken, + wechaty : this as any, // <- FIXME: remove any, Huan(202111) + }) + + log.verbose('WechatyIoMixin', 'start() starting io ...') + await this.__io.start() + log.verbose('WechatyIoMixin', 'start() starting io ... done') + } + + override async stop (): Promise { + log.verbose('WechatyIoMixin', 'stop()') + + try { + if (!this.__io) { + return + } + + const io = this.__io + this.__io = undefined + + try { + log.verbose('WechatyIoMixin', 'stop() starting io ...') + await io.stop() + log.verbose('WechatyIoMixin', 'stop() starting io ... done') + } catch (e) { + this.emitError(e) + } + + } finally { + await super.stop() + } + + } + + } + + return IoMixin +} + +type IoMixin = ReturnType + +type ProtectedPropertyIoMixin = + | '__io' + | '__ioToken' + | 'io' + +export type { + IoMixin, + ProtectedPropertyIoMixin, +} +export { + ioMixin, +} diff --git a/src/wechaty-mixins/login-mixin.spec.ts b/src/wechaty-mixins/login-mixin.spec.ts new file mode 100755 index 0000000..6dc2802 --- /dev/null +++ b/src/wechaty-mixins/login-mixin.spec.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { test } from 'tstest' + +import type { + LoginMixin, + ProtectedPropertyLoginMixin, +} from './login-mixin.js' + +test('ProtectedPropertyLoginMixin', async t => { + type NotExistInMixin = Exclude> + type NotExistTest = NotExistInMixin extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Mixin properties for every protected property') +}) diff --git a/src/wechaty-mixins/login-mixin.ts b/src/wechaty-mixins/login-mixin.ts new file mode 100644 index 0000000..b4bab29 --- /dev/null +++ b/src/wechaty-mixins/login-mixin.ts @@ -0,0 +1,124 @@ +import { log } from 'wechaty-puppet' + +import type { + ContactSelfImpl, + ContactSelfInterface, +} from '../user-modules/mod.js' +import type { WechatySkeleton } from '../wechaty/mod.js' + +import type { GErrorMixin } from './gerror-mixin.js' +import type { PuppetMixin } from './puppet-mixin.js' + +const loginMixin = (mixinBase: MixinBase) => { + log.verbose('WechatyLoginMixin', 'loginMixin(%s)', mixinBase.name) + + abstract class LoginMixin extends mixinBase { + + get authQrCode (): undefined | string { + return this.puppet.authQrCode + } + + /** + * The current user + * + * @returns {ContactSelfInterface} + * @example + * const contact = bot.currentUser + * console.log(`Bot is ${contact.name()}`) + */ + get currentUser (): ContactSelfInterface { + return (this.ContactSelf as typeof ContactSelfImpl) + .load(this.puppet.currentUserId) + } + + /** + * Get the logon / logoff state + * + * @returns {boolean} + * @example + * if (bot.isLoggedin) { + * console.log('Bot logged in') + * } else { + * console.log('Bot not logged in') + * } + */ + get isLoggedIn (): boolean { + try { + // the `this.puppet` might not be initialized yet + return this.puppet.isLoggedIn + } catch (e) { + this.emit('error', e) + + log.warn('WechatyLoginMixin', 'get isLoggedIn puppet instance is not ready yet') + // https://github.com/wechaty/wechaty/issues/1878 + return false + } + } + + __loginMixinInited = false + + constructor (...args: any[]) { + log.verbose('WechatyLoginMixin', 'constructor()') + super(...args) + } + + override async init (): Promise { + log.verbose('WechatyLoginMixin', 'init()') + await super.init() + + if (this.__loginMixinInited) { + return + } + this.__loginMixinInited = true + } + + /** + * Logout the bot + * + * @returns {Promise} + * @example + * await bot.logout() + */ + async logout (): Promise { + log.verbose('WechatyLoginMixin', 'logout()') + await this.puppet.logout() + } + + /** + * @deprecated: use `isLoggedIn` property instead. will be removed after Dec 31, 2022 + */ + logonoff (): boolean { + log.warn('WechatyLoginMixin', 'logonoff() is deprecated: use `isLoggedIn` property instead.\n%s', new Error().stack) + return this.isLoggedIn + } + + /** + * Will be removed after Dec 31, 2022 + * @deprecated use {@link Wechaty#currentUser} instead + */ + userSelf () { + log.warn('WechatyLoginMixin', 'userSelf() deprecated: use currentUser instead.\n%s', + new Error().stack, + ) + return this.currentUser + } + + } + + return LoginMixin +} + +type LoginMixin = ReturnType + +type ProtectedPropertyLoginMixin = + | 'userSelf' // deprecated: use `currentUser` instead. (will be removed after Dec 31, 2022) + | 'logonoff' // deprecated: use `isLoggedIn` instead. ((will be removed after Dec 31, 2022) + | '__loginMixinInited' + +export type { + LoginMixin, + ProtectedPropertyLoginMixin, +} +export { + loginMixin, +} diff --git a/src/wechaty-mixins/misc-mixin.ts b/src/wechaty-mixins/misc-mixin.ts new file mode 100644 index 0000000..0611fdb --- /dev/null +++ b/src/wechaty-mixins/misc-mixin.ts @@ -0,0 +1,93 @@ +import { log } from 'wechaty-puppet' + +import { VERSION } from '../config.js' + +import type { WechatySkeleton } from '../wechaty/mod.js' + +import type { GErrorMixin } from './gerror-mixin.js' +import type { PuppetMixin } from './puppet-mixin.js' + +const miscMixin = (mixinBase: MixinBase) => { + log.verbose('WechatyMiscMixin', 'miscMixin(%s)', mixinBase.name) + + abstract class MiscMixin extends mixinBase { + + constructor (...args: any[]) { + super(...args) + } + + /** + * @ignore + */ + override toString () { + if (Object.keys(this.__options).length <= 0) { + return this.constructor.name + } + + return [ + 'Wechaty#', + this.id, + `<${(this.__options.puppet) || ''}>`, + `(${(this.__memory && this.__memory.name) || ''})`, + ].join('') + } + + /** + * Wechaty bot name set by `options.name` + * default: `wechaty` + */ + name () { + return this.__options.name || 'wechaty' + } + + /** + * @ignore + * Return version of Wechaty + * + * @returns {string} - the version number + * @example + * console.log(Wechaty.instance().version()) // return '#git[af39df]' + * console.log(Wechaty.instance().version(true)) // return '0.7.9' + */ + version (): string { + return VERSION + } + + /** + * @ignore + */ + async sleep (milliseconds: number): Promise { + await new Promise(resolve => + setTimeout(resolve, milliseconds), + ) + } + + /** + * @private + */ + ding (data?: string): void { + log.silly('WechatyMiscMixin', 'ding(%s)', data || '') + + try { + this.puppet.ding(data) + } catch (e) { + this.emitError(e) + } + } + + } + + return MiscMixin +} + +type MiscMixin = ReturnType + +type ProtectedPropertyMiscMixin = never + +export type { + MiscMixin, + ProtectedPropertyMiscMixin, +} +export { + miscMixin, +} diff --git a/src/wechaty-mixins/mod.ts b/src/wechaty-mixins/mod.ts new file mode 100644 index 0000000..c86e21b --- /dev/null +++ b/src/wechaty-mixins/mod.ts @@ -0,0 +1,84 @@ +import type { + ProtectedPropertyGErrorMixin, + GErrorMixin, +} from './gerror-mixin.js' +import { + gErrorMixin, +} from './gerror-mixin.js' + +import type { + ProtectedPropertyPluginMixin, + PluginMixin, +} from './plugin-mixin.js' +import { + pluginMixin, +} from './plugin-mixin.js' + +import type { + ProtectedPropertyPuppetMixin, + PuppetMixin, +} from './puppet-mixin.js' +import { + puppetMixin, +} from './puppet-mixin.js' + +import type { + ProtectedPropertyWechatifyUserModuleMixin, + WechatifyUserModuleMixin, +} from './wechatify-user-module-mixin.js' +import { + wechatifyUserModuleMixin, +} from './wechatify-user-module-mixin.js' + +import type { + ProtectedPropertyIoMixin, + IoMixin, +} from './io-mixin.js' +import { + ioMixin, +} from './io-mixin.js' + +import type { + ProtectedPropertyMiscMixin, + MiscMixin, +} from './misc-mixin.js' +import { + miscMixin, +} from './misc-mixin.js' + +import type { + ProtectedPropertyLoginMixin, + LoginMixin, +} from './login-mixin.js' +import { + loginMixin, +} from './login-mixin.js' + +type WechatyMixinProtectedProperty = + | ProtectedPropertyGErrorMixin + | ProtectedPropertyIoMixin + | ProtectedPropertyLoginMixin + | ProtectedPropertyMiscMixin + | ProtectedPropertyPluginMixin + | ProtectedPropertyPuppetMixin + | ProtectedPropertyWechatifyUserModuleMixin + +export type { + GErrorMixin, + IoMixin, + LoginMixin, + MiscMixin, + PluginMixin, + PuppetMixin, + WechatifyUserModuleMixin, + WechatyMixinProtectedProperty, +} +export { + gErrorMixin, + ioMixin, + loginMixin, + miscMixin, + pluginMixin, + puppetMixin, + wechatifyUserModuleMixin, +} diff --git a/src/wechaty-mixins/plugin-mixin.spec.ts b/src/wechaty-mixins/plugin-mixin.spec.ts new file mode 100755 index 0000000..2f22033 --- /dev/null +++ b/src/wechaty-mixins/plugin-mixin.spec.ts @@ -0,0 +1,98 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { + test, + sinon, +} from 'tstest' + +import { function as FP } from 'fp-ts' + +/** + * Huan(202201): must import `./wechaty-impl.js` first + * + * Or will throw error: + * + * ReferenceError: Cannot access 'wechatifyUserModuleMixin' before initialization + * at file:///home/huan/git/wechaty/wechaty/src/wechaty/wechaty-base.ts:58:3 * + * + * TODO: find out why + */ +import '../wechaty/wechaty-impl.js' + +import { + gErrorMixin, +} from '../wechaty-mixins/mod.js' + +import { + WechatySkeleton, +} from '../wechaty/wechaty-skeleton.js' + +import { + pluginMixin, + PluginMixin, + ProtectedPropertyPluginMixin, +} from './plugin-mixin.js' +import { miscMixin } from './misc-mixin.js' +import { puppetMixin } from './puppet-mixin.js' +import { ioMixin } from './io-mixin.js' +import { wechatifyUserModuleMixin } from './wechatify-user-module-mixin.js' + +test('ProtectedPropertyPluginMixin', async t => { + type NotExistInMixin = Exclude> + type NotExistTest = NotExistInMixin extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Mixin properties for every protected property') +}) + +test('PluginMixin smoke testing', async t => { + const sandbox = sinon.createSandbox({ + useFakeTimers: true, + }) + + const mixinBase = FP.pipe( + WechatySkeleton, + gErrorMixin, + ioMixin, + wechatifyUserModuleMixin, + puppetMixin, + miscMixin, + pluginMixin, + ) + + class PluginMixinTest extends mixinBase { + + counter = 0 + + } + + const Plugin = (wechaty: any) => { + wechaty.counter++ + return () => wechaty.counter-- + } + + const pluginMixinTest = new PluginMixinTest({ puppet: 'wechaty-puppet-mock' }) + let future + + const unuse = pluginMixinTest.use(Plugin) + + t.equal(pluginMixinTest.counter, 1, 'should call plugin function right after use before start') + future = pluginMixinTest.start() + t.equal(pluginMixinTest.counter, 1, 'should call plugin function right after start') + + /** + * finish initializing the system + */ + await sandbox.clock.runAllAsync() + await future + // t.equal(pluginMixinTest.counter, 1, 'should call plugin function after start') + + future = pluginMixinTest.stop() + await sandbox.clock.runAllAsync() + await future + t.equal(pluginMixinTest.counter, 1, 'should not clean plugin context after stop') + + unuse() + t.equal(pluginMixinTest.counter, 0, 'should clean plugin context after call unuse()') + + sandbox.restore() +}) diff --git a/src/wechaty-mixins/plugin-mixin.ts b/src/wechaty-mixins/plugin-mixin.ts new file mode 100644 index 0000000..f390867 --- /dev/null +++ b/src/wechaty-mixins/plugin-mixin.ts @@ -0,0 +1,99 @@ +import { log } from 'wechaty-puppet' + +import type { + WechatyPlugin, + WechatyPluginUninstaller, +} from '../plugin.js' +import { + isWechatyPluginUninstaller, +} from '../plugin.js' + +import type { + WechatySkeleton, +} from '../wechaty/mod.js' + +import type { GErrorMixin } from './gerror-mixin.js' +import type { MiscMixin } from './misc-mixin.js' + +interface Plugable { + use ( + ...plugins: ( + | WechatyPlugin + | WechatyPlugin[] + )[] + ): WechatyPluginUninstaller +} + +const pluginMixin = (mixinBase: MixinBase) => { + log.verbose('WechatyPluginMixin', 'pluginMixin(%s)', mixinBase.name) + + abstract class PluginMixin extends mixinBase implements Plugable { + + constructor (...args: any[]) { + super(...args) + } + + /** + * @param {WechatyPlugin[]} plugins - The plugins you want to use + * + * @return {WechatyInterface} - this for chaining, + * + * @desc + * For wechaty ecosystem, allow user to define a 3rd party plugin for the all wechaty instances + * + * @example + * // Report all chat message to my server. + * + * function WechatyReportPlugin(options: { url: string }) { + * return function (this: Wechaty) { + * this.on('message', message => http.post(options.url, { data: message })) + * } + * } + * + * bot.use(WechatyReportPlugin({ url: 'http://somewhere.to.report.your.data.com' }) + */ + use ( + ...plugins: ( + WechatyPlugin | WechatyPlugin[] + )[] + ): WechatyPluginUninstaller { + const pluginList = plugins.flat() + log.verbose('WechatyPluginMixin', 'use() total %d plugins', pluginList.length) + + const uninstallerList: WechatyPluginUninstaller[] = [] + + for (const plugin of pluginList) { + log.verbose('WechatyPluginMixin', 'use() installing Plugin %s on Wechaty %s ...', plugin.name, this.name()) + + const uninstaller = plugin(this as any) // <- Huan(202110): TODO: remove any + if (isWechatyPluginUninstaller(uninstaller)) { + log.verbose('WechatyPluginMixin', 'use() saving uninstaller for Plugin %s on Wechaty %s ...', plugin.name, this.name()) + uninstallerList.push(uninstaller) + } + } + + /** + * Return the function to uninstall all plugins + */ + return () => uninstallerList.forEach(uninstaller => { + log.verbose('WechatyPluginMixin', 'use() uninstalling Plugin %s on Wechaty %s ...', uninstaller.name, this.name()) + uninstaller() + log.verbose('WechatyPluginMixin', 'use() uninstalling Plugin %s on Wechaty %s ... done', uninstaller.name, this.name()) + }) + } + + } + + return PluginMixin +} + +type PluginMixin = ReturnType + +type ProtectedPropertyPluginMixin = never + +export { + type Plugable, + type PluginMixin, + type ProtectedPropertyPluginMixin, + pluginMixin, +} diff --git a/src/wechaty-mixins/puppet-mixin.spec.ts b/src/wechaty-mixins/puppet-mixin.spec.ts new file mode 100755 index 0000000..ea4a02b --- /dev/null +++ b/src/wechaty-mixins/puppet-mixin.spec.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { test } from 'tstest' + +import type { + PuppetMixin, + ProtectedPropertyPuppetMixin, +} from './puppet-mixin.js' + +test('ProtectedPropertyPuppetMixin', async t => { + type NotExistInMixin = Exclude> + type NotExistTest = NotExistInMixin extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Mixin properties for every protected property') +}) diff --git a/src/wechaty-mixins/puppet-mixin.ts b/src/wechaty-mixins/puppet-mixin.ts new file mode 100644 index 0000000..b9909b1 --- /dev/null +++ b/src/wechaty-mixins/puppet-mixin.ts @@ -0,0 +1,502 @@ +import * as PUPPET from 'wechaty-puppet' +import { log } from 'wechaty-puppet' +import { + GError, + timeoutPromise, + TimeoutPromiseGError, +} from 'gerror' + +import { StateSwitch } from 'state-switch' +import type { + StateSwitchInterface, +} from 'state-switch' + +import { config } from '../config.js' +import { timestampToDate } from '../pure-functions/timestamp-to-date.js' +import type { + ContactImpl, + ContactInterface, + RoomImpl, +} from '../user-modules/mod.js' + +import type { + WechatifyUserModuleMixin, +} from './wechatify-user-module-mixin.js' + +import type { GErrorMixin } from './gerror-mixin.js' +import type { IoMixin } from './io-mixin.js' + +const PUPPET_MEMORY_NAME = 'puppet' + +/** + * Huan(202111): `puppetMixin` must extend `pluginMixin` + * because the `wechaty-redux` plugin need to be installed before + * the puppet started + * + * Huan(20211128): `puppetMixin` must extend `IoMixin` + * because the Io need the puppet instance to be ready when it starts + */ +const puppetMixin = (mixinBase: MixinBase) => { + log.verbose('WechatyPuppetMixin', 'puppetMixin(%s)', mixinBase.name) + + abstract class PuppetMixin extends mixinBase { + + __puppet?: PUPPET.impls.PuppetInterface + + get puppet (): PUPPET.impls.PuppetInterface { + if (!this.__puppet) { + + throw new Error('NOPUPPET') + } + return this.__puppet + } + + readonly __readyState : StateSwitchInterface + + __puppetMixinInited = false + + constructor (...args: any[]) { + log.verbose('WechatyPuppetMixin', 'construct()') + super(...args) + + this.__readyState = new StateSwitch('WechatyReady', { log }) + } + + override async start (): Promise { + log.verbose('WechatyPuppetMixin', 'start()') + + log.verbose('WechatyPuppetMixin', 'start() super.start() ...') + await super.start() + log.verbose('WechatyPuppetMixin', 'start() super.start() ... done') + + try { + /** + * reset the `wechaty.ready()` state + * if it was previous set to `active` + */ + if (this.__readyState.active()) { + this.__readyState.inactive(true) + } + + try { + log.verbose('WechatyPuppetMixin', 'start() starting puppet ...') + await timeoutPromise( + this.puppet.start(), + 15 * 1000, // 15 seconds timeout + ) + log.verbose('WechatyPuppetMixin', 'start() starting puppet ... done') + } catch (e) { + if (e instanceof TimeoutPromiseGError) { + /** + * Huan(202111): + * + * We should throw the Timeout error when the puppet.start() can not be finished in time. + * However, we need to compatible with some buggy puppet implementations which will not resolve the promise. + * + * TODO: throw the Timeout error when the puppet.start() can not be finished in time. + * + * e.g. after resolve @issue https://github.com/padlocal/wechaty-puppet-padlocal/issues/116 + */ + log.warn('WechatyPuppetMixin', 'start() starting puppet ... timeout') + log.warn('WechatyPuppetMixin', 'start() puppet info: %s', this.puppet) + } else { + throw e + } + } + + } catch (e) { + this.emitError(e) + } + } + + override async stop (): Promise { + log.verbose('WechatyPuppetMixin', 'stop()') + + try { + log.verbose('WechatyPuppetMixin', 'stop() stopping puppet ...') + await timeoutPromise( + this.puppet.stop(), + 15 * 1000, // 15 seconds timeout + ) + log.verbose('WechatyPuppetMixin', 'stop() stopping puppet ... done') + } catch (e) { + if (e instanceof TimeoutPromiseGError) { + log.warn('WechatyPuppetMixin', 'stop() stopping puppet ... timeout') + log.warn('WechatyPuppetMixin', 'stop() puppet info: %s', this.puppet) + } + this.emitError(e) + } + + log.verbose('WechatyPuppetMixin', 'stop() super.stop() ...') + await super.stop() + log.verbose('WechatyPuppetMixin', 'stop() super.stop() ... done') + } + + async ready (): Promise { + log.verbose('WechatyPuppetMixin', 'ready()') + await this.__readyState.stable('active') + log.silly('WechatyPuppetMixin', 'ready() this.readyState.stable(on) resolved') + } + + override async init (): Promise { + log.verbose('WechatyPuppetMixin', 'init()') + await super.init() + + if (this.__puppetMixinInited) { + log.verbose('WechatyPuppetMixin', 'init() skipped because this puppet has already been inited before.') + return + } + this.__puppetMixinInited = true + + log.verbose('WechatyPuppetMixin', 'init() instanciating puppet instance ...') + const puppetInstance = await PUPPET.helpers.resolvePuppet({ + puppet: this.__options.puppet || config.systemPuppetName(), + puppetOptions: 'puppetOptions' in this.__options + ? this.__options.puppetOptions + : undefined, + }) + log.verbose('WechatyPuppetMixin', 'init() instanciating puppet instance ... done') + + /** + * Plug the Memory Card to Puppet + */ + log.verbose('WechatyPuppetMixin', 'init() setting memory ...') + const puppetMemory = this.memory.multiplex(PUPPET_MEMORY_NAME) + puppetInstance.setMemory(puppetMemory) + log.verbose('WechatyPuppetMixin', 'init() setting memory ... done') + + /** + * Propagate Puppet Events to Wechaty + */ + log.verbose('WechatyPuppetMixin', 'init() setting up events ...') + this.__setupPuppetEvents(puppetInstance) + log.verbose('WechatyPuppetMixin', 'init() setting up events ... done') + + /** + * Private Event + * - Huan(202005): emit puppet when set + * - Huan(202110): @see https://github.com/wechaty/redux/blob/16af0ae01f72e37f0ee286b49fa5ccf69850323d/src/wechaty-redux.ts#L82-L98 + */ + log.verbose('WechatyPuppetMixin', 'init() emitting "puppet" event ...') + ;(this.emit as any)('puppet', puppetInstance) + log.verbose('WechatyPuppetMixin', 'init() emitting "puppet" event ... done') + + this.__puppet = puppetInstance + } + + __setupPuppetEvents (puppet: PUPPET.impls.PuppetInterface): void { + log.verbose('WechatyPuppetMixin', '__setupPuppetEvents(%s)', puppet) + + const eventNameList: PUPPET.types.PuppetEventName[] = Object.keys(PUPPET.types.PUPPET_EVENT_DICT) as PUPPET.types.PuppetEventName[] + for (const eventName of eventNameList) { + log.verbose('PuppetMixin', + '__setupPuppetEvents() puppet.on(%s) (listenerCount:%s) registering...', + eventName, + puppet.listenerCount(eventName), + ) + + switch (eventName) { + case 'dong': + puppet.on('dong', payload => { + this.emit('dong', payload.data) + }) + break + + case 'error': + puppet.on('error', payload => { + /** + * Huan(202112): + * 1. remove `payload.data` after it has been sunset (after Dec 31, 2022) + * 2. throw error if `payload.gerror` is not exists (for enforce puppet strict follow the error event schema) + */ + this.emit('error', GError.from(payload.gerror || payload.data || payload)) + }) + break + + case 'heartbeat': + puppet.on('heartbeat', payload => { + /** + * Use `watchdog` event from Puppet to `heartbeat` Wechaty. + */ + // TODO: use a throttle queue to prevent beat too fast. + this.emit('heartbeat', payload.data) + }) + break + + case 'friendship': + puppet.on('friendship', async payload => { + const friendship = this.Friendship.load(payload.friendshipId) + try { + await friendship.ready() + this.emit('friendship', friendship) + friendship.contact().emit('friendship', friendship) + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'login': + puppet.on('login', async payload => { + try { + const contact = await this.ContactSelf.find({ id: payload.contactId }) + if (!contact) { + throw new Error('no contact found for id: ' + payload.contactId) + } + this.emit('login', contact) + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'logout': + puppet.on('logout', async payload => { + try { + this.__readyState.inactive(true) + const contact = await this.ContactSelf.find({ id: payload.contactId }) + if (contact) { + this.emit('logout', contact, payload.data) + } else { + log.verbose('PuppetMixin', + '__setupPuppetEvents() logout event contact self not found for id: %s', + payload.contactId, + ) + } + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'message': + puppet.on('message', async payload => { + try { + const msg = await this.Message.find({ id: payload.messageId }) + if (!msg) { + this.emit('error', GError.from('message not found for id: ' + payload.messageId)) + return + } + + this.emit('message', msg) + + const room = msg.room() + const listener = msg.listener() + + if (room) { + room.emit('message', msg) + } else if (listener) { + listener.emit('message', msg) + } else { + this.emit('error', GError.from('message without room and listener')) + } + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'post': + puppet.on('post', async payload => { + try { + const post = await this.Post.find({ id: payload.postId }) + if (!post) { + this.emit('error', GError.from('post not found for id: ' + payload.postId)) + return + } + + this.emit('post', post) + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'ready': + puppet.on('ready', () => { + log.silly('WechatyPuppetMixin', '__setupPuppetEvents() puppet.on(ready)') + + this.emit('ready') + this.__readyState.active(true) + }) + break + + case 'room-invite': + puppet.on('room-invite', async payload => { + const roomInvitation = this.RoomInvitation.load(payload.roomInvitationId) + this.emit('room-invite', roomInvitation) + }) + break + + case 'room-join': + puppet.on('room-join', async payload => { + try { + const room = await this.Room.find({ id: payload.roomId }) + if (!room) { + throw new Error('no room found for id: ' + payload.roomId) + } + await room.sync() + + const validInviteeIdList = payload.inviteeIdList.filter((m) => m) + const inviteeListAll = await Promise.all( + validInviteeIdList.map(id => this.Contact.find({ id })), + ) + const inviteeList = inviteeListAll.filter(c => !!c) as ContactInterface[] + + const inviter = await this.Contact.find({ id: payload.inviterId }) + if (!inviter) { + throw new Error('no inviter found for id: ' + payload.inviterId) + } + + const date = timestampToDate(payload.timestamp) + + this.emit('room-join', room, inviteeList, inviter, date) + room.emit('join', inviteeList, inviter, date) + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'room-leave': + puppet.on('room-leave', async payload => { + try { + const room = await this.Room.find({ id: payload.roomId }) + if (!room) { + throw new Error('no room found for id: ' + payload.roomId) + } + + /** + * See: https://github.com/wechaty/wechaty/pull/1833 + */ + await room.sync() + + const leaverListAll = await Promise.all( + payload.removeeIdList.map(id => this.Contact.find({ id })), + ) + const leaverList = leaverListAll.filter(c => !!c) as ContactInterface[] + + const remover = await this.Contact.find({ id: payload.removerId }) + if (!remover) { + throw new Error('no remover found for id: ' + payload.removerId) + } + const date = timestampToDate(payload.timestamp) + + this.emit('room-leave', room, leaverList, remover, date) + room.emit('leave', leaverList, remover, date) + + // issue #254 + if (payload.removeeIdList.includes(puppet.currentUserId)) { + await puppet.roomPayloadDirty(payload.roomId) + await puppet.roomMemberPayloadDirty(payload.roomId) + } + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'room-topic': + puppet.on('room-topic', async payload => { + try { + const room = await this.Room.find({ id: payload.roomId }) + if (!room) { + throw new Error('no room found for id: ' + payload.roomId) + } + await room.sync() + + const changer = await this.Contact.find({ id: payload.changerId }) + if (!changer) { + throw new Error('no changer found for id: ' + payload.changerId) + } + const date = timestampToDate(payload.timestamp) + + this.emit('room-topic', room, payload.newTopic, payload.oldTopic, changer, date) + room.emit('topic', payload.newTopic, payload.oldTopic, changer, date) + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + case 'scan': + puppet.on('scan', async payload => { + this.__readyState.inactive(true) + this.emit('scan', payload.qrcode || '', payload.status, payload.data) + }) + break + + case 'reset': + // Do not propagation `reset` event from puppet + break + + case 'dirty': + /** + * https://github.com/wechaty/wechaty-puppet-service/issues/43 + */ + puppet.on('dirty', async ({ payloadType, payloadId }) => { + try { + switch (payloadType) { + case PUPPET.types.Payload.RoomMember: + case PUPPET.types.Payload.Contact: { + const contact = await this.Contact.find({ id: payloadId }) as unknown as undefined | ContactImpl + await contact?.ready(true) + break + } + case PUPPET.types.Payload.Room: { + const room = await this.Room.find({ id: payloadId }) as unknown as undefined | RoomImpl + await room?.ready(true) + break + } + + /** + * Huan(202008): noop for the following + */ + case PUPPET.types.Payload.Friendship: + // Friendship has no payload + break + case PUPPET.types.Payload.Message: + // Message does not need to dirty (?) + break + + case PUPPET.types.Payload.Unspecified: + default: + throw new Error('unknown payload type: ' + payloadType) + } + } catch (e) { + this.emit('error', GError.from(e)) + } + }) + break + + default: + /** + * Check: The eventName here should have the type `never` + */ + throw new Error('eventName ' + eventName + ' unsupported!') + + } + } + + log.verbose('WechatyPuppetMixin', '__setupPuppetEvents() ... done') + } + + } + + return PuppetMixin +} + +type PuppetMixin = ReturnType + +type ProtectedPropertyPuppetMixin = + | '__puppet' + | '__readyState' + | '__setupPuppetEvents' + +export type { + PuppetMixin, + ProtectedPropertyPuppetMixin, +} +export { + puppetMixin, +} diff --git a/src/wechaty-mixins/wechatify-user-module-mixin.spec.ts b/src/wechaty-mixins/wechatify-user-module-mixin.spec.ts new file mode 100755 index 0000000..941b64a --- /dev/null +++ b/src/wechaty-mixins/wechatify-user-module-mixin.spec.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { + test, +} from 'tstest' + +import type { + WechatifyUserModuleMixin, + ProtectedPropertyWechatifyUserModuleMixin, +} from './wechatify-user-module-mixin.js' + +test('ProtectedPropertyWechatifyUserModuleMixin', async t => { + type NotExistInMixin = Exclude> + type NotExistTest = NotExistInMixin extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Mixin properties for every protected property') +}) diff --git a/src/wechaty-mixins/wechatify-user-module-mixin.ts b/src/wechaty-mixins/wechatify-user-module-mixin.ts new file mode 100644 index 0000000..3e3e34a --- /dev/null +++ b/src/wechaty-mixins/wechatify-user-module-mixin.ts @@ -0,0 +1,151 @@ +import { log } from 'wechaty-puppet' + +import { + ContactImpl, + ContactSelfImpl, + DelayImpl, + FriendshipImpl, + ImageImpl, + LocationImpl, + MessageImpl, + MiniProgramImpl, + PostImpl, + RoomImpl, + RoomInvitationImpl, + TagImpl, + UrlLinkImpl, + + ContactConstructor, + ContactSelfConstructor, + DelayConstructor, + FriendshipConstructor, + ImageConstructor, + LocationConstructor, + MessageConstructor, + MiniProgramConstructor, + PostConstructor, + RoomConstructor, + RoomInvitationConstructor, + TagConstructor, + UrlLinkConstructor, + + wechatifyUserModule, +} from '../user-modules/mod.js' + +import type { + WechatySkeleton, +} from '../wechaty/mod.js' + +const wechatifyUserModuleMixin = (mixinBase: MixinBase) => { + log.verbose('WechatifyUserModuleMixin', 'wechatifyUserModuleMixin(%s)', mixinBase.name) + + abstract class WechatifyUserModuleMixin extends mixinBase { + + constructor (...args: any[]) { + log.verbose('WechatifyUserModuleMixin', 'constructor()') + super(...args) + } + + __wechatifiedContact? : ContactConstructor + __wechatifiedContactSelf? : ContactSelfConstructor + __wechatifiedDelay? : DelayConstructor + __wechatifiedFriendship? : FriendshipConstructor + __wechatifiedImage? : ImageConstructor + __wechatifiedLocation? : LocationConstructor + __wechatifiedMessage? : MessageConstructor + __wechatifiedMiniProgram? : MiniProgramConstructor + __wechatifiedPost? : PostConstructor + __wechatifiedRoom? : RoomConstructor + __wechatifiedRoomInvitation? : RoomInvitationConstructor + __wechatifiedTag? : TagConstructor + __wechatifiedUrlLink? : UrlLinkConstructor + + get Contact () : ContactConstructor { return guardWechatify(this.__wechatifiedContact) } + get ContactSelf () : ContactSelfConstructor { return guardWechatify(this.__wechatifiedContactSelf) } + get Delay () : DelayConstructor { return guardWechatify(this.__wechatifiedDelay) } + get Friendship () : FriendshipConstructor { return guardWechatify(this.__wechatifiedFriendship) } + get Image () : ImageConstructor { return guardWechatify(this.__wechatifiedImage) } + get Location () : LocationConstructor { return guardWechatify(this.__wechatifiedLocation) } + get Message () : MessageConstructor { return guardWechatify(this.__wechatifiedMessage) } + get MiniProgram () : MiniProgramConstructor { return guardWechatify(this.__wechatifiedMiniProgram) } + get Post () : PostConstructor { return guardWechatify(this.__wechatifiedPost) } + get Room () : RoomConstructor { return guardWechatify(this.__wechatifiedRoom) } + get RoomInvitation () : RoomInvitationConstructor { return guardWechatify(this.__wechatifiedRoomInvitation) } + get Tag () : TagConstructor { return guardWechatify(this.__wechatifiedTag) } + get UrlLink () : UrlLinkConstructor { return guardWechatify(this.__wechatifiedUrlLink) } + + override async init (): Promise { + log.verbose('WechatifyUserModuleMixin', 'init()') + await super.init() + + /** + * Skip if already wechatified + */ + if (this.__wechatifiedMessage) { + log.verbose('WechatifyUserModuleMixin', 'init() Wechaty User Module (WUM)s have already wechatified: skip') + return + } + + log.verbose('WechatifyUserModuleMixin', 'init() initializing Wechaty User Module (WUM) ...') + + /** + * Wechatify User Classes + * 1. Binding the wechaty instance to the class + * + * Huan(202110): FIXME: remove any + */ + this.__wechatifiedContact = wechatifyUserModule(ContactImpl)(this as any) + this.__wechatifiedContactSelf = wechatifyUserModule(ContactSelfImpl)(this as any) + this.__wechatifiedDelay = wechatifyUserModule(DelayImpl)(this as any) + this.__wechatifiedFriendship = wechatifyUserModule(FriendshipImpl)(this as any) + this.__wechatifiedImage = wechatifyUserModule(ImageImpl)(this as any) + this.__wechatifiedLocation = wechatifyUserModule(LocationImpl)(this as any) + this.__wechatifiedMessage = wechatifyUserModule(MessageImpl)(this as any) + this.__wechatifiedMiniProgram = wechatifyUserModule(MiniProgramImpl)(this as any) + this.__wechatifiedPost = wechatifyUserModule(PostImpl)(this as any) + this.__wechatifiedRoom = wechatifyUserModule(RoomImpl)(this as any) + this.__wechatifiedRoomInvitation = wechatifyUserModule(RoomInvitationImpl)(this as any) + this.__wechatifiedTag = wechatifyUserModule(TagImpl)(this as any) + this.__wechatifiedUrlLink = wechatifyUserModule(UrlLinkImpl)(this as any) + + log.verbose('WechatifyUserModuleMixin', 'init() initializing Wechaty User Module (WUM) ... done') + } + + } + + return WechatifyUserModuleMixin +} + +/** + * Huan(202008): we will bind the wechaty puppet with user modules (Contact, Room, etc) together inside the start() method + */ +function guardWechatify (userModule?: T): T { + if (userModule) { + return userModule + } + throw new Error('Wechaty User Module (WUM, for example: wechaty.Room) can not be used before wechaty.start()!') +} + +type WechatifyUserModuleMixin = ReturnType + +type ProtectedPropertyWechatifyUserModuleMixin = + | '__wechatifiedContact' + | '__wechatifiedContactSelf' + | '__wechatifiedDelay' + | '__wechatifiedFriendship' + | '__wechatifiedImage' + | '__wechatifiedLocation' + | '__wechatifiedMessage' + | '__wechatifiedMiniProgram' + | '__wechatifiedRoom' + | '__wechatifiedRoomInvitation' + | '__wechatifiedTag' + | '__wechatifiedUrlLink' + +export type { + WechatifyUserModuleMixin, + ProtectedPropertyWechatifyUserModuleMixin, +} +export { + wechatifyUserModuleMixin, +} diff --git a/src/wechaty/mod.ts b/src/wechaty/mod.ts new file mode 100644 index 0000000..cfe2b09 --- /dev/null +++ b/src/wechaty/mod.ts @@ -0,0 +1,18 @@ +import type { + WechatyInterface, + WechatyConstructor, +} from './wechaty-impl.js' +import { WechatyImpl } from './wechaty-impl.js' + +import type { + WechatyOptions, +} from '../schemas/wechaty-options.js' +import { WechatySkeleton } from './wechaty-skeleton.js' + +export { + type WechatyInterface, + type WechatyConstructor, + type WechatyOptions, + WechatySkeleton, + WechatyImpl, +} diff --git a/src/wechaty/wechaty-base.spec.ts b/src/wechaty/wechaty-base.spec.ts new file mode 100755 index 0000000..8a7e122 --- /dev/null +++ b/src/wechaty/wechaty-base.spec.ts @@ -0,0 +1,392 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + test, + sinon, +} from 'tstest' + +import { MemoryCard } from 'memory-card' +import { PuppetMock } from 'wechaty-puppet-mock' + +/** + * Huan(202111): must import `./wechaty-impl.js` + * before import `./wechaty-base.js` + * + * Or will throw error: + * + * ReferenceError: Cannot access 'WechatyBase' before initialization + * at file:///home/huan/git/wechaty/wechaty/src/wechaty/wechaty-impl.ts:25:47 + * + * TODO: find out why + */ +import './wechaty-impl.js' + +import type { + WechatyBaseProtectedProperty, +} from './wechaty-base.js' +import { + WechatyBase, +} from './wechaty-base.js' + +import type { + WechatyInterface, +} from '../wechaty/wechaty-impl.js' +import { WechatySkeleton } from './wechaty-skeleton.js' + +class WechatyTest extends WechatyBase { +} + +test('static VERSION', async t => { + t.ok('VERSION' in WechatyBase, 'Wechaty should has a static VERSION property') +}) + +test('event:start/stop', async t => { + const wechaty = new WechatyBase({ puppet: 'wechaty-puppet-mock' }) + + const startSpy = sinon.spy() + const stopSpy = sinon.spy() + + wechaty.on('start', startSpy) + wechaty.on('stop', stopSpy) + + await wechaty.start() + await wechaty.stop() + + // console.log(startSpy.callCount) + t.ok(startSpy.calledOnce, 'should get event:start once') + t.ok(stopSpy.calledOnce, 'should get event:stop once') +}) + +// +// FIXME: restore this unit test !!! +// +// test.only('event:scan', async t => { +// const m = {} as any + +// const asyncHook = asyncHooks.createHook({ +// init(asyncId: number, type: string, triggerAsyncId: number, resource: Object) { +// m[asyncId] = type +// }, +// before(asyncId) { +// // delete m[asyncId] +// }, +// after(asyncId) { +// // delete m[asyncId] +// }, +// destroy(asyncId) { +// delete m[asyncId] +// }, +// }) +// asyncHook.enable() + +// const wechaty = Wechaty.instance() + +// const spy = sinon.spy() + +// wechaty.on('scan', spy) + +// const scanFuture = new Promise(resolve => wechaty.once('scan', resolve)) +// // wechaty.once('scan', () => console.log('FAINT')) + +// await wechaty.start() +// await scanFuture +// // await new Promise(r => setTimeout(r, 1000)) +// await wechaty.stop() + +// t.ok(spy.calledOnce, 'should get event:scan') +// asyncHook.disable() + +// console.log(m) +// }) + +test.skip('SKIP DEALING WITH THE LISTENER EXCEPTIONS. on(event, Function)', async t => { + const spy = sinon.spy() + const wechaty = new WechatyBase() + + const EXPECTED_ERROR = new Error('testing123') + wechaty.on('message', () => { throw EXPECTED_ERROR }) + // wechaty.on('scan', () => 42) + wechaty.on('error', spy) + + const messageFuture = new Promise(resolve => wechaty.once('message', resolve)) + wechaty.emit('message', {} as any) + + await messageFuture + await wechaty.stop() + + t.ok(spy.calledOnce, 'should get event:error once') + t.equal(spy.firstCall.args[0], EXPECTED_ERROR, 'should get error from message listener') + +}) + +test.skip('SKIP DEALING WITH THE LISTENER EXCEPTIONS. test async error', async t => { + + // Do not modify the global Wechaty instance + class MyWechatyTest extends WechatyBase {} + + const EXPECTED_ERROR = new Error('test') + + const bot = new MyWechatyTest({ + puppet: new PuppetMock(), + }) + + const asyncErrorFunction = function () { + return new Promise((resolve, reject) => { + setTimeout(function () { + reject(EXPECTED_ERROR) + }, 100) + // tslint ask resolve must be called, + // so write a falsy value, so that it never called + if (+new Date() < 0) { + resolve() + } + }) + } + + bot.on('message', async () => { + await asyncErrorFunction() + }) + bot.on('error', (e) => { + t.ok(e.message === EXPECTED_ERROR.message) + }) + + bot.emit('message', {} as any) + + await bot.stop() +}) + +test('use plugin', async t => { + + // Do not modify the gloabl Wechaty instance + class MyWechatyTest extends WechatyBase {} + + let result = '' + + // const myGlobalPlugin = function () { + // return function (bot: WechatyInterface) { + // bot.on('message', () => { result += 'FROM_GLOBAL_PLUGIN:' }) + // } + // } + + const myPlugin = function () { + return function (bot: WechatyInterface) { + bot.on('message', () => { result += 'FROM_MY_PLUGIN:' }) + } + } + + // MyWechatyTest.use(myGlobalPlugin()) + + const bot = new MyWechatyTest({ + puppet: new PuppetMock(), + }) + + bot.use(myPlugin()) + + await bot.start() + + bot.on('message', () => (result += 'FROM_BOT')) + + bot.emit('message', {} as any) + + await bot.stop() + + t.equal(result, 'FROM_MY_PLUGIN:FROM_BOT', 'should get plugin works') + +}) + +test('wechatifyUserModules init()', async t => { + const wechatyTest = new WechatyTest() + + t.doesNotThrow(() => wechatyTest.init(), 'should not throw for the 1st time init') + t.doesNotThrow(() => wechatyTest.init(), 'should not throw for the 2nd time init (silence skip)') +}) + +// TODO: add test for event args + +test('Perfect restart', async t => { + const wechaty = new WechatyBase({ + puppet: new PuppetMock(), + }) + + try { + for (let i = 0; i < 3; i++) { + await wechaty.start() + await wechaty.stop() + t.pass('start/stop-ed at #' + i) + } + t.pass('Wechaty start/restart successed.') + } catch (e) { + t.fail(e as any) + } + +}) + +test('@event ready', async t => { + const puppet = new PuppetMock() + const wechaty = new WechatyBase({ puppet }) + + const sandbox = sinon.createSandbox() + const spy = sandbox.spy() + + wechaty.on('ready', spy) + t.ok(spy.notCalled, 'should no ready event with new wechaty instance') + + await wechaty.start() + t.ok(spy.notCalled, 'should no ready event right start wechaty started') + + puppet.emit('ready', { data: 'test' }) + t.equal(spy.callCount, 1, 'should fire ready event after puppet ready') + + await wechaty.stop() + await wechaty.start() + t.equal(spy.callCount, 1, 'should fire ready event second time after stop/start wechaty') + + puppet.emit('ready', { data: 'test' }) + + t.equal(spy.callCount, 2, 'should fire ready event third time after stop/start wechaty') + + await wechaty.stop() +}) + +test('ready()', async t => { + const puppet = new PuppetMock() + const wechaty = new WechatyBase({ puppet }) + + const sandbox = sinon.createSandbox() + + const spy = sandbox.spy() + + wechaty.ready() + .then(spy) + .catch(e => t.fail('rejection: ' + e)) + + t.ok(spy.notCalled, 'should not ready with new wechaty instance') + + await wechaty.start() + + t.ok(spy.notCalled, 'should not ready after right start wechaty') + + puppet.emit('ready', { data: 'test' }) + await new Promise(resolve => setImmediate(resolve)) + t.ok(spy.calledOnce, 'should ready after puppet ready') + + await wechaty.stop() + await wechaty.start() + wechaty.ready() + .then(spy) + .catch(e => t.fail('rejection: ' + e)) + + puppet.emit('ready', { data: 'test' }) + await new Promise(resolve => setImmediate(resolve)) + t.ok(spy.calledTwice, 'should ready again after stop/start wechaty') + + await wechaty.stop() +}) + +test('on/off event listener management', async t => { + const puppet = new PuppetMock() + const wechaty = new WechatyBase({ puppet }) + + const onMessage = (_: any) => {} + t.equal(wechaty.listenerCount('message'), 0, 'should no listener after initializing') + + wechaty.on('message', onMessage) + t.equal(wechaty.listenerCount('message'), 1, 'should +1 listener after on(message)') + + wechaty.off('message', onMessage) + t.equal(wechaty.listenerCount('message'), 0, 'should -1 listener after off(message)') +}) + +test('wrapAsync() async function', async t => { + const puppet = new PuppetMock() + const wechaty = new WechatyBase({ puppet }) + + const spy = sinon.spy() + wechaty.on('error', spy) + + const DATA = 'test' + const asyncFunc = async () => DATA + const syncFunc = wechaty.wrapAsync(asyncFunc) + + t.notOk(syncFunc(), 'should get sync function return void') + t.ok(spy.notCalled, 'should not emit error when sync function return value') + + const asyncFunc2 = async () => { throw new Error('test') } + const syncFunc2 = wechaty.wrapAsync(asyncFunc2) + t.doesNotThrow(() => syncFunc2(), 'should not throw when async function throw error') + await wechaty.sleep(0) // wait async event loop task to be executed + t.ok(spy.calledOnce, 'should emit error when async function throw error') +}) + +test('wrapAsync() promise', async t => { + const puppet = new PuppetMock() + const wechaty = new WechatyBase({ puppet }) + + const spy = sinon.spy() + wechaty.on('error', spy) + + const DATA = 'test' + const promise = Promise.resolve(DATA) + const wrappedPromise = wechaty.wrapAsync(promise) + t.equal(await wrappedPromise, undefined, 'should resolve Promise to void') + + const rejection = Promise.reject(new Error('test')) + const wrappedRejection = wechaty.wrapAsync(rejection) + t.equal(wrappedRejection, undefined, 'should be void and not to reject') + + t.equal(spy.callCount, 0, 'should have no error before sleep') + await wechaty.sleep(0) // wait async event loop task to be executed + t.equal(spy.callCount, 1, 'should emit error when promise reject with error') +}) + +test('WechatyBaseProtectedProperty', async t => { + type NotExistInMixin = Exclude + type NotExistTest = NotExistInMixin extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Wechaty properties for every protected property') +}) + +test('WechatySkeleton: super.{start,stop}()', async t => { + const sandbox = sinon.createSandbox() + + const puppet = new PuppetMock() + const memory = new MemoryCard() + + const wechaty = new WechatyTest({ + memory, + puppet, + }) + + const startStub = sandbox.stub(WechatySkeleton.prototype, 'start').resolves() + const stopStub = sandbox.stub(WechatySkeleton.prototype, 'stop').resolves() + + t.ok(startStub.notCalled, 'should not called before start') + t.ok(stopStub.notCalled, 'should not called before stop') + + await wechaty.start() + t.ok(startStub.calledOnce, 'should call the skeleton start(), which means all mixin start()s are chained correctly') + t.ok(stopStub.notCalled, 'should not call stop yet') + + await wechaty.stop() + t.ok(startStub.calledOnce, 'should only call start once') + t.ok(stopStub.calledOnce, 'should call the skeleton stop(), which means all mixin stops()s are chained correctly') +}) diff --git a/src/wechaty/wechaty-base.ts b/src/wechaty/wechaty-base.ts new file mode 100644 index 0000000..ad9c11b --- /dev/null +++ b/src/wechaty/wechaty-base.ts @@ -0,0 +1,307 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + serviceCtlMixin, +} from 'state-switch' +import { function as FP } from 'fp-ts' +import type * as PUPPET from 'wechaty-puppet' + +import { + config, + log, + VERSION, +} from '../config.js' + +import type { + SayableSayer, + Sayable, +} from '../sayable/mod.js' +import { + gErrorMixin, + ioMixin, + loginMixin, + miscMixin, + pluginMixin, + puppetMixin, + wechatifyUserModuleMixin, +} from '../wechaty-mixins/mod.js' + +import { + WechatySkeleton, +} from './wechaty-skeleton.js' +import type { + WechatyInterface, +} from './wechaty-impl.js' +import type { + WechatyOptions, +} from '../schemas/wechaty-options.js' +import type { PostInterface } from '../user-modules/post.js' + +const mixinBase = FP.pipe( + WechatySkeleton, + gErrorMixin, + wechatifyUserModuleMixin, + ioMixin, + puppetMixin, + loginMixin, + miscMixin, + pluginMixin, + /** + * Huan(202002): + * + * The `serviceCtlMixin` must be the most outer mixin + * because the `wechaty.start/stop()` should first entry `serviceCtlMixin.start/stop()` + * which can be managed correctly by the `serviceCtlMixin` + */ + serviceCtlMixin('Wechaty', { log }), +) + +/** + * Huan(2021211): Keep the below call back hell + * because it's easy for testing + * especially when there's some typing mismatch and we need to figure it out. + */ +// const mixinTest = serviceCtlMixin('Wechaty', { log })( +// pluginMixin( +// puppetMixin( +// wechatifyUserModuleMixin( +// gErrorMixin( +// WechatySkeleton, +// ), +// ), +// ), +// ), +// ) + +/** + * Main bot class. + * + * A `Bot` is a WeChat client depends on which puppet you use. + * It may equals + * - web-WeChat, when you use: [puppet-puppeteer](https://github.com/wechaty/wechaty-puppet-puppeteer)/[puppet-wechat4u](https://github.com/wechaty/wechaty-puppet-wechat4u) + * - ipad-WeChat, when you use: [puppet-padchat](https://github.com/wechaty/wechaty-puppet-padchat) + * - ios-WeChat, when you use: puppet-ioscat + * + * See more: + * - [What is a Puppet in Wechaty](https://github.com/wechaty/getting-started/wiki/FAQ#31-what-is-a-puppet-in-wechaty) + * + * > If you want to know how to send message, see [Message](#Message)
+ * > If you want to know how to get contact, see [Contact](#Contact) + * + * @example The World's Shortest ChatBot Code: 6 lines of JavaScript + * import { WechatyBuilder } from 'wechaty' + * const bot = WechatyBuilder.build() + * bot.on('scan', (qrCode, status) => console.log('https://wechaty.js.org/qrcode/' + encodeURIComponent(qrcode))) + * bot.on('login', user => console.log(`User ${user} logged in`)) + * bot.on('message', message => console.log(`Message: ${message}`)) + * bot.start() + */ +class WechatyBase extends mixinBase implements SayableSayer { + + static override readonly VERSION = VERSION + readonly wechaty : WechatyInterface + + readonly _stopCallbackList: (() => void)[] = [] + + /** + * The term [Puppet](https://wechaty.js.org/docs/specs/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). + * The plugins are named XXXPuppet, for example: + * - [PuppetWeChat](https://github.com/wechaty/puppet-wechat): + * - [PuppetWeChat](https://github.com/wechaty/puppet-service): + * - [PuppetXP](https://github.com/wechaty/puppet-xp) + * + * @typedef PuppetModuleName + * @property {string} PUPPET_DEFAULT + * The default puppet. + * @property {string} wechaty-puppet-wechat4u + * The default puppet, using the [wechat4u](https://github.com/nodeWechat/wechat4u) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser. + * @property {string} wechaty-puppet-service + * - Using the gRPC protocol to connect with a Protocol Server for controlling the any protocol of any IM program. + * @property {string} wechaty-puppet-wechat + * - Using the [google puppeteer](https://github.com/GoogleChrome/puppeteer) to control the [WeChat Web API](https://wx.qq.com/) via a chrome browser. + * @property {string} wechaty-puppet-mock + * - Using the mock data to mock wechat operation, just for test. + */ + + /** + * The option parameter to create a wechaty instance + * + * @typedef WechatyOptions + * @property {string} name -Wechaty Name.
+ * When you set this:
+ * `new Wechaty({name: 'wechaty-name'}) `
+ * it will generate a file called `wechaty-name.memory-card.json`.
+ * This file stores the login information for bot.
+ * 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_NAME` to set this value when you start.
+ * eg: `WECHATY_NAME="your-cute-bot-name" node bot.js` + * @property {PuppetModuleName | Puppet} puppet -Puppet name or instance + * @property {Partial} puppetOptions -Puppet TOKEN + * @property {string} ioToken -Io TOKEN + */ + + /** + * Creates an instance of Wechaty. + * @param {WechatyOptions} [options={}] + * + */ + constructor ( + override __options: WechatyOptions = {}, + ) { + super(__options) + log.verbose('Wechaty', 'constructor()') + + this.__memory = this.__options.memory + this.wechaty = this + } + + override async start (): Promise { + log.verbose('Wechaty', 'start()') + await this.init() + return super.start() + } + + override async onStart (): Promise { + log.verbose('Wechaty', 'onStart()') + + log.verbose('Wechaty', '<%s>(%s) onStart() v%s is starting...', + this.__options.puppet || config.systemPuppetName(), + this.__options.name || '', + this.version(), + ) + log.verbose('Wechaty', 'id: %s', this.id) + + const lifeTimer = setInterval(() => { + log.silly('Wechaty', 'onStart() setInterval() this timer is to keep Wechaty running...') + }, 1000 * 60 * 60) + this._stopCallbackList.push(() => clearInterval(lifeTimer)) + + this.emit('start') + log.verbose('Wechaty', 'onStart() ... done') + } + + override async onStop (): Promise { + log.verbose('Wechaty', 'onStop()') + + log.verbose('Wechaty', '<%s> onStop() v%s is stopping ...', + this.__options.puppet || config.systemPuppetName(), + this.version(), + ) + + // put to the end of the event loop in case of it need to be executed while stopping + this._stopCallbackList.map(setImmediate) + this._stopCallbackList.length = 0 + + this.emit('stop') + log.verbose('Wechaty', 'onStop() ... done') + } + + /** + * Send message to currentUser, in other words, bot send message to itself. + * > Tips: + * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) + * + * @param {(string | Contact | FileBox | UrlLink | MiniProgram | Location)} sayable + * send text, Contact, or file to bot.
+ * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file + * + * @returns {Promise} + * + * @example + * const bot = new Wechaty() + * await bot.start() + * // after logged in + * + * // 1. send text to bot itself + * await bot.say('hello!') + * + * // 2. send Contact to bot itself + * const contact = await bot.Contact.find() + * await bot.say(contact) + * + * // 3. send Image to bot itself from remote url + * import { FileBox } from 'wechaty' + * const fileBox = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') + * await bot.say(fileBox) + * + * // 4. send Image to bot itself from local file + * import { FileBox } from 'wechaty' + * const fileBox = FileBox.fromFile('/tmp/text.jpg') + * await bot.say(fileBox) + * + * // 5. send Link to bot itself + * const linkPayload = new UrlLink ({ + * description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love', + * thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4', + * title : 'Welcome to Wechaty', + * url : 'https://github.com/wechaty/wechaty', + * }) + * await bot.say(linkPayload) + * + * // 6. send MiniProgram to bot itself + * const miniPayload = new MiniProgram ({ + * username : 'gh_xxxxxxx', //get from mp.weixin.qq.com + * appid : '', //optional, get from mp.weixin.qq.com + * title : '', //optional + * pagepath : '', //optional + * description : '', //optional + * thumbnailurl : '', //optional + * }) + * await bot.say(miniPayload) + */ + + async say ( + sayable: Sayable, + ): Promise { + log.verbose('Wechaty', 'say(%s)', sayable) + await this.currentUser.say(sayable) + } + + async publish ( + post: PostInterface, + ): Promise { + log.verbose('Wechaty', 'publish(%s)', + (post.payload.sayableList as PUPPET.payloads.Sayable[]) + .map(s => s.type).join(','), + ) + const postId = await this.puppet.postPublish(post.payload) + + if (postId) { + return this.Post.find({ id: postId }) + } + } + +} + +type WechatyBaseProtectedProperty = + // | '_serviceCtlFsmInterpreter' // from ServiceCtlFsm + | '__serviceCtlLogger' // from ServiceCtl(&Fsm) + | '__serviceCtlResettingIndicator' // from ServiceCtl + | 'wechaty' + | 'onStart' + | 'onStop' + +export type { + WechatyBaseProtectedProperty, +} +export { + WechatyBase, +} diff --git a/src/wechaty/wechaty-impl.spec.ts b/src/wechaty/wechaty-impl.spec.ts new file mode 100755 index 0000000..16e4b52 --- /dev/null +++ b/src/wechaty/wechaty-impl.spec.ts @@ -0,0 +1,118 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +import { test } from 'tstest' +import PuppetMock from 'wechaty-puppet-mock' + +import { WechatyEventEmitter } from '../schemas/wechaty-events.js' + +import type { + ContactSelfConstructor, + ContactConstructor, + FriendshipConstructor, + ImageConstructor, + LocationConstructor, + MessageConstructor, + MiniProgramConstructor, + PostConstructor, + RoomInvitationConstructor, + RoomConstructor, + DelayConstructor, + TagConstructor, + UrlLinkConstructor, + MessageInterface, +} from '../user-modules/mod.js' + +import { + type WechatyConstructor, + type WechatyInterface, + type AllProtectedProperty, + WechatyImpl, + // WechatyConstructor, +} from './wechaty-impl.js' + +test('Wechaty interface', async t => { + abstract class WechatyImplementation extends WechatyEventEmitter implements WechatyInterface { + + Contact : ContactConstructor + ContactSelf : ContactSelfConstructor + Delay : DelayConstructor + Friendship : FriendshipConstructor + Image : ImageConstructor + Location : LocationConstructor + Message : MessageConstructor + MiniProgram : MiniProgramConstructor + Post : PostConstructor + Room : RoomConstructor + RoomInvitation : RoomInvitationConstructor + Tag : TagConstructor + UrlLink : UrlLinkConstructor + + constructor () { + super() + // this.puppet + this.Contact + = this.ContactSelf + = this.Delay + = this.Friendship + = this.Image + = this.Location + = this.Message + = this.MiniProgram + = this.Post + = this.Room + = this.RoomInvitation + = this.Tag + = this.UrlLink + = {} as any + } + + abstract authQrCode : WechatyInterface['authQrCode'] + abstract currentUser : WechatyInterface['currentUser'] + abstract ding : WechatyInterface['ding'] + abstract emitError : WechatyInterface['emitError'] + abstract id : WechatyInterface['id'] + abstract init : WechatyInterface['init'] + abstract isLoggedIn : WechatyInterface['isLoggedIn'] + abstract log : WechatyInterface['log'] + abstract logout : WechatyInterface['logout'] + abstract name : WechatyInterface['name'] + abstract publish : WechatyInterface['publish'] + abstract puppet : WechatyInterface['puppet'] + abstract ready : WechatyInterface['ready'] + abstract reset : WechatyInterface['reset'] + abstract say : WechatyInterface['say'] + abstract sleep : WechatyInterface['sleep'] + abstract start : WechatyInterface['start'] + abstract state : WechatyInterface['state'] + abstract stop : WechatyInterface['stop'] + abstract use : WechatyInterface['use'] + abstract version : WechatyInterface['version'] + abstract wrapAsync : WechatyInterface['wrapAsync'] + + } + + const WechatyTest = WechatyImplementation as unknown as WechatyConstructor + const w: WechatyInterface = new WechatyTest() + w.on('message', (msg: MessageInterface) => { + msg.say('ok').catch(console.error) + }) + + t.ok(typeof WechatyImplementation, 'should no typing error') +}) + +test('ProtectedProperties', async t => { + type NotExistInWechaty = Exclude + type NotExistTest = NotExistInWechaty extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Wechaty properties for every protected property') +}) + +test('options.puppet initialization', async t => { + const puppet = new PuppetMock() + const wechaty = new WechatyImpl({ puppet }) + t.throws(() => wechaty.puppet, 'should throw when access puppet getter before init()') + + await wechaty.init() + t.doesNotThrow(() => wechaty.puppet, 'should not throw when access puppet getter after init()') + t.ok(wechaty.puppet, 'should exist puppet instance by setting the options.puppet') +}) diff --git a/src/wechaty/wechaty-impl.ts b/src/wechaty/wechaty-impl.ts new file mode 100644 index 0000000..fc9242b --- /dev/null +++ b/src/wechaty/wechaty-impl.ts @@ -0,0 +1,65 @@ +import type { EventEmitter } from 'events' + +import type { Constructor } from 'clone-class' +import type { + WechatyMixinProtectedProperty, +} from '../wechaty-mixins/mod.js' +import type { WechatyEventListeners } from '../schemas/mod.js' +import { validationMixin } from '../user-mixins/mod.js' + +import type { + WechatySkeletonProtectedProperty, +} from './wechaty-skeleton.js' +import { + WechatyBase, + WechatyBaseProtectedProperty, +} from './wechaty-base.js' + +/** + * Huan(202111): this is for solving the circyle dependency problem + * + * Construct a `WechatyImpl` based the `WechatyImplBase` with `validationMixin` + * + */ +class WechatyImplBase extends validationMixin(WechatyBase)() {} +interface WechatyImplInterface extends WechatyImplBase {} + +class WechatyImpl extends validationMixin(WechatyImplBase)() {} + +/** + * ^ The above code will make a ready-to-use class: `WechatyImpl` + * without any cyclic dependency problem. + */ + +type AllProtectedProperty = + | keyof EventEmitter // Huan(202110): remove all EventEmitter first, and added typed event emitter later: or will get error + | WechatyMixinProtectedProperty + | WechatyBaseProtectedProperty + | WechatySkeletonProtectedProperty + | `_${string}`// remove all property from interface which is start with `_` + +/** + * Huan(202111): + * `on`/`off`/`once` must use TypedEventEmitter instead of WechatyImpl + * or will run into error: cyclic dependency? + */ +interface WechatyEventEmitter { + on: (event: E, listener: WechatyEventListeners[E]) => WechatyInterface + off: (event: E, listener: WechatyEventListeners[E]) => WechatyInterface + once: (event: E, listener: WechatyEventListeners[E]) => WechatyInterface +} + +type WechatyInterface = Omit + & WechatyEventEmitter + +type WechatyConstructor = Constructor< + WechatyInterface, + typeof WechatyImpl +> + +export { + type WechatyInterface, + type WechatyConstructor, + type AllProtectedProperty, + WechatyImpl, +} diff --git a/src/wechaty/wechaty-skeleton.ts b/src/wechaty/wechaty-skeleton.ts new file mode 100644 index 0000000..4bbe267 --- /dev/null +++ b/src/wechaty/wechaty-skeleton.ts @@ -0,0 +1,129 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { MemoryCard } from 'memory-card' +import { log } from 'wechaty-puppet' +import * as UUID from 'uuid' +import type { Loggable } from 'brolog' + +import { + WechatyEventEmitter, + WechatyEventName, +} from '../schemas/mod.js' + +import type { WechatyOptions } from '../schemas/wechaty-options.js' + +abstract class WechatySkeleton extends WechatyEventEmitter { + + static readonly log: Loggable = log + readonly log: Loggable = log + + /** + * the UUID of the Wechaty + * @ignore + */ + readonly id: string + + __memory?: MemoryCard + get memory (): MemoryCard { + if (!this.__memory) { + throw new Error('NOMEMORY') + } + return this.__memory + } + + __options: WechatyOptions + + constructor (...args: any[]) { + log.verbose('WechatySkeleton', 'constructor()') + super() + + this.id = UUID.v4() + this.__options = args[0] || {} as WechatyOptions + + /** + * Huan(202008): + * + * Set max listeners to 1K, so that we can add lots of listeners without the warning message. + * The listeners might be one of the following functionilities: + * 1. Plugins + * 2. Redux Observables + * 3. etc... + */ + super.setMaxListeners(1024) + } + + /** + * Initialize the Wechaty instance for ready to be started. + * + * 1. It will be called automatically by the start() + * 2. It should be allowed for being called multiple times in the same instance, + * by skipping the second time initialization. + */ + async init (): Promise { + log.verbose('WechatySkeleton', 'init()') + + if (!this.__memory) { + this.__memory = new MemoryCard(this.__options.name) + try { + await this.__memory.load() + } catch (_) { + log.silly('WechatySkeleton', 'onStart() memory.load() had already loaded') + } + } + } + + async start (): Promise { + log.verbose('WechatySkeleton', 'start()') + // no super.start() + + /** + * Huan(202203): Call the init() functions (with super.init() inside them for chaining) + */ + await this.init() + } + + async stop (): Promise { + log.verbose('WechatySkeleton', 'stop()') + // no super.stop() + } + + override on (event: WechatyEventName, listener: (...args: any[]) => any): this { + log.verbose('WechatySkeleton', 'on(%s, listener) registering... listenerCount: %s', + event, + this.listenerCount(event), + ) + + return super.on(event, listener) + } + +} + +type WechatySkeletonProtectedProperty = + | '__events' + | '__memory' + | '__options' + | 'memory' + +export type { + WechatySkeletonProtectedProperty, +} +export { + WechatySkeleton, +} diff --git a/src/wechaty/wechaty-skelton.spec.ts b/src/wechaty/wechaty-skelton.spec.ts new file mode 100755 index 0000000..4e00e88 --- /dev/null +++ b/src/wechaty/wechaty-skelton.spec.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { + test, +} from 'tstest' + +import type { + WechatySkeleton, + WechatySkeletonProtectedProperty, +} from './wechaty-skeleton.js' + +test('ProtectedPropertyWechatySkeleton', async t => { + type NotExistInMixin = Exclude + type NotExistTest = NotExistInMixin extends never ? true : false + + const noOneLeft: NotExistTest = true + t.ok(noOneLeft, 'should match Mixin properties for every protected property') +}) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..a343c25 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,987 @@ +# TESTS + +Run unit tests + +```shell +$ npm test +... + +``` + +If you want to see full log messages + +```shell +$ WECHATY_LOG=silly npm test +... + +``` + +## EXAMPLE OUTPUT + +```bash + + +> wechaty@0.17.11 test /home/zixia/chatie/wechaty +> npm run clean && npm run lint && npm run test:unit:retry && npm run test:shell && npm run sloc + + +> wechaty@0.17.11 clean /home/zixia/chatie/wechaty +> shx rm -fr dist/* + + +> wechaty@0.17.11 lint /home/zixia/chatie/wechaty +> npm run check-node-version && npm run lint:ts && npm run lint:es && npm run lint:sh + + +> wechaty@0.17.11 check-node-version /home/zixia/chatie/wechaty +> check-node-version --node ">= 8.5" + + +> wechaty@0.17.11 lint:ts /home/zixia/chatie/wechaty +> tslint --project tsconfig.json && tsc --noEmit + + +> wechaty@0.17.11 lint:es /home/zixia/chatie/wechaty +> eslint "{bin,examples,scripts,src,tests}/**/*.js" --ignore-pattern="tests/fixtures/**" + + +> wechaty@0.17.11 lint:sh /home/zixia/chatie/wechaty +> bash -n bin/*.sh + + +> wechaty@0.17.11 test:unit:retry /home/zixia/chatie/wechaty +> ts-node scripts/retry-unit-tests + +Safe Test: starting... +Safe Test: running for round #0 + +> wechaty@0.17.11 test:unit /home/zixia/chatie/wechaty +> TS_NODE_FILES=1 tap --node-arg=--require=ts-node/register "src/**/*.spec.ts" "src/*.spec.ts" "tests/*.spec.ts" "tests/**/*.spec.ts" + +TAP version 13 +# Accessory smoke testing +ok 1 should throw if read static puppet before initialize +ok 2 should throw if read instance puppet before initialization +ok 3 should get EXPECTED_PUPPET1 from static puppet after set static puppet +ok 4 should get EXPECTED_PUPPET1 from instance puppet after set static puppet +# Two clone-ed classes have different static puppet value +ok 5 should get the puppet as 1 from 1st cloned class +ok 6 should get the puppet as 2 from 2nd cloned class +# Throw error when set the value again +ok 7 instance: should not throw when set at 1st time +ok 8 instance: should throw when set at 2nd time +ok 9 static: should not throw when set at 1st time +ok 10 static: should throw when set at 2nd time +# important variables +ok 11 should exist `puppet` in Config +ok 12 should exist `apihost` in Config +ok 13 should exist `profile` in Config +ok 14 should exist `token` in Config +ok 15 should export DEFAULT_PUPPET +ok 16 should export DEFAULT_PROFILE +ok 17 should export DEFAULT_PROTOCOL +ok 18 should export DEFAULT_APIHOST +# validApiHost() +ok 19 should not throw +ok 20 should not throw +ok 21 should throw +ok 22 should throw +# Io restart without problem +ok 23 start/stop-ed at #0 +ok 24 start/stop-ed at #1 +ok 25 start/restart successed. +# stripHtml() +ok 26 should strip html as expected +# unescapeHtml() +ok 27 should unescape html as expected +# plainText() +ok 28 should convert plain text as expected +# digestEmoji() +ok 29 should digest emoji string 0 as expected +ok 30 should digest emoji string 1 as expected +ok 31 should digest emoji string 2 as expected +ok 32 should digest emoji string 3 as expected +# unifyEmoji() +ok 33 should convert the emoji xml to the expected unified xml +ok 34 should convert the emoji xml to the expected unified xml +# stripEmoji() +ok 35 should strip to the expected str +ok 36 should strip to the expected str +ok 37 should return empty string for `undefined` +# downloadStream() for media +ok 38 should has cookies in req +ok 39 should has a cookie named life value 42 +ok 40 should success download dong from downloadStream() +# getPort() for an available socket port +ok 41 should not be same port even it is available(to provent conflict between concurrency tests in AVA) +ok 42 should has no exception after loop test +# promiseRetry() +ok 43 should got EXPECTED_REJECT when wait not enough +ok 44 should got EXPECTED_RESOLVE when wait enough +# retry() +ok 45 should got EXPECTED_RESOLVE when wait enough +# PuppetMock restart without problem +ok 46 start/stop-ed at #0 +ok 47 start/stop-ed at #1 +ok 48 start/stop-ed at #2 +ok 49 PuppetMock() start/restart successed. +# PadchatManager() cache should be release and can be re-init again. +ok 50 init/release-ed at #0 +ok 51 init/release-ed at #1 +ok 52 init/release-ed at #2 +ok 53 PadchatManager() cache init/release/init successed. +# PadchatManager() cache release 10 instances for the same time +ok 54 release 10 at the same time success +# PadchatManager() should can be able to restart() many times for one instance +ok 55 restarted at #0 +ok 56 restarted at #1 +ok 57 restarted at #2 +ok 58 PadchatManager() restart successed. +# PadchatManager() stop many instances for the same time +ok 59 stop3 at the same time success +# PuppetPadchat() throw exception when instanciate the second instance without options.token +ok 60 should instance the 1st puppet without problem +ok 61 should throw when instance the 2nd instance without the token option +ok 62 should instance the 3rd puppet with token option +# stripBugChatroomId() +ok 63 should return pure user_name for RAW_USER_NAME_1 +ok 64 should return pure user_name for RAW_USER_NAME_2 +ok 65 should return empty string for undifined +# contactRawPayloadParser +ok 66 should parse ContactPayload for personal account payload +ok 67 should parse ContactPayload for official account payload +ok 68 should throw exception for invalid object +ok 69 should throw exception for undifined +# imageBase64ToQrCode() +ok 70 should decode qrcode image base64 +# friendshipConfirmEventMessageParser() EN-confirm-by-other +ok 71 should parse message to contact id +# friendshipConfirmEventMessageParser() EN-confirm-by-bot +ok 72 should parse message to contact id +# friendshipConfirmEventMessageParser() ZH-confirm-by-other +ok 73 tbw # SKIP +# friendshipConfirmEventMessageParser() ZH-confirm-by-bot +ok 74 should parse message to contact id +# friendshipReceiveEventMessageParser() +ok 75 should parse message to receive contact id +# friendshipConfirmEventMessageParser() +ok 76 should parse `undefined` +ok 77 should parse `null` +ok 78 should parse `{}` +ok 79 should parse invalid content +# friendshipReceiveEventMessageParser() +ok 80 should parse `undefined` +ok 81 should parse `null` +ok 82 should parse `{}` +ok 83 should parse invalid content +# friendshipVerifyEventMessageParser() +ok 84 should parse `undefined` +ok 85 should parse `null` +ok 86 should parse `{}` +ok 87 should parse invalid content +# friendshipVerifyEventMessageParser() EN +ok 88 should parse verify message to contact id +# friendshipVerifyEventMessageParser() ZH +ok 89 should parse verify message to contact id +# friendshipRawPayloadParser() +ok 90 should parse friendshipPayload right +# isRoomId() +ok 91 should return true for ROOM_ID +ok 92 should return false for ROOM_ID +ok 93 should return false for undifined +# isContactId() +ok 94 should return true for CONTACT_ID +ok 95 should return false for CONTACT_ID +ok 96 should return false for undifined +# isContactOfficialId() +ok 97 should return true for OFFICIAL_CONTACT_ID +ok 98 should return false for NOT_OFFICIAL_CONTACT_ID +ok 99 should return false for undifined +# isStrangerV1() +ok 100 should return true for STRANGER_V1 +ok 101 should return false for NOT_STRANGER_V1 +# isStrangerV2() +ok 102 should return true for STRANGER_V2 +ok 103 should return false for NOT_STRANGER_V2 +# isPayload() +ok 104 undefined is not payload +ok 105 null is not payload +ok 106 {} is not payload +ok 107 valid payload +# messageRawPayloadParser +ok 108 tbw # SKIP +# sys +ok 109 should parse sys message payload +# status notify +ok 110 should parse status notify message payload +# room invitation created by bot +ok 111 should parse room invitation message payload +# room ownership transfer message +ok 112 should parse ower transfer message +# StatusNotify to roomId +ok 113 should parse status notify message to room id +# share card peer to peer +ok 114 should parse share card message peer to peer +# share card in room +ok 115 should parse share card message peer to peer +# padchatDecode() uri decode with + +ok 116 should parse json text with "+" right +# padchatDecode() plain json text +ok 117 should decode invalid uridecode text +# roomJoinEventMessageParser() EN-other-invite-other +ok 118 should parse event +# roomJoinEventMessageParser() EN-other-invite-others +ok 119 should parse event +# roomJoinEventMessageParser() EN-other-invite-bot +ok 120 should parse event +# roomJoinEventMessageParser() EN-other-invite-bot-with-2-others +ok 121 should parse event +# roomJoinEventMessageParser() EN-bot-invite-one +ok 122 should parse event +# roomJoinEventMessageParser() EN-bot-invite-three-bot-is-owner +ok 123 should parse event +# roomJoinEventMessageParser() EN-bot-invite-three-bot-is-not-owner +ok 124 should parse event +# roomJoinEventMessageParser() EN-other-invite-bot-and-two +ok 125 should parse event +# roomJoinEventMessageParser() EN-scan-qrcode-shared-by-bot-when-bot-is-owner +ok 126 should parse event +# roomJoinEventMessageParser() EN-scan-qrcode-shared-by-bot-when-bot-not-owner +ok 127 should parse event +# roomJoinEventMessageParser() EN-scan-qrcode-shared-by-other-when-bot-is-owner +ok 128 should parse event +# roomJoinEventMessageParser() EN-scan-qrcode-shared-by-other-when-bot-no-owner +ok 129 should parse event +# roomJoinEventMessageParser() EN-bot-invite-many +ok 130 should be the same as the bot-invite-many # SKIP +# roomJoinEventMessageParser() EN-room-create +ok 131 to be confirm # SKIP +# roomJoinEventMessageParser() not detected +ok 132 should return null for undefined +ok 133 should return null for null +ok 134 should return null for string +ok 135 should return null for empty object +ok 136 should return null for PadchatMessagePayload with unknown content +# roomJoinEventMessageParser() Recall Message +ok 137 should return null for a normal message recall payload +# roomJoinEventMessageParser() ZH-other-invite-other +ok 138 should parse room join message payload +# roomJoinEventMessageParser() ZH-other-invite-others +ok 139 tbw # SKIP +# roomJoinEventMessageParser() ZH-other-invite-bot +ok 140 should parse event +# roomJoinEventMessageParser() ZH-other-invite-bot-with-other +ok 141 should parse event +# roomJoinEventMessageParser() ZH-bot-invite-one +ok 142 should parse event +# roomJoinEventMessageParser() ZH-bot-invite-three-bot-is-owner +ok 143 should parse event +# roomJoinEventMessageParser() ZH-bot-invite-three-bot-is-not-owner +ok 144 should parse event +# roomJoinEventMessageParser() ZH-other-invite-bot-and-two +ok 145 should parse event +# roomJoinEventMessageParser() ZH-scan-qrcode-shared-by-bot-when-bot-not-owner +ok 146 should parse event +# roomJoinEventMessageParser() ZH-scan-qrcode-shared-by-bot-when-bot-is-owner +ok 147 should parse event +# roomJoinEventMessageParser() ZH-scan-qrcode-shared-by-other-when-bot-no-owner +ok 148 should parse event +# roomJoinEventMessageParser() ZH-scan-qrcode-shared-by-other-when-bot-is-owner +ok 149 should parse event +# roomJoinEventMessageParser() ZH-bot-invite-three +ok 150 tbw # SKIP +# roomJoinEventMessageParser() ZH-room-create +ok 151 can not get create sys message, because room will not sync or appear before the creater send the first message # SKIP +# roomLeaveEventMessageParser() EN-bot-delete-other +ok 152 should parse room leave message payload +# roomLeaveEventMessageParser() EN-bot-delete-others +ok 153 the same as bot-delete-other # SKIP +# roomLeaveEventMessageParser() EN-other-delete-bot +ok 154 should parse event +# roomLeaveEventMessageParser() EN-other-delete-other +ok 155 can not detect # SKIP +# roomLeaveEventMessageParser() EN-other-delete-others +ok 156 can not detect # SKIP +# roomLeaveEventMessageParser() not detected +ok 157 should return null for undefined +ok 158 should return null for null +ok 159 should return null for string +ok 160 should return null for empty object +ok 161 should return null for PadchatMessagePayload with unknown content +# roomLeaveEventMessageParser() ZH-bot-delete-other +ok 162 should parse room leave message payload +# roomLeaveEventMessageParser() ZH-bot-delete-others +ok 163 the same as bot-delete-other # SKIP +# roomLeaveEventMessageParser() ZH-other-delete-bot +ok 164 should parse room leave message payload +# roomLeaveEventMessageParser() ZH-other-delete-other +ok 165 bot will not see any message, can not detected # SKIP +# roomLeaveEventMessageParser() ZH-other-delete-others +ok 166 bot will not see any message, can not detected # SKIP +# roomTopicEventMessageParser() EN-other-modify-topic +ok 167 should parse event +# roomTopicEventMessageParser() EN-bot-modify-topic +ok 168 should parse event +# roomTopicEventMessageParser() not detected +ok 169 should return null for undefined +ok 170 should return null for null +ok 171 should return null for string +ok 172 should return null for empty object +ok 173 should return null for PadchatMessagePayload with unknown content +# roomTopicEventMessageParser() ZH-bot-modify-topic +ok 174 should parse room topic message payload +# roomTopicEventMessageParser() ZH-other-modify-topic +ok 175 should parse room topic message payload +# splitChineseNameList() +ok 176 should split chinese name list +# splitEnglihshNameList() +ok 177 should split english name list +# PuppetPuppeteerBridge +ok 178 Bridge instnace +# preHtmlToXml() +ok 179 should parse html to xml +# testBlockedMessage() +# not blocked +ok 180 should return false when no block message +# html +ok 181 should get zh blocked message +# zh +ok 182 should get zh blocked message +# clickSwitchAccount() +# switch account needed +ok 183 should click the switch account button +# switch account not needed +ok 184 should no button found +# WechatyBro.ding() +ok 185 should instanciated a bridge +ok 186 should init Bridge +ok 187 should got dong after execute WechatyBro.ding() +ok 188 should got a boolean after call proxyWechaty(loginState) +ok 189 b.quit() +# Puppet Puppeteer Event smoke testing +ok 190 should be inited +ok 191 should be quited +# parseFriendConfirm() +ok 192 should be truthy for confirm msg: You have added ๆŽๅ“ๆก“ as your WeChat contact. Start chatting! +ok 193 should be truthy for confirm msg: ไฝ ๅทฒๆทปๅŠ ไบ†ๆŽๅ“ๆก“๏ผŒ็Žฐๅœจๅฏไปฅๅผ€ๅง‹่Šๅคฉไบ†ใ€‚ +ok 194 should be truthy for confirm msg: johnbassserver@gmail.com just added you to his/her contacts list. Send a message to him/her now! +ok 195 should be truthy for confirm msg: johnbassserver@gmail.comๅˆšๅˆšๆŠŠไฝ ๆทปๅŠ ๅˆฐ้€š่ฎฏๅฝ•๏ผŒ็Žฐๅœจๅฏไปฅๅผ€ๅง‹่Šๅคฉไบ†ใ€‚ +ok 196 should be falsy for other msg +# parseRoomJoin() +ok 197 should check room join message right for You invited ็ฎก็†ๅ‘˜ to the group chat. +ok 198 should get inviteeList right +ok 199 should get inviter right +ok 200 should check room join message right for You invited ๆŽๅ“ๆก“.PreAngelใ€Bruce LEE to the group chat. +ok 201 should get inviteeList right +ok 202 should get inviter right +ok 203 should check room join message right for ็ฎก็†ๅ‘˜ invited ๅฐๆก”ๅปบ็พคๅŠฉๆ‰‹ to the group chat +ok 204 should get inviteeList right +ok 205 should get inviter right +ok 206 should check room join message right for ็ฎก็†ๅ‘˜ invited ๅบ†ๆฌกใ€ๅฐๆก”ๅฆน to the group chat +ok 207 should get inviteeList right +ok 208 should get inviter right +ok 209 should check room join message right for ไฝ ้‚€่ฏท"็ฎก็†ๅ‘˜"ๅŠ ๅ…ฅไบ†็พค่Š +ok 210 should get inviteeList right +ok 211 should get inviter right +ok 212 should check room join message right for "็ฎก็†ๅ‘˜"้‚€่ฏท"ๅฎ้”้”‹"ๅŠ ๅ…ฅไบ†็พค่Š +ok 213 should get inviteeList right +ok 214 should get inviter right +ok 215 should check room join message right for "็ฎก็†ๅ‘˜"้€š่ฟ‡ๆ‰ซๆไฝ ๅˆ†ไบซ็š„ไบŒ็ปด็ ๅŠ ๅ…ฅ็พค่Š +ok 216 should get inviteeList right +ok 217 should get inviter right +ok 218 should check room join message right for " ๆก”ๅฐ็ง˜"้€š่ฟ‡ๆ‰ซๆ"ๆŽไฝณ่Šฎ"ๅˆ†ไบซ็š„ไบŒ็ปด็ ๅŠ ๅ…ฅ็พค่Š +ok 219 should get inviteeList right +ok 220 should get inviter right +ok 221 should check room join message right for "็ฎก็†ๅ‘˜" joined group chat via the QR code you shared. +ok 222 should get inviteeList right +ok 223 should get inviter right +ok 224 should check room join message right for "ๅฎ้”้”‹" joined the group chat via the QR Code shared by "็ฎก็†ๅ‘˜". +ok 225 should get inviteeList right +ok 226 should get inviter right +ok 227 should throws if message is not expected +# parseRoomLeave() +ok 228 should get leaver for leave message: You removed "Bruce LEE" from the group chat +ok 229 should get leaver name right +ok 230 should get leaver for leave message: ไฝ ๅฐ†"ๆŽไฝณ่Šฎ"็งปๅ‡บไบ†็พค่Š +ok 231 should get leaver name right +ok 232 should get remover for leave message: You were removed from the group chat by "ๆก”ๅฐ็ง˜" +ok 233 should get leaver name right +ok 234 should get remover for leave message: ไฝ ่ขซ"ๆŽไฝณ่Šฎ"็งปๅ‡บ็พค่Š +ok 235 should get leaver name right +ok 236 should throw if message is not expected +# parseRoomTopic() +ok 237 should check topic right for content: "ๆŽๅ“ๆก“.PreAngel" changed the group name to "ding" +ok 238 should get right topic +ok 239 should get right changer +ok 240 should check topic right for content: "ๆŽไฝณ่Šฎ"ไฟฎๆ”น็พคๅไธบโ€œdongโ€ +ok 241 should get right topic +ok 242 should get right changer +ok 243 should throw if message is not expected +# PuppetPuppeteer Module Exports +ok 244 should export PuppetPuppeteer +# login/logout events +ok 245 should be inited +ok 246 should be not logined +ok 247 should be logined +ok 248 bridge.getUserName should be called +ok 249 puppet.contactRawPayload should be called +ok 250 contactList stub should be called +ok 251 should call stubContacList 4 times +ok 252 should fire logout event +ok 253 should be logouted +# Contact smoke testing +ok 254 id/UserName right +ok 255 NickName set +ok 256 should get the right alias from Contact +# PuppetPuppeteerFriendship.receive smoke testing +ok 257 should has right request message +ok 258 should have a Contact instance +ok 259 should be receive type +# PuppetPuppeteerFriendship.confirm smoke testing +ok 260 should match confirm message +ok 261 should have a Contact instance +ok 262 should be confirm type +# constructor() +ok 263 id right +ok 264 from right +ok 265 toString() +# ready() +ok 266 id/MsgId right +ok 267 contact ready for FromUserName +ok 268 contact ready for FromNickName +ok 269 contact ready for ToUserName +ok 270 contact ready for ToNickName +# find() +ok 271 Message found +ok 272 Message.id is ok +# findAll() +ok 273 Message.findAll with limit 2 +# self() +ok 274 should identify self message true where message from MOCK_CONTACT +ok 275 should identify self message false when from a different fromId +# mentioned() +ok 276 @_@ in message should not be treat as contact +ok 277 user@email.com in message should not be treat as contact +ok 278 @_@ wow! my email is ruiruibupt@gmail.com in message should not be treat as contact +ok 279 @ๅฐๆก”ๅŒๅญฆ is a contact +ok 280 should get ๅฐๆก”ๅŒๅญฆ id right in rawPayload21 +ok 281 @ๅฐๆก”ๅŒๅญฆ and @wuli่ˆžๅ“ฉๅฎขๆœ is a contact +ok 282 should get ๅฐๆก”ๅŒๅญฆ id right in rawPayload22 +ok 283 should get wuli่ˆžๅ“ฉๅฎขๆœ id right in rawPayload22 +ok 284 @wuli่ˆžๅ“ฉๅฎขๆœ is a contact +ok 285 should get wuli่ˆžๅ“ฉๅฎขๆœ id right in rawPayload31 +ok 286 @ๅฐๆก”ๅŒๅญฆ and @wuli่ˆžๅ“ฉๅฎขๆœ is a contact +ok 287 should get wuli่ˆžๅ“ฉๅฎขๆœ id right in rawPayload32 +ok 288 should get ๅฐๆก”ๅŒๅญฆ id right in rawPayload32 +# Room smok testing +ok 289 should set id/UserName right +ok 290 should set topic/NickName +ok 291 should get roomAlias +ok 292 should return null if not set roomAlias +ok 293 should has contact1 +ok 294 should has no this member +12:02:13 INFO Room owner() +ok 295 should get Contact instance for owner, or null +ok 296 should get the right id from @ad85207730aa94e006ddce28f74e6878, find member by default +ok 297 should get the right id from @72c4767ce32db488871fdd1c27173b81, find member by default +ok 298 should get the right id from @ecff4a7a86f23455dc42317269aa36ab, find member by default +ok 299 should get the right id from @ad85207730aa94e006ddce28f74e6878, find member by roomAlias +ok 300 toString() +# Room iterator for contact in it +ok 301 should get one of the room member: @ecff4a7a86f23455dc42317269aa36ab +ok 302 should get one of the room member: @eac4377ecfd59e4321262f892177169f +ok 303 should get one of the room member: @ad85207730aa94e006ddce28f74e6878 +ok 304 should get one of the room member: @33cc239d22b20d56395bbbd0967b28b9 +ok 305 should get one of the room member: @5e77381e1e3b5641ddcee44670b6e83a +ok 306 should get one of the room member: @56941ef97f3e9c70af88667fdd613b44 +ok 307 should get one of the room member: @72c4767ce32db488871fdd1c27173b81 +ok 308 should get one of the room member: @0b0e2eb9501ab2d84f9f800f6a0b4216 +ok 309 should get one of the room member: @4bfa767be0cd3fb78409b9735d1dcc57 +ok 310 should get one of the room member: @ad954bf2159a572b7743a5bc134739f4 +ok 311 should iterate all the members of the room +# contactQueryFilterFunction() +# filter name by regex +ok 312 should filter the query to id list +# filter name by text +ok 313 should filter query to id list +# filter alias by regex +ok 314 should filter query to id list +# filter alias by text +ok 315 should filter query to id list +# throw if filter key unknown +ok 316 should throw +# throw if filter key are more than one +ok 317 should throw +# roomQueryFilterFunction() +# filter name by regex +ok 318 should filter the query to id list +# filter name by text +ok 319 should filter query to id list +# throw if filter key unknown +ok 320 should throw +# throw if filter key are more than one +ok 321 should throw +# Should not be able to instanciate directly +ok 322 should throw when `Contact.load()` +ok 323 should throw when `Contact.load()` +# Should not be able to instanciate through cloneClass without puppet +ok 324 should throw when `MyContact.load()` without puppet +ok 325 should throw when `MyContact.load()` without puppet +# should be able to instanciate through cloneClass with puppet +ok 326 should get contact instance from `MyContact.load() +ok 327 should not throw when `MyContact().load` +ok 328 should get contact instance from `MyContact.load()` +ok 329 should not throw when `MyContact.load()` +# should throw when instanciate the global class +ok 330 should throw when we instanciate a global class +# Export of the Framework +ok 331 should export Contact +ok 332 should export Friendship +ok 333 should export IoClient +ok 334 should export Message +ok 335 should export Puppet +ok 336 should export Room +ok 337 should export Wechaty +ok 338 should export log +ok 339 should return version as the same in package.json +ok 340 should export version in package.json +# Config setting +ok 341 should export Config +ok 342 should has DEFAULT_PUPPET +# event:start/stop +12:02:13 INFO Wechaty start() v#git[2b61db2] is starting... +12:02:13 INFO Wechaty initPuppet() using puppet: puppeteer +12:02:15 INFO Wechaty stop() v#git[2b61db2] is stoping ... +ok 343 should get event:start once +ok 344 should get event:stop once +# on(event, Function) +12:02:15 INFO Wechaty stop() v#git[2b61db2] is stoping ... +ok 345 should get event:error once +ok 346 should get error from message listener +# initPuppetAccessory() +ok 347 should not throw for the 1st time init +ok 348 should throw for the 2nd time init +# Wechaty restart for many times +12:02:15 INFO Wechaty start() v#git[2b61db2] is starting... +12:02:15 INFO Wechaty stop() v#git[2b61db2] is stoping ... +ok 349 start/stop-ed at #0 +12:02:15 INFO Wechaty start() v#git[2b61db2] is starting... +12:02:15 INFO Wechaty stop() v#git[2b61db2] is stoping ... +ok 350 start/stop-ed at #1 +12:02:15 INFO Wechaty start() v#git[2b61db2] is starting... +12:02:15 INFO Wechaty stop() v#git[2b61db2] is stoping ... +ok 351 start/stop-ed at #2 +ok 352 Wechaty start/restart successed. +# Electron smoke testing +ok 353 test +# Node.js function params destructuring behaviour test +ok 354 should be equal to default args +ok 355 should be equal to default s args +ok 356 should be equal to default n args +# Puppeteer smoke testing +ok 357 Browser instnace +ok 358 should get version +ok 359 should create newPage for browser +ok 360 should open wx.qq.com +ok 361 should get 42 +# evaluate() a function that returns a Promise +ok 362 should get resolved value of promise inside browser +# evaluate() a file and get the returns value +ok 363 should inject file inside browser and return the value +ok 364 should no wechaty by default +ok 365 should has window by default +# page.on(console) +ok 366 should be called once +ok 367 should get log type +ok 368 should get console.log 1st/2nd arg +# page.exposeFunction() +ok 369 should be called once inside browser +ok 370 should be called with 42 +# other demos +ok 371 should get version +ok 372 should get cookies +ok 373 should get cookies with name +ok 374 should evaluated function for () => 8 * 7 = 56 +ok 375 should evaluated 1 + 2 = 3 +ok 376 should get the url right +# en +ok 377 should get en blocked message + +1..377 +# tests 377 +# pass 377 + +# ok + +Safe Test: successed at round #0! + +> wechaty@0.17.11 test:shell /home/zixia/chatie/wechaty +> shellcheck bin/*.sh + + +> wechaty@0.17.11 sloc /home/zixia/chatie/wechaty +> sloc bin examples scripts src tests --details --format cli-table --keys total,source,comment && sloc bin examples scripts src tests + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Path โ”‚ Physical โ”‚ Source โ”‚ Comment โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ bin/doctor.ts โ”‚ 54 โ”‚ 27 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ bin/io-client.ts โ”‚ 76 โ”‚ 43 โ”‚ 20 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ bin/version.ts โ”‚ 23 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/api-ai-bot.ts โ”‚ 217 โ”‚ 131 โ”‚ 62 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/busy-bot.ts โ”‚ 172 โ”‚ 101 โ”‚ 35 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/contact-bot.ts โ”‚ 136 โ”‚ 72 โ”‚ 40 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/ding-dong-bot.ts โ”‚ 211 โ”‚ 130 โ”‚ 52 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/friend-bot.ts โ”‚ 128 โ”‚ 67 โ”‚ 43 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/media-file-bot.ts โ”‚ 55 โ”‚ 24 โ”‚ 25 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/roger-bot.ts โ”‚ 45 โ”‚ 17 โ”‚ 27 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/room-bot.ts โ”‚ 434 โ”‚ 249 โ”‚ 124 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/self-testing-bot.ts โ”‚ 301 โ”‚ 158 โ”‚ 102 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/speech-to-text-bot.ts โ”‚ 184 โ”‚ 93 โ”‚ 68 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/telegram-roger-bot.js โ”‚ 55 โ”‚ 19 โ”‚ 33 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/the-worlds-shortest-chatbot-code-in-6-lines.js โ”‚ 25 โ”‚ 6 โ”‚ 20 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/tuling123-bot.ts โ”‚ 97 โ”‚ 42 โ”‚ 44 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/blessed-twins-bot/index.ts โ”‚ 344 โ”‚ 230 โ”‚ 65 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/gist-bot/index.ts โ”‚ 65 โ”‚ 31 โ”‚ 24 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/gist-bot/on-friend.ts โ”‚ 78 โ”‚ 37 โ”‚ 34 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/gist-bot/on-message.ts โ”‚ 82 โ”‚ 38 โ”‚ 33 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/gist-bot/on-room-join.ts โ”‚ 80 โ”‚ 36 โ”‚ 33 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-import-bot/index.js โ”‚ 42 โ”‚ 7 โ”‚ 33 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/monster/config.js โ”‚ 21 โ”‚ 12 โ”‚ 7 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/monster/index.js โ”‚ 69 โ”‚ 23 โ”‚ 36 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-reload-bot/index.js โ”‚ 120 โ”‚ 69 โ”‚ 37 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-import-bot/listeners/on-friend.js โ”‚ 26 โ”‚ 6 โ”‚ 19 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-import-bot/listeners/on-login.js โ”‚ 21 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-import-bot/listeners/on-message.js โ”‚ 21 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-import-bot/listeners/on-scan.js โ”‚ 26 โ”‚ 7 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/monster/listeners/on-friend.js โ”‚ 40 โ”‚ 9 โ”‚ 26 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/monster/listeners/on-login.js โ”‚ 21 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/monster/listeners/on-message.js โ”‚ 86 โ”‚ 47 โ”‚ 28 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/monster/listeners/on-scan.js โ”‚ 26 โ”‚ 7 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-reload-bot/listener/friend.js โ”‚ 26 โ”‚ 7 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-reload-bot/listener/login.js โ”‚ 21 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-reload-bot/listener/message.js โ”‚ 40 โ”‚ 17 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ examples/hot-reload-bot/listener/scan.js โ”‚ 23 โ”‚ 5 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ scripts/development-release.ts โ”‚ 18 โ”‚ 9 โ”‚ 8 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ scripts/package-publish-config-tag-next.ts โ”‚ 19 โ”‚ 12 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ scripts/retry-unit-tests.ts โ”‚ 54 โ”‚ 42 โ”‚ 10 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ scripts/sort-contributiveness.ts โ”‚ 114 โ”‚ 85 โ”‚ 12 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ scripts/update-license.ts โ”‚ 155 โ”‚ 117 โ”‚ 28 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/accessory.spec.ts โ”‚ 84 โ”‚ 39 โ”‚ 25 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/accessory.ts โ”‚ 172 โ”‚ 83 โ”‚ 60 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/config.spec.ts โ”‚ 86 โ”‚ 33 โ”‚ 43 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/config.ts โ”‚ 235 โ”‚ 127 โ”‚ 77 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/deprecated.ts โ”‚ 6 โ”‚ 6 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/doctor.ts โ”‚ 91 โ”‚ 45 โ”‚ 37 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/index.ts โ”‚ 46 โ”‚ 33 โ”‚ 5 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/io-client.ts โ”‚ 224 โ”‚ 135 โ”‚ 45 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/io.spec.ts โ”‚ 25 โ”‚ 20 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/io.ts โ”‚ 521 โ”‚ 340 โ”‚ 91 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/misc.spec.ts โ”‚ 252 โ”‚ 198 โ”‚ 23 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/misc.ts โ”‚ 313 โ”‚ 177 โ”‚ 118 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-config.ts โ”‚ 28 โ”‚ 12 โ”‚ 13 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/typings.d.ts โ”‚ 13 โ”‚ 9 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/wechaty.spec.ts โ”‚ 184 โ”‚ 93 โ”‚ 55 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/wechaty.ts โ”‚ 976 โ”‚ 553 โ”‚ 305 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/index.ts โ”‚ 10 โ”‚ 8 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/puppet.spec.ts โ”‚ 283 โ”‚ 210 โ”‚ 25 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/puppet.ts โ”‚ 906 โ”‚ 551 โ”‚ 196 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-mock/index.ts โ”‚ 25 โ”‚ 5 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-mock/puppet-mock.spec.ts โ”‚ 27 โ”‚ 21 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-mock/puppet-mock.ts โ”‚ 433 โ”‚ 306 โ”‚ 50 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/config.ts โ”‚ 28 โ”‚ 20 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/index.ts โ”‚ 25 โ”‚ 5 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/padchat-manager.spec.ts โ”‚ 129 โ”‚ 108 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/padchat-manager.ts โ”‚ 979 โ”‚ 660 โ”‚ 140 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/padchat-rpc.ts โ”‚ 1499 โ”‚ 959 โ”‚ 352 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/padchat-rpc.type.ts โ”‚ 190 โ”‚ 148 โ”‚ 93 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/padchat-schemas.ts โ”‚ 445 โ”‚ 145 โ”‚ 320 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/puppet-padchat.spec.ts โ”‚ 25 โ”‚ 16 โ”‚ 3 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/puppet-padchat.ts โ”‚ 1247 โ”‚ 850 โ”‚ 144 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/index.ts โ”‚ 25 โ”‚ 5 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/puppet-wechat4u.ts โ”‚ 883 โ”‚ 529 โ”‚ 201 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/typings.d.ts โ”‚ 20 โ”‚ 15 โ”‚ 3 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/web-schemas.ts โ”‚ 294 โ”‚ 133 โ”‚ 161 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/contact.accessory.spec.ts โ”‚ 76 โ”‚ 56 โ”‚ 5 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/contact.ts โ”‚ 661 โ”‚ 305 โ”‚ 287 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/favorite.ts โ”‚ 17 โ”‚ 10 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/friendship.ts โ”‚ 248 โ”‚ 128 โ”‚ 77 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/index.ts โ”‚ 7 โ”‚ 7 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/message.ts โ”‚ 654 โ”‚ 341 โ”‚ 235 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/moment.ts โ”‚ 19 โ”‚ 12 โ”‚ 4 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/money.ts โ”‚ 7 โ”‚ 5 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/user/room.ts โ”‚ 822 โ”‚ 377 โ”‚ 350 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/bridge.spec.ts โ”‚ 212 โ”‚ 155 โ”‚ 26 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/bridge.ts โ”‚ 1018 โ”‚ 754 โ”‚ 122 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/event.spec.ts โ”‚ 49 โ”‚ 21 โ”‚ 22 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/event.ts โ”‚ 247 โ”‚ 133 โ”‚ 73 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/firer.spec.ts โ”‚ 209 โ”‚ 162 โ”‚ 24 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/firer.ts โ”‚ 497 โ”‚ 281 โ”‚ 132 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/index.spec.ts โ”‚ 30 โ”‚ 8 โ”‚ 20 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/index.ts โ”‚ 25 โ”‚ 5 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/puppet-puppeteer.spec.ts โ”‚ 124 โ”‚ 73 โ”‚ 33 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/puppet-puppeteer.ts โ”‚ 1568 โ”‚ 1066 โ”‚ 292 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/puppeteer-contact.spec.ts โ”‚ 88 โ”‚ 43 โ”‚ 27 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/puppeteer-friendship.spec.ts โ”‚ 145 โ”‚ 95 โ”‚ 23 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/puppeteer-message.spec.ts โ”‚ 463 โ”‚ 334 โ”‚ 32 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/puppeteer-room.spec.ts โ”‚ 227 โ”‚ 133 โ”‚ 49 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/web-schemas.ts โ”‚ 294 โ”‚ 133 โ”‚ 161 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/wechaty-bro.js โ”‚ 882 โ”‚ 605 โ”‚ 171 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/schemas/contact.ts โ”‚ 36 โ”‚ 31 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/schemas/friendship.ts โ”‚ 31 โ”‚ 25 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/schemas/message.ts โ”‚ 38 โ”‚ 33 โ”‚ 5 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/schemas/puppet.ts โ”‚ 74 โ”‚ 53 โ”‚ 14 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet/schemas/room.ts โ”‚ 32 โ”‚ 21 โ”‚ 8 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/compatible-wei-bug.spec.ts โ”‚ 16 โ”‚ 13 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/compatible-wei-bug.ts โ”‚ 20 โ”‚ 6 โ”‚ 13 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/contact-raw-payload-parser.spec.ts โ”‚ 103 โ”‚ 88 โ”‚ 8 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/contact-raw-payload-parser.ts โ”‚ 69 โ”‚ 39 โ”‚ 24 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/file-box-to-qrcode.spec.ts โ”‚ 27 โ”‚ 19 โ”‚ 3 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/file-box-to-qrcode.ts โ”‚ 37 โ”‚ 29 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/friendship-event-message-parser.confirm.spec.ts โ”‚ 80 โ”‚ 66 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/friendship-event-message-parser.receive.spec.ts โ”‚ 34 โ”‚ 25 โ”‚ 3 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/friendship-event-message-parser.spec.ts โ”‚ 78 โ”‚ 73 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/friendship-event-message-parser.ts โ”‚ 123 โ”‚ 83 โ”‚ 16 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/friendship-event-message-parser.verify.spec.ts โ”‚ 56 โ”‚ 44 โ”‚ 4 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/friendship-raw-payload-parser.spec.ts โ”‚ 41 โ”‚ 26 โ”‚ 4 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/friendship-raw-payload-parser.ts โ”‚ 97 โ”‚ 72 โ”‚ 9 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/index.ts โ”‚ 28 โ”‚ 14 โ”‚ 13 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/is-type.spec.ts โ”‚ 65 โ”‚ 49 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/is-type.ts โ”‚ 49 โ”‚ 39 โ”‚ 5 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/message-file-name.ts โ”‚ 9 โ”‚ 8 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/message-raw-payload-parser.spec.ts โ”‚ 304 โ”‚ 225 โ”‚ 51 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/message-raw-payload-parser.ts โ”‚ 191 โ”‚ 104 โ”‚ 50 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/message-type.ts โ”‚ 64 โ”‚ 45 โ”‚ 5 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/padchat-decode.spec.ts โ”‚ 59 โ”‚ 49 โ”‚ 4 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/padchat-decode.ts โ”‚ 22 โ”‚ 14 โ”‚ 5 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-join-message-parser.en.spec.ts โ”‚ 328 โ”‚ 292 โ”‚ 7 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-join-message-parser.spec.ts โ”‚ 65 โ”‚ 52 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-join-message-parser.ts โ”‚ 316 โ”‚ 214 โ”‚ 56 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-join-message-parser.zh.spec.ts โ”‚ 309 โ”‚ 272 โ”‚ 8 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-leave-message-parser.en.spec.ts โ”‚ 81 โ”‚ 65 โ”‚ 3 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-leave-message-parser.spec.ts โ”‚ 47 โ”‚ 35 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-leave-message-parser.ts โ”‚ 93 โ”‚ 64 โ”‚ 14 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-leave-message-parser.zh.spec.ts โ”‚ 82 โ”‚ 65 โ”‚ 4 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-topic-message-parser.en.spec.ts โ”‚ 67 โ”‚ 56 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-topic-message-parser.spec.ts โ”‚ 47 โ”‚ 35 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-topic-message-parser.ts โ”‚ 70 โ”‚ 50 โ”‚ 5 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-event-topic-message-parser.zh.spec.ts โ”‚ 71 โ”‚ 56 โ”‚ 4 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-raw-payload-parser.spec.ts โ”‚ 4 โ”‚ 0 โ”‚ 3 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/room-raw-payload-parser.ts โ”‚ 19 โ”‚ 16 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/split-name.spec.ts โ”‚ 25 โ”‚ 18 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/split-name.ts โ”‚ 9 โ”‚ 6 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-padchat/pure-function-helpers/t.ts โ”‚ 33 โ”‚ 25 โ”‚ 3 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/pure-function-helpers/index.ts โ”‚ 4 โ”‚ 4 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/pure-function-helpers/is-type.ts โ”‚ 7 โ”‚ 6 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/pure-function-helpers/message-file-name.ts โ”‚ 73 โ”‚ 58 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/pure-function-helpers/message-payload-parser.ts โ”‚ 148 โ”‚ 91 โ”‚ 26 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-wechat4u/pure-function-helpers/message-type.ts โ”‚ 54 โ”‚ 30 โ”‚ 16 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/pure-function-helpers/index.ts โ”‚ 5 โ”‚ 5 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/pure-function-helpers/is-type.ts โ”‚ 7 โ”‚ 6 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/pure-function-helpers/message-extname.ts โ”‚ 57 โ”‚ 44 โ”‚ 2 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/pure-function-helpers/message-filename.ts โ”‚ 25 โ”‚ 20 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/pure-function-helpers/message-raw-payload-parser.ts โ”‚ 86 โ”‚ 69 โ”‚ 9 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ src/puppet-puppeteer/pure-function-helpers/web-message-type.ts โ”‚ 51 โ”‚ 27 โ”‚ 16 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/electron.spec.ts โ”‚ 32 โ”‚ 8 โ”‚ 21 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/node.spec.ts โ”‚ 53 โ”‚ 23 โ”‚ 22 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/puppeteer.spec.ts โ”‚ 251 โ”‚ 172 โ”‚ 35 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/inject-file.js โ”‚ 7 โ”‚ 7 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/smoke-testing.ts โ”‚ 27 โ”‚ 23 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/es6-import.js โ”‚ 3 โ”‚ 2 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/import-require.ts โ”‚ 22 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/js-bot.js โ”‚ 4 โ”‚ 3 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/syntax-error.js โ”‚ 1 โ”‚ 1 โ”‚ 0 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/ts-bot.ts โ”‚ 22 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/type-error.ts โ”‚ 21 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/with-package-json/with-import-error.ts โ”‚ 22 โ”‚ 3 โ”‚ 19 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/with-package-json/with-import.ts โ”‚ 22 โ”‚ 3 โ”‚ 18 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/with-package-json/with-require-error.js โ”‚ 4 โ”‚ 3 โ”‚ 1 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ tests/fixtures/docker/with-package-json/with-require.js โ”‚ 4 โ”‚ 3 โ”‚ 0 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + +---------- Result ------------ + + Physical : 27630 + Source : 17120 + Comment : 6772 + Single-line comment : 2313 + Block comment : 4459 + Mixed : 417 + Empty : 4173 + To Do : 77 + +Number of files read : 172 + +------------------------------ +``` diff --git a/tests/docker.bats b/tests/docker.bats new file mode 100644 index 0000000..d36fb86 --- /dev/null +++ b/tests/docker.bats @@ -0,0 +1,88 @@ +#!/usr/bin/env bats + +function dockerRun() { + docker run --rm -v "$(pwd)":/bot ${IMAGE_NAME:-'wechaty/wechaty:artifact'} $@ +} + +fixtures=tests/fixtures/docker + +@test "should succ with a simple javascript" { + cd "$fixtures" + run dockerRun cjs/js-bot.js + [ "$status" -eq 0 ] +} + +@test "should fail when javascript syntax error" { + cd "$fixtures" + run dockerRun syntax-error.js + [ "$status" -ne 0 ] +} + +@test "should succ with javascript ESM import syntax" { + cd "$fixtures" + run dockerRun esm/es6-import.js + [ "$status" -eq 0 ] # should succ +} + +@test "should fail with javascript es6 import when setting NODE_ENV=production" { + cd "$fixtures" + run dockerRun -e NODE_ENV=production es6-import.js + [ "$status" -ne 0 ] # should fail +} + +@test "should succ with a simple typescript" { + cd "$fixtures" + run dockerRun esm/ts-bot.ts + [ "$status" -eq 0 ] +} + +@test "should not fail with unmatch types in typescript (with `ts-node.transpileOnly=true`)" { + cd "$fixtures" + run dockerRun type-error.ts + [ "$status" -eq 0 ] +} + +@test "should succ when using require with javascript" { + cd "$fixtures/with-package-json/" + run dockerRun with-require.cjs + [ "$status" -eq 0 ] +} + +@test "should fail when require a not exist module in javascript" { + cd "$fixtures/with-package-json/" + run dockerRun with-require-error.js + [ "$status" -ne 0 ] +} + +@test "should succ when using import in typescript" { + cd "$fixtures/with-package-json/" + run dockerRun with-import.ts + [ "$status" -eq 0 ] +} + +@test "should fail when import a not exist module in typescript" { + cd "$fixtures/with-package-json/" + run dockerRun with-import-error.ts + [ "$status" -ne 0 ] +} + +@test "should succ with arg: 'doctor' (which is the default by npm run)" { + run dockerRun doctor + [ "$status" -eq 0 ] +} + +@test "should succ with arg: 'run doctor'" { + run dockerRun run doctor + [ "$status" -eq 0 ] +} + +@test "should fail when run an unknown arg" { + run dockerRun run fdasfadsfasdfasdfasdfasd + [ "$status" -ne 0 ] +} + +@test "should fail when passing an unknown arg directly" { + run dockerRun fdasfadsfasdfasdfasdfasd + [ "$status" -ne 0 ] +} + diff --git a/tests/electron.spec.ts b/tests/electron.spec.ts new file mode 100755 index 0000000..d928a40 --- /dev/null +++ b/tests/electron.spec.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + test, +} from 'tstest' + +test('Electron smoke testing', async t => { + t.ok(true, 'test') +}) + +test.skip('Electron open wx', async t => { + t.ok('ok') +}) diff --git a/tests/fixtures/docker/cjs/js-bot.js b/tests/fixtures/docker/cjs/js-bot.js new file mode 100644 index 0000000..1ab3a46 --- /dev/null +++ b/tests/fixtures/docker/cjs/js-bot.js @@ -0,0 +1,4 @@ +const { WechatyBuilder } = require('wechaty') + +const bot = WechatyBuilder.build() +console.log(bot.version()) diff --git a/tests/fixtures/docker/cjs/package.json b/tests/fixtures/docker/cjs/package.json new file mode 100644 index 0000000..a3c15a7 --- /dev/null +++ b/tests/fixtures/docker/cjs/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/fixtures/docker/esm/es6-import.js b/tests/fixtures/docker/esm/es6-import.js new file mode 100644 index 0000000..15ec271 --- /dev/null +++ b/tests/fixtures/docker/esm/es6-import.js @@ -0,0 +1,3 @@ +import { WechatyBuilder } from 'wechaty' + +console.log(WechatyBuilder.build().version()) diff --git a/tests/fixtures/docker/esm/package.json b/tests/fixtures/docker/esm/package.json new file mode 100644 index 0000000..5ffd980 --- /dev/null +++ b/tests/fixtures/docker/esm/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/fixtures/docker/esm/ts-bot.ts b/tests/fixtures/docker/esm/ts-bot.ts new file mode 100644 index 0000000..38b5534 --- /dev/null +++ b/tests/fixtures/docker/esm/ts-bot.ts @@ -0,0 +1,23 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { WechatyBuilder } from 'wechaty' + +const bot = WechatyBuilder.build() +console.info(bot.version()) diff --git a/tests/fixtures/docker/syntax-error.js b/tests/fixtures/docker/syntax-error.js new file mode 100644 index 0000000..da3ea14 --- /dev/null +++ b/tests/fixtures/docker/syntax-error.js @@ -0,0 +1 @@ +hello, I'm not a javascript file diff --git a/tests/fixtures/docker/type-error.ts b/tests/fixtures/docker/type-error.ts new file mode 100644 index 0000000..219336d --- /dev/null +++ b/tests/fixtures/docker/type-error.ts @@ -0,0 +1,22 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +let i = 3 +i = "should set to string because it's type is number" +console.info(i) diff --git a/tests/fixtures/docker/with-package-json/package.json b/tests/fixtures/docker/with-package-json/package.json new file mode 100644 index 0000000..b3f1783 --- /dev/null +++ b/tests/fixtures/docker/with-package-json/package.json @@ -0,0 +1,16 @@ +{ + "name": "with-package-json", + "version": "1.0.0", + "description": "", + "type": "module", + "dependencies": { + "brolog": "^0.4.3" + }, + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/tests/fixtures/docker/with-package-json/with-import-error.ts b/tests/fixtures/docker/with-package-json/with-import-error.ts new file mode 100644 index 0000000..1160c5b --- /dev/null +++ b/tests/fixtures/docker/with-package-json/with-import-error.ts @@ -0,0 +1,23 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { NotExist } from 'not-exist-at-all-fdsafasdfafsad' + +const ne = new NotExist() +console.info(ne) // should not run to here diff --git a/tests/fixtures/docker/with-package-json/with-import.ts b/tests/fixtures/docker/with-package-json/with-import.ts new file mode 100644 index 0000000..4a5b2de --- /dev/null +++ b/tests/fixtures/docker/with-package-json/with-import.ts @@ -0,0 +1,21 @@ +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { log } from 'wechaty' +log.info('OK', 'with-import.ts') diff --git a/tests/fixtures/docker/with-package-json/with-require-error.js b/tests/fixtures/docker/with-package-json/with-require-error.js new file mode 100644 index 0000000..a5e6b38 --- /dev/null +++ b/tests/fixtures/docker/with-package-json/with-require-error.js @@ -0,0 +1,4 @@ +const { NotExist } = require('not-exist-at-all-fasdfasdsfaf') + +const ne = new NowExist() +console.log(ne) // should not run to here diff --git a/tests/fixtures/docker/with-package-json/with-require.cjs b/tests/fixtures/docker/with-package-json/with-require.cjs new file mode 100644 index 0000000..4170708 --- /dev/null +++ b/tests/fixtures/docker/with-package-json/with-require.cjs @@ -0,0 +1,4 @@ +const { Brolog } = require('brolog') + +const brolog = new Brolog() +brolog.info('OK', 'with-require.js') diff --git a/tests/fixtures/smoke-testing.ts b/tests/fixtures/smoke-testing.ts new file mode 100644 index 0000000..f5eb352 --- /dev/null +++ b/tests/fixtures/smoke-testing.ts @@ -0,0 +1,89 @@ +#!/usr/bin/env node +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + Wechaty, + WechatyBuilder, + VERSION, +} from 'wechaty' + +import * as USERS from 'wechaty/users' +import * as IMPLS from 'wechaty/impls' + +import assert from 'assert' + +function getBotList (): Wechaty[] { + const botList = [ + WechatyBuilder.build({ puppet: 'wechaty-puppet-mock' }), + // new Wechaty({ puppet: 'wechaty-puppet-wechat4u' }), + // new Wechaty({ puppet: 'wechaty-puppet-puppeteer' }), + ] + + if (process.env.WECHATY_PUPPET_SERVICE_TOKEN) { + botList.push( + WechatyBuilder.build({ + puppet: 'wechaty-puppet-service', + }) + ) + } + if (process.env.WECHATY_PUPPET_PADLOCAL_TOKEN) { + botList.push( + WechatyBuilder.build({ + puppet: 'wechaty-puppet-padlocal', + }) + ) + } + + return botList +} + +async function main () { + const botList = getBotList() + try { + await Promise.all( + botList.map(bot => bot.start()), + ) + for (const bot of botList) { + console.info(`Wechaty v${bot.version()} smoking test passed.`) + } + } catch (e) { + console.error(e) + // Error! + return 1 + } finally { + await Promise.all( + botList.map(bot => bot.stop()), + ) + } + + const tag: USERS.Tag = {} as any as IMPLS.TagImpl + assert.ok(tag, 'should get valid USERS & IMPLS') + + assert.notStrictEqual(VERSION, '0.0.0', 'VERSION must be set!') + + return 0 +} + +main() + .then(process.exit) + .catch(e => { + console.error(e) + process.exit(1) + }) diff --git a/tests/node.spec.ts b/tests/node.spec.ts new file mode 100755 index 0000000..6edd84f --- /dev/null +++ b/tests/node.spec.ts @@ -0,0 +1,53 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm +/** + * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty + * + * @copyright 2016 Huan LI (ๆŽๅ“ๆก“) , and + * Wechaty Contributors . + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import { + test, + sinon, +} from 'tstest' + +test('Node.js function params destructuring behaviour test', async t => { + const sandbox = sinon.createSandbox() + + const DEFAULT_N = 1 + const DEFAULT_S = 't' + + const paramSpy = sandbox.spy() + function paramTest ({ + n = DEFAULT_N, + s = DEFAULT_S, + } = {}) { + paramSpy(n, s) + } + + paramSpy.resetHistory() + paramTest() + t.same(paramSpy.args[0], [ DEFAULT_N, DEFAULT_S ], 'should be equal to default args') + + paramSpy.resetHistory() + paramTest({ n: 42 }) + t.same(paramSpy.args[0], [ 42, DEFAULT_S ], 'should be equal to default s args') + + paramSpy.resetHistory() + paramTest({ s: 'life' }) + t.same(paramSpy.args[0], [ DEFAULT_N, 'life' ], 'should be equal to default n args') + + sandbox.restore() +}) diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..8693cd0 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist/cjs", + }, +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f8a1fe6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "@chatie/tsconfig", + "compilerOptions": { + "outDir": "dist/esm", + "lib": ["ESNext", "DOM"], + "verbatimModuleSyntax": false, + // See: https://github.com/wechaty/wechaty/issues/2551 + "ignoreDeprecations": "5.0" + }, + "exclude": [ + "node_modules/", + "dist/", + "tests/fixtures/", + ], + "include": [ + "app/**/*.ts", + "bin/*.ts", + "bot/**/*.ts", + "examples/**/*.ts", + "scripts/**/*.ts", + "src/**/*.ts", + "tests/**/*.spec.ts", + ], +}