index.js 5.4 KB

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