nexo 0.1.0
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/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/helpers/nexo/controller_helper.rb +10 -0
- data/app/jobs/nexo/api_clients.rb +25 -0
- data/app/jobs/nexo/base_job.rb +8 -0
- data/app/jobs/nexo/delete_remote_resource_job.rb +10 -0
- data/app/jobs/nexo/folder_sync_job.rb +25 -0
- data/app/jobs/nexo/sync_element_job.rb +91 -0
- data/app/jobs/nexo/synchronizable_changed_job.rb +25 -0
- data/app/jobs/nexo/update_remote_resource_job.rb +56 -0
- data/app/lib/nexo/active_record_google_token_store.rb +43 -0
- data/app/lib/nexo/api_client/api_response.rb +4 -0
- data/app/lib/nexo/api_client/calendar_service.rb +9 -0
- data/app/lib/nexo/api_client/google_auth_service.rb +89 -0
- data/app/lib/nexo/api_client/google_calendar_service.rb +113 -0
- data/app/lib/nexo/api_client/google_dummy_calendar_service.rb +31 -0
- data/app/lib/nexo/errors.rb +28 -0
- data/app/lib/nexo/event_receiver.rb +45 -0
- data/app/lib/nexo/folder_service.rb +67 -0
- data/app/lib/nexo/policy_service.rb +40 -0
- data/app/lib/nexo/service_builder.rb +30 -0
- data/app/models/concerns/nexo/calendar_event.rb +56 -0
- data/app/models/concerns/nexo/folder_policy.rb +24 -0
- data/app/models/concerns/nexo/synchronizable.rb +88 -0
- data/app/models/nexo/application_record.rb +5 -0
- data/app/models/nexo/client.rb +49 -0
- data/app/models/nexo/element.rb +74 -0
- data/app/models/nexo/element_version.rb +38 -0
- data/app/models/nexo/folder.rb +43 -0
- data/app/models/nexo/integration.rb +60 -0
- data/app/models/nexo/token.rb +29 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20250505192315_create_nexo_clients.rb +13 -0
- data/db/migrate/20250505195429_create_nexo_integrations.rb +15 -0
- data/db/migrate/20250506125057_create_nexo_tokens.rb +12 -0
- data/db/migrate/20250512025423_create_nexo_folders.rb +13 -0
- data/db/migrate/20250512025950_create_nexo_elements.rb +16 -0
- data/db/migrate/20250512030530_create_nexo_element_versions.rb +13 -0
- data/db/migrate/20250519210346_create_good_jobs.rb +104 -0
- data/db/seeds.rb +3 -0
- data/lib/nexo/engine.rb +24 -0
- data/lib/nexo/version.rb +5 -0
- data/lib/nexo.rb +6 -0
- metadata +144 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9dfe1d04459f1395bc88a4f93449f8d43c883271c9f217f95f8074cfd2069ca9
|
4
|
+
data.tar.gz: e11b5ee8142fdb61d7f5dd919c98aabd2b1bf3447e0d6f084785c37964d3298b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d519f576a9c86874ca0fc8430a33f51751934911908010a8dea9f62d14f99bddfdfa7b171017be789d3bee510674ea66189efacdb72a2f4b5d927eaddfd2a40a
|
7
|
+
data.tar.gz: 9ee812b64c7342916f0a9d2cf6cbe874507da2ba0c0bca7cb3531f6093ee0edc5d4231260ec4830df5fde6cef50dfe63b09a911dd688d2a864eae638e725f4a9
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Martín Rosso
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# Nexo
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "nexo"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install nexo
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Nexo
|
2
|
+
module ControllerHelper
|
3
|
+
def nexo_integration_params(params)
|
4
|
+
# When upgrading to Rails 8, use "expect"
|
5
|
+
params.require(:integration).permit(:client_id, :name, scope: []).tap do |it|
|
6
|
+
raise Errors::InvalidParamsError, "scope is required" unless it[:scope].present?
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Nexo
|
2
|
+
module ApiClients
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include GoodJob::ActiveJobExtensions::Concurrency
|
7
|
+
|
8
|
+
queue_as :api_clients
|
9
|
+
|
10
|
+
good_job_control_concurrency_with(
|
11
|
+
perform_limit: 1,
|
12
|
+
|
13
|
+
perform_throttle: (Nexo.api_jobs_throttle || [ 100, 5.minute ]),
|
14
|
+
|
15
|
+
key: -> { "#{queue_name}" }
|
16
|
+
)
|
17
|
+
|
18
|
+
retry_on(
|
19
|
+
GoodJob::ActiveJobExtensions::Concurrency::ConcurrencyExceededError,
|
20
|
+
attempts: Float::INFINITY,
|
21
|
+
wait: ->(executions) { ((executions**3) + (Kernel.rand * (executions**3) * 0.5)) + 2 }
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Nexo
|
2
|
+
class FolderSyncJob < BaseJob
|
3
|
+
def perform(folder)
|
4
|
+
policies = PolicyService.instance.policies_for(folder)
|
5
|
+
# flat_map should be equivalent to:
|
6
|
+
# policies.map(&:synchronizable_queries).flatten(1)
|
7
|
+
queries = policies.flat_map(&:synchronizable_queries)
|
8
|
+
|
9
|
+
queries.each do |query|
|
10
|
+
# TODO: avoid calling more than once per synchronizable
|
11
|
+
query.find_each do |synchronizable|
|
12
|
+
folder_service.find_element_and_sync(folder, synchronizable)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# :nocov: mocked
|
20
|
+
def folder_service
|
21
|
+
@folder_service ||= FolderService.new
|
22
|
+
end
|
23
|
+
# :nocov:
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Nexo
|
2
|
+
# Handles synchronization of an Element. Its called anytime there is an internal or external change
|
3
|
+
#
|
4
|
+
# Always must be executed asynchronously to ensure the concurrency limit applies
|
5
|
+
#
|
6
|
+
# Responsabilities:
|
7
|
+
# - Triggering the DeleteRemoteResourceJob
|
8
|
+
# - Triggering the UpdateRemoteResourceJob
|
9
|
+
# - Removing/discarding Element's
|
10
|
+
# - Flagging Synchronizable's as conflicted
|
11
|
+
# - Updating Synchronizable's on external incoming changes
|
12
|
+
#
|
13
|
+
# TODO: implement external ElementVersion creation, not here, on another place
|
14
|
+
class SyncElementJob < BaseJob
|
15
|
+
limits_concurrency key: ->(element) { element.to_gid }
|
16
|
+
|
17
|
+
# discard_on Errors::SyncElementJobError
|
18
|
+
# retry_on StandardError, wait: :polynomially_longer
|
19
|
+
|
20
|
+
def perform(element)
|
21
|
+
validate_element_state!(element)
|
22
|
+
|
23
|
+
if element.flagged_for_deletion?
|
24
|
+
DeleteRemoteResourceJob.perform_later(element)
|
25
|
+
|
26
|
+
element.discard!
|
27
|
+
else
|
28
|
+
current_sequence = element.synchronizable.sequence
|
29
|
+
last_synced_sequence = element.last_synced_sequence
|
30
|
+
|
31
|
+
if element.external_unsynced_change?
|
32
|
+
# :nocov: TODO
|
33
|
+
raise Errors::SyncElementJobError, "not yet implemented"
|
34
|
+
# :nocov:
|
35
|
+
|
36
|
+
# if current_sequence == last_synced_sequence
|
37
|
+
# sync_to_local_element(element)
|
38
|
+
# elsif current_sequence > last_synced_sequence
|
39
|
+
# element.flag_as_conflicted!
|
40
|
+
# else
|
41
|
+
# # :nocov: type: borderline
|
42
|
+
# report_sequence_bigger_than_current_one!
|
43
|
+
# # :nocov:
|
44
|
+
# end
|
45
|
+
else
|
46
|
+
if current_sequence == last_synced_sequence
|
47
|
+
# TODO: log "Element already synced: #{element.to_gid}"
|
48
|
+
elsif current_sequence > last_synced_sequence
|
49
|
+
UpdateRemoteResourceJob.perform_later(element)
|
50
|
+
else
|
51
|
+
# :nocov: borderline
|
52
|
+
raise Errors::LastSyncedSequenceBiggerThanCurrentOne, element
|
53
|
+
# :nocov:
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def validate_element_state!(element)
|
62
|
+
if element.discarded?
|
63
|
+
# TODO: maybe check the case of external incoming changes to flag the element as conflicted
|
64
|
+
raise Errors::ElementDiscarded, element
|
65
|
+
end
|
66
|
+
|
67
|
+
if element.synchronizable.blank? && !element.flagged_for_deletion?
|
68
|
+
# element should have been flagged for deletion
|
69
|
+
raise Errors::SynchronizableNotFound, element
|
70
|
+
end
|
71
|
+
|
72
|
+
if element.conflicted?
|
73
|
+
raise Errors::ElementConflicted, element
|
74
|
+
end
|
75
|
+
|
76
|
+
if element.synchronizable.present? && element.synchronizable.conflicted?
|
77
|
+
raise Errors::SynchronizableConflicted, element
|
78
|
+
end
|
79
|
+
|
80
|
+
if element.synchronizable.present? && element.synchronizable.sequence.nil?
|
81
|
+
raise Errors::SynchronizableSequenceIsNull, element
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# def sync_to_local_element(element)
|
86
|
+
# last_external_unsynced_version = element.last_external_unsynced_version
|
87
|
+
|
88
|
+
# element.synchronizable.update_from!(last_external_unsynced_version)
|
89
|
+
# end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Nexo
|
2
|
+
class SynchronizableChangedJob < BaseJob
|
3
|
+
limits_concurrency key: ->(synchronizable) { synchronizable.to_gid }
|
4
|
+
|
5
|
+
# TODO: check
|
6
|
+
# https://github.com/rails/solid_queue?tab=readme-ov-file#jobs-and-transactional-integrity
|
7
|
+
#
|
8
|
+
# TODO: handle exceptions
|
9
|
+
|
10
|
+
def perform(synchronizable)
|
11
|
+
# Maybe restrict this query to a more specific scope
|
12
|
+
scope = Folder.all
|
13
|
+
|
14
|
+
scope.each do |folder|
|
15
|
+
folder_service.find_element_and_sync(folder, synchronizable)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def folder_service
|
22
|
+
@folder_service ||= FolderService.new
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Nexo
|
2
|
+
class UpdateRemoteResourceJob < BaseJob
|
3
|
+
include ApiClients
|
4
|
+
|
5
|
+
attr_reader :element
|
6
|
+
|
7
|
+
def perform(element)
|
8
|
+
@element = element
|
9
|
+
|
10
|
+
validate_element_state!
|
11
|
+
|
12
|
+
remote_service = ServiceBuilder.instance.build_protocol_service(element.folder)
|
13
|
+
|
14
|
+
response =
|
15
|
+
if element.element_versions.any?
|
16
|
+
remote_service.update(element)
|
17
|
+
else
|
18
|
+
remote_service.insert(element.folder, element.synchronizable).tap do |response|
|
19
|
+
element.update(uuid: response.id)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
save_element_version(response)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validate_element_state!
|
29
|
+
if element.synchronizable.conflicted?
|
30
|
+
raise Errors::ElementConflicted
|
31
|
+
end
|
32
|
+
|
33
|
+
if element.external_unsynced_change?
|
34
|
+
raise Errors::ExternalUnsyncedChange
|
35
|
+
end
|
36
|
+
|
37
|
+
current_sequence = element.synchronizable.sequence
|
38
|
+
last_synced_sequence = element.last_synced_sequence
|
39
|
+
|
40
|
+
unless current_sequence > last_synced_sequence
|
41
|
+
raise Errors::ElementAlreadySynced
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @todo sequence should be fetched before to avoid being outdated
|
46
|
+
def save_element_version(service_response)
|
47
|
+
ElementVersion.create!(
|
48
|
+
element:,
|
49
|
+
origin: :internal,
|
50
|
+
etag: service_response.etag,
|
51
|
+
payload: service_response.payload,
|
52
|
+
sequence: element.synchronizable.sequence,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "googleauth/token_store"
|
2
|
+
|
3
|
+
module Nexo
|
4
|
+
class ActiveRecordGoogleTokenStore < Google::Auth::TokenStore
|
5
|
+
# (see Google::Auth::Stores::TokenStore#load)
|
6
|
+
def load(id)
|
7
|
+
token = find_by_id(id)
|
8
|
+
|
9
|
+
if token.present?
|
10
|
+
token.secret
|
11
|
+
else
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# (see Google::Auth::Stores::TokenStore#store)
|
17
|
+
def store(integration, token)
|
18
|
+
ActiveRecord::Base.transaction do
|
19
|
+
# Maybe these should be destroyed
|
20
|
+
integration.tokens.active.update_all(tpt_status: :expired)
|
21
|
+
|
22
|
+
Token.create!(integration:, secret: token)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# (see Google::Auth::Stores::TokenStore#delete)
|
27
|
+
def delete(id)
|
28
|
+
token = find_by_id(id)
|
29
|
+
|
30
|
+
if token.present?
|
31
|
+
token.update!(tpt_status: :revoked)
|
32
|
+
else
|
33
|
+
# TODO: pg_warn("Couldn't find token for revocation: #{id}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def find_by_id(id)
|
40
|
+
Token.where(environment: Rails.env, integration: id, tpt_status: :active).last
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "googleauth"
|
2
|
+
require "google-apis-oauth2_v2"
|
3
|
+
|
4
|
+
module Nexo
|
5
|
+
# This is actually an OAuth 2.0 flow, and that logic should be extracted to
|
6
|
+
# a generic OAuth2Service
|
7
|
+
class GoogleAuthService
|
8
|
+
class << self
|
9
|
+
def handle_auth_callback_deferred(request)
|
10
|
+
target_url = Google::Auth::WebUserAuthorizer.handle_auth_callback_deferred(request)
|
11
|
+
|
12
|
+
target_url
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(integration)
|
17
|
+
@integration = integration
|
18
|
+
end
|
19
|
+
|
20
|
+
EXCEPTIONS = [
|
21
|
+
Signet::AuthorizationError,
|
22
|
+
|
23
|
+
# user revoked access
|
24
|
+
Google::Apis::ClientError,
|
25
|
+
|
26
|
+
Google::Apis::AuthorizationError
|
27
|
+
]
|
28
|
+
|
29
|
+
def token_info
|
30
|
+
service = Google::Apis::Oauth2V2::Oauth2Service.new
|
31
|
+
credentials = get_credentials
|
32
|
+
if credentials.present?
|
33
|
+
service.authorization = credentials
|
34
|
+
|
35
|
+
# Si el token expiró o le restan pocos segundos para expirar, se
|
36
|
+
# renovará el token.
|
37
|
+
service.tokeninfo
|
38
|
+
end
|
39
|
+
rescue *EXCEPTIONS => e
|
40
|
+
# TODO: handle this
|
41
|
+
# :nocov: TODO
|
42
|
+
Nexo::ActiveRecordGoogleTokenStore.new.delete(@integration)
|
43
|
+
e.class.to_s
|
44
|
+
# :nocov:
|
45
|
+
end
|
46
|
+
|
47
|
+
def revoke_authorization!
|
48
|
+
authorizer.revoke_authorization(@integration)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param [Rack::Request] request
|
52
|
+
#
|
53
|
+
# Debe estar presente en la autorización (cuando google callback redirige
|
54
|
+
# al show)
|
55
|
+
#
|
56
|
+
# Guarda el Token
|
57
|
+
# Si el client tiene más permisos que los que el user solicitó
|
58
|
+
def get_credentials(request = nil)
|
59
|
+
if request.present? && request.session["code_verifier"].present?
|
60
|
+
# :nocov: tricky
|
61
|
+
authorizer.code_verifier = request.session["code_verifier"]
|
62
|
+
# :nocov:
|
63
|
+
end
|
64
|
+
authorizer.get_credentials @integration, request
|
65
|
+
rescue Signet::AuthorizationError
|
66
|
+
# TODO: log
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_authorization_url(request)
|
70
|
+
request.session["code_verifier"] ||= Google::Auth::WebUserAuthorizer.generate_code_verifier
|
71
|
+
authorizer.code_verifier = request.session["code_verifier"]
|
72
|
+
authorizer.get_authorization_url(request:)
|
73
|
+
# authorizer.get_authorization_url(request:, login_hint: "bla@gmail.com")
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def authorizer
|
79
|
+
client = @integration.client
|
80
|
+
client_id = Google::Auth::ClientId.from_hash(client.secret)
|
81
|
+
|
82
|
+
token_store = Nexo::ActiveRecordGoogleTokenStore.new
|
83
|
+
|
84
|
+
@authorizer ||=
|
85
|
+
Google::Auth::WebUserAuthorizer.new(
|
86
|
+
client_id, @integration.external_api_scope, token_store, "/u/google/callback")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require "google-apis-calendar_v3"
|
2
|
+
|
3
|
+
module Nexo
|
4
|
+
# Wrapper around +Google::Apis::CalendarV3+
|
5
|
+
#
|
6
|
+
# @raise [Google::Apis::ClientError] possible messages:
|
7
|
+
# - duplicate: The requested identifier already exists.
|
8
|
+
# - notFound: Not Found
|
9
|
+
# - forbidden: Forbidden (cuando se intenta updatear un evento que ya fue borrado)
|
10
|
+
class GoogleCalendarService < CalendarService
|
11
|
+
# Create an event in a Google Calendar
|
12
|
+
#
|
13
|
+
# @param [Folder] folder
|
14
|
+
#
|
15
|
+
# @todo Debería recibir un {Element}?
|
16
|
+
def insert(folder, calendar_event)
|
17
|
+
validate_folder_state!(folder)
|
18
|
+
|
19
|
+
event = build_event(calendar_event)
|
20
|
+
response = client.insert_event(folder.external_identifier, event)
|
21
|
+
ApiResponse.new(payload: response.to_json, status: :ok, etag: response.etag, id: response.id)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Update an event in a Google Calendar
|
25
|
+
def update(element)
|
26
|
+
validate_folder_state!(element.folder)
|
27
|
+
|
28
|
+
event = build_event(element.synchronizable)
|
29
|
+
response = client.update_event(element.folder.external_identifier, element.uuid, event)
|
30
|
+
ApiResponse.new(payload: response.to_json, status: :ok, etag: response.etag)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Delete an event in a Google Calendar
|
34
|
+
def remove(element)
|
35
|
+
validate_folder_state!(element.folder)
|
36
|
+
|
37
|
+
# TODO: try with cancelled
|
38
|
+
client.delete_event(element.folder.external_identifier, element.uuid)
|
39
|
+
ApiResponse.new(payload: nil, status: :ok, etag: nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create a Google calendar
|
43
|
+
def insert_calendar(folder)
|
44
|
+
cal = build_calendar(folder)
|
45
|
+
response = client.insert_calendar(cal)
|
46
|
+
ApiResponse.new(payload: response.to_json, status: :ok, etag: response.etag, id: response.id)
|
47
|
+
end
|
48
|
+
|
49
|
+
def remove_calendar(folder)
|
50
|
+
client.delete_calendar(folder.external_identifier)
|
51
|
+
ApiResponse.new(status: :ok)
|
52
|
+
end
|
53
|
+
|
54
|
+
# @!visibility private
|
55
|
+
# :nocov: non-production
|
56
|
+
def clear_calendars
|
57
|
+
Folder.all.each do |folder|
|
58
|
+
cid = folder.external_identifier
|
59
|
+
events = client.list_events(cid).items
|
60
|
+
events.each do |event|
|
61
|
+
client.delete_event(cid, event.id)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
# :nocov:
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def validate_folder_state!(folder)
|
70
|
+
if folder.external_identifier.blank?
|
71
|
+
raise Errors::InvalidFolderState, folder
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def build_event(calendar_event)
|
76
|
+
estart = build_event_date_time(calendar_event.datetime_from)
|
77
|
+
eend = build_event_date_time(calendar_event.datetime_to)
|
78
|
+
|
79
|
+
Google::Apis::CalendarV3::Event.new(
|
80
|
+
start: estart,
|
81
|
+
end: eend,
|
82
|
+
summary: calendar_event.summary,
|
83
|
+
description: calendar_event.description
|
84
|
+
)
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_calendar(folder)
|
88
|
+
cal = Google::Apis::CalendarV3::Calendar.new(
|
89
|
+
summary: folder.name,
|
90
|
+
description: folder.description,
|
91
|
+
time_zone: folder.time_zone
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
# @param [Date, DateTime] datetime
|
96
|
+
def build_event_date_time(datetime)
|
97
|
+
if datetime.respond_to?(:hour)
|
98
|
+
Google::Apis::CalendarV3::EventDateTime.new(date_time: datetime)
|
99
|
+
else
|
100
|
+
Google::Apis::CalendarV3::EventDateTime.new(date: datetime)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def client
|
105
|
+
# :nocov: mocked
|
106
|
+
@client ||=
|
107
|
+
Google::Apis::CalendarV3::CalendarService.new.tap do |cli|
|
108
|
+
cli.authorization = integration.credentials
|
109
|
+
end
|
110
|
+
# :nocov:
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# :nocov: tricky
|
2
|
+
module Nexo
|
3
|
+
# Dummy calendar service
|
4
|
+
class GoogleDummyCalendarService < CalendarService
|
5
|
+
def insert(folder, calendar_event)
|
6
|
+
puts "Dummy: Creating event '#{calendar_event.summary}'"
|
7
|
+
ApiResponse.new(payload: "payload", status: :ok, etag: "etag", id: "dummy_id")
|
8
|
+
end
|
9
|
+
|
10
|
+
def update(element)
|
11
|
+
puts "Dummy: Updating event"
|
12
|
+
ApiResponse.new(payload: "payload", status: :ok, etag: "etag", id: "dummy_id")
|
13
|
+
end
|
14
|
+
|
15
|
+
def remove(element)
|
16
|
+
puts "Dummy: Removing event"
|
17
|
+
ApiResponse.new(payload: "payload", status: :ok, etag: "etag", id: "dummy_id")
|
18
|
+
end
|
19
|
+
|
20
|
+
def insert_calendar(folder)
|
21
|
+
puts "Dummy: Creating calendar"
|
22
|
+
ApiResponse.new(payload: "payload", status: :ok, etag: "etag", id: "dummy_id")
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_calendar(folder)
|
26
|
+
puts "Dummy: Removing calendar"
|
27
|
+
ApiResponse.new(payload: "payload", status: :ok, etag: "etag", id: "dummy_id")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
# :nocov:
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Nexo
|
2
|
+
class Errors
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
class ElementConflicted < Error; end
|
6
|
+
class ExternalUnsyncedChange < Error; end
|
7
|
+
class ElementAlreadySynced < Error; end
|
8
|
+
class MoreThanOneElementInFolderForSynchronizable < Error; end
|
9
|
+
class InvalidFolderState < Error; end
|
10
|
+
|
11
|
+
# on ControllerHelper
|
12
|
+
class InvalidParamsError < Error; end
|
13
|
+
|
14
|
+
# on Synchronizable
|
15
|
+
class InterfaceMethodNotImplemented < Error; end
|
16
|
+
|
17
|
+
# on EventReceiver
|
18
|
+
class InvalidSynchronizableState < Error; end
|
19
|
+
|
20
|
+
# on SyncElementJob
|
21
|
+
class SyncElementJobError < Errors::Error; end
|
22
|
+
class SynchronizableConflicted < Error; end
|
23
|
+
class ElementDiscarded < Error; end
|
24
|
+
class SynchronizableNotFound < Error; end
|
25
|
+
class SynchronizableSequenceIsNull < Error; end
|
26
|
+
class LastSyncedSequenceBiggerThanCurrentOne < Error; end
|
27
|
+
end
|
28
|
+
end
|