index.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. require('dotenv').config();
  2. const axios = require('axios');
  3. const BasePlatformService = require('./utils/BasePlatformService');
  4. const { getDb } = require('./utils/MongoDBConnector');
  5. const { decryptToken, warnIfNoKey } = require('./utils/crypto');
  6. const { getWorkspaceCredential } = require('./utils/credentials');
  7. const PINTEREST_API = 'https://api.pinterest.com/v5';
  8. class PinterestService extends BasePlatformService {
  9. constructor() {
  10. super('pinterest');
  11. }
  12. async _getAccount(workspaceId = 'default') {
  13. try {
  14. const db = await getDb();
  15. const cred = await getWorkspaceCredential(db, 'pinterest', workspaceId);
  16. if (cred?.accessToken) {
  17. return { ...cred, accessToken: decryptToken(cred.accessToken) };
  18. }
  19. } catch (_) {}
  20. return null;
  21. }
  22. async getStatus(workspaceId = 'default') {
  23. const account = await this._getAccount(workspaceId);
  24. if (!account?.accessToken) {
  25. return { connected: false, platform: 'pinterest', error: 'Not connected — use Settings to connect via Pinterest OAuth' };
  26. }
  27. try {
  28. const res = await axios.get(`${PINTEREST_API}/user_account`, {
  29. headers: { Authorization: `Bearer ${account.accessToken}` },
  30. timeout: 10000,
  31. });
  32. const selectedBoards = (account.boards || []).filter((b) => b.selected);
  33. return {
  34. connected: selectedBoards.length > 0,
  35. platform: 'pinterest',
  36. username: res.data.username,
  37. displayName: res.data.business_name || res.data.username,
  38. avatar: res.data.profile_image,
  39. boardCount: selectedBoards.length,
  40. };
  41. } catch (err) {
  42. return { connected: false, platform: 'pinterest', error: err.response?.data?.message || err.message };
  43. }
  44. }
  45. async fetchFeed({ limit = 25, workspaceId = 'default' } = {}) {
  46. const account = await this._getAccount(workspaceId);
  47. if (!account?.accessToken) throw new Error('Pinterest not connected');
  48. const allItems = [];
  49. const selectedBoards = (account.boards || []).filter((b) => b.selected);
  50. const boardsToFetch = selectedBoards.length > 0 ? selectedBoards : (account.boards || []).slice(0, 3);
  51. for (const board of boardsToFetch) {
  52. try {
  53. const res = await axios.get(`${PINTEREST_API}/boards/${board.id}/pins`, {
  54. headers: { Authorization: `Bearer ${account.accessToken}` },
  55. params: { page_size: Math.min(Math.ceil(Number(limit) / Math.max(boardsToFetch.length, 1)), 50) },
  56. timeout: 15000,
  57. });
  58. const items = (res.data.items || []).map((pin) =>
  59. this.normalizeFeedItem({
  60. originalId: pin.id,
  61. author: {
  62. name: account.displayName || account.username || 'Pinterest',
  63. username: account.username || '',
  64. },
  65. content: pin.description || pin.title || '',
  66. media: pin.media?.images?.['600x']?.url
  67. ? [{ url: pin.media.images['600x'].url, type: 'image' }]
  68. : [],
  69. metrics: { likes: pin.save_count || 0, comments: pin.comment_count || 0, shares: 0 },
  70. url: `https://www.pinterest.com/pin/${pin.id}/`,
  71. createdAt: pin.created_at || new Date(),
  72. })
  73. );
  74. allItems.push(...items);
  75. } catch (err) {
  76. this.app.log.warn({ action: 'feed_fetch', platform: 'pinterest', boardId: board.id, outcome: 'failure', err: err.message });
  77. }
  78. }
  79. try {
  80. const db = await getDb();
  81. const col = db.collection('feeds');
  82. for (const item of allItems) {
  83. await col.updateOne(
  84. { platform: 'pinterest', originalId: item.originalId, workspaceId },
  85. { $set: { ...item, workspaceId } },
  86. { upsert: true }
  87. );
  88. }
  89. } catch (err) {
  90. this.app.log.error({ action: 'feed_write', platform: 'pinterest', outcome: 'failure', err: err.message });
  91. }
  92. return allItems;
  93. }
  94. async publishPost({ content, imageUrl, accountId: boardId, workspaceId = 'default' } = {}) {
  95. const account = await this._getAccount(workspaceId);
  96. if (!account?.accessToken) throw new Error('Pinterest not connected');
  97. if (!boardId) throw new Error('boardId is required for Pinterest — select a board as destination');
  98. if (!imageUrl) throw new Error('Pinterest requires an image URL');
  99. const pinData = {
  100. board_id: boardId,
  101. description: content || '',
  102. media_source: {
  103. source_type: 'image_url',
  104. url: imageUrl,
  105. },
  106. };
  107. if (content) {
  108. pinData.title = content.slice(0, 100);
  109. }
  110. try {
  111. const res = await axios.post(`${PINTEREST_API}/pins`, pinData, {
  112. headers: {
  113. Authorization: `Bearer ${account.accessToken}`,
  114. 'Content-Type': 'application/json',
  115. },
  116. timeout: 30000,
  117. });
  118. this.app.log.info({ action: 'publish_post', platform: 'pinterest', boardId, pinId: res.data.id, outcome: 'success' });
  119. return { pinId: res.data.id, boardId };
  120. } catch (err) {
  121. const msg = err.response?.data?.message || err.message;
  122. this.app.log.error({ action: 'publish_post', platform: 'pinterest', boardId, outcome: 'failure', err: msg });
  123. throw new Error(msg);
  124. }
  125. }
  126. }
  127. const service = new PinterestService();
  128. // Returns selected boards from DB (used by gateway/compose to list destinations)
  129. service.app.get('/boards', async (request, reply) => {
  130. try {
  131. const workspaceId = request.headers['x-workspace-id'] || 'default';
  132. const account = await service._getAccount(workspaceId);
  133. if (!account) return { boards: [] };
  134. const selected = (account.boards || []).filter((b) => b.selected);
  135. return { boards: selected };
  136. } catch (err) {
  137. reply.code(500).send({ success: false, error: err.message });
  138. }
  139. });
  140. warnIfNoKey('pinterest');
  141. service.start(process.env.PORT || 3008);