diaspora_federation 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 35ffb28db27478325eaf2f2d61290b125f2e20e2
4
+ data.tar.gz: cfaca21b0289d178044895bbb91326b51520fed7
5
+ SHA512:
6
+ metadata.gz: 0ef15d9756b8174572d54d926388e8dffbeabea94f342ceb62ef536f5c52f6c9003e904b208c22a277c4c4906c1723ea7bf7f82181dae3726a9b5aeaa51b77a0
7
+ data.tar.gz: 1e77ba30e7745f64927b58ab0c110fa9467f0b8981e539ce24500754d5a61f548ac63e362c84ad5cfb7b6bbbca92ac065b1aa1e4a9debf1599150dbad27a1f5b
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ Diaspora Federation Rails Engine
2
+ Copyright (C) 2015 Benjamin Neff
3
+
4
+ This program is free software: you can redistribute it and/or modify
5
+ it under the terms of the GNU Affero General Public License as
6
+ published by the Free Software Foundation, either version 3 of the
7
+ License, or (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU Affero General Public License for more details.
13
+
14
+ You should have received a copy of the GNU Affero General Public License
15
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ Some parts are based on an older federation gem from Florian Staudacher:
18
+ https://github.com/Raven24/diaspora-federation
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # diaspora* federation rails engine
2
+
3
+ #### A rails engine that adds the diaspora* federation protocol to a rails app
4
+
5
+ [![Build Status](https://travis-ci.org/SuperTux88/diaspora_federation.svg?branch=master)](https://travis-ci.org/SuperTux88/diaspora_federation)
6
+ [![Code Climate](https://codeclimate.com/github/SuperTux88/diaspora_federation/badges/gpa.svg)](https://codeclimate.com/github/SuperTux88/diaspora_federation)
7
+ [![Test Coverage](https://codeclimate.com/github/SuperTux88/diaspora_federation/badges/coverage.svg)](https://codeclimate.com/github/SuperTux88/diaspora_federation/coverage)
8
+ [![Dependency Status](https://gemnasium.com/SuperTux88/diaspora_federation.svg)](https://gemnasium.com/SuperTux88/diaspora_federation)
9
+ [![Inline docs](https://inch-ci.org/github/SuperTux88/diaspora_federation.svg?branch=master)](https://inch-ci.org/github/SuperTux88/diaspora_federation)
10
+
11
+ [Documentation](http://www.rubydoc.info/github/SuperTux88/diaspora_federation/master) |
12
+ [Project site](https://diasporafoundation.org) |
13
+ [Wiki](https://wiki.diasporafoundation.org) |
14
+ [Bugtracker](https://github.com/SuperTux88/diaspora_federation/issues)
15
+
16
+ ## License
17
+
18
+ This gem is published under the terms of the "GNU Affero General Public License". See the LICENSE file for the exact wording.
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ begin
2
+ require "bundler/setup"
3
+ rescue LoadError
4
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
+ end
6
+
7
+ require "rdoc/task"
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = "rdoc"
11
+ rdoc.title = "DiasporaFederation"
12
+ rdoc.options << "--line-numbers"
13
+ rdoc.rdoc_files.include("lib/**/*.rb")
14
+ end
15
+
16
+ APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
17
+ load "rails/tasks/engine.rake"
18
+
19
+ load "rails/tasks/statistics.rake"
20
+
21
+ Bundler::GemHelper.install_tasks
22
+
23
+ Rails.application.load_tasks
24
+
25
+ task test: %w(spec:prepare spec)
26
+ task default: :test
@@ -0,0 +1,6 @@
1
+ module DiasporaFederation
2
+ ##
3
+ # Base-Controller for all DiasporaFederation-Controller
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ require_dependency "diaspora_federation/application_controller"
2
+
3
+ module DiasporaFederation
4
+ ##
5
+ # this controller generates the hcard
6
+ class HCardController < ApplicationController
7
+ ##
8
+ # returns the hcard of the user
9
+ #
10
+ # GET /hcard/users/:guid
11
+ def hcard
12
+ person = DiasporaFederation.person_class.find_local_by_guid(params[:guid])
13
+
14
+ return render nothing: true, status: 404 if person.nil?
15
+
16
+ logger.info "hcard profile request for: #{person.diaspora_handle}"
17
+ render html: WebFinger::HCard.from_profile(person.hcard_profile_hash).to_html.html_safe
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ require_dependency "diaspora_federation/application_controller"
2
+
3
+ module DiasporaFederation
4
+ ##
5
+ # this controller processes receiving messages
6
+ class ReceiveController < ApplicationController
7
+ before_action :check_for_xml
8
+
9
+ ##
10
+ # receives public messages
11
+ #
12
+ # POST /receive/public
13
+ def public
14
+ logger.info "received a public message"
15
+ logger.debug CGI.unescape(params[:xml])
16
+ render nothing: true, status: :ok
17
+ end
18
+
19
+ ##
20
+ # receives private messages for a user
21
+ #
22
+ # POST /receive/users/:guid
23
+ def private
24
+ logger.info "received a private message for #{params[:guid]}"
25
+ logger.debug CGI.unescape(params[:xml])
26
+ render nothing: true, status: :ok
27
+ end
28
+
29
+ private
30
+
31
+ def check_for_xml
32
+ render nothing: true, status: 422 if params[:xml].nil?
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,60 @@
1
+ require_dependency "diaspora_federation/application_controller"
2
+
3
+ module DiasporaFederation
4
+ ##
5
+ # this controller handles all webfinger-specific requests
6
+ class WebfingerController < ApplicationController
7
+ ##
8
+ # returns the host-meta xml
9
+ #
10
+ # example:
11
+ # <?xml version="1.0" encoding="UTF-8"?>
12
+ # <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
13
+ # <Link rel="lrdd" type="application/xrd+xml" template="https://server.example/webfinger?q={uri}"/>
14
+ # </XRD>
15
+ #
16
+ # GET /.well-known/host-meta
17
+ def host_meta
18
+ render body: WebfingerController.host_meta_xml, content_type: "application/xrd+xml"
19
+ end
20
+
21
+ ##
22
+ # @deprecated this is the pre RFC 7033 webfinger
23
+ #
24
+ # example:
25
+ # <?xml version="1.0" encoding="UTF-8"?>
26
+ # <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
27
+ # <Subject>acct:alice@localhost:3000</Subject>
28
+ # <Alias>http://localhost:3000/people/c8e87290f6a20132963908fbffceb188</Alias>
29
+ # <Link rel="http://microformats.org/profile/hcard" type="text/html" href="http://localhost:3000/hcard/users/c8e87290f6a20132963908fbffceb188"/>
30
+ # <Link rel="http://joindiaspora.com/seed_location" type="text/html" href="http://localhost:3000/"/>
31
+ # <Link rel="http://joindiaspora.com/guid" type="text/html" href="c8e87290f6a20132963908fbffceb188"/>
32
+ # <Link rel="http://webfinger.net/rel/profile-page" type="text/html" href="http://localhost:3000/u/alice"/>
33
+ # <Link rel="http://schemas.google.com/g/2010#updates-from" type="application/atom+xml" href="http://localhost:3000/public/alice.atom"/>
34
+ # <Link rel="salmon" href="http://localhost:3000/receive/users/c8e87290f6a20132963908fbffceb188"/>
35
+ # <Link rel="diaspora-public-key" type="RSA" href="LS0tLS1CRU......"/>
36
+ # </XRD>
37
+ # GET /webfinger?q=<uri>
38
+ def legacy_webfinger
39
+ person = find_person(params[:q]) if params[:q]
40
+
41
+ return render nothing: true, status: 404 if person.nil?
42
+
43
+ logger.info "webfinger profile request for: #{person.diaspora_handle}"
44
+ render body: WebFinger::WebFinger.from_person(person.webfinger_hash).to_xml, content_type: "application/xrd+xml"
45
+ end
46
+
47
+ private
48
+
49
+ ##
50
+ # creates the host-meta xml with the configured server_uri and caches it
51
+ # @return [String] XML string
52
+ def self.host_meta_xml
53
+ @host_meta_xml ||= WebFinger::HostMeta.from_base_url(DiasporaFederation.server_uri.to_s).to_xml
54
+ end
55
+
56
+ def find_person(query)
57
+ DiasporaFederation.person_class.find_local_by_diaspora_handle(query.strip.downcase.gsub("acct:", ""))
58
+ end
59
+ end
60
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,15 @@
1
+ DiasporaFederation::Engine.routes.draw do
2
+ controller :receive do
3
+ post "receive-new/public" => :public, :as => "receive_public"
4
+ post "receive-new/users/:guid" => :private, :as => "receive_private"
5
+ end
6
+
7
+ controller :webfinger do
8
+ get ".well-known/host-meta" => :host_meta, :as => "host_meta"
9
+ get "webfinger" => :legacy_webfinger, :as => "legacy_webfinger"
10
+ end
11
+
12
+ controller :h_card do
13
+ get "hcard/users/:guid" => :hcard, :as => "hcard"
14
+ end
15
+ end
@@ -0,0 +1,120 @@
1
+ require "diaspora_federation/engine"
2
+ require "diaspora_federation/logging"
3
+
4
+ require "diaspora_federation/web_finger"
5
+
6
+ ##
7
+ # diaspora* federation rails engine
8
+ module DiasporaFederation
9
+ extend Logging
10
+
11
+ class << self
12
+ ##
13
+ # the pod url
14
+ #
15
+ # Example:
16
+ # config.server_uri = URI("http://localhost:3000/")
17
+ # or
18
+ # config.server_uri = AppConfig.pod_uri
19
+ attr_accessor :server_uri
20
+
21
+ ##
22
+ # the class to use as +Person+
23
+ #
24
+ # Example:
25
+ # config.person_class = Person.to_s
26
+ #
27
+ # This class must have the following methods:
28
+ #
29
+ # *find_local_by_diaspora_handle*
30
+ # This should return a +Person+, which is on this pod and the account is not closed.
31
+ #
32
+ # *find_local_by_guid*
33
+ # This should return a +Person+, which is on this pod and the account is not closed.
34
+ #
35
+ # *webfinger_hash*
36
+ # This should return a +Hash+ with the following information:
37
+ # {
38
+ # acct_uri: "acct:user@server.example",
39
+ # alias_url: "https://server.example/people/0123456789abcdef",
40
+ # hcard_url: "https://server.example/hcard/users/0123456789abcdef",
41
+ # seed_url: "https://server.example/",
42
+ # profile_url: "https://server.example/u/user",
43
+ # atom_url: "https://server.example/public/user.atom",
44
+ # salmon_url: "https://server.example/receive/users/0123456789abcdef",
45
+ # guid: "0123456789abcdef",
46
+ # pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----"
47
+ # }
48
+ #
49
+ # *hcard_profile_hash*
50
+ # This should return a +Hash+ with the following information:
51
+ # {
52
+ # guid: "0123456789abcdef",
53
+ # nickname: "user",
54
+ # full_name: "User Name",
55
+ # url: "https://server.example/",
56
+ # photo_large_url: "https://server.example/uploads/f.jpg",
57
+ # photo_medium_url: "https://server.example/uploads/m.jpg",
58
+ # photo_small_url: "https://server.example/uploads/s.jpg",
59
+ # pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----",
60
+ # searchable: true,
61
+ # first_name: "User",
62
+ # last_name: "Name"
63
+ # }
64
+ attr_accessor :person_class
65
+ def person_class
66
+ const_get(@person_class)
67
+ end
68
+
69
+ ##
70
+ # configure the federation engine
71
+ #
72
+ # DiasporaFederation.configure do |config|
73
+ # config.server_uri = "http://localhost:3000/"
74
+ # end
75
+ def configure
76
+ yield self
77
+ end
78
+
79
+ ##
80
+ # validates if the engine is configured correctly
81
+ #
82
+ # called from after_initialize
83
+ # @raise [ConfigurationError] if the configuration is incomplete or invalid
84
+ def validate_config
85
+ configuration_error "missing server_uri" unless @server_uri.respond_to? :host
86
+ validate_class(@person_class, "person_class", %i(
87
+ find_local_by_diaspora_handle
88
+ find_local_by_guid
89
+ webfinger_hash
90
+ hcard_profile_hash
91
+ ))
92
+ logger.info "successfully configured the federation engine"
93
+ end
94
+
95
+ private
96
+
97
+ def validate_class(klass, name, methods)
98
+ configuration_error "missing #{name}" unless klass
99
+ entity = const_get(klass)
100
+
101
+ return logger.warn "table for #{entity} doesn't exist, skip validation" unless entity.table_exists?
102
+
103
+ methods.each {|method| entity_respond_to?(entity, name, method) }
104
+ end
105
+
106
+ def entity_respond_to?(entity, name, method)
107
+ valid = entity.respond_to?(method) || entity.column_names.include?(method.to_s) || entity.method_defined?(method)
108
+ configuration_error "the configured class #{entity} for #{name} doesn't respond to #{method}" unless valid
109
+ end
110
+
111
+ def configuration_error(message)
112
+ logger.fatal("diaspora federation configuration error: #{message}")
113
+ raise ConfigurationError, message
114
+ end
115
+ end
116
+
117
+ # raised, if the engine is not configured correctly
118
+ class ConfigurationError < RuntimeError
119
+ end
120
+ end
@@ -0,0 +1,15 @@
1
+ module DiasporaFederation
2
+ ##
3
+ # diaspora* federation rails engine
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace DiasporaFederation
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ config.after_initialize do
12
+ DiasporaFederation.validate_config
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module DiasporaFederation
2
+ ##
3
+ # logging module for the diaspora federation engine
4
+ #
5
+ # it uses the logging-gem if available
6
+ module Logging
7
+ private
8
+
9
+ ##
10
+ # get the logger for this class
11
+ #
12
+ # use the logging-gem if available, else use a default logger
13
+ def logger
14
+ @logger ||= begin
15
+ # use logging-gem if available
16
+ return ::Logging::Logger[self] if Object.const_defined?("::Logging::Logger")
17
+
18
+ # fallback logger
19
+ @logger = Logger.new(STDOUT)
20
+ @logger.level = Logger.const_get(Rails.configuration.log_level.to_s.upcase)
21
+ @logger
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ module DiasporaFederation
2
+ ##
3
+ # the gem version
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,14 @@
1
+ module DiasporaFederation
2
+ ##
3
+ # This module provides the namespace for the various classes implementing
4
+ # WebFinger and other protocols used for metadata discovery on remote servers
5
+ # in the Diaspora* network.
6
+ module WebFinger
7
+ end
8
+ end
9
+
10
+ require "diaspora_federation/web_finger/exceptions"
11
+ require "diaspora_federation/web_finger/xrd_document"
12
+ require "diaspora_federation/web_finger/host_meta"
13
+ require "diaspora_federation/web_finger/web_finger"
14
+ require "diaspora_federation/web_finger/h_card"
@@ -0,0 +1,19 @@
1
+ module DiasporaFederation
2
+ module WebFinger
3
+ ##
4
+ # Raised, if the XML structure is invalid
5
+ class InvalidDocument < RuntimeError
6
+ end
7
+
8
+ ##
9
+ # Raised, if something is wrong with the webfinger data
10
+ #
11
+ # * if the +webfinger_url+ is missing or malformed in {HostMeta.from_base_url} or {HostMeta.from_xml}
12
+ # * if the +data+ given to {WebFinger.from_person} is an invalid type or doesn't contain all required entries
13
+ # * if the parsed XML from {WebFinger.from_xml} is incomplete
14
+ # * if the params passed to {HCard.from_profile} or {HCard.from_html}
15
+ # are in some way malformed, invalid or incomplete.
16
+ class InvalidData < RuntimeError
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,319 @@
1
+ module DiasporaFederation
2
+ module WebFinger
3
+ ##
4
+ # This class provides the means of generating an parsing account data to and
5
+ # from the hCard format.
6
+ # hCard is based on +RFC 2426+ (vCard) which got superseded by +RFC 6350+.
7
+ # There is a draft for a new h-card format specification, that makes use of
8
+ # the new vCard standard.
9
+ #
10
+ # @note The current implementation contains a huge amount of legacy elements
11
+ # and classes, that should be removed and cleaned up in later iterations.
12
+ #
13
+ # @todo This needs some radical restructuring. The generated HTML is not
14
+ # correctly nested according to the hCard standard and class names are
15
+ # partially wrong. Also, apart from that, it's just ugly.
16
+ #
17
+ # @example Creating a hCard document from account data
18
+ # hc = HCard.from_profile({
19
+ # guid: "0123456789abcdef",
20
+ # nickname: "user",
21
+ # full_name: "User Name",
22
+ # url: "https://server.example/",
23
+ # photo_large_url: "https://server.example/uploads/l.jpg",
24
+ # photo_medium_url: "https://server.example/uploads/m.jpg",
25
+ # photo_small_url: "https://server.example/uploads/s.jpg",
26
+ # pubkey: "-----BEGIN PUBLIC KEY-----\nABCDEF==\n-----END PUBLIC KEY-----",
27
+ # searchable: true,
28
+ # first_name: "User",
29
+ # last_name: "Name"
30
+ # })
31
+ # html_string = hc.to_html
32
+ #
33
+ # @example Create a HCard instance from an hCard document
34
+ # hc = HCard.from_html(html_string)
35
+ # ...
36
+ # full_name = hc.full_name
37
+ # ...
38
+ #
39
+ # @see http://microformats.org/wiki/hCard "hCard 1.0"
40
+ # @see http://microformats.org/wiki/h-card "h-card" (draft)
41
+ # @see http://www.ietf.org/rfc/rfc2426.txt "vCard MIME Directory Profile" (obsolete)
42
+ # @see http://www.ietf.org/rfc/rfc6350.txt "vCard Format Specification"
43
+ class HCard
44
+ private_class_method :new
45
+
46
+ # This is just the guid. When a user creates an account on a pod, the pod
47
+ # MUST assign them a guid - a random hexadecimal string of at least 8
48
+ # hexadecimal digits.
49
+ # @return [String] guid
50
+ attr_reader :guid
51
+
52
+ # the first part of the diaspora handle
53
+ # @return [String] nickname
54
+ attr_reader :nickname
55
+
56
+ # @return [String] display name of the user
57
+ attr_reader :full_name
58
+
59
+ # @deprecated should be changed to the profile url. The pod url is in
60
+ # the WebFinger (see {WebFinger#seed_url}, will affect older Diaspora*
61
+ # installations).
62
+ #
63
+ # @return [String] link to the pod
64
+ attr_reader :url
65
+
66
+ # When a user is created on the pod, the pod MUST generate a pgp keypair
67
+ # for them. This key is used for signing messages. The format is a
68
+ # DER-encoded PKCS#1 key beginning with the text
69
+ # "-----BEGIN PUBLIC KEY-----" and ending with "-----END PUBLIC KEY-----".
70
+ # @return [String] public key
71
+ attr_reader :pubkey
72
+
73
+ # @return [String] url to the big avatar (300x300)
74
+ attr_reader :photo_large_url
75
+ # @return [String] url to the medium avatar (100x100)
76
+ attr_reader :photo_medium_url
77
+ # @return [String] url to the small avatar (50x50)
78
+ attr_reader :photo_small_url
79
+
80
+ # @deprecated We decided to only use one name field, these should be removed
81
+ # in later iterations (will affect older Diaspora* installations).
82
+ #
83
+ # @see #full_name
84
+ # @return [String] first name
85
+ attr_reader :first_name
86
+
87
+ # @deprecated We decided to only use one name field, these should be removed
88
+ # in later iterations (will affect older Diaspora* installations).
89
+ #
90
+ # @see #full_name
91
+ # @return [String] last name
92
+ attr_reader :last_name
93
+
94
+ # @deprecated As this is a simple property, consider move to WebFinger instead
95
+ # of HCard. vCard has no comparable field for this information, but
96
+ # Webfinger may declare arbitrary properties (will affect older Diaspora*
97
+ # installations).
98
+ #
99
+ # flag if a user is searchable by name
100
+ # @return [Boolean] searchable flag
101
+ attr_reader :searchable
102
+
103
+ # CSS selectors for finding all the hCard fields
104
+ SELECTORS = {
105
+ uid: ".uid",
106
+ nickname: ".nickname",
107
+ fn: ".fn",
108
+ given_name: ".given_name",
109
+ family_name: ".family_name",
110
+ url: "#pod_location[href]",
111
+ photo: ".entity_photo .photo[src]",
112
+ photo_medium: ".entity_photo_medium .photo[src]",
113
+ photo_small: ".entity_photo_small .photo[src]",
114
+ key: ".key",
115
+ searchable: ".searchable"
116
+ }
117
+
118
+ # Create the HTML string from the current HCard instance
119
+ # @return [String] HTML string
120
+ def to_html
121
+ builder = create_builder
122
+
123
+ content = builder.doc.at_css("#content_inner")
124
+
125
+ add_simple_property(content, :uid, "uid", @guid)
126
+ add_simple_property(content, :nickname, "nickname", @nickname)
127
+ add_simple_property(content, :full_name, "fn", @full_name)
128
+ add_simple_property(content, :searchable, "searchable", @searchable)
129
+
130
+ add_property(content, :key) do |html|
131
+ html.pre(@pubkey.to_s, class: "key")
132
+ end
133
+
134
+ # TODO: remove me! ###################
135
+ add_simple_property(content, :first_name, "given_name", @first_name)
136
+ add_simple_property(content, :family_name, "family_name", @last_name)
137
+ #######################################
138
+
139
+ add_property(content, :url) do |html|
140
+ html.a(@url.to_s, id: "pod_location", class: "url", rel: "me", href: @url.to_s)
141
+ end
142
+
143
+ add_photos(content)
144
+
145
+ builder.doc.to_xhtml(indent: 2, indent_text: " ")
146
+ end
147
+
148
+ # Creates a new HCard instance from the given Hash containing profile data
149
+ # @param [Hash] data account data
150
+ # @return [HCard] HCard instance
151
+ # @raise [InvalidData] if the account data Hash is invalid or incomplete
152
+ def self.from_profile(data)
153
+ raise InvalidData unless account_data_complete?(data)
154
+
155
+ hc = allocate
156
+ hc.instance_eval {
157
+ @guid = data[:guid]
158
+ @nickname = data[:nickname]
159
+ @full_name = data[:full_name]
160
+ @url = data[:url]
161
+ @photo_large_url = data[:photo_large_url]
162
+ @photo_medium_url = data[:photo_medium_url]
163
+ @photo_small_url = data[:photo_small_url]
164
+ @pubkey = data[:pubkey]
165
+ @searchable = data[:searchable]
166
+
167
+ # TODO: remove me! ###################
168
+ @first_name = data[:first_name]
169
+ @last_name = data[:last_name]
170
+ #######################################
171
+ }
172
+ hc
173
+ end
174
+
175
+ # Creates a new HCard instance from the given HTML string.
176
+ # @param html_string [String] HTML string
177
+ # @return [HCard] HCard instance
178
+ # @raise [InvalidData] if the HTML string is invalid or incomplete
179
+ def self.from_html(html_string)
180
+ doc = parse_html_and_validate(html_string)
181
+
182
+ hc = allocate
183
+ hc.instance_eval {
184
+ @guid = content_from_doc(doc, :uid)
185
+ @nickname = content_from_doc(doc, :nickname)
186
+ @full_name = content_from_doc(doc, :fn)
187
+ @url = element_from_doc(doc, :url)["href"]
188
+ @photo_large_url = photo_from_doc(doc, :photo)
189
+ @photo_medium_url = photo_from_doc(doc, :photo_medium)
190
+ @photo_small_url = photo_from_doc(doc, :photo_small)
191
+ @pubkey = content_from_doc(doc, :key) unless element_from_doc(doc, :key).nil?
192
+ @searchable = content_from_doc(doc, :searchable) == "true"
193
+
194
+ # TODO: change me! ###################
195
+ @first_name = content_from_doc(doc, :given_name)
196
+ @last_name = content_from_doc(doc, :family_name)
197
+ #######################################
198
+ }
199
+ hc
200
+ end
201
+
202
+ private
203
+
204
+ # Creates the base HCard html structure
205
+ # @return [Nokogiri::HTML::Builder] HTML Builder instance
206
+ def create_builder
207
+ Nokogiri::HTML::Builder.new do |html|
208
+ html.html {
209
+ html.head {
210
+ html.meta(charset: "UTF-8")
211
+ html.title(@full_name)
212
+ }
213
+
214
+ html.body {
215
+ html.div(id: "content") {
216
+ html.h1(@full_name)
217
+ html.div(id: "content_inner", class: "entity_profile vcard author") {
218
+ html.h2("User profile")
219
+ }
220
+ }
221
+ }
222
+ }
223
+ end
224
+ end
225
+
226
+ # Add a property to the hCard document. The element will be added to the given
227
+ # container element and a "definition list" structure will be created around
228
+ # it. A Nokogiri::HTML::Builder instance will be passed to the given block,
229
+ # which should be used to add the element(s) containing the property data.
230
+ #
231
+ # @param container [Nokogiri::XML::Element] parent element for added property HTML
232
+ # @param name [Symbol] property name
233
+ # @param block [Proc] block returning an element
234
+ def add_property(container, name, &block)
235
+ Nokogiri::HTML::Builder.with(container) do |html|
236
+ html.dl(class: "entity_#{name}") {
237
+ html.dt(name.to_s.capitalize)
238
+ html.dd {
239
+ block.call(html)
240
+ }
241
+ }
242
+ end
243
+ end
244
+
245
+ # Calls {HCard#add_property} for a simple text property.
246
+ # @param container [Nokogiri::XML::Element] parent element
247
+ # @param name [Symbol] property name
248
+ # @param class_name [String] HTML class name
249
+ # @param value [#to_s] property value
250
+ # @see HCard#add_property
251
+ def add_simple_property(container, name, class_name, value)
252
+ add_property(container, name) do |html|
253
+ html.span(value.to_s, class: class_name)
254
+ end
255
+ end
256
+
257
+ # Calls {HCard#add_property} to add the photos
258
+ # @param container [Nokogiri::XML::Element] parent element
259
+ # @see HCard#add_property
260
+ def add_photos(container)
261
+ add_property(container, :photo) do |html|
262
+ html.img(class: "photo avatar", width: "300", height: "300", src: @photo_large_url.to_s)
263
+ end
264
+
265
+ add_property(container, :photo_medium) do |html|
266
+ html.img(class: "photo avatar", width: "100", height: "100", src: @photo_medium_url.to_s)
267
+ end
268
+
269
+ add_property(container, :photo_small) do |html|
270
+ html.img(class: "photo avatar", width: "50", height: "50", src: @photo_small_url.to_s)
271
+ end
272
+ end
273
+
274
+ # Checks the given account data Hash for correct type and completeness.
275
+ # @param [Hash] data account data
276
+ # @return [Boolean] validation result
277
+ def self.account_data_complete?(data)
278
+ data.instance_of?(Hash) &&
279
+ %i(
280
+ guid nickname full_name url
281
+ photo_large_url photo_medium_url photo_small_url
282
+ pubkey searchable first_name last_name
283
+ ).all? {|k| data.key? k }
284
+ end
285
+ private_class_method :account_data_complete?
286
+
287
+ # Make sure some of the most important elements are present in the parsed
288
+ # HTML document.
289
+ # @param [LibXML::XML::Document] doc HTML document
290
+ # @return [Boolean] validation result
291
+ def self.html_document_complete?(doc)
292
+ !(doc.at_css(SELECTORS[:fn]).nil? || doc.at_css(SELECTORS[:nickname]).nil? ||
293
+ doc.at_css(SELECTORS[:url]).nil? || doc.at_css(SELECTORS[:photo]).nil?)
294
+ end
295
+ private_class_method :html_document_complete?
296
+
297
+ def self.parse_html_and_validate(html_string)
298
+ raise ArgumentError, "hcard html is not a string" unless html_string.instance_of?(String)
299
+
300
+ doc = Nokogiri::HTML::Document.parse(html_string)
301
+ raise InvalidData, "hcard html incomplete" unless html_document_complete?(doc)
302
+ doc
303
+ end
304
+ private_class_method :parse_html_and_validate
305
+
306
+ def element_from_doc(doc, selector)
307
+ doc.at_css(SELECTORS[selector])
308
+ end
309
+
310
+ def content_from_doc(doc, content_selector)
311
+ element_from_doc(doc, content_selector).content
312
+ end
313
+
314
+ def photo_from_doc(doc, photo_selector)
315
+ element_from_doc(doc, photo_selector)["src"]
316
+ end
317
+ end
318
+ end
319
+ end