diff --git a/package-lock.json b/package-lock.json index 8eff8a588..4763d5184 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", @@ -11511,6 +11560,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", @@ -12330,6 +12387,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", @@ -14135,6 +14209,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..4e31ee68d --- /dev/null +++ b/src/controllers/socialMediaController.js @@ -0,0 +1,141 @@ +// 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'); + +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); + } + } + }); + + return { textContent, urlSrcs, base64Srcs }; +} + +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 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' }); + } +} + +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, urlSrcs, base64Srcs } = extractTextAndImgUrl(req.body.EmailContent); + + try { + let mediaIds = []; + // 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; + } + }), + ); + mediaIds = mediaIds.concat(urlMediaIds.filter((id) => id !== null)); + console.log('URL media uploaded, IDs:', 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); + + res.status(200).json({ success: true, tweet }); + } catch (error) { + console.error('[Backend] Network or other error: ', error); + res.status(500).json({ success: false, error: 'Internal server error' }); + } +} + +module.exports = { + getTwitterAccessToken, + createTweet, +}; diff --git a/src/routes/socialMediaRouter.js b/src/routes/socialMediaRouter.js new file mode 100644 index 000000000..6ef9a24cf --- /dev/null +++ b/src/routes/socialMediaRouter.js @@ -0,0 +1,13 @@ +const express = require('express'); +const { getTwitterAccessToken, createTweet } = require('../controllers/socialMediaController'); + +const routes = function () { + const socialMediaRouter = express.Router(); + + socialMediaRouter.route('/getTwitterAccessToken').post(getTwitterAccessToken); + 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); };