feat(linear): Auto link Linear issues from private notes (#14405)
When an agent pastes a Linear issue URL into a private note on a
conversation, Chatwoot now links the issue to the conversation
automatically — no need to click "Link to Linear issue" first. The
standard activity message ("X linked Linear issue ABC-123") is posted
just like a manual link.
Fixes
[CW-7032](https://linear.app/chatwoot/issue/CW-7032/if-someone-post-a-linear-url-in-the-private-notes-automatically-link)
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
co-authored by
Muhsin
Sojan Jose
parent
58fdd20625
commit
71cc5168be
@@ -0,0 +1,93 @@
|
||||
class Integrations::Linear::AutoLinkService
|
||||
pattr_initialize [:account!, :message!]
|
||||
|
||||
LINEAR_URL_REGEX = %r{https?://linear\.app/[^/\s]+/issue/[A-Z][A-Z0-9_]+-\d+(?:/[^\s)]*)?}
|
||||
IDENTIFIER_REGEX = %r{/issue/([A-Z][A-Z0-9_]+-\d+)}i
|
||||
WORKSPACE_REGEX = %r{//linear\.app/([^/\s]+)/}i
|
||||
|
||||
def perform
|
||||
return unless valid_message?
|
||||
|
||||
attempt_link
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_message?
|
||||
message.private? && message.content.present? && message.sender.is_a?(User)
|
||||
end
|
||||
|
||||
def attempt_link
|
||||
linear_url = message.content[LINEAR_URL_REGEX]
|
||||
return if linear_url.blank?
|
||||
|
||||
identifier = linear_url[IDENTIFIER_REGEX, 1]&.upcase
|
||||
workspace = linear_url[WORKSPACE_REGEX, 1]&.downcase
|
||||
return if identifier.blank? || workspace.blank? || already_linked?(identifier)
|
||||
|
||||
finalize_link(workspace, identifier)
|
||||
end
|
||||
|
||||
def finalize_link(workspace, identifier)
|
||||
node_id = resolve_node_id(workspace, identifier)
|
||||
return if node_id.blank?
|
||||
return unless link_to_linear(node_id, identifier)
|
||||
|
||||
post_activity_message(identifier)
|
||||
end
|
||||
|
||||
def already_linked?(identifier)
|
||||
response = processor.linked_issues(conversation_link)
|
||||
return false if response[:error]
|
||||
|
||||
response[:data].any? { |attachment| attachment.dig('issue', 'identifier') == identifier }
|
||||
end
|
||||
|
||||
def resolve_node_id(workspace, identifier)
|
||||
response = processor.search_issue(identifier)
|
||||
return if response[:error]
|
||||
|
||||
node = response[:data].find do |issue|
|
||||
issue['identifier'] == identifier && node_workspace(issue) == workspace
|
||||
end
|
||||
node && node['id']
|
||||
end
|
||||
|
||||
def node_workspace(node)
|
||||
node['url']&.match(WORKSPACE_REGEX)&.[](1)&.downcase
|
||||
end
|
||||
|
||||
def link_to_linear(node_id, identifier)
|
||||
response = processor.link_issue(conversation_link, node_id, attachment_title, message.sender)
|
||||
if response[:error].present?
|
||||
Rails.logger.warn("[Linear::AutoLinkService] link_issue failed for #{identifier}: #{response[:error]}")
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
def attachment_title
|
||||
I18n.t(
|
||||
'integration_apps.linear.attachment_link_title',
|
||||
conversation_id: message.conversation.display_id,
|
||||
name: message.conversation.contact&.name
|
||||
)
|
||||
end
|
||||
|
||||
def post_activity_message(identifier)
|
||||
Linear::ActivityMessageService.new(
|
||||
conversation: message.conversation,
|
||||
action_type: :issue_linked,
|
||||
user: message.sender,
|
||||
issue_data: { id: identifier }
|
||||
).perform
|
||||
end
|
||||
|
||||
def conversation_link
|
||||
"#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{message.account_id}/conversations/#{message.conversation.display_id}"
|
||||
end
|
||||
|
||||
def processor
|
||||
@processor ||= Integrations::Linear::ProcessorService.new(account: account)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user