├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_CN.md ├── art ├── bot_chat.png ├── create_hubot.png ├── res_attachment.png ├── res_reply.png └── res_send.png ├── example ├── .gitignore ├── Makefile ├── bin │ ├── hubot │ └── hubot.cmd ├── package.json └── scripts │ └── example.coffee ├── package.json ├── scripts └── attachments_example.coffee └── src ├── bearychat.coffee ├── client_event.coffee ├── helpers.js ├── helpers.test.js ├── http_client.coffee └── rtm_client.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store* 3 | .hubot_history 4 | .editorconfig 5 | .cs_files 6 | .idea 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "7" 5 | - "6" 6 | - "5" 7 | script: 8 | - travis_retry npm run test 9 | notifications: 10 | webhooks: https://hook.bearychat.com/=bw52O/travis/e0ca350e4af6b551b1707feb02795841 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ---- 2 | - Name: hubot-bearychat 3 | ---- 4 | # 0.7.2 / 2017-06-09 5 | 6 | - 修复 attachment 不能发送的问题 7 | 8 | # 0.7.1 / 2017-06-07 9 | 10 | - 修复普通类型消息不能发送的问题 11 | 12 | # 0.7.0 / 2017-06-07 13 | 14 | ## Added 15 | 16 | - 支持 `robot.messageRoom` 方法 17 | 18 | 19 | # 0.6.0 / 2017-05-16 20 | 21 | ## Added 22 | 23 | - 增加断线重连功能 24 | 25 | # 0.5.2 / 2017-05-12 26 | 27 | ## Added 28 | 29 | - 增加断线重连功能 30 | 31 | ## Fixed 32 | 33 | - 修复转换用户 ID 编码的逻辑错误 34 | 35 | # 0.5.1 / 2017-04-20 36 | 37 | ## Added 38 | 39 | - 修改 `robot.send` 行为,由带引用变为不带引用。 40 | - 修改 `robot.reply` 行为,由带 @ 变为不带 @。 41 | 42 | # 0.5.0 / 2017-03-30 43 | 44 | ## Added 45 | 46 | - 更新 bearychat.js 到 1.0.0 47 | - #20 #12 支持发送 attachment 类型消息 48 | 49 | # 0.4.2 / 2017-03-23 50 | 51 | ## Fixed 52 | 53 | - #19 fix(http_client): missing mention sign 54 | 55 | # 0.4.1 / 2017-03-16 56 | 57 | ## Fixed 58 | 59 | - #17 添加缺少的 `EventClosed` 常量 60 | 61 | # 0.4.0 / 2017-02-23 62 | 63 | ## Added 64 | 65 | - 支持发送 attachemnt #14 66 | 67 | # 0.3.1 / 2017-01-24 68 | 69 | ## Fixed 70 | 71 | - #10 72 | 73 | # 0.3.0 / 2017-01-21 74 | 75 | ## Added 76 | 77 | - 添加 rtm 模式实现 78 | 79 | # 0.2.2 / 2016-08-26 80 | 81 | ## Added 82 | 83 | - 初始版本 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Beary Innovative 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hubot-bearychat 2 | 3 | This is a [Hubot](http://hubot.github.com/) adapter to use with [BearyChat](https://bearychat.com). 4 | 5 | [![@BearyChat](http://openapi.beary.chat/badge.svg)](http://openapi.beary.chat/join) 6 | [![Build Status](https://travis-ci.org/bearyinnovative/hubot-bearychat.svg)](https://travis-ci.org/bearyinnovative/hubot-bearychat) 7 | [![npm version](https://badge.fury.io/js/hubot-bearychat.svg)](https://npmjs.com/package/hubot-bearychat) 8 | 9 | [中文文档](./README_CN.md) 10 | 11 | 12 | 13 | - [5 Minutes Setup](#5-minutes-setup) 14 | * [Step 1. get ya a "hubot token"](#step-1-get-ya-a-hubot-token) 15 | * [Step 2. bootstrap your secret hubot project with yeoman](#step-2-bootstrap-your-secret-hubot-project-with-yeoman) 16 | * [Step 3. copy your hubot token and start it](#step-3-copy-your-hubot-token-and-start-it) 17 | * [Step 4. start chatting with your bot!](#step-4-start-chatting-with-your-bot) 18 | - [Mode](#mode) 19 | * [RTM mode](#rtm-mode) 20 | * [HTTP mode](#http-mode) 21 | - [Configuration](#configuration) 22 | - [Hubot Interactions](#hubot-interactions) 23 | * [Send](#send) 24 | * [Reply](#reply) 25 | * [`bearychat.attachment`](#bearychatattachment) 26 | - [LICENSE](#license) 27 | 28 | 29 | 30 | ## 5 Minutes Setup 31 | 32 | ### Step 1. get ya a "hubot token" 33 | 34 | Go to your team robots page in bearychat.com (your-cool-team.bearychat.com/robots) 35 | and create a hubot. You will get your hubot token inside the bot settings form: 36 | 37 | ![art/create_hubot.png](art/create_hubot.png) 38 | 39 | ### Step 2. bootstrap your secret hubot project with yeoman 40 | 41 | - `npm install -g hubot coffee-script yo generator-hubot` 42 | - `mkdir -p /path/to/hubot` 43 | - `cd /path/to/hubot` 44 | - `yo hubot` 45 | - `npm install hubot-bearychat --save` 46 | 47 | Also check out the [hubot docs](https://github.com/github/hubot/tree/master/docs) 48 | for further guidance on how to build your bot. 49 | 50 | ### Step 3. copy your hubot token and start it 51 | 52 | ```shell 53 | $ export HUBOT_BEARYCHAT_TOKENS=token-token-token-here 54 | $ export HUBOT_BEARYCHAT_MODE=rtm 55 | $ ./bin/hubot -a bearychat 56 | ``` 57 | 58 | ### Step 4. start chatting with your bot! 59 | 60 | ![art/bot_chat.png](art/bot_chat.png) 61 | 62 | You can also refer [example/](example) for sample setup. 63 | 64 | ## Mode 65 | 66 | ### RTM mode 67 | 68 | RTM mode uses BearyChat's RTM api and WebSocket as message transport protocol. 69 | In this mode, hubot can receive all messages in real time and hear any messages 70 | from channels it had joined. 71 | 72 | To enable RTM mode, you should specify environment variable 73 | `HUBOT_BEARYCHAT_MODE=rtm` before running the hubot. 74 | 75 | `hubot-bearychat` uses rtm mode by default. 76 | 77 | ### HTTP mode 78 | 79 | HTTP mode is the legacy message transport protocol. In this mode, hubot can 80 | only receive messages that himself was mentioned (e.g. `@hubot how do you do`), 81 | and you need to set the hubot hosted http service url in the hubot settings form. 82 | 83 | To enable HTTP mode, you should specify environment variable 84 | `HUBOT_BEARYCHAT_MODE=http` before running the hubot. 85 | 86 | ## Configuration 87 | 88 | Available configurations are injected via environment variables: 89 | 90 | | envvar | description | 91 | |:------:|:------------| 92 | | `HUBOT_BEARYCHAT_MODE` | running mode for the hubot, by default is `rtm` | 93 | | `HUBOT_BEARYCHAT_TOKENS` | hubot token, required for running hubot | 94 | 95 | ## Hubot Interactions 96 | 97 | ### Send 98 | 99 | Hubot can response a message with `res.send`: 100 | 101 | ``` 102 | robot.hear /hello/, (res) -> 103 | res.send 'hello, world!' 104 | ``` 105 | 106 | ![art/res_send.png](art/res_send.png) 107 | 108 | ### Reply 109 | 110 | If hubot want to response a message and refer the caller`s message, use `res.reply`: 111 | 112 | ``` 113 | robot.hear /how old are you?/, (res) -> 114 | res.reply 'I am Five!' 115 | ``` 116 | 117 | ![art/res_reply.png](art/res_reply.png) 118 | 119 | ### Send message to other room 120 | 121 | If you want to send a message to other channel use `robot.messageRoom` with vchannel_id, and Channel Name support is coming soon: 122 | 123 | ``` 124 | robot.hear /voldemort/i, (res) -> 125 | robot.messageRoom(vchannel_id, "Somebody is talking about you, Voldemort!") 126 | ``` 127 | 128 | ### `bearychat.attachment` 129 | 130 | If hubot want to response more than text, emit `bearychat.attachment`: 131 | 132 | ``` 133 | robot.respond /念两句诗/, (res) -> 134 | robot.emit 'bearychat.attachment', 135 | # required 136 | message: res.message 137 | # requried 138 | text: '当时我就念了...' 139 | attachments: [ 140 | { 141 | color: '#cb3f20', 142 | text: '苟利国家生死以', 143 | }, 144 | { 145 | text: '岂因祸福避趋之', 146 | }, 147 | { 148 | images: [ 149 | {url: 'http://example.com/excited.jpg'}, 150 | ] 151 | } 152 | ] 153 | ``` 154 | 155 | ![art/res_reply.png](art/res_attachment.png) 156 | 157 | ## LICENSE 158 | 159 | MIT 160 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # hubot-bearychat 2 | 3 | [BearyChat](https://bearychat.com) hubot adapter 实现 4 | 5 | [![@BearyChat](http://openapi.beary.chat/badge.svg)](http://openapi.beary.chat/join) 6 | [![Build Status](https://travis-ci.org/bearyinnovative/hubot-bearychat.svg)](https://travis-ci.org/bearyinnovative/hubot-bearychat) 7 | [![npm version](https://badge.fury.io/js/hubot-bearychat.svg)](https://npmjs.com/package/hubot-bearychat) 8 | 9 | 10 | 11 | - [真的只需要 5 分钟就能设置好的教程](#%E7%9C%9F%E7%9A%84%E5%8F%AA%E9%9C%80%E8%A6%81-5-%E5%88%86%E9%92%9F%E5%B0%B1%E8%83%BD%E8%AE%BE%E7%BD%AE%E5%A5%BD%E7%9A%84%E6%95%99%E7%A8%8B) 12 | * [第一步:获取 hubot token](#%E7%AC%AC%E4%B8%80%E6%AD%A5%E8%8E%B7%E5%8F%96-hubot-token) 13 | * [第二步:使用 yeoman 创建 hubot 项目](#%E7%AC%AC%E4%BA%8C%E6%AD%A5%E4%BD%BF%E7%94%A8-yeoman-%E5%88%9B%E5%BB%BA-hubot-%E9%A1%B9%E7%9B%AE) 14 | * [第三步:复制粘贴 hubot token... 以及启动你的 hubot!](#%E7%AC%AC%E4%B8%89%E6%AD%A5%E5%A4%8D%E5%88%B6%E7%B2%98%E8%B4%B4-hubot-token-%E4%BB%A5%E5%8F%8A%E5%90%AF%E5%8A%A8%E4%BD%A0%E7%9A%84-hubot) 15 | * [第四步:开始和 hubot 在讨论组中聊天](#%E7%AC%AC%E5%9B%9B%E6%AD%A5%E5%BC%80%E5%A7%8B%E5%92%8C-hubot-%E5%9C%A8%E8%AE%A8%E8%AE%BA%E7%BB%84%E4%B8%AD%E8%81%8A%E5%A4%A9) 16 | - [Mode](#mode) 17 | * [RTM mode](#rtm-mode) 18 | * [HTTP mode](#http-mode) 19 | - [配置项](#%E9%85%8D%E7%BD%AE%E9%A1%B9) 20 | - [Hubot 响应](#hubot-%E5%93%8D%E5%BA%94) 21 | * [普通回复 `Send`](#%E6%99%AE%E9%80%9A%E5%9B%9E%E5%A4%8D-send) 22 | * [at 回复 `Reply`](#at-%E5%9B%9E%E5%A4%8D-reply) 23 | * [富文本回复 `bearychat.attachment`](#%E5%AF%8C%E6%96%87%E6%9C%AC%E5%9B%9E%E5%A4%8D-bearychatattachment) 24 | - [LICENSE](#license) 25 | 26 | 27 | 28 | ## 真的只需要 5 分钟就能设置好的教程 29 | 30 | ### 第一步:获取 hubot token 31 | 32 | 在你的团队机器人页面 (your-cool-team.bearychat.com/robots) 创建一个 hubot 机器人。 33 | 你可以在机器人详情表单中获取到 hubot token: 34 | 35 | ![art/create_hubot.png](art/create_hubot.png) 36 | 37 | ### 第二步:使用 yeoman 创建 hubot 项目 38 | 39 | - `npm install -g hubot coffee-script yo generator-hubot` 40 | - `mkdir -p /path/to/hubot` 41 | - `cd /path/to/hubot` 42 | - `yo hubot` 43 | - `npm install hubot-bearychat --save` 44 | 45 | 同时你也可以访问 hubot 的[官方文档](https://github.com/github/hubot/tree/master/docs)获得更多帮助。 46 | 47 | ### 第三步:复制粘贴 hubot token... 以及启动你的 hubot! 48 | 49 | ```shell 50 | $ export HUBOT_BEARYCHAT_TOKENS=token-token-token-here 51 | $ export HUBOT_BEARYCHAT_MODE=rtm 52 | $ ./bin/hubot -a bearychat 53 | ``` 54 | 55 | ### 第四步:开始和 hubot 在讨论组中聊天 56 | 57 | ![art/bot_chat.png](art/bot_chat.png) 58 | 59 | 你还可以查看 [example/](example) 查看样例配置。 60 | 61 | ## Mode 62 | 63 | ### RTM mode 64 | 65 | RTM 模式使用 BearyChat 的 RTM api 和 WebSocket 作为 hubot 的底层消息通讯协议。 66 | 在这个模式下,hubot 可以实时获取到所有已加入讨论组的消息。 67 | 68 | 要启用该模式,需要设置环境变量 `HUBOT_BEARYCHAT_MODE=rtm`. 69 | 70 | 目前 `hubot-bearychat` 默认使用该模式。 71 | 72 | ### HTTP mode 73 | 74 | HTTP 模式是兼容的底层消息通讯协议。在这个模式中,hubot 只能接收到讨论组中明确 75 | 提及到的消息(例如 `@hubot 今天吃啥`)。同时你需要在 hubot 设置中填写 hubot 的 76 | HTTP 服务地址。 77 | 78 | 要启用该模式,需要设置环境变量 `HUBOT_BEARYCHAT_MODE=http`. 79 | 80 | ## 配置项 81 | 82 | `hubot-bearychat` 采用环境变量进行配置注入: 83 | 84 | | 配置项 | 说明 | 85 | |:------:|:------------| 86 | | `HUBOT_BEARYCHAT_MODE` | hubot 使用的底层消息通讯协议,默认为 `rtm` | 87 | | `HUBOT_BEARYCHAT_TOKENS` | hubot token, 必填 | 88 | 89 | ## Hubot 响应 90 | 91 | ### 普通回复 `Send` 92 | 93 | Hubot 可以通过 `res.send` 来回复一个消息: 94 | 95 | ``` 96 | robot.hear /hello/, (res) -> 97 | res.send 'hello, world!' 98 | ``` 99 | 100 | ![art/res_send.png](art/res_send.png) 101 | 102 | ### 回复 `Reply` 103 | 104 | 如果 hubot 在回复消息的同时想要引用作者的消息,可以使用 `res.reply`: 105 | 106 | ``` 107 | robot.hear /how old are you?/, (res) -> 108 | res.reply 'I am Five!' 109 | ``` 110 | 111 | ![art/res_reply.png](art/res_reply.png) 112 | 113 | ### 向其他讨论组发消息 114 | 115 | 如果想让 Hubot 往非当前对话的讨论组发送消息可以使用 `robot.messageRoom`, 参数是 vchannelId,讨论组名称也即将支持。 116 | 117 | ``` 118 | robot.hear /voldemort/i, (res) -> 119 | robot.messageRoom(vchannel_id, "Somebody is talking about you, Voldemort!") 120 | ``` 121 | 122 | ### 富文本回复 `bearychat.attachment` 123 | 124 | 如果 hubot 想要回复富文本消息,可以发送 `bearychat.attachment` 事件: 125 | 126 | ``` 127 | robot.respond /念两句诗/, (res) -> 128 | robot.emit 'bearychat.attachment', 129 | # required 130 | message: res.message 131 | # requried 132 | text: '当时我就念了...' 133 | attachments: [ 134 | { 135 | color: '#cb3f20', 136 | text: '苟利国家生死以', 137 | }, 138 | { 139 | text: '岂因祸福避趋之', 140 | }, 141 | { 142 | images: [ 143 | {url: 'http://example.com/excited.jpg'}, 144 | ] 145 | } 146 | ] 147 | ``` 148 | 149 | ![art/res_reply.png](art/res_attachment.png) 150 | 151 | ## LICENSE 152 | 153 | MIT 154 | -------------------------------------------------------------------------------- /art/bot_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearyinnovative/hubot-bearychat/ab2d52d3022a11749105b751d488161caa0e3188/art/bot_chat.png -------------------------------------------------------------------------------- /art/create_hubot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearyinnovative/hubot-bearychat/ab2d52d3022a11749105b751d488161caa0e3188/art/create_hubot.png -------------------------------------------------------------------------------- /art/res_attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearyinnovative/hubot-bearychat/ab2d52d3022a11749105b751d488161caa0e3188/art/res_attachment.png -------------------------------------------------------------------------------- /art/res_reply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearyinnovative/hubot-bearychat/ab2d52d3022a11749105b751d488161caa0e3188/art/res_reply.png -------------------------------------------------------------------------------- /art/res_send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearyinnovative/hubot-bearychat/ab2d52d3022a11749105b751d488161caa0e3188/art/res_send.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store* 3 | .hubot_history 4 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: run 2 | 3 | run: 4 | rm -rf node_modules/hubot-bearychat 5 | npm install 6 | ./bin/hubot -a bearychat 7 | -------------------------------------------------------------------------------- /example/bin/hubot: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | npm install 6 | export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH" 7 | 8 | exec node_modules/.bin/hubot --name "example" "$@" 9 | -------------------------------------------------------------------------------- /example/bin/hubot.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | call npm install 4 | SETLOCAL 5 | SET PATH=node_modules\.bin;node_modules\hubot\node_modules\.bin;%PATH% 6 | 7 | node_modules\.bin\hubot.cmd --name "example" %* 8 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "author": "hbc ", 6 | "description": "example for hubot-bearychat", 7 | "dependencies": { 8 | "hubot": "^2.19.0", 9 | "hubot-bearychat": "file:../" 10 | }, 11 | "engines": { 12 | "node": "0.10.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/scripts/example.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Example scripts for you to examine and try out. 3 | # 4 | # Notes: 5 | # They are commented out by default, because most of them are pretty silly and 6 | # wouldn't be useful and amusing enough for day to day huboting. 7 | # Uncomment the ones you want to try and experiment with. 8 | # 9 | # These are from the scripting documentation: https://github.com/github/hubot/blob/master/docs/scripting.md 10 | 11 | module.exports = (robot) -> 12 | 13 | # robot.hear /badger/i, (res) -> 14 | # res.send "Badgers? BADGERS? WE DON'T NEED NO STINKIN BADGERS" 15 | # 16 | # robot.respond /open the (.*) doors/i, (res) -> 17 | # doorType = res.match[1] 18 | # if doorType is "pod bay" 19 | # res.reply "I'm afraid I can't let you do that." 20 | # else 21 | # res.reply "Opening #{doorType} doors" 22 | # 23 | # robot.hear /I like pie/i, (res) -> 24 | # res.emote "makes a freshly baked pie" 25 | # 26 | # lulz = ['lol', 'rofl', 'lmao'] 27 | # 28 | # robot.respond /lulz/i, (res) -> 29 | # res.send res.random lulz 30 | # 31 | # robot.topic (res) -> 32 | # res.send "#{res.message.text}? That's a Paddlin'" 33 | # 34 | # 35 | # enterReplies = ['Hi', 'Target Acquired', 'Firing', 'Hello friend.', 'Gotcha', 'I see you'] 36 | # leaveReplies = ['Are you still there?', 'Target lost', 'Searching'] 37 | # 38 | # robot.enter (res) -> 39 | # res.send res.random enterReplies 40 | # robot.leave (res) -> 41 | # res.send res.random leaveReplies 42 | # 43 | # answer = process.env.HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING 44 | # 45 | # robot.respond /what is the answer to the ultimate question of life/, (res) -> 46 | # unless answer? 47 | # res.send "Missing HUBOT_ANSWER_TO_THE_ULTIMATE_QUESTION_OF_LIFE_THE_UNIVERSE_AND_EVERYTHING in environment: please set and try again" 48 | # return 49 | # res.send "#{answer}, but what is the question?" 50 | # 51 | # robot.respond /you are a little slow/, (res) -> 52 | # setTimeout () -> 53 | # res.send "Who you calling 'slow'?" 54 | # , 60 * 1000 55 | # 56 | # annoyIntervalId = null 57 | # 58 | # robot.respond /annoy me/, (res) -> 59 | # if annoyIntervalId 60 | # res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH" 61 | # return 62 | # 63 | # res.send "Hey, want to hear the most annoying sound in the world?" 64 | # annoyIntervalId = setInterval () -> 65 | # res.send "AAAAAAAAAAAEEEEEEEEEEEEEEEEEEEEEEEEIIIIIIIIHHHHHHHHHH" 66 | # , 1000 67 | # 68 | # robot.respond /unannoy me/, (res) -> 69 | # if annoyIntervalId 70 | # res.send "GUYS, GUYS, GUYS!" 71 | # clearInterval(annoyIntervalId) 72 | # annoyIntervalId = null 73 | # else 74 | # res.send "Not annoying you right now, am I?" 75 | # 76 | # 77 | # robot.router.post '/hubot/chatsecrets/:room', (req, res) -> 78 | # room = req.params.room 79 | # data = JSON.parse req.body.payload 80 | # secret = data.secret 81 | # 82 | # robot.messageRoom room, "I have a secret: #{secret}" 83 | # 84 | # res.send 'OK' 85 | # 86 | # robot.error (err, res) -> 87 | # robot.logger.error "DOES NOT COMPUTE" 88 | # 89 | # if res? 90 | # res.reply "DOES NOT COMPUTE" 91 | # 92 | # robot.respond /have a soda/i, (res) -> 93 | # # Get number of sodas had (coerced to a number). 94 | # sodasHad = robot.brain.get('totalSodas') * 1 or 0 95 | # 96 | # if sodasHad > 4 97 | # res.reply "I'm too fizzy.." 98 | # 99 | # else 100 | # res.reply 'Sure!' 101 | # 102 | # robot.brain.set 'totalSodas', sodasHad+1 103 | # 104 | # robot.respond /sleep it off/i, (res) -> 105 | # robot.brain.set 'totalSodas', 0 106 | # res.reply 'zzzzz' 107 | robot.respond /foo/i, (res) -> 108 | res.send 'bar' 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-bearychat", 3 | "version": "0.7.2", 4 | "description": "BearyChat adapter for hubot", 5 | "main": "./src/bearychat", 6 | "keywords": [ 7 | "hubot", 8 | "adapter", 9 | "bearychat" 10 | ], 11 | "author": "BearyInnovative Technologies, Inc.", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:bearyinnovative/hubot-bearychat.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/bearyinnovative/hubot-bearychat/issues" 18 | }, 19 | "dependencies": { 20 | "bearychat": "^1.0.0", 21 | "ws": "^1.1.1" 22 | }, 23 | "peerDependencies": { 24 | "hubot": ">=2.0" 25 | }, 26 | "devDependencies": { 27 | "coffee-script": ">=1.2.0", 28 | "jest-cli": "^20.0.4" 29 | }, 30 | "scripts": { 31 | "build-doc": "markdown-toc -i README.md && markdown-toc -i README_CN.md", 32 | "test": "jest" 33 | }, 34 | "license": "MIT", 35 | "contributors": [ 36 | { 37 | "name": "hbc" 38 | }, 39 | { 40 | "name": "currantxx" 41 | }, 42 | { 43 | "name": "yuanbohan", 44 | "email": "yuanbo.han@bearyinnovative.com", 45 | "url": "https://github.com/yuanbohan" 46 | }, 47 | { 48 | "name": "timnew", 49 | "email": "timnewcnyn@gmail.com", 50 | "url": "http://timnew.me" 51 | } 52 | ], 53 | "jest": { 54 | "testEnvironment": "node" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /scripts/attachments_example.coffee: -------------------------------------------------------------------------------- 1 | # Description: 2 | # Example scripts for you to examine attachments and try out. 3 | # 4 | # Commands: 5 | # hubot attachments - show you one attachments format message 6 | 7 | module.exports = (robot) -> 8 | 9 | robot.respond /attachments/i, (res) -> 10 | 11 | robot.emit "bearychat.attachment", 12 | message: res.message 13 | text: "text, this field accept `markdown`" 14 | attachments: [{ 15 | title: "attachment title", 16 | text: "attachment text", 17 | color: "#ffa500", 18 | images: [{url: "http://img7.doubanio.com/icon/ul15067564-30.jpg"}] 19 | }] 20 | -------------------------------------------------------------------------------- /src/bearychat.coffee: -------------------------------------------------------------------------------- 1 | {Adapter} = require 'hubot' 2 | bearychat = require 'bearychat' 3 | 4 | HTTPClient = require './http_client' 5 | RTMClient = require './rtm_client' 6 | { 7 | EventConnected, 8 | EventMessage, 9 | EventClosed, 10 | EventError, 11 | } = require './client_event' 12 | 13 | class BearyChatAdapter extends Adapter 14 | 15 | _setupClient: (mode) -> 16 | if mode && mode.toLowerCase() is 'http' 17 | @robot.logger.info 'Connect using HTTP mode' 18 | @client = new HTTPClient 19 | else 20 | @robot.logger.info 'Connect using RTM mode' 21 | @client = new RTMClient 22 | 23 | @client.on EventConnected, @handleConnected.bind(@) 24 | @client.on EventMessage, @receive.bind(@) 25 | @client.on EventClosed, @handleClosed.bind(@) 26 | @client.on EventError, @handleError.bind(@) 27 | 28 | _setupEvents: (tokens) -> 29 | @robot.on 'bearychat.attachment', (data) => 30 | room = data.message.room 31 | text = data.text 32 | unless room and room.vchannelId and text 33 | @robot.logger.error 'invalid attachment message payload', data 34 | return 35 | 36 | bearychat.message.create({ 37 | token: tokens[0], 38 | vchannel_id: room.vchannelId, 39 | text: text, 40 | attachments: (data.attachments || []), 41 | }) 42 | .catch((err) => @robot.logger.error 'send message failed', err) 43 | 44 | send: (envelope, strings...) -> 45 | if envelope.room and envelope.room.vchannelId 46 | message = @client.packMessage false, envelope, strings 47 | @client.sendMessage envelope, message 48 | else # robot.messageRoom 49 | message = {text: strings[0]} 50 | @client.sendMessageToRoom envelope, message 51 | 52 | reply: (envelope, strings...) -> 53 | message = @client.packMessage true, envelope, strings 54 | @client.sendMessage envelope, message 55 | 56 | run: -> 57 | tokens = (process.env.HUBOT_BEARYCHAT_TOKENS || '').split(',') 58 | unless tokens and tokens.length > 0 59 | @robot.logger.error 'No BearyChat tokens provided' 60 | return 61 | 62 | mode = (process.env.HUBOT_BEARYCHAT_MODE || '').toLowerCase() 63 | 64 | @_setupClient mode 65 | @_setupEvents tokens 66 | 67 | @client.run tokens, @robot 68 | 69 | handleConnected: -> 70 | @emit 'connected' 71 | 72 | handleClosed: -> 73 | @robot.logger.error 'client closed' 74 | 75 | handleError: (e) -> 76 | @robot.logger.error "client error #{e}" 77 | 78 | exports.use = (robot) -> 79 | new BearyChatAdapter(robot) 80 | -------------------------------------------------------------------------------- /src/client_event.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | EventConnected: 'connected' 3 | EventMessage: 'message' 4 | EventClosed: 'closed' 5 | EventError: 'error' 6 | EventUserChanged: 'userchanged' 7 | EventSignedIn: 'signedin' 8 | EventTimedout: 'timedout' 9 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | function decodeMention (text, userId, replaceName) { 2 | return text.replace( 3 | /(@)<=(.*?)=\>/g, 4 | function(_, mentionMark, mentionedUserId) { 5 | if (mentionedUserId === userId) { return replaceName; } 6 | return mentionedUserId; 7 | }) 8 | } 9 | 10 | module.exports = { 11 | decodeMention 12 | }; 13 | -------------------------------------------------------------------------------- /src/helpers.test.js: -------------------------------------------------------------------------------- 1 | const decodeMention = require('./helpers.js').decodeMention; 2 | 3 | test('decodeMention should work well', () => { 4 | expect(decodeMention("hello @<==abc=>")).toBe("hello =abc"); 5 | expect(decodeMention("hello @<==abc=> @<==xyz=>")).toBe("hello =abc =xyz"); 6 | }); 7 | 8 | -------------------------------------------------------------------------------- /src/http_client.coffee: -------------------------------------------------------------------------------- 1 | EventEmitter = require 'events' 2 | bearychat = require 'bearychat' 3 | 4 | { 5 | User, 6 | TextMessage, 7 | } = require 'hubot' 8 | 9 | { 10 | EventConnected, 11 | EventMessage, 12 | EventError, 13 | } = require './client_event' 14 | 15 | class HTTPClient extends EventEmitter 16 | run: (@tokens, @robot) -> 17 | @robot.router.post '/bearychat', @receiveMessageCallback.bind(@) 18 | 19 | @emit EventConnected 20 | 21 | sendMessage: (envelope, message) -> 22 | {token} = envelope.user 23 | message = Object.assign {token: token, attachments: []}, message 24 | bearychat.message.create(message).catch (err) => 25 | @emit(EventError, err) 26 | @robot.logger.error 'send message failed', err 27 | 28 | sendMessageToRoom: (envelope, message) -> 29 | vchannelId = envelope.room 30 | bearychat.message.create({ 31 | token: @tokens[0], 32 | vchannel_id: vchannelId, 33 | text: message.text, 34 | attachments: message.attachments or [] 35 | }) 36 | 37 | packMessage: (isReply, envelope, [text, opts]) -> 38 | text = "@#{envelope.user.name}: #{text}" if isReply 39 | Object.assign opts || {}, { 40 | sender: envelope.user.sender, 41 | vchannel_id: envelope.user.vchannel, 42 | text: text 43 | } 44 | 45 | receiveMessageCallback: (req, res) -> 46 | body = req.body 47 | unless body 48 | @robot.logger.error('No body provided for this request') 49 | return 50 | 51 | unless @isValidToken(body.token) 52 | res.status(404).end() 53 | @robot.logger.error('Invalid token sent for this request') 54 | return 55 | 56 | res.status(200).end() 57 | 58 | text = "#{@robot.name} #{body.text}" 59 | user = new User(body.sender, { 60 | team: body.subdomain, 61 | token: body.token, 62 | sender: body.sender, 63 | vchannel: body.vchannel, 64 | name: body.username, 65 | room: { 66 | vchannelId: body.vchannel, 67 | } 68 | }) 69 | 70 | @emit(EventMessage, new TextMessage(user, text, body.key)) 71 | 72 | isValidToken: (token) -> 73 | @tokens.indexOf(token) != -1 74 | 75 | module.exports = HTTPClient 76 | -------------------------------------------------------------------------------- /src/rtm_client.coffee: -------------------------------------------------------------------------------- 1 | EventEmitter = require 'events' 2 | WebSocket = require 'ws' 3 | { User, TextMessage } = require 'hubot' 4 | bearychat = require 'bearychat' 5 | rtm = bearychat.rtm 6 | 7 | decodeMention = require('./helpers').decodeMention 8 | 9 | { 10 | EventConnected, 11 | EventMessage, 12 | EventError, 13 | EventClosed, 14 | EventUserChanged, 15 | EventSignedIn, 16 | EventTimedout, 17 | } = require './client_event' 18 | 19 | shouldHandleThisMessage = (message) -> 20 | rtm.message.isChatMessage(message) 21 | 22 | class RTMClient extends EventEmitter 23 | # a retry flow contains 3 parts: backoff waiting, ws url fetching and ws conecting. 24 | # if RTMClient in a retryflow, then @isRetrying must be true else false. 25 | # @retryMax is how many times RTMClient can go into retry flow after it disconnected. 26 | constructor: (opts) -> 27 | opts = opts || {} 28 | @retryMax = opts.retryMax or 10 29 | @rtmPingInterval = opts.rtmPingInterval or 2000 30 | @resetRetryTimes() 31 | @isRetrying = false 32 | 33 | resetRetryTimes: () -> 34 | @retryTimes = 0 35 | 36 | run: (tokens, @robot) -> 37 | @token = tokens[0] 38 | unless @token 39 | @robot.logger.error 'No BearyChat RTM token provided' 40 | return 41 | 42 | rtm.start({token: @token}) 43 | .then (resp) => 44 | resp.json() 45 | .then ({ user, ws_host }) => 46 | unless user and ws_host 47 | throw new Error("Fetching WebSocket URL and user data failed") 48 | @robot.logger.info "Connected as @#{user.name}" 49 | @user = user 50 | @emit EventUserChanged, @user 51 | @emit EventSignedIn 52 | @connectToRTM ws_host 53 | .catch (e) => 54 | @emit EventError, e 55 | @isRetrying = false 56 | @rerun() 57 | 58 | clearPingInterval: () -> 59 | if @pingInterval 60 | clearInterval(@pingInterval) 61 | 62 | rerun: () -> 63 | if @isRetrying 64 | return 65 | @isRetrying = true 66 | @retryTimes++ 67 | if (@retryTimes <= @retryMax) 68 | retryBackoff = 1000 * Math.pow(2, @retryTimes - 1) 69 | @robot.logger.info "Retry to connect server #{@retryTimes} times, wait for #{retryBackoff / 1000} second" 70 | @clearPingInterval() 71 | setTimeout () => 72 | @run([@token], @robot) 73 | , retryBackoff 74 | else 75 | @robot.logger.info "Retry #{@retryTimes} times, reach to max, stop retry." 76 | @emit EventTimedout 77 | @clearPingInterval() 78 | @robot.shutdown() 79 | 80 | packMessage: (isReply, envelope, strings) -> 81 | text = strings.join '\n' 82 | if isReply 83 | rtm.message.refer envelope.user.message, text 84 | else 85 | rtm.message.reply envelope.user.message, text 86 | 87 | sendMessage: (envelope, message) -> 88 | @writeWebSocket message 89 | 90 | sendMessageToRoom: (envelope, message) -> 91 | vchannelId = envelope.room 92 | bearychat.message.create({ 93 | token: @token, 94 | vchannel_id: vchannelId, 95 | text: message.text, 96 | attachments: [] 97 | }) 98 | 99 | connectToRTM: (wsHost) -> 100 | @rtmCallId = 0 101 | @rtmConn = new WebSocket wsHost 102 | 103 | @rtmConn.on 'open', @onWebSocketOpen.bind(@) 104 | @rtmConn.on 'close', @onWebSocketClose.bind(@) 105 | @rtmConn.on 'error', @onWebSocketError.bind(@) 106 | @rtmConn.on 'message', @onWebSocketMessage.bind(@) 107 | 108 | nextRTMCallId: () -> @rtmCallId++ 109 | 110 | rtmPing: () -> 111 | @writeWebSocket 112 | type: rtm.message.type.PING 113 | 114 | writeWebSocket: (message) -> 115 | unless message.call_id 116 | message.call_id = @nextRTMCallId() 117 | 118 | @rtmConn.send JSON.stringify message 119 | 120 | onWebSocketOpen: () -> 121 | @emit EventConnected 122 | @isRetrying = false 123 | @resetRetryTimes() 124 | @clearPingInterval() 125 | @pingInterval = setInterval @rtmPing.bind(@), @rtmPingInterval 126 | 127 | onWebSocketClose: () -> 128 | @emit EventClosed 129 | @isRetrying = false # make sure unsuccessful connection should stop current retryflow 130 | @rerun() 131 | 132 | onWebSocketError: (err) -> 133 | @emit EventError, err 134 | 135 | onWebSocketMessage: (data) -> 136 | message = JSON.parse data 137 | 138 | return unless shouldHandleThisMessage message 139 | 140 | fromUserId = message.uid or message.robot_id 141 | 142 | # ignore message from robot himself 143 | return if fromUserId is @user.id 144 | 145 | messageText = decodeMention message.text, @user.id, @robot.name 146 | if rtm.message.isP2P(message) and not messageText.startsWith @robot.name 147 | messageText = "#{@robot.name} #{messageText}" 148 | 149 | messageUser = new User fromUserId, 150 | message: message 151 | room: 152 | vchannelId: message.vchannel_id 153 | 154 | @emit EventMessage, new TextMessage messageUser, messageText, message.key 155 | 156 | module.exports = RTMClient 157 | --------------------------------------------------------------------------------