From 31cfe04aab80adfc9601ef78185964ad6980fa36 Mon Sep 17 00:00:00 2001 From: Ali Hesari Date: Sun, 20 Sep 2026 01:59:54 +0200 Subject: [PATCH] feat: Reddit destinations, the subreddit check and the inbox vote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit accounts() gains the four Reddit reads and writes — the subreddits an account is in, a subreddit's rules and post flairs, and the default subreddit — and validate().subreddit() answers the same existence check before a post exists. inbox().vote() votes an item up or down, or takes the vote back. Account.reconnectRequired, and the inbox item's vote and canVote, are new components on records that already existed. SubredditCheck.over18 carries an explicit alias: the wire spells it over_18 there, and neither the snake strategy nor the camelCase alias crosses a digit boundary. --- README.md | 8 +- .../java/com/fopost/sdk/model/Account.java | 7 +- .../java/com/fopost/sdk/model/InboxItem.java | 2 + .../sdk/model/RedditDefaultSubreddit.java | 8 ++ .../com/fopost/sdk/model/RedditFlair.java | 8 ++ .../com/fopost/sdk/model/RedditFlairs.java | 6 ++ .../com/fopost/sdk/model/RedditSubreddit.java | 17 ++++ .../fopost/sdk/model/RedditSubredditRule.java | 4 + .../sdk/model/RedditSubredditRules.java | 6 ++ .../com/fopost/sdk/model/SubredditCheck.java | 18 ++++ .../fopost/sdk/resource/AccountsResource.java | 45 +++++++++ .../fopost/sdk/resource/InboxResource.java | 12 +++ .../fopost/sdk/resource/ValidateResource.java | 14 +++ src/test/java/com/fopost/sdk/RedditTest.java | 97 +++++++++++++++++++ 14 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/fopost/sdk/model/RedditDefaultSubreddit.java create mode 100644 src/main/java/com/fopost/sdk/model/RedditFlair.java create mode 100644 src/main/java/com/fopost/sdk/model/RedditFlairs.java create mode 100644 src/main/java/com/fopost/sdk/model/RedditSubreddit.java create mode 100644 src/main/java/com/fopost/sdk/model/RedditSubredditRule.java create mode 100644 src/main/java/com/fopost/sdk/model/RedditSubredditRules.java create mode 100644 src/main/java/com/fopost/sdk/model/SubredditCheck.java create mode 100644 src/test/java/com/fopost/sdk/RedditTest.java diff --git a/README.md b/README.md index 50b5bf6..4aab69e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ long failed = client.posts().stream(PostListParams.create().workspaceId(workspac | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `posts()` | `list`, `autoPaginate`, `stream`, `get`, `create`, `update`, `delete`, `duplicate`, `publish`, `retry`, `cancel`, `preflight`, `deliveries`, `publishRuns`, `analytics`, `bulkShift`, `bulkLabel`, `bulkDelete`, `validateImport`, `commitImport`, `rollbackImport` | | `workspaces()` | `list`, `get`, `create`, `update`, `delete`, `analytics` | -| `accounts()` | `list`, `get`, `create`, `rename`, `move`, `delete`, `healthSummary`, `health`, `togglePrimary`, `validate`, `refreshToken`, `analytics`, `createTelegramConnectCode`, `getTelegramConnectStatus`, `getTelegramBotCommands`, `setTelegramBotCommands`, `deleteTelegramBotCommands`, `listSlackChannels`, `listSlackMembers`, `getSlackIdentity`, `updateSlackIdentity`, `getIceBreakers`, `setIceBreakers`, `deleteIceBreakers`, `getPersistentMenu`, `setPersistentMenu`, `deletePersistentMenu`, `getGreeting`, `setGreeting`, `deleteGreeting`, `getWebhookSubscription`, `resubscribeWebhook`, `communities()`, `listDiscordChannels`, `switchDiscordChannel`, `getDiscordIdentity`, `updateDiscordIdentity`, `listDiscordPins`, `deleteDiscordMessage`, `pinDiscordMessage`, `unpinDiscordMessage`, `crosspostDiscordMessage`, `createDiscordThread`, `sendDiscordDm`, `listDiscordEvents`, `getDiscordEvent`, `createDiscordEvent`, `updateDiscordEvent`, `deleteDiscordEvent`, `listDiscordMembers`, `getDiscordMember`, `listDiscordRoles`, `createDiscordRole`, `updateDiscordRole`, `deleteDiscordRole`, `addDiscordMemberRole`, `removeDiscordMemberRole`, `platformMetrics` | +| `accounts()` | `list`, `get`, `create`, `rename`, `move`, `delete`, `healthSummary`, `health`, `togglePrimary`, `validate`, `refreshToken`, `analytics`, `createTelegramConnectCode`, `getTelegramConnectStatus`, `getTelegramBotCommands`, `setTelegramBotCommands`, `deleteTelegramBotCommands`, `redditSubreddits`, `redditSubredditRules`, `redditFlairs`, `setRedditDefaultSubreddit`, `listSlackChannels`, `listSlackMembers`, `getSlackIdentity`, `updateSlackIdentity`, `getIceBreakers`, `setIceBreakers`, `deleteIceBreakers`, `getPersistentMenu`, `setPersistentMenu`, `deletePersistentMenu`, `getGreeting`, `setGreeting`, `deleteGreeting`, `getWebhookSubscription`, `resubscribeWebhook`, `communities()`, `listDiscordChannels`, `switchDiscordChannel`, `getDiscordIdentity`, `updateDiscordIdentity`, `listDiscordPins`, `deleteDiscordMessage`, `pinDiscordMessage`, `unpinDiscordMessage`, `crosspostDiscordMessage`, `createDiscordThread`, `sendDiscordDm`, `listDiscordEvents`, `getDiscordEvent`, `createDiscordEvent`, `updateDiscordEvent`, `deleteDiscordEvent`, `listDiscordMembers`, `getDiscordMember`, `listDiscordRoles`, `createDiscordRole`, `updateDiscordRole`, `deleteDiscordRole`, `addDiscordMemberRole`, `removeDiscordMemberRole`, `platformMetrics` | | `accountGroups()` | `list`, `get`, `create`, `update`, `delete`, `setMembers` | | `labels()` | `list`, `get`, `create`, `update`, `delete` | | `webhooks()` | `list`, `create`, `update`, `delete`, `test`, `verify` | @@ -108,19 +108,19 @@ long failed = client.posts().stream(PostListParams.create().workspaceId(workspac | `automations()` | `list`, `get`, `create`, `update`, `delete`, `toggle`, `runs`, `run`, `trigger`, `stats` | | `media()` | `list`, `limits`, `upload`, `presign`, `complete`, `uploadDirect`, `delete` | | `ai()` | `credits`, `generateCaption`, `rewrite`, `repurposeUrl` | -| `inbox()` | `list`, `threads`, `conversations`, `unreadCount`, `accounts`, `platforms`, `markThreadRead`, `markConversationRead`, `refresh`, `update`, `editComment`, `reply`, `hide`, `unhide`, `delete`, `like`, `unlike`, `pin`, `unpin`, `react`, `startConversation`, `setTyping`, `passThreadControl`, `takeThreadControl`, `handover`, `listApprovals`, `approveReply`, `rejectReply` | +| `inbox()` | `list`, `threads`, `conversations`, `unreadCount`, `accounts`, `platforms`, `markThreadRead`, `markConversationRead`, `refresh`, `update`, `editComment`, `reply`, `hide`, `unhide`, `delete`, `like`, `unlike`, `vote`, `pin`, `unpin`, `react`, `startConversation`, `setTyping`, `passThreadControl`, `takeThreadControl`, `handover`, `listApprovals`, `approveReply`, `rejectReply` | | `contacts()` | `list`, `get`, `create`, `update`, `delete`, `conversations`, `importCsv`, `listFields`, `createField`, `updateField`, `updateFieldOptions`, `deleteField`, `conversationAnalytics` | | `broadcasts()` | `list`, `get`, `create`, `update`, `delete`, `send`, `cancel`, `recipients` | | `sequences()` | `list`, `get`, `create`, `update`, `delete`, `enroll`, `unenroll`, `enrollments` | | `knowledge()` | `list`, `create`, `createText`, `createUrl`, `createFile`, `update`, `delete`, `sync`, `search` | | `ads()` | `list`, `external`, `boostable`, `connections`, `sources`, `authorizeMeta`, `deleteConnection`, `boost`, `create`, `refresh`, `setStatus`, `delete`, `accountTree`, `createCampaign`, `campaign`, `updateCampaign`, `deleteCampaign`, `duplicateCampaign`, `createAdSet`, `adSet`, `updateAdSet`, `deleteAdSet`, `duplicateAdSet`, `createNetworkAd`, `networkAd`, `updateNetworkAd`, `deleteNetworkAd`, `duplicateNetworkAd`, `bulkSetStatus`, `creatives`, `createCreative`, `creative`, `deleteCreative`, `estimateReach`, `insights`, `adInsights`, `audiences`, `createAudience`, `audience`, `updateAudience`, `deleteAudience`, `addAudienceUsers`, `searchTargeting`, `leadForms`, `createLeadForm`, `leadForm`, `archiveLeadForm`, `leads`, `leadsFeed`, `leadPages`, `subscribeLeadPage`, `unsubscribeLeadPage`, `goals`, `catalogs`, `createCatalog`, `catalog`, `updateCatalog`, `deleteCatalog`, `catalogProducts`, `writeCatalogProducts`, `productFeeds`, `createProductFeed`, `deleteProductFeed`, `feedUploads`, `startFeedUpload`, `productSets`, `createProductSet`, `updateProductSet`, `deleteProductSet`, `reachFrequency`, `createReachFrequency`, `reachFrequencyPrediction`, `reserveReachFrequency`, `cancelReachFrequency`, `library`, `partnershipCreators`, `requestPartnership`, `revokePartnership`, `accountActivity`, `labels`, `createLabel`, `updateLabel`, `deleteLabel`, `applyLabel`, `studies`, `createStudy`, `study`, `deleteStudy`, `iosCampaignLimits`, `highDemandPeriods`, `createHighDemandPeriod`, `deleteHighDemandPeriod`, `valueRuleSets`, `createValueRuleSet`, `deleteValueRuleSet` | -| `inbox()` | `list`, `threads`, `conversations`, `unreadCount`, `accounts`, `platforms`, `markThreadRead`, `markConversationRead`, `refresh`, `update`, `editComment`, `reply`, `hide`, `unhide`, `delete`, `like`, `unlike`, `pin`, `unpin`, `react`, `startConversation`, `setTyping`, `listApprovals`, `approveReply`, `rejectReply` | +| `inbox()` | `list`, `threads`, `conversations`, `unreadCount`, `accounts`, `platforms`, `markThreadRead`, `markConversationRead`, `refresh`, `update`, `editComment`, `reply`, `hide`, `unhide`, `delete`, `like`, `unlike`, `vote`, `pin`, `unpin`, `react`, `startConversation`, `setTyping`, `listApprovals`, `approveReply`, `rejectReply` | | `ads()` | `list`, `external`, `boostable`, `connections`, `sources`, `authorizeMeta`, `deleteConnection`, `boost`, `create`, `refresh`, `setStatus`, `delete`, `accountTree`, `createCampaign`, `campaign`, `updateCampaign`, `deleteCampaign`, `duplicateCampaign`, `createAdSet`, `adSet`, `updateAdSet`, `deleteAdSet`, `duplicateAdSet`, `createNetworkAd`, `networkAd`, `updateNetworkAd`, `deleteNetworkAd`, `duplicateNetworkAd`, `bulkSetStatus`, `creatives`, `createCreative`, `creative`, `deleteCreative`, `estimateReach`, `insights`, `adInsights`, `audiences`, `createAudience`, `audience`, `updateAudience`, `deleteAudience`, `addAudienceUsers`, `searchTargeting`, `leadForms`, `createLeadForm`, `leadForm`, `archiveLeadForm`, `leads`, `leadsFeed`, `leadPages`, `subscribeLeadPage`, `unsubscribeLeadPage` | | `whatsapp()` | `getProfile`, `updateProfile`, `requestDisplayName`, `setUsername`, `listTemplates`, `listTemplateLibrary`, `getTemplate`, `createTemplate`, `importTemplate`, `updateTemplate`, `deleteTemplate`, `listGroups`, `createGroup`, `getGroup`, `updateGroup`, `deleteGroup`, `getGroupInviteLink`, `resetGroupInviteLink`, `removeGroupParticipants`, `listBlocked`, `blockUsers`, `unblockUsers`, `getCommerceSettings`, `updateCommerceSettings`, `linkCatalog`, `listFlows`, `getFlow`, `createFlow`, `updateFlow`, `deleteFlow`, `uploadFlowJson`, `publishFlow`, `deprecateFlow`, `listFlowResponses`, `getEncryptionKeyStatus`, `setEncryptionKey`, `getAccountEvents`, `listSandboxSessions`, `createSandboxSession` | | `ads()` | `list`, `external`, `boostable`, `connections`, `sources`, `providers`, `authorize`, `deleteConnection`, `boost`, `create`, `refresh`, `setStatus`, `delete`, `accountTree`, `createCampaign`, `campaign`, `updateCampaign`, `deleteCampaign`, `duplicateCampaign`, `createAdSet`, `adSet`, `updateAdSet`, `deleteAdSet`, `duplicateAdSet`, `createNetworkAd`, `networkAd`, `updateNetworkAd`, `deleteNetworkAd`, `duplicateNetworkAd`, `bulkSetStatus`, `creatives`, `createCreative`, `creative`, `deleteCreative`, `estimateReach`, `insights`, `adInsights`, `audiences`, `createAudience`, `audience`, `updateAudience`, `deleteAudience`, `addAudienceUsers`, `addAudienceCompanies`, `searchTargeting`, `leadForms`, `createLeadForm`, `leadForm`, `archiveLeadForm`, `leads`, `leadsFeed`, `leadPages`, `subscribeLeadPage`, `unsubscribeLeadPage`, `bidPricing`, `supplyForecast`, `conversionRules`, `createConversionRule`, `conversionRule`, `updateConversionRule`, `deleteConversionRule`, `attachConversionRule`, `detachConversionRule`, `conversionMetrics`, `sendConversionEvents` | | `googleAds()` | `recommendations`, `optimizationScore`, `applyRecommendations`, `dismissRecommendations`, `keywords`, `createKeyword`, `updateKeyword`, `deleteKeyword`, `keywordIdeas`, `keywordMetrics`, `searchTerms`, `bidStrategies`, `createBidStrategy`, `adSchedule`, `setAdSchedule`, `negativeKeywordLists`, `createNegativeKeywordList`, `addNegativeKeywords`, `attachNegativeKeywordList`, `assets`, `createAsset`, `attachAsset`, `deleteAsset`, `assetGroups`, `createAssetGroup`, `updateAssetGroup`, `deleteAssetGroup`, `localServicesLeads`, `conversionActions`, `createConversionAction`, `uploadConversions`, `uploadConversionAdjustments`, `query` | | `googleBusiness()` | `getLocation`, `updateLocation`, `getAttributes`, `updateAttributes`, `getMenus`, `replaceMenus`, `getServices`, `replaceServices`, `listMedia`, `addMedia`, `deleteMedia`, `listPlaceActions`, `createPlaceAction`, `updatePlaceAction`, `deletePlaceAction`, `getVerificationOptions`, `startVerification`, `completeVerification`, `getPerformance`, `getSearchKeywords`, `assign` | -| `validate()` | `post`, `length`, `media` | +| `validate()` | `post`, `length`, `media`, `subreddit` | | `activity()` | `list` | `accounts().communities()` covers the X communities an account can post into: `list`, `sync`, diff --git a/src/main/java/com/fopost/sdk/model/Account.java b/src/main/java/com/fopost/sdk/model/Account.java index 8708d18..416f965 100644 --- a/src/main/java/com/fopost/sdk/model/Account.java +++ b/src/main/java/com/fopost/sdk/model/Account.java @@ -6,8 +6,11 @@ * A connected social account. * *

Named {@code Account} for the platform account, not the billing account. Which fields - * arrive depends on the endpoint: the list gives health and primary state, the detail gives - * timestamps and the owning workspace. {@code name} is the display name when one is set, and + * arrive depends on the endpoint: the list gives health, primary state and + * {@code reconnectRequired}, the detail gives timestamps and the owning workspace. + * + *

{@code reconnectRequired} is true when the account was connected before a permission it + * now needs was asked for; reconnecting it is the fix. {@code name} is the display name when one is set, and * {@code platformName} is always the name from the platform. */ public record Account( diff --git a/src/main/java/com/fopost/sdk/model/InboxItem.java b/src/main/java/com/fopost/sdk/model/InboxItem.java index 3f5660a..f358909 100644 --- a/src/main/java/com/fopost/sdk/model/InboxItem.java +++ b/src/main/java/com/fopost/sdk/model/InboxItem.java @@ -41,10 +41,12 @@ public record InboxItem( Boolean canHide, Boolean canDelete, Boolean liked, + String vote, Boolean pinned, String reaction, Instant editedAt, Boolean canLike, + Boolean canVote, Boolean canPin, Boolean canEdit, Boolean canReact, diff --git a/src/main/java/com/fopost/sdk/model/RedditDefaultSubreddit.java b/src/main/java/com/fopost/sdk/model/RedditDefaultSubreddit.java new file mode 100644 index 0000000..455e788 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/RedditDefaultSubreddit.java @@ -0,0 +1,8 @@ +package com.fopost.sdk.model; + +/** + * Where posts from a Reddit account go when a post names no subreddit. + * + *

A null {@code subreddit} means the account's own profile page, which always accepts a post. + */ +public record RedditDefaultSubreddit(String subreddit) {} diff --git a/src/main/java/com/fopost/sdk/model/RedditFlair.java b/src/main/java/com/fopost/sdk/model/RedditFlair.java new file mode 100644 index 0000000..acd4f26 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/RedditFlair.java @@ -0,0 +1,8 @@ +package com.fopost.sdk.model; + +/** + * A post flair, valid only in the subreddit it came from. + * + *

{@code editable} means the label may be replaced with your own text. + */ +public record RedditFlair(String id, String text, Boolean editable) {} diff --git a/src/main/java/com/fopost/sdk/model/RedditFlairs.java b/src/main/java/com/fopost/sdk/model/RedditFlairs.java new file mode 100644 index 0000000..710d320 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/RedditFlairs.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +import java.util.List; + +/** The post flairs one subreddit offers. */ +public record RedditFlairs(String subreddit, List flairs) {} diff --git a/src/main/java/com/fopost/sdk/model/RedditSubreddit.java b/src/main/java/com/fopost/sdk/model/RedditSubreddit.java new file mode 100644 index 0000000..3a89fc3 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/RedditSubreddit.java @@ -0,0 +1,17 @@ +package com.fopost.sdk.model; + +/** + * A subreddit a Reddit account is in, or its own profile page. + * + *

{@code name} carries no {@code r/} prefix. {@code canPost} is false where the account may + * read but not submit, and {@code isDefault} marks where posts go when a post names no subreddit. + */ +public record RedditSubreddit( + String name, + String title, + Long subscribers, + Boolean over18, + Boolean canPost, + Boolean flairEnabled, + String iconUrl, + Boolean isDefault) {} diff --git a/src/main/java/com/fopost/sdk/model/RedditSubredditRule.java b/src/main/java/com/fopost/sdk/model/RedditSubredditRule.java new file mode 100644 index 0000000..e9ebc28 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/RedditSubredditRule.java @@ -0,0 +1,4 @@ +package com.fopost.sdk.model; + +/** One rule a subreddit publishes. {@code appliesTo} is {@code link}, {@code comment} or {@code all}. */ +public record RedditSubredditRule(String name, String description, String appliesTo) {} diff --git a/src/main/java/com/fopost/sdk/model/RedditSubredditRules.java b/src/main/java/com/fopost/sdk/model/RedditSubredditRules.java new file mode 100644 index 0000000..d5028fd --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/RedditSubredditRules.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +import java.util.List; + +/** A subreddit's rules, in its own order. Show them before publishing. */ +public record RedditSubredditRules(String subreddit, List rules) {} diff --git a/src/main/java/com/fopost/sdk/model/SubredditCheck.java b/src/main/java/com/fopost/sdk/model/SubredditCheck.java new file mode 100644 index 0000000..f9f5627 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/SubredditCheck.java @@ -0,0 +1,18 @@ +package com.fopost.sdk.model; + +import com.fasterxml.jackson.annotation.JsonAlias; + +/** + * Whether a subreddit exists and takes a post from a named Reddit account. + * + *

{@code ok} is true when both hold. A private, banned or missing subreddit answers 200 with + * {@code exists} false rather than an error. + */ +public record SubredditCheck( + String subreddit, + Boolean exists, + Boolean canPost, + // Neither the snake strategy nor the camel alias covers a digit boundary. + @JsonAlias("over_18") Boolean over18, + Boolean flairEnabled, + Boolean ok) {} diff --git a/src/main/java/com/fopost/sdk/resource/AccountsResource.java b/src/main/java/com/fopost/sdk/resource/AccountsResource.java index 05fa483..dbb7728 100644 --- a/src/main/java/com/fopost/sdk/resource/AccountsResource.java +++ b/src/main/java/com/fopost/sdk/resource/AccountsResource.java @@ -32,6 +32,10 @@ import com.fopost.sdk.model.InstagramStoryInsights; import com.fopost.sdk.model.LinkedInMention; import com.fopost.sdk.model.PinterestBoard; +import com.fopost.sdk.model.RedditDefaultSubreddit; +import com.fopost.sdk.model.RedditFlairs; +import com.fopost.sdk.model.RedditSubreddit; +import com.fopost.sdk.model.RedditSubredditRules; import com.fopost.sdk.model.SlackChannel; import com.fopost.sdk.model.SlackIdentity; import com.fopost.sdk.model.SlackMember; @@ -554,6 +558,47 @@ private DiscordAck ack(JsonNode body) { return http.convert(ApiClient.unwrap(body), DiscordAck.class); } + /** + * Subreddits a Reddit account is in, busiest first, plus its own profile page. + * + *

A 409 with code {@code reconnect_required} means the grant is short of a permission + * this read needs, and the account has to be reconnected. + */ + public List redditSubreddits(String accountId) { + return http.convertList( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/reddit/subreddits", null)), + RedditSubreddit.class); + } + + /** The rules a subreddit publishes, in its own order. Show them before publishing. */ + public RedditSubredditRules redditSubredditRules(String accountId, String subreddit) { + String path = "/v1/accounts/" + accountId + "/reddit/subreddits/" + subreddit + "/rules"; + return http.convert(ApiClient.unwrap(http.get(path, null)), RedditSubredditRules.class); + } + + /** + * Post flairs one subreddit offers. A flair id is valid only there, and one from elsewhere + * fails preflight. + */ + public RedditFlairs redditFlairs(String accountId, String subreddit) { + return http.convert( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/reddit/flairs", + query("subreddit", subreddit))), + RedditFlairs.class); + } + + /** + * Where posts from this account go when a post names no subreddit. Pass null to fall back to + * the account's own profile page, which always accepts a post. + */ + public RedditDefaultSubreddit setRedditDefaultSubreddit(String accountId, String subreddit) { + Map body = new LinkedHashMap<>(); + body.put("subreddit", subreddit); + return http.convert( + ApiClient.unwrap(http.put("/v1/accounts/" + accountId + "/reddit/default-subreddit", body)), + RedditDefaultSubreddit.class); + } + private static Map query(String key, Object value) { if (value == null) { return null; diff --git a/src/main/java/com/fopost/sdk/resource/InboxResource.java b/src/main/java/com/fopost/sdk/resource/InboxResource.java index 11ad9e1..ef1320f 100644 --- a/src/main/java/com/fopost/sdk/resource/InboxResource.java +++ b/src/main/java/com/fopost/sdk/resource/InboxResource.java @@ -199,6 +199,18 @@ public InboxItem like(String itemId) { return http.convert(ApiClient.unwrap(http.post("/v1/inbox/" + itemId + "/like", null)), InboxItem.class); } + /** + * Vote an item up or down where the network ranks by votes (Reddit). {@code direction} is + * {@code up}, {@code down}, or {@code none} to take an earlier vote back. Only where + * {@code canVote} is true. An upvote is the same call {@code like} makes, so {@code liked} + * moves with it. + */ + public InboxItem vote(String itemId, String direction) { + Map body = new LinkedHashMap<>(); + body.put("direction", direction); + return http.convert(ApiClient.unwrap(http.post("/v1/inbox/" + itemId + "/vote", body)), InboxItem.class); + } + public InboxItem unlike(String itemId) { return http.convert(ApiClient.unwrap(http.post("/v1/inbox/" + itemId + "/unlike", null)), InboxItem.class); } diff --git a/src/main/java/com/fopost/sdk/resource/ValidateResource.java b/src/main/java/com/fopost/sdk/resource/ValidateResource.java index 4e17f85..55ccb50 100644 --- a/src/main/java/com/fopost/sdk/resource/ValidateResource.java +++ b/src/main/java/com/fopost/sdk/resource/ValidateResource.java @@ -4,6 +4,7 @@ import com.fopost.sdk.model.LengthValidation; import com.fopost.sdk.model.MediaValidation; import com.fopost.sdk.model.PostValidation; +import com.fopost.sdk.model.SubredditCheck; import com.fopost.sdk.param.ValidatePostParams; import java.util.LinkedHashMap; import java.util.List; @@ -53,4 +54,17 @@ public MediaValidation media(String url) { body.put("url", url); return http.convert(ApiClient.unwrap(http.post("/v1/validate/media", body)), MediaValidation.class); } + + /** + * Whether a subreddit exists and takes a post from a connected Reddit account. + * + *

The check runs with that account's own token, so {@code accountId} is required. + */ + public SubredditCheck subreddit(String accountId, String name) { + Map query = new LinkedHashMap<>(); + query.put("account_id", accountId); + query.put("name", name); + return http.convert( + ApiClient.unwrap(http.get("/v1/validate/subreddit", query)), SubredditCheck.class); + } } diff --git a/src/test/java/com/fopost/sdk/RedditTest.java b/src/test/java/com/fopost/sdk/RedditTest.java new file mode 100644 index 0000000..a413c34 --- /dev/null +++ b/src/test/java/com/fopost/sdk/RedditTest.java @@ -0,0 +1,97 @@ +package com.fopost.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fopost.sdk.model.InboxItem; +import com.fopost.sdk.model.RedditDefaultSubreddit; +import com.fopost.sdk.model.RedditFlairs; +import com.fopost.sdk.model.RedditSubreddit; +import com.fopost.sdk.model.RedditSubredditRules; +import com.fopost.sdk.model.SubredditCheck; +import java.util.List; +import org.junit.jupiter.api.Test; + +class RedditTest { + + @Test + void subredditsRulesAndFlairsReadTheCamelCaseWire() { + FakeTransport transport = new FakeTransport() + .enqueue(200, """ + {"data":[{"name":"webdev","title":"Web Development","subscribers":2000000, + "over18":false,"canPost":true,"flairEnabled":true, + "iconUrl":null,"isDefault":true}]}""") + .enqueue(200, """ + {"data":{"subreddit":"webdev","rules":[{"name":"No self promotion", + "description":"Keep it useful","appliesTo":"link"}]}}""") + .enqueue(200, """ + {"data":{"subreddit":"webdev","flairs":[{"id":"flair-1", + "text":"Showoff Saturday","editable":false}]}}"""); + FoPost client = TestSupport.client(transport); + + List subreddits = client.accounts().redditSubreddits("a1"); + assertEquals("https://api.fopost.test/v1/accounts/a1/reddit/subreddits", transport.last().url()); + assertEquals("webdev", subreddits.get(0).name()); + assertTrue(subreddits.get(0).isDefault()); + assertTrue(subreddits.get(0).flairEnabled()); + + RedditSubredditRules rules = client.accounts().redditSubredditRules("a1", "webdev"); + assertEquals( + "https://api.fopost.test/v1/accounts/a1/reddit/subreddits/webdev/rules", + transport.last().url()); + assertEquals("link", rules.rules().get(0).appliesTo()); + + RedditFlairs flairs = client.accounts().redditFlairs("a1", "webdev"); + assertEquals( + "https://api.fopost.test/v1/accounts/a1/reddit/flairs?subreddit=webdev", + transport.last().url()); + assertEquals("flair-1", flairs.flairs().get(0).id()); + } + + @Test + void theDefaultSubredditSendsNullToFallBackToTheProfilePage() { + FakeTransport transport = new FakeTransport().enqueue(200, "{\"data\":{\"subreddit\":null}}"); + FoPost client = TestSupport.client(transport); + + RedditDefaultSubreddit result = client.accounts().setRedditDefaultSubreddit("a1", null); + + assertEquals("PUT", transport.last().method()); + assertEquals("{}", transport.lastBody()); + assertNull(result.subreddit()); + } + + @Test + void validatingASubredditNamesTheAccountItReadsAs() { + FakeTransport transport = new FakeTransport() + .enqueue(200, """ + {"data":{"subreddit":"webdev","exists":true,"can_post":true, + "over_18":false,"flair_enabled":true,"ok":true}}"""); + FoPost client = TestSupport.client(transport); + + SubredditCheck check = client.validate().subreddit("a1", "webdev"); + + assertEquals( + "https://api.fopost.test/v1/validate/subreddit?account_id=a1&name=webdev", + transport.last().url()); + assertTrue(check.ok()); + assertTrue(check.canPost()); + assertEquals(Boolean.FALSE, check.over18()); + } + + @Test + void votingSendsTheDirection() { + FakeTransport transport = new FakeTransport() + .enqueue(200, """ + {"data":{"id":"i1","platform":"reddit","type":"comment","state":"unread", + "vote":"down","canVote":true}}"""); + FoPost client = TestSupport.client(transport); + + InboxItem item = client.inbox().vote("i1", "down"); + + assertEquals("https://api.fopost.test/v1/inbox/i1/vote", transport.last().url()); + assertEquals("{\"direction\":\"down\"}", transport.lastBody()); + assertEquals("down", item.vote()); + assertTrue(item.canVote()); + } +}