├── .rspec ├── VERSION ├── .gitignore ├── .document ├── .travis.yml ├── lib ├── flowdock │ └── capistrano.rb └── flowdock.rb ├── spec ├── spec_helper.rb └── flowdock_spec.rb ├── Gemfile ├── LICENSE ├── Rakefile ├── flowdock.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.7.1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | pkg/* 4 | Gemfile.lock 5 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | MIT-LICENSE 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | # - 2.0 3 | # - 2.1 4 | - 2.2 5 | - 2.3 6 | - 2.4 7 | - jruby 8 | -------------------------------------------------------------------------------- /lib/flowdock/capistrano.rb: -------------------------------------------------------------------------------- 1 | warn "Capistrano notifier has been extracted to `capistrano-flowdock` gem" 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 3 | require 'rspec' 4 | require 'webmock/rspec' 5 | 6 | require 'flowdock' 7 | 8 | # Requires supporting files with custom matchers and macros, etc, 9 | # in ./support/ and its subdirectories. 10 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} 11 | 12 | RSpec.configure do |config| 13 | 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | # Add dependencies required to use your gem here. 3 | 4 | gem "httparty", "~> 0.10.0" 5 | gem "multi_json" 6 | gem "rack", "~>1.6.8" 7 | gem "rake", "< 11.0" 8 | 9 | # Add dependencies to develop your gem here. 10 | # Include everything needed to run rake, tests, features, etc. 11 | group :development do 12 | gem 'rake', '< 11.0' 13 | gem "rdoc", ">= 2.4.2" 14 | gem "rspec", "~> 2.6" 15 | gem "webmock" 16 | gem "bundler", "~> 1.0" 17 | gem "jeweler", ">= 2.0.1" 18 | gem "jruby-openssl", :platforms => :jruby 19 | end 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Flowdock Ltd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler' 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require 'rake' 13 | 14 | require 'jeweler' 15 | Jeweler::Tasks.new do |gem| 16 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 17 | gem.name = "flowdock" 18 | gem.homepage = "http://github.com/flowdock/flowdock-api" 19 | gem.license = "MIT" 20 | gem.summary = %Q{Ruby Gem for using Flowdock's API} 21 | gem.email = "team@flowdock.com" 22 | gem.authors = ["Antti Pitkänen"] 23 | # dependencies defined in Gemfile 24 | end 25 | Jeweler::RubygemsDotOrgTasks.new 26 | 27 | require 'rspec/core' 28 | require 'rspec/core/rake_task' 29 | RSpec::Core::RakeTask.new(:spec) do |spec| 30 | spec.pattern = FileList['spec/**/*_spec.rb'] 31 | end 32 | 33 | task :default => :spec 34 | 35 | require 'rdoc/task' 36 | RDoc::Task.new do |rdoc| 37 | version = File.exist?('VERSION') ? File.read('VERSION') : "" 38 | 39 | rdoc.rdoc_dir = 'rdoc' 40 | rdoc.title = "flowdock #{version}" 41 | rdoc.rdoc_files.include('README*') 42 | rdoc.rdoc_files.include('lib/**/*.rb') 43 | end 44 | -------------------------------------------------------------------------------- /flowdock.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | # stub: flowdock 0.7.1 ruby lib 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "flowdock" 9 | s.version = "0.7.1" 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.require_paths = ["lib"] 13 | s.authors = ["Antti Pitk\u{e4}nen"] 14 | s.date = "2015-11-17" 15 | s.email = "team@flowdock.com" 16 | s.extra_rdoc_files = [ 17 | "LICENSE", 18 | "README.md" 19 | ] 20 | s.files = [ 21 | ".document", 22 | ".rspec", 23 | ".travis.yml", 24 | "Gemfile", 25 | "LICENSE", 26 | "README.md", 27 | "Rakefile", 28 | "VERSION", 29 | "flowdock.gemspec", 30 | "lib/flowdock.rb", 31 | "lib/flowdock/capistrano.rb", 32 | "spec/flowdock_spec.rb", 33 | "spec/spec_helper.rb" 34 | ] 35 | s.homepage = "http://github.com/flowdock/flowdock-api" 36 | s.licenses = ["MIT"] 37 | s.rubygems_version = "2.4.8" 38 | s.summary = "Ruby Gem for using Flowdock's API" 39 | 40 | if s.respond_to? :specification_version then 41 | s.specification_version = 4 42 | 43 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 44 | s.add_runtime_dependency(%q, ["~> 0.10.0"]) 45 | s.add_runtime_dependency(%q, [">= 0"]) 46 | s.add_development_dependency(%q, [">= 2.4.2"]) 47 | s.add_development_dependency(%q, ["~> 2.6"]) 48 | s.add_development_dependency(%q, [">= 0"]) 49 | s.add_development_dependency(%q, ["~> 1.0"]) 50 | s.add_development_dependency(%q, [">= 2.0.1"]) 51 | s.add_development_dependency(%q, [">= 0"]) 52 | else 53 | s.add_dependency(%q, ["~> 0.10.0"]) 54 | s.add_dependency(%q, [">= 0"]) 55 | s.add_dependency(%q, [">= 2.4.2"]) 56 | s.add_dependency(%q, ["~> 2.6"]) 57 | s.add_dependency(%q, [">= 0"]) 58 | s.add_dependency(%q, ["~> 1.0"]) 59 | s.add_dependency(%q, [">= 2.0.1"]) 60 | s.add_dependency(%q, [">= 0"]) 61 | end 62 | else 63 | s.add_dependency(%q, ["~> 0.10.0"]) 64 | s.add_dependency(%q, [">= 0"]) 65 | s.add_dependency(%q, [">= 2.4.2"]) 66 | s.add_dependency(%q, ["~> 2.6"]) 67 | s.add_dependency(%q, [">= 0"]) 68 | s.add_dependency(%q, ["~> 1.0"]) 69 | s.add_dependency(%q, [">= 2.0.1"]) 70 | s.add_dependency(%q, [">= 0"]) 71 | end 72 | end 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flowdock 2 | 3 | Ruby gem for using the Flowdock Push API. See the [Push API documentation](http://www.flowdock.com/api/push) for details. 4 | 5 | ## Build Status 6 | 7 | [![Build Status](https://secure.travis-ci.org/flowdock/flowdock-api.png)](http://travis-ci.org/flowdock/flowdock-api) 8 | 9 | The Flowdock gem is tested on Ruby 2.1 and JRuby. 10 | 11 | ## Dependencies 12 | 13 | * HTTParty 14 | * MultiJson 15 | 16 | ## Installing 17 | 18 | gem install flowdock 19 | 20 | If you're using JRuby, you'll also need to install the `jruby-openssl` gem. 21 | 22 | ## Usage 23 | 24 | To post content to a flow's chat or team inbox using `Flowdock::Flow`, you need to use the target flow's API token or a source's flow_token. 25 | 26 | Alternatively, you can use your personal API token and the `Flowdock::Client`. 27 | 28 | Personal and flow's tokens can be found on the [tokens page](https://www.flowdock.com/account/tokens). 29 | 30 | ### REST API 31 | 32 | To create an API client, you need your personal [API token](https://flowdock.com/account/tokens), an [OAuth token](https://www.flowdock.com/api/authentication) or a [source's flow_token](https://www.flowdock.com/api/sources). 33 | 34 | Note that a `flow_token` will only allow you to post [thread messages](https://www.flowdock.com/api/production-integrations#/post-inbox) to the flow that the source belongs to. 35 | 36 | ```ruby 37 | require 'rubygems' 38 | require 'flowdock' 39 | 40 | # Create a client that uses your personal API token to authenticate 41 | api_token_client = Flowdock::Client.new(api_token: '__MY_PERSONAL_API_TOKEN__') 42 | 43 | # Create a client that uses a source's flow_token to authenticate. Can only use post_to_thread 44 | flow_token_client = Flowdock::Client.new(flow_token: '__FLOW_TOKEN__') 45 | ``` 46 | 47 | #### Posting to Chat 48 | 49 | To send a chat message or comment, you can use `client.chat_message`: 50 | 51 | ```ruby 52 | flow_id = 'acdcabbacd0123456789' 53 | 54 | # Send a simple chat message 55 | api_token_client.chat_message(flow: flow_id, content: "I'm sending a message!", tags: ['foo', 'bar']) 56 | 57 | # Send a comment to message 1234 58 | api_token_client.chat_message(flow: flow_id, content: "Now I'm commenting!", message: 1234) 59 | ``` 60 | 61 | Both methods return the created message as a hash. 62 | 63 | #### Post a threaded messages 64 | 65 | You can post `activity` and `discussion` events to a [threaded conversation](https://www.flowdock.com/api/integration-getting-started) in Flowdock. 66 | 67 | ``` 68 | flow_token_client.post_to_thread( 69 | event: "activity", 70 | author: { 71 | name: "anttipitkanen", 72 | avatar: "https://avatars.githubusercontent.com/u/946511?v=2", 73 | }, 74 | title: "activity title", 75 | external_thread_id: "your-id-here", 76 | thread: { 77 | title: "this is required if you provide a thread field at all", 78 | body: "

some html content

", 79 | external_url: "https://example.com/issue/123", 80 | status: { 81 | color: "green", 82 | value: "open" 83 | } 84 | } 85 | ) 86 | ``` 87 | 88 | 89 | #### Arbitary API access 90 | 91 | You can use the client to access the Flowdock API in other ways, too. See the [REST API documentation](http://www.flowdock.com/api/rest) for all the resources. 92 | 93 | ```ruby 94 | 95 | # Fetch all my flows 96 | flows = client.get('/flows') 97 | 98 | # Update a flow's name 99 | client.put('/flows/acme/my_flow', name: 'Your flow') 100 | 101 | # Delete a message 102 | client.delete('/flows/acme/my_flow/messages/12345') 103 | 104 | # Create an invitation 105 | client.post('/flows/acme/my_flow/invitations', email: 'user@example.com', message: "I'm inviting you to our flow using api.") 106 | 107 | ``` 108 | 109 | ### Push API 110 | 111 | **Note:** the Push API is in the process of being deprecated. [Creating a source](https://www.flowdock.com/api/integration-getting-started) along with a flow_token is recommended instead. 112 | 113 | To use the Push API, you need the flow's API token: 114 | 115 | #### Posting to the chat 116 | 117 | ```ruby 118 | require 'rubygems' 119 | require 'flowdock' 120 | 121 | # create a new Flow object with target flow's API token and external user name (enough for posting to the chat) 122 | flow = Flowdock::Flow.new(:api_token => "__FLOW_API_TOKEN__", :external_user_name => "John") 123 | 124 | # send message to Chat 125 | flow.push_to_chat(:content => "Hello!", :tags => ["cool", "stuff"]) 126 | ``` 127 | 128 | #### Posting to the team inbox 129 | 130 | ```ruby 131 | # create a new Flow object with the target flow's API token and sender information 132 | flow = Flowdock::Flow.new(:api_token => "__FLOW_API_TOKEN__", 133 | :source => "myapp", :from => {:name => "John Doe", :address => "john.doe@example.com"}) 134 | 135 | # send message to Team Inbox 136 | flow.push_to_team_inbox(:subject => "Greetings from the Flowdock API gem!", 137 | :content => "

It works!

Now you can start developing your awesome application for Flowdock.

", 138 | :tags => ["cool", "stuff"], :link => "http://www.flowdock.com/") 139 | ``` 140 | 141 | #### Posting to multiple flows 142 | 143 | ```ruby 144 | require 'rubygems' 145 | require 'flowdock' 146 | 147 | # create a new Flow object with the API tokens of the target flows 148 | flow = Flowdock::Flow.new(:api_token => ["__FLOW_API_TOKEN__", "__ANOTHER_FLOW_API_TOKEN__"], ... ) 149 | 150 | # see the above examples of posting to the chat or team inbox 151 | ``` 152 | 153 | ## API methods 154 | 155 | * `Flowdock::Flow` methods 156 | 157 | `push_to_team_inbox` - Send message to the team inbox. See [API documentation](http://www.flowdock.com/api/team-inbox) for details. 158 | 159 | `push_to_chat` - Send message to the chat. See [API documentation](http://www.flowdock.com/api/chat) for details. 160 | 161 | `send_message(params)` - Deprecated. Please use `push_to_team_inbox` instead. 162 | 163 | * `Flowdock::Client` methods 164 | 165 | `chat_message` - Send message to chat. 166 | 167 | `post_to_thread` - Post messages to a team inbox thread. 168 | 169 | `post`, `get`, `put`, `delete` - Send arbitary api calls. First parameter is the path, second is data. See [REST API documentation](http://www.flowdock.com/api/rest). 170 | 171 | ## Deployment notifications 172 | 173 | There are separate gems for deployment notifications: 174 | 175 | * [capistrano-flowdock](https://github.com/flowdock/capistrano-flowdock) 176 | * [mina-flowdock](https://github.com/elskwid/mina-flowdock) 177 | 178 | ## Changelog 179 | * 0.7.0 - Added `post_to_thread` 180 | * 0.5.0 - Added `Flowdock::Client` that authenticates using user credentials and can be used to interact with the API. Better threads support for both `Flow` and `Client` so that comments can be made. 181 | 182 | ## Copyright 183 | 184 | Copyright (c) 2012 Flowdock Ltd. See LICENSE for further details. 185 | -------------------------------------------------------------------------------- /lib/flowdock.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'httparty' 3 | require 'multi_json' 4 | 5 | module Flowdock 6 | FLOWDOCK_API_URL = "https://api.flowdock.com/v1" 7 | 8 | class InvalidParameterError < StandardError; end 9 | class NotFoundError < StandardError; end 10 | class ApiError < StandardError; end 11 | 12 | module Helpers 13 | def blank?(var) 14 | var.nil? || var.respond_to?(:length) && var.length == 0 15 | end 16 | 17 | def handle_response(resp) 18 | body = (resp.body.nil? || resp.body.strip.empty?) ? '{}' : resp.body 19 | 20 | json = MultiJson.decode(body) 21 | 22 | if resp.code == 404 23 | raise NotFoundError, "Flowdock API returned error:\nStatus: #{resp.code}\n Message: #{json["message"]}" 24 | end 25 | 26 | unless resp.code >= 200 && resp.code < 300 27 | errors = json["errors"].map {|k,v| "#{k}: #{v.join(',')}"}.join("\n") unless json["errors"].nil? 28 | raise ApiError, "Flowdock API returned error:\nStatus: #{resp.code}\n Message: #{json["message"]}\n Errors:\n#{errors}" 29 | end 30 | json 31 | rescue MultiJson::DecodeError 32 | raise ApiError, "Flowdock API returned error:\nStatus: #{resp.code}\nBody: #{resp.body}" 33 | end 34 | end 35 | 36 | class Flow 37 | include HTTParty 38 | include Helpers 39 | attr_reader :api_token, :source, :project, :from, :external_user_name 40 | 41 | # Required options keys: :api_token 42 | # Optional keys: :external_user_name, :source, :project, :from => { :name, :address }, :reply_to 43 | def initialize(options = {}) 44 | @api_token = if options[:api_token].kind_of?(Array) 45 | options[:api_token].join(",") 46 | else 47 | options[:api_token].to_s 48 | end 49 | raise InvalidParameterError, "Flow must have :api_token attribute" if blank?(@api_token) 50 | 51 | @source = options[:source] || nil 52 | @project = options[:project] || nil 53 | @from = options[:from] || {} 54 | @reply_to = options[:reply_to] || nil 55 | @external_user_name = options[:external_user_name] || nil 56 | end 57 | 58 | def push_to_team_inbox(params) 59 | @source = params[:source] unless blank?(params[:source]) 60 | raise InvalidParameterError, "Message must have valid :source attribute, only alphanumeric characters and underscores can be used" if blank?(@source) || !@source.match(/^[a-z0-9\-_ ]+$/i) 61 | 62 | @project = params[:project] unless blank?(params[:project]) 63 | raise InvalidParameterError, "Optional attribute :project can only contain alphanumeric characters and underscores" if !blank?(@project) && !@project.match(/^[a-z0-9\-_ ]+$/i) 64 | 65 | raise InvalidParameterError, "Message must have both :subject and :content" if blank?(params[:subject]) || blank?(params[:content]) 66 | 67 | from = (params[:from].kind_of?(Hash)) ? params[:from] : @from 68 | raise InvalidParameterError, "Message's :from attribute must have :address attribute" if blank?(from[:address]) 69 | 70 | reply_to = (!blank?(params[:reply_to])) ? params[:reply_to] : @reply_to 71 | 72 | tags = (params[:tags].kind_of?(Array)) ? params[:tags] : [] 73 | tags.reject! { |tag| !tag.kind_of?(String) || blank?(tag) } 74 | 75 | link = (!blank?(params[:link])) ? params[:link] : nil 76 | 77 | params = { 78 | :source => @source, 79 | :format => 'html', # currently only supported format 80 | :from_address => from[:address], 81 | :subject => params[:subject], 82 | :content => params[:content], 83 | } 84 | params[:from_name] = from[:name] unless blank?(from[:name]) 85 | params[:reply_to] = reply_to unless blank?(reply_to) 86 | params[:tags] = tags.join(",") if tags.size > 0 87 | params[:project] = @project unless blank?(@project) 88 | params[:link] = link unless blank?(link) 89 | 90 | # Send the request 91 | resp = self.class.post(get_flowdock_api_url("messages/team_inbox"), :body => params) 92 | handle_response(resp) 93 | true 94 | end 95 | 96 | def push_to_chat(params) 97 | raise InvalidParameterError, "Message must have :content" if blank?(params[:content]) 98 | 99 | @external_user_name = params[:external_user_name] unless blank?(params[:external_user_name]) 100 | if blank?(@external_user_name) || @external_user_name.match(/^[\S]+$/).nil? || @external_user_name.length > 16 101 | raise InvalidParameterError, "Message must have :external_user_name that has no whitespace and maximum of 16 characters" 102 | end 103 | 104 | tags = (params[:tags].kind_of?(Array)) ? params[:tags] : [] 105 | tags.reject! { |tag| !tag.kind_of?(String) || blank?(tag) } 106 | thread_id = params[:thread_id] 107 | message_id = params[:message_id] || params[:message] 108 | 109 | params = { 110 | :content => params[:content], 111 | :external_user_name => @external_user_name 112 | } 113 | params[:tags] = tags.join(",") if tags.size > 0 114 | params[:thread_id] = thread_id if thread_id 115 | params[:message_id] = message_id if message_id 116 | 117 | # Send the request 118 | resp = self.class.post(get_flowdock_api_url("messages/chat"), :body => params) 119 | handle_response(resp) 120 | true 121 | end 122 | 123 | # DEPRECATED: Please use useful instead. 124 | def send_message(params) 125 | warn "[DEPRECATION] `send_message` is deprecated. Please use `push_to_team_inbox` instead." 126 | push_to_team_inbox(params) 127 | end 128 | 129 | private 130 | 131 | def get_flowdock_api_url(path) 132 | "#{FLOWDOCK_API_URL}/#{path}/#{@api_token}" 133 | end 134 | 135 | end 136 | 137 | class Client 138 | include HTTParty 139 | include Helpers 140 | attr_reader :api_token 141 | def initialize(options = {}) 142 | @api_token = options[:api_token] 143 | @flow_token = options[:flow_token] 144 | raise InvalidParameterError, "Client must have :api_token or an :flow_token" if blank?(@api_token) && blank?(@flow_token) 145 | @proxy_host = options[:proxy_host] || nil 146 | @proxy_port = options[:proxy_port] || nil 147 | @proxy_username = options[:proxy_username] || nil 148 | @proxy_password = options[:proxy_password] || nil 149 | end 150 | 151 | def chat_message(params) 152 | raise InvalidParameterError, "missing api_token" if blank?(@api_token) 153 | raise InvalidParameterError, "Message must have :content" if blank?(params[:content]) 154 | raise InvalidParameterError, "Message must have :flow" if blank?(params[:flow]) 155 | params = params.clone 156 | tags = (params[:tags].kind_of?(Array)) ? params[:tags] : [] 157 | params[:message] = params.delete(:message_id) if params[:message_id] 158 | tags.reject! { |tag| !tag.kind_of?(String) || blank?(tag) } 159 | event = if params[:message] then 'comment' else 'message' end 160 | post(event + 's', params.merge(tags: tags, event: event)) 161 | end 162 | 163 | def private_message(params) 164 | raise InvalidParameterError, "missing api_token" if blank?(@api_token) 165 | raise InvalidParameterError, "Message must have :content" if blank?(params[:content]) 166 | raise InvalidParameterError, "Message must have :user_id" if blank?(params[:user_id]) 167 | 168 | user_id = params.delete(:user_id) 169 | 170 | params = params.clone 171 | event = "message" 172 | 173 | post("private/#{user_id}/messages", params.merge(event: event)) 174 | end 175 | 176 | def post_to_thread(thread) 177 | raise InvalidParameterError, "missing flow_token" if blank?(@flow_token) 178 | resp = self.class.post(api_url("/messages"), 179 | body: MultiJson.dump(thread.merge(flow_token: @flow_token)), 180 | headers: headers, 181 | http_proxyaddr: @proxy_host, 182 | http_proxyport: @proxy_port, 183 | http_proxyuser: @proxy_username, 184 | http_proxypass: @proxy_password) 185 | handle_response resp 186 | end 187 | 188 | def post(path, data = {}) 189 | resp = self.class.post(api_url(path), 190 | :body => MultiJson.dump(data), 191 | :basic_auth => { 192 | :username => @api_token, 193 | :password => '' 194 | }, 195 | :headers => headers, 196 | :http_proxyaddr => @proxy_host, 197 | :http_proxyport => @proxy_port, 198 | :http_proxyuser => @proxy_username, 199 | :http_proxypass => @proxy_password) 200 | handle_response(resp) 201 | end 202 | 203 | def get(path, data = {}) 204 | resp = self.class.get(api_url(path), 205 | :query => data, 206 | :basic_auth => { 207 | :username => @api_token, 208 | :password => '' 209 | }, 210 | :headers => headers, 211 | :http_proxyaddr => @proxy_host, 212 | :http_proxyport => @proxy_port, 213 | :http_proxyuser => @proxy_username, 214 | :http_proxypass => @proxy_password) 215 | handle_response(resp) 216 | end 217 | 218 | def put(path, data = {}) 219 | resp = self.class.put(api_url(path), 220 | :body => MultiJson.dump(data), 221 | :basic_auth => { 222 | :username => @api_token, 223 | :password => '' 224 | }, 225 | :headers => headers, 226 | :http_proxyaddr => @proxy_host, 227 | :http_proxyport => @proxy_port, 228 | :http_proxyuser => @proxy_username, 229 | :http_proxypass => @proxy_password) 230 | handle_response(resp) 231 | end 232 | 233 | def delete(path) 234 | resp = self.class.delete(api_url(path), 235 | :basic_auth => { 236 | :username => @api_token, 237 | :password => '' 238 | }, 239 | :headers => headers, 240 | :http_proxyaddr => @proxy_host, 241 | :http_proxyport => @proxy_port, 242 | :http_proxyuser => @proxy_username, 243 | :http_proxypass => @proxy_password) 244 | handle_response(resp) 245 | end 246 | 247 | private 248 | 249 | def api_url(path) 250 | File.join(FLOWDOCK_API_URL, path) 251 | end 252 | 253 | def headers 254 | {"Content-Type" => "application/json", "Accept" => "application/json"} 255 | end 256 | end 257 | 258 | 259 | end 260 | -------------------------------------------------------------------------------- /spec/flowdock_spec.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'spec_helper' 3 | 4 | describe Flowdock do 5 | describe "with initializing flow" do 6 | it "should succeed with correct token" do 7 | lambda { 8 | @flow = Flowdock::Flow.new(:api_token => "test") 9 | }.should_not raise_error 10 | end 11 | 12 | it "should fail without token" do 13 | lambda { 14 | @flow = Flowdock::Flow.new(:api_token => "") 15 | }.should raise_error(Flowdock::InvalidParameterError) 16 | end 17 | 18 | it "should succeed with array of tokens" do 19 | lambda { 20 | @flow = Flowdock::Flow.new(:api_token => ["test", "foobar"]) 21 | }.should_not raise_error 22 | end 23 | end 24 | 25 | describe "handle_response" do 26 | it "parses a response body that contains an empty string" do 27 | class TestResponse 28 | attr_reader :body, :code 29 | 30 | def initialize 31 | @body = "" 32 | @code = 200 33 | end 34 | end 35 | 36 | class TestHelper 37 | include Flowdock::Helpers 38 | end 39 | 40 | expect(TestHelper.new.handle_response(TestResponse.new)).to eq({}) 41 | end 42 | end 43 | 44 | describe "with sending Team Inbox messages" do 45 | before(:each) do 46 | @token = "test" 47 | @flow_attributes = {:api_token => @token, :source => "myapp", :project => "myproject", 48 | :from => {:name => "Eric Example", :address => "eric@example.com"}, :reply_to => "john@example.com" } 49 | @flow = Flowdock::Flow.new(@flow_attributes) 50 | @example_content = "

Hello

\n

Let's rock and roll!

" 51 | @valid_attributes = {:subject => "Hello World", :content => @example_content, 52 | :link => "http://www.flowdock.com/", :tags => ["cool", "stuff"]} 53 | end 54 | 55 | it "should not send without source" do 56 | lambda { 57 | @flow = Flowdock::Flow.new(@flow_attributes.merge(:source => "")) 58 | @flow.push_to_team_inbox(@valid_attributes) 59 | }.should raise_error(Flowdock::InvalidParameterError) 60 | end 61 | 62 | it "should not send when source is not alphanumeric" do 63 | lambda { 64 | @flow = Flowdock::Flow.new(@flow_attributes.merge(:source => "$foobar")) 65 | @flow.push_to_team_inbox(@valid_attributes) 66 | }.should raise_error(Flowdock::InvalidParameterError) 67 | end 68 | 69 | it "should not send when project is not alphanumeric" do 70 | lambda { 71 | @flow = Flowdock::Flow.new(:api_token => "test", :source => "myapp", :project => "$foobar") 72 | @flow.push_to_team_inbox(@valid_attributes) 73 | }.should raise_error(Flowdock::InvalidParameterError) 74 | end 75 | 76 | it "should not send without sender information" do 77 | lambda { 78 | @flow = Flowdock::Flow.new(@flow_attributes.merge(:from => nil)) 79 | @flow.push_to_team_inbox(@valid_attributes) 80 | }.should raise_error(Flowdock::InvalidParameterError) 81 | end 82 | 83 | it "should not send without subject" do 84 | lambda { 85 | @flow.push_to_team_inbox(@valid_attributes.merge(:subject => "")) 86 | }.should raise_error(Flowdock::InvalidParameterError) 87 | end 88 | 89 | it "should not send without content" do 90 | lambda { 91 | @flow.push_to_team_inbox(@valid_attributes.merge(:content => "")) 92 | }.should raise_error(Flowdock::InvalidParameterError) 93 | end 94 | 95 | it "should send without reply_to address" do 96 | expect { 97 | stub_request(:post, push_to_team_inbox_url(@token)).to_return(:body => "", :status => 200) 98 | @flow.push_to_team_inbox(@valid_attributes.merge(:reply_to => "")) 99 | }.not_to raise_error 100 | end 101 | 102 | it "should succeed with correct token, source and sender information" do 103 | lambda { 104 | stub_request(:post, push_to_team_inbox_url(@token)). 105 | with(:body => { 106 | :source => "myapp", 107 | :format => "html", 108 | :from_name => "Eric Example", 109 | :from_address => "eric@example.com", 110 | :reply_to => "john@example.com", 111 | :subject => "Hello World", 112 | :content => @example_content, 113 | :tags => "cool,stuff", 114 | :link => "http://www.flowdock.com/" 115 | }). 116 | to_return(:body => "", :status => 200) 117 | 118 | @flow = Flowdock::Flow.new(@flow_attributes.merge(:project => "")) 119 | @flow.push_to_team_inbox(@valid_attributes) 120 | }.should_not raise_error 121 | end 122 | 123 | it "should succeed with correct params and multiple tokens" do 124 | lambda { 125 | tokens = ["test", "foobar"] 126 | stub_request(:post, push_to_team_inbox_url(tokens)). 127 | with(:body => { 128 | :source => "myapp", 129 | :format => "html", 130 | :from_name => "Eric Example", 131 | :from_address => "eric@example.com", 132 | :reply_to => "john@example.com", 133 | :subject => "Hello World", 134 | :content => @example_content, 135 | :tags => "cool,stuff", 136 | :link => "http://www.flowdock.com/" 137 | }). 138 | to_return(:body => "", :status => 200) 139 | 140 | @flow = Flowdock::Flow.new(@flow_attributes.merge(:project => "", :api_token => tokens)) 141 | @flow.push_to_team_inbox(@valid_attributes) 142 | }.should_not raise_error 143 | end 144 | 145 | it "should succeed without the optional from-name parameter" do 146 | lambda { 147 | stub_request(:post, push_to_team_inbox_url(@token)). 148 | with(:body => { 149 | :source => "myapp", 150 | :project => "myproject", 151 | :format => "html", 152 | :from_address => "eric@example.com", 153 | :reply_to => "john@example.com", 154 | :subject => "Hello World", 155 | :content => @example_content, 156 | :tags => "cool,stuff", 157 | :link => "http://www.flowdock.com/" 158 | }). 159 | to_return(:body => "", :status => 200) 160 | @flow = Flowdock::Flow.new(@flow_attributes.merge(:from => {:address => "eric@example.com"})) 161 | @flow.push_to_team_inbox(@valid_attributes) 162 | }.should_not raise_error 163 | end 164 | 165 | it "should succeed with correct token, sender information, source and project" do 166 | lambda { 167 | stub_request(:post, push_to_team_inbox_url(@token)). 168 | with(:body => { 169 | :source => "myapp", 170 | :project => "myproject", 171 | :format => "html", 172 | :from_name => "Eric Example", 173 | :from_address => "eric@example.com", 174 | :reply_to => "john@example.com", 175 | :subject => "Hello World", 176 | :content => @example_content, 177 | :tags => "cool,stuff", 178 | :link => "http://www.flowdock.com/" 179 | }). 180 | to_return(:body => "", :status => 200) 181 | @flow = Flowdock::Flow.new(@flow_attributes) 182 | @flow.push_to_team_inbox(@valid_attributes) 183 | }.should_not raise_error 184 | end 185 | 186 | it "should send with valid parameters and return true" do 187 | lambda { 188 | stub_request(:post, push_to_team_inbox_url(@token)). 189 | with(:body => { 190 | :source => "myapp", 191 | :project => "myproject", 192 | :format => "html", 193 | :from_name => "Eric Example", 194 | :from_address => "eric@example.com", 195 | :reply_to => "john@example.com", 196 | :subject => "Hello World", 197 | :content => @example_content, 198 | :tags => "cool,stuff", 199 | :link => "http://www.flowdock.com/" 200 | }). 201 | to_return(:body => "", :status => 200) 202 | 203 | @flow.push_to_team_inbox(:subject => "Hello World", :content => @example_content, 204 | :tags => ["cool", "stuff"], :link => "http://www.flowdock.com/").should be_truthy 205 | }.should_not raise_error 206 | end 207 | 208 | it "should allow overriding sender information per message" do 209 | lambda { 210 | stub_request(:post, push_to_team_inbox_url(@token)). 211 | with(:body => { 212 | :source => "myapp", 213 | :project => "myproject", 214 | :format => "html", 215 | :from_name => "Test", 216 | :from_address => "invalid@nodeta.fi", 217 | :reply_to => "foobar@example.com", 218 | :subject => "Hello World", 219 | :content => @example_content, 220 | :tags => "cool,stuff", 221 | }). 222 | to_return(:body => "", :status => 200) 223 | 224 | @flow.push_to_team_inbox(:subject => "Hello World", :content => @example_content, :tags => ["cool", "stuff"], 225 | :from => {:name => "Test", :address => "invalid@nodeta.fi"}, :reply_to => "foobar@example.com").should be_truthy 226 | }.should_not raise_error 227 | end 228 | 229 | it "should raise error if backend returns anything but 200 OK" do 230 | lambda { 231 | stub_request(:post, push_to_team_inbox_url(@token)). 232 | with(:body => { 233 | :source => "myapp", 234 | :project => "myproject", 235 | :format => "html", 236 | :from_name => "Eric Example", 237 | :from_address => "eric@example.com", 238 | :reply_to => "john@example.com", 239 | :subject => "Hello World", 240 | :content => @example_content 241 | }). 242 | to_return(:body => "Internal Server Error", :status => 500) 243 | 244 | @flow.push_to_team_inbox(:subject => "Hello World", :content => @example_content).should be_false 245 | }.should raise_error(Flowdock::ApiError) 246 | end 247 | 248 | it "should raise error if backend returns 404 NotFound" do 249 | lambda { 250 | stub_request(:post, push_to_team_inbox_url(@token)). 251 | with(:body => { 252 | :source => "myapp", 253 | :project => "myproject", 254 | :format => "html", 255 | :from_name => "Eric Example", 256 | :from_address => "eric@example.com", 257 | :reply_to => "john@example.com", 258 | :subject => "Hello World", 259 | :content => @example_content 260 | }). 261 | to_return(:body => "{}", :status => 404) 262 | 263 | @flow.push_to_team_inbox(:subject => "Hello World", :content => @example_content).should be_false 264 | }.should raise_error(Flowdock::NotFoundError) 265 | end 266 | end 267 | 268 | describe "with sending Chat messages" do 269 | before(:each) do 270 | @token = "test" 271 | @flow = Flowdock::Flow.new(:api_token => @token) 272 | @valid_parameters = {:external_user_name => "foobar", :content => "Hello", :tags => ["cool","stuff"]} 273 | end 274 | 275 | it "should not send without content" do 276 | lambda { 277 | @flow.push_to_chat(@valid_parameters.merge(:content => "")) 278 | }.should raise_error(Flowdock::InvalidParameterError) 279 | end 280 | 281 | it "should not send without external_user_name" do 282 | lambda { 283 | @flow.push_to_chat(@valid_parameters.merge(:external_user_name => "")) 284 | }.should raise_error(Flowdock::InvalidParameterError) 285 | end 286 | 287 | it "should not send with invalid external_user_name" do 288 | lambda { 289 | @flow.push_to_chat(@valid_parameters.merge(:external_user_name => "foo bar")) 290 | }.should raise_error(Flowdock::InvalidParameterError) 291 | end 292 | 293 | it "should send with valid parameters and return true" do 294 | lambda { 295 | stub_request(:post, push_to_chat_url(@token)). 296 | with(:body => @valid_parameters.merge(:tags => "cool,stuff")). 297 | to_return(:body => "", :status => 200) 298 | 299 | @flow.push_to_chat(@valid_parameters).should be_truthy 300 | }.should_not raise_error 301 | end 302 | 303 | it "should accept external_user_name in init" do 304 | lambda { 305 | stub_request(:post, push_to_chat_url(@token)). 306 | with(:body => @valid_parameters.merge(:tags => "cool,stuff", :external_user_name => "foobar2")). 307 | to_return(:body => "", :status => 200) 308 | 309 | @flow = Flowdock::Flow.new(:api_token => @token, :external_user_name => "foobar") 310 | @flow.push_to_chat(@valid_parameters.merge(:external_user_name => "foobar2")) 311 | }.should_not raise_error 312 | end 313 | 314 | it "should allow overriding external_user_name" do 315 | lambda { 316 | stub_request(:post, push_to_chat_url(@token)). 317 | with(:body => @valid_parameters.merge(:tags => "cool,stuff")). 318 | to_return(:body => "", :status => 200) 319 | 320 | @flow = Flowdock::Flow.new(:api_token => @token, :external_user_name => "foobar") 321 | @flow.push_to_chat(@valid_parameters.merge(:external_user_name => "")) 322 | }.should_not raise_error 323 | end 324 | 325 | it "should raise error if backend returns anything but 200 OK" do 326 | lambda { 327 | stub_request(:post, push_to_chat_url(@token)). 328 | with(:body => @valid_parameters.merge(:tags => "cool,stuff")). 329 | to_return(:body => '{"message":"Validation error","errors":{"content":["can\'t be blank"],"external_user_name":["should not contain whitespace"]}}', 330 | :status => 400) 331 | 332 | @flow.push_to_chat(@valid_parameters).should be_false 333 | }.should raise_error(Flowdock::ApiError) 334 | end 335 | 336 | it "should send supplied message to create comments" do 337 | lambda { 338 | stub_request(:post, push_to_chat_url(@token)). 339 | with(:body => /message_id=12345/). 340 | to_return(:body => "", :status => 200) 341 | 342 | @flow = Flowdock::Flow.new(:api_token => @token, :external_user_name => "foobar") 343 | @flow.push_to_chat(@valid_parameters.merge(:message_id => 12345)) 344 | }.should_not raise_error 345 | end 346 | 347 | it "should send supplied thread_id to post to threads" do 348 | lambda { 349 | stub_request(:post, push_to_chat_url(@token)). 350 | with(:body => /thread_id=acdcabbacd/). 351 | to_return(:body => "", :status => 200) 352 | 353 | @flow = Flowdock::Flow.new(:api_token => @token, :external_user_name => "foobar") 354 | @flow.push_to_chat(@valid_parameters.merge(:thread_id => 'acdcabbacd')) 355 | }.should_not raise_error 356 | end 357 | end 358 | 359 | def push_to_chat_url(token) 360 | "#{Flowdock::FLOWDOCK_API_URL}/messages/chat/#{join_tokens(token)}" 361 | end 362 | 363 | def push_to_team_inbox_url(token) 364 | "#{Flowdock::FLOWDOCK_API_URL}/messages/team_inbox/#{join_tokens(token)}" 365 | end 366 | 367 | def join_tokens(tokens) 368 | if tokens.kind_of?(Array) 369 | tokens.join(",") 370 | else 371 | tokens.to_s 372 | end 373 | end 374 | end 375 | 376 | describe Flowdock::Client do 377 | 378 | context "with flow_token" do 379 | let(:token) { SecureRandom.hex } 380 | let(:client) { Flowdock::Client.new(flow_token: token) } 381 | let(:flow) { SecureRandom.hex } 382 | 383 | describe "post a threaded message" do 384 | it "succeeds" do 385 | stub_request(:post, "https://api.flowdock.com/v1/messages"). 386 | with(body: MultiJson.dump(flow: flow, flow_token: token), 387 | headers: {"Accept" => "application/json", "Content-Type" => "application/json"}). 388 | to_return(status: 201, body: '{"id":123}', headers: {"Content-Type" => "application/json"}) 389 | res = client.post_to_thread({flow: flow}) 390 | expect(res).to eq({"id" => 123}) 391 | end 392 | end 393 | end 394 | 395 | context "with api_token" do 396 | let(:token) { SecureRandom.hex(8) } 397 | let(:client) { Flowdock::Client.new(api_token: token) } 398 | 399 | describe 'initializing' do 400 | 401 | it 'should initialize with access token' do 402 | expect { 403 | client = Flowdock::Client.new(api_token: token) 404 | expect(client.api_token).to equal(token) 405 | }.not_to raise_error 406 | end 407 | it 'should raise error if initialized without access token' do 408 | expect { 409 | client = Flowdock::Client.new(api_token: nil) 410 | }.to raise_error(Flowdock::InvalidParameterError) 411 | end 412 | end 413 | 414 | describe 'posting to chat' do 415 | 416 | let(:flow) { SecureRandom.hex(8) } 417 | 418 | it 'posts to /messages' do 419 | expect { 420 | stub_request(:post, "https://api.flowdock.com/v1/messages"). 421 | with(:body => MultiJson.dump(flow: flow, content: "foobar", tags: [], event: "message"), :headers => {"Accept" => "application/json", "Content-Type" => "application/json", 'Authorization'=>'Basic ' + Base64.strict_encode64(token + ":") }). 422 | to_return(:status => 201, :body => '{"id":123}', :headers => {"Content-Type" => "application/json"}) 423 | res = client.chat_message(flow: flow, content: 'foobar') 424 | expect(res).to eq({"id" => 123}) 425 | }.not_to raise_error 426 | end 427 | it 'posts to /comments' do 428 | expect { 429 | stub_request(:post, "https://api.flowdock.com/v1/comments"). 430 | with(:body => MultiJson.dump(flow: flow, content: "foobar", message: 12345, tags: [], event: "comment"), :headers => {"Accept" => "application/json", "Content-Type" => "application/json"}). 431 | to_return(:status => 201, :body => '{"id":1234}', :headers => {"Content-Type" => "application/json"}) 432 | res = client.chat_message(flow: flow, content: 'foobar', message: 12345) 433 | expect(res).to eq({"id" => 1234}) 434 | }.not_to raise_error 435 | end 436 | it 'posts to /private/:user_id/messages' do 437 | expect { 438 | stub_request(:post, "https://api.flowdock.com/v1/private/12345/messages"). 439 | with(:body => MultiJson.dump(content: "foobar", event: "message"), :headers => {"Accept" => "application/json", "Content-Type" => "application/json", 'Authorization'=>'Basic ' + Base64.strict_encode64(token + ":") }). 440 | to_return(:status => 201, :body => '{"id":1234}', :headers => {"Content-Type" => "application/json"}) 441 | res = client.private_message(user_id: "12345", content: 'foobar') 442 | expect(res).to eq({"id" => 1234}) 443 | }.not_to raise_error 444 | end 445 | 446 | it 'raises without flow' do 447 | expect { 448 | client.chat_message(content: 'foobar') 449 | }.to raise_error(Flowdock::InvalidParameterError) 450 | end 451 | it 'raises without content' do 452 | expect { 453 | client.chat_message(flow: flow) 454 | }.to raise_error(Flowdock::InvalidParameterError) 455 | end 456 | it 'handles error responses' do 457 | expect { 458 | stub_request(:post, "https://api.flowdock.com/v1/messages"). 459 | to_return(:body => '{"message":"Validation error","errors":{"content":["can\'t be blank"],"external_user_name":["should not contain whitespace"]}}', 460 | :status => 400) 461 | client.chat_message(flow: flow, content: 'foobar') 462 | }.to raise_error(Flowdock::ApiError) 463 | end 464 | end 465 | 466 | describe 'GET' do 467 | it 'does abstract get with params' do 468 | stub_request(:get, "https://api.flowdock.com/v1/some_path?sort_by=date"). 469 | with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json', 'Authorization'=>'Basic ' + Base64.strict_encode64(token + ":") }). 470 | to_return(:status => 200, :body => '{"id": 123}', :headers => {"Content-Type" => "application/json"}) 471 | expect(client.get('/some_path', {sort_by: 'date'})).to eq({"id" => 123}) 472 | end 473 | end 474 | 475 | describe 'POST' do 476 | it 'does abstract post with body' do 477 | stub_request(:post, "https://api.flowdock.com/v1/other_path"). 478 | with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json', 'Authorization'=>'Basic ' + Base64.strict_encode64(token + ":") }, :body => MultiJson.dump(name: 'foobar')). 479 | to_return(:status => 200, :body => '{"id": 123,"name": "foobar"}', :headers => {"Content-Type" => "application/json"}) 480 | expect(client.post('other_path', {name: 'foobar'})).to eq({"id" => 123, "name" => "foobar"}) 481 | end 482 | 483 | end 484 | 485 | describe 'PUT' do 486 | it 'does abstract put with body' do 487 | stub_request(:put, "https://api.flowdock.com/v1/other_path"). 488 | with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json', 'Authorization'=>'Basic ' + Base64.strict_encode64(token + ":") }, :body => MultiJson.dump(name: 'foobar')). 489 | to_return(:status => 200, :body => '{"id": 123,"name": "foobar"}', :headers => {"Content-Type" => "application/json"}) 490 | expect(client.put('other_path', {name: 'foobar'})).to eq({"id" => 123, "name" => "foobar"}) 491 | end 492 | end 493 | 494 | describe 'DELETE' do 495 | it 'does abstract delete with params' do 496 | stub_request(:delete, "https://api.flowdock.com/v1/some_path"). 497 | with(:headers => {'Accept'=>'application/json', 'Content-Type'=>'application/json', 'Authorization'=>'Basic ' + Base64.strict_encode64(token + ":") }). 498 | to_return(:status => 200, :body => '', :headers => {"Content-Type" => "application/json"}) 499 | expect(client.delete('/some_path')).to eq({}) 500 | end 501 | end 502 | end 503 | end 504 | --------------------------------------------------------------------------------