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 +7 -0
- data/LICENSE +18 -0
- data/README.md +18 -0
- data/Rakefile +26 -0
- data/app/controllers/diaspora_federation/application_controller.rb +6 -0
- data/app/controllers/diaspora_federation/h_card_controller.rb +20 -0
- data/app/controllers/diaspora_federation/receive_controller.rb +35 -0
- data/app/controllers/diaspora_federation/webfinger_controller.rb +60 -0
- data/config/routes.rb +15 -0
- data/lib/diaspora_federation.rb +120 -0
- data/lib/diaspora_federation/engine.rb +15 -0
- data/lib/diaspora_federation/logging.rb +25 -0
- data/lib/diaspora_federation/version.rb +5 -0
- data/lib/diaspora_federation/web_finger.rb +14 -0
- data/lib/diaspora_federation/web_finger/exceptions.rb +19 -0
- data/lib/diaspora_federation/web_finger/h_card.rb +319 -0
- data/lib/diaspora_federation/web_finger/host_meta.rb +100 -0
- data/lib/diaspora_federation/web_finger/web_finger.rb +263 -0
- data/lib/diaspora_federation/web_finger/xrd_document.rb +181 -0
- data/lib/tasks/diaspora_federation_tasks.rake +4 -0
- data/lib/tasks/tests.rake +18 -0
- metadata +94 -0
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
|
+
[](https://travis-ci.org/SuperTux88/diaspora_federation)
|
6
|
+
[](https://codeclimate.com/github/SuperTux88/diaspora_federation)
|
7
|
+
[](https://codeclimate.com/github/SuperTux88/diaspora_federation/coverage)
|
8
|
+
[](https://gemnasium.com/SuperTux88/diaspora_federation)
|
9
|
+
[](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,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,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
|