From 35d592142d5008a89b3c13a7074572c883eee4dc Mon Sep 17 00:00:00 2001 From: Ali Hesari Date: Sun, 20 Sep 2026 20:28:18 +0200 Subject: [PATCH] feat: wrap the per-network account extras Pinterest boards, YouTube playlists and captions, Bluesky post languages, TikTok creator info plus music, place and video lookup, Instagram audio, publishing limit and stories, and LinkedIn mention lookup. All on AccountsResource, all under the accounts scope. --- .../fopost/sdk/model/BlueskyLanguages.java | 6 + .../com/fopost/sdk/model/InstagramAudio.java | 13 ++ .../sdk/model/InstagramPublishingLimit.java | 5 + .../com/fopost/sdk/model/InstagramStory.java | 15 ++ .../sdk/model/InstagramStoryInsights.java | 6 + .../com/fopost/sdk/model/LinkedInMention.java | 5 + .../com/fopost/sdk/model/PinterestBoard.java | 4 + .../fopost/sdk/model/TikTokCreatorInfo.java | 17 ++ .../com/fopost/sdk/model/TikTokMusic.java | 13 ++ .../com/fopost/sdk/model/TikTokPlace.java | 6 + .../fopost/sdk/model/TikTokVideoSource.java | 15 ++ .../fopost/sdk/model/YouTubeCaptionTrack.java | 11 + .../com/fopost/sdk/model/YouTubePlaylist.java | 11 + .../fopost/sdk/model/YouTubeTranscript.java | 4 + .../sdk/param/CreatePinterestBoardParams.java | 32 +++ .../param/CreateYouTubePlaylistParams.java | 32 +++ .../param/UploadYouTubeCaptionsParams.java | 37 ++++ .../fopost/sdk/resource/AccountsResource.java | 204 ++++++++++++++++++ .../com/fopost/sdk/PlatformExtrasTest.java | 135 ++++++++++++ 19 files changed, 571 insertions(+) create mode 100644 src/main/java/com/fopost/sdk/model/BlueskyLanguages.java create mode 100644 src/main/java/com/fopost/sdk/model/InstagramAudio.java create mode 100644 src/main/java/com/fopost/sdk/model/InstagramPublishingLimit.java create mode 100644 src/main/java/com/fopost/sdk/model/InstagramStory.java create mode 100644 src/main/java/com/fopost/sdk/model/InstagramStoryInsights.java create mode 100644 src/main/java/com/fopost/sdk/model/LinkedInMention.java create mode 100644 src/main/java/com/fopost/sdk/model/PinterestBoard.java create mode 100644 src/main/java/com/fopost/sdk/model/TikTokCreatorInfo.java create mode 100644 src/main/java/com/fopost/sdk/model/TikTokMusic.java create mode 100644 src/main/java/com/fopost/sdk/model/TikTokPlace.java create mode 100644 src/main/java/com/fopost/sdk/model/TikTokVideoSource.java create mode 100644 src/main/java/com/fopost/sdk/model/YouTubeCaptionTrack.java create mode 100644 src/main/java/com/fopost/sdk/model/YouTubePlaylist.java create mode 100644 src/main/java/com/fopost/sdk/model/YouTubeTranscript.java create mode 100644 src/main/java/com/fopost/sdk/param/CreatePinterestBoardParams.java create mode 100644 src/main/java/com/fopost/sdk/param/CreateYouTubePlaylistParams.java create mode 100644 src/main/java/com/fopost/sdk/param/UploadYouTubeCaptionsParams.java create mode 100644 src/test/java/com/fopost/sdk/PlatformExtrasTest.java diff --git a/src/main/java/com/fopost/sdk/model/BlueskyLanguages.java b/src/main/java/com/fopost/sdk/model/BlueskyLanguages.java new file mode 100644 index 0000000..2dc540b --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/BlueskyLanguages.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +import java.util.List; + +/** The default post languages for a connection: up to three BCP-47 tags. */ +public record BlueskyLanguages(List languages) {} diff --git a/src/main/java/com/fopost/sdk/model/InstagramAudio.java b/src/main/java/com/fopost/sdk/model/InstagramAudio.java new file mode 100644 index 0000000..52ee022 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/InstagramAudio.java @@ -0,0 +1,13 @@ +package com.fopost.sdk.model; + +/** A track a Reel can carry; pass {@code id} as the {@code audio_id} platform setting. */ +public record InstagramAudio( + String id, + String title, + String artist, + Integer durationMs, + String audioType, + String coverArtworkUrl, + String previewUrl, + String username, + Boolean isAdsEligible) {} diff --git a/src/main/java/com/fopost/sdk/model/InstagramPublishingLimit.java b/src/main/java/com/fopost/sdk/model/InstagramPublishingLimit.java new file mode 100644 index 0000000..680da7b --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/InstagramPublishingLimit.java @@ -0,0 +1,5 @@ +package com.fopost.sdk.model; + +/** What this account has published in the rolling window, and what is left. */ +public record InstagramPublishingLimit( + int quotaUsage, Integer quotaTotal, Integer quotaDurationSec, Integer remaining) {} diff --git a/src/main/java/com/fopost/sdk/model/InstagramStory.java b/src/main/java/com/fopost/sdk/model/InstagramStory.java new file mode 100644 index 0000000..12227f4 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/InstagramStory.java @@ -0,0 +1,15 @@ +package com.fopost.sdk.model; + +import java.util.Map; + +/** A story still inside its 24 hours; {@code insights} is present only when asked for. */ +public record InstagramStory( + String id, + String mediaType, + String mediaProductType, + String permalink, + String mediaUrl, + String thumbnailUrl, + String caption, + String timestamp, + Map insights) {} diff --git a/src/main/java/com/fopost/sdk/model/InstagramStoryInsights.java b/src/main/java/com/fopost/sdk/model/InstagramStoryInsights.java new file mode 100644 index 0000000..ecd5ca4 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/InstagramStoryInsights.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +import java.util.Map; + +/** The insight set for one story. */ +public record InstagramStoryInsights(String storyId, Map insights) {} diff --git a/src/main/java/com/fopost/sdk/model/LinkedInMention.java b/src/main/java/com/fopost/sdk/model/LinkedInMention.java new file mode 100644 index 0000000..af4b594 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/LinkedInMention.java @@ -0,0 +1,5 @@ +package com.fopost.sdk.model; + +/** An entity a post can mention; {@code annotation} is what the post text carries. */ +public record LinkedInMention( + String urn, String name, String vanityName, String logoUrl, String type, String annotation) {} diff --git a/src/main/java/com/fopost/sdk/model/PinterestBoard.java b/src/main/java/com/fopost/sdk/model/PinterestBoard.java new file mode 100644 index 0000000..f6ded6b --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/PinterestBoard.java @@ -0,0 +1,4 @@ +package com.fopost.sdk.model; + +/** A Pinterest board a Pin can land on; pass {@code id} as the {@code board_id} platform setting. */ +public record PinterestBoard(String id, String name, String privacy, String description, String image) {} diff --git a/src/main/java/com/fopost/sdk/model/TikTokCreatorInfo.java b/src/main/java/com/fopost/sdk/model/TikTokCreatorInfo.java new file mode 100644 index 0000000..58cff9c --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/TikTokCreatorInfo.java @@ -0,0 +1,17 @@ +package com.fopost.sdk.model; + +import java.util.List; + +/** + * The switches TikTok enforces at publish time. They are set on the TikTok account itself, not in + * FoPost, so a disabled one cannot be turned back on here. + */ +public record TikTokCreatorInfo( + String username, + String nickname, + String avatarUrl, + List privacyLevelOptions, + boolean commentDisabled, + boolean duetDisabled, + boolean stitchDisabled, + Integer maxVideoPostDurationSec) {} diff --git a/src/main/java/com/fopost/sdk/model/TikTokMusic.java b/src/main/java/com/fopost/sdk/model/TikTokMusic.java new file mode 100644 index 0000000..06287be --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/TikTokMusic.java @@ -0,0 +1,13 @@ +package com.fopost.sdk.model; + +/** + * A track from TikTok's Commercial Music Library. Pass {@code id} as the {@code music_id} platform + * setting to attach it to a post. + */ +public record TikTokMusic( + String id, + String title, + String author, + Integer durationSec, + String coverUrl, + String previewUrl) {} diff --git a/src/main/java/com/fopost/sdk/model/TikTokPlace.java b/src/main/java/com/fopost/sdk/model/TikTokPlace.java new file mode 100644 index 0000000..657fefd --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/TikTokPlace.java @@ -0,0 +1,6 @@ +package com.fopost.sdk.model; + +/** + * A place a post can be tagged with. Pass {@code id} as the {@code location_id} platform setting. + */ +public record TikTokPlace(String id, String name, String address, String city, String country) {} diff --git a/src/main/java/com/fopost/sdk/model/TikTokVideoSource.java b/src/main/java/com/fopost/sdk/model/TikTokVideoSource.java new file mode 100644 index 0000000..2c4a83e --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/TikTokVideoSource.java @@ -0,0 +1,15 @@ +package com.fopost.sdk.model; + +/** + * One of the account's own videos, resolved from a share link. TikTok serves no raw media file, so + * {@code downloadUrl} is the share address, which is what a repurpose run reads. + */ +public record TikTokVideoSource( + String videoId, + String title, + String description, + Integer durationSec, + String coverImageUrl, + String shareUrl, + String embedLink, + String downloadUrl) {} diff --git a/src/main/java/com/fopost/sdk/model/YouTubeCaptionTrack.java b/src/main/java/com/fopost/sdk/model/YouTubeCaptionTrack.java new file mode 100644 index 0000000..d1963d7 --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/YouTubeCaptionTrack.java @@ -0,0 +1,11 @@ +package com.fopost.sdk.model; + +/** A caption track on one of the channel's videos; {@code language} is a BCP-47 tag. */ +public record YouTubeCaptionTrack( + String id, + String language, + String name, + String trackKind, + boolean isDraft, + boolean isAutoSynced, + String lastUpdated) {} diff --git a/src/main/java/com/fopost/sdk/model/YouTubePlaylist.java b/src/main/java/com/fopost/sdk/model/YouTubePlaylist.java new file mode 100644 index 0000000..b9cee6a --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/YouTubePlaylist.java @@ -0,0 +1,11 @@ +package com.fopost.sdk.model; + +/** A playlist on the channel; {@code isDefault} marks the one a new video joins when none is picked. */ +public record YouTubePlaylist( + String id, + String title, + String description, + String privacy, + Integer itemCount, + String thumbnailUrl, + boolean isDefault) {} diff --git a/src/main/java/com/fopost/sdk/model/YouTubeTranscript.java b/src/main/java/com/fopost/sdk/model/YouTubeTranscript.java new file mode 100644 index 0000000..a47ef0b --- /dev/null +++ b/src/main/java/com/fopost/sdk/model/YouTubeTranscript.java @@ -0,0 +1,4 @@ +package com.fopost.sdk.model; + +/** One caption track read back as text, in SRT. */ +public record YouTubeTranscript(String captionId, String transcript) {} diff --git a/src/main/java/com/fopost/sdk/param/CreatePinterestBoardParams.java b/src/main/java/com/fopost/sdk/param/CreatePinterestBoardParams.java new file mode 100644 index 0000000..c077606 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreatePinterestBoardParams.java @@ -0,0 +1,32 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** A new Pinterest board. {@code privacy} is PUBLIC, PROTECTED or SECRET; unset means PUBLIC. */ +public final class CreatePinterestBoardParams { + + private final Map body = new LinkedHashMap<>(); + + private CreatePinterestBoardParams(String name) { + body.put("name", name); + } + + public static CreatePinterestBoardParams of(String name) { + return new CreatePinterestBoardParams(name); + } + + public CreatePinterestBoardParams description(String description) { + body.put("description", description); + return this; + } + + public CreatePinterestBoardParams privacy(String privacy) { + body.put("privacy", privacy); + return this; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/CreateYouTubePlaylistParams.java b/src/main/java/com/fopost/sdk/param/CreateYouTubePlaylistParams.java new file mode 100644 index 0000000..3bbb813 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/CreateYouTubePlaylistParams.java @@ -0,0 +1,32 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** A new playlist. {@code privacy} is public, unlisted or private; unset means private. */ +public final class CreateYouTubePlaylistParams { + + private final Map body = new LinkedHashMap<>(); + + private CreateYouTubePlaylistParams(String title) { + body.put("title", title); + } + + public static CreateYouTubePlaylistParams of(String title) { + return new CreateYouTubePlaylistParams(title); + } + + public CreateYouTubePlaylistParams description(String description) { + body.put("description", description); + return this; + } + + public CreateYouTubePlaylistParams privacy(String privacy) { + body.put("privacy", privacy); + return this; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/param/UploadYouTubeCaptionsParams.java b/src/main/java/com/fopost/sdk/param/UploadYouTubeCaptionsParams.java new file mode 100644 index 0000000..0524386 --- /dev/null +++ b/src/main/java/com/fopost/sdk/param/UploadYouTubeCaptionsParams.java @@ -0,0 +1,37 @@ +package com.fopost.sdk.param; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A caption track to upload. {@code body} is the subtitle file itself; YouTube reads SRT and WebVTT + * and works out which from the bytes, so the format is not declared. + */ +public final class UploadYouTubeCaptionsParams { + + private final Map body = new LinkedHashMap<>(); + + private UploadYouTubeCaptionsParams(String language, String subtitles) { + body.put("language", language); + body.put("body", subtitles); + } + + /** {@code language} is a BCP-47 tag. */ + public static UploadYouTubeCaptionsParams of(String language, String subtitles) { + return new UploadYouTubeCaptionsParams(language, subtitles); + } + + public UploadYouTubeCaptionsParams name(String name) { + body.put("name", name); + return this; + } + + public UploadYouTubeCaptionsParams isDraft(boolean isDraft) { + body.put("is_draft", isDraft); + return this; + } + + public Map toMap() { + return new LinkedHashMap<>(body); + } +} diff --git a/src/main/java/com/fopost/sdk/resource/AccountsResource.java b/src/main/java/com/fopost/sdk/resource/AccountsResource.java index f556268..f3741d9 100644 --- a/src/main/java/com/fopost/sdk/resource/AccountsResource.java +++ b/src/main/java/com/fopost/sdk/resource/AccountsResource.java @@ -24,6 +24,13 @@ import com.fopost.sdk.model.MetaIceBreakers; import com.fopost.sdk.model.MetaPersistentMenu; import com.fopost.sdk.model.MetaPersistentMenuEntry; +import com.fopost.sdk.model.BlueskyLanguages; +import com.fopost.sdk.model.InstagramAudio; +import com.fopost.sdk.model.InstagramPublishingLimit; +import com.fopost.sdk.model.InstagramStory; +import com.fopost.sdk.model.InstagramStoryInsights; +import com.fopost.sdk.model.LinkedInMention; +import com.fopost.sdk.model.PinterestBoard; import com.fopost.sdk.model.SlackChannel; import com.fopost.sdk.model.SlackIdentity; import com.fopost.sdk.model.SlackMember; @@ -31,13 +38,24 @@ import com.fopost.sdk.model.TelegramBotCommands; import com.fopost.sdk.model.TelegramConnectCode; import com.fopost.sdk.model.TelegramConnectStatus; +import com.fopost.sdk.model.TikTokCreatorInfo; +import com.fopost.sdk.model.TikTokMusic; +import com.fopost.sdk.model.TikTokPlace; +import com.fopost.sdk.model.TikTokVideoSource; import com.fopost.sdk.model.TokenRefresh; import com.fopost.sdk.model.WebhookSubscription; import com.fopost.sdk.param.CreateAccountParams; import com.fopost.sdk.param.DiscordEventParams; import com.fopost.sdk.param.DiscordRoleParams; import com.fopost.sdk.param.UpdateDiscordIdentityParams; +import com.fopost.sdk.model.YouTubeCaptionTrack; +import com.fopost.sdk.model.YouTubePlaylist; +import com.fopost.sdk.model.YouTubeTranscript; +import com.fopost.sdk.param.CreateAccountParams; +import com.fopost.sdk.param.CreatePinterestBoardParams; +import com.fopost.sdk.param.CreateYouTubePlaylistParams; import com.fopost.sdk.param.UpdateSlackIdentityParams; +import com.fopost.sdk.param.UploadYouTubeCaptionsParams; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -528,4 +546,190 @@ private static Map query(String key, Object value) { query.put(key, value); return query; } + + // --- Per-network extras ------------------------------------------------ + + /** Boards this Pinterest connection can pin to. */ + public List listPinterestBoards(String accountId) { + return http.convertList( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/pinterest/boards", null)), + PinterestBoard.class); + } + + /** Create a board on the connected Pinterest account. */ + public PinterestBoard createPinterestBoard(String accountId, CreatePinterestBoardParams params) { + return http.convert( + ApiClient.unwrap(http.post("/v1/accounts/" + accountId + "/pinterest/boards", params.toMap())), + PinterestBoard.class); + } + + /** The channel's own playlists, with the stored default marked. */ + public List listYouTubePlaylists(String accountId) { + return http.convertList( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/youtube/playlists", null)), + YouTubePlaylist.class); + } + + /** Create a playlist on the connected channel. */ + public YouTubePlaylist createYouTubePlaylist(String accountId, CreateYouTubePlaylistParams params) { + return http.convert( + ApiClient.unwrap(http.post("/v1/accounts/" + accountId + "/youtube/playlists", params.toMap())), + YouTubePlaylist.class); + } + + /** + * The playlist a new video joins when the post picks none. A null {@code playlistId} clears it. + * Returns what is stored afterwards. + */ + public String setDefaultYouTubePlaylist(String accountId, String playlistId) { + ObjectNode body = Json.MAPPER.createObjectNode(); + body.put("playlist_id", playlistId); + JsonNode data = ApiClient.unwrap( + http.put("/v1/accounts/" + accountId + "/youtube/playlists/default", body)); + JsonNode stored = data == null ? null : data.get("playlist_id"); + return stored == null || stored.isNull() ? null : stored.asText(); + } + + /** Caption tracks on one of the channel's videos. */ + public List listYouTubeCaptions(String accountId, String videoId) { + return http.convertList( + ApiClient.unwrap( + http.get("/v1/accounts/" + accountId + "/youtube/videos/" + videoId + "/captions", null)), + YouTubeCaptionTrack.class); + } + + /** Upload a caption track to a video. */ + public YouTubeCaptionTrack uploadYouTubeCaptions( + String accountId, String videoId, UploadYouTubeCaptionsParams params) { + return http.convert( + ApiClient.unwrap(http.post( + "/v1/accounts/" + accountId + "/youtube/videos/" + videoId + "/captions", params.toMap())), + YouTubeCaptionTrack.class); + } + + /** One caption track read back as text. */ + public YouTubeTranscript readYouTubeTranscript(String accountId, String captionId) { + return http.convert( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/youtube/captions/" + captionId, null)), + YouTubeTranscript.class); + } + + /** What a post from this Bluesky connection is written in when it does not say. */ + public BlueskyLanguages getBlueskyLanguages(String accountId) { + return http.convert( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/bluesky/languages", null)), + BlueskyLanguages.class); + } + + /** Store up to three BCP-47 tags. An empty list clears the default. */ + public BlueskyLanguages setBlueskyLanguages(String accountId, List languages) { + Map body = new LinkedHashMap<>(); + body.put("languages", languages == null ? List.of() : languages); + return http.convert( + ApiClient.unwrap(http.put("/v1/accounts/" + accountId + "/bluesky/languages", body)), + BlueskyLanguages.class); + } + + /** The switches TikTok enforces at publish time, changed in the TikTok app. */ + public TikTokCreatorInfo getTikTokCreatorInfo(String accountId) { + return http.convert( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/tiktok/creator-info", null)), + TikTokCreatorInfo.class); + } + + /** + * TikTok's Commercial Music Library. Needs the Marketing API product on the TikTok app; without + * it the call fails with 403 rather than answering an empty list. + * + * @param limit 1 to 50, or null for the API default of 20 + */ + public List searchTikTokMusic(String accountId, String query, Integer limit) { + return http.convertList( + ApiClient.unwrap( + http.get("/v1/accounts/" + accountId + "/tiktok/music", searchParams(query, limit))), + TikTokMusic.class); + } + + /** Places a post can be tagged with. Same TikTok product as the music library. */ + public List searchTikTokLocations(String accountId, String query, Integer limit) { + return http.convertList( + ApiClient.unwrap( + http.get("/v1/accounts/" + accountId + "/tiktok/locations", searchParams(query, limit))), + TikTokPlace.class); + } + + /** Resolves a share link to one of this account's own videos, for repurposing. */ + public TikTokVideoSource lookupTikTokVideo(String accountId, String url) { + return http.convert( + ApiClient.unwrap( + http.post("/v1/accounts/" + accountId + "/tiktok/video-download", Map.of("url", url))), + TikTokVideoSource.class); + } + + private static Map searchParams(String query, Integer limit) { + Map params = new LinkedHashMap<>(); + params.put("q", query); + if (limit != null) { + params.put("limit", limit); + } + return params; + } + + /** + * Tracks a Reel can carry. A null {@code query} asks Instagram for what is trending; + * {@code audioType} is music (the default) or original_sound. + */ + public List searchInstagramAudio(String accountId, String query, String audioType) { + Map params = new LinkedHashMap<>(); + if (query != null) { + params.put("q", query); + } + if (audioType != null) { + params.put("audio_type", audioType); + } + return http.convertList( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/instagram/audio", params)), + InstagramAudio.class); + } + + /** How many posts are left before Instagram refuses the next one. */ + public InstagramPublishingLimit getInstagramPublishingLimit(String accountId) { + return http.convert( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/instagram/publishing-limit", null)), + InstagramPublishingLimit.class); + } + + /** + * Stories still inside their 24 hours, posted through FoPost or not. Asking for insights costs + * one extra call per story. + */ + public List listInstagramStories(String accountId, boolean insights) { + Map params = new LinkedHashMap<>(); + if (insights) { + params.put("insights", true); + } + return http.convertList( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/instagram/stories", params)), + InstagramStory.class); + } + + /** The insight set for one story. */ + public InstagramStoryInsights getInstagramStoryInsights(String accountId, String storyId) { + return http.convert( + ApiClient.unwrap( + http.get("/v1/accounts/" + accountId + "/instagram/stories/" + storyId + "/insights", null)), + InstagramStoryInsights.class); + } + + /** + * Organizations a LinkedIn post can mention. People are not searchable: LinkedIn has no public + * person search, so a member mention needs a URN the caller already holds. + */ + public List searchLinkedInMentions(String accountId, String query) { + Map params = new LinkedHashMap<>(); + params.put("q", query); + return http.convertList( + ApiClient.unwrap(http.get("/v1/accounts/" + accountId + "/linkedin/mentions", params)), + LinkedInMention.class); + } } diff --git a/src/test/java/com/fopost/sdk/PlatformExtrasTest.java b/src/test/java/com/fopost/sdk/PlatformExtrasTest.java new file mode 100644 index 0000000..7a69b30 --- /dev/null +++ b/src/test/java/com/fopost/sdk/PlatformExtrasTest.java @@ -0,0 +1,135 @@ +package com.fopost.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fopost.sdk.model.InstagramStory; +import com.fopost.sdk.model.LinkedInMention; +import com.fopost.sdk.model.PinterestBoard; +import com.fopost.sdk.model.TikTokCreatorInfo; +import com.fopost.sdk.model.TikTokMusic; +import com.fopost.sdk.model.TikTokVideoSource; +import com.fopost.sdk.model.YouTubePlaylist; +import com.fopost.sdk.param.CreatePinterestBoardParams; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PlatformExtrasTest { + + @Test + void createPinterestBoardSendsOnlyWhatWasGiven() { + FakeTransport transport = new FakeTransport() + .enqueue(201, "{\"data\":{\"id\":\"b1\",\"name\":\"Recipes\",\"privacy\":\"PUBLIC\"}}"); + + PinterestBoard board = TestSupport.client(transport) + .accounts() + .createPinterestBoard("a1", CreatePinterestBoardParams.of("Recipes")); + + assertEquals("POST", transport.last().method()); + assertEquals("https://api.fopost.test/v1/accounts/a1/pinterest/boards", transport.last().url()); + assertEquals("{\"name\":\"Recipes\"}", transport.lastBody()); + assertEquals("b1", board.id()); + } + + @Test + void setDefaultYouTubePlaylistSendsNullToClearIt() { + FakeTransport transport = new FakeTransport().enqueue(200, "{\"data\":{\"playlist_id\":null}}"); + + String stored = TestSupport.client(transport).accounts().setDefaultYouTubePlaylist("a1", null); + + assertEquals("PUT", transport.last().method()); + assertEquals("{\"playlist_id\":null}", transport.lastBody()); + assertNull(stored); + } + + @Test + void playlistsMarkTheStoredDefault() { + FakeTransport transport = new FakeTransport() + .enqueue(200, "{\"data\":[{\"id\":\"PL1\",\"title\":\"Tutorials\",\"is_default\":true}]}"); + + List playlists = TestSupport.client(transport).accounts().listYouTubePlaylists("a1"); + + assertTrue(playlists.get(0).isDefault()); + } + + @Test + void tiktokCreatorInfoReportsTheAccountsOwnSwitches() { + FakeTransport transport = new FakeTransport() + .enqueue(200, """ + {"data":{"privacy_level_options":["PUBLIC_TO_EVERYONE"],"comment_disabled":false, + "duet_disabled":true,"stitch_disabled":false, + "max_video_post_duration_sec":600}}"""); + + TikTokCreatorInfo info = TestSupport.client(transport).accounts().getTikTokCreatorInfo("a1"); + + assertTrue(info.duetDisabled()); + assertFalse(info.stitchDisabled()); + assertEquals(600, info.maxVideoPostDurationSec()); + } + + @Test + void tiktokMusicSearchPassesTheQueryThrough() { + FakeTransport transport = new FakeTransport() + .enqueue(200, "{\"data\":[{\"id\":\"m1\",\"title\":\"Sunrise\",\"author\":\"Kite\"}]}"); + + List tracks = + TestSupport.client(transport).accounts().searchTikTokMusic("a1", "sunrise", 5); + + assertEquals("m1", tracks.get(0).id()); + assertTrue(transport.last().url().contains("q=sunrise")); + assertTrue(transport.last().url().contains("limit=5")); + } + + @Test + void tiktokVideoLookupReturnsTheAddressARepurposeRunReads() { + FakeTransport transport = new FakeTransport() + .enqueue( + 200, + "{\"data\":{\"video_id\":\"7300000000000000000\"," + + "\"download_url\":\"https://www.tiktok.com/@a/video/7300000000000000000\"}}"); + + TikTokVideoSource video = TestSupport.client(transport) + .accounts() + .lookupTikTokVideo("a1", "https://www.tiktok.com/@a/video/7300000000000000000"); + + assertEquals("POST", transport.last().method()); + assertEquals("7300000000000000000", video.videoId()); + assertNotNull(video.downloadUrl()); + } + + @Test + void instagramStoriesAskForInsightsOnlyWhenRequested() { + FakeTransport transport = new FakeTransport() + .enqueue(200, "{\"data\":[{\"id\":\"s1\",\"media_type\":\"IMAGE\"}]}") + .enqueue(200, "{\"data\":[{\"id\":\"s1\",\"media_type\":\"IMAGE\",\"insights\":{\"views\":40}}]}"); + FoPost client = TestSupport.client(transport); + + client.accounts().listInstagramStories("a1", false); + assertEquals("https://api.fopost.test/v1/accounts/a1/instagram/stories", transport.last().url()); + + List stories = client.accounts().listInstagramStories("a1", true); + assertEquals( + "https://api.fopost.test/v1/accounts/a1/instagram/stories?insights=true", + transport.last().url()); + assertEquals(40, stories.get(0).insights().get("views")); + } + + @Test + void linkedInMentionsCarryTheAnnotationToPaste() { + FakeTransport transport = new FakeTransport() + .enqueue(200, """ + {"data":[{"urn":"urn:li:organization:2414183","name":"Devtestco", + "annotation":"@[Devtestco](urn:li:organization:2414183)"}]}"""); + + List mentions = + TestSupport.client(transport).accounts().searchLinkedInMentions("a1", "devtestco"); + + assertEquals( + "https://api.fopost.test/v1/accounts/a1/linkedin/mentions?q=devtestco", + transport.last().url()); + assertEquals("@[Devtestco](urn:li:organization:2414183)", mentions.get(0).annotation()); + } +}