├── .gitignore ├── package.json ├── README.md └── adapter.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-zulip", 3 | "version": "0.1.0", 4 | "description": "Hubot adapter for Zulip", 5 | "main": "adapter.coffee", 6 | "dependencies": { 7 | "zulip": "~0.1.0" 8 | }, 9 | "devDependencies": { 10 | "coffee-script": "~1.6.3" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": ["zulip", "hubot"], 16 | "repository":"https://github.com/zulip/hubot-zulip", 17 | "author": "Zulip, Inc.", 18 | "license": "MIT" 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zulip adapter for Hubot 2 | 3 | Follow the [Getting Started with Hubot](https://hubot.github.com/docs/) page to create your Hubot. 4 | 5 | In your Hubot's directory, run: 6 | 7 | npm install --save hubot-zulip 8 | 9 | On your [Zulip settings page](https://zulip.com/#settings), create a bot account. Note its email and API key; you will use them on the next step. 10 | 11 | The bot account email address and API key are passed to Hubot via environment variables `HUBOT_ZULIP_BOT` and `HUBOT_ZULIP_API_KEY`. 12 | 13 | By default, the bot will listen on all public streams. If you set 14 | `HUBOT_ZULIP_ONLY_SUBSCRIBED_STREAMS`, it will only listen on the 15 | streams that the bot is subscribed to. 16 | 17 | To run Hubot locally, use: 18 | 19 | HUBOT_ZULIP_BOT=hubot-bot@example.com HUBOT_ZULIP_API_KEY=your_key bin/hubot -a zulip 20 | 21 | To run Hubot with a self-hosted version of Zulip, use: 22 | 23 | HUBOT_ZULIP_SITE=https://zulip.example.com HUBOT_ZULIP_BOT=hubot-bot@example.com HUBOT_ZULIP_API_KEY=your_key bin/hubot -a zulip 24 | 25 | To run Hubot on Heroku, edit `Procfile` to change the `-a` option to `-a zulip`. Use the following commands to set the environment variables: 26 | 27 | heroku config:add HUBOT_ZULIP_SITE=https://example.zulipchat.com/api 28 | heroku config:add HUBOT_ZULIP_API_KEY=your_key 29 | heroku config:add HUBOT_ZULIP_BOT=hubot-bot@example.com 30 | -------------------------------------------------------------------------------- /adapter.coffee: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright © 2013 Zulip, Inc. 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | 22 | zulip = require('zulip') 23 | {Adapter, TextMessage, EnterMessage, LeaveMessage, User} = require "../hubot/index" 24 | 25 | class Zulip extends Adapter 26 | send: (envelope, strings...) -> 27 | for content in strings 28 | {type, to, subject} = parse_room(envelope.room) 29 | @zulip.sendMessage {type, to, subject, content} 30 | console.log "Sending", {type, to, subject, content} 31 | 32 | emote: (envelope, strings...) -> 33 | @send envelope, strings.map((str) -> "**#{str}**")... 34 | 35 | reply: (envelope, strings...) -> 36 | @send envelope, strings.map((str) -> "@**#{envelope.user.name}**: #{str}")... 37 | 38 | run: -> 39 | @connected = false 40 | 41 | @zulip = new zulip.Client 42 | client_name: "Hubot" 43 | email: process.env.HUBOT_ZULIP_BOT 44 | api_key: process.env.HUBOT_ZULIP_API_KEY 45 | site: process.env.HUBOT_ZULIP_SITE 46 | 47 | @zulip.registerEventQueue 48 | event_types: ['message'] 49 | all_public_streams: !process.env.HUBOT_ZULIP_ONLY_SUBSCRIBED_STREAMS? 50 | 51 | @zulip.on 'registered', (resp) => 52 | if not @connected 53 | @emit 'connected' 54 | @connected = true 55 | 56 | # Zulip autocompleted @-mentions look like "@**Hubot**". Remove 57 | # the stars so hubot sees it. 58 | name = @robot.name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') 59 | @mention_regex = new RegExp("^[@]\\*\\*(#{name})\\*\\*", 'i') 60 | 61 | @zulip.on 'message', (msg) => 62 | return if msg.sender_email is @zulip.email 63 | 64 | author = this._author_for_message(msg) 65 | 66 | content = msg.content.replace(@mention_regex, '@$1') 67 | console.log(@mention_regex, content) 68 | 69 | message = new TextMessage author, content, msg.id 70 | console.log "Received", message 71 | @receive(message) 72 | 73 | _author_for_message: (msg) -> 74 | author = @robot.brain.userForId msg.sender_email, 75 | name: msg.sender_full_name 76 | email_address: msg.sender_email 77 | # Work around github/hubot#670 by setting room separately. If we pass it 78 | # to userForId, it could delete the existing user from the brain. 79 | author.room = room_for_message(msg) 80 | author 81 | 82 | 83 | exports.use = (robot) -> 84 | new Zulip robot 85 | 86 | encode = (s) -> 87 | s.replace(/%/g, '%25') 88 | .replace(/\+/g, '%2B') 89 | .replace(/:/g, '%3A') 90 | .replace(/[ ]/g, '+') 91 | 92 | decode = (s) -> 93 | s.replace(/\+/g, ' ') 94 | .replace(/%3A/g, ':') 95 | .replace(/%2B/g, '+') 96 | .replace(/%25/g, '%') 97 | 98 | room_for_message = (msg) -> 99 | if msg.type == 'private' 100 | recipient_list = (user.email for user in msg.display_recipient) 101 | "pm-with:#{encode(recipient_list.join(','))}" 102 | else 103 | "stream:#{encode(msg.display_recipient)} topic:#{encode(msg.subject)}" 104 | 105 | parse_room = (room) -> 106 | if m = room.match(/^pm-with:(.*)$/) 107 | {type:'private', to:decode(m[1]).split(',')} 108 | else if m = room.match(/stream:(.*) topic:(.*)/) 109 | {type:'stream', to:[decode(m[1])], subject:decode(m[2])} 110 | else 111 | throw new Error("Couldn't parse room: '#{room}'") 112 | --------------------------------------------------------------------------------