├── .codeclimate.yml ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── codeclimate.yml │ ├── dispatch.yml │ └── go.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS ├── Makefile ├── NOTICE ├── README.md ├── VERSION ├── docs └── images │ ├── go-wechaty.png │ └── goland.png ├── examples └── ding-dong-bot.go ├── go.mod ├── go.sum ├── wechaty-puppet-mock └── puppet_mock.go ├── wechaty-puppet-service ├── ca.go ├── config.go ├── envvars.go ├── filebox.go ├── grpc.go ├── helper.go ├── options.go ├── puppet_service.go ├── puppet_service_test.go ├── resolver.go └── service_endpoint.go ├── wechaty-puppet ├── events │ └── events.go ├── file-box │ ├── file_box.go │ ├── fileboxtype_string.go │ ├── testdata │ │ └── .gitignore │ └── type.go ├── filebox │ ├── file_box.go │ ├── file_box_base64.go │ ├── file_box_file.go │ ├── file_box_qrcode.go │ ├── file_box_stream.go │ ├── file_box_test.go │ ├── file_box_unknown.go │ ├── file_box_url.go │ ├── file_box_uuid.go │ ├── file_box_uuid_test.go │ ├── testdata │ │ └── dchaofei.txt │ ├── type.go │ └── type_string.go ├── helper │ ├── array.go │ ├── async.go │ ├── base64.go │ ├── file.go │ ├── file_test.go │ ├── fix_unknown_message.go │ ├── http_client.go │ ├── parase_recalled_msg.go │ └── testdata │ │ └── a.txt ├── log │ └── log.go ├── memory-card │ ├── memory_card.go │ ├── memory_card_test.go │ ├── storage │ │ ├── backend.go │ │ ├── file.go │ │ ├── file_test.go │ │ ├── nop.go │ │ └── testdata │ │ │ └── .gitignore │ └── testdata │ │ └── .gitignore ├── message_adapter.go ├── option.go ├── puppet.go └── schemas │ ├── contact.go │ ├── contactgender_string.go │ ├── contacttype_string.go │ ├── events.go │ ├── friendship.go │ ├── friendshiptype_string.go │ ├── image.go │ ├── imagetype_string.go │ ├── location.go │ ├── message.go │ ├── messagetype_string.go │ ├── mini_program.go │ ├── payload.go │ ├── payloadtype_string.go │ ├── puppet.go │ ├── puppeteventname_string.go │ ├── room.go │ ├── room_invitation.go │ ├── scanstatus_string.go │ ├── type.go │ └── url_link.go └── wechaty ├── accessory.go ├── config.go ├── config └── config.go ├── event.go ├── factory ├── config.go ├── contact.go ├── friendship.go ├── image.go ├── message.go ├── room.go ├── room_invitation.go ├── tag.go └── url_link.go ├── interface ├── accessory.go ├── contact.go ├── contact_self.go ├── friendship.go ├── image.go ├── message.go ├── mini_program.go ├── room.go ├── room_invitation.go ├── tag.go ├── url_link.go └── wechaty.go ├── option.go ├── plugin.go ├── user ├── config.go ├── contact.go ├── contact_self.go ├── friendship.go ├── image.go ├── location.go ├── message.go ├── mini_program.go ├── room.go ├── room_invitation.go ├── tag.go └── url_link.go ├── wechaty.go └── wechaty_test.go /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | # For more configuration items, please refer to: https://docs.codeclimate.com/docs/maintainability#section-checks 4 | checks: 5 | argument-count: # 方法或函数最多参数个数,过多请考虑通过结构体传递 6 | config: 7 | threshold: 6 8 | complex-logic: # 难以理解的布尔逻辑,过多请考虑 switch 或拆分 9 | config: 10 | threshold: 4 11 | file-lines: # 文件最多行数,过多请拆分相关文件 12 | config: 13 | threshold: 1000 14 | method-complexity: # 函数和方法的逻辑复杂度 15 | config: 16 | threshold: 14 17 | method-count: # 结构体的方法限制 18 | config: 19 | threshold: 60 20 | method-lines: # 单个方法最多行数,过多请进行拆分 21 | config: 22 | threshold: 45 23 | nested-control-flow: # 深度嵌套的控制结构,请尽快返回结果,避免深度嵌套 24 | config: 25 | threshold: 6 26 | return-statements: # 函数或方法返回次数,过多请考虑拆分 27 | config: 28 | threshold: 12 29 | similar-code: # 相似代码检查 30 | config: 31 | threshold: 70 32 | identical-code: # 相同代码检查 33 | config: 34 | threshold: 25 35 | 36 | plugins: 37 | # "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite." - The Go Proverbs 38 | gofmt: 39 | enabled: true 40 | golint: 41 | enabled: true 42 | govet: 43 | enabled: true 44 | 45 | # Excluded folders or files 46 | exclude_patterns: 47 | - examples 48 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | max_line_length = 0 14 | trim_trailing_whitespace = false 15 | 16 | # 4 tab indentation 17 | [Makefile] 18 | indent_style = tab 19 | indent_size = 4 20 | 21 | [{*.go,go.mod,go.sum}] 22 | indent_style = tab 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/about-codeowners/ 3 | # 4 | 5 | * @wechaty/go 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug, question 6 | assignees: dchaofei, dingdayu 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Runtime environment** 27 | - OS: [e.g. Win,MacOS,Linux; darwin,freebsd] 28 | - ARCH: [e.g. amd64,arm] 29 | - Puppet: [e.g. hostie, padplus] 30 | - Version [e.g. v0.1.2] 31 | 32 | **Console output** 33 | Please put the detailed log in the label below: 34 | 35 |
36 |
37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: dchaofei, dingdayu, huan 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Please rate Feature** 20 | Please consider the scope of application and the difficulty of development. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/workflows/codeclimate.yml: -------------------------------------------------------------------------------- 1 | name: Codeclimate 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | name: coverage 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up Go 1.18 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: 1.18 16 | id: go 17 | 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v2 20 | 21 | - name: Codeclimate 22 | run: | 23 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./test-reporter 24 | chmod +x ./test-reporter 25 | ./test-reporter before-build 26 | go test -coverprofile c.out `go list ./... | grep -v /vendor/` -v -count=1 -coverpkg=./... 27 | sed -i "s/github.com\/wechaty\/go-wechaty\///g" c.out 28 | ./test-reporter format-coverage -t gocov 29 | ./test-reporter upload-coverage 30 | env: 31 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 32 | -------------------------------------------------------------------------------- /.github/workflows/dispatch.yml: -------------------------------------------------------------------------------- 1 | name: Send Dispatch 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | 9 | jobs: 10 | send: 11 | name: Send Dispatch 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | # Dispatch to update go-wechaty-getting-started deps. 16 | - name: Repository Dispatch 17 | uses: peter-evans/repository-dispatch@v1.1.1 18 | with: 19 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 20 | repository: wechaty/go-wechaty-getting-started 21 | event-type: updatedeps 22 | client-payload: '{"tag":"${{ github.ref_name }}"}' 23 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | 11 | - name: Set up Go 1.18 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: 1.18 15 | id: go 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v2 19 | 20 | - name: Install dependencies 21 | run: make install 22 | 23 | - name: Go Vet 24 | run: | 25 | go mod download 26 | go vet ./... 27 | 28 | - name: Go Test 29 | run: make test 30 | 31 | - name: Build 32 | run: go build -o ding-dong -v ./examples/ding-dong-bot.go 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .idea/ 17 | .vscode 18 | .DS_Store 19 | 20 | coverage.out 21 | report.json 22 | tests_report.xml 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[go]": { 3 | "editor.formatOnSave": false, 4 | }, 5 | "editor.fontFamily": "'Fira Code iScript', Consolas, 'Courier New', monospace", 6 | "editor.fontLigatures": true, 7 | 8 | "editor.tokenColorCustomizations": { 9 | "textMateRules": [ 10 | { 11 | "scope": [ 12 | //following will be in italics (=Pacifico) 13 | "comment", 14 | // "entity.name.type.class", //class names 15 | "keyword", //import, export, return… 16 | "support.class.builtin.js", //String, Number, Boolean…, this, super 17 | "storage.modifier", //static keyword 18 | "storage.type.class.js", //class keyword 19 | "storage.type.function.js", // function keyword 20 | "storage.type.js", // Variable declarations 21 | "keyword.control.import.js", // Imports 22 | "keyword.control.from.js", // From-Keyword 23 | "entity.name.type.js", // new … Expression 24 | "keyword.control.flow.js", // await 25 | "keyword.control.conditional.js", // if 26 | "keyword.control.loop.js", // for 27 | "keyword.operator.new.js", // new 28 | ], 29 | "settings": { 30 | "fontStyle": "italic", 31 | }, 32 | }, 33 | { 34 | "scope": [ 35 | //following will be excluded from italics (My theme (Monokai dark) has some defaults I don't want to be in italics) 36 | "invalid", 37 | "keyword.operator", 38 | "constant.numeric.css", 39 | "keyword.other.unit.px.css", 40 | "constant.numeric.decimal.js", 41 | "constant.numeric.json", 42 | "entity.name.type.class.js" 43 | ], 44 | "settings": { 45 | "fontStyle": "", 46 | }, 47 | } 48 | ] 49 | }, 50 | 51 | "files.exclude": { 52 | "dist/": true, 53 | "doc/": true, 54 | "node_modules/": true, 55 | "package/": true, 56 | }, 57 | "alignment": { 58 | "operatorPadding": "right", 59 | "indentBase": "firstline", 60 | "surroundSpace": { 61 | "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. 62 | "assignment": [1, 1], // The same as above. 63 | "arrow": [1, 1], // The same as above. 64 | "comment": 2, // Special how much space to add between the trailing comment and the code. 65 | // If this value is negative, it means don't align the trailing comment. 66 | } 67 | }, 68 | "editor.formatOnSave": false, 69 | "python.pythonPath": "python3", 70 | "eslint.validate": [ 71 | "javascript", 72 | "typescript", 73 | ], 74 | } 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Wechaty is a community driven open source project and we welcome any contributor. 4 | 5 | We have also prepared a nomination award, which can be apply after joining the organization. 6 | 7 | ## Before 8 | 9 | ### Sign the CLA 10 | 11 | Click the Sign in with Github to agree button to sign the CLA. See an example [here](https://cla-assistant.io/wechaty/go-wechaty). 12 | 13 | [Why CLA?](https://qastack.cn/software/168020/how-signing-out-a-cla-prevents-legal-issues-in-open-source-projects) 14 | 15 | ### Environment configuration 16 | 17 | You also need to begin to prepare the Go development environment. 18 | 19 | 1. download archive 20 | 2. extract archive 21 | 3. configure the path to an environment variable 22 | 23 | Yes, it is so simple, you can check it [here](https://golang.org/doc/install) 24 | 25 | ## Action 26 | 27 | Action is the best inspiration, contribution is the best result. 28 | 29 | > Yes, this sentence was just made up by me. I hope express to you that the community is inclusive and open, and it is a relaxed environment. 30 | 31 | ### Choose a Topic 32 | 33 | If you find a bug in the code, or something that can be improved, or invalid and redundant code; you can fork the project into your own repository, modify the code, and then submit the PR. 34 | 35 | However, if you modify multiple codes at the same time, please submit them in the form of a theme, so that we can discuss separately and merge into the trunk in stages. 36 | 37 | ### How to Write Go Code 38 | 39 | If you are new to Go, I hope [](https://golang.org/doc/code.html) can help you. 40 | 41 | If you already have relevant experience, hope the following list can unify our coding style: 42 | 1. Effective Go: [https://golang.google.cn/doc/effective\_go.html](https://golang.google.cn/doc/effective\_go.html) 43 | 2. Uber Go Style Guide: [https://github.com/uber-go/guide/blob/master/style.md](https://github.com/uber-go/guide/blob/master/style.md)([译文](https://github.com/gocn/translator/blob/master/2019/w38_uber_go_style_guide.md)) 44 | 3. Go Code Review Comments: [https://github.com/golang/go/wiki/CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) 45 | 46 | At the same time, we have configured related robots on PR to check the syntax and code style of the code. 47 | 48 | ## After 49 | 50 | ### Join Github Org Team 51 | 52 | If you have two PRs that are merged and valid, you will be invited to join Team, of course, the choice is yours. 53 | 54 | ### Nomination 55 | 56 | After joining the github team, please submit an application in the issue below: [Wechaty Contributors Nomination](https://github.com/wechaty/PMC/issues/16) 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Wechaty Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Huan LI (李卓桓) 2 | Xiaoyu DING (丁小雨) 3 | Bojie LI (李博杰) 4 | Chaofei DING (丁超飞) 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Go Wechaty 2 | # 3 | # GitHb: https://github.com/wechaty/python-wechaty 4 | # Author: Huan LI git.io/zixia 5 | # 6 | 7 | SOURCE_GLOB=$(wildcard bin/*.go src/**/*.go tests/**/*.go examples/*.go) 8 | VERSION=$(shell cat VERSION) 9 | 10 | .PHONY: all 11 | all : clean lint 12 | 13 | .PHONY: clean 14 | clean: 15 | rm -fr dist/* 16 | echo "clean what?" 17 | 18 | .PHONY: lint 19 | lint: golint 20 | 21 | .PHONY: golint 22 | golint: 23 | ~/go/bin/golint wechaty 24 | ~/go/bin/golint wechaty-puppet 25 | ~/go/bin/golint wechaty-puppet-service 26 | 27 | .PHONY: install 28 | install: 29 | go install golang.org/x/lint/golint@latest 30 | 31 | .PHONY: gotest 32 | gotest: 33 | go test `go list ./... | grep -v /vendor/` -count=1 -coverpkg=./... 34 | 35 | .PHONY: test 36 | test: golint gotest 37 | 38 | .PHONY: bot 39 | bot: 40 | go run examples/ding-dong-bot.go 41 | 42 | .PHONY: version 43 | version: 44 | @newVersion=$$(awk -F. '{print $$1"."$$2"."$$3+1}' < VERSION) \ 45 | && echo $${newVersion} > VERSION \ 46 | && echo VERSION := \'$${newVersion}\' > src/version.go \ 47 | && git add VERSION src/version.py \ 48 | && git commit -m "$${newVersion}" > /dev/null \ 49 | && git tag "v$${newVersion}" \ 50 | && echo "Bumped version to $${newVersion}" 51 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Go Wechaty Chatbot SDK 2 | Copyright 2020 Wechaty Contributors. 3 | 4 | This product includes software developed at 5 | The Wechaty Organization (https://github.com/wechaty). 6 | 7 | This software contains code derived from the Stackoverflow, 8 | including various modifications by GitHub. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-wechaty 2 | 3 | ![Go Version](https://img.shields.io/github/go-mod/go-version/wechaty/go-wechaty) 4 | [![Go](https://github.com/wechaty/go-wechaty/workflows/Go/badge.svg)](https://github.com/wechaty/go-wechaty/actions?query=workflow%3AGo) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/dbae0a43d431b0fccee5/maintainability)](https://codeclimate.com/github/wechaty/go-wechaty/maintainability) 6 | 7 | ![Go Wechaty](https://wechaty.github.io/go-wechaty/images/go-wechaty.png) 8 | 9 | [![Go Wechaty Getting Started](https://img.shields.io/badge/Go%20Wechaty-Getting%20Started-7de)](https://github.com/wechaty/go-wechaty-getting-started) 10 | [![Wechaty in Go](https://img.shields.io/badge/Wechaty-Go-7de)](https://github.com/wechaty/go-wechaty) 11 | 12 | ## Connecting Chatbots 13 | 14 | [![Powered by Wechaty](https://img.shields.io/badge/Powered%20By-Wechaty-brightgreen.svg)](https://github.com/Wechaty/wechaty) 15 | 16 | Wechaty is a RPA SDK for Wechat **Individual** Account that can help you create a chatbot in 6 lines of Go. 17 | 18 | ## Voice of the Developers 19 | 20 | > "Wechaty is a great solution, I believe there would be much more users recognize it." [link](https://github.com/Wechaty/wechaty/pull/310#issuecomment-285574472) 21 | > — @Gcaufy, Tencent Engineer, Author of [WePY](https://github.com/Tencent/wepy) 22 | > 23 | > "太好用,好用的想哭" 24 | > — @xinbenlv, Google Engineer, Founder of HaoShiYou.org 25 | > 26 | > "最好的微信开发库" [link](http://weibo.com/3296245513/Ec4iNp9Ld?type=comment) 27 | > — @Jarvis, Baidu Engineer 28 | > 29 | > "Wechaty让运营人员更多的时间思考如何进行活动策划、留存用户,商业变现" [link](http://mp.weixin.qq.com/s/dWHAj8XtiKG-1fIS5Og79g) 30 | > — @lijiarui, Founder & CEO of Juzi.BOT. 31 | > 32 | > "If you know js ... try Wechaty, it's easy to use." 33 | > — @Urinx Uri Lee, Author of [WeixinBot(Python)](https://github.com/Urinx/WeixinBot) 34 | 35 | See more at [Wiki:Voice Of Developer](https://github.com/Wechaty/wechaty/wiki/Voice%20Of%20Developer) 36 | 37 | ## Join Us 38 | 39 | Wechaty is used in many ChatBot projects by thousands of developers. If you want to talk with other developers, just scan the following QR Code in WeChat with secret code _go wechaty_, join our **Wechaty Go Developers' Home**. 40 | 41 | ![Wechaty Friday.BOT QR Code](https://wechaty.js.org/img/friday-qrcode.svg) 42 | 43 | Scan now, because other Wechaty Go developers want to talk with you too! (secret code: _go wechaty_) 44 | 45 | ## Usage 46 | 47 | ```go 48 | package main 49 | 50 | import ( 51 | "fmt" 52 | "github.com/wechaty/go-wechaty/wechaty" 53 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 54 | "github.com/wechaty/go-wechaty/wechaty/user" 55 | ) 56 | 57 | func main() { 58 | wechaty.NewWechaty(). 59 | OnScan(func(context *wechaty.Context, qrCode string, status schemas.ScanStatus, data string) { 60 | fmt.Printf("Scan QR Code to login: %s\nhttps://wechaty.github.io/qrcode/%s\n", status, qrCode) 61 | }). 62 | OnLogin(func(context *wechaty.Context, user *user.ContactSelf) { 63 | fmt.Printf("User %s logined\n", user) 64 | }). 65 | OnMessage(func(context *wechaty.Context, message *user.Message) { 66 | fmt.Printf("Message: %s\n", message) 67 | }).DaemonStart() 68 | } 69 | ``` 70 | 71 | ## Requirements 72 | 73 | 1. Go 1.18+ 74 | 75 | ## Install 76 | 77 | ```shell 78 | # go get wechaty 79 | 80 | go get github.com/wechaty/go-wechaty 81 | ``` 82 | 83 | ## Development 84 | 85 | ```sh 86 | make install 87 | make test 88 | ``` 89 | 90 | ## QA 91 | - wechaty-puppet-service: WECHATY_PUPPET_SERVICE_TOKEN not found ? 92 | - go-wechaty is the go language implementation of [wechaty](https://github.com/wechaty/wechaty) (TypeScript). Puppet is required to start wechaty, but it is currently known that puppets are written in TypeScript language. In order to enable go-wechaty to use these puppets, we can use wechaty-gateway to convert puppets into grpc service, let go-wechaty connect to the grpc service, go-wechaty -> wechaty-gateway -> puppet, document: https://wechaty.js.org/docs/puppet-services/diy/ 93 | - puppet list: https://wechaty.js.org/docs/puppet-providers/ 94 | 95 | ## See Also 96 | 97 | - [Learn Go in 12 Minutes](https://www.youtube.com/watch?v=C8LgvuEBraI) 98 | - [How to Write Go Code](https://golang.org/doc/code.html) 99 | - [Journey from OO language to Golang - Sergey Kibish @DevFest Switzerland 2018](https://www.youtube.com/watch?v=1ZjvhGfpwJ8) 100 | - [The Go Blog - Publishing Go Modules](https://blog.golang.org/publishing-go-modules) 101 | - [Effective Go](https://golang.org/doc/effective_go.html) 102 | 103 | ### Golang for Node.js Developer 104 | 105 | - [Golang for Node.js Developers - Examples of Golang examples compared to Node.js for learning](https://github.com/miguelmota/golang-for-nodejs-developers) 106 | - [Learning Go as a Node.js Developer](https://nemethgergely.com/learning-go-as-a-nodejs-developer/) 107 | - [Golang Tutorial for Node.js Developers](https://blog.risingstack.com/golang-tutorial-for-nodejs-developers-getting-started/) 108 | 109 | ## History 110 | 111 | ### master 112 | 113 | ### v0.4 (Jun 19, 2020) 114 | 115 | Go Wechaty Scala Wechaty **BETA** Released! 116 | 117 | Read more from our Multi-language Wechaty Beta Release event from our blog: 118 | 119 | - [Multi Language Wechaty Beta Release Announcement!](https://wechaty.js.org/2020/06/19/multi-language-wechaty-beta-release/) 120 | 121 | ### v0.1 (Apr 03 2020) 122 | 123 | 1. Welcome our second and third Go Wechaty contributors: 124 | - Bojie LI (李博杰) [#9](https://github.com/wechaty/go-wechaty/pull/9) 125 | - Chaofei DING (丁超飞) [#20](https://github.com/wechaty/go-wechaty/pull/20) 126 | 1. Enable [GitHub Actions](https://github.com/wechaty/go-wechaty/actions?query=workflow%3AGo) 127 | 1. Enable linting: [golint](https://github.com/golang/lint) 128 | 1. Enable testing: [testing](https://golang.org/pkg/testing/) 129 | 1. Add Makefile for easy developing 130 | 1. Re-structure module directories: from `src/wechaty` to `wechaty` 131 | 1. Rename example bot to `examples/ding-dong-bot.go` 132 | 133 | ### v0.0.1 (Mar 12, 2020) 134 | 135 | 1. Project created. 136 | 1. Welcome our first Go Wechaty contributor: 137 | - Xiaoyu DING (丁小雨) [#2](https://github.com/wechaty/go-wechaty/pull/2) 138 | 139 | ## Related Projects 140 | 141 | - [Wechaty](https://github.com/wechaty/wechaty) - Conversatioanl AI Chatot SDK for Wechaty Individual Accounts (TypeScript) 142 | - [Python Wechaty](https://github.com/wechaty/python-wechaty) - Python WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Python) 143 | - [Go Wechaty](https://github.com/wechaty/go-wechaty) - Go WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Go) 144 | - [Java Wechaty](https://github.com/wechaty/java-wechaty) - Java WeChaty Conversational AI Chatbot SDK for Wechat Individual Accounts (Java) 145 | - [Scala Wechaty](https://github.com/wechaty/scala-wechaty) - Scala WeChaty Conversational AI Chatbot SDK for WechatyIndividual Accounts (Scala) 146 | 147 | ## Badge 148 | 149 | [![Wechaty in Go](https://img.shields.io/badge/Wechaty-Go-7de)](https://github.com/wechaty/go-wechaty) 150 | 151 | ```md 152 | [![Wechaty in Go](https://img.shields.io/badge/Wechaty-Go-7de)](https://github.com/wechaty/go-wechaty) 153 | ``` 154 | 155 | ## Contributors 156 | 157 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/0)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/0) 158 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/1)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/1) 159 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/2)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/2) 160 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/3)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/3) 161 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/4)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/4) 162 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/5)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/5) 163 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/6)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/6) 164 | [![contributor](https://sourcerer.io/fame/huan/wechaty/go-wechaty/images/7)](https://sourcerer.io/fame/huan/wechaty/go-wechaty/links/7) 165 | 166 | 1. [@SilkageNet](https://github.com/SilkageNet) - Bojie LI (李博杰) 167 | 1. [@huan](https://github.com/huan) - Huan LI (李卓桓) 168 | 169 | ## Creators 170 | 171 | - [@dchaofei](https://github.com/dchaofei) - Chaofei DING (丁超飞) 172 | - [@dingdayu](https://github.com/dingdayu) - Xiaoyu DING (丁小雨) 173 | 174 | ## Copyright & License 175 | 176 | - Code & Docs © 2020 Wechaty Contributors 177 | - Code released under the Apache-2.0 License 178 | - Docs released under Creative Commons 179 | 180 | ## Thanks 181 | goland.png 182 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /docs/images/go-wechaty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/go-wechaty/dd2d312935802edc721b2b7c1719713e39f0bdab/docs/images/go-wechaty.png -------------------------------------------------------------------------------- /docs/images/goland.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/go-wechaty/dd2d312935802edc721b2b7c1719713e39f0bdab/docs/images/goland.png -------------------------------------------------------------------------------- /examples/ding-dong-bot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | "os" 8 | "time" 9 | 10 | "github.com/mdp/qrterminal/v3" 11 | "github.com/wechaty/go-wechaty/wechaty" 12 | wp "github.com/wechaty/go-wechaty/wechaty-puppet" 13 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 14 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 15 | "github.com/wechaty/go-wechaty/wechaty/user" 16 | ) 17 | 18 | func main() { 19 | var bot = wechaty.NewWechaty(wechaty.WithPuppetOption(wp.Option{ 20 | Token: "", 21 | })) 22 | 23 | bot.OnScan(onScan).OnLogin(func(ctx *wechaty.Context, user *user.ContactSelf) { 24 | fmt.Printf("User %s logined\n", user.Name()) 25 | }).OnMessage(onMessage).OnLogout(func(ctx *wechaty.Context, user *user.ContactSelf, reason string) { 26 | fmt.Printf("User %s logouted: %s\n", user, reason) 27 | }) 28 | 29 | bot.DaemonStart() 30 | } 31 | 32 | func onMessage(ctx *wechaty.Context, message *user.Message) { 33 | log.Println(message) 34 | 35 | if message.Self() { 36 | log.Println("Message discarded because its outgoing") 37 | return 38 | } 39 | 40 | if message.Age() > 2*60*time.Second { 41 | log.Println("Message discarded because its TOO OLD(than 2 minutes)") 42 | return 43 | } 44 | 45 | if message.Type() != schemas.MessageTypeText || message.Text() != "#ding" { 46 | log.Println("Message discarded because it does not match #ding") 47 | return 48 | } 49 | 50 | // 1. reply text 'dong' 51 | _, err := message.Say("dong") 52 | if err != nil { 53 | log.Println(err) 54 | return 55 | } 56 | log.Println("REPLY with text: dong") 57 | 58 | // 2. reply image(qrcode image) 59 | fileBox := filebox.FromUrl("https://wechaty.github.io/wechaty/images/bot-qr-code.png") 60 | _, err = message.Say(fileBox) 61 | if err != nil { 62 | log.Println(err) 63 | return 64 | } 65 | 66 | log.Printf("REPLY with image: %s\n", fileBox) 67 | 68 | // 3. reply url link 69 | urlLink := user.NewUrlLink(&schemas.UrlLinkPayload{ 70 | Description: "Go Wechaty is a Conversational SDK for Chatbot Makers Written in Go", 71 | ThumbnailUrl: "https://wechaty.js.org/img/icon.png", 72 | Title: "wechaty/go-wechaty", 73 | Url: "https://github.com/wechaty/go-wechaty", 74 | }) 75 | _, err = message.Say(urlLink) 76 | if err != nil { 77 | log.Println(err) 78 | return 79 | } 80 | log.Printf("REPLY with urlLink: %s\n", urlLink) 81 | } 82 | 83 | func onScan(ctx *wechaty.Context, qrCode string, status schemas.ScanStatus, data string) { 84 | if status == schemas.ScanStatusWaiting || status == schemas.ScanStatusTimeout { 85 | qrterminal.GenerateHalfBlock(qrCode, qrterminal.L, os.Stdout) 86 | 87 | qrcodeImageUrl := fmt.Sprintf("https://wechaty.js.org/qrcode/%s", url.QueryEscape(qrCode)) 88 | fmt.Printf("onScan: %s - %s\n", status, qrcodeImageUrl) 89 | return 90 | } 91 | fmt.Printf("onScan: %s\n", status) 92 | } 93 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wechaty/go-wechaty 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hashicorp/golang-lru v0.5.4 7 | github.com/lucsky/cuid v1.0.2 8 | github.com/mdp/qrterminal/v3 v3.0.0 9 | github.com/otiai10/opengraph v1.1.1 10 | github.com/sirupsen/logrus v1.9.0 11 | github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 12 | github.com/tuotoo/qrcode v0.0.0-20190222102259-ac9c44189bf2 13 | github.com/wechaty/go-grpc v1.5.2 14 | google.golang.org/grpc v1.45.0 15 | google.golang.org/protobuf v1.27.1 16 | ) 17 | 18 | require ( 19 | github.com/golang/protobuf v1.5.2 // indirect 20 | github.com/google/uuid v1.1.2 // indirect 21 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0 // indirect 22 | github.com/maruel/rs v0.0.0-20150922171536-2c81c4312fe4 // indirect 23 | github.com/stretchr/testify v1.8.0 // indirect 24 | github.com/willf/bitset v1.1.10 // indirect 25 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect 26 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 27 | golang.org/x/text v0.3.7 // indirect 28 | google.golang.org/genproto v0.0.0-20220317150908-0efb43f6373e // indirect 29 | rsc.io/qr v0.2.0 // indirect 30 | ) 31 | -------------------------------------------------------------------------------- /wechaty-puppet-mock/puppet_mock.go: -------------------------------------------------------------------------------- 1 | package wechaty_puppet_mock 2 | 3 | import ( 4 | wechatyPuppet "github.com/wechaty/go-wechaty/wechaty-puppet" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 6 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 7 | ) 8 | 9 | var _ wechatyPuppet.IPuppetAbstract = &PuppetMock{} 10 | 11 | type PuppetMock struct { 12 | *wechatyPuppet.Puppet 13 | } 14 | 15 | func (p *PuppetMock) MessageLocation(messageID string) (*schemas.LocationPayload, error) { 16 | //TODO implement me 17 | panic("implement me") 18 | } 19 | 20 | func (p *PuppetMock) MessageSendLocation(conversationID string, payload *schemas.LocationPayload) (string, error) { 21 | //TODO implement me 22 | panic("implement me") 23 | } 24 | 25 | func NewPuppetMock(option wechatyPuppet.Option) (*PuppetMock, error) { 26 | puppetAbstract, err := wechatyPuppet.NewPuppet(option) 27 | if err != nil { 28 | return nil, err 29 | } 30 | puppetMock := &PuppetMock{ 31 | Puppet: puppetAbstract, 32 | } 33 | puppetAbstract.SetPuppetImplementation(puppetMock) 34 | return puppetMock, nil 35 | } 36 | 37 | func (p *PuppetMock) Start() error { 38 | go func() { 39 | // emit scan 40 | p.Emit(schemas.PuppetEventNameScan, &schemas.EventScanPayload{ 41 | BaseEventPayload: schemas.BaseEventPayload{}, 42 | Status: schemas.ScanStatusWaiting, 43 | QrCode: "https://not-exist.com", 44 | }) 45 | }() 46 | return nil 47 | } 48 | 49 | func (p PuppetMock) MessageImage(messageID string, imageType schemas.ImageType) (*filebox.FileBox, error) { 50 | panic("implement me") 51 | } 52 | 53 | func (p PuppetMock) FriendshipRawPayload(friendshipID string) (*schemas.FriendshipPayload, error) { 54 | panic("implement me") 55 | } 56 | 57 | func (p PuppetMock) FriendshipAccept(friendshipID string) error { 58 | panic("implement me") 59 | } 60 | 61 | func (p PuppetMock) RoomInvitationRawPayload(roomInvitationID string) (*schemas.RoomInvitationPayload, error) { 62 | panic("implement me") 63 | } 64 | 65 | func (p PuppetMock) RoomInvitationAccept(roomInvitationID string) error { 66 | panic("implement me") 67 | } 68 | 69 | func (p PuppetMock) MessageSendText(conversationID string, text string, mentionIdList ...string) (string, error) { 70 | panic("implement me") 71 | } 72 | 73 | func (p PuppetMock) MessageSendContact(conversationID string, contactID string) (string, error) { 74 | panic("implement me") 75 | } 76 | 77 | func (p PuppetMock) MessageSendFile(conversationID string, fileBox *filebox.FileBox) (string, error) { 78 | panic("implement me") 79 | } 80 | 81 | func (p PuppetMock) MessageSendURL(conversationID string, urlLinkPayload *schemas.UrlLinkPayload) (string, error) { 82 | panic("implement me") 83 | } 84 | 85 | func (p PuppetMock) MessageSendMiniProgram(conversationID string, urlLinkPayload *schemas.MiniProgramPayload) (string, error) { 86 | panic("implement me") 87 | } 88 | 89 | func (p *PuppetMock) Stop() { 90 | panic("implement me") 91 | } 92 | 93 | func (p *PuppetMock) Logout() error { 94 | panic("implement me") 95 | } 96 | 97 | func (p *PuppetMock) Ding(data string) { 98 | panic("implement me") 99 | } 100 | 101 | func (p *PuppetMock) SetContactAlias(contactID string, alias string) error { 102 | panic("implement me") 103 | } 104 | 105 | func (p *PuppetMock) ContactAlias(contactID string) (string, error) { 106 | panic("implement me") 107 | } 108 | 109 | func (p *PuppetMock) ContactList() ([]string, error) { 110 | panic("implement me") 111 | } 112 | 113 | func (p *PuppetMock) ContactQRCode(contactID string) (string, error) { 114 | panic("implement me") 115 | } 116 | 117 | func (p *PuppetMock) SetContactAvatar(contactID string, fileBox *filebox.FileBox) error { 118 | panic("implement me") 119 | } 120 | 121 | func (p *PuppetMock) ContactAvatar(contactID string) (*filebox.FileBox, error) { 122 | panic("implement me") 123 | } 124 | 125 | func (p *PuppetMock) ContactRawPayload(contactID string) (*schemas.ContactPayload, error) { 126 | panic("implement me") 127 | } 128 | 129 | func (p *PuppetMock) SetContactSelfName(name string) error { 130 | panic("implement me") 131 | } 132 | 133 | func (p *PuppetMock) ContactSelfQRCode() (string, error) { 134 | panic("implement me") 135 | } 136 | 137 | func (p *PuppetMock) SetContactSelfSignature(signature string) error { 138 | panic("implement me") 139 | } 140 | 141 | func (p *PuppetMock) MessageMiniProgram(messageID string) (*schemas.MiniProgramPayload, error) { 142 | panic("implement me") 143 | } 144 | 145 | func (p *PuppetMock) MessageContact(messageID string) (string, error) { 146 | panic("implement me") 147 | } 148 | 149 | func (p *PuppetMock) MessageRecall(messageID string) (bool, error) { 150 | panic("implement me") 151 | } 152 | 153 | func (p *PuppetMock) MessageFile(id string) (*filebox.FileBox, error) { 154 | panic("implement me") 155 | } 156 | 157 | func (p *PuppetMock) MessageRawPayload(id string) (*schemas.MessagePayload, error) { 158 | panic("implement me") 159 | } 160 | 161 | func (p *PuppetMock) MessageURL(messageID string) (*schemas.UrlLinkPayload, error) { 162 | panic("implement me") 163 | } 164 | 165 | func (p *PuppetMock) RoomRawPayload(id string) (*schemas.RoomPayload, error) { 166 | panic("implement me") 167 | } 168 | 169 | func (p *PuppetMock) RoomList() ([]string, error) { 170 | panic("implement me") 171 | } 172 | 173 | func (p *PuppetMock) RoomDel(roomID, contactID string) error { 174 | panic("implement me") 175 | } 176 | 177 | func (p *PuppetMock) RoomAvatar(roomID string) (*filebox.FileBox, error) { 178 | panic("implement me") 179 | } 180 | 181 | func (p *PuppetMock) RoomAdd(roomID, contactID string) error { 182 | panic("implement me") 183 | } 184 | 185 | func (p *PuppetMock) SetRoomTopic(roomID string, topic string) error { 186 | panic("implement me") 187 | } 188 | 189 | func (p *PuppetMock) RoomTopic(roomID string) (string, error) { 190 | panic("implement me") 191 | } 192 | 193 | func (p *PuppetMock) RoomCreate(contactIDList []string, topic string) (string, error) { 194 | panic("implement me") 195 | } 196 | 197 | func (p *PuppetMock) RoomQuit(roomID string) error { 198 | panic("implement me") 199 | } 200 | 201 | func (p *PuppetMock) RoomQRCode(roomID string) (string, error) { 202 | panic("implement me") 203 | } 204 | 205 | func (p *PuppetMock) RoomMemberList(roomID string) ([]string, error) { 206 | panic("implement me") 207 | } 208 | 209 | func (p *PuppetMock) RoomMemberRawPayload(roomID string, contactID string) (*schemas.RoomMemberPayload, error) { 210 | panic("implement me") 211 | } 212 | 213 | func (p *PuppetMock) SetRoomAnnounce(roomID, text string) error { 214 | panic("implement me") 215 | } 216 | 217 | func (p *PuppetMock) RoomAnnounce(roomID string) (string, error) { 218 | panic("implement me") 219 | } 220 | 221 | func (p *PuppetMock) FriendshipSearchPhone(phone string) (string, error) { 222 | panic("implement me") 223 | } 224 | 225 | func (p *PuppetMock) FriendshipSearchWeixin(weixin string) (string, error) { 226 | panic("implement me") 227 | } 228 | 229 | func (p *PuppetMock) FriendshipAdd(contactID, hello string) (err error) { 230 | panic("implement me") 231 | } 232 | 233 | func (p *PuppetMock) TagContactAdd(id, contactID string) (err error) { 234 | panic("implement me") 235 | } 236 | 237 | func (p *PuppetMock) TagContactRemove(id, contactID string) (err error) { 238 | panic("implement me") 239 | } 240 | 241 | func (p *PuppetMock) TagContactDelete(id string) (err error) { 242 | panic("implement me") 243 | } 244 | 245 | func (p *PuppetMock) TagContactList(contactID string) ([]string, error) { 246 | panic("implement me") 247 | } 248 | 249 | func (p *PuppetMock) MessageRawMiniProgramPayload(messageID string) (*schemas.MiniProgramPayload, error) { 250 | panic("implement me") 251 | } 252 | -------------------------------------------------------------------------------- /wechaty-puppet-service/ca.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import "context" 4 | 5 | // TLSInsecureServerCertCommonName Common Name: 6 | // - Server Name Indication (SNI) 7 | // - Case insensitive 8 | // Wechaty Token format: `${SNI}/${UUIDv4}` 9 | const TLSInsecureServerCertCommonName = "insecure" 10 | 11 | // TLSCaCert Wechaty Certificate Authority Repo: 12 | /* https://github.com/wechaty/dotenv/tree/main/ca 13 | 14 | The SSL_ROOT_CERT is a root certificate generated by and for wechaty community. 15 | 16 | Because it's the default root cert for the puppet service, 17 | so all the Polyglot Wechaty SDK should set this cert to be trusted by default. 18 | 19 | Update: 20 | - Huan(202108): init, expired in 3650 days (after 2031/07) 21 | */ 22 | const TLSCaCert = `-----BEGIN CERTIFICATE----- 23 | MIIFxTCCA62gAwIBAgIUYddLAoa8JnLzJ80l2u5vGuFsaEIwDQYJKoZIhvcNAQEL 24 | BQAwcjELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDVNhbiBGcmFuY2lzY28xEjAQBgNV 25 | BAcMCVBhbG8gQWx0bzEQMA4GA1UECgwHV2VjaGF0eTELMAkGA1UECwwCQ0ExGDAW 26 | BgNVBAMMD3dlY2hhdHktcm9vdC1jYTAeFw0yMTA4MDkxNTQ4NTJaFw0zMTA4MDcx 27 | NTQ4NTJaMHIxCzAJBgNVBAYTAlVTMRYwFAYDVQQIDA1TYW4gRnJhbmNpc2NvMRIw 28 | EAYDVQQHDAlQYWxvIEFsdG8xEDAOBgNVBAoMB1dlY2hhdHkxCzAJBgNVBAsMAkNB 29 | MRgwFgYDVQQDDA93ZWNoYXR5LXJvb3QtY2EwggIiMA0GCSqGSIb3DQEBAQUAA4IC 30 | DwAwggIKAoICAQDulLjOZhzQ58TSQ7TfWNYgdtWhlc+5L9MnKb1nznVRhzAkZo3Q 31 | rPLRW/HDjlv2OEbt4nFLaQgaMmc1oJTUVGDBDlrzesI/lJh7z4eA/B0z8eW7f6Cw 32 | /TGc8lgzHvq7UIE507QYPhvfSejfW4Prw+90HJnuodriPdMGS0n9AR37JPdQm6sD 33 | iMFeEvhHmM2SXRo/o7bll8UDZi81DoFu0XuTCx0esfCX1W5QWEmAJ5oAdjWxJ23C 34 | lxI1+EjwBQKXGqp147VP9+pwpYW5Xxpy870kctPBHKjCAti8Bfo+Y6dyWz2UAd4w 35 | 4BFRD+18C/TgX+ECl1s9fsHMY15JitcSGgAIz8gQX1OelECaTMRTQfNaSnNW4LdS 36 | sXMQEI9WxAU/W47GCQFmwcJeZvimqDF1QtflHSaARD3O8tlbduYqTR81LJ63bPoy 37 | 9e1pdB6w2bVOTlHunE0YaGSJERALVc1xz40QpPGcZ52mNCb3PBg462RQc77yv/QB 38 | x/P2RC1y0zDUF2tP9J29gTatWq6+D4MhfEk2flZNyzAgJbDuT6KAIJGzOB1ZJ/MG 39 | o1gS13eTuZYw24LElrhd1PrR6OHK+lkyYzqUPYMulUg4HzaZIDclfHKwAC4lecKm 40 | zC5q9jJB4m4SKMKdzxvpIOfdahoqsZMg34l4AavWRqPTpwEU0C0dboNA/QIDAQAB 41 | o1MwUTAdBgNVHQ4EFgQU0rey3QPklTOgdhMJ9VIA6KbZ5bAwHwYDVR0jBBgwFoAU 42 | 0rey3QPklTOgdhMJ9VIA6KbZ5bAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B 43 | AQsFAAOCAgEAx2uyShx9kLoB1AJ8x7Vf95v6PX95L/4JkJ1WwzJ9Dlf3BcCI7VH7 44 | Fp1dnQ6Ig7mFqSBDBAUUBWAptAnuqIDcgehI6XAEKxW8ZZRxD877pUNwZ/45tSC4 45 | b5U5y9uaiNK7oC3LlDCsB0291b3KSOtevMeDFoh12LcliXAkdIGGTccUxrH+Cyij 46 | cBOc+EKGJFBdLqcjLDU4M6QdMMMFOdfXyAOSpYuWGYqrxqvxQjAjvianEyMpNZWM 47 | lajggJqiPhfF67sZTB2yzvRTmtHdUq7x+iNOVonOBcCHu31aGxa9Py91XEr9jaIQ 48 | EBdl6sycLxKo8mxF/5tyUOns9+919aWNqTOUBmI15D68bqhhOVNyvsb7aVURIt5y 49 | 6A7Sj4gSBR9P22Ba6iFZgbvfLn0zKLzjlBonUGlSPf3rSIYUkawICtDyYPvK5mi3 50 | mANgIChMiOw6LYCPmmUVVAWU/tDy36kr9ZV9YTIZRYAkWswsJB340whjuzvZUVaG 51 | DgW45GPR6bGIwlFZeqCwXLput8Z3C8Sw9bE9vjlB2ZCpjPLmWV/WbDlH3J3uDjgt 52 | 9PoALW0sOPhHfYklH4/rrmsSWMYTUuGS/HqxrEER1vpIOOb0hIiAWENDT/mruq22 53 | VqO8MHX9ebjInSxPmhYOlrSZrOgEcogyMB4Z0SOtKVqPnkWmdR5hatU= 54 | -----END CERTIFICATE-----` 55 | 56 | type callCredToken struct { 57 | token string 58 | } 59 | 60 | func (r callCredToken) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { 61 | return map[string]string{ 62 | "authorization": "Wechaty " + r.token, 63 | }, nil 64 | } 65 | 66 | func (r callCredToken) RequireTransportSecurity() bool { 67 | return true 68 | } 69 | -------------------------------------------------------------------------------- /wechaty-puppet-service/config.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | logger "github.com/wechaty/go-wechaty/wechaty-puppet/log" 5 | ) 6 | 7 | var log = logger.L.WithField("module", "wechaty-puppet-service") 8 | -------------------------------------------------------------------------------- /wechaty-puppet-service/envvars.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | ) 7 | 8 | // ErrTokenNotFound err token not found 9 | var ErrTokenNotFound = errors.New("wechaty-puppet-service: WECHATY_PUPPET_SERVICE_TOKEN not found") 10 | 11 | func envServiceToken(token string) (string, error) { 12 | if token != "" { 13 | return token, nil 14 | } 15 | 16 | token = os.Getenv("WECHATY_PUPPET_SERVICE_TOKEN") 17 | if token != "" { 18 | return token, nil 19 | } 20 | 21 | return "", ErrTokenNotFound 22 | } 23 | 24 | func envEndpoint(endpoint string) string { 25 | if endpoint != "" { 26 | return endpoint 27 | } 28 | 29 | endpoint = os.Getenv("WECHATY_PUPPET_SERVICE_ENDPOINT") 30 | if endpoint != "" { 31 | return endpoint 32 | } 33 | 34 | return "" 35 | } 36 | 37 | func envAuthority(authority string) string { 38 | if authority != "" { 39 | return authority 40 | } 41 | 42 | authority = os.Getenv("WECHATY_PUPPET_SERVICE_AUTHORITY") 43 | if authority != "" { 44 | return authority 45 | } 46 | 47 | return "api.chatie.io" 48 | } 49 | 50 | func envNoTLSInsecureClient(disable bool) bool { 51 | return disable || os.Getenv("WECHATY_PUPPET_SERVICE_NO_TLS_INSECURE_CLIENT") == "true" 52 | } 53 | 54 | func envTLSServerName(serverName string) string { 55 | if serverName != "" { 56 | return serverName 57 | } 58 | 59 | return os.Getenv("WECHATY_PUPPET_SERVICE_TLS_SERVER_NAME") 60 | } 61 | 62 | func envTLSCaCert(caCert string) string { 63 | if caCert != "" { 64 | return caCert 65 | } 66 | caCert = os.Getenv("WECHATY_PUPPET_SERVICE_TLS_CA_CERT") 67 | if caCert != "" { 68 | return caCert 69 | } 70 | return TLSCaCert 71 | } 72 | -------------------------------------------------------------------------------- /wechaty-puppet-service/filebox.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | pbwechaty "github.com/wechaty/go-grpc/wechaty" 7 | pbwechatypuppet "github.com/wechaty/go-grpc/wechaty/puppet" 8 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 9 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 10 | "io" 11 | ) 12 | 13 | // ErrNoName err no name 14 | var ErrNoName = errors.New("no name") 15 | 16 | // NewFileBoxFromMessageFileStream ... 17 | func NewFileBoxFromMessageFileStream(client pbwechaty.Puppet_MessageFileStreamClient) (*filebox.FileBox, error) { 18 | recv, err := client.Recv() 19 | if err != nil { 20 | return nil, err 21 | } 22 | name := recv.FileBoxChunk.GetName() 23 | if name == "" { 24 | return nil, ErrNoName 25 | } 26 | 27 | return filebox.FromStream(NewMessageFile(client), filebox.WithName(name)), nil 28 | } 29 | 30 | // MessageFile 把 grpc 流包装到 io.Reader 接口 31 | type MessageFile struct { 32 | client pbwechaty.Puppet_MessageFileStreamClient 33 | buffer bytes.Buffer 34 | done bool 35 | } 36 | 37 | // Read 把 grpc 流包装到 io.Reader 接口 38 | func (m *MessageFile) Read(p []byte) (n int, err error) { 39 | if m.done { 40 | return m.buffer.Read(p) 41 | } 42 | 43 | for { 44 | if m.buffer.Len() >= len(p) { 45 | break 46 | } 47 | recv, err := m.client.Recv() 48 | if err == io.EOF { 49 | m.done = true 50 | break 51 | } 52 | if err != nil { 53 | return 0, err 54 | } 55 | _, err = m.buffer.Write(recv.FileBoxChunk.GetData()) 56 | if err != nil { 57 | return 0, err 58 | } 59 | } 60 | return m.buffer.Read(p) 61 | } 62 | 63 | // NewMessageFile ... 64 | func NewMessageFile(client pbwechaty.Puppet_MessageFileStreamClient) *MessageFile { 65 | return &MessageFile{ 66 | client: client, 67 | buffer: bytes.Buffer{}, 68 | done: false, 69 | } 70 | } 71 | 72 | // MessageSendFile 把 grpc 流包装到 io.Writer 接口 73 | type MessageSendFile struct { 74 | client pbwechaty.Puppet_MessageSendFileStreamClient 75 | fileBox *filebox.FileBox 76 | count int 77 | } 78 | 79 | // Write 把 grpc 流包装到 io.Writer 接口 80 | func (m *MessageSendFile) Write(p []byte) (n int, err error) { 81 | if len(p) == 0 { 82 | return 0, nil 83 | } 84 | fileDataRequest := &pbwechatypuppet.MessageSendFileStreamRequest{ 85 | FileBoxChunk: &pbwechatypuppet.FileBoxChunk{ 86 | Data: p, 87 | Name: nil, 88 | }, 89 | } 90 | m.count++ 91 | if err := m.client.Send(fileDataRequest); err != nil { 92 | return 0, err 93 | } 94 | return len(p), nil 95 | } 96 | 97 | // ToMessageSendFileWriter 把 grpc 流包装到 io.Writer 接口 98 | func ToMessageSendFileWriter(client pbwechaty.Puppet_MessageSendFileStreamClient, conversationID string, fileBox *filebox.FileBox) (io.Writer, error) { 99 | // 发送 conversationID 100 | { 101 | idRequest := &pbwechatypuppet.MessageSendFileStreamRequest{ 102 | ConversationId: &conversationID, 103 | } 104 | if err := client.Send(idRequest); err != nil { 105 | return nil, err 106 | } 107 | } 108 | 109 | // 发送 fileName 110 | { 111 | fileNameRequest := &pbwechatypuppet.MessageSendFileStreamRequest{ 112 | FileBoxChunk: &pbwechatypuppet.FileBoxChunk{ 113 | Name: &fileBox.Name, 114 | }, 115 | } 116 | if err := client.Send(fileNameRequest); err != nil { 117 | return nil, err 118 | } 119 | } 120 | return &MessageSendFile{ 121 | client: client, 122 | fileBox: fileBox, 123 | }, nil 124 | } 125 | 126 | // DownloadFile 把 grpc download 流包装到 io.Reader 接口 127 | type DownloadFile struct { 128 | client pbwechaty.Puppet_DownloadClient 129 | buffer bytes.Buffer 130 | done bool 131 | } 132 | 133 | // Read 把 grpc download 流包装到 io.Reader 接口 134 | func (m *DownloadFile) Read(p []byte) (n int, err error) { 135 | if m.done { 136 | return m.buffer.Read(p) 137 | } 138 | 139 | for { 140 | if m.buffer.Len() >= len(p) { 141 | break 142 | } 143 | recv, err := m.client.Recv() 144 | if err == io.EOF { 145 | m.done = true 146 | break 147 | } 148 | if err != nil { 149 | return 0, err 150 | } 151 | _, err = m.buffer.Write(recv.Chunk) 152 | if err != nil { 153 | return 0, err 154 | } 155 | } 156 | return m.buffer.Read(p) 157 | } 158 | 159 | // NewDownloadFile 把 grpc download 流包装到 io.Reader 接口 160 | func NewDownloadFile(client pbwechaty.Puppet_DownloadClient) *DownloadFile { 161 | return &DownloadFile{ 162 | client: client, 163 | buffer: bytes.Buffer{}, 164 | done: false, 165 | } 166 | } 167 | 168 | /** 169 | * for testing propose, use 20KB as the threshold 170 | * after stable we should use a value between 64KB to 256KB as the threshold 171 | */ 172 | const passThroughThresholdBytes = 20 * 1024 //nolint:unused, deadcode, varcheck // TODO 未来会被用到 173 | 174 | /** 175 | * 1. Green: 176 | * Can be serialized directly 177 | */ 178 | var greenFileBoxTypes = helper.ArrayInt{ 179 | filebox.TypeUrl, 180 | filebox.TypeUuid, 181 | filebox.TypeQRCode, 182 | } 183 | 184 | /** 185 | * 2. Yellow: 186 | * Can be serialized directly, if the size is less than a threshold 187 | * if it's bigger than the threshold, 188 | * then it should be convert to a UUID file box before send out 189 | */ 190 | var yellowFileBoxTypes = helper.ArrayInt{ 191 | filebox.TypeBase64, 192 | } 193 | 194 | func serializeFileBox(box *filebox.FileBox) (*filebox.FileBox, error) { 195 | if canPassthrough(box) { 196 | return box, nil 197 | } 198 | reader, err := box.ToReader() 199 | if err != nil { 200 | return nil, err 201 | } 202 | uuid, err := filebox.FromStream(reader).ToUuid() 203 | if err != nil { 204 | return nil, err 205 | } 206 | return filebox.FromUuid(uuid, filebox.WithName(box.Name)), nil 207 | } 208 | 209 | func canPassthrough(box *filebox.FileBox) bool { 210 | if greenFileBoxTypes.InArray(int(box.Type())) { 211 | return true 212 | } 213 | 214 | if !yellowFileBoxTypes.InArray(int(box.Type())) { 215 | return false 216 | } 217 | 218 | // checksize 219 | return true 220 | } 221 | -------------------------------------------------------------------------------- /wechaty-puppet-service/grpc.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | pbwechaty "github.com/wechaty/go-grpc/wechaty" 5 | "google.golang.org/grpc" 6 | "google.golang.org/grpc/connectivity" 7 | "google.golang.org/grpc/credentials" 8 | "google.golang.org/grpc/credentials/insecure" 9 | "time" 10 | ) 11 | 12 | func (p *PuppetService) startGrpcClient() error { 13 | var err error 14 | var creds credentials.TransportCredentials 15 | var callOptions []grpc.CallOption 16 | if p.disableTLS { 17 | // TODO 目前不支持 tls,不用打印这个提醒 18 | //log.Warn("PuppetService.startGrpcClient TLS: disabled (INSECURE)") 19 | creds = insecure.NewCredentials() 20 | } else { 21 | callOptions = append(callOptions, grpc.PerRPCCredentials(callCredToken{token: p.token})) 22 | creds, err = p.createCred() 23 | if err != nil { 24 | return err 25 | } 26 | } 27 | 28 | dialOptions := []grpc.DialOption{ 29 | grpc.WithTransportCredentials(creds), 30 | grpc.WithDefaultCallOptions(callOptions...), 31 | grpc.WithResolvers(wechatyResolver()), 32 | } 33 | 34 | if p.disableTLS { 35 | // Deprecated: this block will be removed after Dec 21, 2022. 36 | dialOptions = append(dialOptions, grpc.WithAuthority(p.token)) 37 | } 38 | 39 | conn, err := grpc.Dial(p.endpoint, dialOptions...) 40 | if err != nil { 41 | return err 42 | } 43 | p.grpcConn = conn 44 | 45 | go p.autoReconnectGrpcConn() 46 | 47 | p.grpcClient = pbwechaty.NewPuppetClient(conn) 48 | return nil 49 | } 50 | 51 | func (p *PuppetService) autoReconnectGrpcConn() { 52 | <-p.started 53 | isClose := false 54 | ticker := p.newGrpcReconnectTicket() 55 | defer ticker.Stop() 56 | for { 57 | select { 58 | case <-ticker.C: 59 | connState := p.grpcConn.GetState() 60 | // 重新连接成功 61 | if isClose && connectivity.Ready == connState { 62 | isClose = false 63 | log.Warn("PuppetService.autoReconnectGrpcConn grpc reconnection successful") 64 | if err := p.startGrpcStream(); err != nil { 65 | log.Errorf("PuppetService.autoReconnectGrpcConn startGrpcStream err:%s", err.Error()) 66 | } 67 | } 68 | 69 | if p.grpcConn.GetState() == connectivity.Idle { 70 | isClose = true 71 | p.grpcConn.Connect() 72 | log.Warn("PuppetService.autoReconnectGrpcConn grpc reconnection...") 73 | } 74 | case <-p.stop: 75 | return 76 | } 77 | } 78 | } 79 | 80 | func (p *PuppetService) newGrpcReconnectTicket() *time.Ticker { 81 | interval := 2 * time.Second 82 | if p.opts.GrpcReconnectInterval > 0 { 83 | interval = p.opts.GrpcReconnectInterval 84 | } 85 | return time.NewTicker(interval) 86 | } 87 | -------------------------------------------------------------------------------- /wechaty-puppet-service/helper.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | "google.golang.org/protobuf/types/known/timestamppb" 5 | "time" 6 | ) 7 | 8 | func grpcTimestampToGoTime(t *timestamppb.Timestamp) time.Time { 9 | // 不同的 puppet 返回的时间格式不一致, 需要做转换兼容 10 | // padlocal 返回的是秒,puppet-service 当作是毫秒单位转为秒(除以1000),所以这里 t.Seconds 就只剩下七位,另外3为被分配到 t.Nanos 去了 11 | // https://github.com/wechaty/puppet-service/blob/4de1024ee9b615af6c44674f684a84dd8c11ae9e/src/pure-functions/timestamp.ts#L7-L17 12 | 13 | // 这里我们判断 t.Seconds 是否为7位来特殊处理 14 | //TODO(dchaofei): 未来时间戳每增加一位这里就要判断加一位,那就是200多年之后的事情了,到时还有人在用 wechaty 吗?(2023-09-09) 15 | if t.Seconds/10000000 < 1 { 16 | second := t.Seconds*1000 + int64(t.Nanos)/1000000 17 | return time.Unix(second, 0) 18 | } 19 | 20 | return t.AsTime().Local() 21 | } 22 | -------------------------------------------------------------------------------- /wechaty-puppet-service/options.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | wechatypuppet "github.com/wechaty/go-wechaty/wechaty-puppet" 5 | "time" 6 | ) 7 | 8 | // TLSConfig tls config 9 | type TLSConfig struct { 10 | CaCert string 11 | ServerName string 12 | 13 | Disable bool // only for compatible with old clients/servers 14 | } 15 | 16 | // Options puppet-service options 17 | type Options struct { 18 | wechatypuppet.Option 19 | 20 | GrpcReconnectInterval time.Duration 21 | Authority string 22 | TLS TLSConfig 23 | } 24 | -------------------------------------------------------------------------------- /wechaty-puppet-service/puppet_service_test.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | // TODO 建议 mock http 4 | //func TestPuppetService_discoverServiceIP(t *testing.T) { 5 | // type fields struct { 6 | // Puppet *wechatyPuppet.Puppet 7 | // grpcConn *grpc.ClientConn 8 | // grpcClient wechaty.PuppetClient 9 | // eventStream wechaty.Puppet_EventClient 10 | // } 11 | // tests := []struct { 12 | // name string 13 | // fields fields 14 | // wantS string 15 | // wantErr bool 16 | // }{ 17 | // { 18 | // name: "0.0.0.0", 19 | // fields: fields{ 20 | // Puppet: &wechatyPuppet.Puppet{ 21 | // Option: &wechatyPuppet.Option{Token: "__TOKEN__"}, 22 | // }, 23 | // }, 24 | // wantS: "0.0.0.0", 25 | // wantErr: false, 26 | // }, 27 | // { 28 | // name: "timeout", 29 | // fields: fields{ 30 | // Puppet: &wechatyPuppet.Puppet{ 31 | // Option: &wechatyPuppet.Option{Token: "__TOKEN__", Timeout: 1 * time.Nanosecond}, 32 | // }, 33 | // }, 34 | // wantS: "", 35 | // wantErr: true, 36 | // }, 37 | // } 38 | // for _, tt := range tests { 39 | // t.Run(tt.name, func(t *testing.T) { 40 | // p := &PuppetService{ 41 | // Puppet: tt.fields.Puppet, 42 | // grpcConn: tt.fields.grpcConn, 43 | // grpcClient: tt.fields.grpcClient, 44 | // eventStream: tt.fields.eventStream, 45 | // } 46 | // gotS, err := p.discoverServiceIP() 47 | // if (err != nil) != tt.wantErr { 48 | // t.Errorf("discoverServiceIP() error = %v, wantErr %v", err, tt.wantErr) 49 | // return 50 | // } 51 | // if gotS != tt.wantS { 52 | // t.Errorf("discoverServiceIP() gotS = %v, want %v", gotS, tt.wantS) 53 | // } 54 | // }) 55 | // } 56 | //} 57 | -------------------------------------------------------------------------------- /wechaty-puppet-service/resolver.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "google.golang.org/grpc/resolver" 7 | "google.golang.org/grpc/resolver/manual" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | func wechatyResolver() resolver.Builder { 14 | r := manual.NewBuilderWithScheme("wechaty") 15 | r.BuildCallback = resolverBuildCallBack 16 | return r 17 | } 18 | 19 | func resolverBuildCallBack(target resolver.Target, conn resolver.ClientConn, options resolver.BuildOptions) { 20 | // target.URL.Host `api.chatie.io` in `wechaty://api.chatie.io/__token__` 21 | // target.URL.Path `__token__` in `wechaty://api.chatie.io/__token__` 22 | log.Trace("resolverBuildCallBack()") 23 | uri := fmt.Sprintf("https://%s/v0/hosties%s", target.URL.Host, target.URL.Path) 24 | address, err := discoverAPI(uri) 25 | if err != nil { 26 | conn.ReportError(err) 27 | return 28 | } 29 | if address == nil || address.Host == "" { 30 | conn.ReportError(fmt.Errorf(`token %s does not exist`, strings.TrimLeft(target.URL.Path, "/"))) 31 | return 32 | } 33 | err = conn.UpdateState(resolver.State{ 34 | Addresses: []resolver.Address{{ 35 | Addr: fmt.Sprintf("%s:%d", address.Host, address.Port), 36 | }}, 37 | }) 38 | if err != nil { 39 | log.Error("resolverBuildCallBack UpdateState err: ", err.Error()) 40 | return 41 | } 42 | } 43 | 44 | type serviceAddress struct { 45 | Host string 46 | Port int 47 | } 48 | 49 | func discoverAPI(uri string) (*serviceAddress, error) { 50 | response, err := http.Get(uri) 51 | if err != nil { 52 | return nil, fmt.Errorf("discoverAPI http.Get() %w", err) 53 | } 54 | defer response.Body.Close() 55 | 56 | // 4xx 57 | if response.StatusCode >= 400 && response.StatusCode < 500 { 58 | return nil, nil 59 | } 60 | 61 | // 2xx 62 | if response.StatusCode < 200 || response.StatusCode >= 300 { 63 | return nil, fmt.Errorf("discoverAPI http.Get() status:%s %w", response.Status, err) 64 | } 65 | data, err := ioutil.ReadAll(response.Body) 66 | if err != nil { 67 | return nil, fmt.Errorf("discoverAPI ioutil.ReadAll %w", err) 68 | } 69 | 70 | r := &serviceAddress{} 71 | if err := json.Unmarshal(data, r); err != nil { 72 | return nil, fmt.Errorf("discoverAPI json.Unmarshal %w", err) 73 | } 74 | return r, nil 75 | } 76 | -------------------------------------------------------------------------------- /wechaty-puppet-service/service_endpoint.go: -------------------------------------------------------------------------------- 1 | package puppetservice 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | // ErrNotToken token not found error 10 | // Deprecated 11 | ErrNotToken = errors.New("wechaty-puppet-service: token not found. See: ") 12 | ) 13 | 14 | // ServiceEndPoint api.chatie.io endpoint api response 15 | // Deprecated 16 | type ServiceEndPoint struct { 17 | IP string `json:"ip"` 18 | Port int `json:"port,omitempty"` 19 | } 20 | 21 | // IsValid EndPoint is valid 22 | func (p *ServiceEndPoint) IsValid() bool { 23 | return len(p.IP) > 0 && p.IP != "0.0.0.0" 24 | } 25 | 26 | // Target Export IP+Port 27 | func (p *ServiceEndPoint) Target() string { 28 | port := p.Port 29 | if p.Port == 0 { 30 | port = 8788 31 | } 32 | return fmt.Sprintf("%s:%d", p.IP, port) 33 | } 34 | -------------------------------------------------------------------------------- /wechaty-puppet/file-box/file_box.go: -------------------------------------------------------------------------------- 1 | // file_box 2 | // Deprecated: use filebox package 3 | 4 | package file_box 5 | 6 | import ( 7 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 8 | ) 9 | 10 | // FileBox ... 11 | // Deprecated: use filebox.FileBox 12 | type FileBox = filebox.FileBox 13 | 14 | var ( 15 | // FromJSON ... 16 | // Deprecated: use filebox.FileBox 17 | FromJSON = filebox.FromJSON 18 | 19 | // FromBase64 ... 20 | // Deprecated: use filebox.FromBase64 21 | FromBase64 = filebox.FromBase64 22 | 23 | // FromUrl ... 24 | // Deprecated: use filebox.FromUrl 25 | FromUrl = filebox.FromUrl 26 | 27 | // FromFile ... 28 | // Deprecated: use filebox.FromFile 29 | FromFile = filebox.FromFile 30 | 31 | // FromQRCode ... 32 | // Deprecated: use filebox.FromQRCode 33 | FromQRCode = filebox.FromQRCode 34 | 35 | // FromStream ... 36 | // Deprecated: use filebox.FromStream 37 | FromStream = filebox.FromStream 38 | ) 39 | -------------------------------------------------------------------------------- /wechaty-puppet/file-box/fileboxtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=FileBoxType"; DO NOT EDIT. 2 | 3 | package file_box 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[FileBoxTypeUnknown-0] 12 | } 13 | 14 | const _FileBoxType_name = "FileBoxTypeUnknown" 15 | 16 | var _FileBoxType_index = [...]uint8{0, 18} 17 | 18 | func (i FileBoxType) String() string { 19 | if i >= FileBoxType(len(_FileBoxType_index)-1) { 20 | return "FileBoxType(" + strconv.FormatInt(int64(i), 10) + ")" 21 | } 22 | return _FileBoxType_name[_FileBoxType_index[i]:_FileBoxType_index[i+1]] 23 | } 24 | -------------------------------------------------------------------------------- /wechaty-puppet/file-box/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | test.text 2 | -------------------------------------------------------------------------------- /wechaty-puppet/file-box/type.go: -------------------------------------------------------------------------------- 1 | package file_box 2 | 3 | import "net/http" 4 | 5 | // FileBoxOptionsCommon ... 6 | // Deprecated: use filebox package 7 | type FileBoxOptionsCommon struct { 8 | Name string `json:"Name"` 9 | Metadata map[string]interface{} `json:"metadata"` 10 | BoxType FileBoxType `json:"boxType"` 11 | } 12 | 13 | // FileBoxOptionsBase64 ... 14 | // Deprecated: use filebox package 15 | type FileBoxOptionsBase64 struct { 16 | Base64 string `json:"base64"` 17 | } 18 | 19 | // FileBoxOptionsUrl ... 20 | // Deprecated: use filebox package 21 | type FileBoxOptionsUrl struct { 22 | RemoteUrl string `json:"remoteUrl"` 23 | Headers http.Header `json:"headers"` 24 | } 25 | 26 | // FileBoxOptionsQRCode ... 27 | // Deprecated: use filebox package 28 | type FileBoxOptionsQRCode struct { 29 | QrCode string `json:"qrCode"` 30 | } 31 | 32 | // FileBoxOptions ... 33 | // Deprecated: use filebox package 34 | type FileBoxOptions struct { 35 | FileBoxOptionsCommon 36 | FileBoxOptionsBase64 37 | FileBoxOptionsQRCode 38 | FileBoxOptionsUrl 39 | } 40 | 41 | //go:generate stringer -type=FileBoxType 42 | // FileBoxType ... 43 | // Deprecated: use filebox package 44 | type FileBoxType uint8 45 | 46 | const ( 47 | FileBoxTypeUnknown FileBoxType = 0 48 | 49 | // Deprecated: use filebox package 50 | FileBoxTypeBase64 = 1 51 | // Deprecated: use filebox package 52 | FileBoxTypeUrl = 2 53 | // Deprecated: use filebox package 54 | FileBoxTypeQRCode = 3 55 | // Deprecated: use filebox package 56 | FileBoxTypeBuffer = 4 57 | // Deprecated: use filebox package 58 | FileBoxTypeFile = 5 59 | // Deprecated: use filebox package 60 | FileBoxTypeStream = 6 61 | ) 62 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "github.com/tuotoo/qrcode" 10 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 11 | logger "github.com/wechaty/go-wechaty/wechaty-puppet/log" 12 | "io" 13 | "io/ioutil" 14 | "mime" 15 | "net/url" 16 | "os" 17 | path2 "path" 18 | "path/filepath" 19 | "strings" 20 | ) 21 | 22 | var ( 23 | // ErrToJSON err to json 24 | ErrToJSON = errors.New("FileBox.toJSON() only support TypeUrl,TypeQRCode,TypeBase64, TypeUuid") 25 | // ErrNoBase64Data no base64 data 26 | ErrNoBase64Data = errors.New("no Base64 data") 27 | // ErrNoUrl no url 28 | ErrNoUrl = errors.New("no url") 29 | // ErrNoPath no path 30 | ErrNoPath = errors.New("no path") 31 | // ErrNoQRCode no QR Code 32 | ErrNoQRCode = errors.New("no QR Code") 33 | // ErrNoUuid no uuid 34 | ErrNoUuid = errors.New("no uuid") 35 | ) 36 | 37 | var log = logger.L.WithField("module", "filebox") 38 | 39 | type fileImplInterface interface { 40 | toJSONMap() (map[string]interface{}, error) 41 | toReader() (io.Reader, error) 42 | } 43 | 44 | // FileBox struct 45 | type FileBox struct { 46 | fileImpl fileImplInterface 47 | Name string 48 | metadata map[string]interface{} 49 | boxType Type 50 | mediaType string 51 | size int64 52 | md5 string 53 | 54 | err error 55 | } 56 | 57 | func newFileBox(boxType Type, fileImpl fileImplInterface, options Options) *FileBox { 58 | fb := &FileBox{ 59 | fileImpl: fileImpl, 60 | Name: options.Name, 61 | metadata: options.Metadata, 62 | boxType: boxType, 63 | size: options.Size, 64 | md5: options.Md5, 65 | } 66 | if fb.metadata == nil { 67 | fb.metadata = make(map[string]interface{}) 68 | } 69 | fb.correctName() 70 | fb.guessMediaType() 71 | return fb 72 | } 73 | 74 | func (fb *FileBox) correctName() { 75 | if strings.HasSuffix(fb.Name, ".silk") || strings.HasSuffix(fb.Name, ".slk") { 76 | log.Warn("detect that you want to send voice file which should be .sil pattern. So we help you rename it.") 77 | if strings.HasSuffix(fb.Name, ".silk") { 78 | fb.Name = strings.ReplaceAll(fb.Name, ".silk", ".sil") 79 | } 80 | if strings.HasSuffix(fb.Name, ".slk") { 81 | fb.Name = strings.ReplaceAll(fb.Name, ".slk", ".sil") 82 | } 83 | } 84 | } 85 | 86 | func (fb *FileBox) guessMediaType() { 87 | if strings.HasSuffix(fb.Name, ".sil") { 88 | fb.mediaType = "audio/silk" 89 | if _, ok := fb.metadata["voiceLength"]; !ok { 90 | log.Warn("detect that you want to send voice file, but no voiceLength setting, " + 91 | `so use the default setting: 1000,` + 92 | `you should set it manually: filebox.WithMetadata(map[string]interface{}{"voiceLength": 2000})`) 93 | fb.metadata["voiceLength"] = 1000 94 | } 95 | } else { 96 | fb.mediaType = mime.TypeByExtension(filepath.Ext(fb.Name)) 97 | } 98 | } 99 | 100 | // FromJSON create FileBox from JSON 101 | func FromJSON(s string) *FileBox { 102 | options := new(Options) 103 | if err := json.Unmarshal([]byte(s), options); err != nil { 104 | err = fmt.Errorf("FromJSON json.Unmarshal: %w", err) 105 | return newFileBox(TypeUnknown, &fileBoxUnknown{}, newOptions()).setErr(err) 106 | } 107 | 108 | // 对未来要弃用的 json.BoxTypeDeprecated 做兼容处理 109 | if options.BoxTypeDeprecated != 0 && options.BoxType == 0 { 110 | options.BoxType = options.BoxTypeDeprecated 111 | } 112 | 113 | switch options.BoxType { 114 | case TypeBase64: 115 | return FromBase64(options.Base64, WithOptions(*options)) 116 | case TypeQRCode: 117 | return FromQRCode(options.QrCode, WithOptions(*options)) 118 | case TypeUrl: 119 | return FromUrl(options.RemoteUrl, WithOptions(*options)) 120 | case TypeUuid: 121 | return FromUuid(options.Uuid, WithOptions(*options)) 122 | default: 123 | err := fmt.Errorf("FromJSON invalid value boxType: %v", options.BoxType) 124 | return newFileBox(TypeUnknown, &fileBoxUnknown{}, newOptions()).setErr(err) 125 | } 126 | } 127 | 128 | // FromBase64 create FileBox from Base64 129 | func FromBase64(encode string, options ...Option) *FileBox { 130 | var err error 131 | if encode == "" { 132 | err = fmt.Errorf("FromBase64 %w", ErrNoBase64Data) 133 | } 134 | 135 | o := newOptions(options...) 136 | if o.Name == "" { 137 | o.Name = "base64.dat" 138 | } 139 | o.Size = helper.Base64OrigLength(encode) 140 | return newFileBox(TypeBase64, 141 | newFileBoxBase64(encode), o).setErr(err) 142 | } 143 | 144 | // FromUrl create FileBox from url 145 | func FromUrl(urlString string, options ...Option) *FileBox { 146 | var err error 147 | if urlString == "" { 148 | err = fmt.Errorf("FromUrl %w", ErrNoUrl) 149 | } 150 | 151 | o := newOptions(options...) 152 | if o.Name == "" && err == nil { 153 | if u, e := url.Parse(urlString); e != nil { 154 | err = e 155 | } else { 156 | o.Name = strings.TrimLeft(u.Path, "/") 157 | } 158 | } 159 | return newFileBox(TypeUrl, 160 | newFileBoxUrl(urlString, o.Headers), o).setErr(err) 161 | } 162 | 163 | // FromFile create FileBox from file 164 | func FromFile(path string, options ...Option) *FileBox { 165 | var err error 166 | if path == "" { 167 | err = fmt.Errorf("FromFile %w", ErrNoPath) 168 | } 169 | 170 | o := newOptions(options...) 171 | if o.Name == "" { 172 | o.Name = path2.Base(path) 173 | } 174 | 175 | if err == nil { 176 | if file, e := os.Stat(path); e != nil { 177 | err = e 178 | } else { 179 | o.Size = file.Size() 180 | } 181 | } 182 | 183 | return newFileBox(TypeFile, 184 | newFileBoxFile(path), o).setErr(err) 185 | } 186 | 187 | // FromQRCode create FileBox from QRCode 188 | func FromQRCode(qrCode string, options ...Option) *FileBox { 189 | var err error 190 | if qrCode == "" { 191 | err = fmt.Errorf("FromQRCode %w", ErrNoQRCode) 192 | } 193 | 194 | return newFileBox(TypeQRCode, 195 | newFileBoxQRCode(qrCode), 196 | newOptions(append(options, WithName("qrcode.png"))...)).setErr(err) 197 | } 198 | 199 | // FromStream from io.Reader 200 | func FromStream(reader io.Reader, options ...Option) *FileBox { 201 | o := newOptions(options...) 202 | if o.Name == "" { 203 | o.Name = "stream.dat" 204 | } 205 | return newFileBox(TypeStream, 206 | newFileBoxStream(reader), o) 207 | } 208 | 209 | func FromUuid(uuid string, options ...Option) *FileBox { 210 | var err error 211 | if uuid == "" { 212 | err = fmt.Errorf("FromUuid %w", ErrNoUuid) 213 | } 214 | 215 | o := newOptions(options...) 216 | if o.Name == "" { 217 | o.Name = uuid + ".dat" 218 | } 219 | return newFileBox(TypeUuid, newFileBoxUuid(uuid), o).setErr(err) 220 | } 221 | 222 | // ToJSON to json string 223 | func (fb *FileBox) ToJSON() (string, error) { 224 | if fb.err != nil { 225 | return "", fb.err 226 | } 227 | 228 | jsonMap := map[string]interface{}{ 229 | "name": fb.Name, 230 | "metadata": fb.metadata, 231 | "type": fb.boxType, 232 | "boxType": fb.boxType, //Deprecated 233 | "size": fb.size, 234 | "md5": fb.md5, 235 | "mediaType": fb.mediaType, 236 | } 237 | 238 | switch fb.boxType { 239 | case TypeUrl, TypeQRCode, TypeBase64, TypeUuid: 240 | break 241 | default: 242 | return "", ErrToJSON 243 | } 244 | implJsonMap, err := fb.fileImpl.toJSONMap() 245 | if err != nil { 246 | return "", err 247 | } 248 | for k, v := range implJsonMap { 249 | jsonMap[k] = v 250 | } 251 | marshal, err := json.Marshal(jsonMap) 252 | return string(marshal), err 253 | } 254 | 255 | // ToFile save to file 256 | func (fb *FileBox) ToFile(filePath string, overwrite bool) error { 257 | if fb.err != nil { 258 | return fb.err 259 | } 260 | 261 | if filePath == "" { 262 | filePath = fb.Name 263 | } 264 | path, err := os.Getwd() 265 | if err != nil { 266 | return err 267 | } 268 | fullPath := filepath.Join(path, filePath) 269 | if !overwrite && helper.FileExists(fullPath) { 270 | return os.ErrExist 271 | } 272 | 273 | reader, err := fb.ToReader() 274 | if err != nil { 275 | return err 276 | } 277 | 278 | writer := bufio.NewReader(reader) 279 | file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, os.ModePerm) 280 | if err != nil { 281 | return err 282 | } 283 | if _, err := writer.WriteTo(file); err != nil { 284 | return err 285 | } 286 | return nil 287 | } 288 | 289 | // ToBytes to bytes 290 | func (fb *FileBox) ToBytes() ([]byte, error) { 291 | if fb.err != nil { 292 | return nil, fb.err 293 | } 294 | 295 | reader, err := fb.ToReader() 296 | if err != nil { 297 | return nil, err 298 | } 299 | return ioutil.ReadAll(reader) 300 | } 301 | 302 | // ToBase64 to base64 string 303 | func (fb *FileBox) ToBase64() (string, error) { 304 | if fb.err != nil { 305 | return "", fb.err 306 | } 307 | 308 | if fb.boxType == TypeBase64 { 309 | return fb.fileImpl.(*fileBoxBase64).base64Data, nil 310 | } 311 | 312 | fileBytes, err := fb.ToBytes() 313 | if err != nil { 314 | return "", err 315 | } 316 | return base64.StdEncoding.EncodeToString(fileBytes), nil 317 | } 318 | 319 | // ToDataURL to dataURL 320 | func (fb *FileBox) ToDataURL() (string, error) { 321 | if fb.err != nil { 322 | return "", fb.err 323 | } 324 | 325 | toBase64, err := fb.ToBase64() 326 | if err != nil { 327 | return "", nil 328 | } 329 | return fmt.Sprintf("data:%s;base64,%s", fb.mediaType, toBase64), nil 330 | } 331 | 332 | // ToQRCode to QRCode 333 | func (fb *FileBox) ToQRCode() (string, error) { 334 | if fb.err != nil { 335 | return "", fb.err 336 | } 337 | 338 | reader, err := fb.ToReader() 339 | if err != nil { 340 | return "", err 341 | } 342 | decode, err := qrcode.Decode(reader) 343 | if err != nil { 344 | return "", nil 345 | } 346 | return decode.Content, nil 347 | } 348 | 349 | // ToUuid to uuid 350 | func (fb *FileBox) ToUuid() (string, error) { 351 | if fb.err != nil { 352 | return "", fb.err 353 | } 354 | 355 | if fb.boxType == TypeUuid { 356 | return fb.fileImpl.(*fileBoxUuid).uuid, nil 357 | } 358 | 359 | reader, err := fb.ToReader() 360 | if err != nil { 361 | return "", err 362 | } 363 | 364 | if uuidFromStream == nil { 365 | return "", errors.New("need to use filebox.SetUuidSaver() before dealing with UUID") 366 | } 367 | 368 | return uuidFromStream(reader) 369 | } 370 | 371 | // String ... 372 | func (fb *FileBox) String() string { 373 | return fmt.Sprintf("FileBox#%s<%s>", fb.boxType, fb.Name) 374 | } 375 | 376 | // ToReader to io.Reader 377 | func (fb *FileBox) ToReader() (io.Reader, error) { 378 | if fb.err != nil { 379 | return nil, fb.err 380 | } 381 | 382 | return fb.fileImpl.toReader() 383 | } 384 | 385 | // Type get type 386 | func (fb *FileBox) Type() Type { 387 | return fb.boxType 388 | } 389 | 390 | // MetaData get metadata 391 | func (fb *FileBox) MetaData() map[string]interface{} { 392 | // TODO deep copy? 393 | return fb.metadata 394 | } 395 | 396 | // Error ret err 397 | func (fb *FileBox) Error() error { 398 | return fb.err 399 | } 400 | 401 | func (fb *FileBox) setErr(err error) *FileBox { 402 | fb.err = err 403 | return fb 404 | } 405 | 406 | func (fb *FileBox) Size() { 407 | 408 | } 409 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_base64.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | var _ fileImplInterface = &fileBoxBase64{} 11 | 12 | type fileBoxBase64 struct { 13 | base64Data string 14 | } 15 | 16 | func newFileBoxBase64(base64Data string) *fileBoxBase64 { 17 | return &fileBoxBase64{base64Data: base64Data} 18 | } 19 | 20 | func (fb *fileBoxBase64) toJSONMap() (map[string]interface{}, error) { 21 | if fb.base64Data == "" { 22 | return nil, fmt.Errorf("fileBoxBase64.toJSONMap %w", ErrNoBase64Data) 23 | } 24 | 25 | return map[string]interface{}{ 26 | "base64": fb.base64Data, 27 | }, nil 28 | } 29 | 30 | func (fb *fileBoxBase64) toBytes() ([]byte, error) { //nolint:unused 31 | dec, err := base64.StdEncoding.DecodeString(fb.base64Data) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return dec, nil 36 | } 37 | 38 | func (fb *fileBoxBase64) toReader() (io.Reader, error) { 39 | reader := base64.NewDecoder(base64.StdEncoding, strings.NewReader(fb.base64Data)) 40 | return reader, nil 41 | } 42 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_file.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "encoding/base64" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | var _ fileImplInterface = &fileBoxFile{} 11 | 12 | type fileBoxFile struct { 13 | path string 14 | } 15 | 16 | func newFileBoxFile(path string) *fileBoxFile { 17 | return &fileBoxFile{path: path} 18 | } 19 | 20 | func (fb *fileBoxFile) toJSONMap() (map[string]interface{}, error) { 21 | return nil, nil 22 | } 23 | 24 | func (fb *fileBoxFile) toBytes() ([]byte, error) { //nolint:unused 25 | file, err := ioutil.ReadFile(fb.path) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return file, nil 30 | } 31 | 32 | func (fb *fileBoxFile) toBase64() (string, error) { //nolint:unused 33 | file, err := fb.toBytes() 34 | if err != nil { 35 | return "", err 36 | } 37 | return base64.StdEncoding.EncodeToString(file), nil 38 | } 39 | 40 | func (fb *fileBoxFile) toReader() (io.Reader, error) { 41 | return os.Open(fb.path) 42 | } 43 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_qrcode.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/skip2/go-qrcode" 7 | "io" 8 | ) 9 | 10 | var _ fileImplInterface = &fileBoxQRCode{} 11 | 12 | type fileBoxQRCode struct { 13 | qrCode string 14 | } 15 | 16 | func newFileBoxQRCode(qrCode string) *fileBoxQRCode { 17 | return &fileBoxQRCode{qrCode: qrCode} 18 | } 19 | 20 | func (fb *fileBoxQRCode) toJSONMap() (map[string]interface{}, error) { 21 | if fb.qrCode == "" { 22 | return nil, fmt.Errorf("fileBoxQRCode.toJSONMap %w", ErrNoQRCode) 23 | } 24 | 25 | return map[string]interface{}{ 26 | "qrCode": fb.qrCode, 27 | }, nil 28 | } 29 | 30 | func (fb *fileBoxQRCode) toBytes() ([]byte, error) { 31 | qr, err := qrcode.New(fb.qrCode, qrcode.Medium) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return qr.PNG(256) 36 | } 37 | 38 | func (fb *fileBoxQRCode) toReader() (io.Reader, error) { 39 | byteData, err := fb.toBytes() 40 | if err != nil { 41 | return nil, err 42 | } 43 | return bytes.NewReader(byteData), nil 44 | } 45 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_stream.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | var _ fileImplInterface = &fileBoxStream{} 8 | 9 | type fileBoxStream struct { 10 | Reader io.Reader 11 | } 12 | 13 | func newFileBoxStream(reader io.Reader) *fileBoxStream { 14 | return &fileBoxStream{Reader: reader} 15 | } 16 | 17 | func (fb *fileBoxStream) toJSONMap() (map[string]interface{}, error) { 18 | return nil, nil 19 | } 20 | 21 | func (fb *fileBoxStream) toBytes() ([]byte, error) { // nolint:unused 22 | panic("im") 23 | } 24 | 25 | func (fb *fileBoxStream) toReader() (io.Reader, error) { 26 | return fb.Reader, nil 27 | } 28 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_test.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestFromJSON(t *testing.T) { 10 | t.Run("FromJSON json.Unmarshal err", func(t *testing.T) { 11 | if err := FromJSON("abcd").Error(); !strings.Contains(err.Error(), "FromJSON json.Unmarshal") { 12 | t.Error(err) 13 | } 14 | }) 15 | t.Run("FromJSON invalid value boxType", func(t *testing.T) { 16 | if err := FromJSON("{}").Error(); !strings.Contains(err.Error(), "FromJSON invalid value boxType") { 17 | t.Error(err) 18 | } 19 | }) 20 | t.Run("FromJSON success", func(t *testing.T) { 21 | jsonText := `{"base64":"RmlsZUJveEJhc2U2NAo=","boxType":1,"md5":"","metadata":null,"name":"test.txt","size":14,"type":1}` 22 | if err := FromJSON(jsonText).Error(); err != nil { 23 | t.Error(err) 24 | } 25 | }) 26 | } 27 | 28 | func TestFromBase64(t *testing.T) { 29 | t.Run("FromBase64 no base64 data", func(t *testing.T) { 30 | if err := FromBase64("").Error(); !errors.Is(err, ErrNoBase64Data) { 31 | t.Error(err) 32 | } 33 | }) 34 | t.Run("FromBase64 success", func(t *testing.T) { 35 | fileBox := FromBase64("RmlsZUJveEJhc2U2NAo=") 36 | if err := fileBox.Error(); err != nil { 37 | t.Error(err) 38 | } 39 | }) 40 | } 41 | 42 | func TestFromUrl(t *testing.T) { 43 | t.Run("FromUrl no url", func(t *testing.T) { 44 | if err := FromUrl("").Error(); !errors.Is(err, ErrNoUrl) { 45 | t.Error(err) 46 | } 47 | }) 48 | t.Run("FromUrl success", func(t *testing.T) { 49 | fileBox := FromUrl("https://github.com//dchaofei.jpg?t=123") 50 | if err := fileBox.Error(); err != nil { 51 | t.Error(err) 52 | } 53 | want := "dchaofei.jpg" 54 | if fileBox.Name != want { 55 | t.Errorf("got %s want %s", fileBox.Name, want) 56 | } 57 | }) 58 | } 59 | 60 | func TestFromFile(t *testing.T) { 61 | t.Run("FromFile no path", func(t *testing.T) { 62 | if err := FromFile("").Error(); !errors.Is(err, ErrNoPath) { 63 | t.Error(err) 64 | } 65 | }) 66 | t.Run("FromFile success", func(t *testing.T) { 67 | fileBox := FromFile("testdata/dchaofei.txt") 68 | if err := fileBox.Error(); err != nil { 69 | t.Error(err) 70 | } 71 | 72 | wantName := "dchaofei.txt" 73 | if wantName != fileBox.Name { 74 | t.Errorf("got %s want %s", fileBox.Name, wantName) 75 | } 76 | }) 77 | } 78 | 79 | func TestFromQRCode(t *testing.T) { 80 | t.Run("FromQRCode no QR code", func(t *testing.T) { 81 | if err := FromQRCode("").Error(); !errors.Is(err, ErrNoQRCode) { 82 | t.Error(err) 83 | } 84 | }) 85 | t.Run("FromQRCode success", func(t *testing.T) { 86 | fileBox := FromQRCode("hello") 87 | if err := fileBox.Error(); err != nil { 88 | t.Error(err) 89 | } 90 | }) 91 | } 92 | 93 | func TestFromUuid(t *testing.T) { 94 | t.Run("FromUuid no uuid", func(t *testing.T) { 95 | if err := FromUuid("").Error(); !errors.Is(err, ErrNoUuid) { 96 | t.Error(err) 97 | } 98 | }) 99 | t.Run("FromUuid success", func(t *testing.T) { 100 | fileBox := FromUuid("xxx-xxx-xxx") 101 | if err := fileBox.Error(); err != nil { 102 | t.Error(err) 103 | } 104 | }) 105 | } 106 | 107 | func TestFileBox_ToJSON(t *testing.T) { 108 | t.Run("ToJSON success", func(t *testing.T) { 109 | const base64Encode = "RmlsZUJveEJhc2U2NAo=" 110 | const base64Filename = "test.txt" 111 | const want = `{"base64":"RmlsZUJveEJhc2U2NAo=","boxType":1,"md5":"","mediaType":"text/plain; charset=utf-8","metadata":{},"name":"test.txt","size":14,"type":1}` 112 | jsonString, err := FromBase64(base64Encode, WithName(base64Filename)).ToJSON() 113 | if err != nil { 114 | t.Error(err) 115 | } 116 | if jsonString != want { 117 | t.Errorf("got【%s】, want [%s]", jsonString, want) 118 | } 119 | 120 | newBase64, err := FromJSON(want).ToBase64() 121 | if err != nil { 122 | t.Error(err) 123 | } 124 | if newBase64 != base64Encode { 125 | t.Errorf("got【%s】, want [%s]", newBase64, base64Encode) 126 | } 127 | }) 128 | 129 | t.Run("ToJSON for not supported type", func(t *testing.T) { 130 | if _, err := FromFile("testdata/dchaofei.txt").ToJSON(); err != ErrToJSON { 131 | t.Error(err) 132 | } 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_unknown.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import "io" 4 | 5 | var _ fileImplInterface = &fileBoxUnknown{} 6 | 7 | type fileBoxUnknown struct { 8 | } 9 | 10 | func (f fileBoxUnknown) toJSONMap() (map[string]interface{}, error) { 11 | //TODO implement me 12 | panic("implement me") 13 | } 14 | 15 | func (f fileBoxUnknown) toReader() (io.Reader, error) { 16 | //TODO implement me 17 | panic("implement me") 18 | } 19 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_url.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | ) 11 | 12 | type fileBoxUrl struct { 13 | remoteUrl string 14 | headers http.Header 15 | } 16 | 17 | func newFileBoxUrl(remoteUrl string, headers http.Header) *fileBoxUrl { 18 | return &fileBoxUrl{remoteUrl: remoteUrl, headers: headers} 19 | } 20 | 21 | func (fb *fileBoxUrl) toJSONMap() (map[string]interface{}, error) { 22 | if fb.remoteUrl == "" { 23 | return nil, fmt.Errorf("fileBoxUrl.toJSONMap %w", ErrNoUrl) 24 | } 25 | return map[string]interface{}{ 26 | "headers": fb.headers, 27 | "url": fb.remoteUrl, 28 | }, nil 29 | } 30 | 31 | func (fb *fileBoxUrl) toBytes() ([]byte, error) { //nolint:unused 32 | request, err := http.NewRequest(http.MethodGet, fb.remoteUrl, nil) 33 | if err != nil { 34 | return nil, err 35 | } 36 | request.Header = fb.headers 37 | response, err := helper.HttpClient.Do(request) 38 | if err != nil { 39 | return nil, err 40 | } 41 | defer response.Body.Close() 42 | return ioutil.ReadAll(response.Body) 43 | } 44 | 45 | func (fb *fileBoxUrl) toReader() (io.Reader, error) { 46 | request, err := http.NewRequest(http.MethodGet, fb.remoteUrl, nil) 47 | if err != nil { 48 | return nil, err 49 | } 50 | request.Header = fb.headers 51 | response, err := helper.HttpClient.Do(request) 52 | if err != nil { 53 | return nil, err 54 | } 55 | defer response.Body.Close() 56 | 57 | all, err := ioutil.ReadAll(response.Body) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return bytes.NewReader(all), nil 62 | } 63 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_uuid.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | var _ fileImplInterface = fileBoxUuid{} 10 | 11 | // UuidLoader uuid loader 12 | type UuidLoader func(uuid string) (io.Reader, error) 13 | 14 | // UuidSaver uuid saver 15 | type UuidSaver func(reader io.Reader) (uuid string, err error) 16 | 17 | var uuidToStream UuidLoader 18 | var uuidFromStream UuidSaver 19 | 20 | // SetUuidLoader set uuid loader 21 | func SetUuidLoader(loader UuidLoader) { 22 | uuidToStream = loader 23 | } 24 | 25 | // SetUuidSaver set uuid saver 26 | func SetUuidSaver(saver UuidSaver) { 27 | uuidFromStream = saver 28 | } 29 | 30 | type fileBoxUuid struct { 31 | uuid string 32 | } 33 | 34 | func (f fileBoxUuid) toJSONMap() (map[string]interface{}, error) { 35 | if f.uuid == "" { 36 | return nil, fmt.Errorf("fileBoxUuid.toJSONMap %w", ErrNoUuid) 37 | } 38 | return map[string]interface{}{ 39 | "uuid": f.uuid, 40 | }, nil 41 | } 42 | 43 | func (f fileBoxUuid) toReader() (io.Reader, error) { 44 | if uuidToStream == nil { 45 | return nil, errors.New("need to call filebox.setUuidLoader() to set UUID loader first") 46 | } 47 | return uuidToStream(f.uuid) 48 | } 49 | 50 | func newFileBoxUuid(uuid string) *fileBoxUuid { 51 | return &fileBoxUuid{uuid: uuid} 52 | } 53 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/file_box_uuid_test.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestSetUuidLoader(t *testing.T) { 10 | const data = "hello" 11 | const uuid = "xxxx-xxxx" 12 | loader := func(uuid string) (io.Reader, error) { 13 | return strings.NewReader(data), nil 14 | } 15 | 16 | SetUuidLoader(loader) 17 | 18 | bytes, err := FromUuid(uuid).ToBytes() 19 | if err != nil { 20 | t.Log(err) 21 | } 22 | if string(bytes) != data { 23 | t.Errorf("got %s want %s", string(bytes), data) 24 | } 25 | } 26 | 27 | func TestSetUuidSaver(t *testing.T) { 28 | const data = "https://github.com/dchaofei" 29 | const uuid = "xxxx-xxxx" 30 | saver := func(reader io.Reader) (string, error) { 31 | return uuid, nil 32 | } 33 | SetUuidSaver(saver) 34 | 35 | bytes, err := FromFile("testdata/dchaofei.txt").ToUuid() 36 | if err != nil { 37 | t.Log(err) 38 | } 39 | if string(bytes) != uuid { 40 | t.Errorf("got %s want %s", string(bytes), data) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/testdata/dchaofei.txt: -------------------------------------------------------------------------------- 1 | https://github.com/dchaofei 2 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/type.go: -------------------------------------------------------------------------------- 1 | package filebox 2 | 3 | import "net/http" 4 | 5 | type Option func(options *Options) 6 | 7 | // WithName set name 8 | func WithName(name string) Option { 9 | return func(options *Options) { 10 | options.Name = name 11 | } 12 | } 13 | 14 | func WithOptions(o Options) Option { 15 | return func(options *Options) { 16 | *options = o 17 | } 18 | } 19 | 20 | // WithMetadata set metadata 21 | func WithMetadata(metadata map[string]interface{}) Option { 22 | return func(options *Options) { 23 | options.Metadata = metadata 24 | } 25 | } 26 | 27 | // WithMd5 set md5 28 | func WithMd5(md5 string) Option { 29 | return func(options *Options) { 30 | options.Md5 = md5 31 | } 32 | } 33 | 34 | // WithSize set size 35 | func WithSize(size int64) Option { 36 | return func(options *Options) { 37 | options.Size = size 38 | } 39 | } 40 | 41 | // OptionsCommon common options 42 | type OptionsCommon struct { 43 | Name string `json:"name"` 44 | Metadata map[string]interface{} `json:"metadata"` 45 | BoxType Type `json:"type"` 46 | // Deprecated 47 | BoxTypeDeprecated Type `json:"boxType"` 48 | Size int64 `json:"size"` 49 | Md5 string `json:"md5"` 50 | } 51 | 52 | // OptionsBase64 base64 53 | type OptionsBase64 struct { 54 | Base64 string `json:"base64"` 55 | } 56 | 57 | // OptionsUrl url 58 | type OptionsUrl struct { 59 | RemoteUrl string `json:"url"` 60 | Headers http.Header `json:"headers"` 61 | } 62 | 63 | // OptionsQRCode QRCode 64 | type OptionsQRCode struct { 65 | QrCode string `json:"qrCode"` 66 | } 67 | 68 | // OptionsUuid uuid 69 | type OptionsUuid struct { 70 | Uuid string `json:"uuid"` 71 | } 72 | 73 | // Options ... 74 | type Options struct { 75 | OptionsCommon 76 | OptionsBase64 77 | OptionsQRCode 78 | OptionsUrl 79 | OptionsUuid 80 | } 81 | 82 | func newOptions(options ...Option) Options { 83 | option := Options{} 84 | for _, f := range options { 85 | f(&option) 86 | } 87 | return option 88 | } 89 | 90 | //go:generate stringer -type=Type 91 | type Type uint8 92 | 93 | const ( 94 | TypeUnknown Type = 0 95 | 96 | TypeBase64 = 1 97 | TypeUrl = 2 98 | TypeQRCode = 3 99 | TypeBuffer = 4 100 | TypeFile = 5 101 | TypeStream = 6 102 | TypeUuid = 7 103 | ) 104 | -------------------------------------------------------------------------------- /wechaty-puppet/filebox/type_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=Type"; DO NOT EDIT. 2 | 3 | package filebox 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[TypeUnknown-0] 12 | } 13 | 14 | const _Type_name = "TypeUnknown" 15 | 16 | var _Type_index = [...]uint8{0, 11} 17 | 18 | func (i Type) String() string { 19 | if i >= Type(len(_Type_index)-1) { 20 | return "Type(" + strconv.FormatInt(int64(i), 10) + ")" 21 | } 22 | return _Type_name[_Type_index[i]:_Type_index[i+1]] 23 | } 24 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/array.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | type ArrayInt []int 4 | 5 | func (a ArrayInt) InArray(i int) bool { 6 | for _, v := range a { 7 | if v == i { 8 | return true 9 | } 10 | } 11 | return false 12 | } 13 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/async.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "math" 5 | "runtime" 6 | "sync" 7 | ) 8 | 9 | // DefaultWorkerNum default number of goroutines is twice the number of GOMAXPROCS 10 | var DefaultWorkerNum = runtime.GOMAXPROCS(0) * 2 11 | 12 | type ( 13 | // IAsync interface 14 | IAsync interface { 15 | AddTask(task Task) 16 | Result() []AsyncResult 17 | } 18 | 19 | // AsyncResult result struct 20 | AsyncResult struct { 21 | Value interface{} 22 | Err error 23 | } 24 | 25 | async struct { 26 | tasks []Task 27 | wg sync.WaitGroup 28 | maxWorkerNum int 29 | } 30 | 31 | // Task task func 32 | Task func() (interface{}, error) 33 | ) 34 | 35 | // NewAsync ... 36 | func NewAsync(maxWorkerNum int) IAsync { 37 | if maxWorkerNum <= 0 { 38 | maxWorkerNum = DefaultWorkerNum 39 | } 40 | return &async{ 41 | maxWorkerNum: maxWorkerNum, 42 | wg: sync.WaitGroup{}, 43 | } 44 | } 45 | 46 | func (a *async) AddTask(task Task) { 47 | a.tasks = append(a.tasks, task) 48 | } 49 | 50 | func (a *async) Result() []AsyncResult { 51 | taskChan := make(chan Task) 52 | resultChan := make(chan AsyncResult) 53 | 54 | taskNum := len(a.tasks) 55 | workerNum := int(math.Min(float64(taskNum), float64(a.maxWorkerNum))) 56 | a.wg.Add(taskNum) 57 | 58 | for i := 0; i < workerNum; i++ { 59 | go func() { 60 | for task := range taskChan { 61 | result := AsyncResult{} 62 | result.Value, result.Err = task() 63 | resultChan <- result 64 | a.wg.Done() 65 | } 66 | }() 67 | } 68 | 69 | go func() { 70 | for _, v := range a.tasks { 71 | taskChan <- v 72 | } 73 | a.wg.Wait() 74 | close(resultChan) 75 | close(taskChan) 76 | a.tasks = nil 77 | }() 78 | 79 | result := make([]AsyncResult, 0, taskNum) 80 | for v := range resultChan { 81 | result = append(result, v) 82 | } 83 | return result 84 | } 85 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/base64.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | // Base64OrigLength 计算 base64 数据长度 4 | // https://stackoverflow.com/questions/56140620/how-to-get-original-file-size-from-base64-encode-string 5 | func Base64OrigLength(datas string) int64 { 6 | 7 | l := int64(len(datas)) 8 | 9 | // count how many trailing '=' there are (if any) 10 | eq := int64(0) 11 | if l >= 2 { 12 | if datas[l-1] == '=' { 13 | eq++ 14 | } 15 | if datas[l-2] == '=' { 16 | eq++ 17 | } 18 | 19 | l -= eq 20 | } 21 | 22 | // basically: 23 | // eq == 0 : bits-wasted = 0 24 | // eq == 1 : bits-wasted = 2 25 | // eq == 2 : bits-wasted = 4 26 | 27 | // so orig length == (l*6 - eq*2) / 8 28 | 29 | return (l*3 - eq) / 4 30 | } 31 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/file.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "os" 4 | 5 | func FileExists(path string) bool { 6 | _, err := os.Stat(path) 7 | return !os.IsNotExist(err) 8 | } 9 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/file_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import "testing" 4 | 5 | func TestFileExists(t *testing.T) { 6 | type args struct { 7 | path string 8 | } 9 | tests := []struct { 10 | name string 11 | args args 12 | want bool 13 | }{ 14 | {"exists", args{path: "testdata/a.txt"}, true}, 15 | {"not exists", args{path: "no.txt"}, false}, 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.name, func(t *testing.T) { 19 | if got := FileExists(tt.args.path); got != tt.want { 20 | t.Errorf("FileExists() = %v, want %v", got, tt.want) 21 | } 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/http_client.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | var HttpClient = http.Client{ 9 | Transport: nil, 10 | CheckRedirect: nil, 11 | Jar: nil, 12 | Timeout: 5 * time.Second, 13 | } 14 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/parase_recalled_msg.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "encoding/xml" 5 | ) 6 | 7 | // 8 | // 9 | //wxid_qswi83jdiiw2 10 | //12543453 11 | //500043327888834838 // 元消息id 12 | // 13 | // 14 | // 15 | 16 | // RecalledMsg 撤回消息的 xml 结构体 17 | type RecalledMsg struct { 18 | XMLName xml.Name `xml:"sysmsg"` 19 | Text string `xml:",chardata"` 20 | Type string `xml:"type,attr"` 21 | Revokemsg struct { 22 | Text string `xml:",chardata"` 23 | Session string `xml:"session"` 24 | Msgid string `xml:"msgid"` 25 | Newmsgid string `xml:"newmsgid"` 26 | Replacemsg string `xml:"replacemsg"` 27 | } `xml:"revokemsg"` 28 | } 29 | 30 | // ParseRecalledID 从 xml 中解析撤回的原始消息id 31 | func ParseRecalledID(raw string) string { 32 | msg := &RecalledMsg{} 33 | err := xml.Unmarshal([]byte(raw), msg) 34 | if err != nil { 35 | return raw 36 | } 37 | if msg.Revokemsg.Newmsgid != "" { 38 | return msg.Revokemsg.Newmsgid 39 | } 40 | return raw 41 | } 42 | -------------------------------------------------------------------------------- /wechaty-puppet/helper/testdata/a.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechaty/go-wechaty/dd2d312935802edc721b2b7c1719713e39f0bdab/wechaty-puppet/helper/testdata/a.txt -------------------------------------------------------------------------------- /wechaty-puppet/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "os" 6 | ) 7 | 8 | // L logger 9 | var L = logrus.New() 10 | 11 | // https://github.com/wechaty/wechaty/issues/2167 兼容 wecahty 社区的 log 等级 12 | var logLevels = map[string]string{ 13 | "silent": "panic", 14 | "silly": "trace", 15 | "verbose": "trace", 16 | } 17 | 18 | func init() { 19 | level := os.Getenv("WECHATY_LOG") 20 | if v, ok := logLevels[level]; ok { 21 | level = v 22 | } 23 | logLevel, err := logrus.ParseLevel(level) 24 | if err != nil { 25 | logLevel = logrus.InfoLevel 26 | } 27 | L.SetLevel(logLevel) 28 | L.SetFormatter(&logrus.TextFormatter{ 29 | ForceColors: false, 30 | DisableColors: false, 31 | ForceQuote: false, 32 | DisableQuote: false, 33 | EnvironmentOverrideColors: false, 34 | DisableTimestamp: false, 35 | FullTimestamp: true, 36 | TimestampFormat: "2006-01-02 15:04:05.000", 37 | DisableSorting: false, 38 | DisableLevelTruncation: false, 39 | PadLevelText: false, 40 | QuoteEmptyFields: false, 41 | CallerPrettyfier: nil, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/memory_card.go: -------------------------------------------------------------------------------- 1 | package memory_card 2 | 3 | import ( 4 | "encoding/json" 5 | storage2 "github.com/wechaty/go-wechaty/wechaty-puppet/memory-card/storage" 6 | "sync" 7 | ) 8 | 9 | // memory card interface 10 | type IMemoryCard interface { 11 | GetInt64(key string) int64 12 | GetString(key string) string 13 | SetInt64(key string, value int64) 14 | Clear() 15 | Delete(key string) 16 | Has(key string) bool 17 | Load() error 18 | Save() error 19 | Destroy() error 20 | SetString(key string, value string) 21 | Set(key string, value interface{}) 22 | } 23 | 24 | // TODO: 我将这个地方调整为 把storage的初始化放内部,原实现者可根据情况调整一下 25 | func NewMemoryCard(name string) (IMemoryCard, error) { 26 | var storage, err = storage2.NewFileStorage(name) 27 | if err != nil { 28 | return nil, err 29 | } 30 | var memoryCard = &MemoryCard{payload: &sync.Map{}, storage: storage} 31 | return memoryCard, nil 32 | } 33 | 34 | // memory card 35 | type MemoryCard struct { 36 | payload *sync.Map 37 | storage storage2.IStorage 38 | } 39 | 40 | func (mc *MemoryCard) GetInt64(key string) int64 { 41 | switch raw := mc.get(key).(type) { 42 | case json.Number: 43 | value, _ := raw.Int64() 44 | return value 45 | case int64: 46 | return raw 47 | default: 48 | return 0 49 | } 50 | } 51 | 52 | func (mc *MemoryCard) GetString(key string) string { 53 | value, ok := mc.get(key).(string) 54 | if ok { 55 | return value 56 | } 57 | return "" 58 | } 59 | 60 | func (mc *MemoryCard) get(key string) interface{} { 61 | v, _ := mc.payload.Load(key) 62 | return v 63 | } 64 | 65 | func (mc *MemoryCard) SetInt64(key string, value int64) { 66 | mc.Set(key, value) 67 | } 68 | 69 | func (mc *MemoryCard) SetString(key string, value string) { 70 | mc.Set(key, value) 71 | } 72 | 73 | func (mc *MemoryCard) Set(key string, value interface{}) { 74 | mc.payload.Store(key, value) 75 | } 76 | 77 | func (mc *MemoryCard) Clear() { 78 | mc.payload = &sync.Map{} 79 | } 80 | 81 | func (mc *MemoryCard) Delete(key string) { 82 | mc.payload.Delete(key) 83 | } 84 | 85 | func (mc *MemoryCard) Has(key string) bool { 86 | _, ok := mc.payload.Load(key) 87 | return ok 88 | } 89 | 90 | func (mc *MemoryCard) Load() error { 91 | raw, err := mc.storage.Load() 92 | if err != nil { 93 | return err 94 | } 95 | for k, v := range raw { 96 | mc.Set(k, v) 97 | } 98 | return nil 99 | } 100 | 101 | func (mc *MemoryCard) Save() error { 102 | raw := map[string]interface{}{} 103 | mc.payload.Range(func(key, value interface{}) bool { 104 | raw[key.(string)] = value 105 | return true 106 | }) 107 | return mc.storage.Save(raw) 108 | } 109 | 110 | func (mc *MemoryCard) Destroy() error { 111 | mc.Clear() 112 | return mc.storage.Destroy() 113 | } 114 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/memory_card_test.go: -------------------------------------------------------------------------------- 1 | package memory_card 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "io" 7 | "math/rand" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestMemoryCard_GetInt(t *testing.T) { 13 | mc := newMemoryCard(t) 14 | 15 | t.Run("not value return 0", func(t *testing.T) { 16 | got := mc.GetInt64("not") 17 | if got != 0 { 18 | t.Fatalf("got %d expect 0", got) 19 | } 20 | }) 21 | 22 | t.Run("Set string return 0", func(t *testing.T) { 23 | key := "set_string_return_0" 24 | mc.SetString(key, "string") 25 | got := mc.GetInt64(key) 26 | if got != 0 { 27 | t.Fatalf("got %d expect 0", got) 28 | } 29 | }) 30 | 31 | t.Run("success", func(t *testing.T) { 32 | key := "success" 33 | expect := rand.Int63() 34 | mc.SetInt64(key, expect) 35 | got := mc.GetInt64(key) 36 | if got != expect { 37 | t.Fatalf("got %d expect %d", got, expect) 38 | } 39 | }) 40 | } 41 | 42 | func TestMemoryCard_GetString(t *testing.T) { 43 | mc := newMemoryCard(t) 44 | t.Run("not value return empty string", func(t *testing.T) { 45 | got := mc.GetString("not") 46 | if got != "" { 47 | t.Fatalf("got %s expect empty stirng", got) 48 | } 49 | }) 50 | 51 | t.Run("Set int return empty string", func(t *testing.T) { 52 | key := "set_int_return_empty_string" 53 | mc.SetInt64(key, 1) 54 | got := mc.GetString(key) 55 | if got != "" { 56 | t.Fatalf("got %s expect empty stirng", got) 57 | } 58 | }) 59 | 60 | t.Run("success", func(t *testing.T) { 61 | key := "success" 62 | expect := randString() 63 | mc.SetString(key, expect) 64 | got := mc.GetString(key) 65 | if got != expect { 66 | t.Fatalf("got %s expect %s", got, expect) 67 | } 68 | }) 69 | } 70 | 71 | func TestMemoryCard_SetInt(t *testing.T) { 72 | mc := newMemoryCard(t) 73 | key := "success" 74 | expect := rand.Int63() 75 | mc.SetInt64(key, expect) 76 | got := mc.GetInt64(key) 77 | if got != expect { 78 | t.Fatalf("got %d expect %d", got, expect) 79 | } 80 | } 81 | 82 | func TestMemoryCard_SetString(t *testing.T) { 83 | mc := newMemoryCard(t) 84 | key := "success" 85 | expect := randString() 86 | mc.SetString(key, expect) 87 | got := mc.GetString(key) 88 | if got != expect { 89 | t.Fatalf("got %s expect %s", got, expect) 90 | } 91 | } 92 | 93 | func TestMemoryCard_Clear(t *testing.T) { 94 | mc := newMemoryCard(t) 95 | table := []struct { 96 | key string 97 | value int64 98 | }{ 99 | {"key1", 1}, 100 | {"key2", 2}, 101 | } 102 | for _, v := range table { 103 | mc.SetInt64(v.key, v.value) 104 | } 105 | mc.Clear() 106 | for _, v := range table { 107 | got := mc.GetInt64(v.key) 108 | if got != 0 { 109 | t.Fatalf("got %d expect 0", got) 110 | } 111 | } 112 | } 113 | 114 | func TestMemoryCard_Delete(t *testing.T) { 115 | mc := newMemoryCard(t) 116 | table := []struct { 117 | key string 118 | value int64 119 | }{ 120 | {"key1", 1}, 121 | {"key2", 2}, 122 | } 123 | for _, v := range table { 124 | mc.SetInt64(v.key, v.value) 125 | } 126 | mc.Delete(table[0].key) 127 | got := mc.GetInt64(table[0].key) 128 | if got != 0 { 129 | t.Fatalf("got %d expect 0", got) 130 | } 131 | got = mc.GetInt64(table[1].key) 132 | if got != table[1].value { 133 | t.Fatalf("got %d expect d", table[1].value) 134 | } 135 | } 136 | 137 | func TestMemoryCard_Has(t *testing.T) { 138 | mc := newMemoryCard(t) 139 | if false != mc.Has("null") { 140 | t.Fatalf("got true expect false") 141 | } 142 | mc.SetString("key", "value") 143 | if true != mc.Has("key") { 144 | t.Fatalf("got false expect false") 145 | } 146 | } 147 | 148 | func TestMemoryCard_SaveAndLoad(t *testing.T) { 149 | table := []struct { 150 | key string 151 | value int64 152 | }{ 153 | {"key0", 0}, 154 | {"key1", 1}, 155 | {"key2", 2}, 156 | } 157 | mc1 := newMemoryCard(t) 158 | for _, v := range table { 159 | mc1.Set(v.key, v.value) 160 | } 161 | err := mc1.Save() 162 | if err != nil { 163 | t.Fatal(err.Error()) 164 | } 165 | mc2 := newMemoryCard(t) 166 | err = mc2.Load() 167 | if err != nil { 168 | t.Fatalf(err.Error()) 169 | } 170 | for _, v := range table { 171 | got := mc2.GetInt64(v.key) 172 | if got != v.value { 173 | t.Fatalf("got %d expected %d", got, v.value) 174 | } 175 | } 176 | } 177 | 178 | func randString() string { 179 | t := time.Now() 180 | h := md5.New() 181 | _, _ = io.WriteString(h, t.String()) 182 | return fmt.Sprintf("%x", h.Sum(nil)) 183 | } 184 | 185 | func newMemoryCard(t *testing.T) IMemoryCard { 186 | mc, err := NewMemoryCard("testdata/test") 187 | if err != nil { 188 | t.Fatalf(err.Error()) 189 | } 190 | return mc 191 | } 192 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/storage/backend.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | type IStorage interface { 4 | Save(payload map[string]interface{}) error 5 | Load() (map[string]interface{}, error) 6 | Destroy() error 7 | } 8 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/storage/file.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | helper_functions "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | type FileStorage struct { 13 | absFileName string 14 | } 15 | 16 | func NewFileStorage(absFileName string) (*FileStorage, error) { 17 | absFileName, err := handleAbsFileName(absFileName) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return &FileStorage{absFileName: absFileName}, nil 22 | } 23 | 24 | func (f *FileStorage) Save(payload map[string]interface{}) error { 25 | jsonBytes, err := json.Marshal(payload) 26 | if err != nil { 27 | return err 28 | } 29 | return ioutil.WriteFile(f.absFileName, jsonBytes, os.ModePerm) 30 | } 31 | 32 | func (f *FileStorage) Load() (map[string]interface{}, error) { 33 | if !helper_functions.FileExists(f.absFileName) { 34 | return map[string]interface{}{}, nil 35 | } 36 | file, err := os.Open(f.absFileName) 37 | if err != nil { 38 | return nil, err 39 | } 40 | result := map[string]interface{}{} 41 | decoder := json.NewDecoder(file) 42 | decoder.UseNumber() 43 | if err := decoder.Decode(&result); err != nil { 44 | return nil, err 45 | } 46 | return result, nil 47 | } 48 | 49 | func (f *FileStorage) Destroy() error { 50 | return os.Remove(f.absFileName) 51 | } 52 | 53 | func handleAbsFileName(absFileName string) (string, error) { 54 | const suffix = ".memory-card.json" 55 | if !strings.HasSuffix(absFileName, suffix) { 56 | absFileName = absFileName + suffix 57 | } 58 | if !filepath.IsAbs(absFileName) { 59 | dir, err := os.Getwd() 60 | if err != nil { 61 | return "", err 62 | } 63 | absFileName = filepath.Join(dir, absFileName) 64 | } 65 | return absFileName, nil 66 | } 67 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/storage/file_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "log" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | var data = map[string]interface{}{ 10 | "key1": "key1", 11 | "key2": "key2", 12 | } 13 | 14 | func TestFileStorage_Save(t *testing.T) { 15 | storage := newFileStorage(t) 16 | err := storage.Save(data) 17 | if err != nil { 18 | t.Fatalf(err.Error()) 19 | } 20 | } 21 | 22 | func TestFileStorage_Load(t *testing.T) { 23 | storage := newFileStorage(t) 24 | got, err := storage.Load() 25 | if err != nil { 26 | log.Fatalf(err.Error()) 27 | } 28 | if !reflect.DeepEqual(got, data) { 29 | log.Fatalf("got %v expect %v", got, data) 30 | } 31 | } 32 | 33 | func TestNopStorage_Destroy(t *testing.T) { 34 | storage := newFileStorage(t) 35 | err := storage.Destroy() 36 | if err != nil { 37 | t.Fatalf(err.Error()) 38 | } 39 | } 40 | 41 | func newFileStorage(t *testing.T) *FileStorage { 42 | storage, err := NewFileStorage("testdata/file") 43 | if err != nil { 44 | t.Fatalf(err.Error()) 45 | } 46 | return storage 47 | } 48 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/storage/nop.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | type NopStorage struct { 4 | } 5 | 6 | func (n NopStorage) Save(payload map[string]interface{}) error { 7 | return nil 8 | } 9 | 10 | func (n NopStorage) Load() (map[string]interface{}, error) { 11 | return map[string]interface{}{}, nil 12 | } 13 | 14 | func (n NopStorage) Destroy() {} 15 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/storage/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | file.memory-card.json 2 | -------------------------------------------------------------------------------- /wechaty-puppet/memory-card/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | *.memory-card.json 2 | -------------------------------------------------------------------------------- /wechaty-puppet/message_adapter.go: -------------------------------------------------------------------------------- 1 | package wechatypuppet 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | "regexp" 7 | ) 8 | 9 | var numRegex = regexp.MustCompile(`^\d+$`) 10 | 11 | var rawMsgAdapter = RawMsgAdapter{} 12 | var unknownMsgAdapter = UnknownMsgAdapter{} 13 | var recalledMsgAdapter = RecalledMsgAdapter{} 14 | 15 | // MsgAdapter 消息适配器 16 | type MsgAdapter interface { 17 | Handle(payload *schemas.MessagePayload) 18 | } 19 | 20 | // NewMsgAdapter 各种 puppet 返回的消息有出入,这里做统一 21 | func NewMsgAdapter(msgType schemas.MessageType) MsgAdapter { 22 | switch msgType { 23 | case schemas.MessageTypeUnknown: 24 | return unknownMsgAdapter 25 | case schemas.MessageTypeRecalled: 26 | return recalledMsgAdapter 27 | } 28 | return rawMsgAdapter 29 | } 30 | 31 | // RawMsgAdapter 不需要处理的消息 32 | type RawMsgAdapter struct{} 33 | 34 | // Handle ~ 35 | func (r RawMsgAdapter) Handle(msg *schemas.MessagePayload) {} 36 | 37 | // UnknownMsgAdapter Unknown 类型的消息适配器 38 | type UnknownMsgAdapter struct{} 39 | 40 | // Handle 对 Unknown 类型的消息做适配 41 | func (u UnknownMsgAdapter) Handle(payload *schemas.MessagePayload) { 42 | // 有些消息,puppet 服务端没有解析出来,这里尝试解析 43 | helper.FixUnknownMessage(payload) 44 | } 45 | 46 | // RecalledMsgAdapter 撤回类型的消息适配器 47 | type RecalledMsgAdapter struct{} 48 | 49 | // Handle padlocal 返回的是 xml,需要解析出 msgId 50 | func (r RecalledMsgAdapter) Handle(payload *schemas.MessagePayload) { 51 | if numRegex.MatchString(payload.Text) { 52 | return 53 | } 54 | // padlocal 返回的是 xml,需要解析出 msgId 55 | // https://github.com/wechaty/go-wechaty/issues/87 56 | payload.Text = helper.ParseRecalledID(payload.Text) 57 | } 58 | -------------------------------------------------------------------------------- /wechaty-puppet/option.go: -------------------------------------------------------------------------------- 1 | package wechatypuppet 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Option puppet option 8 | type Option struct { 9 | Endpoint string 10 | Timeout time.Duration 11 | Token string 12 | 13 | // Deprecated: move to wechaty-puppet-service 14 | GrpcReconnectInterval time.Duration 15 | } 16 | 17 | // OptionFn func 18 | type OptionFn func(opts *Option) 19 | 20 | // WithEndpoint with Endpoint 21 | func WithEndpoint(endpoint string) OptionFn { 22 | return func(opts *Option) { 23 | opts.Endpoint = endpoint 24 | } 25 | } 26 | 27 | // WithTimeout with Timeout 28 | func WithTimeout(duration time.Duration) OptionFn { 29 | return func(opts *Option) { 30 | opts.Timeout = duration 31 | } 32 | } 33 | 34 | // WithToken with Token 35 | func WithToken(token string) OptionFn { 36 | return func(opts *Option) { 37 | opts.Token = token 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/contact.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import "regexp" 4 | 5 | //go:generate stringer -type=ContactGender 6 | type ContactGender uint8 7 | 8 | const ( 9 | ContactGenderUnknown ContactGender = 0 10 | ContactGenderMale ContactGender = 1 11 | ContactGenderFemale ContactGender = 2 12 | ) 13 | 14 | //go:generate stringer -type=ContactType 15 | type ContactType uint8 16 | 17 | const ( 18 | ContactTypeUnknown ContactType = 0 19 | ContactTypePersonal ContactType = 1 20 | ContactTypeOfficial ContactType = 2 21 | ) 22 | 23 | // ContactQueryFilter use the first non-empty parameter of all parameters to search 24 | type ContactQueryFilter struct { 25 | // 别名过滤 26 | Alias string 27 | // 别名正则表达式过滤 28 | AliasRegexp *regexp.Regexp 29 | // id 过滤 30 | Id string 31 | // 昵称过滤 32 | Name string 33 | // 昵称正则表达式过滤 34 | NameRegexp *regexp.Regexp 35 | WeiXin string 36 | } 37 | 38 | type ContactPayload struct { 39 | Id string 40 | Gender ContactGender 41 | Type ContactType 42 | Name string 43 | Avatar string 44 | Address string 45 | Alias string 46 | City string 47 | Friend bool 48 | Province string 49 | Signature string 50 | Star bool 51 | WeiXin string 52 | } 53 | 54 | type ContactPayloadFilterFunction func(payload *ContactPayload) bool 55 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/contactgender_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ContactGender"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ContactGenderUnknown-0] 12 | _ = x[ContactGenderMale-1] 13 | _ = x[ContactGenderFemale-2] 14 | } 15 | 16 | const _ContactGender_name = "ContactGenderUnknownContactGenderMaleContactGenderFemale" 17 | 18 | var _ContactGender_index = [...]uint8{0, 20, 37, 56} 19 | 20 | func (i ContactGender) String() string { 21 | if i >= ContactGender(len(_ContactGender_index)-1) { 22 | return "ContactGender(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _ContactGender_name[_ContactGender_index[i]:_ContactGender_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/contacttype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ContactType"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ContactTypeUnknown-0] 12 | _ = x[ContactTypePersonal-1] 13 | _ = x[ContactTypeOfficial-2] 14 | } 15 | 16 | const _ContactType_name = "ContactTypeUnknownContactTypePersonalContactTypeOfficial" 17 | 18 | var _ContactType_index = [...]uint8{0, 18, 37, 56} 19 | 20 | func (i ContactType) String() string { 21 | if i >= ContactType(len(_ContactType_index)-1) { 22 | return "ContactType(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _ContactType_name[_ContactType_index[i]:_ContactType_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/events.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | //go:generate stringer -type=ScanStatus 4 | type ScanStatus uint8 5 | 6 | const ( 7 | ScanStatusUnknown ScanStatus = 0 8 | ScanStatusCancel ScanStatus = 1 9 | ScanStatusWaiting ScanStatus = 2 10 | ScanStatusScanned ScanStatus = 3 11 | ScanStatusConfirmed ScanStatus = 4 12 | ScanStatusTimeout ScanStatus = 5 13 | ) 14 | 15 | type EventFriendshipPayload struct { 16 | FriendshipID string 17 | } 18 | 19 | type EventLoginPayload struct { 20 | ContactId string 21 | } 22 | 23 | type EventLogoutPayload struct { 24 | ContactId string 25 | Data string 26 | } 27 | 28 | type EventMessagePayload struct { 29 | MessageId string 30 | } 31 | 32 | type EventRoomInvitePayload struct { 33 | RoomInvitationId string 34 | } 35 | 36 | type EventRoomJoinPayload struct { 37 | InviteeIdList []string 38 | InviterId string 39 | RoomId string 40 | Timestamp int64 41 | } 42 | 43 | type EventRoomLeavePayload struct { 44 | RemoveeIdList []string 45 | RemoverId string 46 | RoomId string 47 | Timestamp int64 48 | } 49 | 50 | type EventRoomTopicPayload struct { 51 | ChangerId string 52 | NewTopic string 53 | OldTopic string 54 | RoomId string 55 | Timestamp int64 56 | } 57 | 58 | type EventScanPayload struct { 59 | BaseEventPayload 60 | Status ScanStatus 61 | QrCode string 62 | } 63 | 64 | type EventDirtyPayload struct { 65 | PayloadType PayloadType 66 | PayloadId string 67 | } 68 | 69 | type BaseEventPayload struct { 70 | Data string 71 | } 72 | 73 | type EventDongPayload = BaseEventPayload 74 | 75 | type EventErrorPayload = BaseEventPayload 76 | 77 | type EventReadyPayload = BaseEventPayload 78 | 79 | type EventResetPayload = BaseEventPayload 80 | 81 | type EventHeartbeatPayload = BaseEventPayload 82 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/friendship.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | //go:generate stringer -type=FriendshipType 4 | type FriendshipType uint8 5 | 6 | const ( 7 | FriendshipTypeUnknown FriendshipType = 0 8 | FriendshipTypeConfirm FriendshipType = 1 9 | FriendshipTypeReceive FriendshipType = 2 10 | FriendshipTypeVerify FriendshipType = 3 11 | ) 12 | 13 | type FriendshipSceneType uint8 14 | 15 | const ( 16 | FriendshipSceneTypeUnknown FriendshipSceneType = 0 17 | FriendshipSceneTypeQQ FriendshipSceneType = 1 18 | FriendshipSceneTypeEmail FriendshipSceneType = 2 19 | FriendshipSceneTypeWeiXin FriendshipSceneType = 3 20 | FriendshipSceneTypeQQTBD FriendshipSceneType = 12 // QQ号搜索 21 | FriendshipSceneTypeRoom FriendshipSceneType = 14 22 | FriendshipSceneTypePhone FriendshipSceneType = 15 23 | FriendshipSceneTypeCard FriendshipSceneType = 17 // 名片分享 24 | FriendshipSceneTypeLocation FriendshipSceneType = 18 25 | FriendshipSceneTypeBottle FriendshipSceneType = 25 26 | FriendshipSceneTypeShaking FriendshipSceneType = 29 27 | FriendshipSceneTypeQRCode FriendshipSceneType = 30 28 | ) 29 | 30 | type FriendshipPayloadBase struct { 31 | Id string `json:"id"` 32 | ContactId string `json:"contactId"` 33 | Hello string `json:"hello"` 34 | Timestamp int64 `json:"timestamp"` 35 | } 36 | 37 | type FriendshipPayloadConfirm struct { 38 | FriendshipPayloadBase 39 | Type FriendshipType // FriendshipTypeConfirm 40 | } 41 | 42 | type FriendshipPayloadReceive struct { 43 | FriendshipPayloadBase 44 | Type FriendshipType `json:"type"` // FriendshipTypeReceive 45 | Scene FriendshipSceneType `json:"scene"` 46 | Stranger string `json:"stranger"` 47 | Ticket string `json:"ticket"` 48 | } 49 | 50 | type FriendshipPayloadVerify struct { 51 | FriendshipPayloadBase 52 | Type FriendshipType // FriendshipTypeVerify 53 | } 54 | 55 | type FriendshipPayload struct { 56 | FriendshipPayloadReceive 57 | } 58 | 59 | // FriendshipSearchCondition use the first non-empty parameter of all parameters to search 60 | type FriendshipSearchCondition struct { 61 | Phone string 62 | WeiXin string 63 | } 64 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/friendshiptype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=FriendshipType"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[FriendshipTypeUnknown-0] 12 | _ = x[FriendshipTypeConfirm-1] 13 | _ = x[FriendshipTypeReceive-2] 14 | _ = x[FriendshipTypeVerify-3] 15 | } 16 | 17 | const _FriendshipType_name = "FriendshipTypeUnknownFriendshipTypeConfirmFriendshipTypeReceiveFriendshipTypeVerify" 18 | 19 | var _FriendshipType_index = [...]uint8{0, 21, 42, 63, 83} 20 | 21 | func (i FriendshipType) String() string { 22 | if i >= FriendshipType(len(_FriendshipType_index)-1) { 23 | return "FriendshipType(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _FriendshipType_name[_FriendshipType_index[i]:_FriendshipType_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/image.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | //go:generate stringer -type=ImageType 4 | type ImageType uint8 5 | 6 | const ( 7 | ImageTypeUnknown ImageType = iota 8 | ImageTypeThumbnail 9 | ImageTypeHD 10 | ImageTypeArtwork 11 | ) 12 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/imagetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ImageType"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ImageTypeUnknown-0] 12 | _ = x[ImageTypeThumbnail-1] 13 | _ = x[ImageTypeHD-2] 14 | _ = x[ImageTypeArtwork-3] 15 | } 16 | 17 | const _ImageType_name = "ImageTypeUnknownImageTypeThumbnailImageTypeHDImageTypeArtwork" 18 | 19 | var _ImageType_index = [...]uint8{0, 16, 34, 45, 61} 20 | 21 | func (i ImageType) String() string { 22 | if i >= ImageType(len(_ImageType_index)-1) { 23 | return "ImageType(" + strconv.FormatInt(int64(i), 10) + ")" 24 | } 25 | return _ImageType_name[_ImageType_index[i]:_ImageType_index[i+1]] 26 | } 27 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/location.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | type LocationPayload struct { 4 | Accuracy float32 `json:"accuracy"` // Estimated horizontal accuracy of this location, radial, in meters. (same as Android & iOS API) 5 | Address string `json:"address"` // 北京市北京市海淀区45 Chengfu Rd 6 | Latitude float64 `json:"latitude"` // 39.995120999999997 7 | Longitude float64 `json:"longitude"` // 116.3341 8 | Name string `json:"name"` // 东升乡人民政府(海淀区成府路45号) 9 | } 10 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/message.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import ( 4 | "regexp" 5 | "time" 6 | ) 7 | 8 | //go:generate stringer -type=MessageType 9 | type MessageType uint8 10 | 11 | const ( 12 | MessageTypeUnknown MessageType = 0 13 | MessageTypeAttachment MessageType = 1 14 | MessageTypeAudio MessageType = 2 15 | MessageTypeContact MessageType = 3 16 | MessageTypeChatHistory MessageType = 4 17 | MessageTypeEmoticon MessageType = 5 18 | MessageTypeImage MessageType = 6 19 | MessageTypeText MessageType = 7 20 | MessageTypeLocation MessageType = 8 21 | MessageTypeMiniProgram MessageType = 9 22 | MessageTypeGroupNote MessageType = 10 23 | MessageTypeTransfer MessageType = 11 24 | MessageTypeRedEnvelope MessageType = 12 25 | MessageTypeRecalled MessageType = 13 26 | MessageTypeURL MessageType = 14 27 | MessageTypeVideo MessageType = 15 28 | ) 29 | 30 | type WeChatAppMessageType int 31 | 32 | const ( 33 | WeChatAppMessageTypeText WeChatAppMessageType = 1 34 | WeChatAppMessageTypeImg WeChatAppMessageType = 2 35 | WeChatAppMessageTypeAudio WeChatAppMessageType = 3 36 | WeChatAppMessageTypeVideo WeChatAppMessageType = 4 37 | WeChatAppMessageTypeUrl WeChatAppMessageType = 5 38 | WeChatAppMessageTypeAttach WeChatAppMessageType = 6 39 | WeChatAppMessageTypeOpen WeChatAppMessageType = 7 40 | WeChatAppMessageTypeEmoji WeChatAppMessageType = 8 41 | WeChatAppMessageTypeVoiceRemind WeChatAppMessageType = 9 42 | WeChatAppMessageTypeScanGood WeChatAppMessageType = 10 43 | WeChatAppMessageTypeGood WeChatAppMessageType = 13 44 | WeChatAppMessageTypeEmotion WeChatAppMessageType = 15 45 | WeChatAppMessageTypeCardTicket WeChatAppMessageType = 16 46 | WeChatAppMessageTypeRealtimeShareLocation WeChatAppMessageType = 17 47 | WeChatAppMessageTypeChatHistory WeChatAppMessageType = 19 48 | WeChatAppMessageTypeMiniProgram WeChatAppMessageType = 33 49 | WeChatAppMessageTypeTransfers WeChatAppMessageType = 2000 50 | WeChatAppMessageTypeRedEnvelopes WeChatAppMessageType = 2001 51 | WeChatAppMessageTypeReaderType WeChatAppMessageType = 100001 52 | ) 53 | 54 | type WeChatMessageType int 55 | 56 | const ( 57 | WeChatMessageTypeText WeChatMessageType = 1 58 | WeChatMessageTypeImage WeChatMessageType = 3 59 | WeChatMessageTypeVoice WeChatMessageType = 34 60 | WeChatMessageTypeVerifyMsg WeChatMessageType = 37 61 | WeChatMessageTypePossibleFriendMsg WeChatMessageType = 40 62 | WeChatMessageTypeShareCard WeChatMessageType = 42 63 | WeChatMessageTypeVideo WeChatMessageType = 43 64 | WeChatMessageTypeEmoticon WeChatMessageType = 47 65 | WeChatMessageTypeLocation WeChatMessageType = 48 66 | WeChatMessageTypeApp WeChatMessageType = 49 67 | WeChatMessageTypeVOIPMsg WeChatMessageType = 50 68 | WeChatMessageTypeStatusNotify WeChatMessageType = 51 69 | WeChatMessageTypeVOIPNotify WeChatMessageType = 52 70 | WeChatMessageTypeVOIPInvite WeChatMessageType = 53 71 | WeChatMessageTypeMicroVideo WeChatMessageType = 62 72 | WeChatMessageTypeTransfer WeChatMessageType = 2000 // 转账 73 | WeChatMessageTypeRedEnvelope WeChatMessageType = 2001 // 红包 74 | WeChatMessageTypeMiniProgram WeChatMessageType = 2002 // 小程序 75 | WeChatMessageTypeGroupInvite WeChatMessageType = 2003 // 群邀请 76 | WeChatMessageTypeFile WeChatMessageType = 2004 // 文件消息 77 | WeChatMessageTypeSysNotice WeChatMessageType = 9999 78 | WeChatMessageTypeSys WeChatMessageType = 10000 79 | WeChatMessageTypeRecalled WeChatMessageType = 10002 80 | ) 81 | 82 | type MessagePayloadBase struct { 83 | Id string 84 | 85 | // use message id to get rawPayload to get those informations when needed 86 | // contactId string // Contact ShareCard 87 | MentionIdList []string // Mentioned Contacts' Ids 88 | 89 | FileName string 90 | Text string 91 | Timestamp time.Time 92 | Type MessageType 93 | 94 | // 小程序有些消息类型,wechaty服务端解析不处理,框架端解析。 xml type 36 是小程序 95 | FixMiniApp bool 96 | 97 | ReferMessage *ReferMessagePayload 98 | } 99 | 100 | // ReferMessagePayload refer message payload 101 | type ReferMessagePayload struct { 102 | Type MessageType // TODO: 确认是否和 MessageType 一致 103 | SourceMsgID string 104 | TalkerId string 105 | RoomId string 106 | DisplayName string 107 | Content string 108 | Timestamp time.Time 109 | } 110 | 111 | type MessagePayloadRoom struct { 112 | TalkerId string 113 | RoomId string 114 | ListenerId string 115 | } 116 | 117 | type MessagePayloadTo = MessagePayloadRoom 118 | 119 | type MessagePayload struct { 120 | MessagePayloadBase 121 | MessagePayloadRoom 122 | } 123 | 124 | type MessageQueryFilter struct { 125 | TalkerId string 126 | // Deprecated: please use TalkerId 127 | FromId string 128 | Id string 129 | RoomId string 130 | Text string 131 | TextRegExp *regexp.Regexp 132 | // Deprecated: please use ListenerId 133 | ToId string 134 | ListenerId string 135 | Type MessageType 136 | } 137 | 138 | type MessagePayloadFilterFunction func(payload *MessagePayload) bool 139 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/messagetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=MessageType"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[MessageTypeUnknown-0] 12 | _ = x[MessageTypeAttachment-1] 13 | _ = x[MessageTypeAudio-2] 14 | _ = x[MessageTypeContact-3] 15 | _ = x[MessageTypeChatHistory-4] 16 | _ = x[MessageTypeEmoticon-5] 17 | _ = x[MessageTypeImage-6] 18 | _ = x[MessageTypeText-7] 19 | _ = x[MessageTypeLocation-8] 20 | _ = x[MessageTypeMiniProgram-9] 21 | _ = x[MessageTypeGroupNote-10] 22 | _ = x[MessageTypeTransfer-11] 23 | _ = x[MessageTypeRedEnvelope-12] 24 | _ = x[MessageTypeRecalled-13] 25 | _ = x[MessageTypeURL-14] 26 | _ = x[MessageTypeVideo-15] 27 | } 28 | 29 | const _MessageType_name = "MessageTypeUnknownMessageTypeAttachmentMessageTypeAudioMessageTypeContactMessageTypeChatHistoryMessageTypeEmoticonMessageTypeImageMessageTypeTextMessageTypeLocationMessageTypeMiniProgramMessageTypeGroupNoteMessageTypeTransferMessageTypeRedEnvelopeMessageTypeRecalledMessageTypeURLMessageTypeVideo" 30 | 31 | var _MessageType_index = [...]uint16{0, 18, 39, 55, 73, 95, 114, 130, 145, 164, 186, 206, 225, 247, 266, 280, 296} 32 | 33 | func (i MessageType) String() string { 34 | if i >= MessageType(len(_MessageType_index)-1) { 35 | return "MessageType(" + strconv.FormatInt(int64(i), 10) + ")" 36 | } 37 | return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] 38 | } 39 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/mini_program.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import "encoding/json" 4 | 5 | type MiniProgramPayload struct { 6 | Appid string `json:"appid"` // optional, Appid, get from wechat (mp.weixin.qq.com) 7 | Description string `json:"description"` // optional, mini program title 8 | PagePath string `json:"pagePath"` // optional, mini program page path 9 | ThumbUrl string `json:"thumbUrl"` // optional, default picture, convert to thumbnail 10 | Title string `json:"title"` // optional, mini program title 11 | Username string `json:"username"` // original ID, get from wechat (mp.weixin.qq.com) 12 | ThumbKey string `json:"thumbKey"` // original, thumbnailurl and thumbkey will make the headphoto of mini-program better 13 | ShareId string `json:"shareId"` // optional, the unique userId for who share this mini program 14 | IconUrl string `json:"iconUrl"` // optional, mini program icon url 15 | } 16 | 17 | func (m *MiniProgramPayload) ToJson() string { 18 | b, _ := json.Marshal(m) 19 | return string(b) 20 | } 21 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/payload.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | //go:generate stringer -type=PayloadType 4 | 5 | // PayloadType ... 6 | type PayloadType int32 7 | 8 | const ( 9 | // PayloadTypeUnknown unknown 10 | PayloadTypeUnknown PayloadType = iota 11 | PayloadTypeMessage 12 | PayloadTypeContact 13 | PayloadTypeRoom 14 | PayloadTypeRoomMember 15 | PayloadTypeFriendship 16 | ) 17 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/payloadtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=PayloadType"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[PayloadTypeUnknown-0] 12 | _ = x[PayloadTypeMessage-1] 13 | _ = x[PayloadTypeContact-2] 14 | _ = x[PayloadTypeRoom-3] 15 | _ = x[PayloadTypeRoomMember-4] 16 | _ = x[PayloadTypeFriendship-5] 17 | } 18 | 19 | const _PayloadType_name = "PayloadTypeUnknownPayloadTypeMessagePayloadTypeContactPayloadTypeRoomPayloadTypeRoomMemberPayloadTypeFriendship" 20 | 21 | var _PayloadType_index = [...]uint8{0, 18, 36, 54, 69, 90, 111} 22 | 23 | func (i PayloadType) String() string { 24 | if i < 0 || i >= PayloadType(len(_PayloadType_index)-1) { 25 | return "PayloadType(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _PayloadType_name[_PayloadType_index[i]:_PayloadType_index[i+1]] 28 | } 29 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/puppet.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import pbwechaty "github.com/wechaty/go-grpc/wechaty/puppet" 4 | 5 | //go:generate stringer -type=PuppetEventName 6 | type PuppetEventName uint8 7 | 8 | const ( 9 | PuppetEventNameUnknown PuppetEventName = iota 10 | PuppetEventNameFriendship 11 | PuppetEventNameLogin 12 | PuppetEventNameLogout 13 | PuppetEventNameMessage 14 | PuppetEventNameRoomInvite 15 | PuppetEventNameRoomJoin 16 | PuppetEventNameRoomLeave 17 | PuppetEventNameRoomTopic 18 | PuppetEventNameScan 19 | 20 | PuppetEventNameDong 21 | PuppetEventNameError 22 | PuppetEventNameHeartbeat 23 | PuppetEventNameReady 24 | PuppetEventNameReset 25 | PuppetEventNameDirty 26 | 27 | PuppetEventNameStop 28 | PuppetEventNameStart 29 | ) 30 | 31 | var eventNames = []PuppetEventName{ 32 | PuppetEventNameFriendship, 33 | PuppetEventNameLogin, 34 | PuppetEventNameLogout, 35 | PuppetEventNameMessage, 36 | PuppetEventNameRoomInvite, 37 | PuppetEventNameRoomJoin, 38 | PuppetEventNameRoomLeave, 39 | PuppetEventNameRoomTopic, 40 | PuppetEventNameScan, 41 | 42 | PuppetEventNameDong, 43 | PuppetEventNameError, 44 | PuppetEventNameHeartbeat, 45 | PuppetEventNameReady, 46 | PuppetEventNameReset, 47 | PuppetEventNameDirty, 48 | 49 | PuppetEventNameStop, 50 | PuppetEventNameStart, 51 | } 52 | 53 | func GetEventNames() []PuppetEventName { 54 | return eventNames 55 | } 56 | 57 | var pbEventType2PuppetEventName = map[pbwechaty.EventType]PuppetEventName{ 58 | pbwechaty.EventType_EVENT_TYPE_DONG: PuppetEventNameDong, 59 | pbwechaty.EventType_EVENT_TYPE_ERROR: PuppetEventNameError, 60 | pbwechaty.EventType_EVENT_TYPE_HEARTBEAT: PuppetEventNameHeartbeat, 61 | pbwechaty.EventType_EVENT_TYPE_FRIENDSHIP: PuppetEventNameFriendship, 62 | pbwechaty.EventType_EVENT_TYPE_LOGIN: PuppetEventNameLogin, 63 | pbwechaty.EventType_EVENT_TYPE_LOGOUT: PuppetEventNameLogout, 64 | pbwechaty.EventType_EVENT_TYPE_MESSAGE: PuppetEventNameMessage, 65 | pbwechaty.EventType_EVENT_TYPE_READY: PuppetEventNameReady, 66 | pbwechaty.EventType_EVENT_TYPE_ROOM_INVITE: PuppetEventNameRoomInvite, 67 | pbwechaty.EventType_EVENT_TYPE_ROOM_JOIN: PuppetEventNameRoomJoin, 68 | pbwechaty.EventType_EVENT_TYPE_ROOM_LEAVE: PuppetEventNameRoomLeave, 69 | pbwechaty.EventType_EVENT_TYPE_ROOM_TOPIC: PuppetEventNameRoomTopic, 70 | pbwechaty.EventType_EVENT_TYPE_SCAN: PuppetEventNameScan, 71 | pbwechaty.EventType_EVENT_TYPE_RESET: PuppetEventNameReset, 72 | pbwechaty.EventType_EVENT_TYPE_UNSPECIFIED: PuppetEventNameUnknown, 73 | pbwechaty.EventType_EVENT_TYPE_DIRTY: PuppetEventNameDirty, 74 | } 75 | 76 | // PbEventType2PuppetEventName grpc event map wechaty-puppet event name 77 | func PbEventType2PuppetEventName() map[pbwechaty.EventType]PuppetEventName { 78 | return pbEventType2PuppetEventName 79 | } 80 | 81 | var pbEventType2GeneratePayloadFunc = map[pbwechaty.EventType]func() interface{}{ 82 | pbwechaty.EventType_EVENT_TYPE_DONG: func() interface{} { return &EventDongPayload{} }, 83 | pbwechaty.EventType_EVENT_TYPE_ERROR: func() interface{} { return &EventErrorPayload{} }, 84 | pbwechaty.EventType_EVENT_TYPE_HEARTBEAT: func() interface{} { return &EventHeartbeatPayload{} }, 85 | pbwechaty.EventType_EVENT_TYPE_FRIENDSHIP: func() interface{} { return &EventFriendshipPayload{} }, 86 | pbwechaty.EventType_EVENT_TYPE_LOGIN: func() interface{} { return &EventLoginPayload{} }, 87 | pbwechaty.EventType_EVENT_TYPE_LOGOUT: func() interface{} { return &EventLogoutPayload{} }, 88 | pbwechaty.EventType_EVENT_TYPE_MESSAGE: func() interface{} { return &EventMessagePayload{} }, 89 | pbwechaty.EventType_EVENT_TYPE_READY: func() interface{} { return &EventReadyPayload{} }, 90 | pbwechaty.EventType_EVENT_TYPE_ROOM_INVITE: func() interface{} { return &EventRoomInvitePayload{} }, 91 | pbwechaty.EventType_EVENT_TYPE_ROOM_JOIN: func() interface{} { return &EventRoomJoinPayload{} }, 92 | pbwechaty.EventType_EVENT_TYPE_ROOM_LEAVE: func() interface{} { return &EventRoomLeavePayload{} }, 93 | pbwechaty.EventType_EVENT_TYPE_ROOM_TOPIC: func() interface{} { return &EventRoomTopicPayload{} }, 94 | pbwechaty.EventType_EVENT_TYPE_SCAN: func() interface{} { return &EventScanPayload{} }, 95 | pbwechaty.EventType_EVENT_TYPE_RESET: func() interface{} { return &EventResetPayload{} }, 96 | pbwechaty.EventType_EVENT_TYPE_DIRTY: func() interface{} { return &EventDirtyPayload{} }, 97 | } 98 | 99 | // PbEventType2GeneratePayloadFunc grpc event map wechaty-puppet event payload 100 | func PbEventType2GeneratePayloadFunc() map[pbwechaty.EventType]func() interface{} { 101 | return pbEventType2GeneratePayloadFunc 102 | } 103 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/puppeteventname_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=PuppetEventName"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[PuppetEventNameUnknown-0] 12 | _ = x[PuppetEventNameFriendship-1] 13 | _ = x[PuppetEventNameLogin-2] 14 | _ = x[PuppetEventNameLogout-3] 15 | _ = x[PuppetEventNameMessage-4] 16 | _ = x[PuppetEventNameRoomInvite-5] 17 | _ = x[PuppetEventNameRoomJoin-6] 18 | _ = x[PuppetEventNameRoomLeave-7] 19 | _ = x[PuppetEventNameRoomTopic-8] 20 | _ = x[PuppetEventNameScan-9] 21 | _ = x[PuppetEventNameDong-10] 22 | _ = x[PuppetEventNameError-11] 23 | _ = x[PuppetEventNameHeartbeat-12] 24 | _ = x[PuppetEventNameReady-13] 25 | _ = x[PuppetEventNameReset-14] 26 | _ = x[PuppetEventNameDirty-15] 27 | _ = x[PuppetEventNameStop-16] 28 | _ = x[PuppetEventNameStart-17] 29 | } 30 | 31 | const _PuppetEventName_name = "PuppetEventNameUnknownPuppetEventNameFriendshipPuppetEventNameLoginPuppetEventNameLogoutPuppetEventNameMessagePuppetEventNameRoomInvitePuppetEventNameRoomJoinPuppetEventNameRoomLeavePuppetEventNameRoomTopicPuppetEventNameScanPuppetEventNameDongPuppetEventNameErrorPuppetEventNameHeartbeatPuppetEventNameReadyPuppetEventNameResetPuppetEventNameDirtyPuppetEventNameStopPuppetEventNameStart" 32 | 33 | var _PuppetEventName_index = [...]uint16{0, 22, 47, 67, 88, 110, 135, 158, 182, 206, 225, 244, 264, 288, 308, 328, 348, 367, 387} 34 | 35 | func (i PuppetEventName) String() string { 36 | if i >= PuppetEventName(len(_PuppetEventName_index)-1) { 37 | return "PuppetEventName(" + strconv.FormatInt(int64(i), 10) + ")" 38 | } 39 | return _PuppetEventName_name[_PuppetEventName_index[i]:_PuppetEventName_index[i+1]] 40 | } 41 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/room.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import "regexp" 4 | 5 | type RoomMemberQueryFilter struct { 6 | Name string 7 | RoomAlias string 8 | ContactAlias string 9 | } 10 | 11 | type RoomQueryFilter struct { 12 | // 使用 room id 过滤 13 | Id string 14 | // 使用群名称过滤 15 | Topic string 16 | // 群名称正则过滤 17 | TopicRegexp *regexp.Regexp 18 | } 19 | 20 | func (r *RoomQueryFilter) Empty() bool { 21 | return r.Id == "" && r.Topic == "" && r.TopicRegexp == nil 22 | } 23 | 24 | func (r *RoomQueryFilter) All() bool { 25 | return r.Id != "" && r.Topic != "" && r.TopicRegexp != nil 26 | } 27 | 28 | type RoomPayload struct { 29 | Id string 30 | Topic string 31 | Avatar string 32 | MemberIdList []string 33 | OwnerId string 34 | AdminIdList []string 35 | } 36 | 37 | type RoomMemberPayload struct { 38 | Id string 39 | RoomAlias string 40 | InviterId string 41 | Avatar string 42 | Name string 43 | } 44 | 45 | type RoomPayloadFilterFunction func(payload *RoomPayload) bool 46 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/room_invitation.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import "time" 4 | 5 | type RoomInvitationPayload struct { 6 | Id string `json:"id"` 7 | InviterId string `json:"inviterId"` 8 | RoomId string `json:"roomId"` 9 | Topic string `json:"topic"` 10 | Avatar string `json:"avatar"` 11 | Invitation string `json:"invitation"` 12 | MemberCount int `json:"memberCount"` 13 | MemberIdList []string `json:"memberIdList"` 14 | Timestamp time.Time `json:"timestamp"` 15 | ReceiverId string `json:"receiverId"` 16 | } 17 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/scanstatus_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=ScanStatus"; DO NOT EDIT. 2 | 3 | package schemas 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ScanStatusUnknown-0] 12 | _ = x[ScanStatusCancel-1] 13 | _ = x[ScanStatusWaiting-2] 14 | _ = x[ScanStatusScanned-3] 15 | _ = x[ScanStatusConfirmed-4] 16 | _ = x[ScanStatusTimeout-5] 17 | } 18 | 19 | const _ScanStatus_name = "ScanStatusUnknownScanStatusCancelScanStatusWaitingScanStatusScannedScanStatusConfirmedScanStatusTimeout" 20 | 21 | var _ScanStatus_index = [...]uint8{0, 17, 33, 50, 67, 86, 103} 22 | 23 | func (i ScanStatus) String() string { 24 | if i >= ScanStatus(len(_ScanStatus_index)-1) { 25 | return "ScanStatus(" + strconv.FormatInt(int64(i), 10) + ")" 26 | } 27 | return _ScanStatus_name[_ScanStatus_index[i]:_ScanStatus_index[i+1]] 28 | } 29 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/type.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | // EmitStruct receive puppet emit event 4 | type EmitStruct struct { 5 | EventName PuppetEventName 6 | Payload interface{} 7 | } 8 | 9 | // EventParams wechaty emit params 10 | type EventParams struct { 11 | EventName PuppetEventName 12 | Params []interface{} 13 | } 14 | -------------------------------------------------------------------------------- /wechaty-puppet/schemas/url_link.go: -------------------------------------------------------------------------------- 1 | package schemas 2 | 3 | import "encoding/json" 4 | 5 | type UrlLinkPayload struct { 6 | Description string `json:"description"` 7 | ThumbnailUrl string `json:"thumbnailUrl"` 8 | Title string `json:"title"` 9 | Url string `json:"url"` 10 | } 11 | 12 | func (u *UrlLinkPayload) ToJson() string { 13 | b, _ := json.Marshal(u) 14 | return string(b) 15 | } 16 | -------------------------------------------------------------------------------- /wechaty/accessory.go: -------------------------------------------------------------------------------- 1 | package wechaty 2 | 3 | import ( 4 | wechatypuppet "github.com/wechaty/go-wechaty/wechaty-puppet" 5 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 6 | ) 7 | 8 | // Accessory ... 9 | type Accessory struct { 10 | puppet wechatypuppet.IPuppetAbstract 11 | wechaty *Wechaty 12 | } 13 | 14 | // GetPuppet ... 15 | func (a *Accessory) GetPuppet() wechatypuppet.IPuppetAbstract { 16 | return a.puppet 17 | } 18 | 19 | // GetWechaty ... 20 | func (a *Accessory) GetWechaty() _interface.IWechaty { 21 | return a.wechaty 22 | } 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /wechaty/config.go: -------------------------------------------------------------------------------- 1 | package wechaty 2 | 3 | import logger "github.com/wechaty/go-wechaty/wechaty-puppet/log" 4 | 5 | var log = logger.L.WithField("module", "wechaty") 6 | -------------------------------------------------------------------------------- /wechaty/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 5 | "regexp" 6 | ) 7 | 8 | // AtSepratorRegex mobile: \u2005, PC、mac: \u0020 9 | // Deprecated: use AtSeparatorRegexStr 10 | const AtSepratorRegex = "[\u2005\u0020]" 11 | 12 | // AtSeparatorRegexStr mobile: \u2005, PC、mac: \u0020 13 | const AtSeparatorRegexStr = "[\u2005\u0020]" 14 | 15 | const FourPerEmSpace = string(rune(8197)) 16 | 17 | // AtSeparatorRegex regular expression split '@' 18 | var AtSeparatorRegex = regexp.MustCompile(AtSeparatorRegexStr) 19 | 20 | func QRCodeForChatie() *filebox.FileBox { 21 | const chatieOfficialAccountQrcode = "http://weixin.qq.com/r/qymXj7DEO_1ErfTs93y5" 22 | return filebox.FromQRCode(chatieOfficialAccountQrcode) 23 | } 24 | -------------------------------------------------------------------------------- /wechaty/event.go: -------------------------------------------------------------------------------- 1 | package wechaty 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 5 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 6 | "github.com/wechaty/go-wechaty/wechaty/user" 7 | "time" 8 | ) 9 | 10 | type ( 11 | // EventDong ... 12 | EventDong func(context *Context, data string) 13 | // EventError ... 14 | EventError func(context *Context, err error) 15 | // EventFriendship ... 16 | EventFriendship func(context *Context, friendship *user.Friendship) 17 | // EventHeartbeat ... 18 | EventHeartbeat func(context *Context, data string) 19 | // EventLogin ... 20 | EventLogin func(context *Context, user *user.ContactSelf) 21 | // EventLogout ... 22 | EventLogout func(context *Context, user *user.ContactSelf, reason string) 23 | // EventMessage ... 24 | EventMessage func(context *Context, message *user.Message) 25 | // EventReady ... 26 | EventReady func(context *Context) 27 | // EventRoomInvite ... 28 | EventRoomInvite func(context *Context, roomInvitation *user.RoomInvitation) 29 | // EventRoomJoin ... 30 | EventRoomJoin func(context *Context, room *user.Room, inviteeList []_interface.IContact, inviter _interface.IContact, date time.Time) 31 | // EventRoomLeave ... 32 | EventRoomLeave func(context *Context, room *user.Room, leaverList []_interface.IContact, remover _interface.IContact, date time.Time) 33 | // EventRoomTopic ... 34 | EventRoomTopic func(context *Context, room *user.Room, newTopic string, oldTopic string, changer _interface.IContact, date time.Time) 35 | // EventScan ... 36 | EventScan func(context *Context, qrCode string, status schemas.ScanStatus, data string) 37 | // EventStart ... 38 | EventStart func(context *Context) 39 | // EventStop ... 40 | EventStop func(context *Context) 41 | ) 42 | -------------------------------------------------------------------------------- /wechaty/factory/config.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import logger "github.com/wechaty/go-wechaty/wechaty-puppet/log" 4 | 5 | var log = logger.L.WithField("module", "wechaty/factory") 6 | -------------------------------------------------------------------------------- /wechaty/factory/contact.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 8 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 9 | "github.com/wechaty/go-wechaty/wechaty/user" 10 | ) 11 | 12 | type ContactFactory struct { 13 | _interface.IAccessory 14 | pool *sync.Map 15 | } 16 | 17 | // NewContactFactory ... 18 | func NewContactFactory(accessory _interface.IAccessory) *ContactFactory { 19 | return &ContactFactory{ 20 | IAccessory: accessory, 21 | pool: &sync.Map{}, 22 | } 23 | } 24 | 25 | // Load query param is string 26 | func (c *ContactFactory) Load(id string) _interface.IContact { 27 | load, ok := c.pool.Load(id) 28 | if !ok { 29 | contact := user.NewContact(id, c.IAccessory) 30 | c.pool.Store(id, contact) 31 | return contact 32 | } 33 | switch v := load.(type) { 34 | case *user.ContactSelf: 35 | return v.Contact 36 | case *user.Contact: 37 | return v 38 | default: 39 | panic(fmt.Sprintf("ContactFactory Load unknow type: %#v", v)) 40 | } 41 | } 42 | 43 | // LoadSelf query param is string 44 | func (c *ContactFactory) LoadSelf(id string) _interface.IContactSelf { 45 | load, ok := c.pool.Load(id) 46 | if !ok { 47 | contact := user.NewContactSelf(id, c.IAccessory) 48 | c.pool.Store(id, contact) 49 | return contact 50 | } 51 | switch v := load.(type) { 52 | case *user.ContactSelf: 53 | return v 54 | case *user.Contact: 55 | return &user.ContactSelf{Contact: v} 56 | default: 57 | panic(fmt.Sprintf("ContactFactory LoadSelf unknow type: %#v", v)) 58 | } 59 | } 60 | 61 | // Find query params is string or *schemas.ContactQueryFilter 62 | func (c *ContactFactory) Find(query interface{}) _interface.IContact { 63 | contacts := c.FindAll(query) 64 | if len(contacts) == 0 { 65 | return nil 66 | } 67 | if len(contacts) > 1 { 68 | log.Warnf("Contact Find() got more than one(%d) result\n", len(contacts)) 69 | } 70 | for _, v := range contacts { 71 | if c.GetPuppet().ContactValidate(v.ID()) { 72 | return v 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | // FindAll query params is string or *schemas.ContactQueryFilter 79 | func (c *ContactFactory) FindAll(query interface{}) []_interface.IContact { 80 | contactIds, err := c.GetPuppet().ContactSearch(query, nil) 81 | if err != nil { 82 | log.Errorf("Contact c.GetPuppet().ContactSearch() rejected: %s\n", err) 83 | return nil 84 | } 85 | 86 | if len(contactIds) == 0 { 87 | return nil 88 | } 89 | 90 | async := helper.NewAsync(helper.DefaultWorkerNum) 91 | for _, id := range contactIds { 92 | id := id 93 | async.AddTask(func() (interface{}, error) { 94 | contact := c.Load(id) 95 | return contact, contact.Ready(false) 96 | }) 97 | } 98 | 99 | var contacts []_interface.IContact 100 | for _, v := range async.Result() { 101 | if v.Err != nil { 102 | continue 103 | } 104 | contacts = append(contacts, v.Value.(_interface.IContact)) 105 | } 106 | return contacts 107 | } 108 | 109 | // Tags get tags for all contact 110 | func (c *ContactFactory) Tags() []_interface.ITag { 111 | tagIDList, err := c.GetPuppet().TagContactList("") 112 | if err != nil { 113 | log.Errorf("ContactFactory Tags() exception: %s\n", err) 114 | return nil 115 | } 116 | tagList := make([]_interface.ITag, 0, len(tagIDList)) 117 | for _, id := range tagIDList { 118 | tagList = append(tagList, c.GetWechaty().Tag().Load(id)) 119 | } 120 | return tagList 121 | } 122 | -------------------------------------------------------------------------------- /wechaty/factory/friendship.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 7 | "github.com/wechaty/go-wechaty/wechaty/user" 8 | ) 9 | 10 | type FriendshipFactory struct { 11 | _interface.IAccessory 12 | } 13 | 14 | func (m *FriendshipFactory) Load(id string) _interface.IFriendship { 15 | return user.NewFriendship(id, m.IAccessory) 16 | } 17 | 18 | // Search search a Friend by phone or weixin. 19 | func (m *FriendshipFactory) Search(query *schemas.FriendshipSearchCondition) (_interface.IContact, error) { 20 | contactID, err := m.GetPuppet().FriendshipSearch(query) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if contactID == "" { 25 | return nil, nil 26 | } 27 | contact := m.GetWechaty().Contact().Load(contactID) 28 | err = contact.Ready(false) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return contact, nil 33 | } 34 | 35 | // Add send a Friend Request to a `contact` with message `hello`. 36 | // The best practice is to send friend request once per minute. 37 | // Remember not to do this too frequently, or your account may be blocked. 38 | func (m *FriendshipFactory) Add(contact _interface.IContact, hello string) error { 39 | return m.GetPuppet().FriendshipAdd(contact.ID(), hello) 40 | } 41 | 42 | // FromJSON create friendShip by friendshipJson 43 | func (m *FriendshipFactory) FromJSON(payload string) (_interface.IFriendship, error) { 44 | f := new(schemas.FriendshipPayload) 45 | err := json.Unmarshal([]byte(payload), f) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return m.FromPayload(f) 50 | } 51 | 52 | // FromPayload create friendShip by friendshipPayload 53 | func (m *FriendshipFactory) FromPayload(payload *schemas.FriendshipPayload) (_interface.IFriendship, error) { 54 | m.GetPuppet().SetFriendshipPayload(payload.Id, payload) 55 | friendship := m.Load(payload.Id) 56 | err := friendship.Ready() 57 | if err != nil { 58 | return nil, err 59 | } 60 | return friendship, nil 61 | } 62 | -------------------------------------------------------------------------------- /wechaty/factory/image.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | ) 7 | 8 | type ImageFactory struct { 9 | _interface.IAccessory 10 | } 11 | 12 | func (i *ImageFactory) Create(id string) _interface.IImage { 13 | return user.NewImages(id, i.IAccessory) 14 | } 15 | -------------------------------------------------------------------------------- /wechaty/factory/message.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 7 | "github.com/wechaty/go-wechaty/wechaty/user" 8 | ) 9 | 10 | type MessageFactory struct { 11 | _interface.IAccessory 12 | } 13 | 14 | func (m *MessageFactory) Load(id string) _interface.IMessage { 15 | return user.NewMessage(id, m.IAccessory) 16 | } 17 | 18 | // Find find message in cache 19 | func (m *MessageFactory) Find(query interface{}) _interface.IMessage { 20 | var q *schemas.MessageQueryFilter 21 | switch v := query.(type) { 22 | case string: 23 | q = &schemas.MessageQueryFilter{Text: v} 24 | case *schemas.MessageQueryFilter: 25 | q = v 26 | default: 27 | log.Error("not support query type") 28 | // TODO 返回 err 更好 29 | return nil 30 | } 31 | messages := m.FindAll(q) 32 | if len(messages) < 1 { 33 | // TODO 返回 err 更好 34 | return nil 35 | } 36 | if len(messages) > 1 { 37 | log.Errorf("Message FindAll() got more than one(%d) result\n", len(messages)) 38 | } 39 | return messages[0] 40 | } 41 | 42 | // FindAll Find message in cache 43 | func (m *MessageFactory) FindAll(query *schemas.MessageQueryFilter) []_interface.IMessage { 44 | var err error 45 | defer func() { 46 | if err != nil { 47 | log.Errorf("MessageFactory FindAll rejected: %s\n", err) 48 | } 49 | }() 50 | messageIDs, err := m.GetPuppet().MessageSearch(query) 51 | if err != nil { 52 | return nil 53 | } 54 | 55 | async := helper.NewAsync(helper.DefaultWorkerNum) 56 | for _, id := range messageIDs { 57 | id := id 58 | async.AddTask(func() (interface{}, error) { 59 | message := m.Load(id) 60 | return message, message.Ready() 61 | }) 62 | } 63 | 64 | var messages []_interface.IMessage 65 | for _, v := range async.Result() { 66 | if v.Err != nil { 67 | continue 68 | } 69 | messages = append(messages, v.Value.(_interface.IMessage)) 70 | } 71 | return messages 72 | } 73 | -------------------------------------------------------------------------------- /wechaty/factory/room.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 8 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 9 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 10 | "github.com/wechaty/go-wechaty/wechaty/user" 11 | ) 12 | 13 | type RoomFactory struct { 14 | _interface.IAccessory 15 | pool *sync.Map 16 | } 17 | 18 | // NewRoomFactory ... 19 | func NewRoomFactory(accessory _interface.IAccessory) *RoomFactory { 20 | return &RoomFactory{ 21 | IAccessory: accessory, 22 | pool: &sync.Map{}, 23 | } 24 | } 25 | 26 | // Create a new room. 27 | func (r *RoomFactory) Create(contactList []_interface.IContact, topic string) (_interface.IRoom, error) { 28 | if len(contactList) < 2 { 29 | return nil, errors.New("contactList need at least 2 contact to create a new room") 30 | } 31 | contactIDList := make([]string, len(contactList)) 32 | for index, contact := range contactList { 33 | contactIDList[index] = contact.ID() 34 | } 35 | roomID, err := r.GetPuppet().RoomCreate(contactIDList, topic) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return r.Load(roomID), nil 40 | } 41 | 42 | // FindAll query param is string or *schemas.RoomQueryFilter 43 | func (r *RoomFactory) FindAll(query *schemas.RoomQueryFilter) []_interface.IRoom { 44 | roomIDList, err := r.GetPuppet().RoomSearch(query) 45 | if err != nil { 46 | log.Error("RoomFactory err: ", err) 47 | return nil 48 | } 49 | if len(roomIDList) == 0 { 50 | return nil 51 | } 52 | async := helper.NewAsync(helper.DefaultWorkerNum) 53 | for _, id := range roomIDList { 54 | id := id 55 | async.AddTask(func() (interface{}, error) { 56 | room := r.Load(id) 57 | return room, room.Ready(false) 58 | }) 59 | } 60 | var roomList []_interface.IRoom 61 | for _, v := range async.Result() { 62 | if v.Err != nil { 63 | continue 64 | } 65 | roomList = append(roomList, v.Value.(_interface.IRoom)) 66 | } 67 | return roomList 68 | } 69 | 70 | // Find query params is string or *schemas.RoomQueryFilter 71 | func (r *RoomFactory) Find(query interface{}) _interface.IRoom { 72 | var q *schemas.RoomQueryFilter 73 | switch v := query.(type) { 74 | case string: 75 | q = &schemas.RoomQueryFilter{Topic: v} 76 | case *schemas.RoomQueryFilter: 77 | q = v 78 | default: 79 | log.Errorf("not support query type %T\n", query) 80 | // TODO 应该返回 err 81 | return nil 82 | } 83 | roomList := r.FindAll(q) 84 | if len(roomList) == 0 { 85 | return nil 86 | } 87 | for _, room := range roomList { 88 | if r.GetPuppet().RoomValidate(room.ID()) { 89 | return room 90 | } 91 | } 92 | // TODO 应该返回 err 93 | return nil 94 | } 95 | 96 | // Load query param is string 97 | func (r *RoomFactory) Load(id string) _interface.IRoom { 98 | load, ok := r.pool.Load(id) 99 | if ok { 100 | return load.(*user.Room) 101 | } 102 | room := user.NewRoom(id, r.IAccessory) 103 | r.pool.Store(id, room) 104 | return room 105 | } 106 | -------------------------------------------------------------------------------- /wechaty/factory/room_invitation.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 7 | "github.com/wechaty/go-wechaty/wechaty/user" 8 | ) 9 | 10 | type RoomInvitationFactory struct { 11 | _interface.IAccessory 12 | } 13 | 14 | func (r *RoomInvitationFactory) Load(id string) _interface.IRoomInvitation { 15 | return user.NewRoomInvitation(id, r.IAccessory) 16 | } 17 | 18 | func (r *RoomInvitationFactory) FromJSON(s string) (_interface.IRoomInvitation, error) { 19 | payload := new(schemas.RoomInvitationPayload) 20 | err := json.Unmarshal([]byte(s), payload) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return r.FromPayload(payload), nil 25 | } 26 | 27 | func (r *RoomInvitationFactory) FromPayload(payload *schemas.RoomInvitationPayload) _interface.IRoomInvitation { 28 | r.GetPuppet().SetRoomInvitationPayload(payload) 29 | return r.Load(payload.Id) 30 | } 31 | -------------------------------------------------------------------------------- /wechaty/factory/tag.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 5 | "github.com/wechaty/go-wechaty/wechaty/user" 6 | "sync" 7 | ) 8 | 9 | type TagFactory struct { 10 | _interface.IAccessory 11 | pool *sync.Map 12 | } 13 | 14 | // NewTagFactory ... 15 | func NewTagFactory(accessory _interface.IAccessory) *TagFactory { 16 | return &TagFactory{ 17 | IAccessory: accessory, 18 | pool: &sync.Map{}, 19 | } 20 | } 21 | 22 | func (r *TagFactory) Load(id string) _interface.ITag { 23 | load, ok := r.pool.Load(id) 24 | if ok { 25 | return load.(*user.Tag) 26 | } 27 | tag := user.NewTag(id, r.IAccessory) 28 | r.pool.Store(id, tag) 29 | return tag 30 | } 31 | 32 | func (r *TagFactory) Get(tag string) _interface.ITag { 33 | return r.Load(tag) 34 | } 35 | 36 | func (r *TagFactory) Delete(tag _interface.ITag) error { 37 | return r.GetPuppet().TagContactDelete(tag.ID()) 38 | } 39 | -------------------------------------------------------------------------------- /wechaty/factory/url_link.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "errors" 5 | "github.com/otiai10/opengraph" 6 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 7 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 8 | "github.com/wechaty/go-wechaty/wechaty/user" 9 | ) 10 | 11 | var ( 12 | ErrImageUrlOrDescNotFound = errors.New("imgUrl.or.desc.not.found") 13 | ) 14 | 15 | type UrlLinkFactory struct{} 16 | 17 | func (u *UrlLinkFactory) Create(url string) (_interface.IUrlLink, error) { 18 | var og, err = opengraph.Fetch(url) 19 | if err != nil { 20 | return nil, err 21 | } 22 | var payload = &schemas.UrlLinkPayload{ 23 | Url: url, 24 | Title: og.Title, 25 | Description: og.Description, 26 | } 27 | 28 | if len(og.Image) != 0 { 29 | payload.ThumbnailUrl = og.Image[0].URL 30 | } 31 | 32 | if payload.ThumbnailUrl == "" || payload.Description == "" { 33 | return nil, ErrImageUrlOrDescNotFound 34 | } 35 | 36 | return user.NewUrlLink(payload), nil 37 | } 38 | -------------------------------------------------------------------------------- /wechaty/interface/accessory.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import ( 4 | wechatyPuppet "github.com/wechaty/go-wechaty/wechaty-puppet" 5 | ) 6 | 7 | // IAccessory accessory interface 8 | type IAccessory interface { 9 | GetPuppet() wechatyPuppet.IPuppetAbstract 10 | 11 | GetWechaty() IWechaty 12 | } 13 | -------------------------------------------------------------------------------- /wechaty/interface/contact.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | ) 7 | 8 | type IContactFactory interface { 9 | Load(id string) IContact 10 | LoadSelf(id string) IContactSelf 11 | // Find query params is string or *schemas.ContactQueryFilter 12 | // when the parameter is a string type, the name search is used by default 13 | Find(query interface{}) IContact 14 | // FindAll query params is string or *schemas.ContactQueryFilter 15 | // when the parameter is a string type, the name search is used by default 16 | FindAll(query interface{}) []IContact 17 | // Tags get tags for all contact 18 | Tags() []ITag 19 | } 20 | 21 | type IContact interface { 22 | // Ready is For FrameWork ONLY! 23 | Ready(forceSync bool) (err error) 24 | IsReady() bool 25 | // Sync force reload data for Contact, sync data from lowlevel API again. 26 | Sync() error 27 | String() string 28 | ID() string 29 | Name() string 30 | // Say something params {(string | Contact | FileBox | UrlLink | MiniProgram)} 31 | Say(something interface{}) (msg IMessage, err error) 32 | // Friend true for friend of the bot, false for not friend of the bot 33 | Friend() bool 34 | Type() schemas.ContactType 35 | // Star check if the contact is star contact 36 | Star() bool 37 | Gender() schemas.ContactGender 38 | Province() string 39 | City() string 40 | // Avatar get avatar picture file stream 41 | Avatar() *filebox.FileBox 42 | // Self Check if contact is self 43 | Self() bool 44 | // Weixin get the weixin number from a contact 45 | // Sometimes cannot get weixin number due to weixin security mechanism, not recommend. 46 | Weixin() string 47 | // Alias get alias 48 | Alias() string 49 | // SetAlias set alias 50 | SetAlias(newAlias string) 51 | } 52 | -------------------------------------------------------------------------------- /wechaty/interface/contact_self.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 4 | 5 | type IContactSelfFactory interface { 6 | IContactFactory 7 | } 8 | 9 | type IContactSelf interface { 10 | IContact 11 | SetAvatar(box *filebox.FileBox) error 12 | // QRCode get bot qrcode 13 | QRCode() (string, error) 14 | SetName(name string) error 15 | // Signature change bot signature 16 | Signature(signature string) error 17 | } 18 | -------------------------------------------------------------------------------- /wechaty/interface/friendship.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 4 | 5 | type IFriendshipFactory interface { 6 | Load(id string) IFriendship 7 | // Search search a Friend by phone or weixin. 8 | Search(query *schemas.FriendshipSearchCondition) (IContact, error) 9 | // Add send a Friend Request to a `contact` with message `hello`. 10 | // The best practice is to send friend request once per minute. 11 | // Remember not to do this too frequently, or your account may be blocked. 12 | Add(contact IContact, hello string) error 13 | // FromJSON create friendShip by friendshipJson 14 | FromJSON(payload string) (IFriendship, error) 15 | // FromPayload create friendShip by friendshipPayload 16 | FromPayload(payload *schemas.FriendshipPayload) (IFriendship, error) 17 | } 18 | 19 | type IFriendship interface { 20 | Ready() (err error) 21 | IsReady() bool 22 | Contact() IContact 23 | String() string 24 | // Accept accept friend request 25 | Accept() error 26 | Type() schemas.FriendshipType 27 | // Hello get verify message from 28 | Hello() string 29 | // ToJSON get friendShipPayload Json 30 | ToJSON() (string, error) 31 | } 32 | -------------------------------------------------------------------------------- /wechaty/interface/image.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 4 | 5 | type IImageFactory interface { 6 | Create(id string) IImage 7 | } 8 | 9 | type IImage interface { 10 | Thumbnail() (*filebox.FileBox, error) 11 | HD() (*filebox.FileBox, error) 12 | Artwork() (*filebox.FileBox, error) 13 | } 14 | -------------------------------------------------------------------------------- /wechaty/interface/message.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | "time" 7 | ) 8 | 9 | type IMessageFactory interface { 10 | Load(id string) IMessage 11 | // Find find message in cache 12 | Find(query interface{}) IMessage 13 | // FindAll Find message in cache 14 | FindAll(query *schemas.MessageQueryFilter) []IMessage 15 | } 16 | 17 | type IMessage interface { 18 | Ready() (err error) 19 | IsReady() bool 20 | String() string 21 | Room() IRoom 22 | // Type get the type from the message. 23 | Type() schemas.MessageType 24 | // Deprecated: please use Talker() 25 | From() IContact 26 | // Talker Get the talker of a message. 27 | Talker() IContact 28 | Text() string 29 | // Self check if a message is sent by self 30 | Self() bool 31 | Age() time.Duration 32 | // Date sent date 33 | Date() time.Time 34 | // Deprecated: please use Listener() 35 | To() IContact 36 | // Listener Get the destination of the message 37 | // listener() will return nil if a message is in a room 38 | // use Room() to get the room. 39 | Listener() IContact 40 | // ToRecalled Get the recalled message 41 | ToRecalled() (IMessage, error) 42 | // Say reply a Text or Media File message to the sender. 43 | Say(textOrContactOrFileOrUrlOrMini interface{}) (IMessage, error) 44 | // Recall recall a message 45 | Recall() (bool, error) 46 | // MentionList get message mentioned contactList. 47 | MentionList() []IContact 48 | MentionText() string 49 | MentionSelf() bool 50 | Forward(contactOrRoomId string) error 51 | // ToFileBox extract the Media File from the Message, and put it into the FileBox. 52 | ToFileBox() (*filebox.FileBox, error) 53 | // ToImage extract the Image File from the Message, so that we can use different image sizes. 54 | ToImage() (IImage, error) 55 | // ToContact Get Share Card of the Message 56 | // Extract the Contact Card from the Message, and encapsulate it into Contact class 57 | ToContact() (IContact, error) 58 | } 59 | -------------------------------------------------------------------------------- /wechaty/interface/mini_program.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | type IMiniProgram interface { 4 | AppID() string 5 | Description() string 6 | PagePath() string 7 | ThumbUrl() string 8 | Title() string 9 | Username() string 10 | ThumbKey() string 11 | } 12 | -------------------------------------------------------------------------------- /wechaty/interface/room.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | ) 7 | 8 | type IRoomFactory interface { 9 | // Create a new room. 10 | Create(contactList []IContact, topic string) (IRoom, error) 11 | FindAll(query *schemas.RoomQueryFilter) []IRoom 12 | // Find query params is string or *schemas.RoomQueryFilter 13 | // when the parameter is a string type, the room name search is used by default 14 | Find(query interface{}) IRoom 15 | Load(id string) IRoom 16 | } 17 | 18 | type IRoom interface { 19 | // Ready is For FrameWork ONLY! 20 | Ready(forceSync bool) (err error) 21 | IsReady() bool 22 | String() string 23 | ID() string 24 | // MemberAll all contacts in a room 25 | // params nil or string or *schemas.RoomMemberQueryFilter 26 | MemberAll(query interface{}) ([]IContact, error) 27 | // Member Find all contacts in a room, if get many, return the first one. 28 | // query params string or *schemas.RoomMemberQueryFilter 29 | Member(query interface{}) (IContact, error) 30 | // Alias return contact's roomAlias in the room 31 | Alias(contact IContact) (string, error) 32 | // Sync Force reload data for Room, Sync data from puppet API again. 33 | Sync() error 34 | // Say something params {(string | Contact | FileBox | UrlLink | MiniProgram )} 35 | // mentionList @ contact list 36 | Say(something interface{}, mentionList ...IContact) (msg IMessage, err error) 37 | // Add contact in a room 38 | Add(contact IContact) error 39 | // Del delete a contact from the room 40 | // it works only when the bot is the owner of the room 41 | Del(contact IContact) error 42 | // Quit the room itself 43 | Quit() error 44 | // Topic get topic from the room 45 | Topic() string 46 | // Topic set topic from the room 47 | SetTopic(topic string) error 48 | // Announce get announce from the room 49 | Announce() (string, error) 50 | // Announce set announce from the room 51 | // It only works when bot is the owner of the room. 52 | SetAnnounce(text string) error 53 | // QrCode Get QR Code Value of the Room from the room, which can be used as scan and join the room. 54 | QrCode() (string, error) 55 | // Has check if the room has member `contact` 56 | Has(contact IContact) (bool, error) 57 | // Owner get room's owner from the room. 58 | Owner() IContact 59 | // Avatar get avatar from the room. 60 | Avatar() (*filebox.FileBox, error) 61 | } 62 | -------------------------------------------------------------------------------- /wechaty/interface/room_invitation.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 7 | ) 8 | 9 | type IRoomInvitationFactory interface { 10 | Load(id string) IRoomInvitation 11 | FromJSON(s string) (IRoomInvitation, error) 12 | FromPayload(payload *schemas.RoomInvitationPayload) IRoomInvitation 13 | } 14 | 15 | type IRoomInvitation interface { 16 | String() string 17 | ToStringAsync() (string, error) 18 | Accept() error 19 | Inviter() (IContact, error) 20 | Room() (IRoom, error) 21 | Topic() (string, error) 22 | MemberCount() (int, error) 23 | MemberList() ([]IContact, error) 24 | Date() (time.Time, error) 25 | Age() (time.Duration, error) 26 | ToJson() (string, error) 27 | RawPayload() (schemas.RoomInvitationPayload, error) 28 | } 29 | -------------------------------------------------------------------------------- /wechaty/interface/tag.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | type ITagFactory interface { 4 | Load(id string) ITag 5 | Get(tag string) ITag 6 | Delete(tag ITag) error 7 | } 8 | 9 | type ITag interface { 10 | ID() string 11 | Add(to IContact) error 12 | Remove(from IContact) error 13 | } 14 | -------------------------------------------------------------------------------- /wechaty/interface/url_link.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | type IUrlLinkFactory interface { 4 | Create(url string) (IUrlLink, error) 5 | } 6 | 7 | type IUrlLink interface { 8 | String() string 9 | Url() string 10 | Title() string 11 | ThumbnailUrl() string 12 | Description() string 13 | } 14 | -------------------------------------------------------------------------------- /wechaty/interface/wechaty.go: -------------------------------------------------------------------------------- 1 | package _interface 2 | 3 | // IWechaty interface 4 | type IWechaty interface { 5 | Room() IRoomFactory 6 | Contact() IContactFactory 7 | Message() IMessageFactory 8 | Tag() ITagFactory 9 | Friendship() IFriendshipFactory 10 | Image() IImageFactory 11 | UserSelf() IContactSelf 12 | } 13 | -------------------------------------------------------------------------------- /wechaty/option.go: -------------------------------------------------------------------------------- 1 | package wechaty 2 | 3 | import ( 4 | wp "github.com/wechaty/go-wechaty/wechaty-puppet" 5 | puppetservice "github.com/wechaty/go-wechaty/wechaty-puppet-service" 6 | mc "github.com/wechaty/go-wechaty/wechaty-puppet/memory-card" 7 | ) 8 | 9 | // Option wechaty option 10 | type Option struct { 11 | // wechaty name 12 | name string 13 | // puppet instance 14 | puppet wp.IPuppetAbstract 15 | // puppet option 16 | puppetOption wp.Option 17 | // io token 18 | ioToken string 19 | // memory card 20 | memoryCard mc.IMemoryCard 21 | 22 | // puppet-serviceOptions 23 | puppetServiceOptions puppetservice.Options 24 | } 25 | 26 | // OptionFn func 27 | type OptionFn func(opts *Option) 28 | 29 | // WithName with name 30 | func WithName(name string) OptionFn { 31 | return func(opt *Option) { 32 | opt.name = name 33 | } 34 | } 35 | 36 | // WithPuppet with puppet impl 37 | func WithPuppet(puppet wp.IPuppetAbstract) OptionFn { 38 | return func(opt *Option) { 39 | opt.puppet = puppet 40 | } 41 | } 42 | 43 | // WithPuppetOption with puppet option 44 | func WithPuppetOption(puppetOption wp.Option) OptionFn { 45 | return func(opt *Option) { 46 | opt.puppetOption = puppetOption 47 | } 48 | } 49 | 50 | // WithIOToken with io token 51 | func WithIOToken(ioToken string) OptionFn { 52 | return func(opt *Option) { 53 | opt.ioToken = ioToken 54 | } 55 | } 56 | 57 | // WithMemoryCard with memory card 58 | func WithMemoryCard(memoryCard mc.IMemoryCard) OptionFn { 59 | return func(opt *Option) { 60 | opt.memoryCard = memoryCard 61 | } 62 | } 63 | 64 | // WithPuppetServiceOptions puppet service options 65 | func WithPuppetServiceOptions(puppetServiceOptions puppetservice.Options) OptionFn { 66 | return func(opts *Option) { 67 | opts.puppetServiceOptions = puppetServiceOptions 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /wechaty/plugin.go: -------------------------------------------------------------------------------- 1 | package wechaty 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | "reflect" 7 | "runtime/debug" 8 | "sync" 9 | ) 10 | 11 | // PluginEvent stores the event name and the callback function. 12 | type PluginEvent struct { 13 | name schemas.PuppetEventName 14 | f interface{} // callback function 15 | } 16 | 17 | // Plugin ... 18 | type Plugin struct { 19 | Wechaty *Wechaty 20 | mu sync.RWMutex 21 | enable bool 22 | events []PluginEvent 23 | } 24 | 25 | // NewPlugin ... 26 | func NewPlugin() *Plugin { 27 | p := &Plugin{ 28 | enable: true, 29 | } 30 | return p 31 | } 32 | 33 | // SetEnable enable or disable a plugin. 34 | func (p *Plugin) SetEnable(value bool) { 35 | p.mu.Lock() 36 | p.enable = value 37 | p.mu.Unlock() 38 | } 39 | 40 | // IsEnable returns whether the plugin is activated. 41 | func (p *Plugin) IsEnable() bool { 42 | p.mu.RLock() 43 | defer p.mu.RUnlock() 44 | return p.enable 45 | } 46 | 47 | func (p *Plugin) registerPluginEvent(wechaty *Wechaty) { 48 | for _, pluginEvent := range p.events { 49 | pluginEvent := pluginEvent 50 | f := func(data ...interface{}) { 51 | defer func() { 52 | if err := recover(); err != nil { 53 | log.Error("panic: ", err) 54 | log.Error(string(debug.Stack())) 55 | wechaty.events.Emit(schemas.PuppetEventNameError, NewContext(), fmt.Errorf("panic: event %s %v", pluginEvent.name, err)) 56 | } 57 | }() 58 | values := make([]reflect.Value, 0, len(data)) 59 | for _, v := range data { 60 | values = append(values, reflect.ValueOf(v)) 61 | } 62 | // check whether the plugin can be used. 63 | if values[0].Interface().(*Context).IsActive(p) && 64 | !values[0].Interface().(*Context).abort { 65 | _ = reflect.ValueOf(pluginEvent.f).Call(values) 66 | } 67 | } 68 | wechaty.registerEvent(pluginEvent.name, f) 69 | } 70 | } 71 | 72 | // OnScan ... 73 | func (p *Plugin) OnScan(f EventScan) *Plugin { 74 | p.events = append(p.events, PluginEvent{ 75 | name: schemas.PuppetEventNameScan, 76 | f: f, 77 | }) 78 | return p 79 | } 80 | 81 | // OnLogin ... 82 | func (p *Plugin) OnLogin(f EventLogin) *Plugin { 83 | p.events = append(p.events, PluginEvent{ 84 | name: schemas.PuppetEventNameLogin, 85 | f: f, 86 | }) 87 | return p 88 | } 89 | 90 | // OnMessage ... 91 | func (p *Plugin) OnMessage(f EventMessage) *Plugin { 92 | p.events = append(p.events, PluginEvent{ 93 | name: schemas.PuppetEventNameMessage, 94 | f: f, 95 | }) 96 | return p 97 | } 98 | 99 | // OnDong ... 100 | func (p *Plugin) OnDong(f EventDong) *Plugin { 101 | p.events = append(p.events, PluginEvent{ 102 | name: schemas.PuppetEventNameDong, 103 | f: f, 104 | }) 105 | return p 106 | } 107 | 108 | // OnError ... 109 | func (p *Plugin) OnError(f EventError) *Plugin { 110 | p.events = append(p.events, PluginEvent{ 111 | name: schemas.PuppetEventNameError, 112 | f: f, 113 | }) 114 | return p 115 | } 116 | 117 | // OnFriendship ... 118 | func (p *Plugin) OnFriendship(f EventFriendship) *Plugin { 119 | p.events = append(p.events, PluginEvent{ 120 | name: schemas.PuppetEventNameFriendship, 121 | f: f, 122 | }) 123 | return p 124 | } 125 | 126 | // OnHeartbeat ... 127 | func (p *Plugin) OnHeartbeat(f EventHeartbeat) *Plugin { 128 | p.events = append(p.events, PluginEvent{ 129 | name: schemas.PuppetEventNameHeartbeat, 130 | f: f, 131 | }) 132 | return p 133 | } 134 | 135 | // OnLogout ... 136 | func (p *Plugin) OnLogout(f EventLogout) *Plugin { 137 | p.events = append(p.events, PluginEvent{ 138 | name: schemas.PuppetEventNameLogout, 139 | f: f, 140 | }) 141 | return p 142 | } 143 | 144 | // OnReady ... 145 | func (p *Plugin) OnReady(f EventReady) *Plugin { 146 | p.events = append(p.events, PluginEvent{ 147 | name: schemas.PuppetEventNameReady, 148 | f: f, 149 | }) 150 | return p 151 | } 152 | 153 | // OnRoomInvite ... 154 | func (p *Plugin) OnRoomInvite(f EventRoomInvite) *Plugin { 155 | p.events = append(p.events, PluginEvent{ 156 | name: schemas.PuppetEventNameRoomInvite, 157 | f: f, 158 | }) 159 | return p 160 | } 161 | 162 | // OnRoomJoin ... 163 | func (p *Plugin) OnRoomJoin(f EventRoomJoin) *Plugin { 164 | p.events = append(p.events, PluginEvent{ 165 | name: schemas.PuppetEventNameRoomJoin, 166 | f: f, 167 | }) 168 | return p 169 | } 170 | 171 | // OnRoomLeave ... 172 | func (p *Plugin) OnRoomLeave(f EventRoomLeave) *Plugin { 173 | p.events = append(p.events, PluginEvent{ 174 | name: schemas.PuppetEventNameRoomLeave, 175 | f: f, 176 | }) 177 | return p 178 | } 179 | 180 | // OnRoomTopic ... 181 | func (p *Plugin) OnRoomTopic(f EventRoomTopic) *Plugin { 182 | p.events = append(p.events, PluginEvent{ 183 | name: schemas.PuppetEventNameRoomTopic, 184 | f: f, 185 | }) 186 | return p 187 | } 188 | 189 | // OnStart ... 190 | func (p *Plugin) OnStart(f EventStart) *Plugin { 191 | p.events = append(p.events, PluginEvent{ 192 | name: schemas.PuppetEventNameStart, 193 | f: f, 194 | }) 195 | return p 196 | } 197 | 198 | // OnStop ... 199 | func (p *Plugin) OnStop(f EventStop) *Plugin { 200 | p.events = append(p.events, PluginEvent{ 201 | name: schemas.PuppetEventNameStop, 202 | f: f, 203 | }) 204 | return p 205 | } 206 | -------------------------------------------------------------------------------- /wechaty/user/config.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import logger "github.com/wechaty/go-wechaty/wechaty-puppet/log" 4 | 5 | var log = logger.L.WithField("module", "wechaty/user") 6 | -------------------------------------------------------------------------------- /wechaty/user/contact.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Go Wechaty - https://github.com/wechaty/go-wechaty 3 | * 4 | * Authors: Huan LI (李卓桓) 5 | * Bojie LI (李博杰) 6 | * 7 | * 2020-now @ Copyright Wechaty 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the 'License'); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an 'AS IS' BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | package user 23 | 24 | import ( 25 | "fmt" 26 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 27 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 28 | "github.com/wechaty/go-wechaty/wechaty/config" 29 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 30 | ) 31 | 32 | type Contact struct { 33 | _interface.IAccessory 34 | 35 | Id string 36 | payload *schemas.ContactPayload 37 | } 38 | 39 | // NewContact ... 40 | func NewContact(id string, accessory _interface.IAccessory) *Contact { 41 | return &Contact{ 42 | IAccessory: accessory, 43 | Id: id, 44 | } 45 | } 46 | 47 | // Ready is For FrameWork ONLY! 48 | func (c *Contact) Ready(forceSync bool) (err error) { 49 | if !forceSync && c.IsReady() { 50 | return nil 51 | } 52 | 53 | c.payload, err = c.GetPuppet().ContactPayload(c.Id) 54 | if err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | 60 | func (c *Contact) IsReady() bool { 61 | return c.payload != nil 62 | } 63 | 64 | // Sync force reload data for Contact, sync data from lowlevel API again. 65 | func (c *Contact) Sync() error { 66 | err := c.GetPuppet().DirtyPayload(schemas.PayloadTypeContact, c.Id) 67 | if err != nil { 68 | return err 69 | } 70 | return c.Ready(true) 71 | } 72 | 73 | func (c *Contact) String() string { 74 | return fmt.Sprintf("Contact<%s>", c.identity()) 75 | } 76 | 77 | func (c *Contact) identity() string { 78 | identity := "loading..." 79 | if c.payload.Alias != "" { 80 | identity = c.payload.Alias 81 | } else if c.payload.Name != "" { 82 | identity = c.payload.Name 83 | } else if c.Id != "" { 84 | identity = c.Id 85 | } 86 | return identity 87 | } 88 | 89 | func (c *Contact) ID() string { 90 | return c.Id 91 | } 92 | 93 | func (c *Contact) Name() string { 94 | if c.payload == nil { 95 | return "" 96 | } 97 | return c.payload.Name 98 | } 99 | 100 | // Say something params {(string | Contact | FileBox | UrlLink | MiniProgram)} 101 | func (c *Contact) Say(something interface{}) (msg _interface.IMessage, err error) { 102 | var msgID string 103 | switch v := something.(type) { 104 | case string: 105 | msgID, err = c.GetPuppet().MessageSendText(c.Id, v) 106 | case *Contact: 107 | msgID, err = c.GetPuppet().MessageSendContact(c.Id, v.Id) 108 | case *filebox.FileBox: 109 | msgID, err = c.GetPuppet().MessageSendFile(c.Id, v) 110 | case *UrlLink: 111 | msgID, err = c.GetPuppet().MessageSendURL(c.Id, v.payload) 112 | case *MiniProgram: 113 | msgID, err = c.GetPuppet().MessageSendMiniProgram(c.Id, v.payload) 114 | default: 115 | return nil, fmt.Errorf("unsupported arg: %v", something) 116 | } 117 | if err != nil { 118 | return nil, err 119 | } 120 | if msgID == "" { 121 | return nil, nil 122 | } 123 | msg = c.GetWechaty().Message().Load(msgID) 124 | return msg, msg.Ready() 125 | } 126 | 127 | // Friend true for friend of the bot, false for not friend of the bot 128 | func (c *Contact) Friend() bool { 129 | return c.payload.Friend 130 | } 131 | 132 | // Type contact type 133 | func (c *Contact) Type() schemas.ContactType { 134 | return c.payload.Type 135 | } 136 | 137 | // Star check if the contact is star contact 138 | func (c *Contact) Star() bool { 139 | return c.payload.Star 140 | } 141 | 142 | // Gender gender 143 | func (c *Contact) Gender() schemas.ContactGender { 144 | return c.payload.Gender 145 | } 146 | 147 | // Province Get the region 'province' from a contact 148 | func (c *Contact) Province() string { 149 | return c.payload.Province 150 | } 151 | 152 | // City Get the region 'city' from a contact 153 | func (c *Contact) City() string { 154 | return c.payload.City 155 | } 156 | 157 | // Avatar get avatar picture file stream 158 | func (c *Contact) Avatar() *filebox.FileBox { 159 | avatar, err := c.GetPuppet().ContactAvatar(c.Id) 160 | if err != nil { 161 | log.Errorf("Contact Avatar() exception: %s\n", err) 162 | return config.QRCodeForChatie() 163 | } 164 | return avatar 165 | } 166 | 167 | // Self Check if contact is self 168 | func (c *Contact) Self() bool { 169 | return c.GetPuppet().SelfID() == c.Id 170 | } 171 | 172 | // Weixin get the weixin number from a contact 173 | // Sometimes cannot get weixin number due to weixin security mechanism, not recommend. 174 | func (c *Contact) Weixin() string { 175 | return c.payload.WeiXin 176 | } 177 | 178 | // Alias get alias 179 | func (c *Contact) Alias() string { 180 | return c.payload.Alias 181 | } 182 | 183 | // SetAlias set alias 184 | func (c *Contact) SetAlias(newAlias string) { 185 | var err error 186 | defer func() { 187 | if err != nil { 188 | log.Errorf("Contact SetAlias(%s) rejected: %s\n", newAlias, err) 189 | } 190 | }() 191 | err = c.GetPuppet().SetContactAlias(c.Id, newAlias) 192 | if err != nil { 193 | return 194 | } 195 | err = c.GetPuppet().DirtyPayload(schemas.PayloadTypeContact, c.Id) 196 | if err != nil { 197 | log.Error("SetAlias DirtyPayload err:", err) 198 | } 199 | c.payload, err = c.GetPuppet().ContactPayload(c.Id) 200 | if err != nil { 201 | log.Error("SetAlias ContactPayload err:", err) 202 | return 203 | } 204 | if c.payload.Alias != newAlias { 205 | log.Errorf("Contact SetAlias(%s) sync with server fail: set(%s) is not equal to get(%s)\n", newAlias, newAlias, c.payload.Alias) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /wechaty/user/contact_self.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 7 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 8 | ) 9 | 10 | type ContactSelf struct { 11 | *Contact 12 | } 13 | 14 | // NewContactSelf ... 15 | func NewContactSelf(id string, accessory _interface.IAccessory) *ContactSelf { 16 | return &ContactSelf{&Contact{ 17 | IAccessory: accessory, 18 | Id: id, 19 | }} 20 | } 21 | 22 | // SetAvatar SET the avatar for a bot 23 | func (c *ContactSelf) SetAvatar(box *filebox.FileBox) error { 24 | if c.Id != c.GetPuppet().SelfID() { 25 | return errors.New("set avatar only available for user self") 26 | } 27 | return c.GetPuppet().SetContactAvatar(c.Id, box) 28 | } 29 | 30 | // QRCode get bot qrcode 31 | func (c *ContactSelf) QRCode() (string, error) { 32 | puppetId := c.GetPuppet().SelfID() 33 | if puppetId == "" { 34 | return "", errors.New("can not get qrcode, user might be either not logged in or already logged out") 35 | } 36 | if c.Id != puppetId { 37 | return "", errors.New("only can get qrcode for the login userself") 38 | } 39 | code, err := c.GetPuppet().ContactSelfQRCode() 40 | if err != nil { 41 | return "", err 42 | } 43 | return code, nil 44 | } 45 | 46 | // SetName change bot name 47 | func (c *ContactSelf) SetName(name string) error { 48 | puppetId := c.GetPuppet().SelfID() 49 | if puppetId == "" { 50 | return errors.New("can not set name for user self, user might be either not logged in or already logged out") 51 | } 52 | if c.Id != puppetId { 53 | return errors.New("only can set name for user self") 54 | } 55 | err := c.GetPuppet().SetContactSelfName(name) 56 | if err != nil { 57 | return err 58 | } 59 | _ = c.Sync() 60 | return nil 61 | } 62 | 63 | // Signature change bot signature 64 | func (c *ContactSelf) Signature(signature string) error { 65 | puppetId := c.GetPuppet().SelfID() 66 | if puppetId == "" { 67 | return errors.New("can not set signature for user self, user might be either not logged in or already logged out") 68 | } 69 | if c.Id != puppetId { 70 | return errors.New("only can change signature for user self") 71 | } 72 | return c.GetPuppet().SetContactSelfSignature(signature) 73 | } 74 | -------------------------------------------------------------------------------- /wechaty/user/friendship.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 8 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 9 | ) 10 | 11 | type Friendship struct { 12 | _interface.IAccessory 13 | id string 14 | payload *schemas.FriendshipPayload 15 | } 16 | 17 | // NewFriendship ... 18 | func NewFriendship(id string, accessory _interface.IAccessory) *Friendship { 19 | return &Friendship{ 20 | IAccessory: accessory, 21 | id: id, 22 | } 23 | } 24 | 25 | // Ready ... 26 | func (f *Friendship) Ready() (err error) { 27 | if f.IsReady() { 28 | return nil 29 | } 30 | f.payload, err = f.GetPuppet().FriendshipPayload(f.id) 31 | if err != nil { 32 | return err 33 | } 34 | return f.Contact().Ready(false) 35 | } 36 | 37 | // IsReady ... 38 | func (f *Friendship) IsReady() bool { 39 | return f.payload != nil 40 | } 41 | 42 | // Contact ... 43 | func (f *Friendship) Contact() _interface.IContact { 44 | return f.GetWechaty().Contact().Load(f.payload.ContactId) 45 | } 46 | 47 | func (f *Friendship) String() string { 48 | if f.payload == nil { 49 | return "Friendship not payload" 50 | } 51 | return fmt.Sprintf("Friendship#%s<%s>", f.payload.Type, f.payload.ContactId) 52 | } 53 | 54 | // Accept friend request 55 | func (f *Friendship) Accept() error { 56 | if f.payload.Type != schemas.FriendshipTypeReceive { 57 | return fmt.Errorf("accept() need type to be FriendshipType.Receive, but it got a %s", f.payload.Type) 58 | } 59 | err := f.GetPuppet().FriendshipAccept(f.id) 60 | if err != nil { 61 | return err 62 | } 63 | return f.Contact().Sync() 64 | } 65 | 66 | func (f *Friendship) Type() schemas.FriendshipType { 67 | return f.payload.Type 68 | } 69 | 70 | // Hello get verify message from 71 | func (f *Friendship) Hello() string { 72 | return f.payload.Hello 73 | } 74 | 75 | // toJSON get friendShipPayload Json 76 | func (f *Friendship) ToJSON() (string, error) { 77 | marshal, err := json.Marshal(f.payload) 78 | if err != nil { 79 | return "", err 80 | } 81 | return string(marshal), nil 82 | } 83 | -------------------------------------------------------------------------------- /wechaty/user/image.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | "github.com/wechaty/go-wechaty/wechaty/interface" 7 | ) 8 | 9 | type Images struct { 10 | _interface.IAccessory 11 | ImageId string 12 | } 13 | 14 | // NewImages create image struct 15 | func NewImages(id string, accessory _interface.IAccessory) *Images { 16 | if accessory.GetPuppet() == nil { 17 | panic("Image class can not be instantiated without a puppet!") 18 | } 19 | return &Images{accessory, id} 20 | } 21 | 22 | // Thumbnail message thumbnail images 23 | func (img *Images) Thumbnail() (*filebox.FileBox, error) { 24 | return img.IAccessory.GetPuppet().MessageImage(img.ImageId, schemas.ImageTypeThumbnail) 25 | } 26 | 27 | // HD message hd images 28 | func (img *Images) HD() (*filebox.FileBox, error) { 29 | return img.IAccessory.GetPuppet().MessageImage(img.ImageId, schemas.ImageTypeHD) 30 | } 31 | 32 | // Artwork message artwork images 33 | func (img *Images) Artwork() (*filebox.FileBox, error) { 34 | return img.IAccessory.GetPuppet().MessageImage(img.ImageId, schemas.ImageTypeArtwork) 35 | } 36 | -------------------------------------------------------------------------------- /wechaty/user/location.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 6 | ) 7 | 8 | type Location struct { 9 | payload *schemas.LocationPayload 10 | } 11 | 12 | func NewLocation(payload *schemas.LocationPayload) *Location { 13 | return &Location{ 14 | payload: payload, 15 | } 16 | } 17 | 18 | func (l *Location) Payload() schemas.LocationPayload { 19 | return *l.payload 20 | } 21 | 22 | func (l *Location) String() string { 23 | return fmt.Sprintf("Location<%s>", l.payload.Name) 24 | } 25 | 26 | func (l *Location) Address() string { 27 | return l.payload.Address 28 | } 29 | 30 | func (l *Location) Latitude() float64 { 31 | return l.payload.Latitude 32 | } 33 | 34 | func (l *Location) longitude() float64 { 35 | return l.payload.Longitude 36 | } 37 | 38 | func (l *Location) Name() string { 39 | return l.payload.Name 40 | } 41 | 42 | func (l *Location) Accuracy() float32 { 43 | return l.payload.Accuracy 44 | } 45 | -------------------------------------------------------------------------------- /wechaty/user/mini_program.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Go Wechaty - https://github.com/wechaty/go-wechaty 3 | * 4 | * Authors: Huan LI (李卓桓) 5 | * Chao Fei () 6 | * 7 | * 2020-now @ Copyright Wechaty 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the 'License'); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an 'AS IS' BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | package user 23 | 24 | import "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 25 | 26 | type MiniProgram struct { 27 | payload *schemas.MiniProgramPayload 28 | } 29 | 30 | func NewMiniProgram(payload *schemas.MiniProgramPayload) *MiniProgram { 31 | return &MiniProgram{payload: payload} 32 | } 33 | 34 | func (mp *MiniProgram) AppID() string { 35 | if mp.payloadIsNil() { 36 | return "" 37 | } 38 | return mp.payload.Appid 39 | } 40 | 41 | func (mp *MiniProgram) Description() string { 42 | if mp.payloadIsNil() { 43 | return "" 44 | } 45 | return mp.payload.Description 46 | } 47 | 48 | func (mp *MiniProgram) PagePath() string { 49 | if mp.payloadIsNil() { 50 | return "" 51 | } 52 | return mp.payload.PagePath 53 | } 54 | 55 | func (mp *MiniProgram) ThumbUrl() string { 56 | if mp.payloadIsNil() { 57 | return "" 58 | } 59 | return mp.payload.ThumbUrl 60 | } 61 | 62 | func (mp *MiniProgram) Title() string { 63 | if mp.payloadIsNil() { 64 | return "" 65 | } 66 | return mp.payload.Title 67 | } 68 | 69 | func (mp *MiniProgram) Username() string { 70 | if mp.payloadIsNil() { 71 | return "" 72 | } 73 | return mp.payload.Username 74 | } 75 | 76 | func (mp *MiniProgram) ThumbKey() string { 77 | if mp.payloadIsNil() { 78 | return "" 79 | } 80 | return mp.payload.ThumbKey 81 | } 82 | 83 | func (mp *MiniProgram) ShareId() string { 84 | if mp.payloadIsNil() { 85 | return "" 86 | } 87 | return mp.payload.ShareId 88 | } 89 | 90 | func (mp *MiniProgram) IconUrl() string { 91 | if mp.payloadIsNil() { 92 | return "" 93 | } 94 | return mp.payload.IconUrl 95 | } 96 | 97 | func (mp *MiniProgram) Payload() schemas.MiniProgramPayload { 98 | return *mp.payload 99 | } 100 | 101 | func (mp *MiniProgram) payloadIsNil() bool { 102 | return mp.payload == nil 103 | } 104 | -------------------------------------------------------------------------------- /wechaty/user/room.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/wechaty/go-wechaty/wechaty-puppet/filebox" 8 | "github.com/wechaty/go-wechaty/wechaty-puppet/helper" 9 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 10 | "github.com/wechaty/go-wechaty/wechaty/config" 11 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 12 | ) 13 | 14 | type Room struct { 15 | id string 16 | payLoad *schemas.RoomPayload 17 | _interface.IAccessory 18 | } 19 | 20 | // NewRoom ... 21 | func NewRoom(id string, accessory _interface.IAccessory) *Room { 22 | return &Room{ 23 | id: id, 24 | IAccessory: accessory, 25 | } 26 | } 27 | 28 | // Ready is For FrameWork ONLY! 29 | func (r *Room) Ready(forceSync bool) (err error) { 30 | if !forceSync && r.IsReady() { 31 | return nil 32 | } 33 | 34 | r.payLoad, err = r.GetPuppet().RoomPayload(r.id) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | memberIDs, err := r.GetPuppet().RoomMemberList(r.id) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | async := helper.NewAsync(helper.DefaultWorkerNum) 45 | for _, id := range memberIDs { 46 | id := id 47 | async.AddTask(func() (interface{}, error) { 48 | return nil, r.GetWechaty().Contact().Load(id).Ready(false) 49 | }) 50 | } 51 | _ = async.Result() 52 | 53 | return nil 54 | } 55 | 56 | func (r *Room) IsReady() bool { 57 | return r.payLoad != nil 58 | } 59 | 60 | func (r *Room) String() string { 61 | str := "loading" 62 | if r.payLoad.Topic != "" { 63 | str = r.payLoad.Topic 64 | } 65 | return fmt.Sprintf("Room<%s>", str) 66 | } 67 | 68 | func (r *Room) ID() string { 69 | return r.id 70 | } 71 | 72 | // MemberAll all contacts in a room 73 | // params nil or string or *schemas.RoomMemberQueryFilter 74 | func (r *Room) MemberAll(query interface{}) ([]_interface.IContact, error) { 75 | if query == nil { 76 | return r.memberList() 77 | } 78 | idList, err := r.GetPuppet().RoomMemberSearch(r.id, query) 79 | if err != nil { 80 | return nil, err 81 | } 82 | var contactList []_interface.IContact 83 | for _, id := range idList { 84 | contact := r.GetWechaty().Contact().Load(id) 85 | if err := contact.Ready(false); err != nil { 86 | return nil, err 87 | } 88 | contactList = append(contactList, contact) 89 | } 90 | return contactList, nil 91 | } 92 | 93 | // Member Find all contacts in a room, if get many, return the first one. 94 | // query params string or RoomMemberQueryFilter 95 | func (r *Room) Member(query interface{}) (_interface.IContact, error) { 96 | memberList, err := r.MemberAll(query) 97 | if err != nil { 98 | return nil, err 99 | } 100 | if len(memberList) == 0 { 101 | return nil, nil 102 | } 103 | return memberList[0], nil 104 | } 105 | 106 | // get all room member from the room 107 | func (r *Room) memberList() ([]_interface.IContact, error) { 108 | memberIDList, err := r.GetPuppet().RoomMemberList(r.id) 109 | if err != nil { 110 | return nil, err 111 | } 112 | if len(memberIDList) == 0 { 113 | return nil, nil 114 | } 115 | var contactList []_interface.IContact 116 | for _, id := range memberIDList { 117 | contactList = append(contactList, r.GetWechaty().Contact().Load(id)) 118 | } 119 | return contactList, nil 120 | } 121 | 122 | // Alias return contact's roomAlias in the room 123 | func (r *Room) Alias(contact _interface.IContact) (string, error) { 124 | memberPayload, err := r.GetPuppet().RoomMemberPayload(r.id, contact.ID()) 125 | if err != nil { 126 | return "", err 127 | } 128 | return memberPayload.RoomAlias, nil 129 | } 130 | 131 | // Sync Force reload data for Room, Sync data from puppet API again. 132 | func (r *Room) Sync() error { 133 | if err := r.GetPuppet().DirtyPayload(schemas.PayloadTypeRoom, r.id); err != nil { 134 | return err 135 | } 136 | if err := r.GetPuppet().DirtyPayload(schemas.PayloadTypeRoomMember, r.id); err != nil { 137 | return err 138 | } 139 | return r.Ready(true) 140 | } 141 | 142 | // Say something params {(string | Contact | FileBox | UrlLink | MiniProgram )} 143 | // mentionList @ contact list 144 | func (r *Room) Say(something interface{}, mentionList ..._interface.IContact) (msg _interface.IMessage, err error) { 145 | var msgID string 146 | switch v := something.(type) { 147 | case string: 148 | msgID, err = r.sayText(v, mentionList...) 149 | case *Contact: 150 | msgID, err = r.GetPuppet().MessageSendContact(r.id, v.Id) 151 | case *filebox.FileBox: 152 | msgID, err = r.GetPuppet().MessageSendFile(r.id, v) 153 | case *UrlLink: 154 | msgID, err = r.GetPuppet().MessageSendURL(r.id, v.payload) 155 | case *MiniProgram: 156 | msgID, err = r.GetPuppet().MessageSendMiniProgram(r.id, v.payload) 157 | default: 158 | return nil, fmt.Errorf("unsupported arg: %v", something) 159 | } 160 | if err != nil { 161 | return nil, err 162 | } 163 | if msgID == "" { 164 | return nil, nil 165 | } 166 | msg = r.GetWechaty().Message().Load(msgID) 167 | return msg, msg.Ready() 168 | } 169 | 170 | func (r *Room) sayText(text string, mentionList ..._interface.IContact) (string, error) { 171 | var mentionIDList []string 172 | if len(mentionList) > 0 { 173 | mentionAlias := make([]string, 0, len(mentionList)) 174 | const atSeparator = config.FourPerEmSpace 175 | for _, contact := range mentionList { 176 | mentionIDList = append(mentionIDList, contact.ID()) 177 | alias, _ := r.Alias(contact) 178 | if alias == "" { 179 | alias = contact.Name() 180 | } 181 | alias = strings.ReplaceAll(alias, " ", atSeparator) 182 | mentionAlias = append(mentionAlias, "@"+alias) 183 | } 184 | text = strings.Join(mentionAlias, atSeparator) + " " + text 185 | } 186 | return r.GetPuppet().MessageSendText(r.id, text, mentionIDList...) 187 | } 188 | 189 | // Add contact in a room 190 | func (r *Room) Add(contact _interface.IContact) error { 191 | return r.GetPuppet().RoomAdd(r.id, contact.ID()) 192 | } 193 | 194 | // Del delete a contact from the room 195 | // it works only when the bot is the owner of the room 196 | func (r *Room) Del(contact _interface.IContact) error { 197 | return r.GetPuppet().RoomDel(r.id, contact.ID()) 198 | } 199 | 200 | // Quit the room itself 201 | func (r *Room) Quit() error { 202 | return r.GetPuppet().RoomQuit(r.id) 203 | } 204 | 205 | // Topic get topic from the room 206 | func (r *Room) Topic() string { 207 | if r.payLoad.Topic != "" { 208 | return r.payLoad.Topic 209 | } 210 | memberList, err := r.memberList() 211 | if err != nil { 212 | log.Error("Room Topic err: ", err) 213 | return "" 214 | } 215 | i := 1 216 | defaultTopic := "" 217 | for _, member := range memberList { 218 | if i >= 3 { 219 | break 220 | } 221 | if member.ID() == r.GetPuppet().SelfID() { 222 | continue 223 | } 224 | defaultTopic += member.Name() + "," 225 | i++ 226 | } 227 | return strings.TrimRight(defaultTopic, ",") 228 | } 229 | 230 | // SetTopic set topic from the room 231 | func (r *Room) SetTopic(topic string) error { 232 | return r.GetPuppet().SetRoomTopic(r.id, topic) 233 | } 234 | 235 | // Announce get announce from the room 236 | func (r *Room) Announce() (string, error) { 237 | return r.GetPuppet().RoomAnnounce(r.id) 238 | } 239 | 240 | // SetAnnounce set announce from the room 241 | // It only works when bot is the owner of the room. 242 | func (r *Room) SetAnnounce(text string) error { 243 | return r.GetPuppet().SetRoomAnnounce(r.id, text) 244 | } 245 | 246 | // QrCode Get QR Code Value of the Room from the room, which can be used as scan and join the room. 247 | func (r *Room) QrCode() (string, error) { 248 | return r.GetPuppet().RoomQRCode(r.id) 249 | } 250 | 251 | // Has check if the room has member `contact` 252 | func (r *Room) Has(contact _interface.IContact) (bool, error) { 253 | memberIDList, err := r.GetPuppet().RoomMemberList(r.id) 254 | if err != nil { 255 | return false, err 256 | } 257 | for _, id := range memberIDList { 258 | if id == contact.ID() { 259 | return true, nil 260 | } 261 | } 262 | return false, nil 263 | } 264 | 265 | // Owner get room's owner from the room. 266 | func (r *Room) Owner() _interface.IContact { 267 | if r.payLoad.OwnerId == "" { 268 | return nil 269 | } 270 | return r.GetWechaty().Contact().Load(r.payLoad.OwnerId) 271 | } 272 | 273 | // Avatar get avatar from the room. 274 | func (r *Room) Avatar() (*filebox.FileBox, error) { 275 | return r.GetPuppet().RoomAvatar(r.id) 276 | } 277 | -------------------------------------------------------------------------------- /wechaty/user/room_invitation.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 9 | _interface "github.com/wechaty/go-wechaty/wechaty/interface" 10 | ) 11 | 12 | type RoomInvitation struct { 13 | _interface.IAccessory 14 | id string 15 | } 16 | 17 | // NewRoomInvitation ... 18 | func NewRoomInvitation(id string, accessory _interface.IAccessory) *RoomInvitation { 19 | return &RoomInvitation{ 20 | IAccessory: accessory, 21 | id: id, 22 | } 23 | } 24 | 25 | func (ri *RoomInvitation) String() string { 26 | id := "loading" 27 | if ri.id != "" { 28 | id = ri.id 29 | } 30 | return fmt.Sprintf("RoomInvitation#%s", id) 31 | } 32 | 33 | func (ri *RoomInvitation) ToStringAsync() (string, error) { 34 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 35 | if err != nil { 36 | return "", err 37 | } 38 | return fmt.Sprintf("RoomInvitation#%s<%s,%s>", ri.id, payload.Topic, payload.InviterId), nil 39 | } 40 | 41 | // Accept Room Invitation 42 | func (ri *RoomInvitation) Accept() error { 43 | err := ri.GetPuppet().RoomInvitationAccept(ri.id) 44 | if err != nil { 45 | return err 46 | } 47 | inviter, err := ri.Inviter() 48 | if err != nil { 49 | return err 50 | } 51 | topic, err := ri.Topic() 52 | if err != nil { 53 | return err 54 | } 55 | log.Tracef("RoomInvitation accept() with room(%s) & inviter(%s) ready()", topic, inviter) 56 | return inviter.Ready(false) 57 | } 58 | 59 | // Inviter get the inviter from room invitation 60 | func (ri *RoomInvitation) Inviter() (_interface.IContact, error) { 61 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return ri.GetWechaty().Contact().Load(payload.InviterId), nil 66 | } 67 | 68 | // Topic get the room topic from room invitation 69 | func (ri *RoomInvitation) Topic() (string, error) { 70 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 71 | if err != nil { 72 | return "", err 73 | } 74 | return payload.Topic, nil 75 | } 76 | 77 | func (ri *RoomInvitation) MemberCount() (int, error) { 78 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 79 | if err != nil { 80 | return 0, err 81 | } 82 | return payload.MemberCount, nil 83 | } 84 | 85 | // MemberList list of Room Members that you known(is friend) 86 | func (ri *RoomInvitation) MemberList() ([]_interface.IContact, error) { 87 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 88 | if err != nil { 89 | return nil, err 90 | } 91 | contactList := make([]_interface.IContact, 0, len(payload.MemberIdList)) 92 | for _, id := range payload.MemberIdList { 93 | c := ri.GetWechaty().Contact().Load(id) 94 | if err := c.Ready(false); err != nil { 95 | return nil, err 96 | } 97 | contactList = append(contactList, c) 98 | } 99 | return contactList, nil 100 | } 101 | 102 | // Room get the room from room invitation 103 | func (ri *RoomInvitation) Room() (_interface.IRoom, error) { 104 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return ri.GetWechaty().Room().Load(payload.RoomId), nil 109 | } 110 | 111 | // Date get the invitation time 112 | func (ri *RoomInvitation) Date() (time.Time, error) { 113 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 114 | if err != nil { 115 | return time.Time{}, err 116 | } 117 | return payload.Timestamp, nil 118 | } 119 | 120 | // Age returns the room invitation age in seconds 121 | func (ri *RoomInvitation) Age() (time.Duration, error) { 122 | date, err := ri.Date() 123 | if err != nil { 124 | return 0, err 125 | } 126 | return time.Since(date), nil 127 | } 128 | 129 | func (ri *RoomInvitation) ToJson() (string, error) { 130 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 131 | if err != nil { 132 | return "", err 133 | } 134 | marshal, err := json.Marshal(payload) 135 | return string(marshal), err 136 | } 137 | 138 | func (ri *RoomInvitation) RawPayload() (schemas.RoomInvitationPayload, error) { 139 | payload, err := ri.GetPuppet().RoomInvitationPayload(ri.id) 140 | if err != nil { 141 | return schemas.RoomInvitationPayload{}, err 142 | } 143 | 144 | return *payload, nil 145 | } -------------------------------------------------------------------------------- /wechaty/user/tag.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Go Wechaty - https://github.com/wechaty/go-wechaty 3 | * 4 | * Authors: Huan LI (李卓桓) 5 | * Bojie LI (李博杰) 6 | * 7 | * 2020-now @ Copyright Wechaty 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the 'License'); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an 'AS IS' BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | package user 23 | 24 | import _interface "github.com/wechaty/go-wechaty/wechaty/interface" 25 | 26 | type Tag struct { 27 | _interface.IAccessory 28 | id string 29 | } 30 | 31 | // NewTag ... 32 | func NewTag(id string, accessory _interface.IAccessory) *Tag { 33 | return &Tag{accessory, id} 34 | } 35 | 36 | func (t *Tag) ID() string { 37 | return t.id 38 | } 39 | 40 | // Add tag for contact 41 | func (t *Tag) Add(to _interface.IContact) error { 42 | return t.GetPuppet().TagContactAdd(t.id, to.ID()) 43 | } 44 | 45 | // Remove this tag from Contact 46 | func (t *Tag) Remove(from _interface.IContact) error { 47 | return t.GetPuppet().TagContactRemove(t.id, from.ID()) 48 | } 49 | -------------------------------------------------------------------------------- /wechaty/user/url_link.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Go Wechaty - https://github.com/wechaty/go-wechaty 3 | * 4 | * Authors: Huan LI (李卓桓) 5 | * Bojie LI (李博杰) 6 | * 7 | * 2020-now @ Copyright Wechaty 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the 'License'); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an 'AS IS' BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | package user 23 | 24 | import ( 25 | "fmt" 26 | 27 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 28 | ) 29 | 30 | type UrlLink struct { 31 | payload *schemas.UrlLinkPayload 32 | } 33 | 34 | func NewUrlLink(payload *schemas.UrlLinkPayload) *UrlLink { 35 | return &UrlLink{payload: payload} 36 | } 37 | 38 | func (ul *UrlLink) String() string { 39 | return fmt.Sprintf("UrlLink<%s>", ul.Url()) 40 | } 41 | 42 | func (ul *UrlLink) Url() string { 43 | if ul.payload == nil { 44 | return "" 45 | } 46 | return ul.payload.Url 47 | } 48 | 49 | func (ul *UrlLink) Title() string { 50 | if ul.payload == nil { 51 | return "" 52 | } 53 | return ul.payload.Title 54 | } 55 | 56 | func (ul *UrlLink) ThumbnailUrl() string { 57 | if ul.payload == nil { 58 | return "" 59 | } 60 | return ul.payload.ThumbnailUrl 61 | } 62 | 63 | func (ul *UrlLink) Description() string { 64 | if ul.payload == nil { 65 | return "" 66 | } 67 | return ul.payload.Description 68 | } 69 | 70 | // Payload UrlLink payload 71 | func (ul *UrlLink) Payload() schemas.UrlLinkPayload { 72 | return *ul.payload 73 | } 74 | -------------------------------------------------------------------------------- /wechaty/wechaty_test.go: -------------------------------------------------------------------------------- 1 | package wechaty 2 | 3 | import ( 4 | "github.com/wechaty/go-wechaty/wechaty-puppet/schemas" 5 | "testing" 6 | ) 7 | 8 | func TestNewWechaty(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | want *Wechaty 12 | }{ 13 | {name: "new", want: NewWechaty()}, 14 | } 15 | for _, tt := range tests { 16 | t.Run(tt.name, func(t *testing.T) { 17 | _ = NewWechaty() 18 | }) 19 | } 20 | } 21 | 22 | func TestWechaty_Emit(t *testing.T) { 23 | wechaty := NewWechaty() 24 | got := "" 25 | expect := "test" 26 | wechaty.OnHeartbeat(func(context *Context, data string) { 27 | got = data 28 | }) 29 | wechaty.emit(schemas.PuppetEventNameHeartbeat, NewContext(), expect) 30 | if got != expect { 31 | log.Fatalf("got %s expect %s", got, expect) 32 | } 33 | } 34 | 35 | func TestWechaty_On(t *testing.T) { 36 | wechaty := NewWechaty() 37 | got := "" 38 | expect := "ding" 39 | wechaty.OnDong(func(context *Context, data string) { 40 | got = data 41 | }) 42 | wechaty.emit(schemas.PuppetEventNameDong, NewContext(), expect) 43 | if got != expect { 44 | log.Fatalf("got %s expect %s", got, expect) 45 | } 46 | } 47 | 48 | func TestWechatyPluginDisableOnce(t *testing.T) { 49 | testMessage := "abc" 50 | received := false 51 | 52 | plugin := NewPlugin() 53 | plugin.OnHeartbeat(func(context *Context, data string) { 54 | if data == testMessage { 55 | received = true 56 | } 57 | }) 58 | 59 | wechaty := NewWechaty() 60 | wechaty.OnHeartbeat(func(context *Context, data string) { 61 | if data == testMessage { 62 | context.DisableOnce(plugin) 63 | } 64 | }) 65 | wechaty.Use(plugin) 66 | wechaty.emit(schemas.PuppetEventNameHeartbeat, NewContext(), testMessage) 67 | 68 | if received == true { 69 | t.Fatalf("disable plugin method failed.(Context.DisableOnce())") 70 | } 71 | } 72 | 73 | func TestWechatyPluginSetEnable(t *testing.T) { 74 | testMessage := "abc" 75 | received := false 76 | 77 | plugin := NewPlugin() 78 | plugin.OnHeartbeat(func(context *Context, data string) { 79 | if data == testMessage { 80 | received = true 81 | } 82 | }) 83 | 84 | plugin.SetEnable(false) 85 | 86 | wechaty := NewWechaty() 87 | wechaty.Use(plugin) 88 | wechaty.emit(schemas.PuppetEventNameHeartbeat, NewContext(), testMessage) 89 | 90 | if received == true { 91 | t.Fatalf("disable plugin method failed.(Plugin.Disable())") 92 | } 93 | } 94 | 95 | func TestPluginPassingData(t *testing.T) { 96 | testData := "hello" 97 | 98 | p1 := NewPlugin() 99 | p1.OnHeartbeat(func(context *Context, data string) { 100 | context.SetData("helloStr", testData) 101 | }) 102 | 103 | p2 := NewPlugin() 104 | p2.OnHeartbeat(func(context *Context, data string) { 105 | if testData != context.GetData("helloStr").(string) { 106 | t.Fatal("SetData() / GetData() not working.") 107 | } 108 | }) 109 | 110 | wechaty := NewWechaty() 111 | wechaty.Use(p1).Use(p2) 112 | wechaty.emit(schemas.PuppetEventNameHeartbeat, NewContext(), "Data") 113 | } 114 | 115 | func TestPluginAbort(t *testing.T) { 116 | plugin := NewPlugin() 117 | plugin.OnHeartbeat(func(context *Context, data string) { 118 | t.Fatal("Context.Abort() not working.") 119 | }) 120 | 121 | wechaty := NewWechaty() 122 | wechaty.OnHeartbeat(func(context *Context, data string) { 123 | context.Abort() 124 | }) 125 | wechaty.Use(plugin) 126 | wechaty.emit(schemas.PuppetEventNameHeartbeat, NewContext(), "Data") 127 | } 128 | --------------------------------------------------------------------------------