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,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