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,151 @@
1
+ module Forem
2
+ # Provides access to analytics data for the authenticated user's content.
3
+ #
4
+ # All analytics endpoints require authentication. They return analytics for
5
+ # the calling user (or an organization, when +:organization_id+ is passed).
6
+ #
7
+ # The Analytics resource exposes four read-only reporting endpoints:
8
+ # cumulative totals, day-by-day historical data, yesterday's aggregates,
9
+ # and traffic referrer breakdowns.
10
+ #
11
+ # == Response shapes
12
+ #
13
+ # The Forem analytics endpoints don't all return the same kind of object,
14
+ # so this resource preserves each endpoint's natural shape rather than
15
+ # forcing them into a uniform list:
16
+ #
17
+ # * {.totals} — single nested-stats object
18
+ # * {.historical} — +Hash+ keyed by +"YYYY-MM-DD"+ date string
19
+ # * {.past_day} — +Hash+ keyed by +"YYYY-MM-DD"+ (typically 1–2 entries)
20
+ # * {.referrers} — +Array+ of referrer objects (extracted from +domains+)
21
+ #
22
+ # @example Lifetime totals
23
+ # totals = client.analytics.totals
24
+ # puts totals.reactions.total #=> 7
25
+ # puts totals.page_views.total #=> 7
26
+ #
27
+ # @example Historical data for a date range
28
+ # history = client.analytics.historical(start: "2026-04-01", end: "2026-04-30")
29
+ # history.each do |date, stats|
30
+ # puts "#{date}: #{stats.page_views.total} views"
31
+ # end
32
+ #
33
+ # @example Referrer breakdown
34
+ # client.analytics.referrers.each { |r| puts "#{r.domain}: #{r.count}" }
35
+ #
36
+ # @see https://developers.forem.com/api/v1
37
+ class Analytics < APIResource
38
+ OBJECT_NAME = "analytics"
39
+ RESOURCE_PATH = "/api/analytics"
40
+
41
+ # Return cumulative analytics totals for the authenticated user (or org).
42
+ #
43
+ # The response groups totals into nested buckets — +comments+, +follows+,
44
+ # +reactions+, and +page_views+ — each with its own sub-fields. To read
45
+ # a count, navigate one level deeper than you might expect (e.g.
46
+ # +totals.reactions.total+, not +totals.reactions+).
47
+ #
48
+ # @param params [Hash] query parameters
49
+ # @option params [String] :article_id filter to a specific article
50
+ # @option params [Integer] :organization_id ID of the organization
51
+ # @param opts [Hash] per-request options
52
+ # @return [Forem::ForemObject] cumulative stats with nested sub-objects
53
+ # @example
54
+ # totals = client.analytics.totals
55
+ # puts totals.reactions.total #=> 7
56
+ # puts totals.reactions.like #=> 2
57
+ # puts totals.page_views.total #=> 7
58
+ # @see https://developers.forem.com/api/v1
59
+ def self.totals(params = {}, opts = {})
60
+ requestor = opts[:requestor]
61
+ resp = request(:get, "/api/analytics/totals", params, opts)
62
+ Forem::ForemObject.construct_from(resp.parsed_body, requestor: requestor)
63
+ end
64
+
65
+ # Return day-by-day historical analytics for a given date range.
66
+ #
67
+ # The response is a +Hash+ keyed by the calendar date (+"YYYY-MM-DD"+).
68
+ # Each value is a stats object with the same nested shape as {.totals}.
69
+ #
70
+ # @param params [Hash] query parameters
71
+ # @option params [String] :start (required) start date in YYYY-MM-DD form.
72
+ # @option params [String] :end end date in YYYY-MM-DD form.
73
+ # @option params [String] :article_id filter to a specific article.
74
+ # @option params [Integer] :organization_id ID of the organization.
75
+ # @param opts [Hash] per-request options
76
+ # @return [Hash{String => Forem::ForemObject}] stats keyed by date.
77
+ # @example
78
+ # history = client.analytics.historical(start: "2026-04-01", end: "2026-04-15")
79
+ # history.each do |date, stats|
80
+ # puts "#{date}: #{stats.page_views.total} views"
81
+ # end
82
+ # @see https://developers.forem.com/api/v1
83
+ def self.historical(params = {}, opts = {})
84
+ requestor = opts[:requestor]
85
+ resp = request(:get, "/api/analytics/historical", params, opts)
86
+ grouped_by_day(resp.parsed_body, requestor)
87
+ end
88
+
89
+ # Return aggregated analytics for the previous calendar day(s).
90
+ #
91
+ # The response shape mirrors {.historical}: a +Hash+ keyed by date.
92
+ # Forem typically returns one or two entries (yesterday plus today's
93
+ # partial), so callers usually want either the most recent date or
94
+ # iteration with +#each+.
95
+ #
96
+ # @param params [Hash] query parameters
97
+ # @option params [String] :article_id filter to a specific article.
98
+ # @option params [Integer] :organization_id ID of the organization.
99
+ # @param opts [Hash] per-request options
100
+ # @return [Hash{String => Forem::ForemObject}] stats keyed by date.
101
+ # @example
102
+ # stats = client.analytics.past_day
103
+ # latest_date, latest_stats = stats.max_by { |date, _| date }
104
+ # puts "#{latest_date}: #{latest_stats.page_views.total} views"
105
+ # @see https://developers.forem.com/api/v1
106
+ def self.past_day(params = {}, opts = {})
107
+ requestor = opts[:requestor]
108
+ resp = request(:get, "/api/analytics/past_day", params, opts)
109
+ grouped_by_day(resp.parsed_body, requestor)
110
+ end
111
+
112
+ # Return the breakdown of traffic referrers for the authenticated user's
113
+ # content.
114
+ #
115
+ # The endpoint wraps the data in a +{"domains" => [...]}+ envelope. This
116
+ # method unwraps that envelope and returns the inner array directly so
117
+ # callers can iterate without an extra hop.
118
+ #
119
+ # @param params [Hash] query parameters
120
+ # @option params [String] :start start date in YYYY-MM-DD form.
121
+ # @option params [String] :end end date in YYYY-MM-DD form.
122
+ # @option params [String] :article_id filter to a specific article.
123
+ # @option params [Integer] :organization_id ID of the organization.
124
+ # @param opts [Hash] per-request options
125
+ # @return [Array<Forem::ForemObject>] referrer objects with +domain+ and
126
+ # +count+ fields.
127
+ # @example
128
+ # client.analytics.referrers.each { |r| puts "#{r.domain}: #{r.count}" }
129
+ # @see https://developers.forem.com/api/v1
130
+ def self.referrers(params = {}, opts = {})
131
+ requestor = opts[:requestor]
132
+ resp = request(:get, "/api/analytics/referrers", params, opts)
133
+ body = resp.parsed_body
134
+ domains = body.is_a?(Hash) ? (body["domains"] || []) : []
135
+ domains.map { |item| Forem::ForemObject.construct_from(item, requestor: requestor) }
136
+ end
137
+
138
+ # Internal: convert the date-keyed response of historical/past_day into a
139
+ # Hash<String, ForemObject>. Preserves +nil+ stat values (which the API
140
+ # emits for some days) by converting them to an empty ForemObject.
141
+ #
142
+ # @api private
143
+ def self.grouped_by_day(body, requestor)
144
+ return {} unless body.is_a?(Hash)
145
+ body.each_with_object({}) do |(date, stats), out|
146
+ out[date] = Forem::ForemObject.construct_from(stats || {}, requestor: requestor)
147
+ end
148
+ end
149
+ private_class_method :grouped_by_day
150
+ end
151
+ end
@@ -0,0 +1,256 @@
1
+ require "cgi"
2
+
3
+ module Forem
4
+ # Represents a Forem article (blog post).
5
+ #
6
+ # Articles are the core content type on Forem. They support full CRUD
7
+ # operations plus several custom endpoints for filtering by auth state,
8
+ # latest publication order, and full-text search.
9
+ #
10
+ # Available operations (via mixins):
11
+ # - +List+ — GET /api/articles
12
+ # - +Create+ — POST /api/articles
13
+ # - +Retrieve+ — GET /api/articles/:id
14
+ # - +Update+ — PUT /api/articles/:id
15
+ # - +Save+ — instance-level save (create or update)
16
+ #
17
+ # == List Parameters
18
+ #
19
+ # When calling +Article.list+, the following query parameters are supported:
20
+ #
21
+ # - +tag+ (String) — Filter by tag; can combine with +top+
22
+ # - +tags+ (String) — Comma-separated tags; returns articles with ANY of these
23
+ # - +tags_exclude+ (String) — Comma-separated tags to exclude
24
+ # - +username+ (String) — Filter by user or organization username
25
+ # - +state+ (String) — One of: +"fresh"+, +"rising"+, +"all"+.
26
+ # Note: +state=all+ only works combined with +username+ and returns up to 1000 items
27
+ # - +top+ (Integer) — Most popular articles in the last N days; can combine with +tag+
28
+ # - +collection_id+ (Integer) — Articles in a specific collection, ordered by publication date
29
+ # - +page+ (Integer) — Page number (default: 1)
30
+ # - +per_page+ (Integer) — Items per page (default: 30, max: 1000)
31
+ #
32
+ # == Create Parameters
33
+ #
34
+ # When calling +Article.create+, wrap all fields under the +:article+ key:
35
+ #
36
+ # @option params [Hash] :article the article payload
37
+ # @option params [String] 'article.title' (required)
38
+ # @option params [String] 'article.body_markdown' (required)
39
+ # @option params [String] 'article.description' (required)
40
+ # @option params [Boolean] 'article.published' (default: false)
41
+ # @option params [String] 'article.tags' comma-separated tags
42
+ # @option params [String] 'article.series' series name (nullable)
43
+ # @option params [String] 'article.canonical_url' (nullable)
44
+ # @option params [String] 'article.main_image' (nullable)
45
+ # @option params [Integer] 'article.organization_id' (nullable)
46
+ #
47
+ # @example List published articles
48
+ # articles = client.articles.list(per_page: 10, tag: "ruby")
49
+ # articles.data.each { |a| puts a.title }
50
+ #
51
+ # @example Create a new article
52
+ # article = client.articles.create(
53
+ # article: { title: "Hello World", body_markdown: "# Hello", published: false }
54
+ # )
55
+ #
56
+ # @example Retrieve a single article by ID
57
+ # article = client.articles.retrieve(12345)
58
+ # puts article.title
59
+ #
60
+ # @see https://developers.forem.com/api/v1#/operations/getArticles
61
+ # @see https://developers.forem.com/api/v1#/operations/createArticle
62
+ # @see https://developers.forem.com/api/v1#/operations/updateArticle
63
+ class Article < APIResource
64
+ extend APIOperations::Create
65
+ extend APIOperations::List
66
+ extend APIOperations::Retrieve
67
+ extend APIOperations::Update
68
+ include APIOperations::Save
69
+
70
+ OBJECT_NAME = "article"
71
+ RESOURCE_PATH = "/api/articles"
72
+
73
+ # Unpublish this article, reverting it to draft status.
74
+ #
75
+ # Marks the article as draft. Keeps content, deletes notifications,
76
+ # preserves comments. Requires admin or moderator role.
77
+ #
78
+ # Sends a PUT request to +/api/articles/:id/unpublish+.
79
+ #
80
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
81
+ # @return [ForemResponse] the raw API response
82
+ # @example
83
+ # article = client.articles.retrieve(42)
84
+ # article.unpublish
85
+ # @see https://developers.forem.com/api/v1#/operations/unpublishArticle
86
+ def unpublish(opts = {})
87
+ request(:put, "#{resource_url}/unpublish", {}, opts)
88
+ end
89
+
90
+ # Return articles authored by the authenticated user (all statuses).
91
+ #
92
+ # Requires authentication. Returns articles in reverse chronological order,
93
+ # 30 per page.
94
+ #
95
+ # Sends a GET request to +/api/articles/me+.
96
+ #
97
+ # @param params [Hash] query parameters
98
+ # @option params [Integer] :page page number (default: 1)
99
+ # @option params [Integer] :per_page number of results per page (default: 30)
100
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
101
+ # @return [Forem::ListObject<Forem::Article>] paginated list of the
102
+ # authenticated user's articles
103
+ # @example
104
+ # my_articles = client.articles.me(per_page: 5)
105
+ # my_articles.each { |a| puts "#{a.id}: #{a.title}" }
106
+ # @see https://developers.forem.com/api/v1#/operations/getUserArticles
107
+ def self.me(params = {}, opts = {})
108
+ paginated_list("/api/articles/me", params, opts)
109
+ end
110
+
111
+ # Return published articles authored by the authenticated user.
112
+ #
113
+ # Requires authentication. Returns articles in reverse chronological order,
114
+ # 30 per page.
115
+ #
116
+ # Sends a GET request to +/api/articles/me/published+.
117
+ #
118
+ # @param params [Hash] query parameters
119
+ # @option params [Integer] :page page number (default: 1)
120
+ # @option params [Integer] :per_page number of results per page (default: 30)
121
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
122
+ # @return [Forem::ListObject<Forem::Article>] paginated list of the
123
+ # authenticated user's published articles
124
+ # @example
125
+ # published = client.articles.me_published(per_page: 20)
126
+ # published.each { |a| puts a.title }
127
+ # @see https://developers.forem.com/api/v1#/operations/getUserArticles
128
+ def self.me_published(params = {}, opts = {})
129
+ paginated_list("/api/articles/me/published", params, opts)
130
+ end
131
+
132
+ # Return unpublished (draft) articles authored by the authenticated user.
133
+ #
134
+ # Requires authentication. Returns articles in reverse chronological order,
135
+ # 30 per page.
136
+ #
137
+ # Sends a GET request to +/api/articles/me/unpublished+.
138
+ #
139
+ # @param params [Hash] query parameters
140
+ # @option params [Integer] :page page number (default: 1)
141
+ # @option params [Integer] :per_page number of results per page (default: 30)
142
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
143
+ # @return [Forem::ListObject<Forem::Article>] paginated list of the
144
+ # authenticated user's unpublished articles
145
+ # @example
146
+ # drafts = client.articles.me_unpublished
147
+ # drafts.each { |a| puts "Draft: #{a.title}" }
148
+ # @see https://developers.forem.com/api/v1#/operations/getUserArticles
149
+ def self.me_unpublished(params = {}, opts = {})
150
+ paginated_list("/api/articles/me/unpublished", params, opts)
151
+ end
152
+
153
+ # Return all articles authored by the authenticated user regardless of status.
154
+ #
155
+ # Requires authentication. Returns articles in reverse chronological order,
156
+ # 30 per page.
157
+ #
158
+ # Sends a GET request to +/api/articles/me/all+.
159
+ #
160
+ # @param params [Hash] query parameters
161
+ # @option params [Integer] :page page number (default: 1)
162
+ # @option params [Integer] :per_page number of results per page (default: 30)
163
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
164
+ # @return [Forem::ListObject<Forem::Article>] paginated list of all
165
+ # articles (published + drafts) for the authenticated user
166
+ # @example
167
+ # all = client.articles.me_all
168
+ # all.auto_paging_each { |a| puts a.title }
169
+ # @see https://developers.forem.com/api/v1#/operations/getUserArticles
170
+ def self.me_all(params = {}, opts = {})
171
+ paginated_list("/api/articles/me/all", params, opts)
172
+ end
173
+
174
+ # Return the most recently published articles ordered by publication date.
175
+ #
176
+ # Sends a GET request to +/api/articles/latest+.
177
+ #
178
+ # @param params [Hash] query parameters
179
+ # @option params [Integer] :page page number (default: 1)
180
+ # @option params [Integer] :per_page number of results per page (default: 30)
181
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
182
+ # @return [Forem::ListObject<Forem::Article>] paginated list of latest
183
+ # published articles
184
+ # @example
185
+ # latest = client.articles.latest(per_page: 5)
186
+ # latest.each { |a| puts "#{a.published_at}: #{a.title}" }
187
+ # @see https://developers.forem.com/api/v1#/operations/getLatestArticles
188
+ def self.latest(params = {}, opts = {})
189
+ paginated_list("/api/articles/latest", params, opts)
190
+ end
191
+
192
+ # Search published articles by keyword.
193
+ #
194
+ # Sends a GET request to +/api/articles/search+.
195
+ #
196
+ # @param params [Hash] query parameters
197
+ # @option params [String] :q search query string
198
+ # @option params [Integer] :page page number (default: 1)
199
+ # @option params [Integer] :per_page number of results per page (default: 30)
200
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
201
+ # @return [Forem::ListObject<Forem::Article>] paginated list of articles
202
+ # matching the search query
203
+ # @example
204
+ # results = client.articles.search(q: "ruby on rails")
205
+ # results.each { |a| puts a.title }
206
+ # @see https://developers.forem.com/api/v1
207
+ def self.search(params = {}, opts = {})
208
+ paginated_list("/api/articles/search", params, opts)
209
+ end
210
+
211
+ # Semantically search published articles using Forem's embeddings-based search.
212
+ #
213
+ # Unlike +search+ (keyword matching), this endpoint finds articles whose
214
+ # meaning is close to the query text, using vector similarity. Requires
215
+ # authentication.
216
+ #
217
+ # Sends a GET request to +/api/articles/semantic_search+.
218
+ #
219
+ # @param params [Hash] query parameters
220
+ # @option params [String] :q the search query text (required)
221
+ # @option params [Integer] :page page number (default: 1)
222
+ # @option params [Integer] :per_page number of results per page (default: 10, max: 50)
223
+ # @option params [Float] :threshold optional cosine distance threshold (0.0-2.0) used
224
+ # to filter out weakly-related results
225
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
226
+ # @return [Forem::ListObject<Forem::Article>] paginated list of articles
227
+ # matching the query, ordered by relevance. Each article additionally
228
+ # exposes +distance+ (cosine distance) and +similarity+ (1 - distance)
229
+ # attributes.
230
+ # @example
231
+ # results = client.articles.semantic_search(q: "how to deploy rails apps")
232
+ # results.each { |a| puts "#{a.title} (#{a.similarity})" }
233
+ # @see https://developers.forem.com/api/v1
234
+ def self.semantic_search(params = {}, opts = {})
235
+ paginated_list("/api/articles/semantic_search", params, opts)
236
+ end
237
+
238
+ # Retrieve a single article by the author's username and the article's slug.
239
+ #
240
+ # Sends a GET request to +/api/articles/:username/:slug+.
241
+ #
242
+ # @param username [String] the author's Forem username
243
+ # @param slug [String] the article slug (the URL-friendly title segment)
244
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
245
+ # @return [Forem::Article] the matching article
246
+ # @example
247
+ # article = client.articles.retrieve_by_path("ben", "my-first-post")
248
+ # puts article.title
249
+ # @see https://developers.forem.com/api/v1#/operations/getArticleByPath
250
+ def self.retrieve_by_path(username, slug, opts = {})
251
+ requestor = opts[:requestor]
252
+ resp = request(:get, "/api/articles/#{CGI.escape(username)}/#{CGI.escape(slug)}", {}, opts)
253
+ construct_from(resp.parsed_body, requestor: requestor)
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,76 @@
1
+ module Forem
2
+ # Represents a Forem billboard (display advertisement).
3
+ #
4
+ # Billboards are configurable ad units shown to readers on a Forem instance.
5
+ # They support full CRUD operations plus an +unpublish+ action that reverts
6
+ # a live billboard back to draft status. Managing billboards requires an
7
+ # admin API key.
8
+ #
9
+ # Available operations (via mixins):
10
+ # - +List+ — GET /api/billboards
11
+ # - +Create+ — POST /api/billboards
12
+ # - +Retrieve+ — GET /api/billboards/:id
13
+ # - +Update+ — PUT /api/billboards/:id
14
+ # - +Save+ — instance-level save (create or update)
15
+ #
16
+ # == Billboard Fields
17
+ #
18
+ # - +name+ (String) — Internal name to distinguish ads
19
+ # - +body_markdown+ (String, required) — The ad content in markdown
20
+ # - +approved+ (Boolean) — Must be both published AND approved to appear in rotation
21
+ # - +published+ (Boolean) — Must be both published AND approved to appear in rotation
22
+ # - +expires_at+ (String) — ISO 8601 timestamp; automatically unapproved after this time
23
+ # - +placement_area+ (String) — Which area of the site layout the ad appears in
24
+ # - +tag_list+ (String) — Tags on which the ad can display (blank = all tags)
25
+ # - +exclude_article_ids+ (String) — Comma-separated Article IDs where ad should NOT appear
26
+ # - +audience_segment_id+ (Integer) — Target a specific audience segment
27
+ # - +audience_segment_type+ (String) — Must match +audience_segment_id+ if both provided
28
+ # - +target_geolocations+ (Array) — ISO 3166-2 country/region codes (blank = all locations)
29
+ # - +display_to+ (String) — Limits which visitors see the ad
30
+ # - +type_of+ (String) — One of: +"in_house"+ (admin-created), +"community"+ (entity content), +"external"+ (everywhere)
31
+ #
32
+ # @example List all billboards
33
+ # billboards = client.billboards.list
34
+ # billboards.data.each { |b| puts "#{b.id}: #{b.name}" }
35
+ #
36
+ # @example Create a billboard (flat params — there is no +billboard:+ wrapper)
37
+ # billboard = client.billboards.create(
38
+ # name: "Summer Sale",
39
+ # body_markdown: "**50% off** all plans!",
40
+ # placement_area: "sidebar_left"
41
+ # )
42
+ #
43
+ # @example Retrieve a billboard by ID
44
+ # billboard = client.billboards.retrieve(7)
45
+ # puts billboard.name
46
+ #
47
+ # @see https://developers.forem.com/api/v1
48
+ class Billboard < APIResource
49
+ extend APIOperations::Create
50
+ extend APIOperations::List
51
+ extend APIOperations::Retrieve
52
+ extend APIOperations::Update
53
+ include APIOperations::Save
54
+
55
+ OBJECT_NAME = "billboard"
56
+ RESOURCE_PATH = "/api/billboards"
57
+
58
+ # Unpublish this billboard, reverting it to draft/inactive status.
59
+ #
60
+ # Marks the billboard as unpublished. A billboard must be both published
61
+ # AND approved to appear in rotation.
62
+ #
63
+ # Sends a PUT request to +/api/billboards/:id/unpublish+.
64
+ # Requires admin privileges.
65
+ #
66
+ # @param opts [Hash] per-request options (e.g., +:api_key+)
67
+ # @return [ForemResponse] the raw API response
68
+ # @example
69
+ # billboard = client.billboards.retrieve(7)
70
+ # billboard.unpublish
71
+ # @see https://developers.forem.com/api/v1
72
+ def unpublish(opts = {})
73
+ request(:put, "#{resource_url}/unpublish", {}, opts)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,43 @@
1
+ module Forem
2
+ # Represents a comment on a Forem article or podcast episode.
3
+ #
4
+ # Comments are threaded replies attached to articles or podcast episodes.
5
+ # They can be listed (optionally filtered by the parent content item) or
6
+ # retrieved individually by ID.
7
+ #
8
+ # Available operations (via mixins):
9
+ # - +List+ — GET /api/comments (pass +:a_id+ or +:p_id+ to filter)
10
+ # - +Retrieve+ — GET /api/comments/:id (returns comment tree rooted at that ID)
11
+ #
12
+ # == List Parameters
13
+ #
14
+ # Comments must be filtered by either article or podcast episode:
15
+ # - +a_id+ (String) — Article identifier (required if +p_id+ not provided)
16
+ # - +p_id+ (String) — Podcast Episode identifier (required if +a_id+ not provided)
17
+ # - +page+ (Integer) — Page number
18
+ # - +per_page+ (Integer) — Items per page (default: 50)
19
+ #
20
+ # Returns threaded conversations with nested replies.
21
+ #
22
+ # @example List all comments for an article
23
+ # comments = client.comments.list(a_id: "some-article-id")
24
+ # comments.data.each { |c| puts c.body_html }
25
+ #
26
+ # @example List comments for a podcast episode
27
+ # comments = client.comments.list(p_id: "some-podcast-id")
28
+ # comments.data.each { |c| puts c.body_html }
29
+ #
30
+ # @example Retrieve a comment tree by root comment ID
31
+ # comment = client.comments.retrieve("abc123")
32
+ # puts comment.body_html
33
+ #
34
+ # @see https://developers.forem.com/api/v1#/operations/getCommentsByArticleId
35
+ # @see https://developers.forem.com/api/v1#/operations/getCommentById
36
+ class Comment < APIResource
37
+ extend APIOperations::List
38
+ extend APIOperations::Retrieve
39
+
40
+ OBJECT_NAME = "comment"
41
+ RESOURCE_PATH = "/api/comments"
42
+ end
43
+ end