diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index daadb4c6..982bfd3b 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -26,5 +26,5 @@ jobs: with: ruby-version: 3.0 bundler-cache: true # runs 'bundle install' and caches installed gems automatically - - name: Run rubocop + - name: Generate docs run: bundle exec rubocop --config .rubocop.yml diff --git a/lib/nylas.rb b/lib/nylas.rb index 74493af3..8010ba5e 100644 --- a/lib/nylas.rb +++ b/lib/nylas.rb @@ -32,8 +32,13 @@ require_relative "nylas/resources/calendars" require_relative "nylas/resources/connectors" require_relative "nylas/resources/credentials" +require_relative "nylas/resources/drafts" require_relative "nylas/resources/events" require_relative "nylas/resources/grants" require_relative "nylas/resources/messages" +require_relative "nylas/resources/smart_compose" +require_relative "nylas/resources/threads" require_relative "nylas/resources/redirect_uris" require_relative "nylas/resources/webhooks" + +require_relative "nylas/utils/file_utils" diff --git a/lib/nylas/client.rb b/lib/nylas/client.rb index 00303569..45045e12 100644 --- a/lib/nylas/client.rb +++ b/lib/nylas/client.rb @@ -33,6 +33,13 @@ def applications Applications.new(self) end + # The auth resources for your Nylas application. + # + # @return [Nylas::Auth] Auth resources for your Nylas application. + def auth + Auth.new(self) + end + # The calendar resources for your Nylas application. # # @return [Nylas::Calendars] Calendar resources for your Nylas application. @@ -47,6 +54,13 @@ def connectors Connectors.new(self) end + # The draft resources for your Nylas application. + # + # @return [Nylas::Drafts] Draft resources for your Nylas application. + def drafts + Drafts.new(self) + end + # The event resources for your Nylas application. # # @return [Nylas::Events] Event resources for your Nylas application @@ -61,11 +75,11 @@ def messages Messages.new(self) end - # The auth resources for your Nylas application. + # The thread resources for your Nylas application. # - # @return [Nylas::Auth] Auth resources for your Nylas application. - def auth - Auth.new(self) + # @return [Nylas::Threads] Thread resources for your Nylas application. + def threads + Threads.new(self) end # The webhook resources for your Nylas application. diff --git a/lib/nylas/handler/api_operations.rb b/lib/nylas/handler/api_operations.rb index 209cc6b6..e3ed406f 100644 --- a/lib/nylas/handler/api_operations.rb +++ b/lib/nylas/handler/api_operations.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative "http_client" + module Nylas # Allows resources to perform API operations on the Nylas API endpoints without exposing the HTTP # client to the end user. diff --git a/lib/nylas/handler/http_client.rb b/lib/nylas/handler/http_client.rb index d2f9eea4..cfc95cf7 100644 --- a/lib/nylas/handler/http_client.rb +++ b/lib/nylas/handler/http_client.rb @@ -77,17 +77,21 @@ def build_request( url = path url = add_query_params_to_url(url, query) resulting_headers = default_headers.merge(headers).merge(auth_header(api_key)) - serialized_payload = payload&.to_json + if !payload.nil? && !payload["multipart"] + payload = payload&.to_json + resulting_headers["Content-type"] = "application/json" + elsif !payload.nil? && payload["multipart"] + payload.delete("multipart") + end - { method: method, url: url, payload: serialized_payload, headers: resulting_headers, timeout: timeout } + { method: method, url: url, payload: payload, headers: resulting_headers, timeout: timeout } end # Sets the default headers for API requests. def default_headers @default_headers ||= { "X-Nylas-API-Wrapper" => "ruby", - "User-Agent" => "Nylas Ruby SDK #{Nylas::VERSION} - #{RUBY_VERSION}", - "Content-type" => "application/json" + "User-Agent" => "Nylas Ruby SDK #{Nylas::VERSION} - #{RUBY_VERSION}" } end @@ -141,11 +145,11 @@ def error_hash_to_exception(response, status_code, path) NylasOAuthError.new(response[:error], response[:error_description], response[:error_uri], response[:error_code], status_code) else - throw_error(response) + throw_error(response, status_code) end end - def throw_error(response) + def throw_error(response, status_code) error_obj = response[:error] provider_error = error_obj.fetch(:provider_error, nil) @@ -157,7 +161,7 @@ def throw_error(response) # # @return [String] Processed URL, including query params. def add_query_params_to_url(url, query) - unless query.empty? + unless query.nil? || query.empty? uri = URI.parse(url) query = custom_params(query) params = URI.decode_www_form(uri.query || "") + query.to_a diff --git a/lib/nylas/resources/drafts.rb b/lib/nylas/resources/drafts.rb new file mode 100644 index 00000000..654dad60 --- /dev/null +++ b/lib/nylas/resources/drafts.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require_relative "resource" +require_relative "../handler/api_operations" +require_relative "../utils/file_utils" + +module Nylas + # Nylas Drafts API + class Drafts < Resource + include ApiOperations::Get + include ApiOperations::Post + include ApiOperations::Put + include ApiOperations::Delete + + # Return all drafts. + # + # @param identifier [String] Grant ID or email account to query. + # @param query_params [Hash, nil] Query params to pass to the request. + # @return [Array(Array(Hash), String)] The list of drafts and API Request ID. + def list(identifier:, query_params: nil) + get( + path: "#{api_uri}/v3/grants/#{identifier}/drafts", + query_params: query_params + ) + end + + # Return an draft. + # + # @param identifier [String] Grant ID or email account to query. + # @param draft_id [String] The id of the draft to return. + # @return [Array(Hash, String)] The draft and API request ID. + def find(identifier:, draft_id:) + get( + path: "#{api_uri}/v3/grants/#{identifier}/drafts/#{draft_id}" + ) + end + + # Create an draft. + # + # @param identifier [String] Grant ID or email account in which to create the draft. + # @param request_body [Hash] The values to create the message with. + # If you're attaching files, you must pass an array of [File] objects, or + # you can use {FileUtils::attach_file_request_builder} to build each object attach. + # @return [Array(Hash, String)] The created draft and API Request ID. + def create(identifier:, request_body:) + form_body, opened_files = FileUtils.build_form_request(request_body) + response = post( + path: "#{api_uri}/v3/grants/#{identifier}/drafts", + request_body: form_body + ) + + opened_files.each(&:close) + + response + end + + # Update an draft. + # + # @param identifier [String] Grant ID or email account in which to update the draft. + # @param draft_id [String] The id of the draft to update. + # @param request_body [Hash] The values to create the message with. + # If you're attaching files, you must pass an array of [File] objects, or + # you can use {FileUtils::attach_file_request_builder} to build each object attach. + # @return [Array(Hash, String)] The updated draft and API Request ID. + def update(identifier:, draft_id:, request_body:) + form_body, opened_files = FileUtils.build_form_request(request_body) + + response = put( + path: "#{api_uri}/v3/grants/#{identifier}/drafts/#{draft_id}", + request_body: form_body + ) + + opened_files.each(&:close) + + response + end + + # Delete an draft. + # + # @param identifier [String] Grant ID or email account from which to delete an object. + # @param draft_id [String] The id of the draft to delete. + # @return [Array(TrueClass, String)] True and the API Request ID for the delete operation. + def destroy(identifier:, draft_id:) + _, request_id = delete( + path: "#{api_uri}/v3/grants/#{identifier}/drafts/#{draft_id}" + ) + + [true, request_id] + end + end +end diff --git a/lib/nylas/resources/messages.rb b/lib/nylas/resources/messages.rb index c3d7fed7..976895b0 100644 --- a/lib/nylas/resources/messages.rb +++ b/lib/nylas/resources/messages.rb @@ -1,15 +1,27 @@ # frozen_string_literal: true require_relative "resource" +require_relative "smart_compose" require_relative "../handler/api_operations" +require_relative "../utils/file_utils" module Nylas # Nylas Messages API class Messages < Resource include ApiOperations::Get + include ApiOperations::Post include ApiOperations::Put include ApiOperations::Delete + attr_reader :smart_compose + + # Initializes the messages resource. + # @param sdk_instance [Nylas::API] The API instance to which the resource is bound. + def initialize(sdk_instance) + super(sdk_instance) + @smart_compose = SmartCompose.new(sdk_instance) + end + # Return all messages. # # @param identifier [String] Grant ID or email account to query. @@ -26,7 +38,6 @@ def list(identifier:, query_params: nil) # # @param identifier [String] Grant ID or email account to query. # @param message_id [String] The id of the message to return. - # Use "primary" to refer to the primary message associated with grant. # @return [Array(Hash, String)] The message and API request ID. def find(identifier:, message_id:) get( @@ -38,7 +49,6 @@ def find(identifier:, message_id:) # # @param identifier [String] Grant ID or email account in which to update an object. # @param message_id [String] The id of the message to update. - # Use "primary" to refer to the primary message associated with grant. # @param request_body [Hash] The values to update the message with # @return [Array(Hash, String)] The updated message and API Request ID. def update(identifier:, message_id:, request_body:) @@ -52,7 +62,6 @@ def update(identifier:, message_id:, request_body:) # # @param identifier [String] Grant ID or email account from which to delete an object. # @param message_id [String] The id of the message to delete. - # Use "primary" to refer to the primary message associated with grant. # @return [Array(TrueClass, String)] True and the API Request ID for the delete operation. def destroy(identifier:, message_id:) _, request_id = delete( @@ -61,5 +70,57 @@ def destroy(identifier:, message_id:) [true, request_id] end + + # Send a message. + # + # @param identifier [String] Grant ID or email account from which to delete an object. + # @param request_body [Hash] The values to create the message with. + # If you're attaching files, you must pass an array of [File] objects, or + # you can use {FileUtils::attach_file_request_builder} to build each object attach. + # @return [Array(Hash, String)] The sent message and the API Request ID. + def send(identifier:, request_body:) + form_body, opened_files = FileUtils.build_form_request(request_body) + + response = post( + path: "#{api_uri}/v3/grants/#{identifier}/messages/send", + request_body: form_body + ) + + opened_files.each(&:close) + + response + end + + # Retrieve your scheduled messages. + # + # @param identifier [String] Grant ID or email account from which to find the scheduled message from. + # @param schedule_id [String] The id of the scheduled message to stop. + # @return [Array(Hash, String)] The list of scheduled messages and the API Request ID. + def list_scheduled_messages(identifier:, schedule_id:) + get( + path: "#{api_uri}/v3/grants/#{identifier}/messages/schedules/#{schedule_id}" + ) + end + + # Retrieve your scheduled messages. + # + # @param identifier [String] Grant ID or email account from which to list the scheduled messages from. + # @return [Array(Hash, String)] The scheduled message and the API Request ID. + def find_scheduled_messages(identifier:) + get( + path: "#{api_uri}/v3/grants/#{identifier}/messages/schedules" + ) + end + + # Stop a scheduled message. + # + # @param identifier [String] Grant ID or email account from which to list the scheduled messages from. + # @param schedule_id [String] The id of the scheduled message to stop.. + # @return [Array(Hash, String)] The scheduled message and the API Request ID. + def stop_scheduled_messages(identifier:, schedule_id:) + delete( + path: "#{api_uri}/v3/grants/#{identifier}/messages/schedules/#{schedule_id}" + ) + end end end diff --git a/lib/nylas/resources/smart_compose.rb b/lib/nylas/resources/smart_compose.rb new file mode 100644 index 00000000..5c3e95c2 --- /dev/null +++ b/lib/nylas/resources/smart_compose.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative "resource" +require_relative "../handler/api_operations" + +module Nylas + # Nylas Smart Compose API + class SmartCompose < Resource + include ApiOperations::Post + + # Compose a message. + # + # @param identifier [String] Grant ID or email account to generate a message suggestion for. + # @param request_body [Hash] The prompt that smart compose will use to generate a message suggestion. + # @return [Array(Hash, String)] The generated message and API Request ID. + def compose_message(identifier:, request_body:) + post( + path: "#{api_uri}/v3/grants/#{identifier}/messages/smart-compose", + request_body: request_body + ) + end + + # Compose a message reply. + # + # @param identifier [String] Grant ID or email account to generate a message suggestion for. + # @param message_id [String] The id of the message to reply to. + # @param request_body [Hash] The prompt that smart compose will use to generate a message reply suggestion. + # @return [Array(Hash, String)] The generated message reply and API Request ID. + def compose_message_reply(identifier:, message_id:, request_body:) + post( + path: "#{api_uri}/v3/grants/#{identifier}/messages/#{message_id}/smart-compose", + request_body: request_body + ) + end + end +end diff --git a/lib/nylas/resources/threads.rb b/lib/nylas/resources/threads.rb new file mode 100644 index 00000000..cfcea5d1 --- /dev/null +++ b/lib/nylas/resources/threads.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require_relative "resource" +require_relative "../handler/api_operations" + +module Nylas + # Nylas Threads API + class Threads < Resource + include ApiOperations::Get + include ApiOperations::Put + include ApiOperations::Delete + + # Return all threads. + # + # @param identifier [String] Grant ID or email account to query. + # @param query_params [Hash] Query params to pass to the request. + # @return [Array(Array(Hash), String)] The list of threads and API Request ID. + def list(identifier:, query_params: nil) + get( + path: "#{api_uri}/v3/grants/#{identifier}/threads", + query_params: query_params + ) + end + + # Return an thread. + # + # @param identifier [String] Grant ID or email account to query. + # @param thread_id [String] The id of the thread to return. + # @return [Array(Hash, String)] The thread and API request ID. + def find(identifier:, thread_id:) + get( + path: "#{api_uri}/v3/grants/#{identifier}/threads/#{thread_id}" + ) + end + + # Update an thread. + # + # @param identifier [String] Grant ID or email account in which to update the thread. + # @param thread_id [String] The id of the thread to update. + # @param request_body [Hash] The values to update the thread with + # @return [Array(Hash, String)] The updated thread and API Request ID. + def update(identifier:, thread_id:, request_body:) + put( + path: "#{api_uri}/v3/grants/#{identifier}/threads/#{thread_id}", + request_body: request_body + ) + end + + # Delete an thread. + # + # @param identifier [String] Grant ID or email account from which to delete the thread. + # @param thread_id [String] The id of the thread to delete. + # @return [Array(TrueClass, String)] True and the API Request ID for the delete operation. + def destroy(identifier:, thread_id:) + _, request_id = delete( + path: "#{api_uri}/v3/grants/#{identifier}/threads/#{thread_id}" + ) + + [true, request_id] + end + end +end diff --git a/lib/nylas/utils/file_utils.rb b/lib/nylas/utils/file_utils.rb new file mode 100644 index 00000000..75b15d66 --- /dev/null +++ b/lib/nylas/utils/file_utils.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "mime/types" + +module Nylas + # A collection of file-related utilities. + module FileUtils + # Build a form request for the API. + # @param request_body The values to create the message with. + # @return The form data to send to the API and the opened files. + # @!visibility private + def self.build_form_request(request_body) + attachments = request_body.delete(:attachments) || request_body.delete("attachments") || [] + message_payload = request_body.to_json + + # Prepare the data to return + form_data = {} + opened_files = [] + + attachments.each_with_index do |attachment, index| + file = attachment[:content] || attachment["content"] + form_data.merge!({ "file#{index}" => file }) + opened_files << file + end + + form_data.merge!({ "multipart" => true, "message" => message_payload }) + + [form_data, opened_files] + end + + # Build the request to attach a file to a message/draft object. + # @param file_path [String] The path to the file to attach. + # @return [Hash] The request that will attach the file to the message/draft + def self.attach_file_request_builder(file_path) + filename = File.basename(file_path) + content_type = MIME::Types.type_for(file_path).first.to_s + content_type = "application/octet-stream" if content_type.empty? + size = File.size(file_path) + content = File.new(file_path, "rb") + + { + filename: filename, + content_type: content_type, + size: size, + content: content + } + end + end +end diff --git a/nylas.gemspec b/nylas.gemspec index 18e3bd99..897570a3 100644 --- a/nylas.gemspec +++ b/nylas.gemspec @@ -11,6 +11,7 @@ Gem::Specification.new do |gem| gem.license = "MIT" # Runtime dependencies + gem.add_runtime_dependency "mime-types", "~> 3.5", ">= 3.5.1" gem.add_runtime_dependency "rest-client", "~> 2.1", "< 3.0" gem.add_runtime_dependency "yajl-ruby", "~> 1.4.3", ">= 1.2.1"