index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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 GRAPH_API = 'https://graph.facebook.com/v22.0';
  8. class FacebookService extends BasePlatformService {
  9. constructor() {
  10. super('facebook');
  11. }
  12. // Read selected Facebook Pages from MongoDB.
  13. // Falls back to env vars for backwards compatibility.
  14. async _getPages(workspaceId = 'default') {
  15. try {
  16. const db = await getDb();
  17. const cred = await getWorkspaceCredential(db, 'facebook', workspaceId);
  18. const dbPages = (cred?.pages || []).filter((p) => p.selected);
  19. if (dbPages.length > 0) {
  20. return dbPages.map((p) => ({ ...p, accessToken: decryptToken(p.accessToken) })).filter((p) => p.accessToken);
  21. }
  22. } catch (_) { /* fall through */ }
  23. // Env var fallback (legacy single-page mode)
  24. const { FACEBOOK_PAGE_ACCESS_TOKEN, FACEBOOK_PAGE_ID } = process.env;
  25. if (FACEBOOK_PAGE_ACCESS_TOKEN && FACEBOOK_PAGE_ID) {
  26. return [{ id: FACEBOOK_PAGE_ID, accessToken: FACEBOOK_PAGE_ACCESS_TOKEN }];
  27. }
  28. return [];
  29. }
  30. async getStatus(workspaceId = 'default') {
  31. const pages = await this._getPages(workspaceId);
  32. if (pages.length === 0) {
  33. return { connected: false, platform: 'facebook', error: 'No Facebook Pages connected — use Settings to connect via Facebook OAuth' };
  34. }
  35. try {
  36. const first = pages[0];
  37. const res = await axios.get(`${GRAPH_API}/${first.id}`, {
  38. params: {
  39. fields: 'id,name,username,picture',
  40. access_token: first.accessToken,
  41. },
  42. });
  43. return {
  44. connected: true,
  45. platform: 'facebook',
  46. username: res.data.username || res.data.name,
  47. displayName: res.data.name,
  48. avatar: res.data.picture?.data?.url,
  49. pageCount: pages.length,
  50. };
  51. } catch (err) {
  52. return { connected: false, platform: 'facebook', error: err.response?.data?.error?.message || err.message };
  53. }
  54. }
  55. async fetchFeed({ limit = 20, workspaceId = 'default' } = {}) {
  56. const pages = await this._getPages(workspaceId);
  57. if (pages.length === 0) throw new Error('No Facebook Pages connected');
  58. const allItems = [];
  59. for (const page of pages) {
  60. const res = await axios.get(`${GRAPH_API}/${page.id}/feed`, {
  61. params: {
  62. fields: 'id,message,story,full_picture,created_time,permalink_url,likes.summary(true),comments.summary(true),shares',
  63. limit: Math.min(Number(limit), 100),
  64. access_token: page.accessToken,
  65. },
  66. });
  67. const items = (res.data.data || []).map((post) =>
  68. this.normalizeFeedItem({
  69. originalId: post.id,
  70. author: {
  71. name: page.name || '',
  72. username: page.name || '',
  73. },
  74. content: post.message || post.story || '',
  75. media: post.full_picture ? [{ url: post.full_picture, type: 'image' }] : [],
  76. metrics: {
  77. likes: post.likes?.summary?.total_count || 0,
  78. comments: post.comments?.summary?.total_count || 0,
  79. shares: post.shares?.count || 0,
  80. },
  81. url: post.permalink_url,
  82. createdAt: post.created_time,
  83. })
  84. );
  85. allItems.push(...items);
  86. }
  87. try {
  88. const db = await getDb();
  89. const col = db.collection('feeds');
  90. for (const item of allItems) {
  91. await col.updateOne(
  92. { platform: 'facebook', originalId: item.originalId, workspaceId },
  93. { $set: { ...item, workspaceId } },
  94. { upsert: true }
  95. );
  96. }
  97. } catch (err) {
  98. this.app.log.error({ action: 'feed_write', platform: 'facebook', outcome: 'failure', err: err.message });
  99. }
  100. return allItems;
  101. }
  102. async publishPost({ content, link, imageUrl, accountId, firstComment, workspaceId = 'default' } = {}) {
  103. const allPages = await this._getPages(workspaceId);
  104. if (allPages.length === 0) throw new Error('No Facebook Pages connected');
  105. if (!content) throw new Error('content is required');
  106. // If a specific page is requested, target only that page
  107. const pages = accountId ? allPages.filter((p) => p.id === accountId) : allPages;
  108. if (pages.length === 0) throw new Error(`Facebook page ${accountId} not found or not connected`);
  109. const results = [];
  110. for (const page of pages) {
  111. const params = { message: content, access_token: page.accessToken };
  112. if (link) params.link = link;
  113. if (imageUrl) params.picture = imageUrl;
  114. const res = await axios.post(`${GRAPH_API}/${page.id}/feed`, null, { params });
  115. const postId = res.data.id;
  116. if (firstComment?.trim()) {
  117. try {
  118. await axios.post(`${GRAPH_API}/${postId}/comments`, null, {
  119. params: { message: firstComment.trim(), access_token: page.accessToken },
  120. timeout: 10000,
  121. });
  122. this.app.log.info({ action: 'first_comment', platform: 'facebook', postId, outcome: 'success' });
  123. } catch (err) {
  124. this.app.log.warn({ action: 'first_comment', platform: 'facebook', postId, outcome: 'failure', err: err.response?.data?.error?.message || err.message });
  125. }
  126. }
  127. results.push({ pageId: page.id, pageName: page.name, postId });
  128. }
  129. return results;
  130. }
  131. }
  132. const service = new FacebookService();
  133. warnIfNoKey('facebook');
  134. service.start(process.env.PORT || 3006);