fathom-ruby 1.0.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 +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +59 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +38 -0
- data/Gemfile +19 -0
- data/LICENSE +22 -0
- data/README.md +509 -0
- data/Rakefile +10 -0
- data/TESTING.md +123 -0
- data/fathom-ruby.gemspec +36 -0
- data/lib/fathom/client.rb +138 -0
- data/lib/fathom/errors.rb +40 -0
- data/lib/fathom/rate_limiter.rb +44 -0
- data/lib/fathom/resource.rb +110 -0
- data/lib/fathom/resources/meeting.rb +41 -0
- data/lib/fathom/resources/recording.rb +42 -0
- data/lib/fathom/resources/team.rb +17 -0
- data/lib/fathom/resources/team_member.rb +38 -0
- data/lib/fathom/resources/webhook.rb +33 -0
- data/lib/fathom/version.rb +5 -0
- data/lib/fathom.rb +58 -0
- data/scripts/README.md +138 -0
- data/scripts/test_live_api.rb +374 -0
- metadata +68 -0
data/TESTING.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# Testing the Gem
|
2
|
+
|
3
|
+
## Running Unit Tests
|
4
|
+
|
5
|
+
Run the full test suite:
|
6
|
+
|
7
|
+
```bash
|
8
|
+
bundle exec rspec
|
9
|
+
```
|
10
|
+
|
11
|
+
Run specific test files:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
bundle exec rspec spec/fathom/client_spec.rb
|
15
|
+
bundle exec rspec spec/fathom/resources/meeting_spec.rb
|
16
|
+
```
|
17
|
+
|
18
|
+
Run with coverage:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
bundle exec rspec
|
22
|
+
open coverage/index.html
|
23
|
+
```
|
24
|
+
|
25
|
+
## Testing Against Live API
|
26
|
+
|
27
|
+
We've created an integration test script that you can run against your real Fathom account.
|
28
|
+
|
29
|
+
### Quick Start
|
30
|
+
|
31
|
+
1. Get your API key from [Fathom Settings](https://app.fathom.video/settings/integrations)
|
32
|
+
|
33
|
+
2. Run the test script:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
FATHOM_API_KEY=your_key_here ruby scripts/test_live_api.rb
|
37
|
+
```
|
38
|
+
|
39
|
+
### Using .env File (Recommended)
|
40
|
+
|
41
|
+
For convenience, create a `.env` file (already in `.gitignore`):
|
42
|
+
|
43
|
+
```bash
|
44
|
+
# Create .env file with your API key
|
45
|
+
echo "FATHOM_API_KEY=your_actual_api_key_here" > .env
|
46
|
+
|
47
|
+
# Load and run
|
48
|
+
source .env
|
49
|
+
ruby scripts/test_live_api.rb
|
50
|
+
```
|
51
|
+
|
52
|
+
### What Gets Tested
|
53
|
+
|
54
|
+
The live API test script verifies:
|
55
|
+
|
56
|
+
- ✅ **Meetings API**: List, attributes, summaries, transcripts
|
57
|
+
- ✅ **Teams API**: List teams and members
|
58
|
+
- ✅ **Team Members API**: List all members
|
59
|
+
- ✅ **Webhooks API**: Full CRUD (list, create, retrieve, delete)
|
60
|
+
- ✅ **Error Handling**: 404 responses
|
61
|
+
- ✅ **Rate Limiting**: Header processing
|
62
|
+
- ✅ **Pagination**: Limit parameters
|
63
|
+
|
64
|
+
### Sample Output
|
65
|
+
|
66
|
+
```
|
67
|
+
✓ Fathom API Test Suite
|
68
|
+
Testing against live API with your credentials
|
69
|
+
|
70
|
+
================================================================================
|
71
|
+
1. Testing Meetings API
|
72
|
+
================================================================================
|
73
|
+
Fetch meetings list... ✓ PASS
|
74
|
+
Found 5 meeting(s)
|
75
|
+
|
76
|
+
First meeting details:
|
77
|
+
ID: 123456
|
78
|
+
Title: Weekly Team Sync
|
79
|
+
Created: 2025-01-15T10:00:00Z
|
80
|
+
Recording ID: 789012
|
81
|
+
|
82
|
+
Access meeting attributes... ✓ PASS
|
83
|
+
Fetch meeting summary... ✓ PASS
|
84
|
+
Fetch meeting transcript... ✓ PASS
|
85
|
+
|
86
|
+
...
|
87
|
+
|
88
|
+
================================================================================
|
89
|
+
Test Results Summary
|
90
|
+
================================================================================
|
91
|
+
|
92
|
+
Passed: 15
|
93
|
+
Failed: 0
|
94
|
+
Skipped: 0
|
95
|
+
Total: 15
|
96
|
+
|
97
|
+
Pass Rate: 100.0%
|
98
|
+
|
99
|
+
✓ All tests passed!
|
100
|
+
```
|
101
|
+
|
102
|
+
### Important Notes
|
103
|
+
|
104
|
+
- **Mostly Read-Only**: Performs GET requests for meetings, teams, and team members
|
105
|
+
- **Webhook Testing**: Creates a temporary test webhook and immediately deletes it
|
106
|
+
- **Safe**: Test webhook is automatically cleaned up - run as many times as you want
|
107
|
+
- **Rate Limits**: Automatically handled with retries
|
108
|
+
|
109
|
+
See `scripts/README.md` for more details.
|
110
|
+
|
111
|
+
## Code Quality
|
112
|
+
|
113
|
+
Check code style:
|
114
|
+
|
115
|
+
```bash
|
116
|
+
bundle exec rubocop
|
117
|
+
```
|
118
|
+
|
119
|
+
Auto-fix issues:
|
120
|
+
|
121
|
+
```bash
|
122
|
+
bundle exec rubocop -A
|
123
|
+
```
|
data/fathom-ruby.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/fathom/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "fathom-ruby"
|
7
|
+
spec.version = Fathom::VERSION
|
8
|
+
spec.authors = ["Juan Rodriguez"]
|
9
|
+
spec.email = ["jrodriguez@example.com"]
|
10
|
+
|
11
|
+
spec.summary = "Ruby library for the Fathom API"
|
12
|
+
spec.description = "A comprehensive Ruby gem for interacting with the Fathom API, supporting meetings, \
|
13
|
+
recordings, teams, webhooks, and more."
|
14
|
+
spec.homepage = "https://github.com/yourusername/fathom-ruby"
|
15
|
+
spec.license = "MIT"
|
16
|
+
spec.required_ruby_version = ">= 3.1.0"
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
20
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(File.expand_path(f) == __FILE__) ||
|
27
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
spec.bindir = "exe"
|
31
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ["lib"]
|
33
|
+
|
34
|
+
# Runtime dependencies - none! Using only Ruby stdlib
|
35
|
+
# Development dependencies are specified in Gemfile
|
36
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class Client
|
5
|
+
BASE_URL = "https://api.fathom.ai/external/v1"
|
6
|
+
|
7
|
+
attr_reader :rate_limiter
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@rate_limiter = RateLimiter.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(path, params = {}) = request(:get, path, params:)
|
14
|
+
|
15
|
+
def post(path, body = {}) = request(:post, path, body:)
|
16
|
+
|
17
|
+
def put(path, body = {}) = request(:put, path, body:)
|
18
|
+
|
19
|
+
def patch(path, body = {}) = request(:patch, path, body:)
|
20
|
+
|
21
|
+
def delete(path) = request(:delete, path)
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def request(method, path, params: {}, body: {}, retry_count: 0)
|
26
|
+
uri = build_uri(path, params)
|
27
|
+
http = build_http(uri)
|
28
|
+
request_obj = build_request(method, uri, body)
|
29
|
+
|
30
|
+
Fathom.log_http("#{method.upcase} #{uri}")
|
31
|
+
Fathom.log_http("Headers: #{request_obj.to_hash}") if Fathom.debug_http
|
32
|
+
Fathom.log_http("Body: #{body.to_json}") if Fathom.debug_http && body.present?
|
33
|
+
|
34
|
+
response = http.request(request_obj)
|
35
|
+
|
36
|
+
Fathom.log_http("Response: #{response.code} #{response.message}")
|
37
|
+
@rate_limiter.update_from_headers(response.to_hash)
|
38
|
+
|
39
|
+
handle_response(response, method, path, params, body, retry_count)
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_uri(path, params)
|
43
|
+
# Ensure path starts with /
|
44
|
+
path = "/#{path}" unless path.start_with?("/")
|
45
|
+
uri = URI.parse("#{BASE_URL}#{path}")
|
46
|
+
uri.query = URI.encode_www_form(params) unless params.empty?
|
47
|
+
uri
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_http(uri)
|
51
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
52
|
+
http.use_ssl = true
|
53
|
+
http.read_timeout = 60
|
54
|
+
http.open_timeout = 30
|
55
|
+
http
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_request(method, uri, body)
|
59
|
+
request_class =
|
60
|
+
case method
|
61
|
+
in :get then Net::HTTP::Get
|
62
|
+
in :post then Net::HTTP::Post
|
63
|
+
in :put then Net::HTTP::Put
|
64
|
+
in :patch then Net::HTTP::Patch
|
65
|
+
in :delete then Net::HTTP::Delete
|
66
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
67
|
+
end
|
68
|
+
|
69
|
+
request = request_class.new(uri)
|
70
|
+
request["X-Api-Key"] = Fathom.api_key
|
71
|
+
request["Content-Type"] = "application/json"
|
72
|
+
request["Accept"] = "application/json"
|
73
|
+
request["User-Agent"] = "fathom-ruby/#{Fathom::VERSION}"
|
74
|
+
|
75
|
+
request.body = body.to_json unless body.empty?
|
76
|
+
|
77
|
+
request
|
78
|
+
end
|
79
|
+
|
80
|
+
def handle_response(response, method, path, params, body, retry_count)
|
81
|
+
case response.code.to_i
|
82
|
+
when 200, 201
|
83
|
+
parse_json(response.body)
|
84
|
+
when 204
|
85
|
+
{} # No content
|
86
|
+
when 400
|
87
|
+
raise BadRequestError.new(parse_error_message(response), response: response, http_status: 400)
|
88
|
+
when 401
|
89
|
+
raise AuthenticationError.new(parse_error_message(response), response: response, http_status: 401)
|
90
|
+
when 403
|
91
|
+
raise ForbiddenError.new(parse_error_message(response), response: response, http_status: 403)
|
92
|
+
when 404
|
93
|
+
raise NotFoundError.new(parse_error_message(response), response: response, http_status: 404)
|
94
|
+
when 429
|
95
|
+
handle_rate_limit(response, method, path, params, body, retry_count)
|
96
|
+
when 500..599
|
97
|
+
raise ServerError.new(parse_error_message(response), response: response, http_status: response.code.to_i)
|
98
|
+
else
|
99
|
+
raise Error.new("Unexpected response: #{response.code}", response: response, http_status: response.code.to_i)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def handle_rate_limit(response, method, path, params, body, retry_count)
|
104
|
+
if Fathom.auto_retry && retry_count < Fathom.max_retries
|
105
|
+
wait_time = calculate_backoff(retry_count)
|
106
|
+
Fathom.log("Rate limited. Retrying in #{wait_time}s (attempt #{retry_count + 1}/#{Fathom.max_retries})")
|
107
|
+
sleep(wait_time)
|
108
|
+
request(method, path, params: params, body: body, retry_count: retry_count + 1)
|
109
|
+
else
|
110
|
+
raise RateLimitError.new(
|
111
|
+
parse_error_message(response),
|
112
|
+
response: response,
|
113
|
+
http_status: 429,
|
114
|
+
headers: response.to_hash
|
115
|
+
)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Exponential backoff: 2^retry_count seconds, max 60 seconds
|
120
|
+
def calculate_backoff(retry_count) = [2**retry_count, 60].min
|
121
|
+
|
122
|
+
def parse_json(body)
|
123
|
+
return {} if body.nil? || body.empty?
|
124
|
+
|
125
|
+
JSON.parse(body)
|
126
|
+
rescue JSON::ParserError => e
|
127
|
+
Fathom.log("Failed to parse JSON: #{e.message}")
|
128
|
+
{}
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_error_message(response)
|
132
|
+
body = parse_json(response.body)
|
133
|
+
body["error"] || body["message"] || "HTTP #{response.code}: #{response.message}"
|
134
|
+
rescue StandardError
|
135
|
+
"HTTP #{response.code}: #{response.message}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
# Base error class for all Fathom errors
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :response, :http_status
|
7
|
+
|
8
|
+
def initialize(message = nil, response: nil, http_status: nil)
|
9
|
+
@response = response
|
10
|
+
@http_status = http_status
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raised when API authentication fails (401)
|
16
|
+
class AuthenticationError < Error; end
|
17
|
+
|
18
|
+
# Raised when a resource is not found (404)
|
19
|
+
class NotFoundError < Error; end
|
20
|
+
|
21
|
+
# Raised when rate limit is exceeded (429)
|
22
|
+
class RateLimitError < Error
|
23
|
+
attr_reader :rate_limit_remaining, :rate_limit_reset
|
24
|
+
|
25
|
+
def initialize(message = nil, response: nil, http_status: nil, headers: {})
|
26
|
+
@rate_limit_remaining = headers["RateLimit-Remaining"]&.to_i
|
27
|
+
@rate_limit_reset = headers["RateLimit-Reset"]&.to_i
|
28
|
+
super(message, response: response, http_status: http_status)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Raised when the server returns a 5xx error
|
33
|
+
class ServerError < Error; end
|
34
|
+
|
35
|
+
# Raised when the API returns a bad request (400)
|
36
|
+
class BadRequestError < Error; end
|
37
|
+
|
38
|
+
# Raised when the API returns a forbidden error (403)
|
39
|
+
class ForbiddenError < Error; end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class RateLimiter
|
5
|
+
attr_reader :limit, :remaining, :reset
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@limit = nil
|
9
|
+
@remaining = nil
|
10
|
+
@reset = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def update_from_headers(headers)
|
14
|
+
@limit = extract_header_value(headers, "RateLimit-Limit")&.to_i
|
15
|
+
@remaining = extract_header_value(headers, "RateLimit-Remaining")&.to_i
|
16
|
+
@reset = extract_header_value(headers, "RateLimit-Reset")&.to_i
|
17
|
+
|
18
|
+
Fathom.log("Rate limit: #{@remaining}/#{@limit}, resets in #{@reset}s")
|
19
|
+
end
|
20
|
+
|
21
|
+
def should_retry? = Fathom.auto_retry && @remaining&.zero?
|
22
|
+
|
23
|
+
def wait_time
|
24
|
+
return 0 unless @reset
|
25
|
+
|
26
|
+
# Add a small buffer
|
27
|
+
@reset + 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def rate_limited? = @remaining&.zero?
|
31
|
+
|
32
|
+
def to_h = { limit: @limit, remaining: @remaining, reset: @reset }
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def extract_header_value(headers, key)
|
37
|
+
# Net::HTTP lowercases header keys, but direct hash access might not
|
38
|
+
# Try original case first, then lowercase (for Net::HTTP)
|
39
|
+
value = headers[key] || headers[key.downcase]
|
40
|
+
# Handle both string and array formats (Net::HTTP returns arrays)
|
41
|
+
value.is_a?(Array) ? value.first : value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class Resource
|
5
|
+
attr_reader :attributes, :rate_limit_info
|
6
|
+
|
7
|
+
def initialize(attributes = {}, rate_limit_info: nil)
|
8
|
+
@attributes = attributes
|
9
|
+
@rate_limit_info = rate_limit_info
|
10
|
+
end
|
11
|
+
|
12
|
+
def id = @attributes["id"]
|
13
|
+
|
14
|
+
def [](key) = @attributes[key.to_s]
|
15
|
+
|
16
|
+
def []=(key, value)
|
17
|
+
@attributes[key.to_s] = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h = @attributes
|
21
|
+
|
22
|
+
def to_json(...) = @attributes.to_json(...)
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} id=#{id.inspect} #{@attributes.keys.join(", ")}>"
|
26
|
+
end
|
27
|
+
|
28
|
+
# Dynamic attribute access
|
29
|
+
def method_missing(method_name, *args, &)
|
30
|
+
method_str = method_name.to_s
|
31
|
+
if method_str.end_with?("=")
|
32
|
+
@attributes[method_str.chop] = args.first
|
33
|
+
elsif @attributes.key?(method_str)
|
34
|
+
@attributes[method_str]
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_to_missing?(method_name, include_private = false)
|
41
|
+
method_str = method_name.to_s
|
42
|
+
method_str.end_with?("=") || @attributes.key?(method_str) || super
|
43
|
+
end
|
44
|
+
|
45
|
+
class << self
|
46
|
+
def client = @client ||= Client.new
|
47
|
+
|
48
|
+
def resource_name = name.split("::").last.downcase
|
49
|
+
|
50
|
+
def resource_path = "#{resource_name}s"
|
51
|
+
|
52
|
+
def all(params = {})
|
53
|
+
response = client.get(resource_path, params)
|
54
|
+
# Fathom API returns items array
|
55
|
+
data = response["items"] || response["data"] || []
|
56
|
+
|
57
|
+
data.map { |attrs| new(attrs, rate_limit_info: client.rate_limiter.to_h) }
|
58
|
+
end
|
59
|
+
|
60
|
+
def retrieve(id)
|
61
|
+
response = client.get("#{resource_path}/#{id}")
|
62
|
+
data = response["data"] || response[resource_name] || response
|
63
|
+
|
64
|
+
new(data, rate_limit_info: client.rate_limiter.to_h)
|
65
|
+
end
|
66
|
+
|
67
|
+
def create(attributes = {})
|
68
|
+
response = client.post(resource_path, attributes)
|
69
|
+
data = response["data"] || response[resource_name] || response
|
70
|
+
|
71
|
+
new(data, rate_limit_info: client.rate_limiter.to_h)
|
72
|
+
end
|
73
|
+
|
74
|
+
def search(query, params = {})
|
75
|
+
search_params = params.merge(q: query)
|
76
|
+
response = client.get("#{resource_path}/search", search_params)
|
77
|
+
data = response["data"] || response[resource_path] || []
|
78
|
+
|
79
|
+
data.map { |attrs| new(attrs, rate_limit_info: client.rate_limiter.to_h) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def update(attributes = {})
|
84
|
+
response = self.class.client.patch("#{self.class.resource_path}/#{id}", attributes)
|
85
|
+
data = response["data"] || response[self.class.resource_name] || response
|
86
|
+
|
87
|
+
@attributes.merge!(data)
|
88
|
+
@rate_limit_info = self.class.client.rate_limiter.to_h
|
89
|
+
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
def delete
|
94
|
+
self.class.client.delete("#{self.class.resource_path}/#{id}")
|
95
|
+
@rate_limit_info = self.class.client.rate_limiter.to_h
|
96
|
+
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
def reload
|
101
|
+
response = self.class.client.get("#{self.class.resource_path}/#{id}")
|
102
|
+
data = response["data"] || response[self.class.resource_name] || response
|
103
|
+
|
104
|
+
@attributes = data
|
105
|
+
@rate_limit_info = self.class.client.rate_limiter.to_h
|
106
|
+
|
107
|
+
self
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class Meeting < Resource
|
5
|
+
def self.resource_path = "meetings"
|
6
|
+
|
7
|
+
# Get the recording ID for this meeting
|
8
|
+
def recording_id = self["recording_id"]
|
9
|
+
|
10
|
+
# Get the summary for this meeting (use include_summary=true when listing)
|
11
|
+
# Returns the default_summary object or nil
|
12
|
+
def summary = self["default_summary"]
|
13
|
+
|
14
|
+
# Get the transcript for this meeting (use include_transcript=true when listing)
|
15
|
+
# Returns array of transcript segments or nil
|
16
|
+
def transcript = self["transcript"]
|
17
|
+
|
18
|
+
# Fetch the recording summary from the API
|
19
|
+
def fetch_summary(destination_url: nil)
|
20
|
+
return nil unless recording_id
|
21
|
+
|
22
|
+
Recording.get_summary(recording_id, destination_url:)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fetch the recording transcript from the API
|
26
|
+
def fetch_transcript(destination_url: nil)
|
27
|
+
return nil unless recording_id
|
28
|
+
|
29
|
+
Recording.get_transcript(recording_id, destination_url:)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if the meeting has a recording
|
33
|
+
def recording? = !recording_id.nil?
|
34
|
+
|
35
|
+
# Get meeting participants (calendar invitees)
|
36
|
+
def participants = self["calendar_invitees"] || []
|
37
|
+
|
38
|
+
# Get action items for the meeting
|
39
|
+
def action_items = self["action_items"] || []
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class Recording < Resource
|
5
|
+
def self.resource_path = "recordings"
|
6
|
+
|
7
|
+
# Get summary for a recording
|
8
|
+
# @param recording_id [Integer] The recording ID
|
9
|
+
# @param destination_url [String, nil] Optional webhook URL for async delivery
|
10
|
+
# @return [Hash] Summary data or destination confirmation
|
11
|
+
def self.get_summary(recording_id, destination_url: nil)
|
12
|
+
params = destination_url ? { destination_url: } : {}
|
13
|
+
response = client.get("#{resource_path}/#{recording_id}/summary", params)
|
14
|
+
|
15
|
+
response["summary"] || response
|
16
|
+
end
|
17
|
+
|
18
|
+
# Get transcript for a recording
|
19
|
+
# @param recording_id [Integer] The recording ID
|
20
|
+
# @param destination_url [String, nil] Optional webhook URL for async delivery
|
21
|
+
# @return [Hash] Transcript data or destination confirmation
|
22
|
+
def self.get_transcript(recording_id, destination_url: nil)
|
23
|
+
params = destination_url ? { destination_url: } : {}
|
24
|
+
response = client.get("#{resource_path}/#{recording_id}/transcript", params)
|
25
|
+
|
26
|
+
response["transcript"] || response
|
27
|
+
end
|
28
|
+
|
29
|
+
# Recordings don't have a standard list endpoint
|
30
|
+
def self.all(_params = {})
|
31
|
+
raise NotImplementedError,
|
32
|
+
"Recording.all is not supported. Recordings are accessed via Meeting#recording_id"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Recordings don't have a standard retrieve endpoint
|
36
|
+
# Use get_summary or get_transcript instead
|
37
|
+
def self.retrieve(_id)
|
38
|
+
raise NotImplementedError,
|
39
|
+
"Recording.retrieve is not supported. Use Recording.get_summary(id) or Recording.get_transcript(id)"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class Team < Resource
|
5
|
+
def self.resource_path = "teams"
|
6
|
+
|
7
|
+
# Get all members of this team by filtering by team name
|
8
|
+
# Note: Requires the team to have a 'name' attribute
|
9
|
+
def members(params = {}) = TeamMember.all(params.merge(team: name))
|
10
|
+
|
11
|
+
# Get the team name
|
12
|
+
def name = self["name"]
|
13
|
+
|
14
|
+
# Get the created_at timestamp
|
15
|
+
def created_at = self["created_at"]
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class TeamMember < Resource
|
5
|
+
def self.resource_path = "team_members"
|
6
|
+
|
7
|
+
# List all team members, optionally filtered by team name
|
8
|
+
# @param params [Hash] Query parameters
|
9
|
+
# @option params [String] :team Team name to filter by
|
10
|
+
# @option params [String] :cursor Cursor for pagination
|
11
|
+
# @return [Array<TeamMember>]
|
12
|
+
def self.all(params = {})
|
13
|
+
response = client.get(resource_path, params)
|
14
|
+
data = response["items"] || []
|
15
|
+
|
16
|
+
data.map { |attrs| new(attrs, rate_limit_info: client.rate_limiter.to_h) }
|
17
|
+
end
|
18
|
+
|
19
|
+
# Team members don't have individual retrieve endpoint in the API
|
20
|
+
# Use .all with filters instead
|
21
|
+
def self.retrieve(_id)
|
22
|
+
raise NotImplementedError,
|
23
|
+
"TeamMember.retrieve is not supported by the Fathom API. Use TeamMember.all instead."
|
24
|
+
end
|
25
|
+
|
26
|
+
# Get the member's email
|
27
|
+
def email = self["email"]
|
28
|
+
|
29
|
+
# Get the member's name
|
30
|
+
def name = self["name"]
|
31
|
+
|
32
|
+
# Get the created_at timestamp
|
33
|
+
def created_at = self["created_at"]
|
34
|
+
|
35
|
+
# Team members don't have an ID field in the API
|
36
|
+
def id = nil
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Fathom
|
4
|
+
class Webhook < Resource
|
5
|
+
def self.resource_path = "webhooks"
|
6
|
+
|
7
|
+
# Get the webhook URL
|
8
|
+
def url = self["url"]
|
9
|
+
|
10
|
+
# Check if webhook is active
|
11
|
+
def active? = self["active"] == true || self["status"] == "active"
|
12
|
+
|
13
|
+
# Get the webhook secret (if available)
|
14
|
+
def secret = self["secret"]
|
15
|
+
|
16
|
+
# Get triggered_for configuration
|
17
|
+
# Possible values: "my_recordings", "shared_external_recordings",
|
18
|
+
# "my_shared_with_team_recordings", "shared_team_recordings"
|
19
|
+
def triggered_for = self["triggered_for"]
|
20
|
+
|
21
|
+
# Check if transcript is included in webhook payload
|
22
|
+
def include_transcript? = self["include_transcript"] == true
|
23
|
+
|
24
|
+
# Check if summary is included in webhook payload
|
25
|
+
def include_summary? = self["include_summary"] == true
|
26
|
+
|
27
|
+
# Check if action items are included in webhook payload
|
28
|
+
def include_action_items? = self["include_action_items"] == true
|
29
|
+
|
30
|
+
# Check if CRM matches are included in webhook payload
|
31
|
+
def include_crm_matches? = self["include_crm_matches"] == true
|
32
|
+
end
|
33
|
+
end
|