forem-ruby 0.1.0.beta1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/forem-ruby.gemspec +17 -0
  4. data/lib/forem/api_operations/create.rb +47 -0
  5. data/lib/forem/api_operations/delete.rb +88 -0
  6. data/lib/forem/api_operations/list.rb +70 -0
  7. data/lib/forem/api_operations/request.rb +83 -0
  8. data/lib/forem/api_operations/retrieve.rb +43 -0
  9. data/lib/forem/api_operations/save.rb +53 -0
  10. data/lib/forem/api_operations/update.rb +47 -0
  11. data/lib/forem/api_requestor.rb +283 -0
  12. data/lib/forem/api_resource.rb +77 -0
  13. data/lib/forem/client.rb +279 -0
  14. data/lib/forem/configuration.rb +74 -0
  15. data/lib/forem/connection_manager.rb +75 -0
  16. data/lib/forem/errors.rb +118 -0
  17. data/lib/forem/forem_object.rb +264 -0
  18. data/lib/forem/forem_response.rb +50 -0
  19. data/lib/forem/list_object.rb +171 -0
  20. data/lib/forem/resources/admin_concept.rb +169 -0
  21. data/lib/forem/resources/admin_user.rb +152 -0
  22. data/lib/forem/resources/agent_session.rb +110 -0
  23. data/lib/forem/resources/analytics.rb +151 -0
  24. data/lib/forem/resources/article.rb +256 -0
  25. data/lib/forem/resources/billboard.rb +76 -0
  26. data/lib/forem/resources/comment.rb +43 -0
  27. data/lib/forem/resources/concept.rb +192 -0
  28. data/lib/forem/resources/follow.rb +78 -0
  29. data/lib/forem/resources/follower.rb +56 -0
  30. data/lib/forem/resources/health_check.rb +70 -0
  31. data/lib/forem/resources/organization.rb +79 -0
  32. data/lib/forem/resources/page.rb +52 -0
  33. data/lib/forem/resources/podcast_episode.rb +36 -0
  34. data/lib/forem/resources/profile_image.rb +44 -0
  35. data/lib/forem/resources/reaction.rb +61 -0
  36. data/lib/forem/resources/reading_list.rb +29 -0
  37. data/lib/forem/resources/recommended_articles_list.rb +45 -0
  38. data/lib/forem/resources/request_redirect.rb +60 -0
  39. data/lib/forem/resources/segment.rb +103 -0
  40. data/lib/forem/resources/survey.rb +96 -0
  41. data/lib/forem/resources/tag.rb +27 -0
  42. data/lib/forem/resources/trend.rb +80 -0
  43. data/lib/forem/resources/user.rb +229 -0
  44. data/lib/forem/resources/video.rb +28 -0
  45. data/lib/forem/services/admin_concept_service.rb +138 -0
  46. data/lib/forem/services/admin_user_service.rb +114 -0
  47. data/lib/forem/services/agent_session_service.rb +87 -0
  48. data/lib/forem/services/analytics_service.rb +93 -0
  49. data/lib/forem/services/article_service.rb +233 -0
  50. data/lib/forem/services/base_service.rb +45 -0
  51. data/lib/forem/services/billboard_service.rb +91 -0
  52. data/lib/forem/services/comment_service.rb +51 -0
  53. data/lib/forem/services/concept_service.rb +143 -0
  54. data/lib/forem/services/follow_service.rb +61 -0
  55. data/lib/forem/services/follower_service.rb +34 -0
  56. data/lib/forem/services/health_check_service.rb +46 -0
  57. data/lib/forem/services/organization_service.rb +103 -0
  58. data/lib/forem/services/page_service.rb +107 -0
  59. data/lib/forem/services/podcast_episode_service.rb +34 -0
  60. data/lib/forem/services/profile_image_service.rb +32 -0
  61. data/lib/forem/services/reaction_service.rb +72 -0
  62. data/lib/forem/services/reading_list_service.rb +35 -0
  63. data/lib/forem/services/recommended_articles_list_service.rb +87 -0
  64. data/lib/forem/services/request_redirect_service.rb +118 -0
  65. data/lib/forem/services/segment_service.rb +83 -0
  66. data/lib/forem/services/survey_service.rb +48 -0
  67. data/lib/forem/services/tag_service.rb +32 -0
  68. data/lib/forem/services/trend_service.rb +70 -0
  69. data/lib/forem/services/user_service.rb +61 -0
  70. data/lib/forem/services/video_service.rb +33 -0
  71. data/lib/forem/util.rb +43 -0
  72. data/lib/forem/version.rb +4 -0
  73. data/lib/forem.rb +91 -0
  74. metadata +111 -0
@@ -0,0 +1,171 @@
1
+ module Forem
2
+ # Wraps a paginated list of API resources returned by collection endpoints.
3
+ #
4
+ # {ListObject} implements +Enumerable+ so the current page's items can be
5
+ # iterated with the standard Ruby collection methods. Use
6
+ # {#auto_paging_each} to transparently iterate across all pages without
7
+ # managing pagination manually.
8
+ #
9
+ # Two paging modes are supported:
10
+ #
11
+ # * *page-based* (default) — uses +page+/+per_page+ query params. Created
12
+ # by {APIOperations::List#list} and the +paginated_list+ helper on
13
+ # {APIResource}.
14
+ # * *cursor-based* — uses an +after+-style cursor (e.g. survey poll
15
+ # endpoints). Created by the +cursor_list+ helper on {APIResource}.
16
+ #
17
+ # In both modes the public surface is the same: {#data}, {#has_more?},
18
+ # {#each}, {#next_page}, {#auto_paging_each}.
19
+ #
20
+ # @example Iterating the current page
21
+ # articles = client.articles.list(per_page: 5)
22
+ # articles.each { |a| puts a.title }
23
+ #
24
+ # @example Iterating all pages automatically
25
+ # client.articles.list(per_page: 30).auto_paging_each do |article|
26
+ # puts article.title
27
+ # end
28
+ class ListObject
29
+ include Enumerable
30
+
31
+ # @return [Array<ForemObject>] the resource objects on the current page.
32
+ attr_reader :data
33
+
34
+ # @return [Integer, nil] the 1-based current page number for page-based
35
+ # pagination, or +nil+ for cursor-based pagination.
36
+ attr_reader :current_page
37
+
38
+ # @return [Integer] the maximum number of items per page requested.
39
+ attr_reader :per_page
40
+
41
+ # @return [Hash] the non-pagination filter parameters used to build this
42
+ # list (e.g. +{ tag: "ruby" }+). These are forwarded when fetching
43
+ # subsequent pages.
44
+ attr_reader :filters
45
+
46
+ # @return [Class] the resource class used to construct items.
47
+ attr_reader :resource_class
48
+
49
+ # @return [APIRequestor, nil] the requestor that produced this list and
50
+ # that will be re-used for fetching additional pages.
51
+ attr_reader :requestor
52
+
53
+ # Create a new ListObject.
54
+ #
55
+ # @param data [Array<ForemObject>] the resource objects for this page.
56
+ # @param per_page [Integer] the page size requested.
57
+ # @param resource_class [Class] the resource class for this collection.
58
+ # @param filters [Hash] the non-pagination query parameters.
59
+ # @param requestor [APIRequestor, nil] the requestor used to fetch the
60
+ # current page; reused for subsequent pages.
61
+ # @param current_page [Integer, nil] the 1-based current page number, or
62
+ # +nil+ for cursor-based pagination.
63
+ # @param fetcher [Proc, nil] optional callable invoked with
64
+ # <tt>(direction, current_list, extra_params)</tt> to fetch the next or
65
+ # previous page. Used by custom-path or cursor-based endpoints. When
66
+ # +nil+, page-based pagination falls back to
67
+ # <tt>resource_class.list(...)</tt>.
68
+ # @return [ListObject]
69
+ def initialize(data:, per_page:, resource_class:, filters:, requestor:, current_page: nil, fetcher: nil)
70
+ @data = data
71
+ @current_page = current_page
72
+ @per_page = per_page
73
+ @resource_class = resource_class
74
+ @filters = filters
75
+ @requestor = requestor
76
+ @fetcher = fetcher
77
+ end
78
+
79
+ # Return whether more pages of results may exist after this one.
80
+ #
81
+ # Inferred by checking whether the current page returned exactly
82
+ # {#per_page} items.
83
+ #
84
+ # @return [Boolean]
85
+ def has_more?
86
+ @data.length == @per_page && @data.length > 0
87
+ end
88
+
89
+ # Iterate over the resource objects on the current page.
90
+ #
91
+ # @yield [ForemObject] each resource object on the current page.
92
+ # @return [Enumerator] if no block is given.
93
+ def each(&block)
94
+ @data.each(&block)
95
+ end
96
+
97
+ # Indexed access to the underlying page data.
98
+ # @param idx [Integer] zero-based item index within the current page.
99
+ # @return [ForemObject, nil]
100
+ def [](idx)
101
+ @data[idx]
102
+ end
103
+
104
+ # @return [Integer] the number of items on the current page.
105
+ def length
106
+ @data.length
107
+ end
108
+ alias_method :size, :length
109
+
110
+ # @return [Boolean] whether the current page contains zero items.
111
+ def empty?
112
+ @data.empty?
113
+ end
114
+
115
+ # Iterate over every resource across all pages, automatically fetching
116
+ # subsequent pages as needed. Stops when {#has_more?} is false or
117
+ # {#next_page} returns +nil+.
118
+ #
119
+ # @yield [ForemObject] each resource object across all pages.
120
+ # @return [void]
121
+ def auto_paging_each(&block)
122
+ page = self
123
+ loop do
124
+ page.each(&block)
125
+ break unless page.has_more?
126
+ page = page.next_page
127
+ break unless page
128
+ end
129
+ end
130
+
131
+ # Fetch the next page of results.
132
+ #
133
+ # Returns +nil+ when {#has_more?} is +false+ or when the fetcher signals
134
+ # there is no further page.
135
+ #
136
+ # @param params [Hash] additional parameters merged on top of {#filters}.
137
+ # @return [ListObject, nil]
138
+ def next_page(params = {})
139
+ return nil unless has_more?
140
+ if @fetcher
141
+ @fetcher.call(:next, self, params)
142
+ else
143
+ fetch_page(@current_page + 1, params)
144
+ end
145
+ end
146
+
147
+ # Fetch the previous page of results.
148
+ #
149
+ # Page-based: returns +nil+ when already on page 1.
150
+ # Cursor-based: returns +nil+ (cursor pagination is forward-only).
151
+ #
152
+ # @param params [Hash] additional parameters merged on top of {#filters}.
153
+ # @return [ListObject, nil]
154
+ def previous_page(params = {})
155
+ if @fetcher
156
+ @fetcher.call(:previous, self, params)
157
+ else
158
+ return nil if @current_page.nil? || @current_page <= 1
159
+ fetch_page(@current_page - 1, params)
160
+ end
161
+ end
162
+
163
+ private
164
+
165
+ def fetch_page(page_num, extra_params = {})
166
+ params = @filters.merge(page: page_num, per_page: @per_page).merge(extra_params)
167
+ opts = @requestor ? { requestor: @requestor } : {}
168
+ @resource_class.list(params, opts)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,169 @@
1
+ module Forem
2
+ # Represents a Concept managed through the admin-only Concepts API.
3
+ #
4
+ # Concepts are curated topics that Forem uses to classify articles and
5
+ # comments via embedding similarity. The admin endpoints under
6
+ # +/api/admin/concepts+ expose full CRUD over those records plus a
7
+ # +trigger_lookback+ action that re-runs classification over older content.
8
+ # Every endpoint requires an API key belonging to a super admin — a regular
9
+ # user key receives HTTP 401.
10
+ #
11
+ # Read-only access to the public concept endpoints lives on
12
+ # {Forem::Concept}; this resource is strictly the administrative surface.
13
+ #
14
+ # Available operations:
15
+ # - +List+ — GET /api/admin/concepts
16
+ # - +Retrieve+ — GET /api/admin/concepts/:id
17
+ # - +create+ — POST /api/admin/concepts
18
+ # - +update+ — PUT /api/admin/concepts/:id
19
+ # - +Delete+ — DELETE /api/admin/concepts/:id
20
+ # - +trigger_lookback+ — POST /api/admin/concepts/:id/trigger_lookback
21
+ #
22
+ # == Concept Fields
23
+ #
24
+ # - +id+ (Integer) — Unique identifier
25
+ # - +name+ (String, required) — Display name, max 100 characters
26
+ # - +slug+ (String) — Generated from +name+ by the API; not writable
27
+ # - +description+ (String) — Generated from +name+ when omitted on create
28
+ # - +parent_id+ (Integer) — Optional parent concept, forming a hierarchy
29
+ # - +similarity_threshold+ (Float) — Cosine similarity cutoff between +0.0+
30
+ # and +1.0+ used when classifying records (may be +null+)
31
+ # - +score+ (Float) — Ranking score for the concept
32
+ # - +max_lookback_days+ (Integer) — How far back classification has already
33
+ # been run; read-only (set by +trigger_lookback+, never by create/update)
34
+ # - +created_at+ / +updated_at+ (String) — ISO 8601 timestamps
35
+ #
36
+ # The list endpoint returns a trimmed projection (+id+, +name+, +slug+,
37
+ # +description+, +parent_id+, +similarity_threshold+, +max_lookback_days+,
38
+ # +created_at+, +updated_at+) ordered by +name+, while the show endpoint
39
+ # returns the full record.
40
+ #
41
+ # == Writable attributes
42
+ #
43
+ # Only +name+, +description+, +parent_id+, +similarity_threshold+, and
44
+ # +score+ are permitted on create and update. Any other attribute (notably
45
+ # +slug+ and +max_lookback_days+) is silently ignored by the API. Attributes
46
+ # are sent wrapped in a +concept+ object, which this resource does for you.
47
+ #
48
+ # @example List concepts
49
+ # concepts = client.admin_concepts.list(per_page: 100)
50
+ # concepts.each { |c| puts "#{c.id}: #{c.name}" }
51
+ #
52
+ # @example Create a concept (flat params — the +concept:+ wrapper is added)
53
+ # concept = client.admin_concepts.create(
54
+ # name: "Machine Learning",
55
+ # description: "Posts about ML and AI",
56
+ # similarity_threshold: 0.8
57
+ # )
58
+ #
59
+ # @see Forem::Concept
60
+ # @see https://developers.forem.com/api/v1
61
+ class AdminConcept < APIResource
62
+ extend APIOperations::List
63
+ extend APIOperations::Retrieve
64
+ include APIOperations::Delete
65
+
66
+ OBJECT_NAME = "admin_concept"
67
+ RESOURCE_PATH = "/api/admin/concepts"
68
+
69
+ # Create a new concept.
70
+ #
71
+ # Sends a +POST+ to +/api/admin/concepts+ with the attributes wrapped in a
72
+ # +concept+ object. The API generates the slug, and generates the
73
+ # description and anchor embedding from +name+ before saving, so creation
74
+ # only requires a name.
75
+ #
76
+ # @param params [Hash] concept attributes, either flat or already wrapped
77
+ # in +:concept+.
78
+ # @option params [String] :name (required) the concept name (max 100 chars)
79
+ # @option params [String] :description human-readable description
80
+ # @option params [Integer] :parent_id ID of the parent concept
81
+ # @option params [Float] :similarity_threshold cutoff between 0.0 and 1.0
82
+ # @option params [Float] :score ranking score for the concept
83
+ # @param opts [Hash] per-request options.
84
+ # @option opts [String] :api_key override the API key for this request.
85
+ # @option opts [APIRequestor] :requestor a custom requestor to use.
86
+ # @return [AdminConcept] the created concept (HTTP 201).
87
+ # @raise [InvalidRequestError] on HTTP 422 (validation errors).
88
+ def self.create(params = {}, opts = {})
89
+ requestor = opts[:requestor]
90
+ resp = request(:post, resource_path, wrap_params(params), opts)
91
+ construct_from(resp.parsed_body, requestor: requestor)
92
+ end
93
+
94
+ # Update an existing concept.
95
+ #
96
+ # Sends a +PUT+ to +/api/admin/concepts/:id+ with the attributes wrapped
97
+ # in a +concept+ object. Changing +name+ or +description+ causes the API
98
+ # to regenerate the concept's anchor embedding.
99
+ #
100
+ # @param id [Integer, String] the concept ID.
101
+ # @param params [Hash] attributes to change, either flat or already
102
+ # wrapped in +:concept+. Same permitted keys as {.create}.
103
+ # @param opts [Hash] per-request options.
104
+ # @return [AdminConcept] the updated concept.
105
+ # @raise [NotFoundError] on HTTP 404.
106
+ # @raise [InvalidRequestError] on HTTP 422 (validation errors).
107
+ def self.update(id, params = {}, opts = {})
108
+ requestor = opts[:requestor]
109
+ resp = request(:put, "#{resource_path}/#{id}", wrap_params(params), opts)
110
+ construct_from(resp.parsed_body, requestor: requestor)
111
+ end
112
+
113
+ # Queue a classification lookback for a concept.
114
+ #
115
+ # Sends a +POST+ to +/api/admin/concepts/:id/trigger_lookback+, enqueueing
116
+ # a background job that classifies records published within the last
117
+ # +days+ days. The requested +days+ must be positive and strictly greater
118
+ # than the concept's current +max_lookback_days+, otherwise the API
119
+ # responds with HTTP 422.
120
+ #
121
+ # @param id [Integer, String] the concept ID.
122
+ # @param params [Hash] request body.
123
+ # @option params [Integer] :days (required) how many days back to classify.
124
+ # @param opts [Hash] per-request options.
125
+ # @return [ForemObject] an object carrying a +message+ describing the
126
+ # queued lookback.
127
+ # @raise [InvalidRequestError] on HTTP 422 when +days+ is not greater than
128
+ # both zero and the concept's +max_lookback_days+.
129
+ #
130
+ # @example
131
+ # client.admin_concepts.trigger_lookback(7, days: 40).message
132
+ def self.trigger_lookback(id, params = {}, opts = {})
133
+ requestor = opts[:requestor]
134
+ resp = request(:post, "#{resource_path}/#{id}/trigger_lookback", params, opts)
135
+ ForemObject.construct_from(resp.parsed_body, requestor: requestor)
136
+ end
137
+
138
+ # Wrap flat attributes in the +concept+ object the API expects.
139
+ #
140
+ # Params that already carry a +:concept+ (or +"concept"+) key are passed
141
+ # through untouched.
142
+ #
143
+ # @param params [Hash] the caller-supplied attributes.
144
+ # @return [Hash] params wrapped in +concept+.
145
+ def self.wrap_params(params)
146
+ return params if params.key?(:concept) || params.key?("concept")
147
+
148
+ { concept: params }
149
+ end
150
+ private_class_method :wrap_params
151
+
152
+ # Queue a classification lookback for this concept instance.
153
+ #
154
+ # @param days [Integer] how many days back to classify. Must be positive
155
+ # and greater than this concept's +max_lookback_days+.
156
+ # @param opts [Hash] per-request options.
157
+ # @return [ForemObject] an object carrying a +message+.
158
+ # @raise [InvalidRequestError] if the instance has no +id+, or on HTTP 422.
159
+ #
160
+ # @example
161
+ # concept = client.admin_concepts.retrieve(7)
162
+ # concept.trigger_lookback(90)
163
+ def trigger_lookback(days, opts = {})
164
+ requestor = opts[:requestor] || @requestor
165
+ resp = request(:post, "#{resource_url}/trigger_lookback", { days: days }, opts)
166
+ ForemObject.construct_from(resp.parsed_body, requestor: requestor)
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,152 @@
1
+ module Forem
2
+ # Provides admin-level user creation and synchronization for a Forem
3
+ # instance.
4
+ #
5
+ # Requires super_admin privileges. Sends an invitation email to the
6
+ # provided email address.
7
+ #
8
+ # AdminUser exposes the privileged user-creation endpoint under
9
+ # +/api/admin/users+ (postAdminUsersCreate). Unlike the regular User
10
+ # resource, this resource is scoped to admin operations and requires an
11
+ # admin API key to use. General retrieval and moderation of existing users
12
+ # remains on {Forem::User}; identity and notification synchronization lives
13
+ # here.
14
+ #
15
+ # Available operations (via mixins):
16
+ # - +Create+ — POST /api/admin/users
17
+ # - +link_identity+ — POST /api/admin/users/:user_id/identities
18
+ # - +bulk_link_identities+ — POST /api/admin/users/identities/bulk
19
+ # - +identities+ — GET /api/admin/users/:user_id/identities
20
+ # - +unlink_identity+ — DELETE /api/admin/users/:user_id/identities/:id
21
+ # - +update_notification_settings+ — PUT notification settings for a user
22
+ #
23
+ # @!method self.create(params = {}, opts = {})
24
+ # Invite a new user by email (postAdminUsersCreate).
25
+ #
26
+ # Requires super_admin privileges. Sends an invitation email to the
27
+ # provided email address. The endpoint takes flat params — there is
28
+ # no +user:+ wrapper.
29
+ #
30
+ # The Forem API generates the username from the email address, so
31
+ # passing +:username+ has no effect. Optional invite-customization
32
+ # parameters are also accepted: +:custom_invite_subject+,
33
+ # +:custom_invite_message+, +:custom_invite_footnote+.
34
+ #
35
+ # The response body is intentionally empty (HTTP 200) — there is no
36
+ # created-user record returned.
37
+ #
38
+ # @param params [Hash] request body
39
+ # @option params [String] :email (required) the email address to invite
40
+ # @option params [String] :name the user's display name
41
+ # @option params [String] :custom_invite_subject override the invite subject line
42
+ # @option params [String] :custom_invite_message override the invite body
43
+ # @option params [String] :custom_invite_footnote override the invite footer
44
+ # @param opts [Hash] per-request options
45
+ # @return [Forem::AdminUser] an empty record (the API returns no body)
46
+ #
47
+ # @example Create a new user as an admin (flat params)
48
+ # client.admin_users.create(
49
+ # email: "alice@example.com",
50
+ # name: "Alice Example"
51
+ # )
52
+ #
53
+ # @see Forem::User
54
+ # @see https://developers.forem.com/api/v1
55
+ class AdminUser < APIResource
56
+ extend APIOperations::Create
57
+
58
+ OBJECT_NAME = "admin_user"
59
+ RESOURCE_PATH = "/api/admin/users"
60
+
61
+ # Link an external provider identity to a user.
62
+ #
63
+ # @param user_id [Integer, String] the Forem user ID.
64
+ # @param provider [String] the identity provider name.
65
+ # @param uid [String] the provider's stable user identifier.
66
+ # @param opts [Hash] per-request options.
67
+ # @return [ForemObject] the linked identity.
68
+ def self.link_identity(user_id, provider:, uid:, **opts)
69
+ requestor = opts[:requestor]
70
+ resp = request(
71
+ :post,
72
+ "#{resource_path}/#{user_id}/identities",
73
+ { provider: provider, uid: uid },
74
+ opts
75
+ )
76
+ ForemObject.construct_from(resp.parsed_body, requestor: requestor)
77
+ end
78
+
79
+ # Submit multiple external identity links in one request.
80
+ #
81
+ # @param provider [String] the identity provider shared by the identities.
82
+ # @param identities [Array<Hash>] identity hashes containing +user_id+ and
83
+ # +uid+.
84
+ # @param opts [Hash] per-request options.
85
+ # @return [Array<ForemObject>] per-item results containing +user_id+,
86
+ # +status+, and an +error_code+ when that item fails.
87
+ def self.bulk_link_identities(provider:, identities:, **opts)
88
+ requestor = opts[:requestor]
89
+ resp = request(
90
+ :post,
91
+ "#{resource_path}/identities/bulk",
92
+ { provider: provider, identities: identities },
93
+ opts
94
+ )
95
+ results = resp.parsed_body["results"] || []
96
+ results.map do |result|
97
+ ForemObject.construct_from(result, requestor: requestor)
98
+ end
99
+ end
100
+
101
+ # List a user's linked external identities.
102
+ #
103
+ # @param user_id [Integer, String] the Forem user ID.
104
+ # @param opts [Hash] per-request options.
105
+ # @return [Array<ForemObject>] the user's linked identities.
106
+ def self.identities(user_id, **opts)
107
+ requestor = opts[:requestor]
108
+ resp = request(:get, "#{resource_path}/#{user_id}/identities", {}, opts)
109
+ identities = resp.parsed_body["identities"] || []
110
+ identities.map do |identity|
111
+ ForemObject.construct_from(identity, requestor: requestor)
112
+ end
113
+ end
114
+
115
+ # Unlink an external identity from a user.
116
+ #
117
+ # @param user_id [Integer, String] the Forem user ID.
118
+ # @param identity_id [Integer, String] the linked identity ID.
119
+ # @param opts [Hash] per-request options.
120
+ # @return [ForemObject, nil] the unlinked identity when returned by the
121
+ # API, otherwise +nil+ for an empty response.
122
+ def self.unlink_identity(user_id, identity_id, **opts)
123
+ requestor = opts[:requestor]
124
+ resp = request(
125
+ :delete,
126
+ "#{resource_path}/#{user_id}/identities/#{identity_id}",
127
+ {},
128
+ opts
129
+ )
130
+ return nil unless resp.parsed_body
131
+
132
+ ForemObject.construct_from(resp.parsed_body, requestor: requestor)
133
+ end
134
+
135
+ # Update the email newsletter notification setting for a user.
136
+ #
137
+ # @param user_id [Integer, String] the Forem user ID.
138
+ # @param email_newsletter [Boolean] whether newsletter email is enabled.
139
+ # @param opts [Hash] per-request options.
140
+ # @return [ForemObject] the updated notification setting.
141
+ def self.update_notification_settings(user_id, email_newsletter:, **opts)
142
+ requestor = opts[:requestor]
143
+ resp = request(
144
+ :put,
145
+ "#{resource_path}/#{user_id}/notification_settings",
146
+ { notification_setting: { email_newsletter: email_newsletter } },
147
+ opts
148
+ )
149
+ ForemObject.construct_from(resp.parsed_body, requestor: requestor)
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,110 @@
1
+ module Forem
2
+ # Represents an agent session used for AI-assisted content workflows on Forem.
3
+ #
4
+ # Agent sessions are coding conversation transcripts from CLI tools like
5
+ # Claude Code and other AI coding assistants.
6
+ #
7
+ # A presign step generates upload credentials before session data is
8
+ # submitted. After creation, the raw asset URL for the session can be
9
+ # retrieved via the +raw_url+ instance method.
10
+ #
11
+ # Available operations (via mixins):
12
+ # - +List+ — GET /api/agent_sessions (getAgentSessions)
13
+ # - +Create+ — POST /api/agent_sessions (createAgentSession)
14
+ # - +Retrieve+ — GET /api/agent_sessions/:id (getAgentSessionById)
15
+ #
16
+ # == AgentSession Fields
17
+ #
18
+ # - +id+ (Integer)
19
+ # - +slug+ (String) — URL-friendly identifier
20
+ # - +title+ (String)
21
+ # - +tool_name+ (String) — Tool that produced the session
22
+ # - +total_messages+ (Integer)
23
+ # - +published+ (Boolean)
24
+ # - +url+ (String) — Public URL for the session
25
+ #
26
+ # @example Get presigned upload credentials before creating a session
27
+ # presign = client.agent_sessions.presign(filename: "session.json", mime_type: "application/json")
28
+ # puts presign.presigned_url
29
+ #
30
+ # @example Create a new agent session (flat params — no +agent_session:+ wrapper)
31
+ # session = client.agent_sessions.create(
32
+ # title: "My Session",
33
+ # curated_data: curated_json,
34
+ # tool_name: "claude_code"
35
+ # )
36
+ #
37
+ # @example Retrieve the raw asset URL for an existing session
38
+ # session = client.agent_sessions.retrieve(99)
39
+ # puts session.raw_url.raw_url
40
+ #
41
+ # @see https://developers.forem.com/api/v1
42
+ class AgentSession < APIResource
43
+ extend APIOperations::Create
44
+ extend APIOperations::List
45
+ extend APIOperations::Retrieve
46
+
47
+ OBJECT_NAME = "agent_session"
48
+ RESOURCE_PATH = "/api/agent_sessions"
49
+
50
+ # @!method self.create(params = {}, opts = {})
51
+ # Create a new agent session (createAgentSession).
52
+ #
53
+ # Sends a POST request to +/api/agent_sessions+.
54
+ #
55
+ # @param params [Hash] request body
56
+ # @option params [String] :title Title for the session (auto-generated if omitted)
57
+ # @option params [String] :curated_data JSON string of curated session data with messages array and metadata
58
+ # @option params [String] :s3_key S3 key for raw file (from presign endpoint)
59
+ # @option params [String] :tool_name One of: +"claude_code"+, +"codex"+, +"gemini_cli"+, +"github_copilot"+, +"pi"+
60
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
61
+ # @return [Forem::AgentSession] the newly-created session
62
+
63
+ # @!method self.retrieve(id, opts = {})
64
+ # Retrieve an agent session by ID or slug (getAgentSessionById).
65
+ #
66
+ # The +id+ parameter can be a numeric ID or a slug string
67
+ # (e.g., +'my-session-abc123'+).
68
+ #
69
+ # Sends a GET request to +/api/agent_sessions/:id+.
70
+ #
71
+ # @param id [Integer, String] numeric ID or slug of the session
72
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
73
+ # @return [Forem::AgentSession] the matching session
74
+
75
+ # Obtain presigned upload credentials for a new agent session asset.
76
+ #
77
+ # Sends a POST request to +/api/agent_sessions/presign+. The returned
78
+ # object contains a signed upload URL and any required form fields needed
79
+ # to upload the session file directly to object storage.
80
+ #
81
+ # @param params [Hash] request body
82
+ # @option params [String] :filename the intended filename for the upload
83
+ # @option params [String] :mime_type MIME type of the file being uploaded
84
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
85
+ # @return [Forem::ForemObject] presign response containing upload URL and credentials
86
+ # @example
87
+ # presign = client.agent_sessions.presign(filename: "output.json", mime_type: "application/json")
88
+ # puts presign.upload_url
89
+ # @see https://developers.forem.com/api/v1
90
+ def self.presign(params = {}, opts = {})
91
+ resp = request(:post, "/api/agent_sessions/presign", params, opts)
92
+ Forem::ForemObject.construct_from(resp.parsed_body)
93
+ end
94
+
95
+ # Return the raw asset URL associated with this agent session.
96
+ #
97
+ # Sends a GET request to +/api/agent_sessions/:id/raw_url+.
98
+ #
99
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
100
+ # @return [Forem::ForemObject] object containing the +url+ field with the raw asset URL
101
+ # @example
102
+ # session = client.agent_sessions.retrieve(99)
103
+ # puts session.raw_url.url
104
+ # @see https://developers.forem.com/api/v1
105
+ def raw_url(opts = {})
106
+ resp = request(:get, "#{resource_url}/raw_url", {}, opts)
107
+ Forem::ForemObject.construct_from(resp.parsed_body)
108
+ end
109
+ end
110
+ end