dropbox-invite 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Code Climate](https://codeclimate.com/github/Jesus/dropbox-invite/badges/gpa.svg)](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
|