├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── esa.gemspec ├── lib ├── esa.rb └── esa │ ├── api_methods.rb │ ├── client.rb │ ├── errors.rb │ ├── response.rb │ └── version.rb └── spec ├── esa ├── client_spec.rb └── response_spec.rb ├── fixtures └── files │ └── egg.png └── spec_helper.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | ruby: 13 | - "3.1" 14 | - "3.2" 15 | - "3.3" 16 | - "3.4" 17 | - "head" 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Install dependencies 27 | run: bundle install --jobs 4 --retry 3 28 | - name: Build and test with Rake 29 | run: bundle exec rake 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format=progress 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | nothing 4 | 5 | ## 3.1.0 (2025-02-21) 6 | - add: [Add signed_urls method](https://github.com/esaio/esa-ruby/pull/70) 7 | 8 | ## 3.0.0 (2025-02-20) 9 | - :warning: breaking: [Drop Support for Ruby < 3.1](https://github.com/esaio/esa-ruby/pull/67) 10 | - ci: [Tweak CI ruby versions](https://github.com/esaio/esa-ruby/pull/65) 11 | 12 | 13 | ## 2.2.0 (2024-05-30) 14 | - fix: [Request to add base64 gem dependency to esa gem](https://github.com/esaio/esa-ruby/pull/64) 15 | - ci: [Tweak CI ruby versions](https://github.com/esaio/esa-ruby/pull/65) 16 | 17 | 18 | ## 2.1.0 (2023-10-03) 19 | 20 | - add: [Support GET /v1/%{team_name}/members/%{identifier} API by ppworks · Pull Request #63 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/63) 21 | - doc: [Update "DELETE /v1/teams/bar/members/" section in the README](https://github.com/esaio/esa-ruby/pull/62) 22 | - ci: [Bump actions/checkout from 3 to 4](https://github.com/esaio/esa-ruby/pull/61) 23 | - ci: [Add github-actions to Dependabot](https://github.com/esaio/esa-ruby/pull/60) 24 | 25 | ## 2.0.0 (2023-04-06) 26 | 27 | - :warning: breaking: [Support Faraday 2.0.1+ by fukayatsu · Pull Request #58 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/58) 28 | - Drop support for Ruby < 2.7.0 29 | - Drop support for Faraday < 2.0.1 30 | 31 | ## 1.18.0 (2020-10-19) 32 | 33 | - add: [Relax dependencies by fukayatsu · Pull Request #47 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/47) 34 | 35 | ## 1.17.0 (2020-07-02) 36 | 37 | - ci: [Add specs for default_headers](https://github.com/esaio/esa-ruby/pull/45) 38 | - add: [Add Post Append API (beta) by fukayatsu · Pull Request #46 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/46) 39 | - **changed**: Drop support for Ruby 2.4 40 | 41 | ## 1.16.0 (2020-01-20) 42 | 43 | - add: [Enable to set default_headers by fukayatsu · Pull Request #43 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/43) 44 | 45 | ## 1.15.0 (2019-12-27) 46 | 47 | - add: [Add ApiMethods#delete_member by fukayatsu · Pull Request #40 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/40) 48 | - ci: [Use github actions for CI by fukayatsu · Pull Request #38 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/38) 49 | - ci: [Update development environment by fukayatsu · Pull Request #37 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/37) 50 | 51 | ## 1.14.0 (2018-12-13) 52 | 53 | - changed: [Relax gem dependencies by ppworks · Pull Request #35 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/35) 54 | 55 | ## 1.13.0 (2017-11-01) 56 | 57 | - **changed**: [Retry on rate limit exceeded by default by fukayatsu · Pull Request #31 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/31) 58 | - Use `Esa::Client.new(retry_on_rate_limit_exceeded: false, ...)` for previous behavior. 59 | - doc: [Fixed README typo by polidog · Pull Request #30 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/30) 60 | 61 | ## 1.12.0 (2017-10-03) 62 | 63 | - add: [Support /api/v1/comments API by ppworks · Pull Request #29 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/29) 64 | 65 | ## 1.11.0 (2017-09-04) 66 | 67 | - add: [Support emoji API by ppworks · Pull Request #28 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/28) 68 | 69 | ## 1.10.0 (2017-08-22) 70 | 71 | - add: [Support invitation API by ppworks · Pull Request #27 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/27) 72 | 73 | ## 1.9.0 (2017-08-02) 74 | 75 | - add: [Enable to set headers for remote url on #upload_attachment by nownabe · Pull Request #26 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/26) 76 | 77 | ## 1.8.0 (2017-02-25) 78 | 79 | - add: [Add batch_move_category API by fukayatsu · Pull Request #25 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/25) 80 | 81 | ## 1.7.0 (2016-08-16) 82 | 83 | - add: [Support signed_url API by fukayatsu · Pull Request #24 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/24) 84 | - fix: [Fix a typo by ksss · Pull Request #23 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/23) 85 | 86 | ## 1.6.0 (2016-06-10) 87 | 88 | - add: [Add Categories API and Tags API by fukayatsu · Pull Request #22 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/22) 89 | 90 | ## 1.5.0 (2016-05-05) 91 | 92 | - add: [Add Reaction APIs: star&watch by fukayatsu · Pull Request #20 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/20) 93 | 94 | ## 1.4.0 (2016-04-17) 95 | 96 | - add: [Add Authenticated User API by fukayatsu · Pull Request #19 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/19) 97 | 98 | ## 1.3.0 (2016-02-09) 99 | 100 | - add: [Add Sharing API by fukayatsu · Pull Request #17 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/17) 101 | 102 | ## 1.2.0 (2015-12-03) 103 | 104 | - add: [Add Members API by fukayatsu · Pull Request #16 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/16) 105 | 106 | ## 1.1.2 (2015-10-05) 107 | 108 | - fix: [Esa::Client#upload_attachment needs 'multi_xml' gem by ppworks · Pull Request #15 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/15) 109 | 110 | ## 1.1.1 (2015-10-01) 111 | 112 | - add: [Enable to set cookie for remote url on #upload_attachment by fukayatsu · Pull Request #14 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/14) 113 | 114 | ## 1.1.0 (2015-10-01) 115 | 116 | - add: [Enable to Upload Attachment by fukayatsu · Pull Request #13 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/13) 117 | 118 | ## 1.0.0 (2015-09-25) 119 | 120 | - Production Ready (\\( ⁰⊖⁰)/) 121 | - [Add Stats API by fukayatsu · Pull Request #12 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/12) 122 | - [Use Travis CI for testing by hanachin · Pull Request #11 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/11) 123 | 124 | ## 0.0.6 (2015-06-21) 125 | 126 | - [Support Comment API by fukayatsu · Pull Request #9 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/9) 127 | 128 | ## 0.0.5 (2015-05-21) 129 | 130 | - [Use current_team! instead of current_team by fukayatsu · Pull Request #8 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/8) 131 | 132 | ## 0.0.4 (2015-05-14) 133 | 134 | - [Fix Esa::Response#headers and implement its spec by yasaichi · Pull Request #7 · esaio/esa-ruby](https://github.com/esaio/esa-ruby/pull/7) 135 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in esa.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 fukayatsu 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esa-ruby 2 | [![Build Status](https://github.com/esaio/esa-ruby/workflows/build/badge.svg)](https://github.com/esaio/esa-ruby/actions) 3 | 4 | 5 | esa API v1 client library, written in Ruby 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'esa' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install esa 22 | 23 | ## Usage 24 | 25 | ```ruby 26 | # Initialization 27 | client = Esa::Client.new(access_token: "", current_team: 'foo') 28 | 29 | # Authenticated User API 30 | client.user 31 | #=> GET /v1/user 32 | 33 | # Team API 34 | client.teams 35 | #=> GET /v1/teams 36 | 37 | client.team('bar') 38 | #=> GET /v1/teams/bar 39 | 40 | client.stats 41 | #=> GET /v1/teams/bar/stats 42 | 43 | client.members 44 | #=> GET /v1/teams/bar/members 45 | 46 | client.member('me') 47 | #=> GET /v1/teams/bar/members/me 48 | 49 | find_by_screen_name = 'alice' 50 | client.member(find_by_screen_name) 51 | #=> GET /v1/teams/bar/members/alice 52 | 53 | find_by_email_address = 'alice@example.com' 54 | client.member(find_by_email_address) 55 | #=> GET /v1/teams/bar/members/alice@example.com 56 | 57 | delete_by_screen_name = 'alice' 58 | client.delete_member(delete_by_screen_name) 59 | #=> DELETE /v1/teams/bar/members/alice 60 | 61 | delete_by_email_address = 'alice@example.com' 62 | client.delete_member(delete_by_email_address) 63 | #=> DELETE /v1/teams/bar/members/alice@example.com 64 | 65 | # Post API 66 | client.posts 67 | #=> GET /v1/teams/foo/posts 68 | 69 | client.posts(q: 'in:help') 70 | #=> GET /v1/teams/foo/posts?q=in%3Ahelp 71 | 72 | client.current_team = 'foobar' 73 | post_number = 1 74 | client.post(post_number) 75 | #=> GET /v1/teams/foobar/posts/1 76 | 77 | client.create_post(name: 'foo') 78 | #=> POST /v1/teams/foobar/posts 79 | 80 | client.update_post(post_number, name: 'bar') 81 | #=> PATCH /v1/teams/foobar/posts/1 82 | 83 | # (beta) 84 | client.append_post(post_number, content: 'bar') 85 | #=> POST /v1/teams/foobar/posts/1/append 86 | 87 | client.delete_post(post_number) 88 | #=> DELETE /v1/teams/foobar/posts/1 89 | 90 | 91 | # Comment API 92 | client.comments(post_number) 93 | #=> GET /v1/teams/foobar/posts/1/comments 94 | 95 | client.create_comment(post_number, body_md: 'baz') 96 | #=> POST /v1/teams/foobar/posts/1/comments 97 | 98 | comment_id = 123 99 | client.comment(comment_id) 100 | #=> GET /v1/teams/foobar/comments/123 101 | 102 | client.update_comment(comment_id, body_md: 'bazbaz') 103 | #=> PATCH /v1/teams/foobar/comments/123 104 | 105 | client.delete_comment(comment_id) 106 | #=> DELETE /v1/teams/foobar/comments/123 107 | 108 | client.comments 109 | #=> GET /v1/teams/foobar/comments 110 | 111 | client.create_sharing(post_number) 112 | #=> POST /v1/teams/foobar/posts/1/sharing 113 | 114 | client.delete_sharing(post_number) 115 | #=> DELETE /v1/teams/foobar/posts/1/sharing 116 | 117 | 118 | # Star API 119 | client.post_stargazers(post_number) 120 | #=> GET /v1/teams/foobar/posts/1/stargazers 121 | 122 | client.add_post_star(post_number) 123 | #=> POST /v1/teams/foobar/posts/1/star 124 | 125 | client.delete_post_star(post_number) 126 | #=> DELETE /v1/teams/foobar/posts/1/star 127 | 128 | client.comment_stargazers(comment_id) 129 | #=> GET /v1/teams/foobar/comments/123/stargazers 130 | 131 | client.add_comment_star(comment_id) 132 | #=> POST /v1/teams/foobar/comments/123/star 133 | 134 | client.delete_comment_star(comment_id) 135 | #=> DELETE /v1/teams/foobar/comments/123/star 136 | 137 | 138 | # Watch API 139 | client.watchers(post_number) 140 | #=> GET /v1/teams/foobar/posts/1/watchers 141 | 142 | client.add_watch(post_number) 143 | #=> POST /v1/teams/foobar/posts/1/watch 144 | 145 | client.delete_watch(post_number) 146 | #=> DELETE /v1/teams/foobar/posts/1/watch 147 | 148 | # Categories API 149 | client.categories 150 | #=> GET /v1/teams/foobar/categories 151 | 152 | client.batch_move_category(from: '/esa/', to: '/tori/piyo/') 153 | #=> POST /v1/teams/foobar/categories/batch_move 154 | 155 | # Tags API 156 | client.tags 157 | #=> GET /v1/teams/foobar/tags 158 | 159 | # Invitation API 160 | client.invitation 161 | #=> GET /v1/teams/foobar/invitation 162 | 163 | client.regenerate_invitation 164 | #=> POST /v1/teams/foobar/invitation_regenerator 165 | 166 | client.pending_invitations 167 | #=> GET /v1/teams/foobar/invitations 168 | 169 | client.send_invitation(emails) 170 | #=> POST /v1/teams/foobar/invitations 171 | 172 | client.cancel_invitation(invitation_code) 173 | #=> DELETE /v1/teams/foobar/invitations/baz 174 | 175 | # Emoji API 176 | client.emojis 177 | #=> GET /v1/teams/foobar/emojis 178 | 179 | client.create_emoji(code: 'team_emoji', image: '/path/to/image') 180 | #=> POST /v1/teams/foobar/emojis 181 | 182 | client.create_emoji(code: 'alias_code', origin_code: 'team_emoji') 183 | #=> POST /v1/teams/foobar/emojis 184 | 185 | client.delete_emoji('team_emoji') 186 | #=> DELETE /v1/teams/foobar/emojis/team_emoji 187 | 188 | # Upload Attachment(beta) 189 | client.upload_attachment('/Users/foo/Desktop/foo.png') # Path 190 | client.upload_attachment(File.open('/Users/foo/Desktop/foo.png')) # File 191 | client.upload_attachment('http://example.com/foo.png') # Remote URL 192 | client.upload_attachment(['http://example.com/foo.png', cookie_str]) # Remote URL + Cookie 193 | client.upload_attachment(['http://example.com/foo.png', headers_hash]) # Remote URL + Headers 194 | 195 | # Signed url for secure upload(beta): deprecated 196 | client.signed_url('uploads/p/attachments/1/2016/08/16/1/foobar.png') 197 | #=> GET /v1/teams/foobar/signed_url/uploads/p/attachments/1/2016/08/16/1/foobar.png 198 | 199 | # Signed urls for secure upload(beta): upto 1,000 urls at once 200 | client.signed_urls(['https://files.esa.io/.../1', 'https://files.esa.io/.../2']) # expires_in defaults to 60 seconds and can be set from 1 to 604800 201 | client.signed_urls(['https://files.esa.io/.../1', 'https://files.esa.io/.../2'], expires_in: 3600) # expires_in: 1 hour 202 | #=> POST /v1/teams/foobar/signed_urls 203 | #=> { signed_urls: [['https://files.esa.io/.../1', 'https://singed_url1'], ['https://files.esa.io/.../2', 'https://singed_url2']] } 204 | ``` 205 | 206 | 207 | see also: [dev/api/v1 - docs.esa.io](https://docs.esa.io/posts/102) 208 | 209 | ## Contributing 210 | 211 | 1. Fork it ( https://github.com/esaio/esa-ruby/fork ) 212 | 2. Create your feature branch (`git checkout -b my-new-feature`) 213 | 3. Commit your changes (`git commit -am 'Add some feature'`) 214 | 4. Push to the branch (`git push origin my-new-feature`) 215 | 5. Create a new Pull Request 216 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new("spec") 5 | task default: :spec 6 | -------------------------------------------------------------------------------- /esa.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'esa/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'esa' 9 | spec.version = Esa::VERSION 10 | spec.authors = ['fukayatsu'] 11 | spec.email = ['fukayatsu@gmail.com'] 12 | spec.summary = 'esa API v1 client library, written in Ruby' 13 | spec.homepage = 'https://github.com/esaio/esa-ruby/' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match?(%r{^(spec/|\.)}) } 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.require_paths = ['lib'] 19 | 20 | spec.required_ruby_version = ">= 3.1.0" 21 | 22 | spec.add_runtime_dependency 'base64', '>= 0.2' 23 | spec.add_runtime_dependency 'faraday', '>= 2.0.1', '< 3.0' 24 | spec.add_runtime_dependency 'faraday-multipart' 25 | spec.add_runtime_dependency 'faraday-xml' 26 | spec.add_runtime_dependency 'mime-types', '>= 2.6', '< 4.0' 27 | spec.add_runtime_dependency 'multi_xml', '>= 0.5.5', '< 1.0' 28 | 29 | spec.add_development_dependency 'bundler', '~> 2.0' 30 | spec.add_development_dependency 'pry', '~> 0.12' 31 | spec.add_development_dependency 'rake', '~> 13.0' 32 | spec.add_development_dependency 'rspec', '~> 3.9' 33 | spec.add_development_dependency 'webmock', '~> 3.7.6' 34 | end 35 | -------------------------------------------------------------------------------- /lib/esa.rb: -------------------------------------------------------------------------------- 1 | require "faraday" 2 | require "faraday/multipart" 3 | require "faraday/xml" 4 | 5 | require "esa/version" 6 | require "esa/client" 7 | -------------------------------------------------------------------------------- /lib/esa/api_methods.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | require 'mime/types' 3 | 4 | module Esa 5 | module ApiMethods 6 | HTTP_REGEX = %r{^https?://} 7 | 8 | def user(params = nil, headers = nil) 9 | send_get("/v1/user", params, headers) 10 | end 11 | 12 | def teams(params = nil, headers = nil) 13 | send_get("/v1/teams", params, headers) 14 | end 15 | 16 | def team(team_name, params = nil, headers = nil) 17 | send_get("/v1/teams/#{team_name}", params, headers) 18 | end 19 | 20 | def stats(params = nil, headers = nil) 21 | send_get("/v1/teams/#{current_team!}/stats", params, headers) 22 | end 23 | 24 | def members(params = nil, headers = nil) 25 | send_get("/v1/teams/#{current_team!}/members", params, headers) 26 | end 27 | 28 | def member(identifier, params = nil, headers = nil) 29 | send_get("/v1/teams/#{current_team!}/members/#{identifier}", params, headers) 30 | end 31 | 32 | def delete_member(screen_name, params = nil, headers = nil) 33 | send_delete("/v1/teams/#{current_team!}/members/#{screen_name}", params, headers) 34 | end 35 | 36 | def posts(params = nil, headers = nil) 37 | send_get("/v1/teams/#{current_team!}/posts", params, headers) 38 | end 39 | 40 | def post(post_number, params = nil, headers = nil) 41 | send_get("/v1/teams/#{current_team!}/posts/#{post_number}", params, headers) 42 | end 43 | 44 | def create_post(params = nil, headers = nil) 45 | send_post("/v1/teams/#{current_team!}/posts", wrap(params, :post), headers) 46 | end 47 | 48 | def update_post(post_number, params = nil, headers = nil) 49 | send_patch("/v1/teams/#{current_team!}/posts/#{post_number}", wrap(params, :post), headers) 50 | end 51 | 52 | def append_post(post_number, params = nil, headers = nil) 53 | send_post("/v1/teams/#{current_team!}/posts/#{post_number}/append", wrap(params, :post), headers) 54 | end 55 | 56 | def delete_post(post_number, params = nil, headers = nil) 57 | send_delete("/v1/teams/#{current_team!}/posts/#{post_number}", params, headers) 58 | end 59 | 60 | def comments(post_number = nil, params = nil, headers = nil) 61 | if post_number.nil? 62 | send_get("/v1/teams/#{current_team!}/comments", params, headers) 63 | else 64 | send_get("/v1/teams/#{current_team!}/posts/#{post_number}/comments", params, headers) 65 | end 66 | end 67 | 68 | def comment(comment_id, params = nil, headers = nil) 69 | send_get("/v1/teams/#{current_team!}/comments/#{comment_id}", params, headers) 70 | end 71 | 72 | def create_comment(post_number, params = nil, headers = nil) 73 | send_post("/v1/teams/#{current_team!}/posts/#{post_number}/comments", wrap(params, :comment), headers) 74 | end 75 | 76 | def update_comment(comment_id, params = nil, headers = nil) 77 | send_patch("/v1/teams/#{current_team!}/comments/#{comment_id}", wrap(params, :comment), headers) 78 | end 79 | 80 | def delete_comment(comment_id, params = nil, headers = nil) 81 | send_delete("/v1/teams/#{current_team!}/comments/#{comment_id}", params, headers) 82 | end 83 | 84 | def create_sharing(post_number, params = nil, headers = nil) 85 | send_post("/v1/teams/#{current_team!}/posts/#{post_number}/sharing", params, headers) 86 | end 87 | 88 | def delete_sharing(post_number, params = nil, headers = nil) 89 | send_delete("/v1/teams/#{current_team!}/posts/#{post_number}/sharing", params, headers) 90 | end 91 | 92 | def post_stargazers(post_number, params = nil, headers = nil) 93 | send_get("/v1/teams/#{current_team!}/posts/#{post_number}/stargazers", params, headers) 94 | end 95 | 96 | def add_post_star(post_number, params = nil, headers = nil) 97 | send_post("/v1/teams/#{current_team!}/posts/#{post_number}/star", params, headers) 98 | end 99 | 100 | def delete_post_star(post_number, params = nil, headers = nil) 101 | send_delete("/v1/teams/#{current_team!}/posts/#{post_number}/star", params, headers) 102 | end 103 | 104 | def comment_stargazers(comment_id, params = nil, headers = nil) 105 | send_get("/v1/teams/#{current_team!}/comments/#{comment_id}/stargazers", params, headers) 106 | end 107 | 108 | def add_comment_star(comment_id, params = nil, headers = nil) 109 | send_post("/v1/teams/#{current_team!}/comments/#{comment_id}/star", params, headers) 110 | end 111 | 112 | def delete_comment_star(comment_id, params = nil, headers = nil) 113 | send_delete("/v1/teams/#{current_team!}/comments/#{comment_id}/star", params, headers) 114 | end 115 | 116 | def watchers(post_number, params = nil, headers = nil) 117 | send_get("/v1/teams/#{current_team!}/posts/#{post_number}/watchers", params, headers) 118 | end 119 | 120 | def add_watch(post_number, params = nil, headers = nil) 121 | send_post("/v1/teams/#{current_team!}/posts/#{post_number}/watch", params, headers) 122 | end 123 | 124 | def delete_watch(post_number, params = nil, headers = nil) 125 | send_delete("/v1/teams/#{current_team!}/posts/#{post_number}/watch", params, headers) 126 | end 127 | 128 | def categories(params = nil, headers = nil) 129 | send_get("/v1/teams/#{current_team!}/categories", params, headers) 130 | end 131 | 132 | def batch_move_category(params = nil, headers = nil) 133 | send_post("/v1/teams/#{current_team!}/categories/batch_move", params, headers) 134 | end 135 | 136 | def tags(params = nil, headers = nil) 137 | send_get("/v1/teams/#{current_team!}/tags", params, headers) 138 | end 139 | 140 | def invitation(params = nil, headers = nil) 141 | send_get("/v1/teams/#{current_team!}/invitation", params, headers) 142 | end 143 | 144 | def regenerate_invitation(params = nil, headers = nil) 145 | send_post("/v1/teams/#{current_team!}/invitation_regenerator", params, headers) 146 | end 147 | 148 | def pending_invitations(params = nil, headers = nil) 149 | send_get("/v1/teams/#{current_team!}/invitations", params, headers) 150 | end 151 | 152 | def send_invitation(emails, params = {}, headers = nil) 153 | params = params.merge(member: { emails: emails } ) 154 | send_post("/v1/teams/#{current_team!}/invitations", params, headers) 155 | end 156 | 157 | def cancel_invitation(code, params = nil, headers = nil) 158 | send_delete("/v1/teams/#{current_team!}/invitations/#{code}", params, headers) 159 | end 160 | 161 | def emojis(params = nil, headers = nil) 162 | send_get("/v1/teams/#{current_team}/emojis", params, headers) 163 | end 164 | 165 | def create_emoji(params = nil, headers = nil) 166 | params[:image] = Base64.strict_encode64(File.read(params[:image])) if params[:image] 167 | send_post("/v1/teams/#{current_team!}/emojis", wrap(params, :emoji), headers) 168 | end 169 | 170 | def delete_emoji(emoji_code, params = nil, headers = nil) 171 | send_delete("/v1/teams/#{current_team!}/emojis/#{emoji_code}", params, headers) 172 | end 173 | 174 | class PathStringIO < StringIO 175 | attr_accessor :path 176 | 177 | def initialize(*args) 178 | super(*args[1..-1]) 179 | @path = args[0] 180 | end 181 | end 182 | 183 | # beta 184 | def upload_attachment(path_or_file_or_url, params = {}, headers = nil) 185 | file = file_from(path_or_file_or_url) 186 | setup_params_for_upload(params, file) 187 | 188 | response = send_post("/v1/teams/#{current_team!}/attachments/policies", params, headers) 189 | return response unless response.status == 200 190 | 191 | attachment = response.body['attachment'] 192 | form_data = response.body['form'].merge(file: Faraday::FilePart.new(file, params[:type])) 193 | 194 | s3_response = send_s3_request(:post, attachment['endpoint'], form_data) 195 | return s3_response unless s3_response.status == 204 196 | 197 | response.body.delete('form') 198 | response 199 | end 200 | 201 | def signed_url(file_path, params = nil, headers = nil) 202 | send_get("/v1/teams/#{current_team!}/signed_url/#{file_path}", params, headers) 203 | end 204 | 205 | def signed_urls(file_urls, params = {}, headers = nil) 206 | params = params.merge(urls: file_urls) 207 | send_post("/v1/teams/#{current_team!}/signed_urls", params, headers) 208 | end 209 | 210 | private 211 | 212 | def wrap(params, envelope) 213 | return params if params.nil? 214 | return params unless params.is_a?(Hash) 215 | return params if params.has_key?(envelope.to_sym) || params.has_key?(envelope.to_s) 216 | { envelope => params } 217 | end 218 | 219 | def content_type_from_file(file) 220 | if mime_type = MIME::Types.type_for(file.path).first 221 | mime_type.content_type 222 | end 223 | rescue LoadError 224 | msg = 'Please pass content_type or install mime-types gem to guess content type from file' 225 | raise MissingContentTypeError, msg 226 | end 227 | 228 | def file_from(path_or_file_or_url) 229 | path_or_file_or_url, headers_or_cookie = *path_or_file_or_url if path_or_file_or_url.is_a?(Array) 230 | 231 | if path_or_file_or_url.respond_to?(:read) 232 | path_or_file_or_url 233 | elsif path_or_file_or_url.is_a?(String) && HTTP_REGEX.match(path_or_file_or_url) 234 | remote_url = path_or_file_or_url 235 | headers = 236 | if headers_or_cookie 237 | headers_or_cookie.is_a?(Hash) ? headers_or_cookie : { Cookie: headers_or_cookie } 238 | else 239 | {} 240 | end 241 | response = send_simple_request(:get, remote_url, nil, headers) 242 | raise RemoteURLNotAvailableError, "#{remote_url} is not available." unless response.status == 200 243 | PathStringIO.new(File.basename(remote_url), response.body) 244 | else 245 | path = path_or_file_or_url 246 | File.new(path, "r+b") 247 | end 248 | end 249 | 250 | def setup_params_for_upload(params, file) 251 | params[:type] = params.delete(:content_type) || content_type_from_file(file) 252 | params[:size] = file.size 253 | params[:name] = File.basename(file.path) 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /lib/esa/client.rb: -------------------------------------------------------------------------------- 1 | require 'esa/errors' 2 | require 'esa/api_methods' 3 | require "esa/response" 4 | 5 | module Esa 6 | class Client 7 | class TooManyRequestError < StandardError; end 8 | 9 | include ApiMethods 10 | 11 | def initialize(access_token: nil, api_endpoint: nil, current_team: nil, default_headers: {}, retry_on_rate_limit_exceeded: true) 12 | @access_token = access_token 13 | @api_endpoint = api_endpoint 14 | @current_team = current_team 15 | @default_headers = default_headers 16 | @retry_on_rate_limit_exceeded = retry_on_rate_limit_exceeded 17 | end 18 | attr_accessor :current_team, :default_headers, :retry_on_rate_limit_exceeded 19 | 20 | def current_team! 21 | raise TeamNotSpecifiedError, "current_team is not specified" unless @current_team 22 | current_team 23 | end 24 | 25 | def send_get(path, params = nil, headers = nil) 26 | send_request(:get, path, params, headers) 27 | end 28 | 29 | def send_post(path, params = nil, headers = nil) 30 | send_request(:post, path, params, headers) 31 | end 32 | 33 | def send_put(path, params = nil, headers = nil) 34 | send_request(:put, path, params, headers) 35 | end 36 | 37 | def send_patch(path, params = nil, headers = nil) 38 | send_request(:patch, path, params, headers) 39 | end 40 | 41 | def send_delete(path, params = nil, headers = nil) 42 | send_request(:delete, path, params, headers) 43 | end 44 | 45 | def send_request(method, path, params = nil, headers = nil) 46 | response = esa_connection.send(method, path, params, headers) 47 | raise TooManyRequestError if retry_on_rate_limit_exceeded && response.status == 429 # too_many_requests 48 | Esa::Response.new(response) 49 | rescue TooManyRequestError 50 | wait_sec = response.headers['retry-after'].to_i + 5 51 | puts "Rate limit exceeded: will retry after #{wait_sec} seconds." 52 | wait_for(wait_sec) 53 | retry 54 | end 55 | 56 | def send_s3_request(method, path, params = nil, headers = nil) 57 | Esa::Response.new(s3_connection.send(method, path, params, headers)) 58 | end 59 | 60 | def send_simple_request(method, path, params = nil, headers = nil) 61 | Esa::Response.new(simple_connection.send(method, path, params, headers)) 62 | end 63 | 64 | def esa_connection 65 | @esa_connection ||= Faraday.new(faraday_options) do |c| 66 | c.request :json 67 | c.response :json 68 | c.adapter Faraday.default_adapter 69 | end 70 | end 71 | 72 | def s3_connection 73 | @s3_connection ||= Faraday.new do |c| 74 | c.request :multipart 75 | c.request :url_encoded 76 | c.response :xml 77 | c.adapter Faraday.default_adapter 78 | end 79 | end 80 | 81 | def simple_connection 82 | @simple_connection ||= Faraday.new do |c| 83 | c.adapter Faraday.default_adapter 84 | end 85 | end 86 | 87 | private 88 | 89 | def faraday_options 90 | { url: api_url, headers: api_headers } 91 | end 92 | 93 | def api_headers 94 | headers = { 95 | 'Accept' => 'application/json', 96 | 'User-Agent' => "Esa Ruby Gem #{Esa::VERSION}" 97 | }.merge(default_headers) 98 | 99 | if access_token 100 | headers.merge(Authorization: "Bearer #{access_token}") 101 | else 102 | headers 103 | end 104 | end 105 | 106 | def access_token 107 | @access_token || ENV['ESA_ACCESS_TOKEN'] 108 | end 109 | 110 | def api_url 111 | @api_endpoint || ENV['ESA_API_ENDPOINT'] || 'https://api.esa.io' 112 | end 113 | 114 | def wait_for(wait_sec) 115 | return if wait_sec <= 0 116 | 117 | (wait_sec / 10).times do 118 | print '.' 119 | sleep 10 120 | end 121 | sleep wait_sec % 10 122 | puts 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /lib/esa/errors.rb: -------------------------------------------------------------------------------- 1 | module Esa 2 | class EsaError < StandardError; end 3 | class TeamNotSpecifiedError < EsaError; end 4 | class MissingContentTypeError < EsaError; end 5 | class RemoteURLNotAvailableError < EsaError; end 6 | end 7 | -------------------------------------------------------------------------------- /lib/esa/response.rb: -------------------------------------------------------------------------------- 1 | module Esa 2 | class Response 3 | def initialize(faraday_response) 4 | @raw_body = faraday_response.body 5 | @raw_headers = faraday_response.headers 6 | @raw_status = faraday_response.status 7 | end 8 | 9 | def body 10 | @raw_body 11 | end 12 | 13 | def headers 14 | @headers ||= @raw_headers.inject({}) do |result, (key, value)| 15 | result.merge(key.split("-").map(&:capitalize).join("-") => value) 16 | end 17 | end 18 | 19 | def status 20 | @raw_status 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/esa/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Esa 4 | VERSION = '3.1.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/esa/client_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Esa::Client do 4 | let(:access_token) { nil } 5 | let(:api_endpoint) { nil } 6 | let(:current_team) { nil } 7 | let(:default_headers) { {} } 8 | let(:options) do 9 | { 10 | access_token: access_token, 11 | api_endpoint: api_endpoint, 12 | current_team: current_team, 13 | default_headers: default_headers 14 | } 15 | end 16 | subject(:client) { described_class.new(**options) } 17 | 18 | describe "#current_team!" do 19 | context 'team not specified' do 20 | it 'raise error' do 21 | expect do 22 | client.current_team! 23 | end.to raise_error Esa::TeamNotSpecifiedError 24 | end 25 | end 26 | 27 | context 'team specified' do 28 | let(:current_team) { '' } 29 | it 'return current_team' do 30 | expect(client.current_team).to eq current_team 31 | end 32 | end 33 | end 34 | 35 | %i(get post put patch delete).each do |method| 36 | describe "#send_#{method}" do 37 | let(:path) { '' } 38 | let(:params) { '' } 39 | let(:headers) { '' } 40 | it "call send_request with method, path, params and headers" do 41 | expect(client).to receive(:send_request).with(method, path, params, headers) 42 | client.__send__("send_#{method}", path, params, headers) 43 | end 44 | end 45 | end 46 | 47 | describe 'default_headers' do 48 | before do 49 | stub_request(:any, 'https://api.esa.io/v1/teams') 50 | .to_return do |request| 51 | { 52 | body: request.body, 53 | headers: request.headers 54 | } 55 | end 56 | end 57 | 58 | context 'no default_headers option' do 59 | it 'request with basic headers' do 60 | response = client.teams 61 | expect(response.headers).to eq( 62 | 'Accept' => 'application/json', 63 | 'User-Agent' => "Esa Ruby Gem #{Esa::VERSION}", 64 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3' 65 | ) 66 | end 67 | 68 | it 'request with basic headers and additional headers ' do 69 | response = client.teams(nil, { 'X-Foo' => 'bar' }) 70 | expect(response.headers).to eq( 71 | 'Accept' => 'application/json', 72 | 'User-Agent' => "Esa Ruby Gem #{Esa::VERSION}", 73 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 74 | 'X-Foo' => 'bar' 75 | ) 76 | end 77 | end 78 | 79 | context 'with default_headers option' do 80 | let(:default_headers) { { 'X-Default-Foo' => 'Bar' } } 81 | 82 | it 'request with basic headers and default_headers' do 83 | response = client.teams 84 | expect(response.headers).to eq( 85 | 'Accept' => 'application/json', 86 | 'User-Agent' => "Esa Ruby Gem #{Esa::VERSION}", 87 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 88 | 'X-Default-Foo' => 'Bar' 89 | ) 90 | end 91 | 92 | it 'request with basic headers and additional headers ' do 93 | response = client.teams(nil, { 'X-Foo' => 'baz', 'X-Default-Foo' => 'qux' }) 94 | expect(response.headers).to eq( 95 | 'Accept' => 'application/json', 96 | 'User-Agent' => "Esa Ruby Gem #{Esa::VERSION}", 97 | 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 98 | 'X-Default-Foo' => 'qux', 99 | 'X-Foo' => 'baz' 100 | ) 101 | end 102 | end 103 | end 104 | 105 | describe '#upload_attachment' do 106 | let(:current_team) { 'test-team' } 107 | let(:file) { File.open('spec/fixtures/files/egg.png') } 108 | 109 | before do 110 | stub_request(:post, "https://api.esa.io/v1/teams/test-team/attachments/policies") 111 | .with( 112 | body: "{\"type\":\"image/png\",\"size\":49816,\"name\":\"egg.png\"}", 113 | headers: { 114 | 'Accept'=>'application/json', 115 | } 116 | ) 117 | .to_return( 118 | status: 200, 119 | body: { 120 | attachment: { 121 | endpoint: 'https://test.s3-ap-northeast-1.amazonaws.com', 122 | url: 'https://example.com/test.png' 123 | }, 124 | form: { 125 | foo: 'bar' 126 | } 127 | }.to_json, 128 | headers: { 129 | 'Content-Type' => 'application/json; charset=utf-8' 130 | } 131 | ) 132 | 133 | stub_request(:post, "https://test.s3-ap-northeast-1.amazonaws.com/") 134 | .to_return(status: 204, body: "", headers: { 135 | 'Content-Type' => 'application/xml' 136 | }) 137 | end 138 | 139 | it 'return URL for uploaded attachment'do 140 | response = client.upload_attachment(file) 141 | expect(response.body['attachment']['url']).to eq('https://example.com/test.png') 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/esa/response_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Esa::Response do 4 | let(:response_status) { 401 } 5 | let(:response_body) { '{"error":"unauthorized", "message":"Unauthorized"}' } 6 | let(:response_headers) do 7 | { 8 | 'server' => 'Cowboy', 9 | 'content-type' => 'application/json; charset=utf-8', 10 | } 11 | end 12 | let(:connection) do 13 | Faraday.new do |c| 14 | c.adapter :test, Faraday::Adapter::Test::Stubs.new do |stub| 15 | stub.get('/test') { [response_status, response_headers, response_body] } 16 | end 17 | end 18 | end 19 | let(:response) { described_class.new(connection.get('/test')) } 20 | 21 | describe '#body' do 22 | subject { response.body } 23 | it { is_expected.to eq response_body } 24 | end 25 | 26 | describe '#headers' do 27 | subject { response.headers } 28 | let(:expectation) do 29 | { 30 | 'Server' => 'Cowboy', 31 | 'Content-Type' => 'application/json; charset=utf-8', 32 | } 33 | end 34 | 35 | it { is_expected.to eq expectation } 36 | end 37 | 38 | describe '#status' do 39 | subject { response.status } 40 | it { is_expected.to eq response_status } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/fixtures/files/egg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esaio/esa-ruby/41e107b789f9bcf1eaf4fccebcfcffb44e7a08f1/spec/fixtures/files/egg.png -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'esa' 2 | require 'webmock/rspec' 3 | 4 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 5 | RSpec.configure do |config| 6 | config.expect_with :rspec do |expectations| 7 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 8 | end 9 | 10 | config.mock_with :rspec do |mocks| 11 | mocks.verify_partial_doubles = true 12 | end 13 | 14 | config.filter_run :focus 15 | config.run_all_when_everything_filtered = true 16 | 17 | config.disable_monkey_patching! 18 | config.warnings = true 19 | 20 | if config.files_to_run.one? 21 | config.default_formatter = 'doc' 22 | end 23 | 24 | config.profile_examples = 10 25 | config.order = :random 26 | Kernel.srand config.seed 27 | end 28 | --------------------------------------------------------------------------------