dropbox-invite 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 +15 -0
- data/.gitignore +34 -0
- data/.rspec +3 -0
- data/Gemfile +2 -0
- data/LICENSE +675 -0
- data/README.md +106 -0
- data/Rakefile +11 -0
- data/dropbox-invite.gemspec +34 -0
- data/lib/dropbox/api/objects/dir.rb +38 -0
- data/lib/dropbox/api/util/config.rb +21 -0
- data/lib/dropbox/invite.rb +1 -0
- data/lib/dropbox/web_client.rb +12 -0
- data/lib/dropbox/web_client/actions.rb +49 -0
- data/lib/dropbox/web_client/authentication.rb +71 -0
- data/lib/dropbox/web_client/cookie_manager.rb +40 -0
- data/lib/dropbox/web_client/paths.rb +35 -0
- data/lib/dropbox/web_client/response_parser.rb +88 -0
- data/lib/dropbox/web_client/session.rb +27 -0
- data/lib/dropbox/web_client/version.rb +5 -0
- data/spec/actions_spec.rb +62 -0
- data/spec/authentication_spec.rb +13 -0
- data/spec/dropbox_api_spec.rb +59 -0
- data/spec/initialization_spec.rb +22 -0
- data/spec/spec_helper.rb +40 -0
- data/spec/support/vcr.rb +21 -0
- data/spec/vcr/dropbox_api/members_through_api.yml +1003 -0
- data/spec/vcr/dropbox_webclient_actions/invite_existing_folder.yml +1140 -0
- data/spec/vcr/dropbox_webclient_actions/invite_shared_folder.yml +1499 -0
- data/spec/vcr/dropbox_webclient_actions/invite_unexisting_folder.yml +815 -0
- data/spec/vcr/dropbox_webclient_actions/share_opts_existing.yml +736 -0
- data/spec/vcr/dropbox_webclient_actions/share_opts_shared.yml +849 -0
- data/spec/vcr/dropbox_webclient_actions/share_opts_unexisting.yml +735 -0
- data/spec/vcr/dropbox_webclient_authentication/login.yml +598 -0
- metadata +217 -0
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# dropbox-invite
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/Jesus/dropbox-invite)
|
4
|
+
|
5
|
+
## Introduction
|
6
|
+
|
7
|
+
This gem allows you to invite other users to a shared folder in Dropbox. It's
|
8
|
+
known that Dropbox API doesn't allow this operation, however it's a requirement
|
9
|
+
in some scenarios.
|
10
|
+
|
11
|
+
To achieve this functionality and due to the lack of implementation in the
|
12
|
+
official API, the library relies on
|
13
|
+
[rest-client](https://github.com/rest-client/rest-client) and
|
14
|
+
[nokogiri](http://www.nokogiri.org) to perform the action through the web
|
15
|
+
interface.
|
16
|
+
|
17
|
+
### Scope
|
18
|
+
|
19
|
+
This gem is meant to provide a missing functionality, not to build another
|
20
|
+
API implementation. Ideally you'd use this library in combination with
|
21
|
+
some actual implementation of the Dropbox API.
|
22
|
+
|
23
|
+
I recommend [dropbox-api](https://github.com/futuresimple/dropbox-api) just
|
24
|
+
because I've included integration for it. If `Dropbox::API` is found
|
25
|
+
the existing classes will be extended to allow you invite people to your
|
26
|
+
folders. This is shown in the examples below.
|
27
|
+
|
28
|
+
### Disclaimer
|
29
|
+
|
30
|
+
This gem depends 100% on the parsing of the HTML from Dropbox web pages,
|
31
|
+
therefore a change in their layouts might result in a broken library. Please,
|
32
|
+
keep this in mind if you're planning to use this in a production environment.
|
33
|
+
|
34
|
+
Another drawback of using the web interface is of course the speed.
|
35
|
+
|
36
|
+
## How to use
|
37
|
+
|
38
|
+
### Using it with `dropbox-api` (recommended)
|
39
|
+
|
40
|
+
First, you'll need to set up `dropbox-api` as explained in the [gem's README]
|
41
|
+
(https://github.com/futuresimple/dropbox-api):
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
Dropbox::API::Config.app_key = YOUR_APP_KEY
|
45
|
+
Dropbox::API::Config.app_secret = YOUR_APP_SECRET
|
46
|
+
Dropbox::API::Config.mode = "dropbox" # This is a requirement
|
47
|
+
```
|
48
|
+
|
49
|
+
At this point you're able to instantiate a `Dropbox::API::Client` object either
|
50
|
+
through web-based authorization or rake-based. So far, nothing new.
|
51
|
+
|
52
|
+
Additionally you'll need to set up the web login credentials as part of the
|
53
|
+
API settings to enable the initialization of a web client when it's required.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
Dropbox::API::Config.web_session = Dropbox::WebClient::Session.new(
|
57
|
+
:email => "example@corkeryfisher.info",
|
58
|
+
:password => "yourPassw0rd"
|
59
|
+
)
|
60
|
+
```
|
61
|
+
|
62
|
+
Note that the web authentication won't happen until you actually need it, i.e.
|
63
|
+
when the `invite` method is invoked.
|
64
|
+
|
65
|
+
Now, assuming that you've got a `Dropbox::API::Dir` object called `some_dir`,
|
66
|
+
you'd be able to perform this:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
response = some_dir.invite("kirsten.greenholt@corkeryfisher.info")
|
70
|
+
# => #<Dropbox::WebClient::ResponseParser ... >
|
71
|
+
response.error?
|
72
|
+
# => false
|
73
|
+
response.response_data
|
74
|
+
# => {"success_msg"=>"Created shared folder 'folder x'", "sf_info"=>{"mount_point"=>"/folder x", "user_id"=>372486289, "extra_count"=>0, "sort_rank"=>nil, "encoded_sort_key"=>["NkhCMjROBloBDAEMAA=="], "other_emails"=>[], "other_names"=>[], "modified_pretty"=>"just now", "href"=>"/home/folder%20x", "modified_ts"=>1420051083, "filename"=>"folder x", "target_ns_id"=>791334450, "icon"=>"folder_user"}}
|
75
|
+
```
|
76
|
+
|
77
|
+
Additionally you can check who's included in the dir through `members`:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
some_dir.members
|
81
|
+
# => ["example@corkeryfisher.info", "kirsten.greenholt@corkeryfisher.info"]
|
82
|
+
```
|
83
|
+
|
84
|
+
### Using it on its own (standalone, not recommended)
|
85
|
+
```ruby
|
86
|
+
session = Dropbox::WebClient::Session.new(
|
87
|
+
:email => "your@account.com",
|
88
|
+
:password => "yourPassw0rd"
|
89
|
+
)
|
90
|
+
session.invite("/folder path", ["kirsten.greenholt@corkeryfisher.info"])
|
91
|
+
# => #<Dropbox::WebClient::ResponseParser ... >
|
92
|
+
```
|
93
|
+
|
94
|
+
## To do
|
95
|
+
|
96
|
+
Would be nice to:
|
97
|
+
|
98
|
+
- [ ] Record tests in HTTP instead of HTTPS so they'd be readable. To do so
|
99
|
+
we'll need a proxy.
|
100
|
+
- [ ] Implement other functions (share permissions, etc).
|
101
|
+
- [ ] Improve error handling.
|
102
|
+
- [ ] Add setting to disable lazy authentication.
|
103
|
+
|
104
|
+
## Problems?
|
105
|
+
|
106
|
+
Please report them in [issues](https://github.com/Jesus/dropbox-invite/issues)
|
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
# Default directory to look in is `/specs`
|
5
|
+
# Run with `rake spec`
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |task|
|
7
|
+
task.rspec_opts = ['--color', '--format documentation', '--require spec_helper']
|
8
|
+
end
|
9
|
+
|
10
|
+
task :default => :spec
|
11
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "dropbox/web_client/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "dropbox-invite"
|
7
|
+
s.version = Dropbox::WebClient::VERSION
|
8
|
+
s.authors = ["Jesus Burgos Macia"]
|
9
|
+
s.email = ["jburmac@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/Jesus/dropbox-invite"
|
11
|
+
s.summary = "Invite people to shared folders in Dropbox"
|
12
|
+
s.description = "It's capable of working as an extension for dropbox-api to allow folder invites"
|
13
|
+
s.license = "GPL-3.0"
|
14
|
+
|
15
|
+
s.rubyforge_project = "dropbox-invite"
|
16
|
+
|
17
|
+
s.add_dependency 'rest-client', "~> 1.6"
|
18
|
+
s.add_dependency 'nokogiri', "~> 1.6"
|
19
|
+
|
20
|
+
s.add_development_dependency 'dropbox-api', "~> 0.4"
|
21
|
+
|
22
|
+
s.add_development_dependency "bundler", "~> 1.6"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
|
25
|
+
s.add_development_dependency 'rspec', "~> 3.1"
|
26
|
+
s.add_development_dependency 'vcr', "~> 2.9"
|
27
|
+
s.add_development_dependency 'webmock', "~> 1.17"
|
28
|
+
s.add_development_dependency 'debugger', "~> 1.6"
|
29
|
+
|
30
|
+
s.files = `git ls-files -z`.split("\x0")
|
31
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
32
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Dropbox::API::Dir
|
2
|
+
|
3
|
+
# Returns an array of people who have been invited to this shared folder
|
4
|
+
def members
|
5
|
+
@members ||= if is_shared_folder?
|
6
|
+
Dropbox::API::Config.web_session.
|
7
|
+
share_options(self[:path]).
|
8
|
+
response_data[:members].
|
9
|
+
map{|member_info| member_info[:email]}
|
10
|
+
else
|
11
|
+
[]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_shared_folder?
|
16
|
+
@is_shared_folder ||= self.icon == "folder_user" # Not very reliable...
|
17
|
+
end
|
18
|
+
|
19
|
+
def is_shared_folder=(value)
|
20
|
+
@is_shared_folder = value
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sends an invitation to email
|
24
|
+
def invite(emails, message = "")
|
25
|
+
emails = [emails] if emails.is_a? String
|
26
|
+
|
27
|
+
response = Dropbox::API::Config.web_session.invite(self[:path], emails, message, is_shared_folder?)
|
28
|
+
|
29
|
+
# This is now a shared folder
|
30
|
+
@is_shared_folder = true
|
31
|
+
|
32
|
+
# Clear out cache of members so it'll be fetched again when asked
|
33
|
+
@members = nil
|
34
|
+
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dropbox
|
2
|
+
module API
|
3
|
+
|
4
|
+
module Config
|
5
|
+
class << self
|
6
|
+
def web_session=(session)
|
7
|
+
@web_session = session
|
8
|
+
end
|
9
|
+
|
10
|
+
def web_session
|
11
|
+
if @web_session.nil?
|
12
|
+
raise Dropbox::API::Error::Config.new("Web session hasn't been configured")
|
13
|
+
else
|
14
|
+
@web_session
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'dropbox/web_client'
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'dropbox/web_client/response_parser'
|
2
|
+
|
3
|
+
require 'dropbox/web_client/paths'
|
4
|
+
require 'dropbox/web_client/authentication'
|
5
|
+
require 'dropbox/web_client/cookie_manager'
|
6
|
+
require 'dropbox/web_client/actions'
|
7
|
+
require 'dropbox/web_client/session'
|
8
|
+
|
9
|
+
if defined? Dropbox::API
|
10
|
+
require 'dropbox/api/util/config'
|
11
|
+
require 'dropbox/api/objects/dir'
|
12
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Dropbox
|
2
|
+
module WebClient
|
3
|
+
|
4
|
+
module Actions
|
5
|
+
def invite(path, emails, message = "", is_shared_folder = nil)
|
6
|
+
ensure_authenticated
|
7
|
+
|
8
|
+
if is_shared_folder.nil?
|
9
|
+
# Determine if the folder exists
|
10
|
+
share_options_response = share_options(path)
|
11
|
+
|
12
|
+
is_shared_folder = !share_options_response.error?
|
13
|
+
end
|
14
|
+
|
15
|
+
params = {
|
16
|
+
"emails" => emails.join(","),
|
17
|
+
"custom_message" => message,
|
18
|
+
"t" => cookies.login_token,
|
19
|
+
"_subject_uid" => subject_uid
|
20
|
+
}
|
21
|
+
if is_shared_folder
|
22
|
+
url = invite_more_url
|
23
|
+
params["ns_id"] = share_options(path).response_data[:ns_id]
|
24
|
+
else
|
25
|
+
url = invite_url
|
26
|
+
params["path"] = path
|
27
|
+
end
|
28
|
+
|
29
|
+
response = RestClient.post(url, params, {:cookies => cookies.all})
|
30
|
+
response_text = response.body
|
31
|
+
|
32
|
+
return ResponseParser.new(response_text)
|
33
|
+
end
|
34
|
+
|
35
|
+
def share_options(path)
|
36
|
+
ensure_authenticated
|
37
|
+
url = share_options_url(:path => path)
|
38
|
+
|
39
|
+
response = RestClient.post(url, {
|
40
|
+
"t" => cookies.login_token,
|
41
|
+
"_subject_uid" => subject_uid
|
42
|
+
}, {:cookies => cookies.for_share_options})
|
43
|
+
|
44
|
+
return ResponseParser.new(response.body, :share_options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Dropbox
|
2
|
+
module WebClient
|
3
|
+
|
4
|
+
module Authentication
|
5
|
+
|
6
|
+
def authenticated?
|
7
|
+
@authenticated
|
8
|
+
end
|
9
|
+
|
10
|
+
def authenticate
|
11
|
+
get_login
|
12
|
+
post_login
|
13
|
+
|
14
|
+
@authenticated = true
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: Should keep in consideration the session expiration...
|
18
|
+
def ensure_authenticated
|
19
|
+
authenticate unless authenticated?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# Gets the login URL and keeps the cookies, required to continue the
|
26
|
+
# authentication process
|
27
|
+
def get_login
|
28
|
+
response = RestClient.get(login_url)
|
29
|
+
cookies.take response.cookies
|
30
|
+
response
|
31
|
+
end
|
32
|
+
|
33
|
+
# Posts user credentials and keeps the returned cookies, this'll start a
|
34
|
+
# user session.
|
35
|
+
def post_login
|
36
|
+
response = RestClient.post(post_login_url, {
|
37
|
+
"t" => cookies.login_token,
|
38
|
+
"login_email" => @email,
|
39
|
+
"login_password" => @password
|
40
|
+
}, {:cookies => cookies.all}) do |response, request, result, &block|
|
41
|
+
if response.code == 200
|
42
|
+
cookies.take response.cookies
|
43
|
+
parsed_response = ResponseParser.new(response.body)
|
44
|
+
@subject_uid = parsed_response.response_data["id"]
|
45
|
+
else
|
46
|
+
response.return!(request, result, &block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
response
|
50
|
+
end
|
51
|
+
|
52
|
+
# This step might not be required, it's just following the redirect which
|
53
|
+
# we got in the previous authentication step.
|
54
|
+
def get_after_login_url
|
55
|
+
response = RestClient.get(after_login_url, :cookies => current_cookies)
|
56
|
+
|
57
|
+
# Get _subject_uid (hidden field in response HTML)
|
58
|
+
parsed_response = ResponseParser.new(response.body, :html, :html)
|
59
|
+
@subject_uid = parsed_response.response_data[:subject_uid]
|
60
|
+
|
61
|
+
return response
|
62
|
+
end
|
63
|
+
|
64
|
+
def subject_uid
|
65
|
+
@subject_uid
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Dropbox
|
2
|
+
module WebClient
|
3
|
+
|
4
|
+
# Cookie management, this is what will preserve our session
|
5
|
+
class CookieManager
|
6
|
+
def initialize
|
7
|
+
clear_cookies
|
8
|
+
end
|
9
|
+
|
10
|
+
def take(cookies)
|
11
|
+
@cookies ||= {}
|
12
|
+
@cookies.merge! cookies
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns a hash with all collected cookies.
|
16
|
+
def all
|
17
|
+
@cookies
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a hash with cookies relevant for the `share_options` action.
|
21
|
+
def for_share_options
|
22
|
+
valid_keys = ["bjar", "blid", "forumjar", "forumlid", "gvc", "jar",
|
23
|
+
"l", "lid", "locale", "puc", "t"]
|
24
|
+
all.select do |k, v|
|
25
|
+
valid_keys.include? k
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def clear_cookies
|
30
|
+
@cookies = {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def login_token
|
34
|
+
@cookies["t"]
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Dropbox
|
2
|
+
module WebClient
|
3
|
+
|
4
|
+
module Paths
|
5
|
+
@@endpoint = "https://www.dropbox.com"
|
6
|
+
@@paths = {
|
7
|
+
:login => "/login",
|
8
|
+
:post_login => "/ajax_login",
|
9
|
+
:invite => "/share_ajax/existing",
|
10
|
+
:invite_more => "/share_ajax/invite_more",
|
11
|
+
:share_options => "/share_options/:path"
|
12
|
+
}
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def url_from_path(path, *arguments)
|
17
|
+
_path = path.dup
|
18
|
+
options = arguments.last.is_a?(Hash) ? arguments.pop : {}
|
19
|
+
options.each { |key, value| _path.gsub!(":#{key}", value) }
|
20
|
+
_path.gsub! "//", "/"
|
21
|
+
|
22
|
+
URI.encode(File.join(@@endpoint, _path))
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method_sym, *arguments, &block)
|
26
|
+
if (method_sym.to_s =~ /^(.*)_url$/) == 0 and @@paths.keys.include?(path = $1.to_sym)
|
27
|
+
return url_from_path(@@paths[path], *arguments)
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Dropbox
|
2
|
+
module WebClient
|
3
|
+
|
4
|
+
class ResponseParser
|
5
|
+
attr_reader :response_text
|
6
|
+
|
7
|
+
def initialize(response_text, response_type = :json, error_type = :json)
|
8
|
+
@response_text = response_text
|
9
|
+
|
10
|
+
@error_type = error_type
|
11
|
+
@response_type = response_type
|
12
|
+
end
|
13
|
+
|
14
|
+
def error?
|
15
|
+
@response_text.start_with? "err:"
|
16
|
+
end
|
17
|
+
|
18
|
+
def error_data
|
19
|
+
JSON.parse(@response_text.split(":", 2)[1]) if error?
|
20
|
+
end
|
21
|
+
|
22
|
+
def error_data
|
23
|
+
return unless error?
|
24
|
+
|
25
|
+
error_text = @response_text.split(":", 2)[1]
|
26
|
+
|
27
|
+
self.class.send("parse_#{@error_type.to_s}", error_text)
|
28
|
+
end
|
29
|
+
|
30
|
+
def response_data
|
31
|
+
if error?
|
32
|
+
Exception.new(error_data)
|
33
|
+
else
|
34
|
+
begin
|
35
|
+
self.class.send("parse_#{@response_type.to_s}", @response_text)
|
36
|
+
rescue NoMethodError
|
37
|
+
raise Exception.new("Unsupported response format")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def self.parse_json(text)
|
45
|
+
JSON.parse(text)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.parse_text(text)
|
49
|
+
text
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.parse_html(text)
|
53
|
+
doc = Nokogiri::HTML(text)
|
54
|
+
result = {}
|
55
|
+
|
56
|
+
members = doc.css("#sf-members .bs-row")
|
57
|
+
if members.size > 0
|
58
|
+
result[:members] = members.map do |member|
|
59
|
+
{
|
60
|
+
:email => member.css("a[href^=mailto]").text,
|
61
|
+
:name => (member.css(".sf-tooltip-name").children[0].text.strip rescue nil),
|
62
|
+
:access => (member.css(".sf-can-edit-text").text rescue nil),
|
63
|
+
:joined => member.css("> .sf-name > em").text == "(pending)" ? "Still waiting" : "Joined"
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parse ns_id
|
69
|
+
result[:ns_id] = doc.css("[data-ns-id]").first.attr("data-ns-id") rescue nil
|
70
|
+
|
71
|
+
result
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Following parsers are for specific Dropbox actions
|
76
|
+
#
|
77
|
+
|
78
|
+
# Parses a `share_options` response
|
79
|
+
def self.parse_share_options(text)
|
80
|
+
json = parse_json(text)
|
81
|
+
format, html, el = json["actions"].first
|
82
|
+
parse_html(html)
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|