@hasna/connectors 1.3.37 → 1.3.38

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.
@@ -1467,12 +1467,772 @@ class BlueskyAdapter {
1467
1467
  }
1468
1468
  }
1469
1469
 
1470
+ // src/social/http.ts
1471
+ function resolveFetch(fetchImpl) {
1472
+ return fetchImpl ?? globalThis.fetch;
1473
+ }
1474
+ function appendQuery(url, query) {
1475
+ if (!query)
1476
+ return url;
1477
+ const qs = new URLSearchParams;
1478
+ for (const [k, v] of Object.entries(query)) {
1479
+ if (v !== undefined && v !== null && v !== "")
1480
+ qs.append(k, String(v));
1481
+ }
1482
+ const s = qs.toString();
1483
+ return s ? `${url}${url.includes("?") ? "&" : "?"}${s}` : url;
1484
+ }
1485
+ function extractError(data, fallback) {
1486
+ if (data && typeof data === "object") {
1487
+ const d = data;
1488
+ const candidates = [
1489
+ d.error_description,
1490
+ typeof d.error === "string" ? d.error : undefined,
1491
+ d.message,
1492
+ d.error && typeof d.error === "object" ? d.error.message : undefined
1493
+ ];
1494
+ for (const c of candidates) {
1495
+ if (typeof c === "string" && c)
1496
+ return c;
1497
+ }
1498
+ }
1499
+ return fallback;
1500
+ }
1501
+ async function jsonRequest(fetchImpl, url, options = {}) {
1502
+ const { method = "GET", query, body } = options;
1503
+ const finalUrl = appendQuery(url, query);
1504
+ const headers = { Accept: "application/json", ...options.headers ?? {} };
1505
+ let serialized;
1506
+ if (body !== undefined && method !== "GET" && method !== "HEAD") {
1507
+ if (typeof body === "string" || body instanceof FormData) {
1508
+ serialized = body;
1509
+ } else {
1510
+ if (!headers["Content-Type"])
1511
+ headers["Content-Type"] = "application/json";
1512
+ serialized = JSON.stringify(body);
1513
+ }
1514
+ }
1515
+ const res = await fetchImpl(finalUrl, { method, headers, body: serialized });
1516
+ const text = await res.text();
1517
+ const data = text ? safeJsonParse(text) : {};
1518
+ if (!res.ok) {
1519
+ const label = options.errorLabel ?? "request";
1520
+ throw new Error(`${label} ${res.status}: ${extractError(data, res.statusText || "failed")}`);
1521
+ }
1522
+ return data;
1523
+ }
1524
+ function safeJsonParse(text) {
1525
+ try {
1526
+ return JSON.parse(text);
1527
+ } catch {
1528
+ return { raw: text };
1529
+ }
1530
+ }
1531
+
1532
+ // src/social/linkedin.ts
1533
+ var DEFAULT_BASE = "https://api.linkedin.com";
1534
+
1535
+ class LinkedInAdapter {
1536
+ accessToken;
1537
+ baseUrl;
1538
+ authorUrn;
1539
+ fetchImpl;
1540
+ constructor(creds, fetchImpl) {
1541
+ if (!creds || !creds.accessToken) {
1542
+ throw new Error("linkedin credentials require `accessToken`");
1543
+ }
1544
+ this.accessToken = creds.accessToken;
1545
+ this.authorUrn = creds.authorUrn;
1546
+ this.baseUrl = (creds.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
1547
+ this.fetchImpl = resolveFetch(fetchImpl);
1548
+ }
1549
+ static fromCredentials(creds, fetchImpl) {
1550
+ return new LinkedInAdapter(creds, fetchImpl);
1551
+ }
1552
+ headers(extra) {
1553
+ return {
1554
+ Authorization: `Bearer ${this.accessToken}`,
1555
+ "X-Restli-Protocol-Version": "2.0.0",
1556
+ ...extra ?? {}
1557
+ };
1558
+ }
1559
+ async resolveAuthorUrn() {
1560
+ if (this.authorUrn)
1561
+ return this.authorUrn;
1562
+ const me = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/me`, {
1563
+ headers: this.headers(),
1564
+ errorLabel: "LinkedIn"
1565
+ });
1566
+ this.authorUrn = `urn:li:person:${me.id}`;
1567
+ return this.authorUrn;
1568
+ }
1569
+ async accountMe() {
1570
+ const me = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/me`, { headers: this.headers(), errorLabel: "LinkedIn" });
1571
+ this.authorUrn = `urn:li:person:${me.id}`;
1572
+ const displayName = [me.localizedFirstName, me.localizedLastName].filter(Boolean).join(" ") || undefined;
1573
+ return {
1574
+ id: me.id,
1575
+ username: me.vanityName ?? me.id,
1576
+ displayName,
1577
+ url: me.vanityName ? `https://www.linkedin.com/in/${me.vanityName}` : undefined
1578
+ };
1579
+ }
1580
+ async postCreate(input) {
1581
+ const author = await this.resolveAuthorUrn();
1582
+ const visibility = input.visibility ?? "PUBLIC";
1583
+ const hasMedia = Boolean(input.mediaIds && input.mediaIds.length > 0);
1584
+ const shareContent = {
1585
+ shareCommentary: { text: input.text },
1586
+ shareMediaCategory: hasMedia ? "IMAGE" : "NONE"
1587
+ };
1588
+ if (hasMedia) {
1589
+ shareContent.media = input.mediaIds.map((m) => ({ status: "READY", media: m }));
1590
+ }
1591
+ const body = {
1592
+ author,
1593
+ lifecycleState: "PUBLISHED",
1594
+ specificContent: { "com.linkedin.ugc.ShareContent": shareContent },
1595
+ visibility: { "com.linkedin.ugc.MemberNetworkVisibility": visibility }
1596
+ };
1597
+ const res = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/ugcPosts`, {
1598
+ method: "POST",
1599
+ headers: this.headers(),
1600
+ body,
1601
+ errorLabel: "LinkedIn"
1602
+ });
1603
+ return {
1604
+ id: res.id,
1605
+ url: `https://www.linkedin.com/feed/update/${res.id}`
1606
+ };
1607
+ }
1608
+ async postDelete(input) {
1609
+ await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/ugcPosts/${encodeURIComponent(input.id)}`, {
1610
+ method: "DELETE",
1611
+ headers: this.headers(),
1612
+ errorLabel: "LinkedIn"
1613
+ });
1614
+ return { id: input.id, deleted: true };
1615
+ }
1616
+ async mediaUpload(input) {
1617
+ const author = await this.resolveAuthorUrn();
1618
+ const register = await jsonRequest(this.fetchImpl, `${this.baseUrl}/v2/assets?action=registerUpload`, {
1619
+ method: "POST",
1620
+ headers: this.headers(),
1621
+ body: {
1622
+ registerUploadRequest: {
1623
+ owner: author,
1624
+ recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
1625
+ serviceRelationships: [
1626
+ { relationshipType: "OWNER", identifier: "urn:li:userGeneratedContent" }
1627
+ ]
1628
+ }
1629
+ },
1630
+ errorLabel: "LinkedIn"
1631
+ });
1632
+ const asset = register.value.asset;
1633
+ const uploadUrl = register.value.uploadMechanism["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"].uploadUrl;
1634
+ const buffer = decodeBase64(input.dataBase64);
1635
+ const bytes = Uint8Array.from(buffer);
1636
+ const put = await this.fetchImpl(uploadUrl, {
1637
+ method: "PUT",
1638
+ headers: { Authorization: `Bearer ${this.accessToken}`, "Content-Type": input.mimeType },
1639
+ body: bytes
1640
+ });
1641
+ if (!put.ok) {
1642
+ throw new Error(`LinkedIn ${put.status}: media upload failed`);
1643
+ }
1644
+ return { mediaId: asset };
1645
+ }
1646
+ async mentionsList(_input = {}) {
1647
+ throw new ConnectorOperationNotSupported("linkedin", "mentions.list (no public mentions/notifications endpoint)");
1648
+ }
1649
+ async analyticsPost(_input) {
1650
+ throw new ConnectorOperationNotSupported("linkedin", "analytics.post (requires organization socialActions scope; not available for member shares)");
1651
+ }
1652
+ }
1653
+
1654
+ // src/social/reddit.ts
1655
+ var BASE = "https://oauth.reddit.com";
1656
+ function fullname(id) {
1657
+ return id.startsWith("t3_") ? id : `t3_${id}`;
1658
+ }
1659
+
1660
+ class RedditAdapter {
1661
+ accessToken;
1662
+ userAgent;
1663
+ fetchImpl;
1664
+ constructor(creds, fetchImpl) {
1665
+ if (!creds || !creds.accessToken) {
1666
+ throw new Error("reddit credentials require `accessToken`");
1667
+ }
1668
+ this.accessToken = creds.accessToken;
1669
+ this.userAgent = creds.userAgent ?? "hasna-connectors/social";
1670
+ this.fetchImpl = resolveFetch(fetchImpl);
1671
+ }
1672
+ static fromCredentials(creds, fetchImpl) {
1673
+ return new RedditAdapter(creds, fetchImpl);
1674
+ }
1675
+ headers(extra) {
1676
+ return {
1677
+ Authorization: `Bearer ${this.accessToken}`,
1678
+ "User-Agent": this.userAgent,
1679
+ ...extra ?? {}
1680
+ };
1681
+ }
1682
+ form(params) {
1683
+ const qs = new URLSearchParams;
1684
+ for (const [k, v] of Object.entries(params)) {
1685
+ if (v !== undefined)
1686
+ qs.append(k, v);
1687
+ }
1688
+ return qs.toString();
1689
+ }
1690
+ async accountMe() {
1691
+ const me = await jsonRequest(this.fetchImpl, `${BASE}/api/v1/me`, { headers: this.headers(), errorLabel: "Reddit" });
1692
+ return {
1693
+ id: me.id,
1694
+ username: me.name,
1695
+ displayName: me.name,
1696
+ url: `https://www.reddit.com/user/${me.name}`
1697
+ };
1698
+ }
1699
+ async postCreate(input) {
1700
+ if (!input.title) {
1701
+ throw new Error("reddit post.create requires `title`");
1702
+ }
1703
+ if (!input.subreddit) {
1704
+ throw new Error("reddit post.create requires `subreddit`");
1705
+ }
1706
+ const kind = input.kind ?? "self";
1707
+ if (kind === "link" && !input.url) {
1708
+ throw new Error('reddit post.create with kind "link" requires `url`');
1709
+ }
1710
+ const body = this.form({
1711
+ sr: input.subreddit,
1712
+ title: input.title,
1713
+ kind,
1714
+ text: kind === "self" ? input.text : undefined,
1715
+ url: kind === "link" ? input.url : undefined,
1716
+ api_type: "json"
1717
+ });
1718
+ const res = await jsonRequest(this.fetchImpl, `${BASE}/api/submit`, {
1719
+ method: "POST",
1720
+ headers: this.headers({ "Content-Type": "application/x-www-form-urlencoded" }),
1721
+ body,
1722
+ errorLabel: "Reddit"
1723
+ });
1724
+ const errors = res.json?.errors ?? [];
1725
+ if (errors.length > 0) {
1726
+ throw new Error(`Reddit submit failed: ${JSON.stringify(errors)}`);
1727
+ }
1728
+ const data = res.json?.data ?? {};
1729
+ return { id: data.id ?? data.name ?? "", url: data.url };
1730
+ }
1731
+ async postDelete(input) {
1732
+ await jsonRequest(this.fetchImpl, `${BASE}/api/del`, {
1733
+ method: "POST",
1734
+ headers: this.headers({ "Content-Type": "application/x-www-form-urlencoded" }),
1735
+ body: this.form({ id: fullname(input.id) }),
1736
+ errorLabel: "Reddit"
1737
+ });
1738
+ return { id: input.id, deleted: true };
1739
+ }
1740
+ async mediaUpload(_input) {
1741
+ throw new ConnectorOperationNotSupported("reddit", "media.upload (image uploads require the media asset-lease flow; not supported by this SDK)");
1742
+ }
1743
+ async mentionsList(input = {}) {
1744
+ const res = await jsonRequest(this.fetchImpl, `${BASE}/message/inbox`, {
1745
+ headers: this.headers(),
1746
+ query: { limit: input.limit, after: input.sinceId },
1747
+ errorLabel: "Reddit"
1748
+ });
1749
+ const items = (res.data?.children ?? []).map((c) => ({
1750
+ id: c.data.name ?? c.data.id,
1751
+ text: c.data.body ?? "",
1752
+ authorId: c.data.author_fullname,
1753
+ authorHandle: c.data.author,
1754
+ createdAt: c.data.created_utc ? new Date(c.data.created_utc * 1000).toISOString() : undefined
1755
+ }));
1756
+ return { items };
1757
+ }
1758
+ async analyticsPost(input) {
1759
+ const res = await jsonRequest(this.fetchImpl, `${BASE}/api/info`, {
1760
+ headers: this.headers(),
1761
+ query: { id: fullname(input.id) },
1762
+ errorLabel: "Reddit"
1763
+ });
1764
+ const post = res.data?.children?.[0]?.data;
1765
+ if (!post) {
1766
+ throw new ConnectorOperationNotSupported("reddit", `analytics.post (post not found: ${input.id})`);
1767
+ }
1768
+ return {
1769
+ metrics: {
1770
+ ups: post.ups ?? 0,
1771
+ downs: post.downs ?? 0,
1772
+ score: post.score ?? 0,
1773
+ numComments: post.num_comments ?? 0,
1774
+ upvoteRatio: post.upvote_ratio ?? 0
1775
+ }
1776
+ };
1777
+ }
1778
+ }
1779
+
1780
+ // src/social/tiktok.ts
1781
+ var BASE2 = "https://open.tiktokapis.com";
1782
+
1783
+ class TikTokAdapter {
1784
+ accessToken;
1785
+ fetchImpl;
1786
+ constructor(creds, fetchImpl) {
1787
+ if (!creds || !creds.accessToken) {
1788
+ throw new Error("tiktok credentials require `accessToken`");
1789
+ }
1790
+ this.accessToken = creds.accessToken;
1791
+ this.fetchImpl = resolveFetch(fetchImpl);
1792
+ }
1793
+ static fromCredentials(creds, fetchImpl) {
1794
+ return new TikTokAdapter(creds, fetchImpl);
1795
+ }
1796
+ headers(extra) {
1797
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
1798
+ }
1799
+ async accountMe() {
1800
+ const res = await jsonRequest(this.fetchImpl, `${BASE2}/v2/user/info/`, {
1801
+ headers: this.headers(),
1802
+ query: { fields: "open_id,union_id,display_name,profile_deep_link" },
1803
+ errorLabel: "TikTok"
1804
+ });
1805
+ const user = res.data?.user ?? {};
1806
+ return {
1807
+ id: user.open_id ?? user.union_id ?? "",
1808
+ username: user.display_name,
1809
+ displayName: user.display_name,
1810
+ url: user.profile_deep_link
1811
+ };
1812
+ }
1813
+ async postCreate(input) {
1814
+ const videoId = input.mediaIds?.[0];
1815
+ if (!videoId) {
1816
+ throw new ConnectorOperationNotSupported("tiktok", "post.create (TikTok is video-only: provide a video media id in `mediaIds` via media.upload)");
1817
+ }
1818
+ const body = {
1819
+ post_info: {
1820
+ title: input.text ?? "",
1821
+ privacy_level: input.privacyLevel ?? "PUBLIC_TO_EVERYONE"
1822
+ },
1823
+ source_info: {
1824
+ source: "FILE_UPLOAD",
1825
+ video_id: videoId
1826
+ }
1827
+ };
1828
+ const res = await jsonRequest(this.fetchImpl, `${BASE2}/v2/post/publish/video/init/`, { method: "POST", headers: this.headers({ "Content-Type": "application/json" }), body, errorLabel: "TikTok" });
1829
+ return { id: res.data?.publish_id ?? videoId };
1830
+ }
1831
+ async postDelete(_input) {
1832
+ throw new ConnectorOperationNotSupported("tiktok", "post.delete (the Content Posting API does not expose post deletion)");
1833
+ }
1834
+ async mediaUpload(input) {
1835
+ const buffer = decodeBase64(input.dataBase64);
1836
+ const size = buffer.byteLength;
1837
+ const init = await jsonRequest(this.fetchImpl, `${BASE2}/v2/post/publish/inbox/video/init/`, {
1838
+ method: "POST",
1839
+ headers: this.headers({ "Content-Type": "application/json" }),
1840
+ body: {
1841
+ source_info: {
1842
+ source: "FILE_UPLOAD",
1843
+ video_size: size,
1844
+ chunk_size: size,
1845
+ total_chunk_count: 1
1846
+ }
1847
+ },
1848
+ errorLabel: "TikTok"
1849
+ });
1850
+ const uploadUrl = init.data?.upload_url;
1851
+ const publishId = init.data?.publish_id;
1852
+ if (!uploadUrl || !publishId) {
1853
+ throw new Error("TikTok media.upload: init did not return an upload_url/publish_id");
1854
+ }
1855
+ const bytes = Uint8Array.from(buffer);
1856
+ const put = await this.fetchImpl(uploadUrl, {
1857
+ method: "PUT",
1858
+ headers: {
1859
+ "Content-Type": input.mimeType,
1860
+ "Content-Range": `bytes 0-${size - 1}/${size}`
1861
+ },
1862
+ body: bytes
1863
+ });
1864
+ if (!put.ok) {
1865
+ throw new Error(`TikTok ${put.status}: video upload failed`);
1866
+ }
1867
+ return { mediaId: publishId };
1868
+ }
1869
+ async mentionsList(_input = {}) {
1870
+ throw new ConnectorOperationNotSupported("tiktok", "mentions.list (no mentions endpoint in the Content Posting / Display API)");
1871
+ }
1872
+ async analyticsPost(_input) {
1873
+ throw new ConnectorOperationNotSupported("tiktok", "analytics.post (per-post metrics require the Research/Business API, not available here)");
1874
+ }
1875
+ }
1876
+
1877
+ // src/social/youtube.ts
1878
+ var API = "https://www.googleapis.com/youtube/v3";
1879
+ var UPLOAD = "https://www.googleapis.com/upload/youtube/v3";
1880
+
1881
+ class YouTubeAdapter {
1882
+ accessToken;
1883
+ fetchImpl;
1884
+ constructor(creds, fetchImpl) {
1885
+ if (!creds || !creds.accessToken) {
1886
+ throw new Error("youtube credentials require `accessToken`");
1887
+ }
1888
+ this.accessToken = creds.accessToken;
1889
+ this.fetchImpl = resolveFetch(fetchImpl);
1890
+ }
1891
+ static fromCredentials(creds, fetchImpl) {
1892
+ return new YouTubeAdapter(creds, fetchImpl);
1893
+ }
1894
+ headers(extra) {
1895
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
1896
+ }
1897
+ async accountMe() {
1898
+ const res = await jsonRequest(this.fetchImpl, `${API}/channels`, {
1899
+ headers: this.headers(),
1900
+ query: { part: "snippet", mine: "true" },
1901
+ errorLabel: "YouTube"
1902
+ });
1903
+ const channel = res.items?.[0];
1904
+ if (!channel) {
1905
+ throw new Error("YouTube account.me: no channel found for the authenticated user");
1906
+ }
1907
+ return {
1908
+ id: channel.id,
1909
+ username: channel.snippet?.customUrl ?? channel.snippet?.title,
1910
+ displayName: channel.snippet?.title,
1911
+ url: `https://www.youtube.com/channel/${channel.id}`
1912
+ };
1913
+ }
1914
+ async postCreate(input) {
1915
+ const videoId = input.mediaIds?.[0];
1916
+ if (!videoId) {
1917
+ throw new ConnectorOperationNotSupported("youtube", "post.create (YouTube is video-only: upload a video via media.upload and pass its id in `mediaIds`)");
1918
+ }
1919
+ const title = input.title ?? input.text ?? "";
1920
+ const description = input.description ?? input.text ?? "";
1921
+ await jsonRequest(this.fetchImpl, `${API}/videos`, {
1922
+ method: "PUT",
1923
+ headers: this.headers({ "Content-Type": "application/json" }),
1924
+ query: { part: "snippet" },
1925
+ body: {
1926
+ id: videoId,
1927
+ snippet: { title, description, categoryId: input.categoryId ?? "22" }
1928
+ },
1929
+ errorLabel: "YouTube"
1930
+ });
1931
+ return { id: videoId, url: `https://www.youtube.com/watch?v=${videoId}` };
1932
+ }
1933
+ async postDelete(input) {
1934
+ await jsonRequest(this.fetchImpl, `${API}/videos`, {
1935
+ method: "DELETE",
1936
+ headers: this.headers(),
1937
+ query: { id: input.id },
1938
+ errorLabel: "YouTube"
1939
+ });
1940
+ return { id: input.id, deleted: true };
1941
+ }
1942
+ async mediaUpload(input) {
1943
+ const buffer = decodeBase64(input.dataBase64);
1944
+ const size = buffer.byteLength;
1945
+ const start = await this.fetchImpl(`${UPLOAD}/videos?uploadType=resumable&part=snippet,status`, {
1946
+ method: "POST",
1947
+ headers: this.headers({
1948
+ "Content-Type": "application/json",
1949
+ "X-Upload-Content-Type": input.mimeType,
1950
+ "X-Upload-Content-Length": String(size)
1951
+ }),
1952
+ body: JSON.stringify({
1953
+ snippet: { title: input.title ?? input.altText ?? "Untitled", description: input.description ?? "" },
1954
+ status: { privacyStatus: input.privacyStatus ?? "private" }
1955
+ })
1956
+ });
1957
+ if (!start.ok) {
1958
+ throw new Error(`YouTube ${start.status}: resumable upload init failed`);
1959
+ }
1960
+ const location = start.headers?.get("location");
1961
+ if (!location) {
1962
+ throw new Error("YouTube media.upload: resumable session did not return a Location header");
1963
+ }
1964
+ const bytes = Uint8Array.from(buffer);
1965
+ const put = await this.fetchImpl(location, {
1966
+ method: "PUT",
1967
+ headers: { "Content-Type": input.mimeType, "Content-Length": String(size) },
1968
+ body: bytes
1969
+ });
1970
+ const text = await put.text();
1971
+ const data = text ? JSON.parse(text) : {};
1972
+ if (!put.ok) {
1973
+ throw new Error(`YouTube ${put.status}: video upload failed`);
1974
+ }
1975
+ return { mediaId: String(data.id) };
1976
+ }
1977
+ async mentionsList(_input = {}) {
1978
+ throw new ConnectorOperationNotSupported("youtube", "mentions.list (no mentions endpoint in the YouTube Data API)");
1979
+ }
1980
+ async analyticsPost(input) {
1981
+ const res = await jsonRequest(this.fetchImpl, `${API}/videos`, {
1982
+ headers: this.headers(),
1983
+ query: { part: "statistics", id: input.id },
1984
+ errorLabel: "YouTube"
1985
+ });
1986
+ const stats = res.items?.[0]?.statistics;
1987
+ if (!stats) {
1988
+ throw new ConnectorOperationNotSupported("youtube", `analytics.post (video not found: ${input.id})`);
1989
+ }
1990
+ return {
1991
+ metrics: {
1992
+ viewCount: Number(stats.viewCount ?? 0),
1993
+ likeCount: Number(stats.likeCount ?? 0),
1994
+ commentCount: Number(stats.commentCount ?? 0),
1995
+ favoriteCount: Number(stats.favoriteCount ?? 0)
1996
+ }
1997
+ };
1998
+ }
1999
+ }
2000
+
2001
+ // src/social/pinterest.ts
2002
+ var BASE3 = "https://api.pinterest.com";
2003
+
2004
+ class PinterestAdapter {
2005
+ accessToken;
2006
+ defaultBoardId;
2007
+ fetchImpl;
2008
+ constructor(creds, fetchImpl) {
2009
+ if (!creds || !creds.accessToken) {
2010
+ throw new Error("pinterest credentials require `accessToken`");
2011
+ }
2012
+ this.accessToken = creds.accessToken;
2013
+ this.defaultBoardId = creds.boardId;
2014
+ this.fetchImpl = resolveFetch(fetchImpl);
2015
+ }
2016
+ static fromCredentials(creds, fetchImpl) {
2017
+ return new PinterestAdapter(creds, fetchImpl);
2018
+ }
2019
+ headers(extra) {
2020
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
2021
+ }
2022
+ async accountMe() {
2023
+ const acct = await jsonRequest(this.fetchImpl, `${BASE3}/v5/user_account`, { headers: this.headers(), errorLabel: "Pinterest" });
2024
+ return {
2025
+ id: acct.username ?? "",
2026
+ username: acct.username,
2027
+ displayName: acct.username,
2028
+ url: acct.username ? `https://www.pinterest.com/${acct.username}/` : undefined
2029
+ };
2030
+ }
2031
+ async postCreate(input) {
2032
+ const boardId = input.boardId ?? this.defaultBoardId;
2033
+ if (!boardId) {
2034
+ throw new Error("pinterest post.create requires `boardId` (in input or credentials)");
2035
+ }
2036
+ const mediaId = input.mediaIds?.[0];
2037
+ let mediaSource;
2038
+ if (mediaId) {
2039
+ mediaSource = { source_type: "image_upload", media_id: mediaId };
2040
+ } else if (input.imageUrl) {
2041
+ mediaSource = { source_type: "image_url", url: input.imageUrl };
2042
+ } else {
2043
+ throw new Error("pinterest post.create requires an image: provide a `mediaIds` entry or `imageUrl`");
2044
+ }
2045
+ const body = {
2046
+ board_id: boardId,
2047
+ description: input.text,
2048
+ media_source: mediaSource
2049
+ };
2050
+ if (input.title)
2051
+ body.title = input.title;
2052
+ if (input.link)
2053
+ body.link = input.link;
2054
+ const pin = await jsonRequest(this.fetchImpl, `${BASE3}/v5/pins`, {
2055
+ method: "POST",
2056
+ headers: this.headers({ "Content-Type": "application/json" }),
2057
+ body,
2058
+ errorLabel: "Pinterest"
2059
+ });
2060
+ return { id: pin.id, url: `https://www.pinterest.com/pin/${pin.id}/` };
2061
+ }
2062
+ async postDelete(input) {
2063
+ await jsonRequest(this.fetchImpl, `${BASE3}/v5/pins/${encodeURIComponent(input.id)}`, {
2064
+ method: "DELETE",
2065
+ headers: this.headers(),
2066
+ errorLabel: "Pinterest"
2067
+ });
2068
+ return { id: input.id, deleted: true };
2069
+ }
2070
+ async mediaUpload(input) {
2071
+ const register = await jsonRequest(this.fetchImpl, `${BASE3}/v5/media`, {
2072
+ method: "POST",
2073
+ headers: this.headers({ "Content-Type": "application/json" }),
2074
+ body: { media_type: "image" },
2075
+ errorLabel: "Pinterest"
2076
+ });
2077
+ const buffer = decodeBase64(input.dataBase64);
2078
+ const bytes = Uint8Array.from(buffer);
2079
+ const form = new FormData;
2080
+ for (const [k, v] of Object.entries(register.upload_parameters ?? {})) {
2081
+ form.append(k, v);
2082
+ }
2083
+ form.append("file", new Blob([bytes], { type: input.mimeType }));
2084
+ const put = await this.fetchImpl(register.upload_url, { method: "POST", body: form });
2085
+ if (!put.ok) {
2086
+ throw new Error(`Pinterest ${put.status}: media upload failed`);
2087
+ }
2088
+ return { mediaId: register.media_id };
2089
+ }
2090
+ async mentionsList(_input = {}) {
2091
+ throw new ConnectorOperationNotSupported("pinterest", "mentions.list (no mentions endpoint in the Pinterest API)");
2092
+ }
2093
+ async analyticsPost(input) {
2094
+ const res = await jsonRequest(this.fetchImpl, `${BASE3}/v5/pins/${encodeURIComponent(input.id)}/analytics`, {
2095
+ headers: this.headers(),
2096
+ query: { metric_types: "IMPRESSION,PIN_CLICK,SAVE,OUTBOUND_CLICK" },
2097
+ errorLabel: "Pinterest"
2098
+ });
2099
+ const summary = res.all?.["DAILY"]?.summary_metrics ?? res.all?.["TOTAL"]?.summary_metrics ?? {};
2100
+ const metrics = {};
2101
+ for (const [k, v] of Object.entries(summary)) {
2102
+ metrics[k] = typeof v === "number" ? v : Number(v) || 0;
2103
+ }
2104
+ return { metrics };
2105
+ }
2106
+ }
2107
+
2108
+ // src/social/googlebusinessprofile.ts
2109
+ var ACCT_MGMT = "https://mybusinessaccountmanagement.googleapis.com/v1";
2110
+ var V4 = "https://mybusiness.googleapis.com/v4";
2111
+
2112
+ class GoogleBusinessProfileAdapter {
2113
+ accessToken;
2114
+ defaultAccountId;
2115
+ defaultLocationId;
2116
+ fetchImpl;
2117
+ constructor(creds, fetchImpl) {
2118
+ if (!creds || !creds.accessToken) {
2119
+ throw new Error("googlebusinessprofile credentials require `accessToken`");
2120
+ }
2121
+ this.accessToken = creds.accessToken;
2122
+ this.defaultAccountId = creds.accountId;
2123
+ this.defaultLocationId = creds.locationId;
2124
+ this.fetchImpl = resolveFetch(fetchImpl);
2125
+ }
2126
+ static fromCredentials(creds, fetchImpl) {
2127
+ return new GoogleBusinessProfileAdapter(creds, fetchImpl);
2128
+ }
2129
+ headers(extra) {
2130
+ return { Authorization: `Bearer ${this.accessToken}`, ...extra ?? {} };
2131
+ }
2132
+ resolveIds(input) {
2133
+ const accountId = input.accountId ?? this.defaultAccountId;
2134
+ const locationId = input.locationId ?? this.defaultLocationId;
2135
+ if (!accountId)
2136
+ throw new Error("googlebusinessprofile requires `accountId` (in input or credentials)");
2137
+ if (!locationId)
2138
+ throw new Error("googlebusinessprofile requires `locationId` (in input or credentials)");
2139
+ return { accountId, locationId };
2140
+ }
2141
+ async accountMe() {
2142
+ const res = await jsonRequest(this.fetchImpl, `${ACCT_MGMT}/accounts`, { headers: this.headers(), errorLabel: "GoogleBusinessProfile" });
2143
+ const account = res.accounts?.[0];
2144
+ if (!account) {
2145
+ throw new Error("googlebusinessprofile account.me: no accounts found for the authenticated user");
2146
+ }
2147
+ const id = (account.name ?? "").replace(/^accounts\//, "");
2148
+ return {
2149
+ id,
2150
+ username: account.accountName,
2151
+ displayName: account.accountName,
2152
+ url: undefined
2153
+ };
2154
+ }
2155
+ async postCreate(input) {
2156
+ const { accountId, locationId } = this.resolveIds(input);
2157
+ const parent = `accounts/${accountId}/locations/${locationId}`;
2158
+ const body = {
2159
+ languageCode: "en-US",
2160
+ summary: input.text,
2161
+ topicType: "STANDARD"
2162
+ };
2163
+ if (input.cta) {
2164
+ body.callToAction = { actionType: input.cta.actionType, url: input.cta.url };
2165
+ }
2166
+ if (input.mediaIds && input.mediaIds.length > 0) {
2167
+ body.media = input.mediaIds.map((m) => ({ mediaFormat: "PHOTO", sourceUrl: m }));
2168
+ }
2169
+ const post = await jsonRequest(this.fetchImpl, `${V4}/${parent}/localPosts`, { method: "POST", headers: this.headers({ "Content-Type": "application/json" }), body, errorLabel: "GoogleBusinessProfile" });
2170
+ return { id: post.name, url: post.searchUrl };
2171
+ }
2172
+ async postDelete(input) {
2173
+ await jsonRequest(this.fetchImpl, `${V4}/${input.id}`, {
2174
+ method: "DELETE",
2175
+ headers: this.headers(),
2176
+ errorLabel: "GoogleBusinessProfile"
2177
+ });
2178
+ return { id: input.id, deleted: true };
2179
+ }
2180
+ async mediaUpload(input) {
2181
+ if (!input.sourceUrl) {
2182
+ throw new ConnectorOperationNotSupported("googlebusinessprofile", "media.upload (provide a public `sourceUrl`; raw binary upload uses a separate resumable service)");
2183
+ }
2184
+ const { accountId, locationId } = this.resolveIds(input);
2185
+ const parent = `accounts/${accountId}/locations/${locationId}`;
2186
+ const media = await jsonRequest(this.fetchImpl, `${V4}/${parent}/media`, {
2187
+ method: "POST",
2188
+ headers: this.headers({ "Content-Type": "application/json" }),
2189
+ body: {
2190
+ mediaFormat: "PHOTO",
2191
+ locationAssociation: { category: "ADDITIONAL" },
2192
+ sourceUrl: input.sourceUrl
2193
+ },
2194
+ errorLabel: "GoogleBusinessProfile"
2195
+ });
2196
+ return { mediaId: media.name };
2197
+ }
2198
+ async mentionsList(_input = {}) {
2199
+ throw new ConnectorOperationNotSupported("googlebusinessprofile", "mentions.list (no mentions concept; reviews are a separate surface)");
2200
+ }
2201
+ async analyticsPost(_input) {
2202
+ throw new ConnectorOperationNotSupported("googlebusinessprofile", "analytics.post (per-localPost insights are not exposed; use location-level Performance API)");
2203
+ }
2204
+ }
2205
+
1470
2206
  // src/social/index.ts
1471
- var SUPPORTED_CONNECTORS = ["x", "mastodon", "bluesky"];
2207
+ var SUPPORTED_CONNECTORS = [
2208
+ "x",
2209
+ "mastodon",
2210
+ "bluesky",
2211
+ "linkedin",
2212
+ "reddit",
2213
+ "tiktok",
2214
+ "youtube",
2215
+ "pinterest",
2216
+ "googlebusinessprofile"
2217
+ ];
2218
+ var ALL_OPS = [
2219
+ "account.me",
2220
+ "post.create",
2221
+ "post.delete",
2222
+ "media.upload",
2223
+ "mentions.list",
2224
+ "analytics.post"
2225
+ ];
1472
2226
  var SUPPORTED_OPERATIONS = {
1473
- x: ["account.me", "post.create", "post.delete", "media.upload", "mentions.list", "analytics.post"],
1474
- mastodon: ["account.me", "post.create", "post.delete", "media.upload", "mentions.list", "analytics.post"],
1475
- bluesky: ["account.me", "post.create", "post.delete", "media.upload", "mentions.list", "analytics.post"]
2227
+ x: [...ALL_OPS],
2228
+ mastodon: [...ALL_OPS],
2229
+ bluesky: [...ALL_OPS],
2230
+ linkedin: ["account.me", "post.create", "post.delete", "media.upload"],
2231
+ reddit: ["account.me", "post.create", "post.delete", "mentions.list", "analytics.post"],
2232
+ tiktok: ["account.me", "post.create", "media.upload"],
2233
+ youtube: ["account.me", "post.create", "post.delete", "media.upload", "analytics.post"],
2234
+ pinterest: ["account.me", "post.create", "post.delete", "media.upload", "analytics.post"],
2235
+ googlebusinessprofile: ["account.me", "post.create", "post.delete", "media.upload"]
1476
2236
  };
1477
2237
  function listSocialConnectors() {
1478
2238
  return [...SUPPORTED_CONNECTORS];
@@ -1492,6 +2252,18 @@ function buildAdapter(connector, credentials) {
1492
2252
  return MastodonAdapter.fromCredentials(credentials);
1493
2253
  case "bluesky":
1494
2254
  return BlueskyAdapter.fromCredentials(credentials);
2255
+ case "linkedin":
2256
+ return LinkedInAdapter.fromCredentials(credentials);
2257
+ case "reddit":
2258
+ return RedditAdapter.fromCredentials(credentials);
2259
+ case "tiktok":
2260
+ return TikTokAdapter.fromCredentials(credentials);
2261
+ case "youtube":
2262
+ return YouTubeAdapter.fromCredentials(credentials);
2263
+ case "pinterest":
2264
+ return PinterestAdapter.fromCredentials(credentials);
2265
+ case "googlebusinessprofile":
2266
+ return GoogleBusinessProfileAdapter.fromCredentials(credentials);
1495
2267
  default:
1496
2268
  throw new ConnectorOperationNotSupported(connector, "*");
1497
2269
  }