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,192 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Represents a Forem concept (a semantic, ML-generated tag).
|
|
3
|
+
#
|
|
4
|
+
# Concepts are semantic categories derived from article embeddings rather
|
|
5
|
+
# than from explicit user tags: each concept carries an anchor embedding
|
|
6
|
+
# generated from its +description+, and articles whose embeddings fall
|
|
7
|
+
# within the concept's +similarity_threshold+ are classified under it.
|
|
8
|
+
# They are used for advanced semantic categorization, automated feeds, and
|
|
9
|
+
# interest mapping.
|
|
10
|
+
#
|
|
11
|
+
# The public concepts endpoints are readable by any authenticated user (a
|
|
12
|
+
# super admin sees every concept, other users see the concepts they have
|
|
13
|
+
# been granted access to). Creating and deleting concepts is an admin-only
|
|
14
|
+
# operation exposed separately under +/api/admin/concepts+.
|
|
15
|
+
#
|
|
16
|
+
# Available operations (via mixins):
|
|
17
|
+
# - +List+ — GET /api/concepts
|
|
18
|
+
# - +Update+ — PUT /api/concepts/:id
|
|
19
|
+
#
|
|
20
|
+
# Custom class methods:
|
|
21
|
+
# - +retrieve+ — GET /api/concepts/:id
|
|
22
|
+
# - +articles+ — GET /api/concepts/:id/articles
|
|
23
|
+
# - +search+ — GET /api/concepts/search
|
|
24
|
+
#
|
|
25
|
+
# == Concept Fields
|
|
26
|
+
#
|
|
27
|
+
# - +id+ (Integer) — Unique concept ID
|
|
28
|
+
# - +name+ (String) — Human readable label for the concept
|
|
29
|
+
# - +slug+ (String) — URL-friendly identifier
|
|
30
|
+
# - +description+ (String, nullable) — Semantic definition used to generate
|
|
31
|
+
# the concept's anchor embedding
|
|
32
|
+
# - +parent_id+ (Integer, nullable) — Parent concept when using a hierarchy
|
|
33
|
+
# - +score+ (Float) — Concept popularity / curation score
|
|
34
|
+
# - +similarity_threshold+ (Float, nullable) — Cosine distance threshold
|
|
35
|
+
# (0.0–1.0) an article embedding must satisfy to be classified under the
|
|
36
|
+
# concept
|
|
37
|
+
# - +created_at+ / +updated_at+ (String) — ISO 8601 timestamps
|
|
38
|
+
# - +daily_metrics+ (Array) — Nested per-day activity rollups, newest first.
|
|
39
|
+
# Each entry has +date+, +articles_count+, +comments_count+,
|
|
40
|
+
# +page_views+, +reactions_count+, and +popularity_score+
|
|
41
|
+
# - +top_articles+ (Array) — Only on +retrieve+ / +update+ responses; the
|
|
42
|
+
# three highest-scoring articles for the concept, each with +id+,
|
|
43
|
+
# +title+, +slug+, +score+, and +published_at+
|
|
44
|
+
#
|
|
45
|
+
# Results from {search} carry two extra fields: +distance+ (cosine distance
|
|
46
|
+
# from the query embedding) and +similarity+ (+1.0 - distance+).
|
|
47
|
+
#
|
|
48
|
+
# @example List concepts with a 30-day metrics window
|
|
49
|
+
# concepts = client.concepts.list(per_page: 20, days: 30)
|
|
50
|
+
# concepts.each { |c| puts "#{c.id}: #{c.name} (#{c.score})" }
|
|
51
|
+
#
|
|
52
|
+
# @example Retrieve a concept and read its nested daily metrics
|
|
53
|
+
# concept = client.concepts.retrieve(7)
|
|
54
|
+
# concept.daily_metrics.each { |m| puts "#{m.date}: #{m.articles_count}" }
|
|
55
|
+
#
|
|
56
|
+
# @example Update a concept (params are wrapped in +concept:+ automatically)
|
|
57
|
+
# client.concepts.update(7, description: "Databases and storage engines")
|
|
58
|
+
#
|
|
59
|
+
# @example List the articles classified under a concept
|
|
60
|
+
# client.concepts.articles(7, sort: "score", per_page: 25).each do |a|
|
|
61
|
+
# puts a.title
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# @example Semantic search across accessible concepts
|
|
65
|
+
# client.concepts.search(q: "vector databases", per_page: 5).each do |c|
|
|
66
|
+
# puts "#{c.name}: #{c.similarity}"
|
|
67
|
+
# end
|
|
68
|
+
#
|
|
69
|
+
# @see https://developers.forem.com/api/v1
|
|
70
|
+
class Concept < APIResource
|
|
71
|
+
extend APIOperations::List
|
|
72
|
+
extend APIOperations::Update
|
|
73
|
+
|
|
74
|
+
OBJECT_NAME = "concept"
|
|
75
|
+
RESOURCE_PATH = "/api/concepts"
|
|
76
|
+
|
|
77
|
+
# Retrieve a single concept by its numeric ID.
|
|
78
|
+
#
|
|
79
|
+
# Sends a GET request to +/api/concepts/:id+. Unlike the standard
|
|
80
|
+
# {APIOperations::Retrieve} mixin this accepts query params, because the
|
|
81
|
+
# endpoint takes a +days+ window that controls how many nested
|
|
82
|
+
# +daily_metrics+ entries are returned.
|
|
83
|
+
#
|
|
84
|
+
# @param id [Integer, String] the concept ID
|
|
85
|
+
# @param params [Hash] query parameters
|
|
86
|
+
# @option params [Integer] :days days of activity to include in
|
|
87
|
+
# +daily_metrics+ (default: 7, minimum: 1)
|
|
88
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
89
|
+
# @return [Forem::Concept] the concept with the given ID
|
|
90
|
+
# @example
|
|
91
|
+
# Forem::Concept.retrieve(7, { days: 30 }, requestor: requestor)
|
|
92
|
+
# @see https://developers.forem.com/api/v1
|
|
93
|
+
def self.retrieve(id, params = {}, opts = {})
|
|
94
|
+
requestor = opts[:requestor]
|
|
95
|
+
resp = request(:get, "#{resource_path}/#{id}", params, opts)
|
|
96
|
+
construct_from(resp.parsed_body, requestor: requestor)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Update an existing concept.
|
|
100
|
+
#
|
|
101
|
+
# Sends a PUT request to +/api/concepts/:id+. The Forem API expects the
|
|
102
|
+
# attributes to be nested under a +concept+ key, so flat params are
|
|
103
|
+
# wrapped automatically; already-wrapped params are passed through
|
|
104
|
+
# untouched.
|
|
105
|
+
#
|
|
106
|
+
# Only +score+, +description+, and +similarity_threshold+ are permitted
|
|
107
|
+
# by the API. Changing the description regenerates the concept's anchor
|
|
108
|
+
# embedding, and changing either the description or the similarity
|
|
109
|
+
# threshold enqueues a background re-classification of existing articles.
|
|
110
|
+
#
|
|
111
|
+
# @param id [Integer, String] the concept ID to update
|
|
112
|
+
# @param params [Hash] concept attributes to change
|
|
113
|
+
# @option params [Float] :score new curation score
|
|
114
|
+
# @option params [String] :description new semantic description
|
|
115
|
+
# @option params [Float] :similarity_threshold new cosine distance
|
|
116
|
+
# threshold (0.0–1.0)
|
|
117
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
118
|
+
# @return [Forem::Concept] the updated concept
|
|
119
|
+
# @example
|
|
120
|
+
# Forem::Concept.update(7, { similarity_threshold: 0.8 }, requestor: requestor)
|
|
121
|
+
# @see https://developers.forem.com/api/v1
|
|
122
|
+
def self.update(id, params = {}, opts = {})
|
|
123
|
+
super(id, wrap_params(params), opts)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Return the published articles classified under a concept.
|
|
127
|
+
#
|
|
128
|
+
# Sends a GET request to +/api/concepts/:id/articles+. Articles are
|
|
129
|
+
# ordered by cosine similarity to the concept (closest first), with the
|
|
130
|
+
# article score as a tiebreaker, unless +sort+ is +"score"+ in which case
|
|
131
|
+
# they are ordered by article score alone.
|
|
132
|
+
#
|
|
133
|
+
# @param id [Integer, String] the concept ID
|
|
134
|
+
# @param params [Hash] query parameters
|
|
135
|
+
# @option params [String] :sort +"score"+ to sort by article score;
|
|
136
|
+
# omitted or any other value sorts by semantic distance
|
|
137
|
+
# @option params [Integer] :page page number (default: 1)
|
|
138
|
+
# @option params [Integer] :per_page number of results per page
|
|
139
|
+
# (default: 10)
|
|
140
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
141
|
+
# @return [Forem::ListObject<Forem::Article>] paginated list of articles
|
|
142
|
+
# belonging to the concept
|
|
143
|
+
# @example
|
|
144
|
+
# articles = Forem::Concept.articles(7, { sort: "score" }, requestor: requestor)
|
|
145
|
+
# articles.each { |a| puts a.title }
|
|
146
|
+
# @see https://developers.forem.com/api/v1
|
|
147
|
+
def self.articles(id, params = {}, opts = {})
|
|
148
|
+
Forem::Article.paginated_list("#{resource_path}/#{id}/articles", params, opts)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Semantically search the concepts accessible to the caller.
|
|
152
|
+
#
|
|
153
|
+
# Sends a GET request to +/api/concepts/search+. The query text is
|
|
154
|
+
# embedded and compared against each concept's anchor embedding; results
|
|
155
|
+
# are returned closest-first and are not paginated (only the number of
|
|
156
|
+
# results is configurable). Each returned concept carries +distance+ and
|
|
157
|
+
# +similarity+ in addition to the usual concept fields.
|
|
158
|
+
#
|
|
159
|
+
# Requires an API key — unlike the other concept endpoints, this action
|
|
160
|
+
# cannot be called with a session user.
|
|
161
|
+
#
|
|
162
|
+
# @param params [Hash] query parameters
|
|
163
|
+
# @option params [String] :q (required) the search text
|
|
164
|
+
# @option params [Integer] :per_page number of concepts to return
|
|
165
|
+
# (default: 10, max: 50)
|
|
166
|
+
# @option params [Float] :threshold optional maximum cosine distance
|
|
167
|
+
# (0.0–2.0); concepts further away are filtered out
|
|
168
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
169
|
+
# @return [Array<Forem::Concept>] matching concepts, closest first
|
|
170
|
+
# @example
|
|
171
|
+
# results = Forem::Concept.search({ q: "databases" }, requestor: requestor)
|
|
172
|
+
# results.each { |c| puts "#{c.name} #{c.distance}" }
|
|
173
|
+
# @see https://developers.forem.com/api/v1
|
|
174
|
+
def self.search(params = {}, opts = {})
|
|
175
|
+
requestor = opts[:requestor]
|
|
176
|
+
resp = request(:get, "#{resource_path}/search", params, opts)
|
|
177
|
+
(resp.parsed_body || []).map { |item| construct_from(item, requestor: requestor) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Nest flat attributes under the +concept+ key expected by the API.
|
|
181
|
+
#
|
|
182
|
+
# @param params [Hash] the caller-supplied attributes
|
|
183
|
+
# @return [Hash] params wrapped in a +concept+ key, or unchanged if they
|
|
184
|
+
# already are
|
|
185
|
+
def self.wrap_params(params)
|
|
186
|
+
return params if params.key?(:concept) || params.key?("concept")
|
|
187
|
+
|
|
188
|
+
{ concept: params }
|
|
189
|
+
end
|
|
190
|
+
private_class_method :wrap_params
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Represents a follow relationship — a user, organization, or tag the
|
|
3
|
+
# authenticated user follows.
|
|
4
|
+
#
|
|
5
|
+
# The +list+ endpoint returns followed tags only (the route is
|
|
6
|
+
# +/api/follows/tags+). The +create+ endpoint follows users and/or
|
|
7
|
+
# organizations in bulk.
|
|
8
|
+
#
|
|
9
|
+
# @example List the tags the authenticated user is following
|
|
10
|
+
# follows = client.follows.list(per_page: 10)
|
|
11
|
+
# follows.each { |f| puts f.name }
|
|
12
|
+
#
|
|
13
|
+
# @example Follow users in bulk
|
|
14
|
+
# client.follows.create(user_ids: [42, 99])
|
|
15
|
+
# #=> #<Forem::Follow {"outcome" => "followed 2 users"}>
|
|
16
|
+
#
|
|
17
|
+
# @example Follow organizations in bulk
|
|
18
|
+
# client.follows.create(organization_ids: [7])
|
|
19
|
+
#
|
|
20
|
+
# @example Combined call
|
|
21
|
+
# client.follows.create(user_ids: [42], organization_ids: [7])
|
|
22
|
+
#
|
|
23
|
+
# @see https://developers.forem.com/api/v1#/operations/getFollowedTags
|
|
24
|
+
class Follow < APIResource
|
|
25
|
+
OBJECT_NAME = "follow"
|
|
26
|
+
RESOURCE_PATH = "/api/follows"
|
|
27
|
+
|
|
28
|
+
# Return a paginated list of tags followed by the authenticated user.
|
|
29
|
+
#
|
|
30
|
+
# Requires authentication. Sends a GET request to +/api/follows/tags+
|
|
31
|
+
# (the +list+ verb is mapped to the tags collection rather than the
|
|
32
|
+
# base resource path on this endpoint).
|
|
33
|
+
#
|
|
34
|
+
# @param params [Hash] query parameters
|
|
35
|
+
# @option params [Integer] :page page number (default: 1)
|
|
36
|
+
# @option params [Integer] :per_page number of results per page (default: 30)
|
|
37
|
+
# @param opts [Hash] per-request options
|
|
38
|
+
# @return [Forem::ListObject<Forem::Follow>] paginated list of followed tags
|
|
39
|
+
# @example
|
|
40
|
+
# follows = client.follows.list(per_page: 10)
|
|
41
|
+
# follows.each { |f| puts f.name }
|
|
42
|
+
# @see https://developers.forem.com/api/v1#/operations/getFollowedTags
|
|
43
|
+
def self.list(params = {}, opts = {})
|
|
44
|
+
paginated_list("/api/follows/tags", params, opts)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Follow one or more users and/or organizations on behalf of the
|
|
48
|
+
# authenticated user.
|
|
49
|
+
#
|
|
50
|
+
# Sends a POST request to +/api/follows+. The Forem API does *not*
|
|
51
|
+
# follow tags through this endpoint (unlike what {#list} returns). It
|
|
52
|
+
# accepts:
|
|
53
|
+
#
|
|
54
|
+
# * +user_ids:+ — Array<Integer> of user IDs to follow
|
|
55
|
+
# * +organization_ids:+ — Array<Integer> of organization IDs to follow
|
|
56
|
+
#
|
|
57
|
+
# The response is an outcome summary (e.g.
|
|
58
|
+
# +{"outcome" => "followed 2 users"}+), not a full follow record.
|
|
59
|
+
# If neither param is supplied, the API silently reports
|
|
60
|
+
# +"followed 0 users"+ — pass +user_ids+ or +organization_ids+
|
|
61
|
+
# explicitly.
|
|
62
|
+
#
|
|
63
|
+
# @param params [Hash] request body
|
|
64
|
+
# @option params [Array<Integer>] :user_ids list of user IDs to follow
|
|
65
|
+
# @option params [Array<Integer>] :organization_ids list of org IDs to follow
|
|
66
|
+
# @param opts [Hash] per-request options
|
|
67
|
+
# @return [Forem::Follow] the outcome object returned by the API
|
|
68
|
+
# @example
|
|
69
|
+
# client.follows.create(user_ids: [42, 99])
|
|
70
|
+
# #=> #<Forem::Follow {"outcome" => "followed 2 users"}>
|
|
71
|
+
# @see https://developers.forem.com/api/v1
|
|
72
|
+
def self.create(params = {}, opts = {})
|
|
73
|
+
requestor = opts[:requestor]
|
|
74
|
+
resp = request(:post, resource_path, params, opts)
|
|
75
|
+
construct_from(resp.parsed_body, requestor: requestor)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Represents a user who follows the authenticated user.
|
|
3
|
+
#
|
|
4
|
+
# A Follower record describes a user that has subscribed to the
|
|
5
|
+
# authenticated user's content. Only listing is supported — you cannot
|
|
6
|
+
# create or delete follower relationships through this resource directly.
|
|
7
|
+
#
|
|
8
|
+
# Requires authentication. Default: 80 per page.
|
|
9
|
+
#
|
|
10
|
+
# Note: the list endpoint hits +/api/followers/users+ (not +/api/followers+),
|
|
11
|
+
# and the default page size is 80 (larger than most other resources).
|
|
12
|
+
#
|
|
13
|
+
# @example List users who follow the authenticated user
|
|
14
|
+
# followers = client.followers.list
|
|
15
|
+
# followers.data.each { |f| puts f.name }
|
|
16
|
+
#
|
|
17
|
+
# @example Iterate over all followers using auto-pagination
|
|
18
|
+
# client.followers.list.auto_paging_each { |f| puts f.username }
|
|
19
|
+
#
|
|
20
|
+
# @see https://developers.forem.com/api/v1#/operations/getFollowers
|
|
21
|
+
class Follower < APIResource
|
|
22
|
+
OBJECT_NAME = "follower"
|
|
23
|
+
RESOURCE_PATH = "/api/followers"
|
|
24
|
+
|
|
25
|
+
# Return a paginated list of users who follow the authenticated user.
|
|
26
|
+
#
|
|
27
|
+
# Requires authentication. Default: 80 per page.
|
|
28
|
+
#
|
|
29
|
+
# Sends a GET request to +/api/followers/users+.
|
|
30
|
+
#
|
|
31
|
+
# @param params [Hash] query parameters
|
|
32
|
+
# @option params [Integer] :page page number (default: 1)
|
|
33
|
+
# @option params [Integer] :per_page number of results per page (default: 80)
|
|
34
|
+
# @option params [String] :sort sort order; default +'created_at'+; use
|
|
35
|
+
# +'-created_at'+ for newest first
|
|
36
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
37
|
+
# @return [Forem::ListObject<Forem::Follower>] paginated list of followers
|
|
38
|
+
# @example
|
|
39
|
+
# followers = client.followers.list(per_page: 25, sort: "name")
|
|
40
|
+
# followers.data.each { |f| puts f.name }
|
|
41
|
+
# @see https://developers.forem.com/api/v1#/operations/getFollowers
|
|
42
|
+
def self.list(params = {}, opts = {})
|
|
43
|
+
requestor = opts[:requestor]
|
|
44
|
+
resp = request(:get, "/api/followers/users", params, opts)
|
|
45
|
+
data = (resp.parsed_body || []).map { |item| construct_from(item) }
|
|
46
|
+
per_page = params[:per_page] || params["per_page"] || 80
|
|
47
|
+
page = params[:page] || params["page"] || 1
|
|
48
|
+
ListObject.new(
|
|
49
|
+
data: data, current_page: page.to_i, per_page: per_page.to_i,
|
|
50
|
+
resource_class: self,
|
|
51
|
+
filters: params.reject { |k, _| [:page, :per_page, "page", "per_page"].include?(k) },
|
|
52
|
+
requestor: requestor
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Provides access to the health-check endpoints of a Forem instance.
|
|
3
|
+
#
|
|
4
|
+
# Health checks let you verify that the various subsystems of a Forem
|
|
5
|
+
# deployment are operational. There are three separate checks: the
|
|
6
|
+
# application server, the database, and the cache layer.
|
|
7
|
+
#
|
|
8
|
+
# == Authentication
|
|
9
|
+
#
|
|
10
|
+
# In production, the endpoints require an +health-check-token+ header
|
|
11
|
+
# whose value matches the instance's
|
|
12
|
+
# +Settings::General.health_check_token+ setting. The Forem controller
|
|
13
|
+
# bypasses the token check for requests originating from +localhost+,
|
|
14
|
+
# so a local-development instance accepts unauthenticated calls.
|
|
15
|
+
#
|
|
16
|
+
# Pass the token via the +token:+ keyword on each method (or on the
|
|
17
|
+
# service-level wrapper). The +api-key+ header is *not* used by these
|
|
18
|
+
# endpoints — the token is a separate, dedicated mechanism.
|
|
19
|
+
#
|
|
20
|
+
# @example Production: pass the configured token
|
|
21
|
+
# client.health_checks.app(token: ENV.fetch("FOREM_HEALTH_CHECK_TOKEN"))
|
|
22
|
+
# #=> #<Forem::ForemObject {"message" => "App is up!"}>
|
|
23
|
+
#
|
|
24
|
+
# @example Local-development Forem (token bypassed for localhost)
|
|
25
|
+
# client.health_checks.app
|
|
26
|
+
# #=> #<Forem::ForemObject {"message" => "App is up!"}>
|
|
27
|
+
#
|
|
28
|
+
# @see https://developers.forem.com/api/v1
|
|
29
|
+
class HealthCheck < APIResource
|
|
30
|
+
OBJECT_NAME = "health_check"
|
|
31
|
+
RESOURCE_PATH = "/api/health_checks"
|
|
32
|
+
|
|
33
|
+
# Application-server health.
|
|
34
|
+
# @param token [String, nil] value for the +health-check-token+ header
|
|
35
|
+
# (required in production).
|
|
36
|
+
# @param opts [Hash] per-request options
|
|
37
|
+
# @return [Forem::ForemObject] response body
|
|
38
|
+
def self.app(token: nil, **opts)
|
|
39
|
+
check("/api/health_checks/app", token, opts)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Database connectivity & responsiveness.
|
|
43
|
+
# @param token [String, nil] value for the +health-check-token+ header.
|
|
44
|
+
# @param opts [Hash] per-request options
|
|
45
|
+
# @return [Forem::ForemObject] response body
|
|
46
|
+
def self.database(token: nil, **opts)
|
|
47
|
+
check("/api/health_checks/database", token, opts)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Cache (Redis) connectivity & responsiveness.
|
|
51
|
+
# @param token [String, nil] value for the +health-check-token+ header.
|
|
52
|
+
# @param opts [Hash] per-request options
|
|
53
|
+
# @return [Forem::ForemObject] response body
|
|
54
|
+
def self.cache(token: nil, **opts)
|
|
55
|
+
check("/api/health_checks/cache", token, opts)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.check(path, token, opts)
|
|
59
|
+
requestor = opts[:requestor]
|
|
60
|
+
opts = opts.dup
|
|
61
|
+
if token
|
|
62
|
+
existing = opts[:headers] || {}
|
|
63
|
+
opts[:headers] = existing.merge("health-check-token" => token)
|
|
64
|
+
end
|
|
65
|
+
resp = request(:get, path, {}, opts)
|
|
66
|
+
Forem::ForemObject.construct_from(resp.parsed_body, requestor: requestor)
|
|
67
|
+
end
|
|
68
|
+
private_class_method :check
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Represents a Forem organization (a group account that can publish articles).
|
|
3
|
+
#
|
|
4
|
+
# Organizations are team or company accounts on a Forem instance. They
|
|
5
|
+
# support full CRUD operations and expose sub-collection endpoints for
|
|
6
|
+
# listing their members and articles. Organizations can be retrieved by
|
|
7
|
+
# either numeric ID or username slug.
|
|
8
|
+
#
|
|
9
|
+
# Available operations (via mixins):
|
|
10
|
+
# - +List+ — GET /api/organizations (default: 10 per page)
|
|
11
|
+
# - +Create+ — POST /api/organizations
|
|
12
|
+
# - +Retrieve+ — GET /api/organizations/:id_or_username
|
|
13
|
+
# - +Update+ — PUT /api/organizations/:id
|
|
14
|
+
# - +Delete+ — instance-level delete
|
|
15
|
+
# - +Save+ — instance-level save (create or update)
|
|
16
|
+
#
|
|
17
|
+
# @example List all organizations
|
|
18
|
+
# orgs = client.organizations.list(per_page: 20)
|
|
19
|
+
# orgs.data.each { |o| puts o.name }
|
|
20
|
+
#
|
|
21
|
+
# @example Create an organization
|
|
22
|
+
# org = client.organizations.create(
|
|
23
|
+
# organization: { name: "Acme Corp", summary: "We make things." }
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# @example Retrieve an organization by username
|
|
27
|
+
# org = client.organizations.retrieve("acme-corp")
|
|
28
|
+
# puts org.name
|
|
29
|
+
#
|
|
30
|
+
# @see https://developers.forem.com/api/v1#/operations/getOrganizations
|
|
31
|
+
# @see https://developers.forem.com/api/v1#/operations/createOrganization
|
|
32
|
+
class Organization < APIResource
|
|
33
|
+
extend APIOperations::Create
|
|
34
|
+
extend APIOperations::List
|
|
35
|
+
extend APIOperations::Retrieve
|
|
36
|
+
extend APIOperations::Update
|
|
37
|
+
include APIOperations::Delete
|
|
38
|
+
include APIOperations::Save
|
|
39
|
+
|
|
40
|
+
OBJECT_NAME = "organization"
|
|
41
|
+
RESOURCE_PATH = "/api/organizations"
|
|
42
|
+
|
|
43
|
+
# Return the members of this organization.
|
|
44
|
+
#
|
|
45
|
+
# Sends a GET request to +/api/organizations/:id/users+.
|
|
46
|
+
#
|
|
47
|
+
# @param params [Hash] query parameters
|
|
48
|
+
# @option params [Integer] :page page number (default: 1)
|
|
49
|
+
# @option params [Integer] :per_page number of results per page (default: 30)
|
|
50
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
51
|
+
# @return [Array<Forem::User>] members of the organization
|
|
52
|
+
# @example
|
|
53
|
+
# org = client.organizations.retrieve("acme-corp")
|
|
54
|
+
# org.users.each { |u| puts u.username }
|
|
55
|
+
# @see https://developers.forem.com/api/v1#/operations/getOrgUsers
|
|
56
|
+
def users(params = {}, opts = {})
|
|
57
|
+
resp = request(:get, "#{resource_url}/users", params, opts)
|
|
58
|
+
(resp.parsed_body || []).map { |item| Forem::User.construct_from(item) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return articles published under this organization.
|
|
62
|
+
#
|
|
63
|
+
# Sends a GET request to +/api/organizations/:id/articles+.
|
|
64
|
+
#
|
|
65
|
+
# @param params [Hash] query parameters
|
|
66
|
+
# @option params [Integer] :page page number (default: 1)
|
|
67
|
+
# @option params [Integer] :per_page number of results per page (default: 30)
|
|
68
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
69
|
+
# @return [Array<Forem::Article>] articles belonging to the organization
|
|
70
|
+
# @example
|
|
71
|
+
# org = client.organizations.retrieve("acme-corp")
|
|
72
|
+
# org.articles.each { |a| puts a.title }
|
|
73
|
+
# @see https://developers.forem.com/api/v1
|
|
74
|
+
def articles(params = {}, opts = {})
|
|
75
|
+
resp = request(:get, "#{resource_url}/articles", params, opts)
|
|
76
|
+
(resp.parsed_body || []).map { |item| Forem::Article.construct_from(item) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Represents a custom static page on a Forem instance.
|
|
3
|
+
#
|
|
4
|
+
# Pages are CMS-style content pages on a Forem instance (e.g., about
|
|
5
|
+
# pages, code of conduct).
|
|
6
|
+
#
|
|
7
|
+
# Pages are standalone content items (not articles) used for things like
|
|
8
|
+
# an "About" page, a "Code of Conduct", or any other informational page
|
|
9
|
+
# that lives at a dedicated URL on the community site. Full CRUD operations
|
|
10
|
+
# are available, but managing pages requires an admin API key.
|
|
11
|
+
#
|
|
12
|
+
# Available operations (via mixins):
|
|
13
|
+
# - +List+ — GET /api/pages
|
|
14
|
+
# - +Create+ — POST /api/pages
|
|
15
|
+
# - +Retrieve+ — GET /api/pages/:id
|
|
16
|
+
# - +Update+ — PUT /api/pages/:id
|
|
17
|
+
# - +Delete+ — instance-level delete (DELETE /api/pages/:id)
|
|
18
|
+
# - +Save+ — instance-level save (create or update)
|
|
19
|
+
#
|
|
20
|
+
# @example List all pages
|
|
21
|
+
# pages = client.pages.list
|
|
22
|
+
# pages.each { |p| puts "#{p.slug}: #{p.title}" }
|
|
23
|
+
#
|
|
24
|
+
# @example Create a new page (flat params — no +page:+ wrapper)
|
|
25
|
+
# page = client.pages.create(
|
|
26
|
+
# title: "About Us",
|
|
27
|
+
# slug: "about",
|
|
28
|
+
# body_markdown: "We are a community of developers.",
|
|
29
|
+
# is_top_level_path: true
|
|
30
|
+
# )
|
|
31
|
+
#
|
|
32
|
+
# @example Retrieve a page by ID
|
|
33
|
+
# page = client.pages.retrieve(3)
|
|
34
|
+
# puts page.title
|
|
35
|
+
#
|
|
36
|
+
# @example Delete a page
|
|
37
|
+
# page = client.pages.retrieve(3)
|
|
38
|
+
# page.delete
|
|
39
|
+
#
|
|
40
|
+
# @see https://developers.forem.com/api/v1
|
|
41
|
+
class Page < APIResource
|
|
42
|
+
extend APIOperations::Create
|
|
43
|
+
extend APIOperations::List
|
|
44
|
+
extend APIOperations::Retrieve
|
|
45
|
+
extend APIOperations::Update
|
|
46
|
+
include APIOperations::Delete
|
|
47
|
+
include APIOperations::Save
|
|
48
|
+
|
|
49
|
+
OBJECT_NAME = "page"
|
|
50
|
+
RESOURCE_PATH = "/api/pages"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Represents a podcast episode published on a Forem instance.
|
|
3
|
+
#
|
|
4
|
+
# Podcast episodes are audio content items associated with a podcast channel.
|
|
5
|
+
# The public API supports listing episodes, optionally filtered by username
|
|
6
|
+
# (the podcast owner) or a specific podcast slug.
|
|
7
|
+
#
|
|
8
|
+
# Returns active episodes from published podcasts, ordered by descending
|
|
9
|
+
# publication date. Default: 30 per page.
|
|
10
|
+
#
|
|
11
|
+
# Available operations (via mixins):
|
|
12
|
+
# - +List+ — GET /api/podcast_episodes
|
|
13
|
+
#
|
|
14
|
+
# @example List all podcast episodes
|
|
15
|
+
# episodes = client.podcast_episodes.list(per_page: 30)
|
|
16
|
+
# episodes.data.each { |ep| puts ep.title }
|
|
17
|
+
#
|
|
18
|
+
# @example List episodes for a specific podcast
|
|
19
|
+
# episodes = client.podcast_episodes.list(username: "codenewbie")
|
|
20
|
+
# episodes.data.each { |ep| puts ep.title }
|
|
21
|
+
#
|
|
22
|
+
# == List Parameters
|
|
23
|
+
#
|
|
24
|
+
# - +username+ (String) — Retrieve episodes from a specific podcast
|
|
25
|
+
# (e.g., +'codenewbie'+)
|
|
26
|
+
# - +page+ (Integer) — Page number (default: 1)
|
|
27
|
+
# - +per_page+ (Integer) — Items per page (default: 30)
|
|
28
|
+
#
|
|
29
|
+
# @see https://developers.forem.com/api/v1#/operations/getPodcastEpisodes
|
|
30
|
+
class PodcastEpisode < APIResource
|
|
31
|
+
extend APIOperations::List
|
|
32
|
+
|
|
33
|
+
OBJECT_NAME = "podcast_episode"
|
|
34
|
+
RESOURCE_PATH = "/api/podcast_episodes"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
|
|
3
|
+
module Forem
|
|
4
|
+
# Represents the profile image associated with a Forem user or organization.
|
|
5
|
+
#
|
|
6
|
+
# Retrieve a user's profile image URL by their username.
|
|
7
|
+
#
|
|
8
|
+
# Profile images are avatar graphics that appear alongside user and
|
|
9
|
+
# organization names across the Forem UI. This resource exposes a single
|
|
10
|
+
# retrieve operation that looks up the image by username (or organization
|
|
11
|
+
# slug) and returns URLs to the image at various sizes.
|
|
12
|
+
#
|
|
13
|
+
# @example Retrieve a user's profile image
|
|
14
|
+
# image = client.profile_images.retrieve("alice")
|
|
15
|
+
# puts image.profile_image
|
|
16
|
+
# puts image.profile_image_90
|
|
17
|
+
#
|
|
18
|
+
# @example Retrieve an organization's profile image
|
|
19
|
+
# image = client.profile_images.retrieve("acme-corp")
|
|
20
|
+
# puts image.profile_image
|
|
21
|
+
#
|
|
22
|
+
# @see https://developers.forem.com/api/v1
|
|
23
|
+
class ProfileImage < APIResource
|
|
24
|
+
OBJECT_NAME = "profile_image"
|
|
25
|
+
RESOURCE_PATH = "/api/profile_images"
|
|
26
|
+
|
|
27
|
+
# Retrieve the profile image URLs for a user or organization.
|
|
28
|
+
#
|
|
29
|
+
# Sends a GET request to +/api/profile_images/:username+.
|
|
30
|
+
#
|
|
31
|
+
# @param username [String] the username or organization slug to look up
|
|
32
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
33
|
+
# @return [Forem::ProfileImage] object containing +profile_image+ and +profile_image_90+ URL fields
|
|
34
|
+
# @example
|
|
35
|
+
# image = client.profile_images.retrieve("alice")
|
|
36
|
+
# puts image.profile_image # full-size URL
|
|
37
|
+
# puts image.profile_image_90 # 90px thumbnail URL
|
|
38
|
+
# @see https://developers.forem.com/api/v1
|
|
39
|
+
def self.retrieve(username, opts = {})
|
|
40
|
+
resp = request(:get, "#{resource_path}/#{CGI.escape(username)}", {}, opts)
|
|
41
|
+
construct_from(resp.parsed_body)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module Forem
|
|
2
|
+
# Represents a reaction (like, unicorn, exploding head, etc.) on a Forem article or comment.
|
|
3
|
+
#
|
|
4
|
+
# Toggle user's reaction to a reactable (Article, Comment, or User). First
|
|
5
|
+
# call creates the reaction; second call removes it.
|
|
6
|
+
#
|
|
7
|
+
# Reactions are emoji-style responses that authenticated users can add to
|
|
8
|
+
# articles or comments. The create endpoint adds a reaction, and the toggle
|
|
9
|
+
# endpoint adds it if it does not exist or removes it if it already does.
|
|
10
|
+
#
|
|
11
|
+
# Available operations (via mixins):
|
|
12
|
+
# - +Create+ — POST /api/reactions
|
|
13
|
+
#
|
|
14
|
+
# @example Create a reaction on an article
|
|
15
|
+
# client.reactions.create(
|
|
16
|
+
# reactable_id: 12345,
|
|
17
|
+
# reactable_type: "Article",
|
|
18
|
+
# category: "like"
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example Toggle a reaction (add if absent, remove if present)
|
|
22
|
+
# result = client.reactions.toggle(
|
|
23
|
+
# reactable_id: 12345,
|
|
24
|
+
# reactable_type: "Article",
|
|
25
|
+
# category: "unicorn"
|
|
26
|
+
# )
|
|
27
|
+
# puts result.result # => "create" or "destroy"
|
|
28
|
+
#
|
|
29
|
+
# @see https://developers.forem.com/api/v1
|
|
30
|
+
class Reaction < APIResource
|
|
31
|
+
extend APIOperations::Create
|
|
32
|
+
|
|
33
|
+
OBJECT_NAME = "reaction"
|
|
34
|
+
RESOURCE_PATH = "/api/reactions"
|
|
35
|
+
|
|
36
|
+
# Toggle a reaction on a reactable object.
|
|
37
|
+
#
|
|
38
|
+
# If the authenticated user has not yet reacted with the given category,
|
|
39
|
+
# the reaction is created. If they have already reacted, it is removed.
|
|
40
|
+
# Sends a POST request to +/api/reactions/toggle+.
|
|
41
|
+
#
|
|
42
|
+
# @param params [Hash] request body
|
|
43
|
+
# @option params [String] :category the reaction type (e.g., +"like"+, +"readinglist"+)
|
|
44
|
+
# @option params [Integer] :reactable_id the ID of the object to react to
|
|
45
|
+
# @option params [String] :reactable_type One of: +"Article"+, +"Comment"+, +"User"+
|
|
46
|
+
# @param opts [Hash] per-request options (e.g., +:api_key+)
|
|
47
|
+
# @return [Forem::Reaction] reaction object with a +result+ field (++"create"++ or ++"destroy"++)
|
|
48
|
+
# @example
|
|
49
|
+
# result = client.reactions.toggle(
|
|
50
|
+
# reactable_id: 42,
|
|
51
|
+
# reactable_type: "Article",
|
|
52
|
+
# category: "fire"
|
|
53
|
+
# )
|
|
54
|
+
# puts result.result # => "create" or "destroy"
|
|
55
|
+
# @see https://developers.forem.com/api/v1
|
|
56
|
+
def self.toggle(params = {}, opts = {})
|
|
57
|
+
resp = request(:post, "/api/reactions/toggle", params, opts)
|
|
58
|
+
construct_from(resp.parsed_body)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|