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.
Files changed (34) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +34 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE +675 -0
  6. data/README.md +106 -0
  7. data/Rakefile +11 -0
  8. data/dropbox-invite.gemspec +34 -0
  9. data/lib/dropbox/api/objects/dir.rb +38 -0
  10. data/lib/dropbox/api/util/config.rb +21 -0
  11. data/lib/dropbox/invite.rb +1 -0
  12. data/lib/dropbox/web_client.rb +12 -0
  13. data/lib/dropbox/web_client/actions.rb +49 -0
  14. data/lib/dropbox/web_client/authentication.rb +71 -0
  15. data/lib/dropbox/web_client/cookie_manager.rb +40 -0
  16. data/lib/dropbox/web_client/paths.rb +35 -0
  17. data/lib/dropbox/web_client/response_parser.rb +88 -0
  18. data/lib/dropbox/web_client/session.rb +27 -0
  19. data/lib/dropbox/web_client/version.rb +5 -0
  20. data/spec/actions_spec.rb +62 -0
  21. data/spec/authentication_spec.rb +13 -0
  22. data/spec/dropbox_api_spec.rb +59 -0
  23. data/spec/initialization_spec.rb +22 -0
  24. data/spec/spec_helper.rb +40 -0
  25. data/spec/support/vcr.rb +21 -0
  26. data/spec/vcr/dropbox_api/members_through_api.yml +1003 -0
  27. data/spec/vcr/dropbox_webclient_actions/invite_existing_folder.yml +1140 -0
  28. data/spec/vcr/dropbox_webclient_actions/invite_shared_folder.yml +1499 -0
  29. data/spec/vcr/dropbox_webclient_actions/invite_unexisting_folder.yml +815 -0
  30. data/spec/vcr/dropbox_webclient_actions/share_opts_existing.yml +736 -0
  31. data/spec/vcr/dropbox_webclient_actions/share_opts_shared.yml +849 -0
  32. data/spec/vcr/dropbox_webclient_actions/share_opts_unexisting.yml +735 -0
  33. data/spec/vcr/dropbox_webclient_authentication/login.yml +598 -0
  34. metadata +217 -0
@@ -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)
@@ -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