twingly-search 5.0.1 → 5.1.0

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 (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>