instagram 0.3.2 → 0.6

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 (75) hide show
  1. data/.gitignore +12 -0
  2. data/.rspec +3 -0
  3. data/.yardopts +9 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.md +20 -0
  6. data/README.md +144 -55
  7. data/Rakefile +21 -9
  8. data/instagram.gemspec +41 -0
  9. data/lib/faraday/oauth2.rb +36 -0
  10. data/lib/faraday/raise_http_4xx.rb +37 -0
  11. data/lib/faraday/raise_http_5xx.rb +29 -0
  12. data/lib/instagram.rb +21 -55
  13. data/lib/instagram/api.rb +23 -0
  14. data/lib/instagram/client.rb +19 -0
  15. data/lib/instagram/client/comments.rb +62 -0
  16. data/lib/instagram/client/likes.rb +58 -0
  17. data/lib/instagram/client/locations.rb +59 -0
  18. data/lib/instagram/client/media.rb +63 -0
  19. data/lib/instagram/client/real_time.rb +8 -0
  20. data/lib/instagram/client/subscriptions.rb +141 -0
  21. data/lib/instagram/client/tags.rb +59 -0
  22. data/lib/instagram/client/users.rb +165 -0
  23. data/lib/instagram/client/utils.rb +15 -0
  24. data/lib/instagram/configuration.rb +90 -0
  25. data/lib/instagram/connection.rb +31 -0
  26. data/lib/instagram/error.rb +16 -0
  27. data/lib/instagram/oauth.rb +27 -0
  28. data/lib/instagram/request.rb +45 -0
  29. data/lib/instagram/version.rb +3 -0
  30. data/spec/faraday/response_spec.rb +28 -0
  31. data/spec/fixtures/access_token.json +9 -0
  32. data/spec/fixtures/followed_by.json +1 -0
  33. data/spec/fixtures/follows.json +1 -0
  34. data/spec/fixtures/location.json +1 -0
  35. data/spec/fixtures/location_recent_media.json +1 -0
  36. data/spec/fixtures/location_search.json +1 -0
  37. data/spec/fixtures/media.json +1 -0
  38. data/spec/fixtures/media_comment.json +1 -0
  39. data/spec/fixtures/media_comment_deleted.json +1 -0
  40. data/spec/fixtures/media_comments.json +1 -0
  41. data/spec/fixtures/media_liked.json +1 -0
  42. data/spec/fixtures/media_likes.json +1 -0
  43. data/spec/fixtures/media_popular.json +1 -0
  44. data/spec/fixtures/media_search.json +1 -0
  45. data/spec/fixtures/media_unliked.json +1 -0
  46. data/spec/fixtures/mikeyk.json +1 -0
  47. data/spec/fixtures/recent_media.json +1 -0
  48. data/spec/fixtures/requested_by.json +12 -0
  49. data/spec/fixtures/shayne.json +1 -0
  50. data/spec/fixtures/subscription.json +12 -0
  51. data/spec/fixtures/subscription_deleted.json +1 -0
  52. data/spec/fixtures/subscription_payload.json +14 -0
  53. data/spec/fixtures/subscriptions.json +22 -0
  54. data/spec/fixtures/tag.json +1 -0
  55. data/spec/fixtures/tag_recent_media.json +1 -0
  56. data/spec/fixtures/tag_search.json +1 -0
  57. data/spec/fixtures/user_media_feed.json +1 -0
  58. data/spec/fixtures/user_search.json +1 -0
  59. data/spec/instagram/api_spec.rb +110 -0
  60. data/spec/instagram/client/comments_spec.rb +71 -0
  61. data/spec/instagram/client/likes_spec.rb +66 -0
  62. data/spec/instagram/client/locations_spec.rb +78 -0
  63. data/spec/instagram/client/media_spec.rb +78 -0
  64. data/spec/instagram/client/real_time_spec.rb +13 -0
  65. data/spec/instagram/client/subscriptions_spec.rb +118 -0
  66. data/spec/instagram/client/tags_spec.rb +78 -0
  67. data/spec/instagram/client/users_spec.rb +237 -0
  68. data/spec/instagram/client_spec.rb +23 -0
  69. data/spec/instagram_spec.rb +97 -0
  70. data/spec/spec_helper.rb +54 -0
  71. metadata +265 -33
  72. data/MIT-LICENSE +0 -18
  73. data/lib/instagram/cached.rb +0 -25
  74. data/lib/instagram/failsafe_store.rb +0 -52
  75. data/lib/instagram/models.rb +0 -163
@@ -0,0 +1,23 @@
1
+ require File.expand_path('../connection', __FILE__)
2
+ require File.expand_path('../request', __FILE__)
3
+ require File.expand_path('../oauth', __FILE__)
4
+
5
+ module Instagram
6
+ # @private
7
+ class API
8
+ # @private
9
+ attr_accessor *Configuration::VALID_OPTIONS_KEYS
10
+
11
+ # Creates a new API
12
+ def initialize(options={})
13
+ options = Instagram.options.merge(options)
14
+ Configuration::VALID_OPTIONS_KEYS.each do |key|
15
+ send("#{key}=", options[key])
16
+ end
17
+ end
18
+
19
+ include Connection
20
+ include Request
21
+ include OAuth
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Instagram
2
+ # Wrapper for the Instagram REST API
3
+ #
4
+ # @note All methods have been separated into modules and follow the same grouping used in {TODO:doc_URL the Instagram API Documentation}.
5
+ # @see TODO:doc_url
6
+ class Client < API
7
+ Dir[File.expand_path('../client/*.rb', __FILE__)].each{|f| require f}
8
+
9
+ include Instagram::Client::Utils
10
+
11
+ include Instagram::Client::Users
12
+ include Instagram::Client::Media
13
+ include Instagram::Client::Locations
14
+ include Instagram::Client::Tags
15
+ include Instagram::Client::Comments
16
+ include Instagram::Client::Likes
17
+ include Instagram::Client::Subscriptions
18
+ end
19
+ end
@@ -0,0 +1,62 @@
1
+ module Instagram
2
+ class Client
3
+ # Defines methods related to comments
4
+ module Comments
5
+ # Returns a list of comments for a given media item ID
6
+ #
7
+ # @overload media_comments(id)
8
+ # @param id [Integer] An Instagram media item ID
9
+ # @return [Hashie::Mash] The requested comment.
10
+ # @example Returns a list of comments for the media item of ID 1234
11
+ # Instagram.media_comments(777)
12
+ # @format :json
13
+ # @authenticated true
14
+ #
15
+ # If getting this data of a protected user, you must be authenticated (and be allowed to see that user).
16
+ # @rate_limited true
17
+ # @see TODO:docs url
18
+ def media_comments(id, *args)
19
+ response = get("media/#{id}/comments")
20
+ response["data"]
21
+ end
22
+
23
+ # Creates a comment for a given media item ID
24
+ #
25
+ # @overload create_media_comment(id, text)
26
+ # @param id [Integer] An Instagram media item ID
27
+ # @param text [String] The text of your comment
28
+ # @return [Hashie::Mash] The comment created.
29
+ # @example Creates a new comment on media item with ID 777
30
+ # Instagram.create_media_comment(777, "Oh noes!")
31
+ # @format :json
32
+ # @authenticated true
33
+ #
34
+ # If getting this data of a protected user, you must be authenticated (and be allowed to see that user).
35
+ # @rate_limited true
36
+ # @see TODO:docs url
37
+ def create_media_comment(id, text, options={})
38
+ response = post("media/#{id}/comments", options.merge(:text => text))
39
+ response["data"]
40
+ end
41
+
42
+ # Deletes a comment for a given media item ID
43
+ #
44
+ # @overload delete_media_comment(media_id, comment_id)
45
+ # @param media_id [Integer] An Instagram media item ID.
46
+ # @param comment_id [Integer] Your comment ID of the comment you wish to delete.
47
+ # @return [nil]
48
+ # @example Delete the comment with ID of 1234, on the media item with ID of 777
49
+ # Instagram.delete_media_comment(777, 1234)
50
+ # @format :json
51
+ # @authenticated true
52
+ #
53
+ # In order to remove a comment, you must be either the owner comment or the media item (or both).
54
+ # @rate_limited true
55
+ # @see TODO:docs url
56
+ def delete_media_comment(media_id, comment_id, options={})
57
+ response = delete("media/#{media_id}/comments/#{comment_id}", options)
58
+ response["data"]
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ module Instagram
2
+ class Client
3
+ # Defines methods related to likes
4
+ module Likes
5
+ # Returns a list of users who like a given media item ID
6
+ #
7
+ # @overload media_likes(id)
8
+ # @param media [Integer] An Instagram media item ID
9
+ # @return [Hashie::Mash] A list of users.
10
+ # @example Returns a list of users who like the media item of ID 1234
11
+ # Instagram.media_likes(777)
12
+ # @format :json
13
+ # @authenticated true
14
+ #
15
+ # If getting this data of a protected user, you must be authenticated (and be allowed to see that user).
16
+ # @rate_limited true
17
+ # @see TODO:docs url
18
+ def media_likes(id, *args)
19
+ response = get("media/#{id}/likes")
20
+ response["data"]
21
+ end
22
+
23
+ # Issues a like by the currently authenticated user, for a given media item ID
24
+ #
25
+ # @overload like_media(id, text)
26
+ # @param id [Integer] An Instagram media item ID
27
+ # @return [nil]
28
+ # @example Like media item with ID 777
29
+ # Instagram.like_media(777)
30
+ # @format :json
31
+ # @authenticated true
32
+ #
33
+ # If getting this data of a protected user, you must be authenticated (and be allowed to see that user).
34
+ # @rate_limited true
35
+ # @see TODO:docs url
36
+ def like_media(id, options={})
37
+ response = post("media/#{id}/likes", options)
38
+ response["data"]
39
+ end
40
+
41
+ # Removes the like on a givem media item ID for the currently authenticated user
42
+ #
43
+ # @overload unlike_media(id)
44
+ # @param media_id [Integer] An Instagram media item ID.
45
+ # @return [nil]
46
+ # @example Remove the like for the currently authenticated user on the media item with the ID of 777
47
+ # Instagram.unlike_media(777)
48
+ # @format :json
49
+ # @authenticated true
50
+ # @rate_limited true
51
+ # @see TODO:docs url
52
+ def unlike_media(id, options={})
53
+ response = delete("media/#{id}/likes", options)
54
+ response["data"]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,59 @@
1
+ module Instagram
2
+ class Client
3
+ # Defines methods related to media items
4
+ module Locations
5
+ # Returns extended information of a given Instagram location
6
+ #
7
+ # @overload location(id)
8
+ # @param location [Integer] An Instagram location ID
9
+ # @return [Hashie::Mash] The requested location.
10
+ # @example Return extended information for the Instagram office
11
+ # Instagram.location(514276)
12
+ # @format :json
13
+ # @authenticated false
14
+ # @rate_limited true
15
+ # @see TODO:docs url
16
+ def location(id, *args)
17
+ response = get("locations/#{id}")
18
+ response["data"]
19
+ end
20
+
21
+ # Returns a list of recent media items for a given Instagram location
22
+ #
23
+ # @overload location_recent_media(id, options={})
24
+ # @param user [Integer] An Instagram user ID.
25
+ # @param options [Hash] A customizable set of options.
26
+ # @option options [Integer] :max_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID.
27
+ # @option options [Integer] :count (nil) Limits the number of results returned per page, maximum 150.
28
+ # @return [Hashie::Mash]
29
+ # @example Return a list of the most recent media items taken at the Instagram office
30
+ # Instagram.location_recent_media(514276)
31
+ # @see TODO:docs url
32
+ # @format :json
33
+ # @authenticated false
34
+ # @rate_limited true
35
+ def location_recent_media(id, *args)
36
+ options = args.last.is_a?(Hash) ? args.pop : {}
37
+ response = get("locations/#{id}/media/recent", options)
38
+ response["data"]
39
+ end
40
+
41
+ # Returns Instagram locations within proximity of given lat,lng
42
+ #
43
+ # @param lat [String] A given latitude in decimal format
44
+ # @param lng [String] A given longitude in decimal format
45
+ # @option options [Integer] :count The number of media items to retrieve. Maxiumum of 100 allowed per page.
46
+ # @return [Array]
47
+ # @example Return locations around 37.7808851, -122.3948632 (164 S Park, SF, CA USA)
48
+ # Instagram.location_search("37.7808851", "-122.3948632")
49
+ # @see TODO:doc url
50
+ # @format :json
51
+ # @authenticated false
52
+ # @rate_limited true
53
+ def location_search(lat, lng, options={})
54
+ response = get('locations/search', options.merge(:lat => lat, :lng => lng))
55
+ response["data"]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,63 @@
1
+ module Instagram
2
+ class Client
3
+ # Defines methods related to media items
4
+ module Media
5
+ # Returns extended information of a given media item
6
+ #
7
+ # @overload media_item(id)
8
+ # @param user [Integer] An Instagram media item ID
9
+ # @return [Hashie::Mash] The requested media item.
10
+ # @example Return extended information for media item 1234
11
+ # Instagram.media_item(1324)
12
+ # @format :json
13
+ # @authenticated false unless requesting media from a protected user
14
+ #
15
+ # If getting this data of a protected user, you must authenticate (and be allowed to see that user).
16
+ # @rate_limited true
17
+ # @see TODO:docs url
18
+ def media_item(*args)
19
+ id = args.first || 'self'
20
+ response = get("media/#{id}")
21
+ response["data"]
22
+ end
23
+
24
+ # Returns a list of the overall most popular media
25
+ #
26
+ # @overload media_popular(options={})
27
+ # @param options [Hash] A customizable set of options.
28
+ # @return [Hashie::Mash]
29
+ # @example Returns a list of the overall most popular media
30
+ # Instagram.media_popular
31
+ # @see TODO:docs url
32
+ # @format :json
33
+ # @authenticated false unless requesting it from a protected user
34
+ #
35
+ # If getting this data of a protected user, you must authenticate (and be allowed to see that user).
36
+ # @rate_limited true
37
+ def media_popular(*args)
38
+ options = args.last.is_a?(Hash) ? args.pop : {}
39
+ id = args.first || "self"
40
+ response = get("media/popular", options)
41
+ response["data"]
42
+ end
43
+
44
+ # Returns media items within proximity of given lat,lng
45
+ #
46
+ # @param lat [String] A given latitude in decimal format
47
+ # @param lng [String] A given longitude in decimal format
48
+ # @param options [Hash] A customizable set of options.
49
+ # @option options [Integer] :count The number of media items to retrieve. Maxiumum of 100 allowed per page.
50
+ # @return [Array]
51
+ # @example Return media around 37.7808851, -122.3948632 (164 S Park, SF, CA USA)
52
+ # Instagram.media_search("37.7808851", "-122.3948632")
53
+ # @see TODO:doc url
54
+ # @format :json
55
+ # @authenticated false
56
+ # @rate_limited true
57
+ def media_search(lat, lng, options={})
58
+ response = get('media/search', options.merge(:lat => lat, :lng => lng))
59
+ response["data"]
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,8 @@
1
+ module Instagram
2
+ class Client
3
+ # Defines methods related to real-time
4
+ module RealTime
5
+ # TODO
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,141 @@
1
+ module Instagram
2
+ class Client
3
+ # Defines methods related to real-time
4
+ module Subscriptions
5
+ # Returns a list of active real-time subscriptions
6
+ #
7
+ # @overload subscriptions(options={})
8
+ # @return [Hashie::Mash] The list of subscriptions.
9
+ # @example Returns a list of subscriptions for the authenticated application
10
+ # Instagram.subscriptions
11
+ # @format :json
12
+ # @authenticated true
13
+ #
14
+ # Requires client_secret to be set on the client or passed in options
15
+ # @rate_limited true
16
+ # @see https://api.instagram.com/developer/realtime/
17
+ def subscriptions(options={})
18
+ response = get("subscriptions", options.merge(:client_secret => client_secret))
19
+ response["data"]
20
+ end
21
+
22
+ # Creates a real-time subscription
23
+ #
24
+ # @overload create_subscription(options={})
25
+ # @param options [Hash] A set of parameters
26
+ # @option options [String] :object The object you'd like to subscribe to (user, tag, location or geography)
27
+ # @option options [String] :callback_url The subscription callback URL
28
+ # @option options [String] :aspect The aspect of the object you'd like to subscribe to (in this case, "media").
29
+ # @option options [String, Integer] :object_id When specifying a location or tag use the location's ID or tag name respectively
30
+ # @option options [String, Float] :lat The center latitude of an area, used when subscribing to a geography object
31
+ # @option options [String, Float] :lng The center longitude of an area, used when subscribing to a geography object
32
+ # @option options [String, Integer] :radius The distance in meters you'd like to capture around a given point (max 5000 meters)
33
+ # @overload create_subscription(object, callback_url, aspect="media", options={})
34
+ # @param object [String] The object you'd like to subscribe to (user, tag, location or geography)
35
+ # @param callback_url [String] The subscription callback URL
36
+ # @param aspect [String] he aspect of the object you'd like to subscribe to (in this case, "media").
37
+ # @param options [Hash] Addition options and parameters
38
+ # @option options [String, Integer] :object_id When specifying a location or tag use the location's ID or tag name respectively
39
+ # @option options [String, Float] :lat The center latitude of an area, used when subscribing to a geography object
40
+ # @option options [String, Float] :lng The center longitude of an area, used when subscribing to a geography object
41
+ # @option options [String, Integer] :radius The distance in meters you'd like to capture around a given point (max 5000 meters)
42
+ #
43
+ # Note that we only support "media" at this time, but we might support other types of subscriptions in the future.
44
+ # @return [Hashie::Mash] The subscription created.
45
+ # @example Creates a new subscription to receive notifications for user media changes.
46
+ # Instagram.create_subscription("user", "http://example.com/instagram/callback")
47
+ # @format :json
48
+ # @authenticated true
49
+ #
50
+ # Requires client_secret to be set on the client or passed in options
51
+ # @rate_limited true
52
+ # @see https://api.instagram.com/developer/realtime/
53
+ def create_subscription(*args)
54
+ options = args.last.is_a?(Hash) ? args.pop : {}
55
+ object = args.shift
56
+ callback_url = args.shift
57
+ aspect = args.shift
58
+ options.tap {|o|
59
+ o[:object] = object unless object.nil?
60
+ o[:callback_url] = callback_url unless callback_url.nil?
61
+ o[:aspect] = aspect || o[:aspect] || "media"
62
+ }
63
+ response = post("subscriptions", options.merge(:client_secret => client_secret))
64
+ response["data"]
65
+ end
66
+
67
+ # Deletes a real-time subscription
68
+ #
69
+ # @overload delete_subscription(options={})
70
+ # @param options [Hash] Addition options and parameters
71
+ # @option options [Integer] :subscription_id The subscription's ID
72
+ # @option options [String] :object When specified will remove all subscriptions of this object type, unless an :object_id is also specified (user, tag, location or geography)
73
+ # @option options [String, Integer] :object_id When specifying :object, inlcude an :object_id to only remove subscriptions of that object and object_id
74
+ # @overload delete_subscription(subscription_id, options={})
75
+ # @param subscription_id [Integer] The subscription's ID
76
+ # @param options [Hash] Addition options and parameters
77
+ # @option options [String] :object When specified will remove all subscriptions of this object type, unless an :object_id is also specified (user, tag, location or geography)
78
+ # @option options [String, Integer] :object_id When specifying :object, inlcude an :object_id to only remove subscriptions of that object and object_id
79
+ # @return [nil]
80
+ # @example Deletes an application's user change subscription
81
+ # Instagram.delete_subscription(:object => "user")
82
+ # @format :json
83
+ # @authenticated true
84
+ #
85
+ # Requires client_secret to be set on the client or passed in options
86
+ # @rate_limited true
87
+ # @see https://api.instagram.com/developer/realtime/
88
+ def delete_subscription(*args)
89
+ options = args.last.is_a?(Hash) ? args.pop : {}
90
+ subscription_id = args.first
91
+ options.merge!(:id => subscription_id) if subscription_id
92
+ response = delete("subscriptions", options.merge(:client_secret => client_secret))
93
+ response["data"]
94
+ end
95
+
96
+ # Process a subscription notification JSON payload
97
+ #
98
+ # @overload process_subscription(json, &block)
99
+ # @param json [String] The JSON response received by the Instagram real-time server
100
+ # @param block [Proc] A callable in which callbacks are defined
101
+ # @return [nil]
102
+ # @example Process and handle a notification for a user media change
103
+ # Instagram.process_subscription(params[:body]) do |handler|
104
+ #
105
+ # handler.on_user_changed do |user_id, data|
106
+ #
107
+ # user = User.by_instagram_id(user_id)
108
+ # @client = Instagram.client(:access_token => _access_token_for_user(user))
109
+ # latest_media = @client.user_recent_media[0]
110
+ # user.media.create_with_hash(latest_media)
111
+ # end
112
+ #
113
+ # end
114
+ # @format :json
115
+ # @authenticated true
116
+ #
117
+ # Requires client_secret to be set on the client or passed in options
118
+ # @rate_limited true
119
+ # @see https://api.instagram.com/developer/realtime/
120
+ def process_subscription(json, &block)
121
+ raise ArgumentError, "callbacks block expected" unless block_given?
122
+ payload = MultiJson.decode(json)
123
+ @changes = Hash.new { |h,k| h[k] = [] }
124
+ for change in payload
125
+ @changes[change['object']] << change
126
+ end
127
+ block.call(self)
128
+ end
129
+
130
+ [:user, :tag, :location, :geography].each do |object|
131
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ +1
132
+ def on_#{object}_changed(&block)
133
+ for change in @changes['#{object}']
134
+ yield change.delete('object_id'), change
135
+ end
136
+ end
137
+ RUBY_EVAL
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,59 @@
1
+ module Instagram
2
+ class Client
3
+ # Defines methods related to tags
4
+ module Tags
5
+ # Returns extended information of a given Instagram tag
6
+ #
7
+ # @overload tag(tag)
8
+ # @param tag [String] An Instagram tag name
9
+ # @return [Hashie::Mash] The requested tag.
10
+ # @example Return extended information for the tag "cat"
11
+ # Instagram.tag('cat')
12
+ # @format :json
13
+ # @authenticated false
14
+ # @rate_limited true
15
+ # @see TODO:docs url
16
+ def tag(tag, *args)
17
+ response = get("tags/#{tag}")
18
+ response["data"]
19
+ end
20
+
21
+ # Returns a list of recent media items for a given Instagram tag
22
+ #
23
+ # @overload tag_recent_media(tag, options={})
24
+ # @param user [String] An Instagram tag name.
25
+ # @param options [Hash] A customizable set of options.
26
+ # @option options [Integer] :max_id (nil) Returns results with an ID less than (that is, older than) or equal to the specified ID.
27
+ # @option options [Integer] :count (nil) Limits the number of results returned per page, maximum 150.
28
+ # @return [Hashie::Mash]
29
+ # @example Return a list of the most recent media items tagged "cat"
30
+ # Instagram.tag_recent_media('cat')
31
+ # @see TODO:docs url
32
+ # @format :json
33
+ # @authenticated false
34
+ # @rate_limited true
35
+ def tag_recent_media(id, *args)
36
+ options = args.last.is_a?(Hash) ? args.pop : {}
37
+ response = get("tags/#{id}/media/recent", options)
38
+ response["data"]
39
+ end
40
+
41
+ # Returns a list of tags starting with the given search query
42
+ #
43
+ # @format :json
44
+ # @authenticated false
45
+ # @rate_limited true
46
+ # @param query [String] The beginning or complete tag name to search for
47
+ # @param options [Hash] A customizable set of options.
48
+ # @option options [Integer] :count The number of media items to retrieve. Maxiumum of 100 allowed per page.
49
+ # @return [Array]
50
+ # @see TODO:doc url
51
+ # @example Return tags that start with "cat"
52
+ # Instagram.tag_search("cat")
53
+ def tag_search(query, options={})
54
+ response = get('tags/search', options.merge(:q => query))
55
+ response["data"]
56
+ end
57
+ end
58
+ end
59
+ end