Files
71cc5168be 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>
2026-05-12 13:03:40 +04:00

94 lines
2.6 KiB
Ruby

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