Module: Asana
- Defined in:
- app/services/asana.rb
Overview
Asana import (card #7): paste a task URL, get a card. First use walks through connecting a Personal Access Token, which lives as a 0600 file in .cardinal/ (like the Claude token) — never in the database, never in git.
Defined Under Namespace
Classes: Error
Constant Summary collapse
- API =
"https://app.asana.com/api/1.0".freeze
Class Method Summary collapse
-
.comment!(task_url, text) ⇒ Object
Post text to the task as a comment (an Asana "story").
- .connected? ⇒ Boolean
- .disconnect! ⇒ Object
- .import!(board, url) ⇒ Object
- .request(path, auth, method: :get, body: nil) ⇒ Object
- .save_token!(value) ⇒ Object
-
.task_gid(url) ⇒ Object
Task URLs come in several vintages (/0/
/ , /0/.../f, /1/ /project/ /task/
) — the task gid is the last long digit run. - .token ⇒ Object
- .token_path ⇒ Object
-
.verify!(candidate) ⇒ Object
Cheapest possible "does this token work" — also gives us a name to show.
Class Method Details
.comment!(task_url, text) ⇒ Object
Post text to the task as a comment (an Asana "story").
58 59 60 61 |
# File 'app/services/asana.rb', line 58 def self.comment!(task_url, text) request("/tasks/#{task_gid(task_url)}/stories", token, method: :post, body: { data: { text: text } }) end |
.connected? ⇒ Boolean
15 |
# File 'app/services/asana.rb', line 15 def self.connected? = !!File.size?(token_path) |
.disconnect! ⇒ Object
24 |
# File 'app/services/asana.rb', line 24 def self.disconnect! = FileUtils.rm_f(token_path) |
.import!(board, url) ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'app/services/asana.rb', line 38 def self.import!(board, url) data = request("/tasks/#{task_gid(url)}?opt_fields=name,notes,permalink_url,tags.name", token) permalink = data["permalink_url"].presence || url if (existing = board.cards.find_by(asana_url: permalink)) return existing end inbox = board.columns.inbox.order(:position).first card = board.cards.create!( column: inbox, title: data["name"].presence || "Asana task", asana_url: permalink, tags: Array(data["tags"]).filter_map { |t| t["name"] }.first(5), description: "#{data["notes"].presence || "(no description on the Asana task)"}\n\n---\n_Imported from Asana: #{permalink}_" ) card.log!("status_change", actor: "user", text: "Imported from Asana: #{permalink}") card end |
.request(path, auth, method: :get, body: nil) ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'app/services/asana.rb', line 63 def self.request(path, auth, method: :get, body: nil) uri = URI("#{API}#{path}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.open_timeout = 10 http.read_timeout = 15 headers = { "Authorization" => "Bearer #{auth}" } response = if method == :post http.post(uri.request_uri, body.to_json, headers.merge("Content-Type" => "application/json")) else http.get(uri.request_uri, headers) end unless [200, 201].include?(response.code.to_i) raise Error, "Asana said no (HTTP #{response.code}) — check the token, and that it can see this task" end JSON.parse(response.body)["data"] rescue JSON::ParserError raise Error, "Asana returned something unreadable" rescue SocketError, Net::OpenTimeout, Net::ReadTimeout => e raise Error, "Couldn't reach Asana (#{e.class.name.demodulize}) — check your connection" end |
.save_token!(value) ⇒ Object
18 19 20 21 22 |
# File 'app/services/asana.rb', line 18 def self.save_token!(value) FileUtils.mkdir_p(File.dirname(token_path)) File.write(token_path, value.strip) File.chmod(0o600, token_path) end |
.task_gid(url) ⇒ Object
Task URLs come in several vintages (/0/ /task/
34 35 36 |
# File 'app/services/asana.rb', line 34 def self.task_gid(url) url.to_s.scan(/\d{6,}/).last or raise Error, "That doesn't look like an Asana task URL" end |
.token ⇒ Object
16 |
# File 'app/services/asana.rb', line 16 def self.token = File.read(token_path).strip |
.token_path ⇒ Object
11 12 13 |
# File 'app/services/asana.rb', line 11 def self.token_path Pathname(File.(ENV["CARDINAL_DATA_DIR"].presence || Rails.root.join(".cardinal"))).join("asana-token") end |
.verify!(candidate) ⇒ Object
Cheapest possible "does this token work" — also gives us a name to show.
27 28 29 30 |
# File 'app/services/asana.rb', line 27 def self.verify!(candidate) data = request("/users/me", candidate) data["name"].presence || data["email"].presence || "connected" end |