chat_sdk-linear 0.4.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 610c576e330ed052f595520e180085ee02ee57ff6f38beef2d3a5de4ad078d44
4
+ data.tar.gz: e52b81ffe0a6eb62ae36f503f04812b54be7dcd381ef8182edec126fe580faf3
5
+ SHA512:
6
+ metadata.gz: 02bb007d06f983ec8ba7b354e029dd40fa66bcf943087804663d0c9d25115f12a2c8791544713924dfd29d32d7e92a7051ddd357e903286cb7033c912838d7cf
7
+ data.tar.gz: 9fa10d981d2c67703271b34ffe2a898fd5a7559f1ff20eb9d012f04f74b0d9934f00836a08d797dad9ab1cf40f00d16983c66e8c4e5c136ed6b89f872d7a6e76
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "rack/utils"
5
+
6
+ module ChatSDK
7
+ module Linear
8
+ class Adapter < ChatSDK::Adapter::Base
9
+ capabilities :reactions
10
+
11
+ attr_reader :client
12
+
13
+ def initialize(api_key: nil, webhook_secret: nil, bot_username: nil)
14
+ @api_key = api_key || ENV["LINEAR_API_KEY"]
15
+ @webhook_secret = webhook_secret || ENV["LINEAR_WEBHOOK_SECRET"]
16
+ @bot_username = bot_username || ENV["LINEAR_BOT_USERNAME"]
17
+ raise ChatSDK::ConfigurationError, "Linear api_key required" unless @api_key
18
+ raise ChatSDK::ConfigurationError, "Linear webhook_secret required" unless @webhook_secret
19
+
20
+ @client = ApiClient.new(@api_key)
21
+ end
22
+
23
+ def name
24
+ :linear
25
+ end
26
+
27
+ # Inbound
28
+ def verify_request!(rack_request)
29
+ body = rack_request.body.read
30
+ rack_request.body.rewind
31
+
32
+ signature = rack_request.get_header("HTTP_LINEAR_SIGNATURE")
33
+
34
+ unless signature
35
+ raise ChatSDK::SignatureVerificationError, "Missing Linear signature header"
36
+ end
37
+
38
+ expected = OpenSSL::HMAC.hexdigest("SHA256", @webhook_secret, body)
39
+
40
+ unless Rack::Utils.secure_compare(signature, expected)
41
+ raise ChatSDK::SignatureVerificationError, "Invalid Linear signature"
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ def ack_response(_rack_request)
48
+ nil
49
+ end
50
+
51
+ def parse_events(rack_request)
52
+ payload = read_json_body(rack_request)
53
+ EventParser.parse(payload, bot_username: @bot_username)
54
+ rescue JSON::ParserError
55
+ []
56
+ end
57
+
58
+ # Outbound
59
+ def post_message(channel_id:, message:, thread_id: nil)
60
+ msg = ChatSDK::PostableMessage.from(message)
61
+ text = msg.text || msg.card&.fallback_text || ""
62
+
63
+ issue_id = channel_id
64
+ parent_id = nil
65
+
66
+ if thread_id&.include?(":c:")
67
+ parts = thread_id.split(":c:")
68
+ parent_id = parts.last
69
+ end
70
+
71
+ result = @client.create_comment(issue_id: issue_id, body: text, parent_id: parent_id)
72
+ comment = result.dig("data", "commentCreate", "comment") || {}
73
+ comment_id = comment["id"]
74
+ user = comment["user"] || {}
75
+
76
+ ChatSDK::Message.new(
77
+ id: comment_id,
78
+ text: text,
79
+ author: ChatSDK::Author.new(
80
+ id: user["id"] || "bot",
81
+ name: user["name"] || "bot",
82
+ platform: :linear,
83
+ bot: true
84
+ ),
85
+ thread_id: "linear:#{issue_id}:c:#{comment_id}",
86
+ channel_id: channel_id,
87
+ platform: :linear,
88
+ raw: result
89
+ )
90
+ end
91
+
92
+ def add_reaction(channel_id:, message_id:, emoji:) # rubocop:disable Lint/UnusedMethodArgument
93
+ @client.create_reaction(comment_id: message_id, emoji: emoji)
94
+ end
95
+
96
+ def remove_reaction(channel_id:, message_id:, emoji:) # rubocop:disable Lint/UnusedMethodArgument
97
+ @client.delete_reaction(comment_id: message_id, emoji: emoji)
98
+ end
99
+
100
+ def mention(user_id)
101
+ "@#{user_id}"
102
+ end
103
+
104
+ def render(postable_message)
105
+ postable_message.text
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Linear
5
+ class ApiClient < ChatSDK::ApiClient::Base
6
+ BASE_URL = "https://api.linear.app"
7
+
8
+ def initialize(api_key)
9
+ @api_key = api_key
10
+ end
11
+
12
+ def create_comment(issue_id:, body:, parent_id: nil)
13
+ query = <<~GQL
14
+ mutation CommentCreate($input: CommentCreateInput!) {
15
+ commentCreate(input: $input) {
16
+ success
17
+ comment {
18
+ id
19
+ body
20
+ user { id name }
21
+ }
22
+ }
23
+ }
24
+ GQL
25
+ input = {issueId: issue_id, body: body}
26
+ input[:parentId] = parent_id if parent_id
27
+ graphql(query, {input: input})
28
+ end
29
+
30
+ def create_reaction(comment_id:, emoji:)
31
+ query = <<~GQL
32
+ mutation($input: ReactionCreateInput!) {
33
+ reactionCreate(input: $input) {
34
+ success
35
+ }
36
+ }
37
+ GQL
38
+ graphql(query, {input: {commentId: comment_id, emoji: emoji}})
39
+ end
40
+
41
+ def delete_reaction(comment_id:, emoji:)
42
+ query = <<~GQL
43
+ mutation($input: ReactionCreateInput!) {
44
+ reactionDelete(input: $input) {
45
+ success
46
+ }
47
+ }
48
+ GQL
49
+ graphql(query, {input: {commentId: comment_id, emoji: emoji}})
50
+ end
51
+
52
+ private
53
+
54
+ def base_url
55
+ BASE_URL
56
+ end
57
+
58
+ def adapter_name
59
+ :linear
60
+ end
61
+
62
+ def configure_auth(faraday)
63
+ faraday.headers["Authorization"] = @api_key.to_s
64
+ end
65
+
66
+ def extract_error_message(response)
67
+ body = response.body
68
+ return response.status.to_s unless body.is_a?(Hash)
69
+
70
+ body.dig("errors", 0, "message") || body.dig("error") || response.status.to_s
71
+ end
72
+
73
+ def extract_success_body(response)
74
+ response.body
75
+ end
76
+
77
+ def graphql(query, variables = {})
78
+ request(:post, "/graphql", {query: query, variables: variables})
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChatSDK
4
+ module Linear
5
+ class EventParser
6
+ class << self
7
+ def parse(payload, bot_username: nil)
8
+ return [] unless payload.is_a?(Hash)
9
+ return [] unless payload["type"] == "Comment" && payload["action"] == "create"
10
+
11
+ data = payload["data"] || {}
12
+ comment_id = data["id"]
13
+ body = data["body"] || ""
14
+ issue_id = data["issueId"] || data.dig("issue", "id")
15
+ user = data["user"] || {}
16
+ user_id = user["id"]
17
+ user_name = user["name"]
18
+
19
+ return [] if bot_username && user_name == bot_username
20
+
21
+ thread_id = "linear:#{issue_id}:c:#{comment_id}"
22
+
23
+ author = ChatSDK::Author.new(
24
+ id: user_id || "unknown",
25
+ name: user_name || "unknown",
26
+ platform: :linear,
27
+ bot: false
28
+ )
29
+
30
+ msg = ChatSDK::Message.new(
31
+ id: comment_id&.to_s,
32
+ text: body,
33
+ author: author,
34
+ thread_id: thread_id,
35
+ channel_id: issue_id,
36
+ platform: :linear,
37
+ raw: data
38
+ )
39
+
40
+ [
41
+ ChatSDK::Events::Mention.new(
42
+ message: msg,
43
+ thread_id: thread_id,
44
+ channel_id: issue_id,
45
+ platform: :linear,
46
+ adapter_name: :linear,
47
+ raw: data
48
+ )
49
+ ]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "chat_sdk"
4
+ require "faraday"
5
+ require "faraday/net_http"
6
+ require "zeitwerk"
7
+
8
+ module ChatSDK
9
+ module Linear
10
+ class << self
11
+ def loader
12
+ @loader ||= begin
13
+ loader = Zeitwerk::Loader.new
14
+ loader.tag = "chat_sdk-linear"
15
+ loader.inflector.inflect("chat_sdk" => "ChatSDK", "linear" => "Linear")
16
+ loader.push_dir("#{__dir__}/linear", namespace: ChatSDK::Linear)
17
+ loader.setup
18
+ loader
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ ChatSDK::Linear.loader
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: chat_sdk-linear
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Quentin Rousseau
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: chat_sdk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ description: Linear issue comment adapter for the ChatSDK framework using GraphQL
41
+ email:
42
+ - quentin@rootly.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - lib/chat_sdk/linear.rb
48
+ - lib/chat_sdk/linear/adapter.rb
49
+ - lib/chat_sdk/linear/api_client.rb
50
+ - lib/chat_sdk/linear/event_parser.rb
51
+ homepage: https://github.com/rootlyhq/chat-sdk
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ rubygems_mfa_required: 'true'
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 3.2.0
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 4.0.16
71
+ specification_version: 4
72
+ summary: Linear adapter for ChatSDK
73
+ test_files: []