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()); + } +}