calendlyr 0.7.5 → 1.0.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 +4 -4
- data/.github/workflows/ci.yml +9 -5
- data/.gitignore +3 -1
- data/CHANGELOG.md +98 -0
- data/README.md +209 -73
- data/calendlyr.gemspec +8 -6
- data/docs/resources/activity_log/list_activity_log_entries.md +2 -1
- data/docs/resources/availabilities/user_availability_schedule.md +3 -2
- data/docs/resources/availabilities/user_busy_time.md +3 -2
- data/docs/resources/data_compliance.md +1 -1
- data/docs/resources/event_types/availability_schedule.md +28 -0
- data/docs/resources/event_types/available_time.md +15 -11
- data/docs/resources/event_types/event_type.md +21 -9
- data/docs/resources/event_types/membership.md +15 -16
- data/docs/resources/events/cancellation.md +1 -1
- data/docs/resources/events/event.md +6 -5
- data/docs/resources/events/invitee.md +18 -0
- data/docs/resources/events/invitee_no_show.md +2 -1
- data/docs/resources/groups/group.md +2 -1
- data/docs/resources/locations/location.md +16 -0
- data/docs/resources/organizations/membership.md +3 -2
- data/docs/resources/organizations/organization.md +13 -9
- data/docs/resources/outgoing_communications/outgoing_communication.md +28 -0
- data/docs/resources/routing_forms/routing_form.md +2 -1
- data/docs/resources/routing_forms/submission.md +2 -1
- data/docs/resources/scheduling_links/scheduling_link.md +26 -0
- data/docs/resources/share.md +4 -10
- data/docs/resources/webhooks/invitee_payload.md +10 -16
- data/docs/resources/webhooks/payload.md +32 -3
- data/docs/resources/webhooks/sample.md +23 -0
- data/docs/resources/webhooks/subscription.md +10 -6
- data/lib/calendlyr/client.rb +7 -2
- data/lib/calendlyr/collection.rb +42 -9
- data/lib/calendlyr/configuration.rb +24 -0
- data/lib/calendlyr/error.rb +65 -27
- data/lib/calendlyr/object.rb +85 -13
- data/lib/calendlyr/objects/event_type.rb +0 -4
- data/lib/calendlyr/objects/event_types/availability_schedule.rb +8 -0
- data/lib/calendlyr/objects/event_types/available_time.rb +5 -1
- data/lib/calendlyr/objects/event_types/membership.rb +4 -7
- data/lib/calendlyr/objects/events/invitee.rb +1 -1
- data/lib/calendlyr/objects/location.rb +4 -0
- data/lib/calendlyr/objects/organization.rb +0 -4
- data/lib/calendlyr/objects/outgoing_communication.rb +6 -0
- data/lib/calendlyr/objects/scheduling_link.rb +2 -5
- data/lib/calendlyr/objects/share.rb +0 -5
- data/lib/calendlyr/objects/webhooks/payload.rb +30 -0
- data/lib/calendlyr/resource.rb +84 -12
- data/lib/calendlyr/resources/availability.rb +14 -2
- data/lib/calendlyr/resources/event_types.rb +45 -5
- data/lib/calendlyr/resources/events.rb +20 -2
- data/lib/calendlyr/resources/groups.rb +14 -2
- data/lib/calendlyr/resources/locations.rb +13 -0
- data/lib/calendlyr/resources/organizations.rb +25 -3
- data/lib/calendlyr/resources/outgoing_communications.rb +11 -3
- data/lib/calendlyr/resources/routing_forms.rb +14 -2
- data/lib/calendlyr/resources/scheduling_links.rb +5 -2
- data/lib/calendlyr/resources/shares.rb +1 -0
- data/lib/calendlyr/resources/webhooks.rb +11 -3
- data/lib/calendlyr/version.rb +1 -1
- data/lib/calendlyr/webhook.rb +105 -0
- data/lib/calendlyr.rb +50 -0
- data/logos/calendlyr.png +0 -0
- data/logos/calendlyr_bg_white.png +0 -0
- data/test/calendlyr/client_test.rb +29 -0
- data/test/calendlyr/collection_test.rb +168 -0
- data/test/calendlyr/configuration_test.rb +157 -0
- data/test/calendlyr/object_test.rb +82 -1
- data/test/calendlyr/objects/event_type_test.rb +0 -15
- data/test/calendlyr/objects/event_types/availability_schedule_test.rb +20 -0
- data/test/calendlyr/objects/events/cancellation_test.rb +1 -1
- data/test/calendlyr/objects/events/guest_test.rb +1 -1
- data/test/calendlyr/objects/events/invitee_no_show_test.rb +1 -1
- data/test/calendlyr/objects/events/invitee_test.rb +10 -3
- data/test/calendlyr/objects/location_test.rb +22 -0
- data/test/calendlyr/objects/organization_test.rb +0 -8
- data/test/calendlyr/objects/organizations/invitation_test.rb +1 -1
- data/test/calendlyr/objects/share_test.rb +3 -9
- data/test/calendlyr/objects/webhooks/payload_test.rb +15 -0
- data/test/calendlyr/resource_test.rb +456 -2
- data/test/calendlyr/resources/availabilities/user_busy_times_test.rb +26 -0
- data/test/calendlyr/resources/availabilities/user_schedules_test.rb +25 -0
- data/test/calendlyr/resources/data_compliance_test.rb +1 -4
- data/test/calendlyr/resources/event_types_test.rb +132 -0
- data/test/calendlyr/resources/events_test.rb +87 -0
- data/test/calendlyr/resources/groups_test.rb +54 -0
- data/test/calendlyr/resources/locations_test.rb +30 -0
- data/test/calendlyr/resources/organizations_test.rb +96 -2
- data/test/calendlyr/resources/outgoing_communications_test.rb +34 -8
- data/test/calendlyr/resources/routing_forms_test.rb +57 -0
- data/test/calendlyr/resources/scheduling_links_test.rb +31 -6
- data/test/calendlyr/resources/shares_test.rb +15 -0
- data/test/calendlyr/resources/webhooks_test.rb +63 -5
- data/test/calendlyr/webhook_test.rb +292 -0
- data/test/fixtures/activity_log/list_page2.json +30 -0
- data/test/fixtures/event_invitees/list_page2.json +35 -0
- data/test/fixtures/event_invitees/retrieve.json +11 -1
- data/test/fixtures/event_type_availability_schedules/list.json +17 -0
- data/test/fixtures/event_type_availability_schedules/update.json +3 -0
- data/test/fixtures/event_type_available_times/list.json +0 -12
- data/test/fixtures/event_type_memberships/list.json +43 -0
- data/test/fixtures/event_type_memberships/list_page2.json +33 -0
- data/test/fixtures/event_types/create.json +30 -0
- data/test/fixtures/event_types/list_page2.json +37 -0
- data/test/fixtures/event_types/update.json +30 -0
- data/test/fixtures/events/create_invitee.json +37 -0
- data/test/fixtures/events/list_page2.json +29 -0
- data/test/fixtures/events/retrieve.json +12 -2
- data/test/fixtures/group_relationships/list_page2.json +35 -0
- data/test/fixtures/groups/list_page2.json +16 -0
- data/test/fixtures/locations/list.json +16 -0
- data/test/fixtures/locations/list_page2.json +16 -0
- data/test/fixtures/objects/event.json +10 -2
- data/test/fixtures/objects/event_types/availability_schedule.json +6 -0
- data/test/fixtures/objects/location.json +5 -0
- data/test/fixtures/organizations/list_invitations_page2.json +18 -0
- data/test/fixtures/organizations/list_memberships_page2.json +26 -0
- data/test/fixtures/organizations/retrieve.json +11 -0
- data/test/fixtures/outgoing_communications/list.json +4 -6
- data/test/fixtures/outgoing_communications/list_page2.json +21 -0
- data/test/fixtures/routing_forms/list_page2.json +17 -0
- data/test/fixtures/routing_forms/list_routing_form_submission_page2.json +29 -0
- data/test/fixtures/user_availability_schedules/list_page1.json +16 -0
- data/test/fixtures/user_availability_schedules/list_page2.json +16 -0
- data/test/fixtures/user_busy_times/list_page1.json +18 -0
- data/test/fixtures/user_busy_times/list_page2.json +13 -0
- data/test/fixtures/webhooks/list_page2.json +23 -0
- data/test/fixtures/webhooks/sample.json +55 -94
- data/test/test_helper.rb +19 -7
- metadata +70 -27
- data/docs/resources/scheduling_link.md +0 -26
- data/test/calendlyr/objects/event_types/available_time_test.rb +0 -20
- data/test/calendlyr/objects/event_types/membership_test.rb +0 -32
- data/test/calendlyr/objects/scheduling_link_test.rb +0 -17
- data/test/calendlyr/resources/event_types/membership_test.rb +0 -22
- data/test/fixtures/objects/event_types/available_time.json +0 -6
- data/test/fixtures/objects/event_types/membership.json +0 -65
- data/test/fixtures/objects/scheduling_links/event_type.json +0 -5
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
module Calendlyr
|
|
2
2
|
class GroupsResource < Resource
|
|
3
3
|
def list(organization:, **params)
|
|
4
|
+
next_page_caller = ->(page_token:) { list(organization: organization, **params, page_token: page_token) }
|
|
5
|
+
organization = expand_uri(organization, "organizations")
|
|
4
6
|
response = get_request("groups", params: params.merge(organization: organization))
|
|
5
|
-
Collection.from_response(response, type: Group, client: client)
|
|
7
|
+
Collection.from_response(response, type: Group, client: client, next_page_caller: next_page_caller)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list_all(organization:, **params)
|
|
11
|
+
list(organization: organization, **params).auto_paginate.to_a
|
|
6
12
|
end
|
|
7
13
|
|
|
8
14
|
def retrieve(uuid:)
|
|
@@ -11,8 +17,14 @@ module Calendlyr
|
|
|
11
17
|
|
|
12
18
|
# Relationships
|
|
13
19
|
def list_relationships(**params)
|
|
20
|
+
next_page_caller = ->(page_token:) { list_relationships(**params, page_token: page_token) }
|
|
21
|
+
params[:organization] = expand_uri(params[:organization], "organizations") if params[:organization]
|
|
14
22
|
response = get_request("group_relationships", params: params)
|
|
15
|
-
Collection.from_response(response, type: Groups::Relationship, client: client)
|
|
23
|
+
Collection.from_response(response, type: Groups::Relationship, client: client, next_page_caller: next_page_caller)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list_all_relationships(**params)
|
|
27
|
+
list_relationships(**params).auto_paginate.to_a
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
def retrieve_relationship(uuid:)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Calendlyr
|
|
2
|
+
class LocationsResource < Resource
|
|
3
|
+
def list(**params)
|
|
4
|
+
next_page_caller = ->(page_token:) { list(**params, page_token: page_token) }
|
|
5
|
+
response = get_request("locations", params: params)
|
|
6
|
+
Collection.from_response(response, type: Location, client: client, next_page_caller: next_page_caller)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def list_all(**params)
|
|
10
|
+
list(**params).auto_paginate.to_a
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
module Calendlyr
|
|
2
2
|
class OrganizationsResource < Resource
|
|
3
|
+
def retrieve(uuid:)
|
|
4
|
+
Organization.new get_request("organizations/#{uuid}").dig("resource").merge(client: client)
|
|
5
|
+
end
|
|
6
|
+
|
|
3
7
|
def activity_log(organization: nil, **params)
|
|
8
|
+
next_page_caller = ->(page_token:) { activity_log(organization: organization, **params, page_token: page_token) }
|
|
9
|
+
organization = expand_uri(organization, "organizations")
|
|
4
10
|
response = get_request("activity_log_entries", params: {organization: organization}.merge(params).compact)
|
|
5
|
-
Collection.from_response(response, type: ActivityLog, client: client)
|
|
11
|
+
Collection.from_response(response, type: ActivityLog, client: client, next_page_caller: next_page_caller)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def list_all_activity_log(organization: nil, **params)
|
|
15
|
+
activity_log(organization: organization, **params).auto_paginate.to_a
|
|
6
16
|
end
|
|
7
17
|
|
|
8
18
|
# Memberships
|
|
9
19
|
def list_memberships(**params)
|
|
20
|
+
next_page_caller = ->(page_token:) { list_memberships(**params, page_token: page_token) }
|
|
21
|
+
params[:organization] = expand_uri(params[:organization], "organizations") if params[:organization]
|
|
22
|
+
params[:user] = expand_uri(params[:user], "users") if params[:user]
|
|
10
23
|
response = get_request("organization_memberships", params: params)
|
|
11
|
-
Collection.from_response(response, type: Organizations::Membership, client: client)
|
|
24
|
+
Collection.from_response(response, type: Organizations::Membership, client: client, next_page_caller: next_page_caller)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def list_all_memberships(**params)
|
|
28
|
+
list_memberships(**params).auto_paginate.to_a
|
|
12
29
|
end
|
|
13
30
|
|
|
14
31
|
def retrieve_membership(uuid:)
|
|
@@ -21,8 +38,13 @@ module Calendlyr
|
|
|
21
38
|
|
|
22
39
|
# Invitations
|
|
23
40
|
def list_invitations(uuid:, **params)
|
|
41
|
+
next_page_caller = ->(page_token:) { list_invitations(uuid: uuid, **params, page_token: page_token) }
|
|
24
42
|
response = get_request("organizations/#{uuid}/invitations", params: params)
|
|
25
|
-
Collection.from_response(response, type: Organizations::Invitation, client: client)
|
|
43
|
+
Collection.from_response(response, type: Organizations::Invitation, client: client, next_page_caller: next_page_caller)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def list_all_invitations(uuid:, **params)
|
|
47
|
+
list_invitations(uuid: uuid, **params).auto_paginate.to_a
|
|
26
48
|
end
|
|
27
49
|
|
|
28
50
|
def retrieve_invitation(org_uuid:, uuid:)
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Calendlyr
|
|
2
4
|
class OutgoingCommunicationsResource < Resource
|
|
3
|
-
def list(**params)
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
def list(organization:, **params)
|
|
6
|
+
next_page_caller = ->(page_token:) { list(organization: organization, **params, page_token: page_token) }
|
|
7
|
+
organization = expand_uri(organization, "organizations")
|
|
8
|
+
response = get_request("outgoing_communications", params: {organization: organization}.merge(params))
|
|
9
|
+
Collection.from_response(response, type: OutgoingCommunication, client: client, next_page_caller: next_page_caller)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def list_all(organization:, **params)
|
|
13
|
+
list(organization: organization, **params).auto_paginate.to_a
|
|
6
14
|
end
|
|
7
15
|
end
|
|
8
16
|
end
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
module Calendlyr
|
|
2
2
|
class RoutingFormsResource < Resource
|
|
3
3
|
def list(organization:, **params)
|
|
4
|
+
next_page_caller = ->(page_token:) { list(organization: organization, **params, page_token: page_token) }
|
|
5
|
+
organization = expand_uri(organization, "organizations")
|
|
4
6
|
response = get_request("routing_forms", params: {organization: organization}.merge(params))
|
|
5
|
-
Collection.from_response(response, type: RoutingForm, client: client)
|
|
7
|
+
Collection.from_response(response, type: RoutingForm, client: client, next_page_caller: next_page_caller)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list_all(organization:, **params)
|
|
11
|
+
list(organization: organization, **params).auto_paginate.to_a
|
|
6
12
|
end
|
|
7
13
|
|
|
8
14
|
def retrieve(uuid:)
|
|
@@ -11,8 +17,14 @@ module Calendlyr
|
|
|
11
17
|
|
|
12
18
|
# Routing Form Submission
|
|
13
19
|
def list_submissions(form:, **params)
|
|
20
|
+
next_page_caller = ->(page_token:) { list_submissions(form: form, **params, page_token: page_token) }
|
|
21
|
+
form = expand_uri(form, "routing_forms")
|
|
14
22
|
response = get_request("routing_form_submissions", params: {form: form}.merge(params))
|
|
15
|
-
Collection.from_response(response, type: RoutingForms::Submission, client: client)
|
|
23
|
+
Collection.from_response(response, type: RoutingForms::Submission, client: client, next_page_caller: next_page_caller)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def list_all_submissions(form:, **params)
|
|
27
|
+
list_submissions(form: form, **params).auto_paginate.to_a
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
def retrieve_submission(uuid:)
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Calendlyr
|
|
2
4
|
class SchedulingLinksResource < Resource
|
|
3
|
-
def create(owner:,
|
|
4
|
-
|
|
5
|
+
def create(owner:, owner_type: "EventType", max_event_count: 1)
|
|
6
|
+
owner = expand_uri(owner, "event_types")
|
|
7
|
+
body = {max_event_count: max_event_count, owner: owner, owner_type: owner_type}
|
|
5
8
|
SchedulingLink.new post_request("scheduling_links", body: body).dig("resource").merge(client: client)
|
|
6
9
|
end
|
|
7
10
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Calendlyr
|
|
2
2
|
class SharesResource < Resource
|
|
3
3
|
def create(event_type:, **params)
|
|
4
|
+
event_type = expand_uri(event_type, "event_types")
|
|
4
5
|
body = {event_type: event_type}.merge(params)
|
|
5
6
|
Share.new post_request("shares", body: body).dig("resource").merge(client: client)
|
|
6
7
|
end
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
module Calendlyr
|
|
2
2
|
class WebhooksResource < Resource
|
|
3
3
|
def list(organization:, scope:, **params)
|
|
4
|
+
next_page_caller = ->(page_token:) { list(organization: organization, scope: scope, **params, page_token: page_token) }
|
|
5
|
+
organization = expand_uri(organization, "organizations")
|
|
4
6
|
response = get_request("webhook_subscriptions", params: params.merge(organization: organization, scope: scope).compact)
|
|
5
|
-
Collection.from_response(response, type: Webhooks::Subscription, client: client)
|
|
7
|
+
Collection.from_response(response, type: Webhooks::Subscription, client: client, next_page_caller: next_page_caller)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list_all(organization:, scope:, **params)
|
|
11
|
+
list(organization: organization, scope: scope, **params).auto_paginate.to_a
|
|
6
12
|
end
|
|
7
13
|
|
|
8
14
|
def create(url:, events:, organization:, scope:, **params)
|
|
15
|
+
organization = expand_uri(organization, "organizations")
|
|
9
16
|
body = params.merge(url: url, events: events, organization: organization, scope: scope)
|
|
10
17
|
Webhooks::Subscription.new post_request("webhook_subscriptions", body: body).dig("resource").merge(client: client)
|
|
11
18
|
end
|
|
@@ -18,8 +25,9 @@ module Calendlyr
|
|
|
18
25
|
delete_request("webhook_subscriptions/#{webhook_uuid}")
|
|
19
26
|
end
|
|
20
27
|
|
|
21
|
-
def
|
|
22
|
-
|
|
28
|
+
def sample(event:, organization:, scope:, **params)
|
|
29
|
+
organization = expand_uri(organization, "organizations")
|
|
30
|
+
get_request("sample_webhook_data", params: {event: event, organization: organization, scope: scope}.merge(params))
|
|
23
31
|
end
|
|
24
32
|
end
|
|
25
33
|
end
|
data/lib/calendlyr/version.rb
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module Calendlyr
|
|
7
|
+
module Webhook
|
|
8
|
+
DEFAULT_TOLERANCE = 300
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def verify!(payload:, signing_key:, header: nil, signature_header: nil, tolerance: DEFAULT_TOLERANCE)
|
|
13
|
+
raise ArgumentError, "signing_key is required" if signing_key.nil? || signing_key.empty?
|
|
14
|
+
|
|
15
|
+
resolved_header = resolve_signature_header(header: header, signature_header: signature_header)
|
|
16
|
+
timestamp, provided_signature = parse_header(resolved_header)
|
|
17
|
+
verify_timestamp!(timestamp, tolerance)
|
|
18
|
+
|
|
19
|
+
expected_signature = compute_signature(payload, timestamp, signing_key)
|
|
20
|
+
return true if secure_compare?(expected_signature, provided_signature)
|
|
21
|
+
|
|
22
|
+
raise WebhookSignatureError, "Invalid webhook signature"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def valid?(payload:, signing_key:, header: nil, signature_header: nil, tolerance: DEFAULT_TOLERANCE)
|
|
26
|
+
verify!(payload: payload, header: header, signature_header: signature_header, signing_key: signing_key, tolerance: tolerance)
|
|
27
|
+
rescue WebhookSignatureError, WebhookTimestampError
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse(payload:, signing_key:, header: nil, signature_header: nil, tolerance: DEFAULT_TOLERANCE)
|
|
32
|
+
verify!(payload: payload, header: header, signature_header: signature_header, signing_key: signing_key, tolerance: tolerance)
|
|
33
|
+
parsed_payload = JSON.parse(payload)
|
|
34
|
+
Webhooks::Payload.new(parsed_payload.merge(client: nil))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def resolve_signature_header(header:, signature_header:)
|
|
38
|
+
if header && signature_header && header != signature_header
|
|
39
|
+
raise ArgumentError, "Provide either header or signature_header"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
header || signature_header
|
|
43
|
+
end
|
|
44
|
+
private_class_method :resolve_signature_header
|
|
45
|
+
|
|
46
|
+
def parse_header(header)
|
|
47
|
+
raise WebhookSignatureError, "Missing webhook signature header" if header.nil? || header.strip.empty?
|
|
48
|
+
|
|
49
|
+
pairs = header.split(",").map(&:strip)
|
|
50
|
+
parsed = Hash.new { |hash, key| hash[key] = [] }
|
|
51
|
+
|
|
52
|
+
pairs.each do |pair|
|
|
53
|
+
key, value = pair.split("=", 2)
|
|
54
|
+
raise WebhookSignatureError, "Malformed webhook signature header" if key.nil? || value.nil?
|
|
55
|
+
|
|
56
|
+
parsed[key] << value.strip
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
timestamp_value = extract_single!(parsed, "t")
|
|
60
|
+
signature_value = extract_single!(parsed, "v1")
|
|
61
|
+
|
|
62
|
+
raise WebhookSignatureError, "Malformed webhook timestamp" unless timestamp_value.match?(/\A\d+\z/)
|
|
63
|
+
|
|
64
|
+
normalized_signature = signature_value.downcase
|
|
65
|
+
raise WebhookSignatureError, "Malformed webhook signature" unless normalized_signature.match?(/\A\h{64}\z/)
|
|
66
|
+
|
|
67
|
+
[timestamp_value.to_i, normalized_signature]
|
|
68
|
+
end
|
|
69
|
+
private_class_method :parse_header
|
|
70
|
+
|
|
71
|
+
def extract_single!(parsed, key)
|
|
72
|
+
values = parsed[key]
|
|
73
|
+
raise WebhookSignatureError, "Missing #{key} in webhook signature header" if values.empty?
|
|
74
|
+
raise WebhookSignatureError, "Duplicate #{key} in webhook signature header" if values.size > 1
|
|
75
|
+
|
|
76
|
+
value = values.first
|
|
77
|
+
raise WebhookSignatureError, "Blank #{key} in webhook signature header" if value.nil? || value.empty?
|
|
78
|
+
|
|
79
|
+
value
|
|
80
|
+
end
|
|
81
|
+
private_class_method :extract_single!
|
|
82
|
+
|
|
83
|
+
def verify_timestamp!(timestamp, tolerance)
|
|
84
|
+
return if tolerance.nil?
|
|
85
|
+
|
|
86
|
+
age = Time.now.to_i - timestamp
|
|
87
|
+
return if age.abs <= tolerance
|
|
88
|
+
|
|
89
|
+
raise WebhookTimestampError, "Webhook timestamp outside tolerance"
|
|
90
|
+
end
|
|
91
|
+
private_class_method :verify_timestamp!
|
|
92
|
+
|
|
93
|
+
def compute_signature(payload, timestamp, signing_key)
|
|
94
|
+
OpenSSL::HMAC.hexdigest("SHA256", signing_key, "#{timestamp}.#{payload}")
|
|
95
|
+
end
|
|
96
|
+
private_class_method :compute_signature
|
|
97
|
+
|
|
98
|
+
def secure_compare?(expected_signature, provided_signature)
|
|
99
|
+
return false unless expected_signature.bytesize == provided_signature.bytesize
|
|
100
|
+
|
|
101
|
+
OpenSSL.fixed_length_secure_compare(expected_signature, provided_signature)
|
|
102
|
+
end
|
|
103
|
+
private_class_method :secure_compare?
|
|
104
|
+
end
|
|
105
|
+
end
|
data/lib/calendlyr.rb
CHANGED
|
@@ -2,11 +2,54 @@ require "calendlyr/version"
|
|
|
2
2
|
|
|
3
3
|
module Calendlyr
|
|
4
4
|
autoload :Client, "calendlyr/client"
|
|
5
|
+
autoload :Configuration, "calendlyr/configuration"
|
|
5
6
|
autoload :Collection, "calendlyr/collection"
|
|
6
7
|
autoload :Object, "calendlyr/object"
|
|
7
8
|
autoload :Resource, "calendlyr/resource"
|
|
8
9
|
|
|
10
|
+
class << self
|
|
11
|
+
def configure
|
|
12
|
+
yield(configuration)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def client
|
|
20
|
+
raise ArgumentError, "Missing Calendly token. Configure it with Calendlyr.configure { |c| c.token = \"...\" }" if configuration.token.nil?
|
|
21
|
+
|
|
22
|
+
signature = configuration_signature
|
|
23
|
+
@client = nil if @client_signature != signature
|
|
24
|
+
@client ||= Client.new(**client_attributes)
|
|
25
|
+
@client_signature = signature
|
|
26
|
+
@client
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset!
|
|
30
|
+
@configuration = nil
|
|
31
|
+
@client = nil
|
|
32
|
+
@client_signature = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def configuration_signature
|
|
38
|
+
[configuration.token, configuration.open_timeout, configuration.read_timeout, configuration.logger]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def client_attributes
|
|
42
|
+
{
|
|
43
|
+
token: configuration.token,
|
|
44
|
+
open_timeout: configuration.open_timeout,
|
|
45
|
+
read_timeout: configuration.read_timeout,
|
|
46
|
+
logger: configuration.logger
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
9
51
|
# Errors
|
|
52
|
+
autoload :ERROR_TYPES, "calendlyr/error"
|
|
10
53
|
autoload :BadRequest, "calendlyr/error"
|
|
11
54
|
autoload :Error, "calendlyr/error"
|
|
12
55
|
autoload :ExternalCalendarError, "calendlyr/error"
|
|
@@ -17,6 +60,9 @@ module Calendlyr
|
|
|
17
60
|
autoload :Unauthenticated, "calendlyr/error"
|
|
18
61
|
autoload :TooManyRequests, "calendlyr/error"
|
|
19
62
|
autoload :ResponseErrorHandler, "calendlyr/error"
|
|
63
|
+
autoload :WebhookSignatureError, "calendlyr/error"
|
|
64
|
+
autoload :WebhookTimestampError, "calendlyr/error"
|
|
65
|
+
autoload :Webhook, "calendlyr/webhook"
|
|
20
66
|
|
|
21
67
|
# High-level categories of Calendly API calls
|
|
22
68
|
autoload :AvailabilityResource, "calendlyr/resources/availability"
|
|
@@ -25,6 +71,7 @@ module Calendlyr
|
|
|
25
71
|
autoload :EventTypesResource, "calendlyr/resources/event_types"
|
|
26
72
|
autoload :GroupsResource, "calendlyr/resources/groups"
|
|
27
73
|
autoload :OrganizationsResource, "calendlyr/resources/organizations"
|
|
74
|
+
autoload :LocationsResource, "calendlyr/resources/locations"
|
|
28
75
|
autoload :OutgoingCommunicationsResource, "calendlyr/resources/outgoing_communications"
|
|
29
76
|
autoload :RoutingFormsResource, "calendlyr/resources/routing_forms"
|
|
30
77
|
autoload :SchedulingLinksResource, "calendlyr/resources/scheduling_links"
|
|
@@ -38,6 +85,8 @@ module Calendlyr
|
|
|
38
85
|
autoload :EventType, "calendlyr/objects/event_type"
|
|
39
86
|
autoload :Group, "calendlyr/objects/group"
|
|
40
87
|
autoload :Organization, "calendlyr/objects/organization"
|
|
88
|
+
autoload :Location, "calendlyr/objects/location"
|
|
89
|
+
autoload :OutgoingCommunication, "calendlyr/objects/outgoing_communication"
|
|
41
90
|
autoload :RoutingForm, "calendlyr/objects/routing_form"
|
|
42
91
|
autoload :SchedulingLink, "calendlyr/objects/scheduling_link"
|
|
43
92
|
autoload :Share, "calendlyr/objects/share"
|
|
@@ -57,6 +106,7 @@ module Calendlyr
|
|
|
57
106
|
end
|
|
58
107
|
|
|
59
108
|
module EventTypes
|
|
109
|
+
autoload :AvailabilitySchedule, "calendlyr/objects/event_types/availability_schedule"
|
|
60
110
|
autoload :AvailableTime, "calendlyr/objects/event_types/available_time"
|
|
61
111
|
autoload :Membership, "calendlyr/objects/event_types/membership"
|
|
62
112
|
autoload :Profile, "calendlyr/objects/event_types/profile"
|
data/logos/calendlyr.png
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require "test_helper"
|
|
2
|
+
require "logger"
|
|
3
|
+
require "stringio"
|
|
2
4
|
|
|
3
5
|
class ClientTest < Minitest::Test
|
|
4
6
|
def test_token
|
|
@@ -19,4 +21,31 @@ class ClientTest < Minitest::Test
|
|
|
19
21
|
assert client.respond_to?(:users)
|
|
20
22
|
refute client.respond_to?(:useers)
|
|
21
23
|
end
|
|
24
|
+
|
|
25
|
+
def test_default_timeouts
|
|
26
|
+
new_client = Calendlyr::Client.new(token: "fake")
|
|
27
|
+
|
|
28
|
+
assert_equal 30, new_client.open_timeout
|
|
29
|
+
assert_equal 30, new_client.read_timeout
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_custom_timeouts
|
|
33
|
+
new_client = Calendlyr::Client.new(token: "fake", open_timeout: 10, read_timeout: 15)
|
|
34
|
+
|
|
35
|
+
assert_equal 10, new_client.open_timeout
|
|
36
|
+
assert_equal 15, new_client.read_timeout
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_default_logger_is_nil
|
|
40
|
+
new_client = Calendlyr::Client.new(token: "fake")
|
|
41
|
+
|
|
42
|
+
assert_nil new_client.logger
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_custom_logger
|
|
46
|
+
logger = Logger.new(StringIO.new)
|
|
47
|
+
new_client = Calendlyr::Client.new(token: "fake", logger: logger)
|
|
48
|
+
|
|
49
|
+
assert_equal logger, new_client.logger
|
|
50
|
+
end
|
|
22
51
|
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class CollectionTest < Minitest::Test
|
|
4
|
+
def build_collection
|
|
5
|
+
data = [
|
|
6
|
+
Calendlyr::Object.new(name: "Alice"),
|
|
7
|
+
Calendlyr::Object.new(name: "Bob")
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
Calendlyr::Collection.new(data: data, count: 2, next_page_url: nil, client: client)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_each_yields_items
|
|
14
|
+
collection = build_collection
|
|
15
|
+
names = []
|
|
16
|
+
|
|
17
|
+
collection.each do |item|
|
|
18
|
+
names << item.name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
assert_equal %w[Alice Bob], names
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_map_is_available_from_enumerable
|
|
25
|
+
collection = build_collection
|
|
26
|
+
|
|
27
|
+
assert_equal %w[Alice Bob], collection.map(&:name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_collection_responds_to_select
|
|
31
|
+
collection = build_collection
|
|
32
|
+
|
|
33
|
+
assert collection.respond_to?(:select)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_count_without_block_returns_metadata_count
|
|
37
|
+
collection = Calendlyr::Collection.new(
|
|
38
|
+
data: [Calendlyr::Object.new(name: "Alice")],
|
|
39
|
+
count: 10,
|
|
40
|
+
next_page_url: nil,
|
|
41
|
+
client: client
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert_equal 10, collection.count
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_count_with_block_uses_enumerable_count
|
|
48
|
+
collection = build_collection
|
|
49
|
+
|
|
50
|
+
assert_equal(1, collection.count { |item| item.name == "Alice" })
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_count_with_argument_uses_enumerable_count
|
|
54
|
+
alice = Calendlyr::Object.new(name: "Alice")
|
|
55
|
+
collection = Calendlyr::Collection.new(
|
|
56
|
+
data: [alice, Calendlyr::Object.new(name: "Bob")],
|
|
57
|
+
count: 10,
|
|
58
|
+
next_page_url: nil,
|
|
59
|
+
client: client
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert_equal 1, collection.count(alice)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_next_page_returns_next_collection
|
|
66
|
+
next_data = [Calendlyr::Object.new(name: "Charlie")]
|
|
67
|
+
next_collection = Calendlyr::Collection.new(
|
|
68
|
+
data: next_data, count: 1, next_page_url: nil, client: client
|
|
69
|
+
)
|
|
70
|
+
caller_lambda = ->(page_token:) { next_collection }
|
|
71
|
+
|
|
72
|
+
collection = Calendlyr::Collection.new(
|
|
73
|
+
data: [Calendlyr::Object.new(name: "Alice")],
|
|
74
|
+
count: 1,
|
|
75
|
+
next_page_url: "https://api.calendly.com/scheduled_events?page_token=TOKEN123",
|
|
76
|
+
client: client,
|
|
77
|
+
next_page_caller: caller_lambda
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
result = collection.next_page
|
|
81
|
+
|
|
82
|
+
assert_equal next_collection, result
|
|
83
|
+
assert_equal Calendlyr::Collection, result.class
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_next_page_returns_nil_when_no_token
|
|
87
|
+
collection = Calendlyr::Collection.new(
|
|
88
|
+
data: [Calendlyr::Object.new(name: "Alice")],
|
|
89
|
+
count: 1,
|
|
90
|
+
next_page_url: nil,
|
|
91
|
+
client: client,
|
|
92
|
+
next_page_caller: ->(page_token:) { raise "should not be called" }
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert_nil collection.next_page
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_next_page_returns_nil_when_no_caller
|
|
99
|
+
collection = Calendlyr::Collection.new(
|
|
100
|
+
data: [Calendlyr::Object.new(name: "Alice")],
|
|
101
|
+
count: 1,
|
|
102
|
+
next_page_url: "https://api.calendly.com/scheduled_events?page_token=TOKEN123",
|
|
103
|
+
client: client
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert_nil collection.next_page
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_auto_paginate_yields_all_items_across_pages
|
|
110
|
+
page3_data = [Calendlyr::Object.new(name: "Charlie")]
|
|
111
|
+
page3 = Calendlyr::Collection.new(
|
|
112
|
+
data: page3_data, count: 1, next_page_url: nil, client: client
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
page2_data = [Calendlyr::Object.new(name: "Bob")]
|
|
116
|
+
page2 = Calendlyr::Collection.new(
|
|
117
|
+
data: page2_data, count: 1,
|
|
118
|
+
next_page_url: "https://api.calendly.com/events?page_token=P3",
|
|
119
|
+
client: client,
|
|
120
|
+
next_page_caller: ->(page_token:) { page3 }
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
page1_data = [Calendlyr::Object.new(name: "Alice")]
|
|
124
|
+
page1 = Calendlyr::Collection.new(
|
|
125
|
+
data: page1_data, count: 1,
|
|
126
|
+
next_page_url: "https://api.calendly.com/events?page_token=P2",
|
|
127
|
+
client: client,
|
|
128
|
+
next_page_caller: ->(page_token:) { page2 }
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
result = page1.auto_paginate.to_a
|
|
132
|
+
|
|
133
|
+
assert_equal %w[Alice Bob Charlie], result.map(&:name)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def test_auto_paginate_empty_collection
|
|
137
|
+
collection = Calendlyr::Collection.new(
|
|
138
|
+
data: [], count: 0, next_page_url: nil, client: client
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert_equal [], collection.auto_paginate.to_a
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def test_auto_paginate_lazy_does_not_prefetch
|
|
145
|
+
page2_called = false
|
|
146
|
+
|
|
147
|
+
page2_data = [Calendlyr::Object.new(name: "Bob")]
|
|
148
|
+
page2 = Calendlyr::Collection.new(
|
|
149
|
+
data: page2_data, count: 1, next_page_url: nil, client: client
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
page1_data = [Calendlyr::Object.new(name: "Alice")]
|
|
153
|
+
page1 = Calendlyr::Collection.new(
|
|
154
|
+
data: page1_data, count: 1,
|
|
155
|
+
next_page_url: "https://api.calendly.com/events?page_token=P2",
|
|
156
|
+
client: client,
|
|
157
|
+
next_page_caller: lambda { |page_token:|
|
|
158
|
+
page2_called = true
|
|
159
|
+
page2
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
result = page1.auto_paginate.first(1)
|
|
164
|
+
|
|
165
|
+
assert_equal ["Alice"], result.map(&:name)
|
|
166
|
+
refute page2_called, "Page 2 lambda should NOT have been called"
|
|
167
|
+
end
|
|
168
|
+
end
|