diff --git a/Gemfile b/Gemfile index 38dee9bc4..cf366e79e 100644 --- a/Gemfile +++ b/Gemfile @@ -155,6 +155,9 @@ gem 'stripe' ## to populate db with sample data gem 'faker' +# to consume GraphQL-based APIs +gem 'graphlient' + # Include logrange conditionally in intializer using env variable gem 'lograge', '~> 0.14.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 5dc67bef1..1f32a7cc6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -333,6 +333,14 @@ GEM multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) + graphlient (0.8.0) + faraday (~> 2.0) + graphql-client + graphql (2.3.2) + base64 + graphql-client (0.22.0) + activesupport (>= 3.0) + graphql (>= 1.13.0) groupdate (6.2.1) activesupport (>= 5.2) grpc (1.62.0) @@ -876,6 +884,7 @@ DEPENDENCIES google-cloud-dialogflow-v2 google-cloud-storage google-cloud-translate-v3 + graphlient groupdate grpc haikunator diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb new file mode 100644 index 000000000..f2f8792dc --- /dev/null +++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb @@ -0,0 +1,22 @@ +class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController + before_action :fetch_conversation, only: [:list] + + def list + issues = linear_processor_service.list_issues + render json: issues, status: :ok + end + + private + + def linear_processor_service + Integrations::Linear::ProcessorService.new(account: Current.account, conversation: @conversation) + end + + def permitted_params + params.permit(:conversation_id) + end + + def fetch_conversation + @conversation = Current.account.conversations.find_by!(display_id: permitted_params[:conversation_id]) + end +end diff --git a/app/javascript/dashboard/store/modules/integrations.js b/app/javascript/dashboard/store/modules/integrations.js index 477c02b03..3183afbe3 100644 --- a/app/javascript/dashboard/store/modules/integrations.js +++ b/app/javascript/dashboard/store/modules/integrations.js @@ -21,9 +21,13 @@ const state = { }; const isAValidAppIntegration = integration => { - return ['dialogflow', 'dyte', 'google_translate', 'openai'].includes( - integration.id - ); + return [ + 'dialogflow', + 'dyte', + 'google_translate', + 'openai', + 'linear', + ].includes(integration.id); }; export const getters = { getIntegrations($state) { diff --git a/config/integration/apps.yml b/config/integration/apps.yml index 408c56e1c..58bf07655 100644 --- a/config/integration/apps.yml +++ b/config/integration/apps.yml @@ -159,3 +159,27 @@ openai: }, ] visible_properties: ['api_key', 'label_suggestion'] + dyte: + id: linear + logo: linear.png + i18n_key: linear + action: /linear + hook_type: account + allow_multiple_hooks: false + settings_json_schema: { + "type": "object", + "properties": { + "api_key": { "type": "string" }, + }, + "required": ["api_key"], + "additionalProperties": false, + } + settings_form_schema: [ + { + "label": "API Key", + "type": "text", + "name": "api_key", + "validation": "required", + }, + ] + visible_properties: [] diff --git a/config/locales/en.yml b/config/locales/en.yml index 92c56574d..5db6ef197 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -220,6 +220,9 @@ en: openai: name: "OpenAI" description: "Integrate powerful AI features into Chatwoot by leveraging the GPT models from OpenAI." + linear: + name: "Linear" + description: "Connect your account with Linear to get realtime updates on the status of your conversations." public_portal: search: search_placeholder: Search for article by title or body... diff --git a/config/routes.rb b/config/routes.rb index 658f2c1ac..33f6ce887 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -226,6 +226,11 @@ Rails.application.routes.draw do post :add_participant_to_meeting end end + resource :linear, controller: 'linear', only: [] do + collection do + post :list + end + end end resources :working_hours, only: [:update] diff --git a/lib/integrations/linear/processor_service.rb b/lib/integrations/linear/processor_service.rb new file mode 100644 index 000000000..770c6cf4a --- /dev/null +++ b/lib/integrations/linear/processor_service.rb @@ -0,0 +1,23 @@ +class Integrations::Linear::ProcessorService + pattr_initialize [:account!, :conversation!] + + def create_issue; end + + def link_issue; end + + def list_issues + issues = linear_client.list_issues + issues.map(&:as_json) + end + + private + + def linear_hook + @linear_hook ||= account.hooks.find_by!(app_id: 'linear') + end + + def linear_client + credentials = linear_hook.settings + @linear_client ||= Linear.new(credentials['api_key']) + end +end diff --git a/lib/linear.rb b/lib/linear.rb new file mode 100644 index 000000000..715c74fd6 --- /dev/null +++ b/lib/linear.rb @@ -0,0 +1,39 @@ +require 'graphlient' + +class Linear + BASE_URL = 'https://api.linear.app/graphql'.freeze + + def initialize(api_key) + @api_key = api_key + raise ArgumentError, 'Missing Credentials' if @api_key.blank? + + @client = Graphlient::Client.new( + BASE_URL, + headers: { + 'Authorization' => @api_key, + 'Content-Type' => 'application/json' + } + ) + end + + def list_issues + query = <<~GRAPHQL + query { + issues(first: 5) { + nodes { + id + title + } + } + } + GRAPHQL + + begin + response = @client.query(query) + response.data.issues.nodes + rescue StandardError => e + # return error message + e.message + end + end +end diff --git a/public/dashboard/images/integrations/linear.png b/public/dashboard/images/integrations/linear.png new file mode 100644 index 000000000..a2c578bc1 Binary files /dev/null and b/public/dashboard/images/integrations/linear.png differ