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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/forem-ruby.gemspec +17 -0
- data/lib/forem/api_operations/create.rb +47 -0
- data/lib/forem/api_operations/delete.rb +88 -0
- data/lib/forem/api_operations/list.rb +70 -0
- data/lib/forem/api_operations/request.rb +83 -0
- data/lib/forem/api_operations/retrieve.rb +43 -0
- data/lib/forem/api_operations/save.rb +53 -0
- data/lib/forem/api_operations/update.rb +47 -0
- data/lib/forem/api_requestor.rb +283 -0
- data/lib/forem/api_resource.rb +77 -0
- data/lib/forem/client.rb +279 -0
- data/lib/forem/configuration.rb +74 -0
- data/lib/forem/connection_manager.rb +75 -0
- data/lib/forem/errors.rb +118 -0
- data/lib/forem/forem_object.rb +264 -0
- data/lib/forem/forem_response.rb +50 -0
- data/lib/forem/list_object.rb +171 -0
- data/lib/forem/resources/admin_concept.rb +169 -0
- data/lib/forem/resources/admin_user.rb +152 -0
- data/lib/forem/resources/agent_session.rb +110 -0
- data/lib/forem/resources/analytics.rb +151 -0
- data/lib/forem/resources/article.rb +256 -0
- data/lib/forem/resources/billboard.rb +76 -0
- data/lib/forem/resources/comment.rb +43 -0
- data/lib/forem/resources/concept.rb +192 -0
- data/lib/forem/resources/follow.rb +78 -0
- data/lib/forem/resources/follower.rb +56 -0
- data/lib/forem/resources/health_check.rb +70 -0
- data/lib/forem/resources/organization.rb +79 -0
- data/lib/forem/resources/page.rb +52 -0
- data/lib/forem/resources/podcast_episode.rb +36 -0
- data/lib/forem/resources/profile_image.rb +44 -0
- data/lib/forem/resources/reaction.rb +61 -0
- data/lib/forem/resources/reading_list.rb +29 -0
- data/lib/forem/resources/recommended_articles_list.rb +45 -0
- data/lib/forem/resources/request_redirect.rb +60 -0
- data/lib/forem/resources/segment.rb +103 -0
- data/lib/forem/resources/survey.rb +96 -0
- data/lib/forem/resources/tag.rb +27 -0
- data/lib/forem/resources/trend.rb +80 -0
- data/lib/forem/resources/user.rb +229 -0
- data/lib/forem/resources/video.rb +28 -0
- data/lib/forem/services/admin_concept_service.rb +138 -0
- data/lib/forem/services/admin_user_service.rb +114 -0
- data/lib/forem/services/agent_session_service.rb +87 -0
- data/lib/forem/services/analytics_service.rb +93 -0
- data/lib/forem/services/article_service.rb +233 -0
- data/lib/forem/services/base_service.rb +45 -0
- data/lib/forem/services/billboard_service.rb +91 -0
- data/lib/forem/services/comment_service.rb +51 -0
- data/lib/forem/services/concept_service.rb +143 -0
- data/lib/forem/services/follow_service.rb +61 -0
- data/lib/forem/services/follower_service.rb +34 -0
- data/lib/forem/services/health_check_service.rb +46 -0
- data/lib/forem/services/organization_service.rb +103 -0
- data/lib/forem/services/page_service.rb +107 -0
- data/lib/forem/services/podcast_episode_service.rb +34 -0
- data/lib/forem/services/profile_image_service.rb +32 -0
- data/lib/forem/services/reaction_service.rb +72 -0
- data/lib/forem/services/reading_list_service.rb +35 -0
- data/lib/forem/services/recommended_articles_list_service.rb +87 -0
- data/lib/forem/services/request_redirect_service.rb +118 -0
- data/lib/forem/services/segment_service.rb +83 -0
- data/lib/forem/services/survey_service.rb +48 -0
- data/lib/forem/services/tag_service.rb +32 -0
- data/lib/forem/services/trend_service.rb +70 -0
- data/lib/forem/services/user_service.rb +61 -0
- data/lib/forem/services/video_service.rb +33 -0
- data/lib/forem/util.rb +43 -0
- data/lib/forem/version.rb +4 -0
- data/lib/forem.rb +91 -0
- 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
|