twingly-search 5.0.1 → 5.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -2
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +33 -34
  5. data/Rakefile +0 -6
  6. data/examples/find_all_posts_mentioning_github.rb +3 -3
  7. data/examples/hello_world.rb +2 -2
  8. data/examples/livefeed_loop.rb +24 -0
  9. data/lib/twingly/livefeed/client.rb +121 -0
  10. data/lib/twingly/livefeed/error.rb +28 -0
  11. data/lib/twingly/livefeed/parser.rb +96 -0
  12. data/lib/twingly/livefeed/post.rb +66 -0
  13. data/lib/twingly/livefeed/result.rb +39 -0
  14. data/lib/twingly/livefeed/version.rb +5 -0
  15. data/lib/twingly/livefeed.rb +6 -0
  16. data/lib/twingly/search/client.rb +3 -2
  17. data/lib/twingly/search/error.rb +6 -5
  18. data/lib/twingly/search/parser.rb +39 -13
  19. data/lib/twingly/search/post.rb +65 -21
  20. data/lib/twingly/search/query.rb +46 -16
  21. data/lib/twingly/search/result.rb +11 -0
  22. data/lib/twingly/search/version.rb +1 -1
  23. data/spec/client_spec.rb +2 -2
  24. data/spec/error_spec.rb +27 -7
  25. data/spec/fixtures/incomplete_result.xml +2 -0
  26. data/spec/fixtures/livefeed/empty_api_key_result.xml +3 -0
  27. data/spec/fixtures/livefeed/non_xml_result.xml +1 -0
  28. data/spec/fixtures/livefeed/not_found_result.xml +3 -0
  29. data/spec/fixtures/livefeed/service_unavailable_result.xml +3 -0
  30. data/spec/fixtures/livefeed/unauthorized_api_key_result.xml +3 -0
  31. data/spec/fixtures/livefeed/valid_empty_result.xml +2 -0
  32. data/spec/fixtures/livefeed/valid_result.xml +79 -0
  33. data/spec/fixtures/minimal_valid_result.xml +81 -52
  34. data/spec/fixtures/nonexistent_api_key_result.xml +3 -3
  35. data/spec/fixtures/service_unavailable_result.xml +3 -3
  36. data/spec/fixtures/unauthorized_api_key_result.xml +3 -3
  37. data/spec/fixtures/undefined_error_result.xml +3 -3
  38. data/spec/fixtures/valid_empty_result.xml +2 -2
  39. data/spec/fixtures/valid_links_result.xml +36 -0
  40. data/spec/fixtures/vcr_cassettes/livefeed_valid_request.yml +169 -0
  41. data/spec/fixtures/vcr_cassettes/search_for_spotify_on_sv_blogs.yml +578 -447
  42. data/spec/fixtures/vcr_cassettes/search_without_valid_api_key.yml +15 -14
  43. data/spec/livefeed/client_spec.rb +135 -0
  44. data/spec/livefeed/error_spec.rb +51 -0
  45. data/spec/livefeed/parser_spec.rb +351 -0
  46. data/spec/livefeed/post_spec.rb +26 -0
  47. data/spec/livefeed/result_spec.rb +18 -0
  48. data/spec/parser_spec.rb +191 -94
  49. data/spec/post_spec.rb +25 -6
  50. data/spec/query_spec.rb +41 -34
  51. data/spec/result_spec.rb +1 -0
  52. data/spec/spec_helper.rb +10 -0
  53. data/twingly-search-api-ruby.gemspec +2 -3
  54. metadata +44 -24
  55. data/spec/fixtures/valid_non_blog_result.xml +0 -26
  56. data/spec/fixtures/valid_result.xml +0 -22975
@@ -6,16 +6,16 @@ module Twingly
6
6
  # Parse an API response body.
7
7
  #
8
8
  # @param [String] document containing an API response XML.
9
- # @raise [Error] which error depends on the API response (see {Error.from_api_response_message}).
9
+ # @raise [Error] which error depends on the API response (see {Error.from_api_response}).
10
10
  # @return [Result] containing the result.
11
11
  def parse(document)
12
12
  nokogiri = Nokogiri::XML(document)
13
13
 
14
- failure = nokogiri.at_xpath('//name:blogstream/name:operationResult[@resultType="failure"]', name: 'http://www.twingly.com')
14
+ failure = nokogiri.at_xpath('/error')
15
15
  handle_failure(failure) if failure
16
16
 
17
17
  data_node = nokogiri.at_xpath('/twinglydata')
18
- handle_non_xml_document(nokogiri) unless data_node
18
+ handle_non_xml_document(document) unless data_node
19
19
 
20
20
  create_result(data_node)
21
21
  end
@@ -27,41 +27,67 @@ module Twingly
27
27
  result.number_of_matches_returned = data_node.attribute('numberOfMatchesReturned').value.to_i
28
28
  result.number_of_matches_total = data_node.attribute('numberOfMatchesTotal').value.to_i
29
29
  result.seconds_elapsed = data_node.attribute('secondsElapsed').value.to_f
30
+ result.incomplete_result = incomplete_result?(data_node)
30
31
 
31
- data_node.xpath('//post[@contentType="blog"]').each do |post|
32
+ data_node.xpath('//post').each do |post|
32
33
  result.posts << parse_post(post)
33
34
  end
34
35
 
35
36
  result
36
37
  end
37
38
 
39
+ def incomplete_result?(data_node)
40
+ data_node.attribute('incompleteResult').value == "true"
41
+ end
42
+
38
43
  def parse_post(element)
39
44
  post_params = {}
40
45
  element.element_children.each do |child|
41
- if child.name == 'tags'
42
- post_params[child.name] = parse_tags(child)
43
- else
44
- post_params[child.name] = child.text
45
- end
46
+ post_params[child.name] =
47
+ case child.name
48
+ when *%w(tags links images)
49
+ parse_array(child)
50
+ when "coordinates"
51
+ parse_coordinates(child)
52
+ else
53
+ child.text
54
+ end
46
55
  end
56
+
47
57
  post = Post.new
48
58
  post.set_values(post_params)
49
59
  post
50
60
  end
51
61
 
52
- def parse_tags(element)
62
+ def parse_array(element)
53
63
  element.element_children.map do |child|
54
64
  child.text
55
65
  end
56
66
  end
57
67
 
68
+ # TODO: Decide if a class or hash should be used...
69
+ def parse_coordinates(element)
70
+ return {} if element.children.empty?
71
+
72
+ {
73
+ latitude: element.at_xpath("latitude/text()"),
74
+ longitude: element.at_xpath("longitude/text()"),
75
+ }
76
+ end
77
+
58
78
  def handle_failure(failure)
59
- fail Error.from_api_response_message(failure.text)
79
+ code = failure.attribute('code').value
80
+ message = failure.at_xpath('message').text
81
+
82
+ fail Error.from_api_response(code, message)
60
83
  end
61
84
 
62
85
  def handle_non_xml_document(document)
63
- response_text = document.search('//text()').map(&:text)
64
- fail ServerError, response_text
86
+ fail ServerError, "Failed to parse response: \"#{document}\""
87
+ end
88
+
89
+ def parse_time(time)
90
+ Time.parse(time)
65
91
  end
66
92
  end
67
93
  end
@@ -1,44 +1,88 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'date'
4
-
5
3
  module Twingly
6
4
  module Search
7
5
  # A blog post
8
6
  #
9
- # @attr_reader [String] url the post URL.
10
- # @attr_reader [String] title the post title.
11
- # @attr_reader [String] summary the blog post text.
7
+ # @attr_reader [String] id the post ID (Twingly internal identification)
8
+ # @attr_reader [String] author the author of the blog post
9
+ # @attr_reader [String] url the post URL
10
+ # @attr_reader [String] title the post title
11
+ # @attr_reader [String] text the blog post text
12
12
  # @attr_reader [String] language_code ISO two letter language code for the
13
- # language that the post was written in.
14
- # @attr_reader [Time] indexed the time, in UTC, when the post was indexed by Twingly.
15
- # @attr_reader [Time] published the time, in UTC, when the post was published.
16
- # @attr_reader [String] blog_url the blog URL.
17
- # @attr_reader [String] blog_name name of the blog.
18
- # @attr_reader [String] authority the blog's authority/influence.
19
- # See https://developer.twingly.com/resources/ranking/#authority
13
+ # language that the post was written in
14
+ # @attr_reader [String] location_code ISO two letter country code for the
15
+ # location of the blog
16
+ # @attr_reader [Hash] coordinates a hash containing :latitude and :longitude
17
+ # from the post RSS
18
+ # @attr_reader [Array] links all links from the blog post to other resources
19
+ # @attr_reader [Array] tags the post tags/categories
20
+ # @attr_reader [Array] images image URLs from the post (currently not populated)
21
+ # @attr_reader [Time] indexed_at the time, in UTC, when the post was indexed by Twingly
22
+ # @attr_reader [Time] published_at the time, in UTC, when the post was published
23
+ # @attr_reader [Time] reindexed_at timestamp when the post last was changed in our database/index
24
+ # @attr_reader [String] inlinks_count number of links to this post that was found in other blog posts
25
+ # @attr_reader [String] blog_id the blog ID (Twingly internal identification)
26
+ # @attr_reader [String] blog_name the name of the blog
27
+ # @attr_reader [String] blog_url the blog URL
20
28
  # @attr_reader [Integer] blog_rank the rank of the blog, based on authority and language.
21
29
  # See https://developer.twingly.com/resources/ranking/#blogrank
22
- # @attr_reader [Array] tags
30
+ # @attr_reader [Integer] authority the blog's authority/influence.
31
+ # See https://developer.twingly.com/resources/ranking/#authority
23
32
  class Post
24
- attr_reader :url, :title, :summary, :language_code, :indexed,
25
- :published, :blog_url, :blog_name, :authority, :blog_rank, :tags
33
+ attr_reader :id, :author, :url, :title, :text, :location_code,
34
+ :language_code, :coordinates, :links, :tags, :images, :indexed_at,
35
+ :published_at, :reindexed_at, :inlinks_count, :blog_id, :blog_name,
36
+ :blog_url, :blog_rank, :authority
26
37
 
27
38
  # Sets all instance variables for the {Post}, given a Hash.
28
39
  #
29
40
  # @param [Hash] params containing blog post data.
30
41
  def set_values(params)
42
+ @id = params.fetch('id')
43
+ @author = params.fetch('author')
31
44
  @url = params.fetch('url')
32
45
  @title = params.fetch('title')
33
- @summary = params.fetch('summary')
46
+ @text = params.fetch('text')
34
47
  @language_code = params.fetch('languageCode')
35
- @published = Time.parse(params.fetch('published'))
36
- @indexed = Time.parse(params.fetch('indexed'))
37
- @blog_url = params.fetch('blogUrl')
48
+ @location_code = params.fetch('locationCode')
49
+ @coordinates = params.fetch('coordinates', {})
50
+ @links = params.fetch('links', [])
51
+ @tags = params.fetch('tags', [])
52
+ @images = params.fetch('images', [])
53
+ @indexed_at = Time.parse(params.fetch('indexedAt'))
54
+ @published_at = Time.parse(params.fetch('publishedAt'))
55
+ @reindexed_at = Time.parse(params.fetch('reindexedAt'))
56
+ @inlinks_count = params.fetch('inlinksCount').to_i
57
+ @blog_id = params.fetch('blogId')
38
58
  @blog_name = params.fetch('blogName')
39
- @authority = params.fetch('authority').to_i
59
+ @blog_url = params.fetch('blogUrl')
40
60
  @blog_rank = params.fetch('blogRank').to_i
41
- @tags = params.fetch('tags', [])
61
+ @authority = params.fetch('authority').to_i
62
+ end
63
+
64
+ # @deprecated Please use {#text} instead
65
+ def summary
66
+ warn "[DEPRECATION] `summary` is deprecated. Please use `text` instead."
67
+ text
68
+ end
69
+
70
+ # @deprecated Please use {#indexed_at} instead
71
+ def indexed
72
+ warn "[DEPRECATION] `indexed` is deprecated. Please use `indexed_at` instead."
73
+ indexed_at
74
+ end
75
+
76
+ # @deprecated Please use {#published_at} instead
77
+ def published
78
+ warn "[DEPRECATION] `published` is deprecated. Please use `published_at` instead."
79
+ published_at
80
+ end
81
+
82
+ # @deprecated Please use {#links} instead
83
+ def outlinks
84
+ warn "[DEPRECATION] `outlinks` is deprecated. Please use `links` instead."
85
+ links
42
86
  end
43
87
  end
44
88
  end
@@ -5,11 +5,34 @@ module Twingly
5
5
  module Search
6
6
  # Twingly Search API query
7
7
  #
8
- # @attr [String] pattern the search query.
9
- # @attr [String] language language to restrict the query to.
8
+ # @attr [String] search_query the search query.
10
9
  # @attr [Client] client the client that this query is connected to.
11
10
  class Query
12
- attr_accessor :pattern, :language, :client
11
+ attr_accessor :search_query, :client
12
+
13
+ # @deprecated Please use {#search_query} instead
14
+ def pattern
15
+ warn "[DEPRECATION] `pattern` is deprecated. Please use `search_query` instead."
16
+ @search_query
17
+ end
18
+
19
+ # @deprecated Please use {#search_query=} instead
20
+ def pattern=(search_query)
21
+ warn "[DEPRECATION] `pattern=` is deprecated. Please use `search_query=` instead."
22
+ @search_query = search_query
23
+ end
24
+
25
+ # @deprecated Please use {#search_query} instead
26
+ def language
27
+ warn "[DEPRECATION] `language` is deprecated. Please use `search_query` instead."
28
+ @language
29
+ end
30
+
31
+ # @deprecated Please use {#search_query=} instead
32
+ def language=(language_code)
33
+ warn "[DEPRECATION] `language=` is deprecated. Please use `search_query=` instead."
34
+ @language = language_code
35
+ end
13
36
 
14
37
  # @return [Time] the time that was set with {#start_time=}.
15
38
  def start_time
@@ -36,7 +59,7 @@ module Twingly
36
59
 
37
60
  # Executes the query and returns the result.
38
61
  #
39
- # @raise [QueryError] if {#pattern} is empty.
62
+ # @raise [QueryError] if {#search_query} is empty.
40
63
  # @raise [AuthError] if the API couldn't authenticate you. Make sure your API key is correct.
41
64
  # @raise [ServerError] if the query could not be executed due to a server error.
42
65
  # @return [Result] the result for this query.
@@ -50,18 +73,21 @@ module Twingly
50
73
  Faraday::Utils.build_query(request_parameters)
51
74
  end
52
75
 
53
- # @raise [QueryError] if {#pattern} is empty.
76
+ # @raise [QueryError] if {#search_query} is empty.
54
77
  # @return [Hash] the request parameters.
55
78
  def request_parameters
56
- fail QueryError, "Missing pattern" if pattern.to_s.empty?
79
+ full_search_query = search_query.to_s.dup
80
+ full_search_query << " lang:#{@language}" unless @language.to_s.empty?
81
+ full_search_query << " start-date:#{formatted_start_date}" if start_time
82
+ full_search_query << " end-date:#{formatted_end_date}" if end_time
83
+
84
+ if full_search_query.to_s.empty?
85
+ fail QueryError, "Search query cannot be empty"
86
+ end
57
87
 
58
88
  {
59
- key: client.api_key,
60
- searchpattern: pattern,
61
- documentlang: language,
62
- ts: ts,
63
- tsTo: ts_to,
64
- xmloutputversion: 2,
89
+ apikey: client.api_key,
90
+ q: full_search_query
65
91
  }
66
92
  end
67
93
 
@@ -95,12 +121,16 @@ module Twingly
95
121
  fail QueryError, "Not a Time object" unless time.respond_to?(:to_time)
96
122
  end
97
123
 
98
- def ts
99
- start_time.to_time.utc.strftime("%F %T") if start_time
124
+ def formatted_start_date
125
+ format_timestamp_for_query(start_time) if start_time
126
+ end
127
+
128
+ def formatted_end_date
129
+ format_timestamp_for_query(end_time) if end_time
100
130
  end
101
131
 
102
- def ts_to
103
- end_time.to_time.utc.strftime("%F %T") if end_time
132
+ def format_timestamp_for_query(timestamp)
133
+ timestamp.to_time.utc.strftime("%FT%T")
104
134
  end
105
135
  end
106
136
  end
@@ -12,6 +12,16 @@ module Twingly
12
12
  class Result
13
13
  attr_accessor :number_of_matches_returned, :number_of_matches_total,
14
14
  :seconds_elapsed
15
+ attr_writer :incomplete_result
16
+
17
+ # @return [true] if one or multiple servers were too slow to respond
18
+ # within the maximum allowed query time.
19
+ # @return [false] if all servers responded within the maximum allowed
20
+ # query time.
21
+ # @see https://developer.twingly.com/resources/search/#response
22
+ def incomplete?
23
+ @incomplete_result
24
+ end
15
25
 
16
26
  # @return [Array<Post>] all posts that matched the {Query}.
17
27
  def posts
@@ -28,6 +38,7 @@ module Twingly
28
38
  matches = "@posts, "
29
39
  matches << "@number_of_matches_returned=#{self.number_of_matches_returned}, "
30
40
  matches << "@number_of_matches_total=#{self.number_of_matches_total}"
41
+ matches << "@incomplete_result=#{self.incomplete?}"
31
42
 
32
43
  sprintf("#<%s:0x%x %s>", self.class.name, __id__, matches)
33
44
  end
@@ -1,5 +1,5 @@
1
1
  module Twingly
2
2
  module Search
3
- VERSION = "5.0.1"
3
+ VERSION = "5.1.0"
4
4
  end
5
5
  end
data/spec/client_spec.rb CHANGED
@@ -90,13 +90,13 @@ describe Client do
90
90
 
91
91
  let(:query) do
92
92
  query = subject.query
93
- query.pattern = "something"
93
+ query.search_query = "something"
94
94
  query
95
95
  end
96
96
 
97
97
  it "should raise error on invalid API key" do
98
98
  VCR.use_cassette("search_without_valid_api_key") do
99
- expect { subject.execute_query(query) }.to raise_error(AuthError, "The API key does not exist.")
99
+ expect { subject.execute_query(query) }.to raise_error(AuthError, /Unauthorized/)
100
100
  end
101
101
  end
102
102
  end
data/spec/error_spec.rb CHANGED
@@ -3,17 +3,37 @@ require "spec_helper"
3
3
  describe Twingly::Search::Error do
4
4
  it { is_expected.to be_a(StandardError) }
5
5
 
6
- describe ".from_api_response_message" do
7
- subject { described_class.from_api_response_message(server_response_message) }
6
+ let(:message) { "This is the error message!" }
8
7
 
9
- context "when given message containing 'API key'" do
10
- let(:server_response_message) { "... API key ..." }
8
+ describe ".from_api_response" do
9
+ subject { described_class.from_api_response(code, message) }
11
10
 
12
- it { is_expected.to be_an(AuthError) }
11
+ context "when given code 401" do
12
+ let(:code) { 401 }
13
+
14
+ it { is_expected.to be_a(AuthError) }
15
+ end
16
+
17
+ context "when given code 402" do
18
+ let(:code) { 402 }
19
+
20
+ it { is_expected.to be_a(AuthError) }
21
+ end
22
+
23
+ context "when given code 400" do
24
+ let(:code) { 400 }
25
+
26
+ it { is_expected.to be_a(QueryError) }
27
+ end
28
+
29
+ context "when given code 404" do
30
+ let(:code) { 404 }
31
+
32
+ it { is_expected.to be_a(QueryError) }
13
33
  end
14
34
 
15
- context "when given a server error message" do
16
- let(:server_response_message) { "An error occured." }
35
+ context "when given another code" do
36
+ let(:code) { 500 }
17
37
 
18
38
  it { is_expected.to be_a(ServerError) }
19
39
  end
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <twinglydata numberOfMatchesReturned="0" secondsElapsed="0.203" numberOfMatchesTotal="0" incompleteResult="true" />
@@ -0,0 +1,3 @@
1
+ <error code="40001">
2
+ <message>Parameter apikey may not be empty</message>
3
+ </error>
@@ -0,0 +1 @@
1
+ <html><body>This is a non XML fixture.</body></html>
@@ -0,0 +1,3 @@
1
+ <error code="40401">
2
+ <message>Not Found</message>
3
+ </error>
@@ -0,0 +1,3 @@
1
+ <error code="50301">
2
+ <message>Authentication service unavailable</message>
3
+ </error>
@@ -0,0 +1,3 @@
1
+ <error code="40101">
2
+ <message>Unauthorized</message>
3
+ </error>
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <twinglydata ts="2017-04-25T08:25:35.9747845Z" from="2017-04-25T22:00:00Z" numberOfPosts="0" maxNumberOfPosts="3" nextTimestamp="2017-04-25T22:00:00Z"/>
@@ -0,0 +1,79 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <twinglydata ts="2017-04-11T15:09:48.8750635Z" from="2017-04-10T22:00:00Z" numberOfPosts="3" maxNumberOfPosts="3" firstPost="2017-04-10T22:00:29.267Z" lastPost="2017-04-10T22:11:47.243Z" nextTimestamp="2017-04-10T22:11:47.244Z">
3
+ <post>
4
+ <id>727444183574244541</id>
5
+ <author />
6
+ <url>http://flinnman.blogg.se/2017/april/mandag-igen.html</url>
7
+ <title>Måndag igen</title>
8
+ <text>Hoppla hejsan bloggy!Måndag. Förutom trist hosta på E så mår vi fint. Vi använde helgen till massa umgänge med nära och kära. Kändes extra skönt efter fredagen. Vi hade en tyst minut i en full matsal i polishuset idag. Det var fint! Well. Ny vecka och ... - Läs hela inlägget här</text>
9
+ <languageCode>sv</languageCode>
10
+ <locationCode>se</locationCode>
11
+ <coordinates />
12
+ <links />
13
+ <tags>
14
+ <tag>Jag</tag>
15
+ </tags>
16
+ <images />
17
+ <indexedAt>2017-04-10T22:00:24Z</indexedAt>
18
+ <publishedAt>2017-04-10T19:11:11Z</publishedAt>
19
+ <reindexedAt>2017-04-10T22:00:24Z</reindexedAt>
20
+ <inlinksCount>0</inlinksCount>
21
+ <blogId>10357806725947705095</blogId>
22
+ <blogName>Frida L</blogName>
23
+ <blogUrl>http://flinnman.blogg.se</blogUrl>
24
+ <blogRank>1</blogRank>
25
+ <authority>2</authority>
26
+ </post>
27
+ <post>
28
+ <id>6564050082079070812</id>
29
+ <author />
30
+ <url>http://malinbs.blogg.se/2017/april/du-ska-hedra-din-fader-och-din-moder.html</url>
31
+ <title>Du ska hedra din fader och din moder</title>
32
+ <text>Vilken solig och god helg det har varit på mina breddgrader! Minns för en massa år sedan (25? Eller lite mera?) när Herrn i Huset byggde altanen och samtidigt såg till att det blev eluttag där. Min mammas första kommentar blev: Tänk så bra med ett uttag, ... - Läs hela inlägget här</text>
33
+ <languageCode>sv</languageCode>
34
+ <locationCode>se</locationCode>
35
+ <coordinates />
36
+ <links />
37
+ <tags>
38
+ <tag>Allmänt</tag>
39
+ </tags>
40
+ <images />
41
+ <indexedAt>2017-04-10T22:00:16Z</indexedAt>
42
+ <publishedAt>2017-04-09T17:12:08Z</publishedAt>
43
+ <reindexedAt>2017-04-10T22:00:16Z</reindexedAt>
44
+ <inlinksCount>0</inlinksCount>
45
+ <blogId>14781290076709326355</blogId>
46
+ <blogName>Blogga, ett sätt att umgås!</blogName>
47
+ <blogUrl>http://malinbs.blogg.se</blogUrl>
48
+ <blogRank>1</blogRank>
49
+ <authority>4</authority>
50
+ </post>
51
+ <post>
52
+ <id>3062976931264108164</id>
53
+ <author>josegacel</author>
54
+ <url>https://josegabrielcelis.wordpress.com/2017/04/09/1476/</url>
55
+ <title />
56
+ <text>from Instagram: http://ift.tt/2ofZdhV</text>
57
+ <languageCode>sv</languageCode>
58
+ <locationCode />
59
+ <coordinates />
60
+ <links>
61
+ <link>http://www.ift.tt/2ofZdhV</link>
62
+ <link>http://feeds.wordpress.com/1.0/gocomments/josegabrielcelis.wordpress.com/1476</link>
63
+ </links>
64
+ <tags>
65
+ <tag>Fotos</tag>
66
+ <tag>Instagram</tag>
67
+ </tags>
68
+ <images />
69
+ <indexedAt>2017-04-10T22:00:28Z</indexedAt>
70
+ <publishedAt>2017-04-09T22:31:34Z</publishedAt>
71
+ <reindexedAt>2017-04-10T22:00:28Z</reindexedAt>
72
+ <inlinksCount>0</inlinksCount>
73
+ <blogId>1811310581070495497</blogId>
74
+ <blogName>José Gabriel Celis</blogName>
75
+ <blogUrl>https://josegabrielcelis.wordpress.com</blogUrl>
76
+ <blogRank>1</blogRank>
77
+ <authority>0</authority>
78
+ </post>
79
+ </twinglydata>