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.
- checksums.yaml +4 -4
- data/Rakefile +4 -2
- data/lib/active_project/adapters/base.rb +9 -7
- data/lib/active_project/adapters/basecamp/comments.rb +27 -0
- data/lib/active_project/adapters/basecamp/connection.rb +49 -0
- data/lib/active_project/adapters/basecamp/issues.rb +158 -0
- data/lib/active_project/adapters/basecamp/lists.rb +54 -0
- data/lib/active_project/adapters/basecamp/projects.rb +110 -0
- data/lib/active_project/adapters/basecamp/webhooks.rb +73 -0
- data/lib/active_project/adapters/basecamp_adapter.rb +46 -437
- data/lib/active_project/adapters/jira/comments.rb +28 -0
- data/lib/active_project/adapters/jira/connection.rb +47 -0
- data/lib/active_project/adapters/jira/issues.rb +150 -0
- data/lib/active_project/adapters/jira/projects.rb +100 -0
- data/lib/active_project/adapters/jira/transitions.rb +68 -0
- data/lib/active_project/adapters/jira/webhooks.rb +89 -0
- data/lib/active_project/adapters/jira_adapter.rb +61 -485
- data/lib/active_project/adapters/trello/comments.rb +21 -0
- data/lib/active_project/adapters/trello/connection.rb +37 -0
- data/lib/active_project/adapters/trello/issues.rb +133 -0
- data/lib/active_project/adapters/trello/lists.rb +27 -0
- data/lib/active_project/adapters/trello/projects.rb +82 -0
- data/lib/active_project/adapters/trello/webhooks.rb +91 -0
- data/lib/active_project/adapters/trello_adapter.rb +59 -377
- data/lib/active_project/association_proxy.rb +9 -2
- data/lib/active_project/configuration.rb +1 -3
- data/lib/active_project/configurations/trello_configuration.rb +1 -3
- data/lib/active_project/resource_factory.rb +20 -10
- data/lib/active_project/resources/issue.rb +0 -3
- data/lib/active_project/resources/project.rb +0 -1
- data/lib/active_project/version.rb +3 -1
- data/lib/activeproject.rb +2 -2
- metadata +19 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9960da80d5e32d0cdfa4fce3ffe824ebf3c36a8710d0a7782dbac3ec7001b863
|
4
|
+
data.tar.gz: 1ca92b2e165afbe314b70d970cb42817e5987ae3bfaa5a7d0ba58987999afe45
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 46a0f331e8f7e956d643e6070d21dc20abe3825308c0761067df3773b3a20fcea7b94aa9c07f333c0fc794443945419436486b3daec49e4a19b5e473b05b4c6c
|
7
|
+
data.tar.gz: d78685242c2ea12a5a522a74e64b8f438d613dd2bb2977c14aca0ed7ba918ef953719f17b23b8cf6400723ea23886f7a63df898a9b804214eab4db635c8fb794
|
data/Rakefile
CHANGED
@@ -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
|
92
|
-
# @param
|
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(
|
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
|