typetalk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +4 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +215 -0
  7. data/Rakefile +7 -0
  8. data/lib/typetalk.rb +36 -0
  9. data/lib/typetalk/api.rb +49 -0
  10. data/lib/typetalk/api/auth.rb +43 -0
  11. data/lib/typetalk/api/mention.rb +28 -0
  12. data/lib/typetalk/api/message.rb +105 -0
  13. data/lib/typetalk/api/notification.rb +77 -0
  14. data/lib/typetalk/api/topic.rb +56 -0
  15. data/lib/typetalk/api/user.rb +17 -0
  16. data/lib/typetalk/connection.rb +31 -0
  17. data/lib/typetalk/error.rb +7 -0
  18. data/lib/typetalk/version.rb +3 -0
  19. data/spec/cassettes/Typetalk_Api_Auth/_get_access_token/should_get_the_correct_resource_by_authorization_code.yml +40 -0
  20. data/spec/cassettes/Typetalk_Api_Auth/_get_access_token/should_get_the_correct_resource_by_client_credentials.yml +40 -0
  21. data/spec/cassettes/Typetalk_Api_Auth/_get_access_token/should_get_the_correct_resource_by_client_credentials_when_scope_changed.yml +40 -0
  22. data/spec/cassettes/Typetalk_Api_Auth/_get_access_token/should_raise_error_when_authorization_code_is_wrong.yml +40 -0
  23. data/spec/cassettes/Typetalk_Api_Auth/_get_access_token/should_raise_error_when_client_id_is_wrong.yml +40 -0
  24. data/spec/cassettes/Typetalk_Api_Auth/_get_access_token/should_raise_error_when_client_secret_is_wrong.yml +40 -0
  25. data/spec/cassettes/Typetalk_Api_Auth/_get_access_token/should_raise_error_when_redirect_uri_mismatch.yml +40 -0
  26. data/spec/cassettes/Typetalk_Api_Mention/_get_mentions/should_get_the_correct_resource.yml +85 -0
  27. data/spec/cassettes/Typetalk_Api_Mention/_get_mentions/should_get_the_correct_resource_till_mention.yml +83 -0
  28. data/spec/cassettes/Typetalk_Api_Mention/_get_mentions/should_get_the_unread_resource.yml +83 -0
  29. data/spec/cassettes/Typetalk_Api_Mention/_read_mention/should_get_the_correct_resource.yml +83 -0
  30. data/spec/cassettes/Typetalk_Api_Mention/_read_mention/should_raise_error_when_mention_id_is_wrong.yml +73 -0
  31. data/spec/cassettes/Typetalk_Api_Message/_get_message/should_get_the_correct_resource.yml +129 -0
  32. data/spec/cassettes/Typetalk_Api_Message/_get_message/should_raise_error_when_post_id_is_wrong.yml +71 -0
  33. data/spec/cassettes/Typetalk_Api_Message/_get_message/should_raise_error_when_topic_id_is_wrong.yml +71 -0
  34. data/spec/cassettes/Typetalk_Api_Message/_like_message/should_get_the_correct_resource.yml +161 -0
  35. data/spec/cassettes/Typetalk_Api_Message/_like_message/should_raise_error_when_post_id_is_wrong.yml +73 -0
  36. data/spec/cassettes/Typetalk_Api_Message/_post_message/should_get_the_correct_resource.yml +85 -0
  37. data/spec/cassettes/Typetalk_Api_Message/_post_message/should_get_the_correct_resource_for_attachments.yml +536 -0
  38. data/spec/cassettes/Typetalk_Api_Message/_post_message/should_get_the_correct_resource_for_reply.yml +130 -0
  39. data/spec/cassettes/Typetalk_Api_Message/_post_message/should_get_the_correct_resource_for_talks.yml +88 -0
  40. data/spec/cassettes/Typetalk_Api_Message/_post_message/should_raise_error_when_topic_id_is_wrong.yml +73 -0
  41. data/spec/cassettes/Typetalk_Api_Message/_read_message/should_get_the_correct_resource.yml +171 -0
  42. data/spec/cassettes/Typetalk_Api_Message/_read_message/should_get_the_correct_resource_till_post.yml +171 -0
  43. data/spec/cassettes/Typetalk_Api_Message/_read_message/should_raise_error_when_post_id_is_wrong.yml +73 -0
  44. data/spec/cassettes/Typetalk_Api_Message/_remove_message/should_get_the_correct_resource.yml +199 -0
  45. data/spec/cassettes/Typetalk_Api_Message/_remove_message/should_raise_error_when_post_id_is_wrong.yml +71 -0
  46. data/spec/cassettes/Typetalk_Api_Message/_unlike_message/should_get_the_correct_resource.yml +200 -0
  47. data/spec/cassettes/Typetalk_Api_Message/_unlike_message/should_raise_error_when_post_id_is_wrong.yml +71 -0
  48. data/spec/cassettes/Typetalk_Api_Message/_upload_attachment/should_get_the_correct_resource.yml +258 -0
  49. data/spec/cassettes/Typetalk_Api_Message/_upload_attachment/should_raise_error_when_topic_id_is_wrong.yml +250 -0
  50. data/spec/cassettes/Typetalk_Api_Notification/_accept_team/should_get_the_correct_resource.yml +119 -0
  51. data/spec/cassettes/Typetalk_Api_Notification/_accept_team/should_raise_error_when_invite_team_id_is_wrong.yml +73 -0
  52. data/spec/cassettes/Typetalk_Api_Notification/_accept_team/should_raise_error_when_team_id_is_wrong.yml +73 -0
  53. data/spec/cassettes/Typetalk_Api_Notification/_accept_topic/should_get_the_correct_resource.yml +120 -0
  54. data/spec/cassettes/Typetalk_Api_Notification/_accept_topic/should_raise_error_when_invite_topic_id_is_wrong.yml +73 -0
  55. data/spec/cassettes/Typetalk_Api_Notification/_accept_topic/should_raise_error_when_topic_id_is_wrong.yml +73 -0
  56. data/spec/cassettes/Typetalk_Api_Notification/_decline_team/should_get_the_correct_resource.yml +118 -0
  57. data/spec/cassettes/Typetalk_Api_Notification/_decline_team/should_raise_error_when_invite_team_id_is_wrong.yml +73 -0
  58. data/spec/cassettes/Typetalk_Api_Notification/_decline_team/should_raise_error_when_team_id_is_wrong.yml +73 -0
  59. data/spec/cassettes/Typetalk_Api_Notification/_decline_topic/should_get_the_correct_resource.yml +120 -0
  60. data/spec/cassettes/Typetalk_Api_Notification/_decline_topic/should_raise_error_when_invite_topic_id_is_wrong.yml +73 -0
  61. data/spec/cassettes/Typetalk_Api_Notification/_decline_topic/should_raise_error_when_topic_id_is_wrong.yml +73 -0
  62. data/spec/cassettes/Typetalk_Api_Notification/_get_notifications/should_get_the_correct_resource.yml +101 -0
  63. data/spec/cassettes/Typetalk_Api_Notification/_get_notifications_status/should_get_the_correct_resource.yml +79 -0
  64. data/spec/cassettes/Typetalk_Api_Notification/_read_notifications/should_get_the_correct_resource.yml +81 -0
  65. data/spec/cassettes/Typetalk_Api_Topic/_favorite_topic/should_get_the_correct_resource.yml +82 -0
  66. data/spec/cassettes/Typetalk_Api_Topic/_favorite_topic/should_raise_error_when_topic_id_is_wrong.yml +73 -0
  67. data/spec/cassettes/Typetalk_Api_Topic/_get_topic/should_get_the_correct_backward_resource.yml +85 -0
  68. data/spec/cassettes/Typetalk_Api_Topic/_get_topic/should_get_the_correct_forward_resource.yml +84 -0
  69. data/spec/cassettes/Typetalk_Api_Topic/_get_topic/should_get_the_correct_resource.yml +88 -0
  70. data/spec/cassettes/Typetalk_Api_Topic/_get_topic/should_raise_error_when_topic_id_is_wrong.yml +71 -0
  71. data/spec/cassettes/Typetalk_Api_Topic/_get_topic_members/should_get_the_correct_resource.yml +81 -0
  72. data/spec/cassettes/Typetalk_Api_Topic/_get_topic_members/should_raise_error_when_topic_id_is_wrong.yml +71 -0
  73. data/spec/cassettes/Typetalk_Api_Topic/_get_topics/should_get_the_correct_resource.yml +83 -0
  74. data/spec/cassettes/Typetalk_Api_Topic/_unfavorite_topic/should_get_the_correct_resource.yml +80 -0
  75. data/spec/cassettes/Typetalk_Api_Topic/_unfavorite_topic/should_raise_error_when_topic_id_is_wrong.yml +71 -0
  76. data/spec/cassettes/Typetalk_Api_User/_get_profile/should_get_the_correct_resource.yml +81 -0
  77. data/spec/cassettes/Typetalk_Api_User/_get_profile/should_raise_error_when_access_token_is_wrong.yml +73 -0
  78. data/spec/cassettes/Typetalk_Api_User/_get_profile/should_raise_error_when_scope_is_wrong.yml +73 -0
  79. data/spec/fixtures/attachments/logo_cacoo.jpg +0 -0
  80. data/spec/fixtures/attachments/logo_typetalk.jpg +0 -0
  81. data/spec/spec_helper.rb +36 -0
  82. data/spec/typetalk/api/auth_spec.rb +81 -0
  83. data/spec/typetalk/api/mention_spec.rb +73 -0
  84. data/spec/typetalk/api/message_spec.rb +232 -0
  85. data/spec/typetalk/api/notification_spec.rb +200 -0
  86. data/spec/typetalk/api/topic_spec.rb +150 -0
  87. data/spec/typetalk/api/user_spec.rb +27 -0
  88. data/typetalk.gemspec +33 -0
  89. metadata +326 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c8abe69ef420d358276adc435f463c3bcdbb1d17
4
+ data.tar.gz: cf7b032089f7482505217065765e9b7614e37859
5
+ SHA512:
6
+ metadata.gz: 286c0578f142feff79ed45b1f1757e4bc15591301feba0dc4cfe116431e1f8f73efdb52bb10538966c858d24a9f024dbeda0a5bb1ac8943a6b1e282021b13cc8
7
+ data.tar.gz: 6576852ef676d8745a7a9a3010040c3e5ee73f00830999f3692170275fd98caab156b902f396635a29271bc166832615e1feead36b9e11814668eacf484a0104
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+ vendor/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format=nested
3
+ --backtrace
4
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in typetalk.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 umakoz
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.
data/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # Typetalk
2
+
3
+ A Ruby wrapper for [Typetalk API](http://developers.typetalk.in/api.html). The Typetalk gem provides an easy-to-use wrapper for Typetalk's REST APIs.
4
+
5
+
6
+
7
+
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'typetalk'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install typetalk
22
+
23
+
24
+
25
+
26
+
27
+ ## Configuration
28
+
29
+ You need to provide your Client ID and Client Secret.
30
+
31
+ Typetalk.configure do |config|
32
+ config.client_id = '...'
33
+ config.client_secret = '...'
34
+ # option
35
+ config.scope = 'topic.read,topic.post,my'
36
+ config.grant_type = 'client_credentials' # or 'authorization_code'
37
+ config.redirect_uri = '...' # for authorization code
38
+ config.proxy = '...'
39
+ end
40
+
41
+ You can also specify these values via ENV:
42
+
43
+ export TYPETALK_CLIENT_ID='...'
44
+ export TYPETALK_CLIENT_SECRET='...'
45
+
46
+
47
+
48
+
49
+
50
+ ## Usage
51
+
52
+ ### Authetication
53
+
54
+ It is usually not necessary if there is a configuration, because it automatically acquires.
55
+
56
+ require 'rubygems'
57
+ require 'typetalk'
58
+
59
+ api = Typetalk::Api.new
60
+
61
+ # Get access token using client credentials
62
+ response = api.get_access_token(scope: 'my')
63
+ access_token = response.access_token
64
+
65
+
66
+
67
+ ### Profile
68
+
69
+ # api is a Typetalk::Api
70
+
71
+ # Get my profile
72
+ response = api.get_profile
73
+ my_name = response.account.name
74
+ my_full_name = response.account.fullName
75
+
76
+
77
+
78
+ ### Topics
79
+
80
+ # api is a Typetalk::Api
81
+
82
+ # Get my topics
83
+ response = api.get_topics
84
+ topic_id = response.topics[0].topic.id
85
+ topic_name = response.topics[0].topic.name
86
+
87
+ # Get topic messages
88
+ topic = api.get_topic(topic_id)
89
+ post_id = topic.posts[0].id
90
+ message_text = topic.posts[0].message
91
+ sender = topic.posts[0].account.name
92
+
93
+ # Get topic members
94
+ members = api.get_topic_members(topic_id)
95
+ member_id = members.accounts[0].account.id
96
+ member_name = members.accounts[0].account.name
97
+
98
+ # Favorite topic
99
+ api.favorite_topic(topic_id)
100
+
101
+ # unfavorite topic
102
+ api.unfavorite_topic(topic_id)
103
+
104
+
105
+
106
+ ### Message
107
+
108
+ # api is a Typetalk::Api
109
+
110
+ # Post message
111
+ api.post_message(topic_id, 'message text')
112
+
113
+ # Post message with attachment
114
+ attachment = api.upload_attachment(topic_id, '/path/to/attachment.jpg')
115
+ response = api.post_message(topic_id, 'message text', file_keys:[attachment.fileKey])
116
+
117
+ # Get message
118
+ message = api.get_message(topic_id, response.post.id)
119
+ message_text = message.post.message
120
+ sender = message.post.account.name
121
+
122
+ # Like message
123
+ api.like_message(topic_id, post_id)
124
+
125
+ # Unlike message
126
+ api.unlike_message(topic_id, post_id)
127
+
128
+ # Read message
129
+ api.read_message(topic_id, post_id)
130
+
131
+ # Read all messages
132
+ api.read_message(topic_id)
133
+
134
+ # Remove message
135
+ api.remove_message(topic_id, post_id)
136
+
137
+
138
+
139
+ ### Notification
140
+
141
+ # api is a Typetalk::Api
142
+
143
+ # Get notification list
144
+ response = api.get_notifications
145
+ # Team invitation
146
+ team_id = response.invites.teams[0].team.id
147
+ invite_team_id = response.invites.teams[0].id
148
+ # Topic invitation
149
+ topic_id = response.invites.topic[0].topic.id
150
+ invite_topic_id = response.invites.topic[0].id
151
+ # Mention
152
+ mention_message_text = response.mentions[0].post.message
153
+ mention_topic_name = response.mentions[0].post.topic.name
154
+ mention_sender = response.mentions[0].post.account.name
155
+
156
+ # Get notification count
157
+ response = api.get_notifications_status
158
+ unopened = response.access.unopened
159
+ team_pending = response.invite.team.pending
160
+ topic_pending = response.invite.topic.pending
161
+ mention_unread = response.mention.unread
162
+
163
+ # Read notification
164
+ api.read_notifications
165
+
166
+ # Accept team invitation
167
+ api.accept_team(team_id, invite_team_id)
168
+
169
+ # Decline team invitation
170
+ api.decline_team(team_id, invite_team_id)
171
+
172
+ # Accept topic invitation
173
+ api.accept_topic(topic_id, invite_topic_id)
174
+
175
+ # Decline topic invitation
176
+ api.decline_topic(topic_id, invite_topic_id)
177
+
178
+
179
+
180
+ ### Mention
181
+
182
+ # api is a Typetalk::Api
183
+
184
+ # Get mention list
185
+ response = api.get_mentions
186
+ mention_id = response.mentions[0].id
187
+ message_text = response.mentions[0].post.message
188
+ topic_name = response.mentions[0].post.topic.name
189
+ sender = response.mentions[0].post.account.name
190
+
191
+ # Get unread mention list
192
+ response = api.get_mentions(unread:true)
193
+
194
+ # Read mention
195
+ api.read_mention(mention_id)
196
+
197
+
198
+
199
+
200
+
201
+ ## Contributing
202
+
203
+ 1. Fork it ( https://github.com/umakoz/typetalk/fork )
204
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
205
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
206
+ 4. Push to the branch (`git push origin my-new-feature`)
207
+ 5. Create a new Pull Request
208
+
209
+
210
+
211
+
212
+
213
+ ## Copyright
214
+
215
+ Copyright (c) 2014- [Makoto Umami](mailto:umakoz@gmail.com). See [LICENSE](LICENSE.txt) for details.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task :default => :spec
data/lib/typetalk.rb ADDED
@@ -0,0 +1,36 @@
1
+ require "typetalk/version"
2
+ require 'typetalk/error'
3
+ require 'typetalk/api'
4
+
5
+ require 'hashie'
6
+
7
+
8
+ module Typetalk
9
+
10
+ DEFAULT_OPTIONS = {
11
+ client_id: ENV['TYPETALK_CLIENT_ID'],
12
+ client_secret: ENV['TYPETALK_CLIENT_SECRET'],
13
+ redirect_uri: nil,
14
+ grant_type: 'client_credentials', # or 'authorization_code'
15
+ scope: 'topic.read,topic.post,my',
16
+ endpoint: 'https://typetalk.in/api/v1',
17
+ proxy: nil,
18
+ }
19
+
20
+ class << self
21
+
22
+ def config
23
+ @config ||= Hashie::Mash.new(Typetalk::DEFAULT_OPTIONS)
24
+ end
25
+
26
+ def reset_config
27
+ @config = nil
28
+ end
29
+
30
+ def configure
31
+ yield config
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,49 @@
1
+ require 'typetalk/connection'
2
+
3
+ module Typetalk
4
+
5
+ class Api
6
+ include Connection
7
+
8
+ Dir[File.join(__dir__, 'api/*.rb')].each{|f| require f}
9
+ include Auth
10
+ include User
11
+ include Topic
12
+ include Message
13
+ include Mention
14
+ include Notification
15
+
16
+
17
+ def access_token
18
+ @access_token ||= get_access_token
19
+ @access_token['access_token']
20
+ end
21
+
22
+
23
+ protected
24
+ def parse_response(response)
25
+
26
+ # TODO remove debug print
27
+ #require 'awesome_print'
28
+ #ap response
29
+
30
+ case response.status
31
+ when 400, 401
32
+ raise InvalidRequest, response_values(response)
33
+ when 404
34
+ raise NotFound, response_values(response)
35
+ when 413
36
+ raise InvalidFileSize, response_values(response)
37
+ end
38
+
39
+ body = JSON.parse(response.body) rescue response.body
40
+ body.is_a?(Hash) ? Hashie::Mash.new(body) : body
41
+ end
42
+
43
+ def response_values(response)
44
+ {status: response.status, headers: response.headers, body: response.body}
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,43 @@
1
+ module Typetalk
2
+ class Api
3
+
4
+ module Auth
5
+ attr_accessor :authorization_code
6
+
7
+ def get_access_token(client_id:nil, client_secret:nil, grant_type:nil, scope:nil, code:nil, redirect_uri:nil)
8
+ body = {
9
+ client_id: client_id || Typetalk.config.client_id,
10
+ client_secret: client_secret || Typetalk.config.client_secret,
11
+ grant_type: grant_type || Typetalk.config.grant_type,
12
+ scope: scope || Typetalk.config.scope,
13
+ }
14
+
15
+ if body[:grant_type] == 'authorization_code'
16
+ body[:code] = code || @authorization_code
17
+ body[:redirect_uri] = redirect_uri || Typetalk.config.redirect_uri
18
+ end
19
+
20
+ response = connection.post do |req|
21
+ req.url 'https://typetalk.in/oauth2/access_token'
22
+ req.body = body
23
+ end
24
+ parse_response(response)
25
+ end
26
+
27
+
28
+ def self.authorize_url(client_id:nil, redirect_uri:nil, scope:nil)
29
+ params = {
30
+ client_id: client_id || Typetalk.config.client_id,
31
+ redirect_uri: redirect_uri || Typetalk.config.redirect_uri,
32
+ scope: scope || Typetalk.config.scope,
33
+ response_type: 'code',
34
+ }
35
+ url = URI.parse('https://typetalk.in/oauth2/authorize')
36
+ url.query = URI.encode_www_form(params)
37
+ url.to_s
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ module Typetalk
2
+ class Api
3
+
4
+ module Mention
5
+
6
+ def get_mentions(token:nil, from:nil, unread:nil)
7
+ response = connection.get do |req|
8
+ req.url "#{endpoint}/mentions"
9
+ req.params[:access_token] = token || access_token
10
+ req.params[:from] = from unless from.nil?
11
+ req.params[:unread] = unread unless unread.nil?
12
+ end
13
+ parse_response(response)
14
+ end
15
+
16
+
17
+ def read_mention(mention_id, token:nil)
18
+ response = connection.put do |req|
19
+ req.url "#{endpoint}/mentions/#{mention_id}"
20
+ req.params[:access_token] = token || access_token
21
+ end
22
+ parse_response(response)
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end