├── .github ├── settings.yml └── workflows │ └── image.yaml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── Dockerfile.ubuntu ├── Jenkinsfile ├── LICENSE ├── Makefile ├── README.md ├── README_zh_CN.md ├── go.mod ├── go.sum ├── main.go ├── pkg ├── api │ ├── media.go │ └── token.go ├── article │ ├── api.go │ ├── api_test.go │ ├── articles.go │ ├── articles_test.go │ └── types.go ├── config │ ├── config.go │ ├── config.yaml │ ├── local_file.go │ └── local_file_test.go ├── github │ └── webhook.go ├── health │ └── health.go ├── menu │ └── menu.go ├── mock │ └── article │ │ └── articles-mock.go ├── reply │ ├── core.go │ ├── core_test.go │ ├── github_bind.go │ ├── github_bind_test.go │ ├── gitter.go │ ├── gitter_test.go │ ├── match.go │ ├── match_test.go │ ├── register.go │ ├── search.go │ ├── search_test.go │ ├── unknown.go │ ├── unknown_test.go │ ├── welcome.go │ └── welcome_test.go ├── service │ ├── config.go │ └── config_test.go ├── token │ └── token.go └── types.go └── sonar-project.properties /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | name: wechat-backend 3 | description: Jenkins 微信公众号机器人 4 | homepage: https://jenkins-zh.cn 5 | private: false 6 | has_issues: true 7 | has_wiki: false 8 | has_downloads: false 9 | default_branch: master 10 | allow_squash_merge: true 11 | allow_merge_commit: true 12 | allow_rebase_merge: true 13 | labels: 14 | - name: newbie 15 | color: abe7f4 16 | description: 新手上路 17 | - name: bug 18 | color: d73a4a 19 | description: Something isn't working 20 | - name: enhancement 21 | color: a2eeef 22 | description: New feature or request 23 | - name: help wanted 24 | color: 008672 25 | description: Extra attention is needed 26 | branches: 27 | - name: master 28 | protection: 29 | required_pull_request_reviews: 30 | required_approving_review_count: 2 31 | dismiss_stale_reviews: true 32 | require_code_owner_reviews: false 33 | dismissal_restrictions: 34 | users: [] 35 | teams: [] 36 | required_status_checks: 37 | strict: true 38 | contexts: [] 39 | enforce_admins: false 40 | restrictions: 41 | users: [] 42 | teams: [] 43 | -------------------------------------------------------------------------------- /.github/workflows/image.yaml: -------------------------------------------------------------------------------- 1 | name: Build Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Build 14 | run: | 15 | docker login --username ${{ secrets.DOCKER_HUB_USER }} --password ${{secrets.DOCKER_HUB_TOKEN}} 16 | docker build -t jenkinszh/wechat-backend:v0.0.2 . 17 | docker push jenkinszh/wechat-backend:v0.0.2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Build stuffs 15 | bin/ 16 | wechat-backend 17 | 18 | # test data files 19 | pkg/reply/*.yaml 20 | .idea 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | dist: trusty 3 | 4 | go: 5 | - 1.12.x 6 | 7 | env: 8 | global: 9 | - GO111MODULE=on 10 | 11 | addons: 12 | sonarcloud: 13 | organization: "jenkins-zh" 14 | token: 15 | secure: 3caad1285eb0edf2b4f65ee07b3cd8edde6c5176 # encrypted value of your token 16 | 17 | script: 18 | - make test 19 | # And finally run the SonarQube analysis - read the "sonar-project.properties" 20 | # file to see the specific configuration 21 | - curl -LsS https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.0.0.1744-linux.zip > sonar-scanner-cli-4.0.0.1744-linux.zip 22 | - unzip sonar-scanner-cli-4.0.0.1744-linux.zip 23 | - sonar-scanner-4.0.0.1744-linux/bin/sonar-scanner -D sonar.branch.name=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then echo $TRAVIS_BRANCH; else echo pr-$TRAVIS_PULL_REQUEST; fi) -Dsonar.projectKey=jenkins-zh_jenkins-cli -Dsonar.organization=jenkins-zh -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=674e187e300edc0ad56a05705bd0b21cbe18bd52 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | WORKDIR /workspace 3 | COPY . . 4 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -a -installsuffix cgo -o wechat-backend 5 | RUN chmod u+x wechat-backend 6 | 7 | FROM alpine:3.3 8 | USER root 9 | RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories 10 | RUN apk add --no-cache ca-certificates curl 11 | COPY --from=0 /workspace/wechat-backend . 12 | CMD ["./wechat-backend"] 13 | -------------------------------------------------------------------------------- /Dockerfile.ubuntu: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | USER root 4 | 5 | COPY bin/wechat-backend wechat-backend 6 | 7 | RUN chmod u+x wechat-backend 8 | 9 | CMD ["./wechat-backend"] 10 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | def scmObj 2 | pipeline { 3 | agent { 4 | label "golang" 5 | } 6 | 7 | environment { 8 | FOLDER = 'src/github.com/jenkins-zh/wechat-backend' 9 | } 10 | 11 | stages{ 12 | stage("clone") { 13 | steps { 14 | dir(FOLDER) { 15 | script { 16 | scmObj = checkout scm 17 | } 18 | } 19 | } 20 | } 21 | 22 | stage("build") { 23 | environment { 24 | GOPATH = "${WORKSPACE}" 25 | } 26 | steps { 27 | dir(FOLDER) { 28 | container('golang'){ 29 | sh ''' 30 | CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -a -installsuffix cgo -o bin/wechat-backend 31 | ''' 32 | } 33 | container('tools') { 34 | sh 'upx bin/wechat-backend' 35 | } 36 | } 37 | } 38 | } 39 | 40 | stage("image") { 41 | environment { 42 | IMAGE_TAG = getCurrentCommit(scmObj) 43 | } 44 | steps { 45 | container('tools'){ 46 | dir(FOLDER) { 47 | sh ''' 48 | docker build -t surenpi/jenkins-wechat:$IMAGE_TAG . 49 | docker build -t surenpi/jenkins-wechat:$IMAGE_TAG-ubuntu -f Dockerfile.ubuntu . 50 | ''' 51 | } 52 | } 53 | } 54 | } 55 | 56 | stage("push-image") { 57 | environment { 58 | DOCKER_CREDS = credentials('docker-surenpi') 59 | IMAGE_TAG = getCurrentCommit(scmObj) 60 | } 61 | steps { 62 | container('tools') { 63 | sh ''' 64 | docker login -u $DOCKER_CREDS_USR -p $DOCKER_CREDS_PSW 65 | docker push surenpi/jenkins-wechat:$IMAGE_TAG 66 | docker push surenpi/jenkins-wechat:$IMAGE_TAG-ubuntu 67 | docker logout 68 | ''' 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | def getCurrentCommit(scmObj) { 76 | return scmObj.GIT_COMMIT 77 | } 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Zhao Xiaojie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CGO_ENABLED = 0 2 | 3 | TAG=dev-$(shell cat .version)-$(shell git config --get user.email | sed -e "s/@/-/") 4 | 5 | build: 6 | CGO_ENABLED=0 GOOS=linux go build -ldflags "-w -s" -a -installsuffix cgo -o bin/wechat-backend 7 | upx bin/wechat-backend 8 | 9 | run: 10 | CGO_ENABLED=0 go build -ldflags "-w -s" -a -installsuffix cgo -o bin/wechat-backend 11 | ./bin/wechat-backend 12 | 13 | build-local: 14 | env go build -o bin/wechat-backend 15 | upx bin/wechat-backend 16 | 17 | image: build 18 | docker build -t jenkinszh/jenkins-wechat:${TAG} . 19 | 20 | push-image: image 21 | docker push jenkinszh/jenkins-wechat:${TAG} 22 | 23 | image-ubuntu: build 24 | docker build -t jenkinszh/jenkins-wechat:ubuntu . -f Dockerfile.ubuntu 25 | docker push jenkinszh/jenkins-wechat:ubuntu 26 | 27 | init-mock-dep: 28 | go get github.com/golang/mock/gomock 29 | go install github.com/golang/mock/mockgen 30 | 31 | update: 32 | kubectl set image deploy/wechat wechat=jenkinszh/jenkins-wechat:${TAG} 33 | make restart 34 | 35 | restart: 36 | kubectl scale deploy/wechat --replicas=0 37 | kubectl scale deploy/wechat --replicas=1 38 | 39 | test: 40 | go test ./... -v -coverprofile coverage.out 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Docker Pulls](https://img.shields.io/docker/pulls/jenkinszh/wechat-backend.svg)](https://hub.docker.com/r/jenkinszh/wechat-backend/tags) 2 | [![HitCount](http://hits.dwyl.com/jenkins-zh/wechat-backend.svg)](http://hits.dwyl.com/jenkins-zh/wechat-backend) 3 | 4 | # WeChat Backend 5 | As a robot, I can take care of some simple works for you. 6 | 7 | # Features 8 | * Welcome new members 9 | * Auto Replay as Code 10 | * Update backend configuration via WebHook (GitHub) 11 | 12 | # Auto-reply 13 | This robot will auto replay by configured files. You should put those config files into a fixed directory: `/management/auto-reply`. 14 | It will reply a fixed sentence if there's no matched word. But you can give it a config file which contains the keyword `unknown`. 15 | 16 | The structure of the config file like blew: 17 | ``` 18 | keyword: join 19 | msgType: text 20 | content: There is the sentence which will be replyed 21 | ``` 22 | 23 | See some examples from [here](https://github.com/jenkins-zh/wechat-bot-config/tree/master/management/auto-reply). 24 | 25 | # Docker 26 | One simple command could bring the WeChat backend up: 27 | 28 | `docker run -t -p 12345:8080 -v /var/wechat/config:/config jenkinszh/wechat-backend:v0.0.1` 29 | 30 | Sample config.yaml: 31 | 32 | ``` 33 | token: wechat-token 34 | git_url: https://github.com/jenkins-zh/wechat 35 | git_branch: master 36 | github_webhook_secret: github-secret 37 | appID: wechat-appid 38 | appSecret: wechat-appsecret 39 | server_port: 8080 40 | ``` 41 | -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | # 微信公众号自动应答 2 | 3 | 作为一个机器人🤖️,我帮您做一些简单事情。 4 | 5 | # 功能 6 | 7 | * 欢迎👏新成员 8 | * 自动回复即代码 9 | * 支持通过 WebHook 自动更新配置(GitHub) 10 | 11 | # Docker 12 | 13 | 一条简单的 Docker 命令就可以把微信公众号的自动应答程序运行起来: 14 | 15 | `docker run -t -p 12345:8080 -v /var/wechat/config:/config surenpi/jenkins-wechat` 16 | 17 | 示例配置文件 config.yaml: 18 | 19 | ``` 20 | token: wechat-token 21 | git_url: https://github.com/jenkins-zh/wechat 22 | git_branch: master 23 | github_webhook_secret: github-secret 24 | appID: wechat-appid 25 | appSecret: wechat-appsecret 26 | server_port: 8080 27 | ``` 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jenkins-zh/wechat-backend 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/golang/mock v1.4.4 7 | github.com/onsi/ginkgo v1.14.2 8 | github.com/onsi/gomega v1.10.3 9 | github.com/stretchr/testify v1.6.1 10 | gopkg.in/go-playground/webhooks.v5 v5.16.0 11 | gopkg.in/src-d/go-git.v4 v4.13.1 12 | gopkg.in/yaml.v2 v2.4.0 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= 2 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 3 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 4 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= 9 | github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= 10 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 11 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 12 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 13 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 14 | github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= 15 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 16 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 19 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 20 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 21 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 22 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 23 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 24 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 25 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 28 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 29 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 30 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 31 | github.com/jenkins-zh/wechat-backend v0.0.0-20190722125634-aaa2fb475a13 h1:nbipNV/UPnRA8VchiSp4FIvz4/au8bABlWXeT/ztDXY= 32 | github.com/jenkins-zh/wechat-backend v0.0.0-20190722125634-aaa2fb475a13/go.mod h1:x7mB2tG1VReAmOmmKmDDHgAePfiDy1MtUcOiCLPFx6s= 33 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 34 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= 35 | github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 36 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 37 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 38 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 39 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 40 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 41 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 42 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 43 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 44 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= 45 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 46 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 47 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 48 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 49 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 50 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 51 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 52 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 53 | github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= 54 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 55 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 56 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 57 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 58 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 59 | github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= 60 | github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= 61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 63 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 64 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 65 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 66 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= 68 | github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= 69 | golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 70 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 71 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= 72 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 73 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 74 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 75 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 76 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 77 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 78 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= 79 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 80 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 81 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 82 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 83 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 84 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 85 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 86 | golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 87 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= 89 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 91 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 92 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 93 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 94 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 95 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 96 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 97 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 98 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 99 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 100 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 101 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 102 | golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 103 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 104 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 105 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 106 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 107 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 108 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 109 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 110 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 111 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 112 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 113 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 114 | gopkg.in/go-playground/webhooks.v5 v5.13.0 h1:e9vtkQZK464+UdL3YjRox2yR8JSmh2094PUBMvdriFs= 115 | gopkg.in/go-playground/webhooks.v5 v5.13.0/go.mod h1:LZbya/qLVdbqDR1aKrGuWV6qbia2zCYSR5dpom2SInQ= 116 | gopkg.in/go-playground/webhooks.v5 v5.16.0/go.mod h1:LZbya/qLVdbqDR1aKrGuWV6qbia2zCYSR5dpom2SInQ= 117 | gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= 118 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 119 | gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= 120 | gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= 121 | gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= 122 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 123 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 124 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 125 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 126 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 127 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 128 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 129 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 130 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 131 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 132 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 133 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/xml" 6 | "fmt" 7 | "github.com/jenkins-zh/wechat-backend/pkg/api" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "sort" 13 | "strings" 14 | 15 | core "github.com/jenkins-zh/wechat-backend/pkg" 16 | "github.com/jenkins-zh/wechat-backend/pkg/article" 17 | "github.com/jenkins-zh/wechat-backend/pkg/config" 18 | "github.com/jenkins-zh/wechat-backend/pkg/github" 19 | "github.com/jenkins-zh/wechat-backend/pkg/health" 20 | "github.com/jenkins-zh/wechat-backend/pkg/menu" 21 | "github.com/jenkins-zh/wechat-backend/pkg/reply" 22 | "github.com/jenkins-zh/wechat-backend/pkg/service" 23 | ) 24 | 25 | // WeChat represents WeChat 26 | type WeChat struct { 27 | Config *config.WeChatConfig 28 | } 29 | 30 | func (we *WeChat) makeSignature(timestamp, nonce string) string { 31 | sl := []string{we.Config.Token, timestamp, nonce} 32 | sort.Strings(sl) 33 | s := sha1.New() 34 | io.WriteString(s, strings.Join(sl, "")) 35 | return fmt.Sprintf("%x", s.Sum(nil)) 36 | } 37 | 38 | func (we *WeChat) validateURL(w http.ResponseWriter, r *http.Request) bool { 39 | timestamp := strings.Join(r.Form["timestamp"], "") 40 | nonce := strings.Join(r.Form["nonce"], "") 41 | signatureGen := we.makeSignature(timestamp, nonce) 42 | 43 | signatureIn := strings.Join(r.Form["signature"], "") 44 | if signatureGen != signatureIn { 45 | return false 46 | } 47 | echostr := strings.Join(r.Form["echostr"], "") 48 | fmt.Fprintf(w, echostr) 49 | return true 50 | } 51 | 52 | func (we *WeChat) procRequest(w http.ResponseWriter, r *http.Request) { 53 | r.ParseForm() 54 | if !we.validateURL(w, r) { 55 | we.normalRequest(w, r) 56 | log.Println("Wechat Service: this http request is not from Wechat platform!") 57 | return 58 | } 59 | log.Println("Wechat Service: validateURL Ok!") 60 | 61 | if we.Config.Valid { 62 | log.Println("request url", r.URL.String()) 63 | if strings.HasPrefix(r.URL.String(), "/?signature=") { 64 | log.Println("just for valid") 65 | return 66 | } 67 | } 68 | 69 | switch r.Method { 70 | case http.MethodPost: 71 | we.wechatRequest(w, r) 72 | } 73 | } 74 | 75 | func (we *WeChat) normalRequest(w http.ResponseWriter, r *http.Request) { 76 | if _, err := w.Write([]byte("Welcome aboard Jenkins WeChat.")); err != nil { 77 | fmt.Printf("got error when response normal request, %v", err) 78 | } 79 | } 80 | 81 | func (we *WeChat) wechatRequest(writer http.ResponseWriter, r *http.Request) { 82 | textRequestBody := we.parseTextRequestBody(r) 83 | if textRequestBody != nil { 84 | autoReplyInitChains := reply.AutoReplyChains() 85 | fmt.Printf("found [%d] autoReply", len(autoReplyInitChains)) 86 | 87 | var potentialReplys []reply.AutoReply 88 | for _, autoReplyInit := range autoReplyInitChains { 89 | if autoReplyInit == nil { 90 | fmt.Printf("found a nil autoReply.") 91 | continue 92 | } 93 | autoReply := autoReplyInit() 94 | if !autoReply.Accept(textRequestBody) { 95 | continue 96 | } 97 | 98 | potentialReplys = append(potentialReplys, autoReply) 99 | } 100 | 101 | sort.Sort(reply.ByWeight(potentialReplys)) 102 | 103 | if len(potentialReplys) > 0 { 104 | autoReply := potentialReplys[0] 105 | fmt.Printf("going to handle by %s\n", autoReply.Name()) 106 | 107 | if data, err := autoReply.Handle(); err != nil { 108 | fmt.Printf("handle auto replay error: %v\n", err) 109 | } else if len(data) == 0 { 110 | fmt.Println("response body is empty.") 111 | } else { 112 | fmt.Printf("response:%s\n", data) 113 | fmt.Fprintf(writer, data) 114 | } 115 | } else { 116 | fmt.Println("should have at least one reply") 117 | } 118 | } 119 | } 120 | 121 | func (we *WeChat) parseTextRequestBody(r *http.Request) *core.TextRequestBody { 122 | body, err := ioutil.ReadAll(r.Body) 123 | if err != nil { 124 | log.Fatal(err) 125 | return nil 126 | } 127 | fmt.Println(string(body)) 128 | requestBody := &core.TextRequestBody{} 129 | xml.Unmarshal(body, requestBody) 130 | return requestBody 131 | } 132 | 133 | func main() { 134 | configurator := &config.LocalFileConfig{} 135 | weConfig, err := configurator.LoadConfig(core.ConfigPath) 136 | if err != nil { 137 | log.Printf("load config error %v\n", err) 138 | } 139 | 140 | // TODO this should be handle by config function 141 | if weConfig == nil { 142 | weConfig = &config.WeChatConfig{ 143 | ServerPort: 8080, 144 | } 145 | } 146 | 147 | if weConfig.ServerPort <= 0 { 148 | weConfig.ServerPort = 8080 149 | } 150 | 151 | defaultRM := article.NewDefaultResponseManager() 152 | reply.SetResponseManager(defaultRM) 153 | 154 | wechat := WeChat{ 155 | Config: weConfig, 156 | } 157 | go func() { 158 | defaultRM.InitCheck(weConfig) 159 | }() 160 | menu.CreateWxMenu(weConfig) 161 | 162 | http.HandleFunc("/", wechat.procRequest) 163 | http.HandleFunc("/status", health.SimpleHealthHandler) 164 | http.HandleFunc("/medias", func(w http.ResponseWriter, r *http.Request) { 165 | api.ListMedias(w, r, configurator) 166 | }) 167 | http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) { 168 | github.WebhookHandler(w, r, weConfig, defaultRM.InitCheck) 169 | }) 170 | http.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) { 171 | service.HandleConfig(w, r, configurator) 172 | }) 173 | 174 | log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", weConfig.ServerPort), nil)) 175 | } 176 | -------------------------------------------------------------------------------- /pkg/api/media.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/jenkins-zh/wechat-backend/pkg/config" 12 | ) 13 | 14 | func ListMedias(w http.ResponseWriter, r *http.Request, cfg config.WeChatConfigurator) { 15 | r.ParseForm() 16 | // imageName := r.Form["imageName"] 17 | 18 | if weConfig, err := cfg.LoadConfig("config/wechat.yaml"); err == nil { 19 | token := GetAccessToken(weConfig) 20 | 21 | url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=%s", token) 22 | 23 | request := ListRequest{ 24 | Type: "image", 25 | Offset: 0, 26 | Count: 20, 27 | } 28 | 29 | var data []byte 30 | var err error 31 | if data, err = json.Marshal(&request); err != nil { 32 | data = []byte("") 33 | } 34 | 35 | log.Println("rquest ", string(data)) 36 | 37 | postReq, err := http.NewRequest(http.MethodPost, 38 | url, 39 | bytes.NewReader(data)) 40 | client := &http.Client{} 41 | resp, err := client.Do(postReq) 42 | if err != nil { 43 | log.Println("rquest media failure", err) 44 | return 45 | } else { 46 | log.Println("media request success") 47 | } 48 | 49 | data, err = ioutil.ReadAll(resp.Body) 50 | if err != nil { 51 | log.Println("read media request body error", err) 52 | return 53 | } else { 54 | log.Println("read body success, data: ", string(data)) 55 | } 56 | 57 | itemList := MediaItemList{} 58 | if err = json.Unmarshal(data, &itemList); err == nil { 59 | for _, item := range itemList.ItemList { 60 | fmt.Println("name: ", item.Name, ", id: ", item.MediaID, "url: ", item.URL) 61 | } 62 | } else { 63 | log.Printf("read yaml error %v, data: %s", err, string(data)) 64 | } 65 | } 66 | } 67 | 68 | // 素材的类型,图片(image)、视频(video)、语音 (voice)、图文(news) 69 | type ListRequest struct { 70 | Type string `json:"type"` 71 | Offset int `json:"offset"` 72 | Count int `json:"count"` 73 | } 74 | 75 | type MediaItemList struct { 76 | TotalCouont int `json:"total_count"` 77 | ItemCount int `json:"item_count"` 78 | ItemList []MediaItem `json:"item"` 79 | } 80 | 81 | type MediaItem struct { 82 | MediaID string `json:"media_id"` 83 | Name string `json:"name"` 84 | UpdateTime string `json:"update_time"` 85 | URL string `json:"url"` 86 | } 87 | -------------------------------------------------------------------------------- /pkg/api/token.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/jenkins-zh/wechat-backend/pkg/config" 10 | ) 11 | 12 | //https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET 13 | 14 | type AccessToken struct { 15 | AccessToken string `json:"access_token"` 16 | Expires int `json:"expires_in"` 17 | } 18 | 19 | func GetAccessToken(config *config.WeChatConfig) string { 20 | resp, err := http.Get(strings.Join([]string{"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential", 21 | "&appid=", config.AppID, "&secret=", config.AppSecret}, "")) 22 | if err != nil { 23 | return "" 24 | } 25 | 26 | if resp.StatusCode != http.StatusOK { 27 | log.Printf("search query failed: %s\n", resp.Status) 28 | return "" 29 | } 30 | 31 | var result AccessToken 32 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 33 | return "" 34 | } 35 | 36 | return result.AccessToken 37 | } 38 | -------------------------------------------------------------------------------- /pkg/article/api.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | type ArticleReader struct { 12 | API string 13 | } 14 | 15 | func (a *ArticleReader) FetchArticles() (articles []Article, err error) { 16 | var apiURL *url.URL 17 | 18 | apiURL, err = url.Parse(a.API) 19 | if err != nil { 20 | return 21 | } 22 | 23 | var resp *http.Response 24 | resp, err = http.Get(apiURL.String()) 25 | if err != nil { 26 | return 27 | } 28 | 29 | var data []byte 30 | data, err = ioutil.ReadAll(resp.Body) 31 | if err != nil { 32 | return 33 | } 34 | 35 | var allArticles []Article 36 | err = json.Unmarshal(data, &allArticles) 37 | 38 | for _, article := range allArticles { 39 | if article.Title == "" || article.Description == "" || 40 | article.URI == "" { 41 | continue 42 | } 43 | articles = append(articles, article) 44 | } 45 | return 46 | } 47 | 48 | func (a *ArticleReader) FindByTitle(title string) (articles []Article, err error) { 49 | var allArticles []Article 50 | 51 | allArticles, err = a.FetchArticles() 52 | if err != nil { 53 | return 54 | } 55 | 56 | for _, article := range allArticles { 57 | if strings.Contains(article.Title, title) { 58 | articles = append(articles, article) 59 | } 60 | } 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /pkg/article/api_test.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestFetchArticles(t *testing.T) { 8 | reader := &ArticleReader{ 9 | API: "https://jenkins-zh.github.io/index.json", 10 | } 11 | articles, err := reader.FetchArticles() 12 | if err != nil { 13 | t.Errorf("fetch error %v", err) 14 | } else if len(articles) == 0 { 15 | t.Errorf("fetch zero article") 16 | } else { 17 | for i, article := range articles { 18 | if article.Title == "" || article.Description == "" || 19 | article.URI == "" { 20 | t.Errorf("article [%d] title, description or uri is empty", i) 21 | } 22 | } 23 | } 24 | 25 | ar, err := reader.FindByTitle("行为") 26 | if err != nil { 27 | t.Errorf("%v", err) 28 | } 29 | 30 | for _, a := range ar { 31 | t.Errorf("%v", a) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/article/articles.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | 9 | "strings" 10 | 11 | core "github.com/jenkins-zh/wechat-backend/pkg" 12 | "github.com/jenkins-zh/wechat-backend/pkg/config" 13 | "gopkg.in/src-d/go-git.v4" 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | const ( 18 | CONFIG = "wechat" 19 | ) 20 | 21 | type ResponseManager interface { 22 | // GetResponse find the response, return false if there's no the correct one 23 | GetResponse(string) (interface{}, bool) 24 | InitCheck(weConfig *config.WeChatConfig) 25 | } 26 | 27 | type DefaultResponseManager struct { 28 | ResponseMap map[string]interface{} 29 | } 30 | 31 | // NewDefaultResponseManager should always call this method to get a object 32 | func NewDefaultResponseManager() (mgr *DefaultResponseManager) { 33 | mgr = &DefaultResponseManager{ 34 | ResponseMap: make(map[string]interface{}, 10), 35 | } 36 | return 37 | } 38 | 39 | func (drm *DefaultResponseManager) GetResponse(keyword string) (interface{}, bool) { 40 | res, ok := drm.ResponseMap[keyword] 41 | return res, ok 42 | } 43 | 44 | func (drm *DefaultResponseManager) InitCheck(weConfig *config.WeChatConfig) { 45 | var err error 46 | 47 | _, err = os.Stat(CONFIG) 48 | if err != nil { 49 | if os.IsNotExist(err) { 50 | _, err = git.PlainClone(CONFIG, false, &git.CloneOptions{ 51 | URL: weConfig.GitURL, 52 | Progress: os.Stdout, 53 | }) 54 | if err != nil { 55 | log.Println("clone failure", err) 56 | return 57 | } 58 | log.Println("the clone progress is done") 59 | } else { 60 | r, err := git.PlainOpen(CONFIG) 61 | if err == nil { 62 | w, err := r.Worktree() 63 | if err == nil { 64 | err = w.Pull(&git.PullOptions{ 65 | RemoteName: "origin", 66 | }) 67 | if err != nil { 68 | log.Println(err) 69 | // return 70 | } 71 | } else { 72 | log.Println("open work tree with git error", err) 73 | os.Remove(CONFIG) 74 | drm.InitCheck(weConfig) 75 | } 76 | } else { 77 | log.Println("open dir with git error", err) 78 | os.Remove(CONFIG) 79 | drm.InitCheck(weConfig) 80 | } 81 | } 82 | } else { 83 | log.Println("can't get config dir status", err) 84 | 85 | if os.RemoveAll(CONFIG) == nil { 86 | drm.InitCheck(weConfig) 87 | } 88 | } 89 | 90 | if err == nil { 91 | log.Println("going to update the cache.") 92 | drm.update() 93 | } 94 | } 95 | 96 | func (drm *DefaultResponseManager) responseHandler(yamlContent []byte) { 97 | reps := core.ResponseBody{} 98 | err := yaml.Unmarshal(yamlContent, &reps) 99 | if err == nil { 100 | log.Println(reps.MsgType, reps.Keyword, reps) 101 | 102 | switch reps.MsgType { 103 | case "text": 104 | text := core.TextResponseBody{} 105 | yaml.Unmarshal(yamlContent, &text) 106 | drm.ResponseMap[reps.Keyword] = text 107 | case "image": 108 | image := core.ImageResponseBody{} 109 | yaml.Unmarshal(yamlContent, &image) 110 | drm.ResponseMap[reps.Keyword] = image 111 | case "news": 112 | news := core.NewsResponseBody{} 113 | yaml.Unmarshal(yamlContent, &news) 114 | drm.ResponseMap[reps.Keyword] = news 115 | case "random": // TODO this not the regular way 116 | random := core.RandomResponseBody{} 117 | yaml.Unmarshal(yamlContent, &random) 118 | drm.ResponseMap[reps.Keyword] = random 119 | default: 120 | log.Println("unknown type", reps.MsgType) 121 | } 122 | } else { 123 | fmt.Println(err) 124 | } 125 | } 126 | 127 | func (drm *DefaultResponseManager) update() { 128 | root := CONFIG + "/management/auto-reply" 129 | files, err := ioutil.ReadDir(root) 130 | if err != nil { 131 | log.Fatal(err) 132 | return 133 | } 134 | for _, file := range files { 135 | if !strings.Contains(file.Name(), "keywords") { 136 | continue 137 | } 138 | 139 | content, err := ioutil.ReadFile(root + "/" + file.Name()) 140 | if err == nil { 141 | drm.responseHandler(content) 142 | } else { 143 | log.Println("Can't read file ", file.Name()) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pkg/article/articles_test.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | import ( 4 | "testing" 5 | 6 | core "github.com/jenkins-zh/wechat-backend/pkg" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestImageResponseBody(t *testing.T) { 11 | yml := ` 12 | msgType: image 13 | 14 | keyword: hi 15 | content: say hello from jenkins. 16 | image: 17 | mediaID: mediaId 18 | ` 19 | 20 | mgr := NewDefaultResponseManager() 21 | mgr.responseHandler([]byte(yml)) 22 | 23 | resp := mgr.ResponseMap["hi"] 24 | if resp == nil { 25 | t.Error("Can't find response by keyword: hi.") 26 | } 27 | 28 | imageResp, ok := resp.(core.ImageResponseBody) 29 | if !ok { 30 | t.Error("Get the wrong type, should be ImageResponseBody.") 31 | } 32 | assert.Equal(t, imageResp.Image.MediaID, "mediaId", "ImageResponseBody parse error, can't find the correct mediaId: ", imageResp.Image.MediaID) 33 | } 34 | 35 | func TestNewsResponseBody(t *testing.T) { 36 | yml := ` 37 | keyword: about 38 | 39 | msgType: news 40 | articleCount: 1 41 | articles: 42 | - title: "title" 43 | description: "desc" 44 | picUrl: "http://pic.com" 45 | url: "http://blog.com" 46 | ` 47 | 48 | mgr := NewDefaultResponseManager() 49 | mgr.responseHandler([]byte(yml)) 50 | 51 | resp := mgr.ResponseMap["about"] 52 | if resp == nil { 53 | t.Error("Can't find response by keyword: about.") 54 | return 55 | } 56 | 57 | newsResp, ok := resp.(core.NewsResponseBody) 58 | if !ok { 59 | t.Error("Get the wrong type, should be NewsResponseBody.") 60 | } 61 | assert.Equal(t, newsResp.Articles.Articles[0].Title, "title", "title parse error.") 62 | } 63 | 64 | func TestRandomResponseBody(t *testing.T) { 65 | yml := ` 66 | keyword: weixin 67 | msgType: random 68 | items: 69 | - abc 70 | - def 71 | ` 72 | 73 | mgr := NewDefaultResponseManager() 74 | mgr.responseHandler([]byte(yml)) 75 | 76 | resp := mgr.ResponseMap["weixin"] 77 | if resp == nil { 78 | t.Error("Can't find response by keyword: weixin.") 79 | return 80 | } 81 | 82 | newsResp, ok := resp.(core.RandomResponseBody) 83 | if !ok { 84 | t.Error("Get the wrong type, should be RandomResponseBody.") 85 | } 86 | assert.Equal(t, len(newsResp.Items), 2, "can not parse items") 87 | assert.Equal(t, newsResp.Items[0], "abc") 88 | } 89 | -------------------------------------------------------------------------------- /pkg/article/types.go: -------------------------------------------------------------------------------- 1 | package article 2 | 3 | type Article struct { 4 | Title string 5 | Description string 6 | URI string 7 | } 8 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // WeChatConfig represents WeChat config 4 | type WeChatConfig struct { 5 | GitURL string `yaml:"git_url"` 6 | GitBranch string `yaml:"git_branch"` 7 | GitHubWebHookSecret string `yaml:"github_webhook_secret"` 8 | 9 | ServerPort int `yaml:"server_port"` 10 | 11 | AppID string `yaml:"appID"` 12 | AppSecret string `yaml:"appSecret"` 13 | Token string `yaml:"token"` 14 | 15 | Valid bool `yaml:"valid"` 16 | } 17 | 18 | // NewConfig new config instance 19 | func NewConfig() *WeChatConfig { 20 | return &WeChatConfig{} 21 | } 22 | 23 | // WeChatConfigurator represent the spec for configuration reader 24 | type WeChatConfigurator interface { 25 | LoadConfig(string) (*WeChatConfig, error) 26 | GetConfig() *WeChatConfig 27 | SaveConfig() error 28 | } 29 | -------------------------------------------------------------------------------- /pkg/config/config.yaml: -------------------------------------------------------------------------------- 1 | token: Token 2 | git_url: GitURL 3 | git_branch: GitBranch 4 | github_webhook_secret: GitHubWebHookSecret 5 | server_port: 80 6 | appID: appID 7 | appSecret: appSecret -------------------------------------------------------------------------------- /pkg/config/local_file.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | 7 | yaml "gopkg.in/yaml.v2" 8 | ) 9 | 10 | // LocalFileConfig implement the WeChatConfigurator by local file system 11 | type LocalFileConfig struct { 12 | path string 13 | config *WeChatConfig 14 | } 15 | 16 | // LoadConfig load config from the file 17 | func (l *LocalFileConfig) LoadConfig(configFile string) ( 18 | cfg *WeChatConfig, err error) { 19 | var content []byte 20 | content, err = ioutil.ReadFile(configFile) 21 | if err != nil { 22 | log.Printf("load config file [%s] error: %v\n", configFile, err) 23 | return 24 | } 25 | 26 | cfg = &WeChatConfig{} 27 | l.config = cfg 28 | l.path = configFile 29 | err = yaml.Unmarshal(content, cfg) 30 | if err != nil { 31 | log.Printf("parse config file error: %v\n", err) 32 | } 33 | return 34 | } 35 | 36 | // GetConfig just return exists config object 37 | func (l *LocalFileConfig) GetConfig() *WeChatConfig { 38 | return l.config 39 | } 40 | 41 | // SaveConfig save the config into local file 42 | func (l *LocalFileConfig) SaveConfig() (err error) { 43 | if l.config == nil { 44 | return 45 | } 46 | 47 | var data []byte 48 | data, err = yaml.Marshal(l.config) 49 | if err == nil { 50 | err = ioutil.WriteFile(l.path, data, 0644) 51 | } 52 | return 53 | } 54 | 55 | var _ WeChatConfigurator = &LocalFileConfig{} 56 | -------------------------------------------------------------------------------- /pkg/config/local_file_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "testing" 4 | 5 | func TestLoadConfig(t *testing.T) { 6 | cfg := &LocalFileConfig{} 7 | 8 | config, err := cfg.LoadConfig("config.yaml") 9 | if err != nil { 10 | t.Errorf("load config error %v", err) 11 | } 12 | 13 | if config.Token != "Token" || config.GitURL != "GitURL" || 14 | config.GitBranch != "GitBranch" || 15 | config.GitHubWebHookSecret != "GitHubWebHookSecret" || 16 | config.ServerPort != 80 || 17 | config.AppID != "appID" || 18 | config.AppSecret != "appSecret" { 19 | t.Errorf("parse error, config %#v", config) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/github/webhook.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/jenkins-zh/wechat-backend/pkg/config" 8 | "gopkg.in/go-playground/webhooks.v5/github" 9 | ) 10 | 11 | func WebhookHandler(w http.ResponseWriter, r *http.Request, weConfig *config.WeChatConfig, initCheck func(*config.WeChatConfig)) { 12 | hook, _ := github.New(github.Options.Secret("secret")) 13 | 14 | payload, err := hook.Parse(r, github.PushEvent) 15 | if err != nil { 16 | if err == github.ErrEventNotFound { 17 | // ok event wasn;t one of the ones asked to be parsed 18 | return 19 | } 20 | } 21 | 22 | switch payload.(type) { 23 | case github.PushPayload: 24 | push := payload.(github.PushPayload) 25 | // Do whatever you want from here... 26 | log.Printf("push ref is %s.\n", push.Ref) 27 | 28 | if push.Ref != "refs/heads/master" { 29 | return 30 | } 31 | } 32 | 33 | log.Println("Going to update wechat config.") 34 | initCheck(weConfig) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/health/health.go: -------------------------------------------------------------------------------- 1 | package health 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // SimpleHealthHandler indicate server status 8 | func SimpleHealthHandler(w http.ResponseWriter, r *http.Request) { 9 | w.Write([]byte("ok")) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/menu/menu.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/jenkins-zh/wechat-backend/pkg/api" 10 | "github.com/jenkins-zh/wechat-backend/pkg/config" 11 | ) 12 | 13 | func pushWxMenuCreate(accessToken string, menuJsonBytes []byte) error { 14 | postReq, err := http.NewRequest(http.MethodPost, 15 | strings.Join([]string{"https://api.weixin.qq.com/cgi-bin/menu/create", "?access_token=", accessToken}, ""), 16 | bytes.NewReader(menuJsonBytes)) 17 | 18 | if err != nil { 19 | log.Println("向微信发送菜单建立请求失败", err) 20 | return err 21 | } 22 | 23 | postReq.Header.Set("Content-Type", "application/json; encoding=utf-8") 24 | 25 | client := &http.Client{} 26 | resp, err := client.Do(postReq) 27 | if err != nil { 28 | log.Println("client向微信发送菜单建立请求失败", err) 29 | return err 30 | } 31 | 32 | defer resp.Body.Close() 33 | log.Println("向微信发送菜单建立成功") 34 | 35 | return nil 36 | } 37 | 38 | // CreateWxMenu create wechat menu 39 | func CreateWxMenu(config *config.WeChatConfig) { 40 | menuStr := `{ 41 | "button": [ 42 | { 43 | "name": "进入商城", 44 | "type": "view", 45 | "url": "http://www.baidu.com/" 46 | }, 47 | { 48 | 49 | "name":"管理中心", 50 | "sub_button":[ 51 | { 52 | "name": "用户中心", 53 | "type": "click", 54 | "key": "molan_user_center" 55 | }, 56 | { 57 | "name": "公告", 58 | "type": "click", 59 | "key": "molan_institution" 60 | }] 61 | }, 62 | { 63 | "name": "资料修改", 64 | "type": "view", 65 | "url": "http://www.baidu.com/user_view" 66 | } 67 | ] 68 | }` 69 | 70 | //发送建立菜单的post请求 71 | token := api.GetAccessToken(config) 72 | pushWxMenuCreate(token, []byte(menuStr)) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/mock/article/articles-mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: pkg/article/articles.go 3 | 4 | // Package mock_article is a generated GoMock package. 5 | package mock_article 6 | 7 | import ( 8 | gomock "github.com/golang/mock/gomock" 9 | config "github.com/jenkins-zh/wechat-backend/pkg/config" 10 | reflect "reflect" 11 | ) 12 | 13 | // MockResponseManager is a mock of ResponseManager interface 14 | type MockResponseManager struct { 15 | ctrl *gomock.Controller 16 | recorder *MockResponseManagerMockRecorder 17 | } 18 | 19 | // MockResponseManagerMockRecorder is the mock recorder for MockResponseManager 20 | type MockResponseManagerMockRecorder struct { 21 | mock *MockResponseManager 22 | } 23 | 24 | // NewMockResponseManager creates a new mock instance 25 | func NewMockResponseManager(ctrl *gomock.Controller) *MockResponseManager { 26 | mock := &MockResponseManager{ctrl: ctrl} 27 | mock.recorder = &MockResponseManagerMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use 32 | func (m *MockResponseManager) EXPECT() *MockResponseManagerMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // GetResponse mocks base method 37 | func (m *MockResponseManager) GetResponse(arg0 string) (interface{}, bool) { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "GetResponse", arg0) 40 | ret0, _ := ret[0].(interface{}) 41 | ret1, _ := ret[1].(bool) 42 | return ret0, ret1 43 | } 44 | 45 | // GetResponse indicates an expected call of GetResponse 46 | func (mr *MockResponseManagerMockRecorder) GetResponse(arg0 interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResponse", reflect.TypeOf((*MockResponseManager)(nil).GetResponse), arg0) 49 | } 50 | 51 | // InitCheck mocks base method 52 | func (m *MockResponseManager) InitCheck(weConfig *config.WeChatConfig) { 53 | m.ctrl.T.Helper() 54 | m.ctrl.Call(m, "InitCheck", weConfig) 55 | } 56 | 57 | // InitCheck indicates an expected call of InitCheck 58 | func (mr *MockResponseManagerMockRecorder) InitCheck(weConfig interface{}) *gomock.Call { 59 | mr.mock.ctrl.T.Helper() 60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitCheck", reflect.TypeOf((*MockResponseManager)(nil).InitCheck), weConfig) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/reply/core.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "encoding/xml" 5 | "time" 6 | 7 | core "github.com/jenkins-zh/wechat-backend/pkg" 8 | ) 9 | 10 | // AutoReply represent auto reply interface 11 | type AutoReply interface { 12 | Accept(request *core.TextRequestBody) bool 13 | Handle() (string, error) 14 | Name() string 15 | Weight() int 16 | } 17 | 18 | type Init func() AutoReply 19 | 20 | func makeTextResponseBody(fromUserName, toUserName string, content string) ([]byte, error) { 21 | textResponseBody := &core.TextResponseBody{} 22 | textResponseBody.FromUserName = fromUserName 23 | textResponseBody.ToUserName = toUserName 24 | textResponseBody.MsgType = "text" 25 | textResponseBody.Content = content 26 | textResponseBody.CreateTime = time.Duration(time.Now().Unix()) 27 | return marshal(textResponseBody) 28 | } 29 | 30 | func makeImageResponseBody(fromUserName, toUserName, mediaID string) ([]byte, error) { 31 | imageResponseBody := &core.ImageResponseBody{} 32 | imageResponseBody.FromUserName = fromUserName 33 | imageResponseBody.ToUserName = toUserName 34 | imageResponseBody.MsgType = "image" 35 | imageResponseBody.CreateTime = time.Duration(time.Now().Unix()) 36 | imageResponseBody.Image = core.Image{ 37 | MediaID: mediaID, 38 | } 39 | return marshal(imageResponseBody) 40 | } 41 | 42 | func makeNewsResponseBody(fromUserName, toUserName string, news core.NewsResponseBody) ([]byte, error) { 43 | newsResponseBody := &core.NewsResponseBody{} 44 | newsResponseBody.FromUserName = fromUserName 45 | newsResponseBody.ToUserName = toUserName 46 | newsResponseBody.MsgType = "news" 47 | newsResponseBody.ArticleCount = 1 48 | newsResponseBody.Articles = core.Articles{ 49 | Articles: news.Articles.Articles, 50 | } 51 | newsResponseBody.CreateTime = time.Duration(time.Now().Unix()) 52 | return marshal(newsResponseBody) 53 | } 54 | 55 | func marshal(response interface{}) ([]byte, error) { 56 | return xml.MarshalIndent(response, " ", " ") 57 | } 58 | -------------------------------------------------------------------------------- /pkg/reply/core_test.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import "testing" 4 | 5 | func TestXML(t *testing.T) { 6 | data, err := makeTextResponseBody("", "", "") 7 | if err != nil { 8 | t.Errorf("xml error %v", err) 9 | } 10 | 11 | t.Errorf("%s", string(data)) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/reply/github_bind.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "io/ioutil" 9 | 10 | core "github.com/jenkins-zh/wechat-backend/pkg" 11 | yaml "gopkg.in/yaml.v2" 12 | ) 13 | 14 | // GitHubBindAutoReply only reply for match 15 | type GitHubBindAutoReply struct { 16 | Request *core.TextRequestBody 17 | GitHubBind GitHubBind 18 | Event string 19 | Keyword string 20 | } 21 | 22 | var _ AutoReply = &GitHubBindAutoReply{} 23 | 24 | const ( 25 | GitHubEventRegister = "注册" 26 | GitHubEventUnregister = "注销" 27 | ) 28 | 29 | // Name indicate reply's name 30 | func (m *GitHubBindAutoReply) Name() string { 31 | return "GitHubBindAutoReply" 32 | } 33 | 34 | // Weight weight for order 35 | func (m *GitHubBindAutoReply) Weight() int { 36 | return 0 37 | } 38 | 39 | // Accept consider if it will accept the request 40 | func (m *GitHubBindAutoReply) Accept(request *core.TextRequestBody) bool { 41 | m.Request = request 42 | m.Keyword = request.Content 43 | 44 | if "text" != request.MsgType { 45 | return false 46 | } 47 | 48 | if strings.HasPrefix(request.Content, GitHubEventRegister) { 49 | m.Event = GitHubEventRegister 50 | m.Keyword = strings.TrimLeft(m.Keyword, fmt.Sprintf("%s ", GitHubEventRegister)) 51 | } else if strings.HasPrefix(request.Content, GitHubEventUnregister) { 52 | m.Event = GitHubEventUnregister 53 | m.Keyword = strings.TrimLeft(m.Keyword, fmt.Sprintf("%s ", GitHubEventUnregister)) 54 | } else { 55 | return false 56 | } 57 | 58 | return true 59 | } 60 | 61 | // Handle hanlde the request then return data 62 | func (m *GitHubBindAutoReply) Handle() (string, error) { 63 | from := m.Request.ToUserName 64 | to := m.Request.FromUserName 65 | var reply string 66 | 67 | if m.Keyword != "" { 68 | switch m.Event { 69 | case GitHubEventRegister: 70 | m.GitHubBind.Add(GitHubBindData{ 71 | WeChatID: to, 72 | GitHubID: m.Keyword, 73 | }) 74 | reply = "register success" 75 | case GitHubEventUnregister: 76 | m.GitHubBind.Remove(to) 77 | reply = "unregister success" 78 | default: 79 | reply = "unknow event" 80 | } 81 | } else { 82 | reply = "need your github id" 83 | } 84 | 85 | var err error 86 | var data []byte 87 | data, err = makeTextResponseBody(from, to, reply) 88 | if err != nil { 89 | err = fmt.Errorf("Wechat Service: makeTextResponseBody error: %v", err) 90 | } 91 | return string(data), err 92 | } 93 | 94 | type GitHubBind interface { 95 | Add(GitHubBindData) error 96 | Update(GitHubBindData) error 97 | Remove(string) 98 | Exists(string) bool 99 | Find(string) *GitHubBindData 100 | Count() int 101 | } 102 | 103 | type GitHubBindData struct { 104 | WeChatID string 105 | GitHubID string 106 | } 107 | 108 | type GitHubBindDataList []GitHubBindData 109 | 110 | type GitHubBinder struct { 111 | File string 112 | DataList GitHubBindDataList 113 | } 114 | 115 | func (g *GitHubBinder) Read() (err error) { 116 | if _, err = os.Stat(g.File); os.IsNotExist(err) { 117 | g.DataList = GitHubBindDataList{} 118 | return nil 119 | } 120 | 121 | var content []byte 122 | if content, err = ioutil.ReadFile(g.File); err != nil { 123 | return 124 | } 125 | 126 | g.DataList = GitHubBindDataList{} 127 | err = yaml.Unmarshal(content, &g.DataList) 128 | return 129 | } 130 | 131 | func (g *GitHubBinder) Add(bindData GitHubBindData) (err error) { 132 | g.Read() 133 | 134 | if g.Exists(bindData.WeChatID) { 135 | return 136 | } 137 | 138 | g.DataList = append(g.DataList, bindData) 139 | var data []byte 140 | if data, err = yaml.Marshal(g.DataList); err == nil { 141 | err = ioutil.WriteFile(g.File, data, 0644) 142 | } 143 | return 144 | } 145 | 146 | func (g *GitHubBinder) Update(bindData GitHubBindData) (err error) { 147 | return 148 | } 149 | 150 | func (g *GitHubBinder) Remove(wechatID string) { 151 | } 152 | 153 | func (g *GitHubBinder) Exists(wechatID string) bool { 154 | g.Read() 155 | if g.DataList == nil { 156 | return false 157 | } 158 | 159 | for _, item := range g.DataList { 160 | if item.WeChatID == wechatID { 161 | return true 162 | } 163 | } 164 | return false 165 | } 166 | 167 | func (g *GitHubBinder) Find(wechatID string) *GitHubBindData { 168 | g.Read() 169 | if g.DataList == nil { 170 | return nil 171 | } 172 | 173 | for _, item := range g.DataList { 174 | if item.WeChatID == wechatID { 175 | return &item 176 | } 177 | } 178 | return nil 179 | } 180 | 181 | func (g *GitHubBinder) Count() int { 182 | g.Read() 183 | return len(g.DataList) 184 | } 185 | 186 | func init() { 187 | Register(func() AutoReply { 188 | return &GitHubBindAutoReply{ 189 | GitHubBind: &GitHubBinder{ 190 | File: "config/github_bind.yaml", 191 | }, 192 | } 193 | }) 194 | } 195 | -------------------------------------------------------------------------------- /pkg/reply/github_bind_test.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | func TestGitHubBind(t *testing.T) { 12 | RegisterFailHandler(Fail) 13 | RunSpecs(t, "Unknown keywords") 14 | } 15 | 16 | var _ = Describe("github bind", func() { 17 | var ( 18 | binder GitHubBind 19 | data GitHubBindData 20 | ctrl *gomock.Controller 21 | ) 22 | 23 | BeforeEach(func() { 24 | ctrl = gomock.NewController(GinkgoT()) 25 | binder = &GitHubBinder{ 26 | File: "github_bind.yaml", 27 | } 28 | data = GitHubBindData{ 29 | WeChatID: "WeChatID", 30 | GitHubID: "GitHubID", 31 | } 32 | }) 33 | 34 | It("should not error", func() { 35 | Expect(binder.Exists("none")).To(Equal(false)) 36 | 37 | Expect(binder.Add(data)).To(BeNil()) 38 | 39 | Expect(binder.Exists("WeChatID")).To(Equal(true)) 40 | 41 | Expect(binder.Find("WeChatID").GitHubID).To(Equal("GitHubID")) 42 | }) 43 | 44 | It("non-repetitive", func() { 45 | Expect(binder.Add(data)).To(BeNil()) 46 | 47 | Expect(binder.Count()).To(Equal(1)) 48 | }) 49 | 50 | AfterEach(func() { 51 | ctrl.Finish() 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /pkg/reply/gitter.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | 9 | core "github.com/jenkins-zh/wechat-backend/pkg" 10 | ) 11 | 12 | // GitterAutoReply only reply for match 13 | type GitterAutoReply struct { 14 | Request *core.TextRequestBody 15 | Keyword string 16 | Callback string 17 | } 18 | 19 | var _ AutoReply = &GitterAutoReply{} 20 | 21 | // Name indicate reply's name 22 | func (m *GitterAutoReply) Name() string { 23 | return "GitterAutoReply" 24 | } 25 | 26 | // Weight weight for order 27 | func (m *GitterAutoReply) Weight() int { 28 | return 0 29 | } 30 | 31 | // Accept consider if it will accept the request 32 | func (m *GitterAutoReply) Accept(request *core.TextRequestBody) (ok bool) { 33 | m.Request = request 34 | m.Keyword = request.Content 35 | m.Keyword = strings.TrimLeft(m.Keyword, "问 ") 36 | m.Keyword = strings.TrimLeft(m.Keyword, "q ") 37 | 38 | if "text" != request.MsgType { 39 | return false 40 | } 41 | 42 | return strings.HasPrefix(request.Content, "问") || 43 | strings.HasPrefix(request.Content, "q") 44 | } 45 | 46 | // Handle hanlde the request then return data 47 | func (m *GitterAutoReply) Handle() (string, error) { 48 | from := m.Request.ToUserName 49 | to := m.Request.FromUserName 50 | var err error 51 | var data []byte 52 | 53 | binder := &GitHubBinder{ 54 | File: "config/github_bind.yaml", 55 | } 56 | 57 | var sender string 58 | gitHubBindData := binder.Find(to) 59 | if gitHubBindData == nil { 60 | sender = "anonymous" 61 | } else { 62 | sender = gitHubBindData.GitHubID 63 | } 64 | 65 | sendMsg(m.Callback, fmt.Sprintf("@%s %s", sender, m.Keyword)) 66 | data, err = makeTextResponseBody(from, to, "sent") 67 | if err != nil { 68 | err = fmt.Errorf("Wechat Service: makeTextResponseBody error: %v", err) 69 | } 70 | return string(data), err 71 | } 72 | 73 | func init() { 74 | Register(func() AutoReply { 75 | return &GitterAutoReply{ 76 | Callback: "https://webhooks.gitter.im/e/911738f12cb4ca5d3c41", 77 | } 78 | }) 79 | } 80 | 81 | // SendMsg send message to server 82 | func sendMsg(server, message string) { 83 | value := url.Values{ 84 | "message": {message}, 85 | } 86 | 87 | http.PostForm(server, value) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/reply/gitter_test.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import "testing" 4 | 5 | func TestGitter(t *testing.T) { 6 | // SendMsg("https://webhooks.gitter.im/e/392bac9a454b919ef965", "sdf") 7 | } 8 | -------------------------------------------------------------------------------- /pkg/reply/match.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "math/rand" 7 | "time" 8 | 9 | core "github.com/jenkins-zh/wechat-backend/pkg" 10 | "github.com/jenkins-zh/wechat-backend/pkg/article" 11 | ) 12 | 13 | var responseManager article.ResponseManager 14 | 15 | func SetResponseManager(manager article.ResponseManager) { 16 | responseManager = manager 17 | } 18 | 19 | // MatchAutoReply only reply for match 20 | type MatchAutoReply struct { 21 | Response interface{} 22 | Request *core.TextRequestBody 23 | } 24 | 25 | var _ AutoReply = &MatchAutoReply{} 26 | 27 | func (m *MatchAutoReply) Name() string { 28 | return "SimpleMatchReply" 29 | } 30 | 31 | func (m *MatchAutoReply) Weight() int { 32 | return 0 33 | } 34 | 35 | // Accept consider if it will accept the request 36 | func (m *MatchAutoReply) Accept(request *core.TextRequestBody) (ok bool) { 37 | m.Request = request 38 | keyword := request.Content 39 | 40 | fmt.Printf("request is %v\n", request) 41 | 42 | if responseManager == nil || "text" != request.MsgType { 43 | log.Printf("responseManager is nil or not support msgType %s", request.MsgType) 44 | return false 45 | } 46 | 47 | m.Response, ok = responseManager.GetResponse(keyword) 48 | return ok 49 | } 50 | 51 | // Handle handle the request then return data 52 | func (m *MatchAutoReply) Handle() (string, error) { 53 | resp := m.Response 54 | from := m.Request.ToUserName 55 | to := m.Request.FromUserName 56 | var err error 57 | 58 | if resp == nil { 59 | err = fmt.Errorf("response is nil") 60 | return "", err 61 | } 62 | 63 | fmt.Printf("response %v\n", resp) 64 | 65 | var data []byte 66 | if text, ok := resp.(core.TextResponseBody); ok { 67 | data, err = makeTextResponseBody(from, to, text.Content) 68 | fmt.Printf("data %v\n", string(data)) 69 | if err != nil { 70 | err = fmt.Errorf("wechat Service: makeTextResponseBody error: %v", err) 71 | } 72 | } else if image, ok := resp.(core.ImageResponseBody); ok { 73 | data, err = makeImageResponseBody(from, to, image.Image.MediaID) 74 | if err != nil { 75 | err = fmt.Errorf("wechat Service: makeImageResponseBody error: %v", err) 76 | } 77 | } else if news, ok := resp.(core.NewsResponseBody); ok { 78 | data, err = makeNewsResponseBody(from, to, news) 79 | if err != nil { 80 | err = fmt.Errorf("wechat Service: makeNewsResponseBody error: %v", err) 81 | } 82 | } else if random, ok := resp.(core.RandomResponseBody); ok { 83 | items := random.Items 84 | count := len(items) 85 | 86 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 87 | index := r.Intn(count) 88 | 89 | fmt.Printf("response random item count: %d, index: %d\n", count, index) 90 | 91 | rondomText := fmt.Sprintf("%s\n%s", random.Content, items[index]) 92 | 93 | data, err = makeTextResponseBody(from, to, rondomText) 94 | if err != nil { 95 | err = fmt.Errorf("wechat Service: RandomResponseBody error: %v", err) 96 | } 97 | } else { 98 | err = fmt.Errorf("type error %v", resp) 99 | } 100 | 101 | return string(data), err 102 | } 103 | 104 | func init() { 105 | Register(func() AutoReply { 106 | return &MatchAutoReply{} 107 | }) 108 | } 109 | -------------------------------------------------------------------------------- /pkg/reply/match_test.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | core "github.com/jenkins-zh/wechat-backend/pkg" 8 | mArticle "github.com/jenkins-zh/wechat-backend/pkg/mock/article" 9 | ) 10 | 11 | func TestAccept(t *testing.T) { 12 | ctrl := gomock.NewController(t) 13 | defer ctrl.Finish() 14 | var reply AutoReply 15 | reply = &MatchAutoReply{} 16 | 17 | if reply.Accept(&core.TextRequestBody{}) { 18 | t.Errorf("should not accept") 19 | } 20 | 21 | m := mArticle.NewMockResponseManager(ctrl) 22 | m.EXPECT().GetResponse("hello"). 23 | Return(&core.TextResponseBody{}, true) 24 | SetResponseManager(m) 25 | 26 | if !reply.Accept(&core.TextRequestBody{ 27 | MsgType: "text", 28 | Content: "hello", 29 | }) { 30 | t.Errorf("should accept") 31 | } 32 | } 33 | 34 | func TestHandle(t *testing.T) { 35 | ctrl := gomock.NewController(t) 36 | defer ctrl.Finish() 37 | reply := &MatchAutoReply{} 38 | 39 | m := mArticle.NewMockResponseManager(ctrl) 40 | m.EXPECT().GetResponse("hello"). 41 | Return(core.TextResponseBody{ 42 | ResponseBody: core.ResponseBody{ 43 | MsgType: "text", 44 | }, 45 | Content: "hello", 46 | }, true) 47 | 48 | SetResponseManager(m) 49 | if !reply.Accept(&core.TextRequestBody{ 50 | MsgType: "text", 51 | Content: "hello", 52 | }) { 53 | t.Errorf("should accept") 54 | } 55 | 56 | data, err := reply.Handle() 57 | if err != nil { 58 | t.Errorf("should not error %v", err) 59 | } else if string(data) != "hello" { 60 | t.Errorf("got an error content: %s", string(data)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/reply/register.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | var autoReplyInitChains []Init 4 | 5 | // Register add an implement of AutoReply 6 | func Register(initFunc Init) { 7 | autoReplyInitChains = append(autoReplyInitChains, initFunc) 8 | } 9 | 10 | // AutoReplyChains return all implements of AutoReply 11 | func AutoReplyChains() []Init { 12 | return autoReplyInitChains 13 | } 14 | 15 | type ByWeight []AutoReply 16 | 17 | func (b ByWeight) Len() int { 18 | return len(b) 19 | } 20 | 21 | func (b ByWeight) Less(i, j int) bool { 22 | return b[i].Weight() < b[j].Weight() 23 | } 24 | 25 | func (b ByWeight) Swap(i, j int) { 26 | b[i], b[j] = b[j], b[i] 27 | } 28 | 29 | // func init() { 30 | // autoReplyInitChains = make([]Init, 3) 31 | // } 32 | -------------------------------------------------------------------------------- /pkg/reply/search.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | core "github.com/jenkins-zh/wechat-backend/pkg" 8 | "github.com/jenkins-zh/wechat-backend/pkg/article" 9 | ) 10 | 11 | // SearchAutoReply only reply for match 12 | type SearchAutoReply struct { 13 | ResponseMap map[string]interface{} 14 | Response interface{} 15 | Request *core.TextRequestBody 16 | Keyword string 17 | } 18 | 19 | var _ AutoReply = &SearchAutoReply{} 20 | 21 | func (m *SearchAutoReply) Name() string { 22 | return "SearchAutoReply" 23 | } 24 | 25 | func (m *SearchAutoReply) Weight() int { 26 | return 0 27 | } 28 | 29 | // Accept consider if it will accept the request 30 | func (m *SearchAutoReply) Accept(request *core.TextRequestBody) (ok bool) { 31 | m.Request = request 32 | m.Keyword = request.Content 33 | m.Keyword = strings.TrimLeft(m.Keyword, "search ") 34 | m.Keyword = strings.TrimLeft(m.Keyword, "搜索 ") 35 | 36 | if "text" != request.MsgType { 37 | return false 38 | } 39 | 40 | return strings.HasPrefix(request.Content, "搜索") || 41 | strings.HasPrefix(request.Content, "search") 42 | } 43 | 44 | // Handle hanlde the request then return data 45 | func (m *SearchAutoReply) Handle() (string, error) { 46 | from := m.Request.ToUserName 47 | to := m.Request.FromUserName 48 | var err error 49 | 50 | reader := &article.ArticleReader{ 51 | API: "https://jenkins-zh.github.io/index.json", 52 | } 53 | 54 | var data []byte 55 | articles, err := reader.FindByTitle(m.Keyword) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | fmt.Printf("found aritcle count [%d]\n", len(articles)) 61 | var targetArticle article.Article 62 | if len(articles) == 0 { 63 | targetArticle = article.Article{ 64 | Title: "404", 65 | Description: "没有找到相关的文章,给我们留言,或者直接发 PR 过来!", 66 | URI: "https://jenkins-zh.github.io", 67 | } 68 | } else { 69 | targetArticle = articles[0] 70 | } 71 | 72 | news := core.NewsResponseBody{ 73 | Articles: core.Articles{ 74 | Articles: []core.Article{ 75 | { 76 | Title: targetArticle.Title, 77 | Description: targetArticle.Description, 78 | PicUrl: "https://mmbiz.qpic.cn/mmbiz_jpg/nh8sibXrvHrvicMyefXop7qwrnWQc5gtBgia05BicxFCWdjPkee3Ku9FLwBZR3JJVDwvVDL25p90BLPOTOWUCrribLA/0?wx_fmt=jpeg", 79 | Url: targetArticle.URI, 80 | }, 81 | }, 82 | }, 83 | } 84 | data, err = makeNewsResponseBody(from, to, news) 85 | if err != nil { 86 | err = fmt.Errorf("Wechat Service: makeNewsResponseBody error: %v", err) 87 | } 88 | return string(data), err 89 | } 90 | 91 | func init() { 92 | Register(func() AutoReply { 93 | return &SearchAutoReply{} 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/reply/search_test.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "testing" 5 | 6 | core "github.com/jenkins-zh/wechat-backend/pkg" 7 | ) 8 | 9 | func TestSearch(t *testing.T) { 10 | reply := SearchAutoReply{} 11 | 12 | reply.Accept(&core.TextRequestBody{}) 13 | 14 | data, err := reply.Handle() 15 | if err != nil { 16 | t.Errorf("error %v", err) 17 | } 18 | 19 | t.Errorf("%s", string(data)) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/reply/unknown.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "math" 5 | 6 | core "github.com/jenkins-zh/wechat-backend/pkg" 7 | ) 8 | 9 | // UnknownAutoReply unknown auto reply 10 | type UnknownAutoReply struct { 11 | Request *core.TextRequestBody 12 | } 13 | 14 | // Name represent name for current auto reply 15 | func (u *UnknownAutoReply) Name() string { 16 | return "UnknownReply" 17 | } 18 | 19 | // Accept all keywords 20 | func (u *UnknownAutoReply) Accept(request *core.TextRequestBody) bool { 21 | u.Request = request 22 | return true 23 | } 24 | 25 | // Handle take care of unknown things 26 | func (u *UnknownAutoReply) Handle() (string, error) { 27 | from := u.Request.ToUserName 28 | to := u.Request.FromUserName 29 | commonReply := `您的提的问题已经远远超过了我的智商,请回复"小助手",社区机器人会把您拉进群里。更多关键字,请回复"帮助"。` 30 | 31 | // try to find a configured reply sentence 32 | if response, ok := responseManager.GetResponse("unknown"); ok && response != nil { 33 | if text, ok := response.(core.TextResponseBody); ok { 34 | commonReply = text.Content 35 | } 36 | } 37 | 38 | data, err := makeTextResponseBody(from, to, commonReply) 39 | return string(data), err 40 | } 41 | 42 | // Weight should be the last one 43 | func (u *UnknownAutoReply) Weight() int { 44 | return math.MaxInt64 45 | } 46 | 47 | var _ AutoReply = &UnknownAutoReply{} 48 | 49 | func init() { 50 | Register(func() AutoReply { 51 | return &UnknownAutoReply{} 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/reply/unknown_test.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | core "github.com/jenkins-zh/wechat-backend/pkg" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | func TestUnknown(t *testing.T) { 13 | RegisterFailHandler(Fail) 14 | RunSpecs(t, "Unknown keywords") 15 | } 16 | 17 | var _ = Describe("Unknon keywords", func() { 18 | var ( 19 | reply AutoReply 20 | ctrl *gomock.Controller 21 | ) 22 | 23 | BeforeEach(func() { 24 | ctrl = gomock.NewController(GinkgoT()) 25 | reply = &UnknownAutoReply{} 26 | }) 27 | 28 | It("should not error", func() { 29 | Expect(reply.Accept(&core.TextRequestBody{})).To(Equal(true)) 30 | }) 31 | 32 | AfterEach(func() { 33 | ctrl.Finish() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/reply/welcome.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "fmt" 5 | 6 | core "github.com/jenkins-zh/wechat-backend/pkg" 7 | ) 8 | 9 | // WelcomeReply for welcome event 10 | type WelcomeReply struct { 11 | AutoReply 12 | } 13 | 14 | var _ AutoReply = &WelcomeReply{} 15 | 16 | // Name represents the name for reply 17 | func (m *WelcomeReply) Name() string { 18 | return "WelcomeReply" 19 | } 20 | 21 | // Accept consider if it will accept the request 22 | func (m *WelcomeReply) Accept(request *core.TextRequestBody) (ok bool) { 23 | if "event" == request.MsgType && "subscribe" == request.Event { 24 | request.Content = "welcome" 25 | request.MsgType = "text" 26 | m.AutoReply = &MatchAutoReply{} 27 | ok = m.AutoReply.Accept(request) 28 | } 29 | return 30 | } 31 | 32 | func (m *WelcomeReply) Weight() int { 33 | return 0 34 | } 35 | 36 | func init() { 37 | fmt.Println("register for welcome") 38 | Register(func() AutoReply { 39 | return &WelcomeReply{} 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/reply/welcome_test.go: -------------------------------------------------------------------------------- 1 | package reply 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | core "github.com/jenkins-zh/wechat-backend/pkg" 8 | mArticle "github.com/jenkins-zh/wechat-backend/pkg/mock/article" 9 | ) 10 | 11 | func TestWelcome(t *testing.T) { 12 | ctrl := gomock.NewController(t) 13 | defer ctrl.Finish() 14 | reply := WelcomeReply{} 15 | request := &core.TextRequestBody{ 16 | MsgType: "event", 17 | Event: "subscribe", 18 | } 19 | 20 | m := mArticle.NewMockResponseManager(ctrl) 21 | m.EXPECT().GetResponse("welcome"). 22 | Return(core.TextResponseBody{ 23 | ResponseBody: core.ResponseBody{ 24 | MsgType: "text", 25 | }, 26 | Content: "welcome", 27 | }, true) 28 | 29 | SetResponseManager(m) 30 | 31 | if !reply.Accept(request) { 32 | t.Errorf("should accept") 33 | } 34 | 35 | if _, err := reply.Handle(); err != nil { 36 | t.Errorf("should not error: %v", err) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/service/config.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/jenkins-zh/wechat-backend/pkg/config" 9 | ) 10 | 11 | // HandleConfig handle the config modify 12 | func HandleConfig(w http.ResponseWriter, r *http.Request, weConfig config.WeChatConfigurator) { 13 | r.ParseForm() 14 | 15 | validStr := strings.Join(r.Form["valid"], "") 16 | 17 | config := weConfig.GetConfig() 18 | config.Valid = (validStr == "true") 19 | 20 | w.Write([]byte(fmt.Sprintf("WeChat valid: %v", config.Valid))) 21 | 22 | weConfig.SaveConfig() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/service/config_test.go: -------------------------------------------------------------------------------- 1 | package service_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/golang/mock/gomock" 7 | "github.com/jenkins-zh/wechat-backend/pkg/config" 8 | "github.com/jenkins-zh/wechat-backend/pkg/service" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | 12 | "net/http" 13 | "net/http/httptest" 14 | ) 15 | 16 | func TestService(t *testing.T) { 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "Service Test") 19 | } 20 | 21 | var _ = Describe("", func() { 22 | var ( 23 | weConfig *config.WeChatConfig 24 | ctrl *gomock.Controller 25 | request *http.Request 26 | writer http.ResponseWriter 27 | ) 28 | 29 | BeforeEach(func() { 30 | ctrl = gomock.NewController(GinkgoT()) 31 | weConfig = config.NewConfig() 32 | 33 | request = httptest.NewRequest("GET", "/test?valid=true", nil) 34 | writer = httptest.NewRecorder() 35 | }) 36 | 37 | It("turn on the valid", func() { 38 | Expect(weConfig.Valid).To(Equal(false)) 39 | 40 | service.HandleConfig(writer, request, weConfig) 41 | 42 | Expect(weConfig.Valid).To(Equal(true)) 43 | }) 44 | 45 | Context("turn off the valid", func() { 46 | JustBeforeEach(func() { 47 | request = httptest.NewRequest("GET", "/test", nil) 48 | }) 49 | 50 | It("turn off the valid", func() { 51 | weConfig.Valid = true 52 | service.HandleConfig(writer, request, weConfig) 53 | Expect(weConfig.Valid).To(Equal(false)) 54 | }) 55 | }) 56 | 57 | AfterEach(func() { 58 | ctrl.Finish() 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /pkg/token/token.go: -------------------------------------------------------------------------------- 1 | <<<<<<< HEAD:pkg/token/token.go 2 | package token 3 | ======= 4 | package api 5 | >>>>>>> master:pkg/api/token.go 6 | 7 | import ( 8 | "encoding/json" 9 | "log" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/jenkins-zh/wechat-backend/pkg/config" 14 | ) 15 | 16 | //https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET 17 | 18 | type AccessToken struct { 19 | AccessToken string `json:"access_token"` 20 | Expires int `json:"expires_in"` 21 | } 22 | 23 | func GetAccessToken(config *config.WeChatConfig) string { 24 | resp, err := http.Get(strings.Join([]string{"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential", 25 | "&appid=", config.AppID, "&secret=", config.AppSecret}, "")) 26 | if err != nil { 27 | return "" 28 | } 29 | 30 | if resp.StatusCode != http.StatusOK { 31 | log.Printf("search query failed: %s\n", resp.Status) 32 | return "" 33 | } 34 | 35 | var result AccessToken 36 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 37 | return "" 38 | } 39 | 40 | return result.AccessToken 41 | } 42 | -------------------------------------------------------------------------------- /pkg/types.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "encoding/xml" 5 | "time" 6 | ) 7 | 8 | // ConfigPath WeChat config file path 9 | const ConfigPath = "config/wechat.yaml" 10 | 11 | type TextRequestBody struct { 12 | XMLName xml.Name `xml:"xml"` 13 | ToUserName string 14 | FromUserName string 15 | CreateTime time.Duration 16 | MsgType string 17 | Content string 18 | MsgId int 19 | Event string 20 | } 21 | 22 | type ResponseBody struct { 23 | Keyword string `json:"keyword" xml:"-"` 24 | 25 | MsgType string `json:"msgType" yaml:"msgType" xml:"MsgType"` 26 | ToUserName string 27 | FromUserName string 28 | CreateTime time.Duration 29 | } 30 | 31 | type TextResponseBody struct { 32 | ResponseBody `yaml:",inline"` 33 | XMLName xml.Name `xml:"xml"` 34 | Content string 35 | } 36 | 37 | type NewsResponseBody struct { 38 | ResponseBody `yaml:",inline"` 39 | XMLName xml.Name `xml:"xml"` 40 | ArticleCount int `json:"articleCount" yaml:"articleCount" xml:"ArticleCount"` 41 | Articles Articles `yaml:",inline"` 42 | } 43 | 44 | type ImageResponseBody struct { 45 | ResponseBody `yaml:",inline"` 46 | XMLName xml.Name `xml:"xml"` 47 | Image Image 48 | } 49 | 50 | // RandomResponseBody is not a regular type from wechat system 51 | type RandomResponseBody struct { 52 | ResponseBody `yaml:",inline"` 53 | XMLName xml.Name `xml:"xml"` 54 | Items []string 55 | Content string 56 | } 57 | 58 | type Articles struct { 59 | // XMLName xml.Name `xml:"Articles"` 60 | Articles []Article `xml:"item"` 61 | } 62 | 63 | type Image struct { 64 | MediaID string `json:"mediaId" yaml:"mediaID" xml:"MediaId"` 65 | } 66 | 67 | type Article struct { 68 | Title string 69 | Description string 70 | PicUrl string `json:"picUrl" yaml:"picUrl" xml:"PicUrl"` 71 | Url string 72 | } 73 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=jenkins-zh_wechat-backend 2 | # this is the name and version displayed in the SonarCloud UI. 3 | sonar.projectName=wechat-backend 4 | sonar.projectVersion=1.0 5 | 6 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 7 | # This property is optional if sonar.modules is set. 8 | sonar.sources=. 9 | 10 | # Encoding of the source code. Default is default system encoding 11 | #sonar.sourceEncoding=UTF-8 12 | 13 | sonar.go.exclusions=**/vendor/** 14 | --------------------------------------------------------------------------------