From af5208e0386581e72f44f05cebdd593ce8f13209 Mon Sep 17 00:00:00 2001 From: angelalala Date: Fri, 6 Sep 2024 17:51:57 -0700 Subject: [PATCH 01/18] feat: social media content post --- package-lock.json | 79 +++++++++++++ package.json | 2 + src/controllers/socialMediaController.js | 136 +++++++++++++++++++++++ src/routes/socialMediaRouter.js | 20 ++++ src/startup/routes.js | 3 + 5 files changed, 240 insertions(+) create mode 100644 src/controllers/socialMediaController.js create mode 100644 src/routes/socialMediaRouter.js diff --git a/package-lock.json b/package-lock.json index 3c4e3e99e..e126aaf50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4797,6 +4797,11 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4923,6 +4928,33 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5321,6 +5353,23 @@ } } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -11549,6 +11598,14 @@ } } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12368,6 +12425,23 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -14173,6 +14247,11 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "twitter-api-v2": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.17.2.tgz", + "integrity": "sha512-V8QvCkuQ+ydIakwYbVC4cuQxGlvjdRZI0L7TT5v9zGsf+SwX40rv3IFRHB8Z1cXJLcRFT0FI3xG3/f4zwS6cRA==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 8b88744dc..657536da0 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "babel-plugin-module-resolver": "^5.0.0", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", + "cheerio": "^1.0.0-rc.12", "cors": "^2.8.4", "cron": "^1.8.2", "dotenv": "^5.0.1", @@ -77,6 +78,7 @@ "redis": "^4.2.0", "sanitize-html": "^2.13.0", "supertest": "^6.3.4", + "twitter-api-v2": "^1.17.2", "uuid": "^3.4.0", "ws": "^8.17.1" }, diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js new file mode 100644 index 000000000..8516a9ac7 --- /dev/null +++ b/src/controllers/socialMediaController.js @@ -0,0 +1,136 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const fetch = require('node-fetch'); +// eslint-disable-next-line import/no-extraneous-dependencies +const cheerio = require('cheerio'); +// eslint-disable-next-line import/no-extraneous-dependencies +const { TwitterApi } = require('twitter-api-v2'); + +const TwitterClient = new TwitterApi({ + appKey: process.env.REACT_APP_TWITTER_APP_KEY, + appSecret: process.env.REACT_APP_TWITTER_APP_SECRET, + accessToken: process.env.REACT_APP_TWITTER_ACCESS_TOKEN, + accessSecret: process.env.REACT_APP_TWITTER_ACCESS_SECRET, +}); + +// const TwitterClient = new TwitterApi(process.env.REACT_APP_TWITTER_TOKEN); + +function extractTextAndImgUrl(htmlString) { + const $ = cheerio.load(htmlString); + + const textContent = $('body').text().replace(/\+/g, '').trim(); + + const imgUrls = $('img') + .map((i, img) => $(img).attr('src')) + .get(); + + return { textContent, imgUrls }; +} + +async function getPinterestAccessToken(req, res) { + const authCode = req.body.code; + const clientId = '1503261'; + const clientSecret = '2644a99853f263bd5688935762a32135293b950b'; + const accessTokenUrl = 'https://api-sandbox.pinterest.com/v5/oauth/token'; + + const authToken = btoa(`${clientId}:${clientSecret}`); + + const requestBody = new URLSearchParams(); + requestBody.append('grant_type', 'authorization_code'); + requestBody.append('code', authCode); + requestBody.append('redirect_uri', 'http://localhost:3000'); + + try { + console.log('try to fetch'); + const response = await fetch(accessTokenUrl, { + method: 'POST', + headers: { + Authorization: `Basic ${authToken}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: requestBody.toString(), + }); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('backend error: ', error); + } +} + +async function getListPins(req, res) { + const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; + const authToken = req.body.Authorization; + + try { + const response = await fetch(requestUrl, { + method: 'GET', + headers: { + Authorization: `${authToken}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); + const data = await response.json(); + res.json(data); + } catch (error) { + console.error('backend error: ', error); + } +} + +async function createPin(req, res) { + const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; + const authToken = req.body.Authorization; + + const { textContent, imgUrls } = extractTextAndImgUrl(req.body.EmailContent); + + console.log('Create Pin:', textContent, imgUrls); + + const requestBody = JSON.stringify({ + title: 'Weekly Update', + description: textContent, + dominant_color: '#6E7874', + board_id: '1074812336009724062', + media_source: { + source_type: 'image_url', + url: imgUrls[0], + }, + }); + + const response = await fetch(requestUrl, { + method: 'POST', + headers: { + Authorization: `${authToken}`, + 'Content-Type': 'application/json', + }, + body: requestBody, + }); + + const data = await response.json(); + console.log(data); + res.json(data); +} + +async function createTweet(req, res) { + const rwClient = TwitterClient.readWrite; + const { textContent, imgUrls } = extractTextAndImgUrl(req.body.EmailContent); + + console.log('Create Tweet:', textContent, imgUrls); + + console.log('#: ', TwitterClient); + + try { + const userInfo = await rwClient.v2.me(); + console.log('User Info:', userInfo); + const tweet = await rwClient.v2.tweet(textContent); + res.json({ success: true, data: tweet }); + } catch (error) { + console.error('Error posting tweet:', error); + res.status(500).json({ success: false, error: error.message }); + } +} + +module.exports = { + getPinterestAccessToken, + getListPins, + createPin, + createTweet, +}; diff --git a/src/routes/socialMediaRouter.js b/src/routes/socialMediaRouter.js new file mode 100644 index 000000000..65d982e0c --- /dev/null +++ b/src/routes/socialMediaRouter.js @@ -0,0 +1,20 @@ +const express = require('express'); +const { + getPinterestAccessToken, + getListPins, + createPin, + createTweet, +} = require('../controllers/socialMediaController'); + +const routes = function () { + const socialMediaRouter = express.Router(); + + socialMediaRouter.route('/getPinterestAccessToken').post(getPinterestAccessToken); + socialMediaRouter.route('/getListPins').post(getListPins); + socialMediaRouter.route('/createPin').post(createPin); + socialMediaRouter.route('/createTweet').post(createTweet); + + return socialMediaRouter; +}; + +module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index 82a4155a8..1aa5473ed 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -125,6 +125,8 @@ const blueSquareEmailAssignmentRouter = require('../routes/BlueSquareEmailAssign userProfile, ); +const socialMediaRouter = require('../routes/socialMediaRouter')(); + module.exports = function (app) { app.use('/api', forgotPwdRouter); app.use('/api', loginRouter); @@ -173,4 +175,5 @@ module.exports = function (app) { app.use('/api/bm', bmEquipmentRouter); app.use('/api/bm', bmConsumablesRouter); app.use('api', bmIssueRouter); + app.use('/api', socialMediaRouter); }; From c3bda61bbe1d4a2727653d2294bb7071c1eeba77 Mon Sep 17 00:00:00 2001 From: angelalala Date: Thu, 26 Sep 2024 21:09:19 -0700 Subject: [PATCH 02/18] feat: Tweet imagebase64 format picture --- src/controllers/socialMediaController.js | 33 +++++++++++++++++------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 8516a9ac7..add110e5d 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -19,11 +19,11 @@ function extractTextAndImgUrl(htmlString) { const textContent = $('body').text().replace(/\+/g, '').trim(); - const imgUrls = $('img') + const imgSrcs = $('img') .map((i, img) => $(img).attr('src')) .get(); - return { textContent, imgUrls }; + return { textContent, imgSrcs }; } async function getPinterestAccessToken(req, res) { @@ -111,17 +111,32 @@ async function createPin(req, res) { async function createTweet(req, res) { const rwClient = TwitterClient.readWrite; - const { textContent, imgUrls } = extractTextAndImgUrl(req.body.EmailContent); - - console.log('Create Tweet:', textContent, imgUrls); - - console.log('#: ', TwitterClient); + const { textContent, imgSrcs } = extractTextAndImgUrl(req.body.EmailContent); try { const userInfo = await rwClient.v2.me(); console.log('User Info:', userInfo); - const tweet = await rwClient.v2.tweet(textContent); - res.json({ success: true, data: tweet }); + + let mediaIds = []; + if (imgSrcs && imgSrcs.length > 0) { + console.log('Uploading media...'); + mediaIds = await Promise.all( + imgSrcs.map(async (imgSrc) => { + const base64Data = imgSrc.replace(/^data:image\/\w+;base64,/, ''); + const buffer = Buffer.from(base64Data, 'base64'); + const mimeType = 'image/png'; + return rwClient.v1.uploadMedia(buffer, { mimeType }); + }), + ); + console.log('Media uploaded, IDs:', mediaIds); + } + + const tweet = await rwClient.v2.tweet({ + text: textContent, + media: { media_ids: mediaIds }, + }); + + res.json({ success: true, tweet }); } catch (error) { console.error('Error posting tweet:', error); res.status(500).json({ success: false, error: error.message }); From c69d777242bb51717d5288d51b5ff11bce087437 Mon Sep 17 00:00:00 2001 From: angelalala Date: Thu, 26 Sep 2024 23:04:14 -0700 Subject: [PATCH 03/18] feat: upload base64 image in Pinterest --- src/controllers/socialMediaController.js | 94 +++++++++++++++--------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index add110e5d..ab6b11cba 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -79,34 +79,65 @@ async function getListPins(req, res) { async function createPin(req, res) { const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; const authToken = req.body.Authorization; + const { textContent, imgSrcs } = extractTextAndImgUrl(req.body.EmailContent); + try { + let requestBody; + + if (imgSrcs.length === 1) { + requestBody = JSON.stringify({ + title: 'Weekly Update', + description: textContent, + dominant_color: '#6E7874', + board_id: '1074812336009724062', + media_source: { + source_type: 'image_base64', + content_type: 'image/jpeg', + data: imgSrcs[0].replace(/^data:image\/\w+;base64,/, ''), + }, + }); + } else { + const items = imgSrcs.map((imgSrc) => ({ + content_type: 'image/jpeg', + data: imgSrc.replace(/^data:image\/\w+;base64,/, ''), + })); + + requestBody = JSON.stringify({ + title: 'Weekly Update', + description: textContent, + dominant_color: '#6E7874', + board_id: '1074812336009724062', + media_source: { + source_type: 'multiple_image_base64', + items, + }, + }); + } - const { textContent, imgUrls } = extractTextAndImgUrl(req.body.EmailContent); - - console.log('Create Pin:', textContent, imgUrls); - - const requestBody = JSON.stringify({ - title: 'Weekly Update', - description: textContent, - dominant_color: '#6E7874', - board_id: '1074812336009724062', - media_source: { - source_type: 'image_url', - url: imgUrls[0], - }, - }); - - const response = await fetch(requestUrl, { - method: 'POST', - headers: { - Authorization: `${authToken}`, - 'Content-Type': 'application/json', - }, - body: requestBody, - }); - - const data = await response.json(); - console.log(data); - res.json(data); + const response = await fetch(requestUrl, { + method: 'POST', + headers: { + Authorization: `${authToken}`, + 'Content-Type': 'application/json', + }, + body: requestBody, + }); + + const statusCode = response.status; + + if (statusCode >= 200 && statusCode < 300) { + const data = await response.json(); + res.status(200).json(data); + } else { + const errorData = await response.json(); + console.error('Error creating pin: ', errorData.message); + res.status(statusCode).json({ + message: errorData.message || 'Unexpected error', + }); + } + } catch (error) { + console.error('Network or other error: ', error); + res.status(500).json({ success: false, error: 'Internal server error' }); + } } async function createTweet(req, res) { @@ -114,9 +145,6 @@ async function createTweet(req, res) { const { textContent, imgSrcs } = extractTextAndImgUrl(req.body.EmailContent); try { - const userInfo = await rwClient.v2.me(); - console.log('User Info:', userInfo); - let mediaIds = []; if (imgSrcs && imgSrcs.length > 0) { console.log('Uploading media...'); @@ -136,10 +164,10 @@ async function createTweet(req, res) { media: { media_ids: mediaIds }, }); - res.json({ success: true, tweet }); + res.status(200).json({ success: true, tweet }); } catch (error) { - console.error('Error posting tweet:', error); - res.status(500).json({ success: false, error: error.message }); + console.error('Network or other error: ', error); + res.status(500).json({ success: false, error: 'Internal server error' }); } } From dfaa3f102f80efb9eafdf47460482aa443b5855f Mon Sep 17 00:00:00 2001 From: angelalala Date: Fri, 27 Sep 2024 21:30:24 -0700 Subject: [PATCH 04/18] refactor function --- src/controllers/socialMediaController.js | 79 ++++++++++++------------ 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index ab6b11cba..50e53ac59 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -75,67 +75,66 @@ async function getListPins(req, res) { console.error('backend error: ', error); } } - +// TODO: IF scr is link? async function createPin(req, res) { const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; const authToken = req.body.Authorization; const { textContent, imgSrcs } = extractTextAndImgUrl(req.body.EmailContent); + + if (imgSrcs.length === 0) { + return res.status(400).json({ message: 'No image found in the email content' }); + } + try { - let requestBody; - - if (imgSrcs.length === 1) { - requestBody = JSON.stringify({ - title: 'Weekly Update', - description: textContent, - dominant_color: '#6E7874', - board_id: '1074812336009724062', - media_source: { - source_type: 'image_base64', - content_type: 'image/jpeg', - data: imgSrcs[0].replace(/^data:image\/\w+;base64,/, ''), - }, - }); - } else { - const items = imgSrcs.map((imgSrc) => ({ - content_type: 'image/jpeg', - data: imgSrc.replace(/^data:image\/\w+;base64,/, ''), - })); - - requestBody = JSON.stringify({ - title: 'Weekly Update', - description: textContent, - dominant_color: '#6E7874', - board_id: '1074812336009724062', - media_source: { - source_type: 'multiple_image_base64', - items, - }, - }); - } + const baseRequestBody = { + title: 'Weekly Update', + description: textContent, + dominant_color: '#6E7874', + board_id: '1074812336009724062', + }; + + const mediaSource = + imgSrcs.length === 1 + ? { + source_type: 'image_base64', + content_type: 'image/jpeg', + data: imgSrcs[0].replace(/^data:image\/\w+;base64,/, ''), + } + : { + source_type: 'multiple_image_base64', + items: imgSrcs.map((imgSrc) => ({ + content_type: 'image/jpeg', + data: imgSrc.replace(/^data:image\/\w+;base64,/, ''), + })), + }; + + const requestBody = JSON.stringify({ + ...baseRequestBody, + media_source: mediaSource, + }); const response = await fetch(requestUrl, { method: 'POST', headers: { - Authorization: `${authToken}`, + Authorization: authToken, 'Content-Type': 'application/json', }, body: requestBody, }); const statusCode = response.status; + const responseData = await response.json(); if (statusCode >= 200 && statusCode < 300) { - const data = await response.json(); - res.status(200).json(data); + res.status(200).json(responseData); } else { - const errorData = await response.json(); - console.error('Error creating pin: ', errorData.message); + console.error('[Backend] Error creating Pin: ', responseData.message); res.status(statusCode).json({ - message: errorData.message || 'Unexpected error', + message: responseData.message || 'Unexpected error', }); } } catch (error) { - console.error('Network or other error: ', error); + console.error('[Backend] Network or other error: ', error); res.status(500).json({ success: false, error: 'Internal server error' }); } } @@ -166,7 +165,7 @@ async function createTweet(req, res) { res.status(200).json({ success: true, tweet }); } catch (error) { - console.error('Network or other error: ', error); + console.error('[Backend] Network or other error: ', error); res.status(500).json({ success: false, error: 'Internal server error' }); } } From 3707e048971c866cd64aec4cd0995ad712e4c6ed Mon Sep 17 00:00:00 2001 From: angelalala Date: Mon, 30 Sep 2024 21:40:14 -0700 Subject: [PATCH 05/18] rm unused function --- src/controllers/socialMediaController.js | 20 -------------------- src/routes/socialMediaRouter.js | 2 -- 2 files changed, 22 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 50e53ac59..27a04d68d 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -56,25 +56,6 @@ async function getPinterestAccessToken(req, res) { } } -async function getListPins(req, res) { - const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; - const authToken = req.body.Authorization; - - try { - const response = await fetch(requestUrl, { - method: 'GET', - headers: { - Authorization: `${authToken}`, - 'Content-Type': 'application/json', - Accept: 'application/json', - }, - }); - const data = await response.json(); - res.json(data); - } catch (error) { - console.error('backend error: ', error); - } -} // TODO: IF scr is link? async function createPin(req, res) { const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; @@ -172,7 +153,6 @@ async function createTweet(req, res) { module.exports = { getPinterestAccessToken, - getListPins, createPin, createTweet, }; diff --git a/src/routes/socialMediaRouter.js b/src/routes/socialMediaRouter.js index 65d982e0c..1a65874f4 100644 --- a/src/routes/socialMediaRouter.js +++ b/src/routes/socialMediaRouter.js @@ -1,7 +1,6 @@ const express = require('express'); const { getPinterestAccessToken, - getListPins, createPin, createTweet, } = require('../controllers/socialMediaController'); @@ -10,7 +9,6 @@ const routes = function () { const socialMediaRouter = express.Router(); socialMediaRouter.route('/getPinterestAccessToken').post(getPinterestAccessToken); - socialMediaRouter.route('/getListPins').post(getListPins); socialMediaRouter.route('/createPin').post(createPin); socialMediaRouter.route('/createTweet').post(createTweet); From 24a569886662609a8eb9bf17d992002c876ed67a Mon Sep 17 00:00:00 2001 From: angelalala Date: Sun, 13 Oct 2024 16:24:55 -0700 Subject: [PATCH 06/18] feat: get Twitter token --- src/controllers/socialMediaController.js | 54 +++++++++++++++++++----- src/routes/socialMediaRouter.js | 2 + 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 27a04d68d..6bd7b929a 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -5,15 +5,6 @@ const cheerio = require('cheerio'); // eslint-disable-next-line import/no-extraneous-dependencies const { TwitterApi } = require('twitter-api-v2'); -const TwitterClient = new TwitterApi({ - appKey: process.env.REACT_APP_TWITTER_APP_KEY, - appSecret: process.env.REACT_APP_TWITTER_APP_SECRET, - accessToken: process.env.REACT_APP_TWITTER_ACCESS_TOKEN, - accessSecret: process.env.REACT_APP_TWITTER_ACCESS_SECRET, -}); - -// const TwitterClient = new TwitterApi(process.env.REACT_APP_TWITTER_TOKEN); - function extractTextAndImgUrl(htmlString) { const $ = cheerio.load(htmlString); @@ -56,6 +47,39 @@ async function getPinterestAccessToken(req, res) { } } +async function getTwitterAccessToken(req, res) { + const twitterOAuth = new TwitterApi({ + clientId: process.env.REACT_APP_TWITTER_CLIENT_ID, + clientSecret: process.env.REACT_APP_TWITTER_CLIENT_SECRET, + }); + + const { code, state, codeVerifier } = req.body; + + if (!code || !state || !codeVerifier) { + return res.status(400).json({ error: 'Missing required parameters' }); + } + + try { + twitterOAuth + .loginWithOAuth2({ code, codeVerifier, redirectUri: 'http://localhost:3000/announcements' }) + .then(async ({ client: loggedClient, accessToken, expiresIn, scope }) => { + try { + const { data } = await loggedClient.v2.me(); + console.log('User data:', data); + console.log('scope:', scope); + res.json({ + access_token: accessToken, + expires_in: expiresIn, + }); + } catch (error) { + console.error('API Error:', error); + } + }); + } catch (error) { + console.error('Error exchanging code for token:', error); + res.status(500).json({ error: 'Failed to obtain access token' }); + } +} // TODO: IF scr is link? async function createPin(req, res) { const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; @@ -121,8 +145,16 @@ async function createPin(req, res) { } async function createTweet(req, res) { + const TwitterClient = new TwitterApi({ + appKey: process.env.REACT_APP_TWITTER_APP_KEY, + appSecret: process.env.REACT_APP_TWITTER_APP_SECRET, + accessToken: process.env.REACT_APP_TWITTER_ACCESS_TOKEN, + accessSecret: process.env.REACT_APP_TWITTER_ACCESS_SECRET, + }); + const rwClient = TwitterClient.readWrite; const { textContent, imgSrcs } = extractTextAndImgUrl(req.body.EmailContent); + console.log('Text content:', textContent); try { let mediaIds = []; @@ -138,10 +170,11 @@ async function createTweet(req, res) { ); console.log('Media uploaded, IDs:', mediaIds); } + console.log('Hello:'); const tweet = await rwClient.v2.tweet({ text: textContent, - media: { media_ids: mediaIds }, + // media: { media_ids: mediaIds }, }); res.status(200).json({ success: true, tweet }); @@ -153,6 +186,7 @@ async function createTweet(req, res) { module.exports = { getPinterestAccessToken, + getTwitterAccessToken, createPin, createTweet, }; diff --git a/src/routes/socialMediaRouter.js b/src/routes/socialMediaRouter.js index 1a65874f4..c0d3c956b 100644 --- a/src/routes/socialMediaRouter.js +++ b/src/routes/socialMediaRouter.js @@ -1,6 +1,7 @@ const express = require('express'); const { getPinterestAccessToken, + getTwitterAccessToken, createPin, createTweet, } = require('../controllers/socialMediaController'); @@ -9,6 +10,7 @@ const routes = function () { const socialMediaRouter = express.Router(); socialMediaRouter.route('/getPinterestAccessToken').post(getPinterestAccessToken); + socialMediaRouter.route('/getTwitterAccessToken').post(getTwitterAccessToken); socialMediaRouter.route('/createPin').post(createPin); socialMediaRouter.route('/createTweet').post(createTweet); From b200e72633b593a055292b29edb6143145d27a66 Mon Sep 17 00:00:00 2001 From: angelalala Date: Sun, 13 Oct 2024 20:40:32 -0700 Subject: [PATCH 07/18] feat: support url image upload to Twitter --- src/controllers/socialMediaController.js | 74 +++++++++++++++++++----- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 6bd7b929a..c2048473f 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -9,12 +9,30 @@ function extractTextAndImgUrl(htmlString) { const $ = cheerio.load(htmlString); const textContent = $('body').text().replace(/\+/g, '').trim(); + const urlSrcs = []; + const base64Srcs = []; + + $('img').each((i, img) => { + const src = $(img).attr('src'); + if (src) { + if (src.startsWith('data:image')) { + base64Srcs.push(src); + } else { + urlSrcs.push(src); + } + } + }); - const imgSrcs = $('img') - .map((i, img) => $(img).attr('src')) - .get(); + return { textContent, urlSrcs, base64Srcs }; +} - return { textContent, imgSrcs }; +async function downloadImage(url) { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + return { + buffer: Buffer.from(arrayBuffer), + mimeType: response.headers.get('content-type'), + }; } async function getPinterestAccessToken(req, res) { @@ -153,28 +171,52 @@ async function createTweet(req, res) { }); const rwClient = TwitterClient.readWrite; - const { textContent, imgSrcs } = extractTextAndImgUrl(req.body.EmailContent); + const { textContent, urlSrcs, base64Srcs } = extractTextAndImgUrl(req.body.EmailContent); console.log('Text content:', textContent); try { let mediaIds = []; - if (imgSrcs && imgSrcs.length > 0) { - console.log('Uploading media...'); - mediaIds = await Promise.all( - imgSrcs.map(async (imgSrc) => { - const base64Data = imgSrc.replace(/^data:image\/\w+;base64,/, ''); - const buffer = Buffer.from(base64Data, 'base64'); - const mimeType = 'image/png'; - return rwClient.v1.uploadMedia(buffer, { mimeType }); + // src type is url + if (urlSrcs && urlSrcs.length > 0) { + console.log('Uploading URL media...'); + const urlMediaIds = await Promise.all( + urlSrcs.map(async (imageUrl) => { + try { + const { buffer, mimeType } = await downloadImage(imageUrl); + return await rwClient.v1.uploadMedia(buffer, { mimeType }); + } catch (error) { + console.error(`Error uploading URL image: ${imageUrl}`, error); + return null; + } }), ); - console.log('Media uploaded, IDs:', mediaIds); + mediaIds = mediaIds.concat(urlMediaIds.filter((id) => id !== null)); + console.log('URL media uploaded, IDs:', mediaIds); } - console.log('Hello:'); + // src type is base64 + if (base64Srcs && base64Srcs.length > 0) { + console.log('Uploading base64 media...'); + const base64MediaIds = await Promise.all( + base64Srcs.map(async (imgSrc) => { + try { + const base64Data = imgSrc.replace(/^data:image\/\w+;base64,/, ''); + const buffer = Buffer.from(base64Data, 'base64'); + const mimeType = imgSrc.split(';')[0].split(':')[1] || 'image/png'; + return await rwClient.v1.uploadMedia(buffer, { mimeType }); + } catch (error) { + console.error('Error uploading base64 image', error); + return null; + } + }), + ); + mediaIds = mediaIds.concat(base64MediaIds.filter((id) => id !== null)); + console.log('Base64 media uploaded, IDs:', mediaIds); + } + // TODO: mediaIds.length === 0 const tweet = await rwClient.v2.tweet({ text: textContent, - // media: { media_ids: mediaIds }, + media: { media_ids: mediaIds }, }); res.status(200).json({ success: true, tweet }); From e47b5283aa9c95a62b1848ca1d7716fbdd9c4ea0 Mon Sep 17 00:00:00 2001 From: angelalala Date: Sun, 13 Oct 2024 20:47:13 -0700 Subject: [PATCH 08/18] feat: support only text content when tweet --- src/controllers/socialMediaController.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index c2048473f..a15785758 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -172,7 +172,6 @@ async function createTweet(req, res) { const rwClient = TwitterClient.readWrite; const { textContent, urlSrcs, base64Srcs } = extractTextAndImgUrl(req.body.EmailContent); - console.log('Text content:', textContent); try { let mediaIds = []; @@ -213,11 +212,14 @@ async function createTweet(req, res) { mediaIds = mediaIds.concat(base64MediaIds.filter((id) => id !== null)); console.log('Base64 media uploaded, IDs:', mediaIds); } - // TODO: mediaIds.length === 0 - const tweet = await rwClient.v2.tweet({ - text: textContent, - media: { media_ids: mediaIds }, - }); + + const tweetOptions = { text: textContent }; + + if (mediaIds && mediaIds.length > 0) { + tweetOptions.media = { media_ids: mediaIds }; + } + + const tweet = await rwClient.v2.tweet(tweetOptions); res.status(200).json({ success: true, tweet }); } catch (error) { From 0c29642b505f29f31c478d593f4d0e36c3ef2ae0 Mon Sep 17 00:00:00 2001 From: angelalala Date: Sun, 13 Oct 2024 21:38:18 -0700 Subject: [PATCH 09/18] feat: support image url for Pinterest --- src/controllers/socialMediaController.js | 56 +++++++++++++++++------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index a15785758..d634fbb51 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -102,12 +102,19 @@ async function getTwitterAccessToken(req, res) { async function createPin(req, res) { const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; const authToken = req.body.Authorization; - const { textContent, imgSrcs } = extractTextAndImgUrl(req.body.EmailContent); + const { textContent, urlSrcs, base64Srcs } = extractTextAndImgUrl(req.body.EmailContent); - if (imgSrcs.length === 0) { + if (urlSrcs.length === 0 && base64Srcs.length === 0) { return res.status(400).json({ message: 'No image found in the email content' }); } + if (urlSrcs.length > 0 && base64Srcs.length > 0) { + return res.status(400).json({ + message: + 'Both URL and base64 images found in the email content. Please choose only one type.', + }); + } + try { const baseRequestBody = { title: 'Weekly Update', @@ -116,20 +123,37 @@ async function createPin(req, res) { board_id: '1074812336009724062', }; - const mediaSource = - imgSrcs.length === 1 - ? { - source_type: 'image_base64', - content_type: 'image/jpeg', - data: imgSrcs[0].replace(/^data:image\/\w+;base64,/, ''), - } - : { - source_type: 'multiple_image_base64', - items: imgSrcs.map((imgSrc) => ({ - content_type: 'image/jpeg', - data: imgSrc.replace(/^data:image\/\w+;base64,/, ''), - })), - }; + let mediaSource = {}; + + if (base64Srcs.length !== 0) { + mediaSource = + base64Srcs.length === 1 + ? { + source_type: 'image_base64', + content_type: base64Srcs[0].split(';')[0].split(':')[1] || 'image/png', + data: base64Srcs[0].replace(/^data:image\/\w+;base64,/, ''), + } + : { + source_type: 'multiple_image_base64', + items: base64Srcs.map((imgSrc) => ({ + content_type: imgSrc.split(';')[0].split(':')[1] || 'image/png', + data: imgSrc.replace(/^data:image\/\w+;base64,/, ''), + })), + }; + } + + if (urlSrcs.length !== 0) { + mediaSource = + urlSrcs.length === 1 + ? { + source_type: 'image_url', + url: urlSrcs[0], + } + : { + source_type: 'multiple_image_urls', + items: urlSrcs.map((url) => ({ url })), + }; + } const requestBody = JSON.stringify({ ...baseRequestBody, From efd62d751a65e72cf882b0f5ed7cf4ebc9b9e063 Mon Sep 17 00:00:00 2001 From: angelalala Date: Sun, 13 Oct 2024 21:38:48 -0700 Subject: [PATCH 10/18] remove TODO --- src/controllers/socialMediaController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index d634fbb51..4e96eb1bd 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -98,7 +98,7 @@ async function getTwitterAccessToken(req, res) { res.status(500).json({ error: 'Failed to obtain access token' }); } } -// TODO: IF scr is link? + async function createPin(req, res) { const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; const authToken = req.body.Authorization; From 231b7bab50393192fed2f9fa20448ce355b14d78 Mon Sep 17 00:00:00 2001 From: angelalala Date: Fri, 8 Nov 2024 15:08:31 -0800 Subject: [PATCH 11/18] feat: env variable --- src/controllers/socialMediaController.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 4e96eb1bd..a283cf9fe 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -37,8 +37,8 @@ async function downloadImage(url) { async function getPinterestAccessToken(req, res) { const authCode = req.body.code; - const clientId = '1503261'; - const clientSecret = '2644a99853f263bd5688935762a32135293b950b'; + const clientId = process.env.REACT_APP_PINTEREST_CLIENT_ID; + const clientSecret = process.env.REACT_APP_PINTEREST_CLIENT_SECRET; const accessTokenUrl = 'https://api-sandbox.pinterest.com/v5/oauth/token'; const authToken = btoa(`${clientId}:${clientSecret}`); @@ -119,8 +119,8 @@ async function createPin(req, res) { const baseRequestBody = { title: 'Weekly Update', description: textContent, - dominant_color: '#6E7874', - board_id: '1074812336009724062', + dominant_color: '', // Hex color code + board_id: '', // Pinterest board ID }; let mediaSource = {}; From e220af8a97b69af02076d58aea86ccf921c9cd04 Mon Sep 17 00:00:00 2001 From: angelalala Date: Sun, 17 Nov 2024 22:36:21 -0800 Subject: [PATCH 12/18] decouple poster --- src/controllers/socialMediaController.js | 119 ----------------------- src/routes/socialMediaRouter.js | 9 +- 2 files changed, 1 insertion(+), 127 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index a283cf9fe..4e31ee68d 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -35,36 +35,6 @@ async function downloadImage(url) { }; } -async function getPinterestAccessToken(req, res) { - const authCode = req.body.code; - const clientId = process.env.REACT_APP_PINTEREST_CLIENT_ID; - const clientSecret = process.env.REACT_APP_PINTEREST_CLIENT_SECRET; - const accessTokenUrl = 'https://api-sandbox.pinterest.com/v5/oauth/token'; - - const authToken = btoa(`${clientId}:${clientSecret}`); - - const requestBody = new URLSearchParams(); - requestBody.append('grant_type', 'authorization_code'); - requestBody.append('code', authCode); - requestBody.append('redirect_uri', 'http://localhost:3000'); - - try { - console.log('try to fetch'); - const response = await fetch(accessTokenUrl, { - method: 'POST', - headers: { - Authorization: `Basic ${authToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: requestBody.toString(), - }); - const data = await response.json(); - res.json(data); - } catch (error) { - console.error('backend error: ', error); - } -} - async function getTwitterAccessToken(req, res) { const twitterOAuth = new TwitterApi({ clientId: process.env.REACT_APP_TWITTER_CLIENT_ID, @@ -99,93 +69,6 @@ async function getTwitterAccessToken(req, res) { } } -async function createPin(req, res) { - const requestUrl = 'https://api-sandbox.pinterest.com/v5/pins'; - const authToken = req.body.Authorization; - const { textContent, urlSrcs, base64Srcs } = extractTextAndImgUrl(req.body.EmailContent); - - if (urlSrcs.length === 0 && base64Srcs.length === 0) { - return res.status(400).json({ message: 'No image found in the email content' }); - } - - if (urlSrcs.length > 0 && base64Srcs.length > 0) { - return res.status(400).json({ - message: - 'Both URL and base64 images found in the email content. Please choose only one type.', - }); - } - - try { - const baseRequestBody = { - title: 'Weekly Update', - description: textContent, - dominant_color: '', // Hex color code - board_id: '', // Pinterest board ID - }; - - let mediaSource = {}; - - if (base64Srcs.length !== 0) { - mediaSource = - base64Srcs.length === 1 - ? { - source_type: 'image_base64', - content_type: base64Srcs[0].split(';')[0].split(':')[1] || 'image/png', - data: base64Srcs[0].replace(/^data:image\/\w+;base64,/, ''), - } - : { - source_type: 'multiple_image_base64', - items: base64Srcs.map((imgSrc) => ({ - content_type: imgSrc.split(';')[0].split(':')[1] || 'image/png', - data: imgSrc.replace(/^data:image\/\w+;base64,/, ''), - })), - }; - } - - if (urlSrcs.length !== 0) { - mediaSource = - urlSrcs.length === 1 - ? { - source_type: 'image_url', - url: urlSrcs[0], - } - : { - source_type: 'multiple_image_urls', - items: urlSrcs.map((url) => ({ url })), - }; - } - - const requestBody = JSON.stringify({ - ...baseRequestBody, - media_source: mediaSource, - }); - - const response = await fetch(requestUrl, { - method: 'POST', - headers: { - Authorization: authToken, - 'Content-Type': 'application/json', - }, - body: requestBody, - }); - - const statusCode = response.status; - const responseData = await response.json(); - - if (statusCode >= 200 && statusCode < 300) { - res.status(200).json(responseData); - } else { - console.error('[Backend] Error creating Pin: ', responseData.message); - res.status(statusCode).json({ - message: responseData.message || 'Unexpected error', - }); - } - } catch (error) { - console.error('[Backend] Network or other error: ', error); - res.status(500).json({ success: false, error: 'Internal server error' }); - } -} - async function createTweet(req, res) { const TwitterClient = new TwitterApi({ appKey: process.env.REACT_APP_TWITTER_APP_KEY, @@ -253,8 +136,6 @@ async function createTweet(req, res) { } module.exports = { - getPinterestAccessToken, getTwitterAccessToken, - createPin, createTweet, }; diff --git a/src/routes/socialMediaRouter.js b/src/routes/socialMediaRouter.js index c0d3c956b..6ef9a24cf 100644 --- a/src/routes/socialMediaRouter.js +++ b/src/routes/socialMediaRouter.js @@ -1,17 +1,10 @@ const express = require('express'); -const { - getPinterestAccessToken, - getTwitterAccessToken, - createPin, - createTweet, -} = require('../controllers/socialMediaController'); +const { getTwitterAccessToken, createTweet } = require('../controllers/socialMediaController'); const routes = function () { const socialMediaRouter = express.Router(); - socialMediaRouter.route('/getPinterestAccessToken').post(getPinterestAccessToken); socialMediaRouter.route('/getTwitterAccessToken').post(getTwitterAccessToken); - socialMediaRouter.route('/createPin').post(createPin); socialMediaRouter.route('/createTweet').post(createTweet); return socialMediaRouter; From b1a28b8c3ffdcdb31c9baf9f25cdf8ebdb4a298a Mon Sep 17 00:00:00 2001 From: Shefali Mittal Date: Fri, 17 Jan 2025 23:59:07 -0600 Subject: [PATCH 13/18] Adding twitter scheduler post api and db schema for schedulePost --- src/controllers/socialMediaController.js | 35 +++++++++++++++++++++++- src/models/scheduledPostSchema.js | 31 +++++++++++++++++++++ src/routes/socialMediaRouter.js | 7 ++++- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/models/scheduledPostSchema.js diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 4e31ee68d..8b551857f 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -4,6 +4,7 @@ const fetch = require('node-fetch'); const cheerio = require('cheerio'); // eslint-disable-next-line import/no-extraneous-dependencies const { TwitterApi } = require('twitter-api-v2'); +const ScheduledPost = require('../models/scheduledPostSchema'); function extractTextAndImgUrl(htmlString) { const $ = cheerio.load(htmlString); @@ -36,6 +37,7 @@ async function downloadImage(url) { } async function getTwitterAccessToken(req, res) { + console.log('gTAT'); const twitterOAuth = new TwitterApi({ clientId: process.env.REACT_APP_TWITTER_CLIENT_ID, clientSecret: process.env.REACT_APP_TWITTER_CLIENT_SECRET, @@ -49,7 +51,7 @@ async function getTwitterAccessToken(req, res) { try { twitterOAuth - .loginWithOAuth2({ code, codeVerifier, redirectUri: 'http://localhost:3000/announcements' }) + .loginWithOAuth2({ code, codeVerifier, redirectUri: 'http://localhost:4500/announcements' }) .then(async ({ client: loggedClient, accessToken, expiresIn, scope }) => { try { const { data } = await loggedClient.v2.me(); @@ -69,7 +71,37 @@ async function getTwitterAccessToken(req, res) { } } +async function scheduleTweet(req, res) { + console.log('scheduleTweet call'); + const { textContent, urlSrcs, base64Srcs } = extractTextAndImgUrl(req.body.EmailContent); + const scheduledTime = req.body.ScheduleDate; + + if (!scheduledTime) { + return res.status(400).json({ error: 'Missing required parameter: scheduledTime' }); + } + const platform = 'twitter'; + const newScheduledTweet = new ScheduledPost({ + textContent, + urlSrcs, + base64Srcs, + scheduledTime, + platform, + }); + + newScheduledTweet + .save() + .then((scheduledTweet) => { + console.log('scheduledTweet saved:', scheduledTweet); + res.status(200).json({ success: true, scheduledTweet }); + }) + .catch((error) => { + console.error('[Backend] Database error: ', error); + res.status(500).json({ success: false, error: 'Internal server error' }); + }); +} + async function createTweet(req, res) { + console.log('createTweet call'); const TwitterClient = new TwitterApi({ appKey: process.env.REACT_APP_TWITTER_APP_KEY, appSecret: process.env.REACT_APP_TWITTER_APP_SECRET, @@ -138,4 +170,5 @@ async function createTweet(req, res) { module.exports = { getTwitterAccessToken, createTweet, + scheduleTweet, }; diff --git a/src/models/scheduledPostSchema.js b/src/models/scheduledPostSchema.js new file mode 100644 index 000000000..79ae3c6ca --- /dev/null +++ b/src/models/scheduledPostSchema.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose'); + +const scheduledPostSchema = new mongoose.Schema({ + textContent: { + type: String, + required: true, + }, + urlSrcs: { + type: [String], + required: false, + }, + base64Srcs: { + type: [String], + required: false, + }, + scheduledTime: { + type: Date, + required: true, + }, + platform: { + type: String, + enum: ['twitter'], + required: true, + }, + createdAt: { + type: Date, + default: Date.now, + }, +}); + +module.exports = mongoose.model('ScheduledPost', scheduledPostSchema); diff --git a/src/routes/socialMediaRouter.js b/src/routes/socialMediaRouter.js index 6ef9a24cf..936302018 100644 --- a/src/routes/socialMediaRouter.js +++ b/src/routes/socialMediaRouter.js @@ -1,11 +1,16 @@ const express = require('express'); -const { getTwitterAccessToken, createTweet } = require('../controllers/socialMediaController'); +const { + getTwitterAccessToken, + createTweet, + scheduleTweet, +} = require('../controllers/socialMediaController'); const routes = function () { const socialMediaRouter = express.Router(); socialMediaRouter.route('/getTwitterAccessToken').post(getTwitterAccessToken); socialMediaRouter.route('/createTweet').post(createTweet); + socialMediaRouter.route('/scheduleTweet').post(scheduleTweet); return socialMediaRouter; }; From ef4f8c9bda7e04ddf2235a74965b454ecba20dd8 Mon Sep 17 00:00:00 2001 From: Shefali Mittal Date: Sat, 18 Jan 2025 01:33:01 -0600 Subject: [PATCH 14/18] changes in post schema and scheduling --- src/controllers/socialMediaController.js | 49 ++++++++++++++++++++++++ src/cronjobs/socialPostScheduler.js | 44 +++++++++++++++++++++ src/models/scheduledPostSchema.js | 2 +- src/server.js | 1 + 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/cronjobs/socialPostScheduler.js diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 8b551857f..0a8949597 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -36,6 +36,54 @@ async function downloadImage(url) { }; } +async function postToTwitter(content, image) { + console.log('create Scheduled Tweet call'); + const TwitterClient = new TwitterApi({ + appKey: process.env.REACT_APP_TWITTER_APP_KEY, + appSecret: process.env.REACT_APP_TWITTER_APP_SECRET, + accessToken: process.env.REACT_APP_TWITTER_ACCESS_TOKEN, + accessSecret: process.env.REACT_APP_TWITTER_ACCESS_SECRET, + }); + + const rwClient = TwitterClient.readWrite; + const textContent = content; + const base64Srcs = image; + + try { + let mediaIds = []; + // src type is base64 + if (base64Srcs && base64Srcs.length > 0) { + console.log('Uploading base64 media...'); + const base64MediaIds = await Promise.all( + base64Srcs.map(async (imgSrc) => { + try { + const base64Data = imgSrc.replace(/^data:image\/\w+;base64,/, ''); + const buffer = Buffer.from(base64Data, 'base64'); + const mimeType = imgSrc.split(';')[0].split(':')[1] || 'image/png'; + return await rwClient.v1.uploadMedia(buffer, { mimeType }); + } catch (error) { + console.error('Error uploading base64 image', error); + return null; + } + }), + ); + mediaIds = mediaIds.concat(base64MediaIds.filter((id) => id !== null)); + console.log('Base64 media uploaded, IDs:', mediaIds); + } + + const tweetOptions = { text: textContent }; + + if (mediaIds && mediaIds.length > 0) { + tweetOptions.media = { media_ids: mediaIds }; + } + + const tweet = await rwClient.v2.tweet(tweetOptions); + console.log('Tweet posted successfully:', tweet); + } catch (error) { + console.error('[Backend] Network or other error: ', error); + } +} + async function getTwitterAccessToken(req, res) { console.log('gTAT'); const twitterOAuth = new TwitterApi({ @@ -171,4 +219,5 @@ module.exports = { getTwitterAccessToken, createTweet, scheduleTweet, + postToTwitter, }; diff --git a/src/cronjobs/socialPostScheduler.js b/src/cronjobs/socialPostScheduler.js new file mode 100644 index 000000000..651e93aef --- /dev/null +++ b/src/cronjobs/socialPostScheduler.js @@ -0,0 +1,44 @@ +const { CronJob } = require('cron'); +const moment = require('moment-timezone'); +// const mongoose = require('mongoose'); +const ScheduledPost = require('../models/scheduledPostSchema'); +const socialMediaController = require('../controllers/socialMediaController'); + +const socialPostScheduler = () => { + const checkScheduledPosts = new CronJob( + '0 0 * * *', // Run once daily at midnight + // '*/1 * * * *',// Run every minute for testing + async () => { + console.log('cron jb started'); + const now = moment().tz('America/Los_Angeles').startOf('day'); + const posts = await ScheduledPost.find({ + scheduledTime: { + $gte: now.toDate(), + $lt: now.add(1, 'day').toDate(), + }, + }); + + posts.forEach(async (post) => { + console.log(`Running scheduled task for ${post.platform} at`, new Date()); + + switch (post.platform) { + case 'twitter': + await socialMediaController.postToTwitter(post.textContent, post.base64Srcs); + break; + default: + console.error('Unknown platform:', post.platform); + } + + // Remove the post after execution while testing + // await ScheduledPost.findByIdAndDelete(post._id); + }); + }, + null, + false, + 'America/Los_Angeles', + ); + + checkScheduledPosts.start(); +}; + +module.exports = socialPostScheduler; diff --git a/src/models/scheduledPostSchema.js b/src/models/scheduledPostSchema.js index 79ae3c6ca..a739fa636 100644 --- a/src/models/scheduledPostSchema.js +++ b/src/models/scheduledPostSchema.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const scheduledPostSchema = new mongoose.Schema({ textContent: { type: String, - required: true, + required: false, }, urlSrcs: { type: [String], diff --git a/src/server.js b/src/server.js index e53949703..7debe92e6 100644 --- a/src/server.js +++ b/src/server.js @@ -6,6 +6,7 @@ const websockets = require('./websockets').default; require('./startup/db')(); require('./cronjobs/userProfileJobs')(); +require('./cronjobs/socialPostScheduler')(); const port = process.env.PORT || 4500; From 56ce6b3c2504c4843092d82dc3126de44e013b3b Mon Sep 17 00:00:00 2001 From: Shefali Mittal Date: Sat, 25 Jan 2025 03:06:16 -0600 Subject: [PATCH 15/18] Added changes for posting tweets at particular time --- src/controllers/socialMediaController.js | 16 +++- src/cronjobs/socialPostScheduler.js | 93 ++++++++++++++++++------ src/models/scheduledPostSchema.js | 6 +- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 0a8949597..1dc52be18 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -121,17 +121,25 @@ async function getTwitterAccessToken(req, res) { async function scheduleTweet(req, res) { console.log('scheduleTweet call'); + console.log('Request body:', req.body); const { textContent, urlSrcs, base64Srcs } = extractTextAndImgUrl(req.body.EmailContent); - const scheduledTime = req.body.ScheduleDate; - - if (!scheduledTime) { - return res.status(400).json({ error: 'Missing required parameter: scheduledTime' }); + const scheduledDate = req.body.ScheduleDate; + const scheduledTime = req.body.ScheduleTime; + console.log('scheduledDate', scheduledDate); + console.log('scheduledTime', scheduledTime); + + if (!scheduledDate || !scheduledTime) { + return res + .status(400) + .json({ error: 'Missing required parameters: scheduledDate or scheduledTime' }); } + const platform = 'twitter'; const newScheduledTweet = new ScheduledPost({ textContent, urlSrcs, base64Srcs, + scheduledDate, scheduledTime, platform, }); diff --git a/src/cronjobs/socialPostScheduler.js b/src/cronjobs/socialPostScheduler.js index 651e93aef..c0a5e4a8f 100644 --- a/src/cronjobs/socialPostScheduler.js +++ b/src/cronjobs/socialPostScheduler.js @@ -1,44 +1,91 @@ const { CronJob } = require('cron'); const moment = require('moment-timezone'); -// const mongoose = require('mongoose'); const ScheduledPost = require('../models/scheduledPostSchema'); const socialMediaController = require('../controllers/socialMediaController'); const socialPostScheduler = () => { const checkScheduledPosts = new CronJob( - '0 0 * * *', // Run once daily at midnight - // '*/1 * * * *',// Run every minute for testing + '*/1 * * * *', // Run every minute for testing async () => { - console.log('cron jb started'); - const now = moment().tz('America/Los_Angeles').startOf('day'); - const posts = await ScheduledPost.find({ - scheduledTime: { - $gte: now.toDate(), - $lt: now.add(1, 'day').toDate(), - }, - }); + console.log('cron job started'); + const now = moment().tz('America/Chicago').format('YYYY-MM-DD HH:mm'); // right now i kept timezone as america/chicago,will change later. + console.log('Current date and time:', now); + + const posts = await ScheduledPost.find(); posts.forEach(async (post) => { - console.log(`Running scheduled task for ${post.platform} at`, new Date()); - - switch (post.platform) { - case 'twitter': - await socialMediaController.postToTwitter(post.textContent, post.base64Srcs); - break; - default: - console.error('Unknown platform:', post.platform); - } + const scheduledDateTime = moment + .tz(`${post.scheduledDate} ${post.scheduledTime}`, 'YYYY-MM-DD HH:mm', 'America/Chicago') + .format('YYYY-MM-DD HH:mm'); - // Remove the post after execution while testing - // await ScheduledPost.findByIdAndDelete(post._id); + if (scheduledDateTime === now) { + console.log(`Running scheduled task for ${post.platform} at`, new Date()); + + switch (post.platform) { + case 'twitter': + await socialMediaController.postToTwitter(post.textContent, post.base64Srcs); + break; + default: + console.error('Unknown platform:', post.platform); + } + + // Remove the post after execution + await ScheduledPost.findByIdAndDelete(post._id); + } }); }, null, false, - 'America/Los_Angeles', + 'America/Chicago', ); checkScheduledPosts.start(); }; module.exports = socialPostScheduler; + +// const { CronJob } = require('cron'); +// const moment = require('moment-timezone'); +// // const mongoose = require('mongoose'); +// const ScheduledPost = require('../models/scheduledPostSchema'); +// const socialMediaController = require('../controllers/socialMediaController'); +// const socialPostScheduler = () => { +// const checkScheduledPosts = new CronJob( +// // '0 0 * * *', // Run once daily at midnight +// // '0 0 * * *', // Run once daily at midnight +// '*/1 * * * *', // Run every minute for testing +// async () => { +// console.log('cron job started'); +// const now = moment().tz('America/Chicago'); +// console.log('Current date and time:', now.format()); +// const posts = await ScheduledPost.find({ +// scheduledTime: { +// $gte: now.startOf('minute').toDate(), +// $lt: now.add(1, 'minute').toDate(), +// }, +// }); + +// posts.forEach(async (post) => { +// console.log(`Running scheduled task for ${post.platform} at`, new Date()); + +// switch (post.platform) { +// case 'twitter': +// await socialMediaController.postToTwitter(post.textContent, post.base64Srcs, post.scheduledTime); +// break; +// default: +// console.error('Unknown platform:', post.platform); +// } + +// // Remove the post after execution while testing +// await ScheduledPost.findByIdAndDelete(post._id); +// }); +// }, +// null, +// false, +// 'America/Los_Angeles', +// ); + +// checkScheduledPosts.start(); +// }; + +// module.exports = socialPostScheduler; diff --git a/src/models/scheduledPostSchema.js b/src/models/scheduledPostSchema.js index a739fa636..ead81f90b 100644 --- a/src/models/scheduledPostSchema.js +++ b/src/models/scheduledPostSchema.js @@ -13,8 +13,12 @@ const scheduledPostSchema = new mongoose.Schema({ type: [String], required: false, }, + scheduledDate: { + type: String, // mm/dd/yyyy + required: true, + }, scheduledTime: { - type: Date, + type: String, // hr:min AM/PM required: true, }, platform: { From bce89ced12030c531af804c312e94ae27e44a1df Mon Sep 17 00:00:00 2001 From: Shefali Mittal Date: Thu, 6 Mar 2025 15:52:56 -0600 Subject: [PATCH 16/18] feat: Add Read Update Delete functionalities --- src/app.js | 5 ++ src/controllers/socialMediaController.js | 82 ++++++++++++++++++++++++ src/cronjobs/socialPostScheduler.js | 3 +- src/models/scheduledPostSchema.js | 24 +++++++ src/routes/socialMediaRouter.js | 10 +++ 5 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index 817017108..94313eeca 100644 --- a/src/app.js +++ b/src/app.js @@ -4,6 +4,7 @@ const Sentry = require('@sentry/node'); const app = express(); const logger = require('./startup/logger'); const globalErrorHandler = require('./utilities/errorHandling/globalErrorHandler'); +// const socialMediaRoutes = require('./routes/socialMediaRoutes'); logger.init(); @@ -15,6 +16,10 @@ require('./startup/bodyParser')(app); require('./startup/middleware')(app); require('./startup/routes')(app); +app.set('view engine', 'ejs'); + +// app.use('/social-media', socialMediaRoutes); + // The error handler must be before any other error middleware and after all controllers app.use(Sentry.Handlers.errorHandler()); diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 1dc52be18..56e0c45d8 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -86,6 +86,9 @@ async function postToTwitter(content, image) { async function getTwitterAccessToken(req, res) { console.log('gTAT'); + console.log('Twitter Client ID:', process.env.REACT_APP_TWITTER_CLIENT_ID); + console.log('Twitter Client Secret:', process.env.REACT_APP_TWITTER_CLIENT_SECRET); + const twitterOAuth = new TwitterApi({ clientId: process.env.REACT_APP_TWITTER_CLIENT_ID, clientSecret: process.env.REACT_APP_TWITTER_CLIENT_SECRET, @@ -142,6 +145,7 @@ async function scheduleTweet(req, res) { scheduledDate, scheduledTime, platform, + status: 'scheduled', }); newScheduledTweet @@ -216,6 +220,18 @@ async function createTweet(req, res) { const tweet = await rwClient.v2.tweet(tweetOptions); + const newTweet = new ScheduledPost({ + textContent, + urlSrcs, + base64Srcs, + scheduledDate: new Date().toLocaleDateString(), + scheduledTime: new Date().toLocaleTimeString(), + platform: 'twitter', + status: 'posted', + }); + + await newTweet.save(); + res.status(200).json({ success: true, tweet }); } catch (error) { console.error('[Backend] Network or other error: ', error); @@ -223,9 +239,75 @@ async function createTweet(req, res) { } } +async function getPosts(req, res) { + console.log('getPosts call'); + try { + const posts = await ScheduledPost.find({}).select( + 'textContent urlSrcs scheduledDate scheduledTime platform createdAt base64Srcs', + ); + res.status(200).json({ success: true, posts }); + } catch (error) { + console.error('[Backend] Database error: ', error); + res.status(500).json({ success: false, error: 'Internal server error' }); + } +} + +async function deletePosts(req, res) { + console.log('Request URL:', req.originalUrl); + console.log('Request Method:', req.method); + console.log('deletePosts call'); + const { _id } = req.body; + + if (!_id) { + return res.status(400).json({ error: 'Missing required parameter: postId' }); + } + + try { + const deletedPost = await ScheduledPost.findOneAndDelete({ _id }); + + if (!deletedPost) { + return res.status(404).json({ error: 'Post not found or already deleted' }); + } + + res.status(200).json({ success: true, message: 'Post deleted successfully' }); + } catch (error) { + console.error('[Backend] Database error: ', error); + res.status(500).json({ success: false, error: 'Internal server error' }); + } +} + +async function updatePosts(req, res) { + console.log('updatePosts call'); + const { _id, textContent, urlSrcs, scheduledDate, scheduledTime, platform } = req.body; + + if (!_id) { + return res.status(400).json({ error: 'Missing required parameter: _id' }); + } + + try { + const updatedPost = await ScheduledPost.findOneAndUpdate( + { _id }, + { textContent, urlSrcs, scheduledDate, scheduledTime, platform }, + { new: true }, + ); + + if (!updatedPost) { + return res.status(404).json({ error: 'Post not found' }); + } + + res.status(200).json({ success: true, updatedPost }); + } catch (error) { + console.error('[Backend] Database error: ', error); + res.status(500).json({ success: false, error: 'Internal server error' }); + } +} + module.exports = { getTwitterAccessToken, createTweet, scheduleTweet, postToTwitter, + getPosts, + deletePosts, + updatePosts, }; diff --git a/src/cronjobs/socialPostScheduler.js b/src/cronjobs/socialPostScheduler.js index c0a5e4a8f..ba9c851ee 100644 --- a/src/cronjobs/socialPostScheduler.js +++ b/src/cronjobs/socialPostScheduler.js @@ -5,7 +5,8 @@ const socialMediaController = require('../controllers/socialMediaController'); const socialPostScheduler = () => { const checkScheduledPosts = new CronJob( - '*/1 * * * *', // Run every minute for testing + // '*/1 * * * *', // Run every minute for testing + '0 0 * * *', // Run once daily at midnight async () => { console.log('cron job started'); const now = moment().tz('America/Chicago').format('YYYY-MM-DD HH:mm'); // right now i kept timezone as america/chicago,will change later. diff --git a/src/models/scheduledPostSchema.js b/src/models/scheduledPostSchema.js index ead81f90b..07ca6cc22 100644 --- a/src/models/scheduledPostSchema.js +++ b/src/models/scheduledPostSchema.js @@ -1,6 +1,12 @@ const mongoose = require('mongoose'); +const { v4: uuidv4 } = require('uuid'); const scheduledPostSchema = new mongoose.Schema({ + postId: { + type: String, + default: uuidv4, // Generate a UUID for each post + unique: true, + }, textContent: { type: String, required: false, @@ -26,6 +32,24 @@ const scheduledPostSchema = new mongoose.Schema({ enum: ['twitter'], required: true, }, + status: { + type: String, + enum: ['scheduled', 'posted'], + default: 'scheduled', + }, + createdBy: { + type: String, + required: false, + }, + updatedBy: { + type: String, + required: false, + }, + updatedAt: { + type: Date, + default: Date.now, + required: false, + }, createdAt: { type: Date, default: Date.now, diff --git a/src/routes/socialMediaRouter.js b/src/routes/socialMediaRouter.js index 936302018..f320680e4 100644 --- a/src/routes/socialMediaRouter.js +++ b/src/routes/socialMediaRouter.js @@ -3,6 +3,9 @@ const { getTwitterAccessToken, createTweet, scheduleTweet, + getPosts, + deletePosts, + updatePosts, } = require('../controllers/socialMediaController'); const routes = function () { @@ -11,6 +14,13 @@ const routes = function () { socialMediaRouter.route('/getTwitterAccessToken').post(getTwitterAccessToken); socialMediaRouter.route('/createTweet').post(createTweet); socialMediaRouter.route('/scheduleTweet').post(scheduleTweet); + socialMediaRouter.route('/getPosts').get(getPosts); + socialMediaRouter.route('/deletePosts').delete(deletePosts); + + socialMediaRouter.route('/posts').post(scheduleTweet); + socialMediaRouter.route('/posts').delete(deletePosts); + socialMediaRouter.route('/posts').get(getPosts); + socialMediaRouter.route('/posts').put(updatePosts); return socialMediaRouter; }; From 375d8faa46ea1520edfae619f9a3167f01d1464f Mon Sep 17 00:00:00 2001 From: Shefali Mittal Date: Thu, 20 Mar 2025 18:54:00 -0500 Subject: [PATCH 17/18] feat: add new social media post scheduler --- src/controllers/socialMediaController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/socialMediaController.js b/src/controllers/socialMediaController.js index 56e0c45d8..cc7c3519d 100644 --- a/src/controllers/socialMediaController.js +++ b/src/controllers/socialMediaController.js @@ -245,6 +245,7 @@ async function getPosts(req, res) { const posts = await ScheduledPost.find({}).select( 'textContent urlSrcs scheduledDate scheduledTime platform createdAt base64Srcs', ); + console.log(posts); res.status(200).json({ success: true, posts }); } catch (error) { console.error('[Backend] Database error: ', error); From d0b03504765d7fe12049681099486e811fbe51c9 Mon Sep 17 00:00:00 2001 From: Shefali Mittal Date: Sat, 29 Mar 2025 19:12:15 -0500 Subject: [PATCH 18/18] feat: latest changes --- src/cronjobs/socialPostScheduler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cronjobs/socialPostScheduler.js b/src/cronjobs/socialPostScheduler.js index ba9c851ee..f613a14ad 100644 --- a/src/cronjobs/socialPostScheduler.js +++ b/src/cronjobs/socialPostScheduler.js @@ -5,8 +5,8 @@ const socialMediaController = require('../controllers/socialMediaController'); const socialPostScheduler = () => { const checkScheduledPosts = new CronJob( - // '*/1 * * * *', // Run every minute for testing - '0 0 * * *', // Run once daily at midnight + '*/1 * * * *', // Run every minute for testing + // '0 0 * * *', // Run once daily at midnight async () => { console.log('cron job started'); const now = moment().tz('America/Chicago').format('YYYY-MM-DD HH:mm'); // right now i kept timezone as america/chicago,will change later.