activeproject 0.1.0 → 0.2.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +4 -2
  3. data/lib/active_project/adapters/base.rb +9 -7
  4. data/lib/active_project/adapters/basecamp/comments.rb +27 -0
  5. data/lib/active_project/adapters/basecamp/connection.rb +49 -0
  6. data/lib/active_project/adapters/basecamp/issues.rb +158 -0
  7. data/lib/active_project/adapters/basecamp/lists.rb +54 -0
  8. data/lib/active_project/adapters/basecamp/projects.rb +110 -0
  9. data/lib/active_project/adapters/basecamp/webhooks.rb +73 -0
  10. data/lib/active_project/adapters/basecamp_adapter.rb +46 -437
  11. data/lib/active_project/adapters/jira/comments.rb +28 -0
  12. data/lib/active_project/adapters/jira/connection.rb +47 -0
  13. data/lib/active_project/adapters/jira/issues.rb +150 -0
  14. data/lib/active_project/adapters/jira/projects.rb +100 -0
  15. data/lib/active_project/adapters/jira/transitions.rb +68 -0
  16. data/lib/active_project/adapters/jira/webhooks.rb +89 -0
  17. data/lib/active_project/adapters/jira_adapter.rb +61 -485
  18. data/lib/active_project/adapters/trello/comments.rb +21 -0
  19. data/lib/active_project/adapters/trello/connection.rb +37 -0
  20. data/lib/active_project/adapters/trello/issues.rb +133 -0
  21. data/lib/active_project/adapters/trello/lists.rb +27 -0
  22. data/lib/active_project/adapters/trello/projects.rb +82 -0
  23. data/lib/active_project/adapters/trello/webhooks.rb +91 -0
  24. data/lib/active_project/adapters/trello_adapter.rb +59 -377
  25. data/lib/active_project/association_proxy.rb +9 -2
  26. data/lib/active_project/configuration.rb +1 -3
  27. data/lib/active_project/configurations/trello_configuration.rb +1 -3
  28. data/lib/active_project/resource_factory.rb +20 -10
  29. data/lib/active_project/resources/issue.rb +0 -3
  30. data/lib/active_project/resources/project.rb +0 -1
  31. data/lib/active_project/version.rb +3 -1
  32. data/lib/activeproject.rb +2 -2
  33. metadata +19 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b69a05303fa485956e8bdf7e56984aa5690df8f4bb7b9119253d760f0940916
4
- data.tar.gz: 45dab21a69a42325b067f01510f47e16f9b8c9ebb312b11793e1b0c447f563a6
3
+ metadata.gz: 9960da80d5e32d0cdfa4fce3ffe824ebf3c36a8710d0a7782dbac3ec7001b863
4
+ data.tar.gz: 1ca92b2e165afbe314b70d970cb42817e5987ae3bfaa5a7d0ba58987999afe45
5
5
  SHA512:
6
- metadata.gz: fcd6726cf79cfa6819ecfe3de62d5502d94317bfbb824a4f5e4c3579b836fa65a06ee408ff69f601d1e9ba47b7d3d45848e326e48608b413a7431b87e208dbff
7
- data.tar.gz: 23191e09bd239c8fc21552c64520de969e70875fbedcb514449dbe1fe4f157cc204b150eb45a9b71ddb0af0edf0774fc4934ba1deb9cafe5202ca64b1245d7e2
6
+ metadata.gz: 46a0f331e8f7e956d643e6070d21dc20abe3825308c0761067df3773b3a20fcea7b94aa9c07f333c0fc794443945419436486b3daec49e4a19b5e473b05b4c6c
7
+ data.tar.gz: d78685242c2ea12a5a522a74e64b8f438d613dd2bb2977c14aca0ed7ba918ef953719f17b23b8cf6400723ea23886f7a63df898a9b804214eab4db635c8fb794
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
- require "bundler/setup"
1
+ # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
3
+ require 'bundler/setup'
4
+
5
+ require 'bundler/gem_tasks'
@@ -18,7 +18,6 @@ module ActiveProject
18
18
  raise NotImplementedError, "#{self.class.name} must implement #find_project"
19
19
  end
20
20
 
21
-
22
21
  # Creates a new project.
23
22
  # @param attributes [Hash] Project attributes (platform-specific).
24
23
  # @return [ActiveProject::Project] The created project object.
@@ -35,7 +34,6 @@ module ActiveProject
35
34
  raise NotImplementedError, "#{self.class.name} does not support #create_list or must implement #create_list"
36
35
  end
37
36
 
38
-
39
37
  # Deletes a project. Use with caution.
40
38
  # @param project_id [String, Integer] The ID or key of the project to delete.
41
39
  # @return [Boolean] true if deletion was successful (or accepted), false otherwise.
@@ -44,7 +42,6 @@ module ActiveProject
44
42
  raise NotImplementedError, "#{self.class.name} does not support #delete_project or must implement it"
45
43
  end
46
44
 
47
-
48
45
  # Lists issues within a specific project.
49
46
  # @param project_id [String, Integer] The ID or key of the project.
50
47
  # @param options [Hash] Optional filtering/pagination options.
@@ -78,6 +75,12 @@ module ActiveProject
78
75
  raise NotImplementedError, "#{self.class.name} must implement #update_issue"
79
76
  end
80
77
 
78
+ # Base implementation of delete_issue that raises NotImplementedError
79
+ # This will be included in the base adapter class and overridden by specific adapters
80
+ def delete_issue(id, context = {})
81
+ raise NotImplementedError, "The #{self.class.name} adapter does not implement delete_issue"
82
+ end
83
+
81
84
  # Adds a comment to an issue.
82
85
  # @param issue_id [String, Integer] The ID or key of the issue.
83
86
  # @param comment_body [String] The text of the comment.
@@ -88,11 +91,11 @@ module ActiveProject
88
91
  end
89
92
 
90
93
  # Verifies the signature of an incoming webhook request, if supported by the platform.
91
- # @param request_body [String] The raw request body.
92
- # @param signature_header [String] The value of the platform-specific signature header (e.g., 'X-Trello-Webhook').
94
+ # @param _request_body [String] The raw request body.
95
+ # @param _signature_header [String] The value of the platform-specific signature header (e.g., 'X-Trello-Webhook').
93
96
  # @return [Boolean] true if the signature is valid or verification is not supported/needed, false otherwise.
94
97
  # @raise [NotImplementedError] if verification is applicable but not implemented by a subclass.
95
- def verify_webhook_signature(request_body, signature_header)
98
+ def verify_webhook_signature(_request_body, _signature_header)
96
99
  # Default implementation assumes no verification needed or supported.
97
100
  # Adapters supporting verification should override this.
98
101
  true
@@ -107,7 +110,6 @@ module ActiveProject
107
110
  raise NotImplementedError, "#{self.class.name} must implement #parse_webhook"
108
111
  end
109
112
 
110
-
111
113
  # Retrieves details for the currently authenticated user.
112
114
  # @return [ActiveProject::Resources::User] The user object.
113
115
  # @raise [ActiveProject::AuthenticationError] if authentication fails.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Basecamp
6
+ module Comments
7
+ # Adds a comment to a To-do in Basecamp.
8
+ # @param todo_id [String, Integer] The ID of the Basecamp To-do.
9
+ # @param comment_body [String] The comment text (HTML).
10
+ # @param context [Hash] Required context: { project_id: '...' }.
11
+ # @return [ActiveProject::Resources::Comment] The created comment resource.
12
+ def add_comment(todo_id, comment_body, context = {})
13
+ project_id = context[:project_id]
14
+ unless project_id
15
+ raise ArgumentError,
16
+ "Missing required context: :project_id must be provided for BasecampAdapter#add_comment"
17
+ end
18
+
19
+ path = "buckets/#{project_id}/recordings/#{todo_id}/comments.json"
20
+ payload = { content: comment_body }.to_json
21
+ comment_data = make_request(:post, path, payload)
22
+ map_comment_data(comment_data, todo_id.to_i)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Basecamp
6
+ module Connection
7
+ BASE_URL_TEMPLATE = "https://3.basecampapi.com/%<account_id>s/"
8
+ # Initializes the Basecamp Adapter.
9
+ # @param config [Configurations::BaseAdapterConfiguration] The configuration object for Basecamp.
10
+ # @raise [ArgumentError] if required configuration options (:account_id, :access_token) are missing.
11
+ def initialize(config:)
12
+ # For now, Basecamp uses the base config. If specific Basecamp options are added,
13
+ # create BasecampConfiguration and check for that type.
14
+ unless config.is_a?(ActiveProject::Configurations::BaseAdapterConfiguration)
15
+ raise ArgumentError, "BasecampAdapter requires a BaseAdapterConfiguration object"
16
+ end
17
+
18
+ @config = config
19
+
20
+ account_id = @config.options[:account_id].to_s # Ensure it's a string
21
+ access_token = @config.options[:access_token]
22
+
23
+ unless account_id && !account_id.empty? && access_token && !access_token.empty?
24
+ raise ArgumentError, "BasecampAdapter configuration requires :account_id and :access_token"
25
+ end
26
+
27
+ @base_url = format(BASE_URL_TEMPLATE, account_id: account_id)
28
+ @connection = initialize_connection
29
+ end
30
+
31
+ private
32
+
33
+ # Initializes the Faraday connection object.
34
+ def initialize_connection
35
+ access_token = @config.options[:access_token]
36
+
37
+ Faraday.new(url: @base_url) do |conn|
38
+ conn.request :authorization, :bearer, access_token
39
+ conn.request :retry
40
+ conn.response :raise_error
41
+ conn.headers["Content-Type"] = "application/json"
42
+ conn.headers["Accept"] = "application/json"
43
+ conn.headers["User-Agent"] = ActiveProject.user_agent
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Basecamp
6
+ module Issues
7
+ # Lists To-dos within a specific project.
8
+ # @param project_id [String, Integer] The ID of the Basecamp project.
9
+ # @param options [Hash] Optional options. Accepts :todolist_id and :page_size.
10
+ # @return [Array<ActiveProject::Resources::Issue>] An array of issue resources.
11
+ def list_issues(project_id, options = {})
12
+ all_todos = []
13
+ todolist_id = options[:todolist_id]
14
+ page_size = options[:page_size] || 50
15
+
16
+ unless todolist_id
17
+ todolist_id = find_first_todolist_id(project_id)
18
+ return [] unless todolist_id
19
+ end
20
+
21
+ path = "buckets/#{project_id}/todolists/#{todolist_id}/todos.json"
22
+ query = {}
23
+ query[:per_page] = page_size if page_size
24
+
25
+ loop do
26
+ response = @connection.get(path, query)
27
+ todos_data = begin
28
+ JSON.parse(response.body)
29
+ rescue StandardError
30
+ []
31
+ end
32
+ break if todos_data.empty?
33
+
34
+ todos_data.each do |todo_data|
35
+ all_todos << map_todo_data(todo_data, project_id)
36
+ end
37
+
38
+ link_header = response.headers["Link"]
39
+ next_url = parse_next_link(link_header)
40
+ break unless next_url
41
+
42
+ path = next_url.sub(@base_url, "").sub(%r{^/}, "")
43
+ query = {} # Clear query as pagination is in the URL now
44
+ end
45
+
46
+ all_todos
47
+ rescue Faraday::Error => e
48
+ handle_faraday_error(e)
49
+ end
50
+
51
+ # Finds a specific To-do by its ID.
52
+ # @param todo_id [String, Integer] The ID of the Basecamp To-do.
53
+ # @param context [Hash] Required context: { project_id: '...' }.
54
+ # @return [ActiveProject::Resources::Issue] The issue resource.
55
+ def find_issue(todo_id, context = {})
56
+ project_id = context[:project_id]
57
+ unless project_id
58
+ raise ArgumentError, "Missing required context: :project_id must be provided for BasecampAdapter#find_issue"
59
+ end
60
+
61
+ path = "buckets/#{project_id}/todos/#{todo_id}.json"
62
+ todo_data = make_request(:get, path)
63
+ map_todo_data(todo_data, project_id)
64
+ end
65
+
66
+ # Creates a new To-do in Basecamp.
67
+ # @param project_id [String, Integer] The ID of the Basecamp project.
68
+ # @param attributes [Hash] To-do attributes. Required: :todolist_id, :title. Optional: :description, :due_on, :assignee_ids.
69
+ # @return [ActiveProject::Resources::Issue] The created issue resource.
70
+ def create_issue(project_id, attributes)
71
+ todolist_id = attributes[:todolist_id]
72
+ title = attributes[:title]
73
+
74
+ unless todolist_id && title && !title.empty?
75
+ raise ArgumentError, "Missing required attributes for Basecamp to-do creation: :todolist_id, :title"
76
+ end
77
+
78
+ path = "buckets/#{project_id}/todolists/#{todolist_id}/todos.json"
79
+
80
+ payload = {
81
+ content: title,
82
+ description: attributes[:description],
83
+ due_on: attributes[:due_on].respond_to?(:strftime) ? attributes[:due_on].strftime("%Y-%m-%d") : attributes[:due_on],
84
+ assignee_ids: attributes[:assignee_ids]
85
+ }.compact
86
+
87
+ todo_data = make_request(:post, path, payload.to_json)
88
+ map_todo_data(todo_data, project_id)
89
+ end
90
+
91
+ # Updates an existing To-do in Basecamp.
92
+ # Handles updates to standard fields via PUT and status changes via POST/DELETE completion endpoints.
93
+ # @param todo_id [String, Integer] The ID of the Basecamp To-do.
94
+ # @param attributes [Hash] Attributes to update (e.g., :title, :description, :status, :assignee_ids, :due_on).
95
+ # @param context [Hash] Required context: { project_id: '...' }.
96
+ # @return [ActiveProject::Resources::Issue] The updated issue resource (fetched after updates).
97
+ def update_issue(todo_id, attributes, context = {})
98
+ project_id = context[:project_id]
99
+ unless project_id
100
+ raise ArgumentError,
101
+ "Missing required context: :project_id must be provided for BasecampAdapter#update_issue"
102
+ end
103
+
104
+ put_payload = {}
105
+ put_payload[:content] = attributes[:title] if attributes.key?(:title)
106
+ put_payload[:description] = attributes[:description] if attributes.key?(:description)
107
+ if attributes.key?(:due_on)
108
+ due_on_val = attributes[:due_on]
109
+ put_payload[:due_on] = due_on_val.respond_to?(:strftime) ? due_on_val.strftime("%Y-%m-%d") : due_on_val
110
+ end
111
+ put_payload[:assignee_ids] = attributes[:assignee_ids] if attributes.key?(:assignee_ids)
112
+
113
+ status_change_required = attributes.key?(:status)
114
+ target_status = attributes[:status] if status_change_required
115
+
116
+ unless !put_payload.empty? || status_change_required
117
+ raise ArgumentError, "No attributes provided to update for BasecampAdapter#update_issue"
118
+ end
119
+
120
+ unless put_payload.empty?
121
+ put_path = "buckets/#{project_id}/todos/#{todo_id}.json"
122
+ make_request(:put, put_path, put_payload.compact.to_json)
123
+ end
124
+
125
+ if status_change_required
126
+ completion_path = "buckets/#{project_id}/todos/#{todo_id}/completion.json"
127
+ begin
128
+ if target_status == :closed
129
+ make_request(:post, completion_path)
130
+ elsif target_status == :open
131
+ make_request(:delete, completion_path)
132
+ end
133
+ rescue NotFoundError
134
+ raise unless target_status == :open
135
+ end
136
+ end
137
+
138
+ find_issue(todo_id, context)
139
+ end
140
+
141
+ # Deletes a To-do in Basecamp.
142
+ # @param todo_id [String, Integer] The ID of the Basecamp To-do to delete.
143
+ # @param context [Hash] Required context: { project_id: '...' }.
144
+ # @return [Boolean] True if successfully deleted.
145
+ def delete_issue(todo_id, context = {})
146
+ project_id = context[:project_id]
147
+ unless project_id
148
+ raise ArgumentError, "Missing required context: :project_id must be provided for BasecampAdapter#delete_issue"
149
+ end
150
+
151
+ path = "buckets/#{project_id}/todos/#{todo_id}.json"
152
+ make_request(:delete, path)
153
+ true
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Basecamp
6
+ module Lists
7
+ # Creates a new Todolist within a project.
8
+ # @param project_id [String, Integer] The ID of the Basecamp project (bucket).
9
+ # @param attributes [Hash] Todolist attributes. Required: :name. Optional: :description.
10
+ # @return [Hash] The raw data hash of the created todolist.
11
+ def create_list(project_id, attributes)
12
+ unless attributes[:name] && !attributes[:name].empty?
13
+ raise ArgumentError, "Missing required attribute for Basecamp todolist creation: :name"
14
+ end
15
+
16
+ project_data = make_request(:get, "projects/#{project_id}.json")
17
+ todoset_dock_entry = project_data&.dig("dock")&.find { |d| d["name"] == "todoset" }
18
+ todoset_url = todoset_dock_entry&.dig("url")
19
+ raise ApiError, "Could not find todoset URL for project #{project_id}" unless todoset_url
20
+
21
+ todoset_id = todoset_url.match(%r{todosets/(\d+)\.json$})&.captures&.first
22
+ raise ApiError, "Could not extract todoset ID from URL: #{todoset_url}" unless todoset_id
23
+
24
+ path = "buckets/#{project_id}/todosets/#{todoset_id}/todolists.json"
25
+ payload = {
26
+ name: attributes[:name],
27
+ description: attributes[:description]
28
+ }.compact
29
+
30
+ make_request(:post, path, payload.to_json)
31
+ end
32
+
33
+ # Finds the ID of the first todolist in a project.
34
+ # @param project_id [String, Integer]
35
+ # @return [String, nil]
36
+ def find_first_todolist_id(project_id)
37
+ project_data = make_request(:get, "projects/#{project_id}.json")
38
+ todoset_dock_entry = project_data&.dig("dock")&.find { |d| d["name"] == "todoset" }
39
+ todoset_url = todoset_dock_entry&.dig("url")
40
+ return nil unless todoset_url
41
+
42
+ todoset_id = todoset_url.match(%r{todosets/(\d+)\.json$})&.captures&.first
43
+ return nil unless todoset_id
44
+
45
+ todolists_url_path = "buckets/#{project_id}/todosets/#{todoset_id}/todolists.json"
46
+ todolists_data = make_request(:get, todolists_url_path)
47
+ todolists_data&.first&.dig("id")
48
+ rescue NotFoundError
49
+ nil
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Basecamp
6
+ module Projects
7
+ # Lists projects accessible by the configured credentials.
8
+ # Handles pagination automatically using the Link header.
9
+ # @return [Array<ActiveProject::Resources::Project>] An array of project resources.
10
+ def list_projects
11
+ all_projects = []
12
+ path = "projects.json"
13
+
14
+ loop do
15
+ response = @connection.get(path)
16
+ projects_data = begin
17
+ JSON.parse(response.body)
18
+ rescue StandardError
19
+ []
20
+ end
21
+ break if projects_data.empty?
22
+
23
+ projects_data.each do |project_data|
24
+ all_projects << Resources::Project.new(self,
25
+ id: project_data["id"],
26
+ key: nil,
27
+ name: project_data["name"],
28
+ adapter_source: :basecamp,
29
+ raw_data: project_data)
30
+ end
31
+
32
+ link_header = response.headers["Link"]
33
+ next_url = parse_next_link(link_header)
34
+ break unless next_url
35
+
36
+ path = next_url.sub(@base_url, "").sub(%r{^/}, "")
37
+ end
38
+
39
+ all_projects
40
+ rescue Faraday::Error => e
41
+ handle_faraday_error(e)
42
+ end
43
+
44
+ # Finds a specific project by its ID.
45
+ # @param project_id [String, Integer] The ID of the Basecamp project.
46
+ # @return [ActiveProject::Resources::Project] The project resource.
47
+ def find_project(project_id)
48
+ path = "projects/#{project_id}.json"
49
+ project_data = make_request(:get, path)
50
+ return nil unless project_data
51
+
52
+ raise NotFoundError, "Basecamp project ID #{project_id} is trashed." if project_data["status"] == "trashed"
53
+
54
+ Resources::Project.new(self,
55
+ id: project_data["id"],
56
+ key: nil,
57
+ name: project_data["name"],
58
+ adapter_source: :basecamp,
59
+ raw_data: project_data)
60
+ end
61
+
62
+ # Creates a new project in Basecamp.
63
+ # @param attributes [Hash] Project attributes. Required: :name. Optional: :description.
64
+ # @return [ActiveProject::Resources::Project] The created project resource.
65
+ def create_project(attributes)
66
+ unless attributes[:name] && !attributes[:name].empty?
67
+ raise ArgumentError, "Missing required attribute for Basecamp project creation: :name"
68
+ end
69
+
70
+ path = "projects.json"
71
+ payload = {
72
+ name: attributes[:name],
73
+ description: attributes[:description]
74
+ }.compact
75
+
76
+ project_data = make_request(:post, path, payload.to_json)
77
+
78
+ Resources::Project.new(self,
79
+ id: project_data["id"],
80
+ key: nil,
81
+ name: project_data["name"],
82
+ adapter_source: :basecamp,
83
+ raw_data: project_data)
84
+ end
85
+
86
+ # Recovers a trashed project in Basecamp.
87
+ # @param project_id [String, Integer] The ID of the project to recover.
88
+ # @return [Boolean] true if recovery was successful (API returns 204).
89
+ def untrash_project(project_id)
90
+ path = "projects/#{project_id}.json"
91
+ make_request(:put, path, { "status": "active" }.to_json)
92
+ true
93
+ end
94
+
95
+ # Archives (trashes) a project in Basecamp.
96
+ # Note: Basecamp API doesn't offer permanent deletion via this endpoint.
97
+ # @param project_id [String, Integer] The ID of the project to trash.
98
+ # @return [Boolean] true if trashing was successful (API returns 204).
99
+ # @raise [NotFoundError] if the project is not found.
100
+ # @raise [AuthenticationError] if credentials lack permission.
101
+ # @raise [ApiError] for other errors.
102
+ def delete_project(project_id)
103
+ path = "projects/#{project_id}.json"
104
+ make_request(:delete, path)
105
+ true
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveProject
4
+ module Adapters
5
+ module Basecamp
6
+ module Webhooks
7
+ # Parses an incoming Basecamp webhook payload.
8
+ # @param request_body [String] The raw JSON request body.
9
+ # @param headers [Hash] Request headers (unused).
10
+ # @return [ActiveProject::WebhookEvent, nil] Parsed event or nil if unhandled.
11
+ def parse_webhook(request_body, _headers = {})
12
+ payload = begin
13
+ JSON.parse(request_body)
14
+ rescue StandardError
15
+ nil
16
+ end
17
+ return nil unless payload.is_a?(Hash)
18
+
19
+ kind = payload["kind"]
20
+ recording = payload["recording"]
21
+ creator = payload["creator"]
22
+ timestamp = begin
23
+ Time.parse(payload["created_at"])
24
+ rescue StandardError
25
+ nil
26
+ end
27
+ return nil unless recording && kind
28
+
29
+ event_type = nil
30
+ object_kind = nil
31
+ event_object_id = recording["id"]
32
+ object_key = nil
33
+ project_id = recording.dig("bucket", "id")
34
+ changes = nil
35
+ object_data = nil
36
+
37
+ case kind
38
+ when /todo_created$/
39
+ event_type = :issue_created
40
+ object_kind = :issue
41
+ when /todo_assignment_changed$/, /todo_completion_changed$/, /todo_content_updated$/, /todo_description_changed$/, /todo_due_on_changed$/
42
+ event_type = :issue_updated
43
+ object_kind = :issue
44
+ when /comment_created$/
45
+ event_type = :comment_added
46
+ object_kind = :comment
47
+ when /comment_content_changed$/
48
+ event_type = :comment_updated
49
+ object_kind = :comment
50
+ else
51
+ return nil
52
+ end
53
+
54
+ WebhookEvent.new(
55
+ event_type: event_type,
56
+ object_kind: object_kind,
57
+ event_object_id: event_object_id,
58
+ object_key: object_key,
59
+ project_id: project_id,
60
+ actor: map_user_data(creator),
61
+ timestamp: timestamp,
62
+ adapter_source: :basecamp,
63
+ changes: changes,
64
+ object_data: object_data,
65
+ raw_data: payload
66
+ )
67
+ rescue JSON::ParserError
68
+ nil
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end