index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. require('dotenv').config();
  2. const axios = require('axios');
  3. const BasePlatformService = require('./utils/BasePlatformService');
  4. const { getDb } = require('./utils/MongoDBConnector');
  5. const GRAPH_API = 'https://graph.facebook.com/v22.0';
  6. class InstagramService extends BasePlatformService {
  7. constructor() {
  8. super('instagram');
  9. }
  10. // Read selected Instagram Business Accounts from MongoDB.
  11. // Falls back to env vars for backwards compatibility.
  12. async _getAccounts() {
  13. try {
  14. const db = await getDb();
  15. const cred = await db.collection('platform_credentials').findOne({ _id: 'instagram' });
  16. const dbAccounts = (cred?.accounts || []).filter((a) => a.selected);
  17. if (dbAccounts.length > 0) return dbAccounts;
  18. } catch (_) { /* fall through */ }
  19. // Env var fallback (legacy single-account mode)
  20. const { INSTAGRAM_ACCESS_TOKEN, INSTAGRAM_BUSINESS_ACCOUNT_ID } = process.env;
  21. if (INSTAGRAM_ACCESS_TOKEN && INSTAGRAM_BUSINESS_ACCOUNT_ID) {
  22. return [{ id: INSTAGRAM_BUSINESS_ACCOUNT_ID, accessToken: INSTAGRAM_ACCESS_TOKEN }];
  23. }
  24. return [];
  25. }
  26. async getStatus() {
  27. const accounts = await this._getAccounts();
  28. if (accounts.length === 0) {
  29. return { connected: false, platform: 'instagram', error: 'No Instagram accounts connected — use Settings to connect via Facebook OAuth' };
  30. }
  31. try {
  32. const first = accounts[0];
  33. const res = await axios.get(`${GRAPH_API}/${first.id}`, {
  34. params: {
  35. fields: 'id,name,username,profile_picture_url',
  36. access_token: first.accessToken,
  37. },
  38. });
  39. return {
  40. connected: true,
  41. platform: 'instagram',
  42. username: res.data.username || res.data.name,
  43. displayName: res.data.name,
  44. avatar: res.data.profile_picture_url,
  45. accountCount: accounts.length,
  46. };
  47. } catch (err) {
  48. return { connected: false, platform: 'instagram', error: err.response?.data?.error?.message || err.message };
  49. }
  50. }
  51. async fetchFeed({ limit = 20 } = {}) {
  52. const accounts = await this._getAccounts();
  53. if (accounts.length === 0) throw new Error('No Instagram accounts connected');
  54. const allItems = [];
  55. for (const account of accounts) {
  56. const res = await axios.get(`${GRAPH_API}/${account.id}/media`, {
  57. params: {
  58. fields: 'id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count,username',
  59. limit: Math.min(Number(limit), 100),
  60. access_token: account.accessToken,
  61. },
  62. });
  63. const items = (res.data.data || []).map((post) =>
  64. this.normalizeFeedItem({
  65. originalId: post.id,
  66. author: {
  67. name: post.username || account.username || '',
  68. username: post.username || account.username || '',
  69. profileUrl: `https://www.instagram.com/${post.username || account.username || ''}/`,
  70. },
  71. content: post.caption || '',
  72. media: post.media_url
  73. ? [{
  74. url: post.media_url,
  75. type: (post.media_type || 'IMAGE').toLowerCase(),
  76. thumbnail: post.thumbnail_url || post.media_url,
  77. }]
  78. : [],
  79. metrics: {
  80. likes: post.like_count || 0,
  81. comments: post.comments_count || 0,
  82. },
  83. url: post.permalink,
  84. createdAt: post.timestamp,
  85. })
  86. );
  87. allItems.push(...items);
  88. }
  89. try {
  90. const db = await getDb();
  91. const col = db.collection('feeds');
  92. for (const item of allItems) {
  93. await col.updateOne(
  94. { platform: 'instagram', originalId: item.originalId },
  95. { $set: item },
  96. { upsert: true }
  97. );
  98. }
  99. } catch (err) {
  100. this.app.log.error({ action: 'feed_write', platform: 'instagram', outcome: 'failure', err: err.message });
  101. }
  102. return allItems;
  103. }
  104. // Instagram requires media (image_url or video_url) — text-only posts are not supported.
  105. async publishPost({ content, imageUrl, videoUrl, accountId } = {}) {
  106. const allAccounts = await this._getAccounts();
  107. if (allAccounts.length === 0) throw new Error('No Instagram accounts connected');
  108. if (!imageUrl && !videoUrl) {
  109. throw new Error('Instagram requires imageUrl or videoUrl — text-only posts are not supported by the Graph API');
  110. }
  111. // If a specific account is requested, target only that account
  112. const accounts = accountId ? allAccounts.filter((a) => a.id === accountId) : allAccounts;
  113. if (accounts.length === 0) throw new Error(`Instagram account ${accountId} not found or not connected`);
  114. const results = [];
  115. for (const account of accounts) {
  116. const containerParams = {
  117. caption: content,
  118. access_token: account.accessToken,
  119. };
  120. if (videoUrl) {
  121. containerParams.media_type = 'REELS';
  122. containerParams.video_url = videoUrl;
  123. } else {
  124. containerParams.image_url = imageUrl;
  125. }
  126. const containerRes = await axios.post(
  127. `${GRAPH_API}/${account.id}/media`,
  128. null,
  129. { params: containerParams }
  130. );
  131. const publishRes = await axios.post(
  132. `${GRAPH_API}/${account.id}/media_publish`,
  133. null,
  134. { params: { creation_id: containerRes.data.id, access_token: account.accessToken } }
  135. );
  136. results.push({ accountId: account.id, username: account.username, postId: publishRes.data.id });
  137. }
  138. return results;
  139. }
  140. }
  141. const service = new InstagramService();
  142. service.start(process.env.PORT || 3005);