From b8c34d238dfec50eeb4bd335016fefc828d09f43 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 16 Apr 2021 06:37:28 -0300 Subject: [PATCH 01/14] Updating bulk return --- src/app.js | 618 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 351 insertions(+), 267 deletions(-) diff --git a/src/app.js b/src/app.js index 7ec37cc..2006c80 100644 --- a/src/app.js +++ b/src/app.js @@ -17,318 +17,402 @@ const app = express(); app.use(bodyParser.json()); const config = { - consumer_key: process.env.TWITTER_CONSUMER_KEY, - consumer_secret: process.env.TWITTER_CONSUMER_SECRET, + consumer_key: process.env.TWITTER_CONSUMER_KEY, + consumer_secret: process.env.TWITTER_CONSUMER_SECRET, }; const cacheDuration = process.env.CACHE_DURATION || 2592000; const requestTwitterList = async (client, searchFor, profile, limit, callback) => { - let cursor = -1; - const list = []; - let total = 0; - const param = { - screen_name: profile, - }; - - const responseUser = await client.get('users/show', param); - total = responseUser[`${searchFor}_count`]; - - async.whilst( - () => cursor === -1, - async (next) => { - const params = { + let cursor = -1; + const list = []; + let total = 0; + const param = { screen_name: profile, - count: limit, - cursor, - }; - - try { - const responseList = await client.get(`${searchFor}/list`, params); - responseList.users.forEach((current) => { - list.push(current); - }); - cursor = responseList.next_cursor; - next(); - } catch (error) { - console.error(error); - next(error); - } - }, - (err) => { - const object = { - metadata: { - count: list.length, - total, + }; + + const responseUser = await client.get('users/show', param); + total = responseUser[`${searchFor}_count`]; + + async.whilst( + () => cursor === -1, + async (next) => { + const params = { + screen_name: profile, + count: limit, + cursor, + }; + + try { + const responseList = await client.get(`${searchFor}/list`, params); + responseList.users.forEach((current) => { + list.push(current); + }); + cursor = responseList.next_cursor; + next(); + } catch (error) { + console.error(error); + next(error); + } }, - profiles: [], - }; - - list.forEach((value) => { - object.profiles.push({ - username: value.screen_name, - url: `https://twitter.com/${value.screen_name}`, - avatar: value.profile_image_url, - user_profile_language: value.lang, - }); - }); - - if (err) { - object.metadata.error = err; - } - - callback(object); - }, - ); + (err) => { + const object = { + metadata: { + count: list.length, + total, + }, + profiles: [], + }; + + list.forEach((value) => { + object.profiles.push({ + username: value.screen_name, + url: `https://twitter.com/${value.screen_name}`, + avatar: value.profile_image_url, + user_profile_language: value.lang, + }); + }); + + if (err) { + object.metadata.error = err; + } + + callback(object); + }, + ); }; // If you get an error 415, add your callback url to your Twitter App. async function getTokenUrl(req, searchFor, profile, limit) { - try { - const ssl = req.connection.encrypted ? 'https://' : 'http://'; - const oauthCallback = `${ssl + req.headers.host}/resultados?socialnetwork=twitter&authenticated=true&profile=${profile}&search_for=${searchFor}&limit=${limit}#conteudo`; + try { + const ssl = req.connection.encrypted ? 'https://' : 'http://'; + const oauthCallback = `${ssl + req.headers.host}/resultados?socialnetwork=twitter&authenticated=true&profile=${profile}&search_for=${searchFor}&limit=${limit}#conteudo`; - const client = new TwitterLite({ - consumer_key: config.consumer_key, - consumer_secret: config.consumer_secret, - }); + const client = new TwitterLite({ + consumer_key: config.consumer_key, + consumer_secret: config.consumer_secret, + }); - const reqData = await client.getRequestToken(oauthCallback); - const uri = `${'https://api.twitter.com/oauth/authenticate?'}${qs.stringify({ oauth_token: reqData.oauth_token })}`; - return uri; - } catch (error) { - return error; - } + const reqData = await client.getRequestToken(oauthCallback); + const uri = `${'https://api.twitter.com/oauth/authenticate?'}${qs.stringify({ oauth_token: reqData.oauth_token })}`; + return uri; + } catch (error) { + return error; + } } -app.all('/', function(req, res, next) { - res.header("Access-Control-Allow-Origin", '"$http_origin" always'); - res.header("Access-Control-Allow-Headers", "X-Requested-With, Origin, Accept"); - next(); - }); +app.all('/', function (req, res, next) { + res.header("Access-Control-Allow-Origin", '"$http_origin" always'); + res.header("Access-Control-Allow-Headers", "X-Requested-With, Origin, Accept"); + next(); +}); app.get('/botometer', async (req, res) => { - const target = req.query.search_for; - const { profile } = req.query; - let { limit } = req.query; - const { authenticated } = req.query; - const cacheInterval = req.query.cache_duration; - const key = `${target}:${profile}`; - const cachedKey = mcache.get(key); - const verbose = req.query.verbose || req.query.logging; - const isAdmin = req.query.is_admin; - const origin = isAdmin ? 'admin' : 'website'; - const wantsDocument = req.query.documento; - const lang = req.headers["accept-language"]; - - const referer = req.get('referer'); - const sentimentLang = library.getDefaultLanguage(referer); - - const { getData } = req.query; - - console.log('profile', profile); - if (!limit || limit > 200) { - limit = 200; - } - if (typeof target === 'undefined' || typeof profile === 'undefined') { - res.status(400).send('One parameter is missing'); - } else if (cachedKey) { - res.send(cachedKey); - } else if (target === 'profile') { - try { - const result = await spottingbot(profile, config, { friend: false }, - sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, lang).catch((err) => err); - - if (result && result.profiles && result.profiles[0]) console.log(result.profiles[0]); - - if (!result || result.errors || result.error) { - let toSend = result; - if (result.errors) toSend = result.errors; - - // The error format varies according to the error - // Not all of them will have an error code - // Use first error to determine message - const firstError = result.errors ? result.errors[0] : result; - console.log(firstError); - - let errorMessage; - if ( firstError.code === 34 ) { - errorMessage = 'Esse usuário não existe' - } - else if ( firstError.error === 'Not authorized.' ) { - errorMessage = 'Sem permissão para acessar. Usuário pode estar bloqueado/suspendido.' - } - else { - errorMessage = 'Erro ao procurar pelo perfil' - } - - res.status(400).json({ metadata: { error: toSend }, message: errorMessage }); - return; - } - - if (wantsDocument === '1' && result.profiles[0].bot_probability.extraDetails) { - res.send(result.profiles[0].bot_probability.extraDetails); - } else if (verbose === '1' && result.profiles[0].bot_probability.info) { - const loggingText = result.profiles[0].bot_probability.info; - const fileName = `${profile}_analise.txt`; - res.set({ 'Content-Disposition': `attachment; filename="${fileName}"`, 'Content-type': 'application/octet-stream' }); - res.send(loggingText); - } else { - res.json(result); - } - } catch (error) { - console.log('error', error); - res.status(500).json({ metadata: { error } }); + const target = req.query.search_for; + const { profile } = req.query; + let { limit } = req.query; + const { authenticated } = req.query; + const cacheInterval = req.query.cache_duration; + const key = `${target}:${profile}`; + const cachedKey = mcache.get(key); + const verbose = req.query.verbose || req.query.logging; + const isAdmin = req.query.is_admin; + const origin = isAdmin ? 'admin' : 'website'; + const wantsDocument = req.query.documento; + const lang = req.headers["accept-language"]; + + const referer = req.get('referer'); + const sentimentLang = library.getDefaultLanguage(referer); + + const { getData } = req.query; + + console.log('profile', profile); + if (!limit || limit > 200) { + limit = 200; } - } else if (target === 'followers' || target === 'friends') { - if (authenticated === 'true') { - // const token = req.query.oauth_token; - // const tokenSecret = mcache.get(token); - // const verifier = req.query.oauth_verifier; - - const client = new TwitterLite({ - consumer_key: config.consumer_key, - consumer_secret: config.consumer_secret, - access_token_key: undefined, - access_token_secret: undefined, - }); - - requestTwitterList(client, target, profile, limit, (object) => { - if (typeof object.metadata.error === 'undefined') { - mcache.put(key, JSON.stringify(object), cacheDuration * 1000); + if (typeof target === 'undefined' || typeof profile === 'undefined') { + res.status(400).send('One parameter is missing'); + } else if (cachedKey) { + res.send(cachedKey); + } else if (target === 'profile') { + try { + const result = await spottingbot(profile, config, { friend: false }, + sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, lang).catch((err) => err); + + if (result && result.profiles && result.profiles[0]) console.log(result.profiles[0]); + + if (!result || result.errors || result.error) { + let toSend = result; + if (result.errors) toSend = result.errors; + + // The error format varies according to the error + // Not all of them will have an error code + // Use first error to determine message + const firstError = result.errors ? result.errors[0] : result; + console.log(firstError); + + let errorMessage; + if (firstError.code === 34) { + errorMessage = 'Esse usuário não existe' + } + else if (firstError.error === 'Not authorized.') { + errorMessage = 'Sem permissão para acessar. Usuário pode estar bloqueado/suspendido.' + } + else { + errorMessage = 'Erro ao procurar pelo perfil' + } + + res.status(400).json({ metadata: { error: toSend }, message: errorMessage }); + return; + } + + if (wantsDocument === '1' && result.profiles[0].bot_probability.extraDetails) { + res.send(result.profiles[0].bot_probability.extraDetails); + } else if (verbose === '1' && result.profiles[0].bot_probability.info) { + const loggingText = result.profiles[0].bot_probability.info; + const fileName = `${profile}_analise.txt`; + res.set({ 'Content-Disposition': `attachment; filename="${fileName}"`, 'Content-type': 'application/octet-stream' }); + res.send(loggingText); + } else { + res.json(result); + } + } catch (error) { + console.log('error', error); + res.status(500).json({ metadata: { error } }); + } + } else if (target === 'followers' || target === 'friends') { + if (authenticated === 'true') { + // const token = req.query.oauth_token; + // const tokenSecret = mcache.get(token); + // const verifier = req.query.oauth_verifier; + + const client = new TwitterLite({ + consumer_key: config.consumer_key, + consumer_secret: config.consumer_secret, + access_token_key: undefined, + access_token_secret: undefined, + }); + + requestTwitterList(client, target, profile, limit, (object) => { + if (typeof object.metadata.error === 'undefined') { + mcache.put(key, JSON.stringify(object), cacheDuration * 1000); + } + res.json(object); + }); + } else { + const result = await getTokenUrl(req, target, profile, limit); + if (result.errors) { + res.status(500).send(result); + } else { + res.json({ request_url: result }); + } } - res.json(object); - }); } else { - const result = await getTokenUrl(req, target, profile, limit); - if (result.errors) { - res.status(500).send(result); - } else { - res.json({ request_url: result }); - } + res.status(400).send('search_for is wrong'); } - } else { - res.status(400).send('search_for is wrong'); - } }); // request app.post('/feedback', async (req, res) => { - const { opinion } = req.body; - const analysisID = req.body.analysis_id; - - const result = await library.saveFeedback(analysisID, opinion); - if (result && result.id) { - res.status(200).send(result); - } else { - res.status(500).send(result); - } + const { opinion } = req.body; + const analysisID = req.body.analysis_id; + + const result = await library.saveFeedback(analysisID, opinion); + if (result && result.id) { + res.status(200).send(result); + } else { + res.status(500).send(result); + } }); app.get('/feedback', (req, res) => { - if (fs.existsSync('opinion.json') === false) { - res.send('No feedback yet'); - return; - } - const content = fs.readFileSync('opinion.json'); - const data = JSON.parse(content); - res.json(data); + if (fs.existsSync('opinion.json') === false) { + res.send('No feedback yet'); + return; + } + const content = fs.readFileSync('opinion.json'); + const data = JSON.parse(content); + res.json(data); }); app.get('/user-timeline-rate-limit', async (req, res) => { - const rateLimits = await library.getRateLimits(null, true); - let status = 200; - if (!rateLimits || rateLimits.error) status = 500; - res.status(status).json(rateLimits); + const rateLimits = await library.getRateLimits(null, true); + let status = 200; + if (!rateLimits || rateLimits.error) status = 500; + res.status(status).json(rateLimits); }); app.get('/status', (req, res) => { - res.sendStatus(200); + res.sendStatus(200); }); app.get('/analyze', async (req, res) => { - const target = req.query.search_for; - - const { profile } = req.query; - let { limit } = req.query; - const { authenticated } = req.query; - - const cacheInterval = req.query.cache_duration; - const key = `${target}:${profile}`; - const cachedKey = mcache.get(key); - const fullAnalysisCache = 0; - - const verbose = req.query.verbose || req.query.logging; - const isAdmin = req.query.is_admin; - const origin = isAdmin ? 'admin' : 'website'; - const wantsDocument = 1; - - const referer = req.get('referer'); - const sentimentLang = library.getDefaultLanguage(referer); - - const lang = req.headers["accept-language"]; - - const { getData } = req.query; - - console.log('profile', profile); - if (!limit || limit > 200) { - limit = 200; - } - if (typeof target === 'undefined' || typeof profile === 'undefined') { - res.status(400).send('One parameter is missing'); - } else if (cachedKey) { - res.send(cachedKey); - } else if (target === 'profile') { - try { - const result = await spottingbot(profile, config, { friend: false }, - sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, fullAnalysisCache).catch((err) => err); + const target = req.query.search_for; - if (!result || result.errors || result.error) { - let toSend = result; - if (result.errors) toSend = result.errors; + const { profile } = req.query; + let { limit } = req.query; + const { authenticated } = req.query; - res.status(404).json({ metadata: { error: toSend } }); - return; - } - - res.send(await library.buildAnalyzeReturn(result.profiles[0].bot_probability.extraDetails, lang)); + const cacheInterval = req.query.cache_duration; + const key = `${target}:${profile}`; + const cachedKey = mcache.get(key); + const fullAnalysisCache = 0; - } catch (error) { - console.log('error', error); - res.status(500).json({ metadata: { error } }); + const verbose = req.query.verbose || req.query.logging; + const isAdmin = req.query.is_admin; + const origin = isAdmin ? 'admin' : 'website'; + const wantsDocument = 1; + + const referer = req.get('referer'); + const sentimentLang = library.getDefaultLanguage(referer); + + const lang = req.headers["accept-language"]; + + const { getData } = req.query; + + console.log('profile', profile); + if (!limit || limit > 200) { + limit = 200; } - } else if (target === 'followers' || target === 'friends') { - if (authenticated === 'true') { - // const token = req.query.oauth_token; - // const tokenSecret = mcache.get(token); - // const verifier = req.query.oauth_verifier; - - const client = new TwitterLite({ - consumer_key: config.consumer_key, - consumer_secret: config.consumer_secret, - access_token_key: undefined, - access_token_secret: undefined, - }); - - requestTwitterList(client, target, profile, limit, (object) => { - if (typeof object.metadata.error === 'undefined') { - mcache.put(key, JSON.stringify(object), cacheDuration * 1000); + if (typeof target === 'undefined' || typeof profile === 'undefined') { + res.status(400).send('One parameter is missing'); + } else if (cachedKey) { + res.send(cachedKey); + } else if (target === 'profile') { + try { + const result = await spottingbot(profile, config, { friend: false }, + sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, fullAnalysisCache).catch((err) => err); + + if (!result || result.errors || result.error) { + let toSend = result; + if (result.errors) toSend = result.errors; + + res.status(404).json({ metadata: { error: toSend } }); + return; + } + + res.send(await library.buildAnalyzeReturn(result.profiles[0].bot_probability.extraDetails, lang)); + + } catch (error) { + console.log('error', error); + res.status(500).json({ metadata: { error } }); + } + } else if (target === 'followers' || target === 'friends') { + if (authenticated === 'true') { + // const token = req.query.oauth_token; + // const tokenSecret = mcache.get(token); + // const verifier = req.query.oauth_verifier; + + const client = new TwitterLite({ + consumer_key: config.consumer_key, + consumer_secret: config.consumer_secret, + access_token_key: undefined, + access_token_secret: undefined, + }); + + requestTwitterList(client, target, profile, limit, (object) => { + if (typeof object.metadata.error === 'undefined') { + mcache.put(key, JSON.stringify(object), cacheDuration * 1000); + } + res.json(object); + }); + } else { + const result = await getTokenUrl(req, target, profile, limit); + if (result.errors) { + res.status(500).send(result); + } else { + res.json({ request_url: result }); + } } - res.json(object); - }); } else { - const result = await getTokenUrl(req, target, profile, limit); - if (result.errors) { - res.status(500).send(result); - } else { - res.json({ request_url: result }); - } + res.status(400).send('search_for is wrong'); } - } else { - res.status(400).send('search_for is wrong'); - } -}); +}); + +app.get('/botometer-bulk', async (req, res) => { + const apiKey = req.headers["x-api-key"]; + if (!apiKey || apiKey != process.env.BULK_API_KEY) return res.status(403).send('forbidden'); + + + const { profiles, is_admin, twitter_api_consumer_key, twitter_api_consumer_secret } = req.body; + if (profiles.length > 50) return res.status(400).json({ message: 'max profiles size is 50' }); + + if (twitter_api_consumer_key && twitter_api_consumer_secret) { + config = { + consumer_key: twitter_api_consumer_key, + consumer_secret: twitter_api_consumer_secret, + }; + } + + const getData = true; + const referer = req.get('referer'); + const origin = is_admin ? 'admin' : 'website'; + + const profiles_results = profiles.map(profile => { + + return spottingbot( + profile, config, { friend: false }, library.getDefaultLanguage(referer), getData, origin + ).then((result) => { + return { + twitter_user_data: { + id: result.twitter_data.user_id, + handle: '@' + result.profiles[0].username, + user_name: result.twitter_data.user_name, + url: result.profiles[0].url, + avatar: result.profiles[0].avatar, + created_at: result.twitter_data.created_at, + }, + twitter_user_meta_data: { + tweet_count: result.twitter_data.number_tweets, + follower_count: result.twitter_data.followers, + following_count: result.twitter_data.following, + hashtags: result.twitter_data.hashtags, + mentions: result.twitter_data.mentions, + }, + pegabot_analysis: { + user_index: result.profiles[0].language_independent.user, + temporal_index: result.profiles[0].language_independent.temporal, + network_index: result.profiles[0].language_independent.network, + sentiment_index: result.profiles[0].language_dependent.sentiment.value, + bot_probability: result.profiles[0].bot_probability.all, + }, + metadata: { + used_cache: result.twitter_data.usedCache + } + } + }).catch((err) => { + const firstError = err.errors ? err.errors[0] : err; + + let errorMessage; + if (firstError.code === 34) { + errorMessage = 'Esse usuário não existe' + } + else if (firstError.error === 'Not authorized.') { + errorMessage = 'Sem permissão para acessar. Usuário pode estar bloqueado/suspendido.' + } + else { + errorMessage = 'Erro ao procurar pelo perfil' + } + + return { + twitter_user_data: { + user_handle: profile, + }, + metadata: { + error: errorMessage + } + } + }); + + }); + + Promise.all(profiles_results).then(function (results) { + res.status(200).json({ analyses_count: results.length, analyses: results }); + return; + }); + +}); export default app; From e8faf43501908010dec08fbe22302e00436515d3 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 16 Apr 2021 14:59:21 -0300 Subject: [PATCH 02/14] Current dev state --- src/analyze.js | 325 +++++++++++++------------- src/infra/database/models/analyses.js | 120 ++++++---- src/library.js | 120 +++++----- 3 files changed, 290 insertions(+), 275 deletions(-) diff --git a/src/analyze.js b/src/analyze.js index 565bfea..a2b3bed 100644 --- a/src/analyze.js +++ b/src/analyze.js @@ -25,7 +25,7 @@ module.exports = (screenName, config, index = { }, sentimentLang, getData, cacheInterval, verbose, origin, wantDocument, fullAnalysisCache, cb) => new Promise(async (resolve, reject) => { // eslint-disable-line no-async-promise-executor let useCache = process.env.USE_CACHE; - if ( typeof fullAnalysisCache != 'undefined' && fullAnalysisCache === 0 ) useCache = 0; + if (typeof fullAnalysisCache != 'undefined' && fullAnalysisCache === 0) useCache = 0; if (!screenName || !config) { const error = 'You need to provide an username to analyze and a config for twitter app'; @@ -46,6 +46,12 @@ module.exports = (screenName, config, index = { // check if we have a saved analysis of that user withing the desired time interval if (useCache === '1') { + // let analysisForHandle = await library.getTwitterIDForHandle(screenName); + // let twitter_user_id = analysisForHandle[0].dataValues.twitter_user_id; + + // let cachedAnalysis = await library.getCachedAnalysis(twitter_user_id); + // console.log(cachedAnalysis); + let cachedResult = await library.getCachedRequest(screenName, cacheInterval); if (cachedResult) { @@ -214,171 +220,176 @@ module.exports = (screenName, config, index = { } }, ], - // This function is the final one and occurs when all indexes get calculated - async (err, results) => { - if (err) { - if (cb) cb(err, null); - reject(err); - return err; - } + // This function is the final one and occurs when all indexes get calculated + async (err, results) => { + if (err) { + if (cb) cb(err, null); + reject(err); + return err; + } - explanations.push('\nCálculo do resultado final\n'); - - // Save all results in the correct variable - let userScore = results[0][0]; - let friendsScore = (results[1] + (results[2] * 1.5)) / (2 * 1.5); - let temporalScore = results[3][0]; - let networkScore = results[3][1]; - let sentimentScore = results[3][2]; - - // If any scores is not calculated, null is set for avoid error during the final calculation - const isNumber = (value) => !Number.isNaN(Number(value)); - if (!isNumber(userScore)) userScore = null; - if (!isNumber(friendsScore)) friendsScore = null; - if (!isNumber(temporalScore)) temporalScore = null; - if (!isNumber(networkScore)) networkScore = null; - if (!isNumber(sentimentScore)) sentimentScore = null; - - explanations.push(`Score User: ${userScore}`); - explanations.push(`Score Friend (Ignorado): ${friendsScore}`); - explanations.push(`Score Temporal: ${temporalScore}`); - explanations.push(`Score Netword: ${networkScore}`); - explanations.push(`Score Sentiment: ${sentimentScore}`); - - const scoreSum = userScore + friendsScore + temporalScore + networkScore + sentimentScore; - - // Adjustment for not getting any score more than 0.99 in the final result - const total = Math.min(scoreSum / indexCount, 0.99); - - explanations.push(`Somamos todos os ${indexCount} scores que temos: ${userScore} + ${friendsScore} + ${temporalScore} + ${networkScore} + ${sentimentScore} = ${scoreSum}`); - explanations.push('Dividimos a soma pela quantidade de scores e limitamos o resultado a 0.99'); - explanations.push(`Fica: ${scoreSum} / ${indexCount} = ${total}`); - explanations.push(`Total: ${total}`); - - if (networkScore > 1) { - networkScore /= 2; - } else if (networkScore > 2) { - networkScore = 1; - } + explanations.push('\nCálculo do resultado final\n'); + + // Save all results in the correct variable + let userScore = results[0][0]; + let friendsScore = (results[1] + (results[2] * 1.5)) / (2 * 1.5); + let temporalScore = results[3][0]; + let networkScore = results[3][1]; + let sentimentScore = results[3][2]; + + // If any scores is not calculated, null is set for avoid error during the final calculation + const isNumber = (value) => !Number.isNaN(Number(value)); + if (!isNumber(userScore)) userScore = null; + if (!isNumber(friendsScore)) friendsScore = null; + if (!isNumber(temporalScore)) temporalScore = null; + if (!isNumber(networkScore)) networkScore = null; + if (!isNumber(sentimentScore)) sentimentScore = null; + + explanations.push(`Score User: ${userScore}`); + explanations.push(`Score Friend (Ignorado): ${friendsScore}`); + explanations.push(`Score Temporal: ${temporalScore}`); + explanations.push(`Score Netword: ${networkScore}`); + explanations.push(`Score Sentiment: ${sentimentScore}`); + + const scoreSum = userScore + friendsScore + temporalScore + networkScore + sentimentScore; + + // Adjustment for not getting any score more than 0.99 in the final result + const total = Math.min(scoreSum / indexCount, 0.99); + + explanations.push(`Somamos todos os ${indexCount} scores que temos: ${userScore} + ${friendsScore} + ${temporalScore} + ${networkScore} + ${sentimentScore} = ${scoreSum}`); + explanations.push('Dividimos a soma pela quantidade de scores e limitamos o resultado a 0.99'); + explanations.push(`Fica: ${scoreSum} / ${indexCount} = ${total}`); + explanations.push(`Total: ${total}`); + + if (networkScore > 1) { + networkScore /= 2; + } else if (networkScore > 2) { + networkScore = 1; + } - if (temporalScore > 1) { - temporalScore /= 2; - } else if (temporalScore > 2) { - temporalScore = 1; - } + if (temporalScore > 1) { + temporalScore /= 2; + } else if (temporalScore > 2) { + temporalScore = 1; + } - // Sorting weights - const sortedWeights = {}; - var keys = Object.keys(weights); + // Sorting weights + const sortedWeights = {}; + var keys = Object.keys(weights); - keys.sort(function(a, b) { + keys.sort(function (a, b) { return weights[b] - weights[a] //inverted comparison - }).forEach(function(k) { - sortedWeights[k] = weights[k]; - }); + }).forEach(function (k) { + sortedWeights[k] = weights[k]; + }); - // Using the first key of weights to build the info - const weightKey = Object.keys(sortedWeights)[0]; - let info; + // Using the first key of weights to build the info + const weightKey = Object.keys(sortedWeights)[0]; + let info; - if (weightKey === 'USER_INDEX_WEIGHT') { - info = '

Um dos critérios que mais teve peso na análise foi o índice de Perfil

'; - } - else if ( weightKey === 'NETWORK_INDEX_WEIGHT:' ) { - info = '

Um dos critérios que mais teve peso na análise foi o índice de Rede

'; - } - else if ( weightKey === 'TEMPORAL_INDEX_WEIGHT' ) { - info = '

Um dos critérios que mais teve peso na análise foi o índice Temporal

'; - } - else { - info = '

Um dos critérios que mais teve peso na análise foi o índice de Sentimento

'; - } + if (weightKey === 'USER_INDEX_WEIGHT') { + info = '

Um dos critérios que mais teve peso na análise foi o índice de Perfil

'; + } + else if (weightKey === 'NETWORK_INDEX_WEIGHT:') { + info = '

Um dos critérios que mais teve peso na análise foi o índice de Rede

'; + } + else if (weightKey === 'TEMPORAL_INDEX_WEIGHT') { + info = '

Um dos critérios que mais teve peso na análise foi o índice Temporal

'; + } + else { + info = '

Um dos critérios que mais teve peso na análise foi o índice de Sentimento

'; + } - // Create the response object - const object = { - metadata: { - count: 1, - }, - profiles: [{ - username: param.screen_name, - url: `https://twitter.com/${param.screen_name}`, - avatar: user.profile_image_url, - language_dependent: { - sentiment: { - value: sentimentScore, - }, - }, - language_independent: { - friend: friendsScore, - temporal: temporalScore, - network: networkScore, - user: userScore, + // Create the response object + const object = { + metadata: { + count: 1, }, - bot_probability: { - all: total, - info: info, - }, - user_profile_language: user.lang, - }], - }; - - const details = await document.getExtraDetails(extraDetails); - if (verbose) object.profiles[0].bot_probability.info = library.getLoggingtext(explanations); - if (wantDocument) object.profiles[0].bot_probability.extraDetails = details; - - // add data from twitter to complement return (if getDate is true) and save to database - const data = {}; - - data.created_at = user.created_at; - data.user_id = user.id_str; - data.user_name = user.name; - data.following = user.friends_count; - data.followers = user.followers_count; - data.number_tweets = user.statuses_count; - - data.hashtags = hashtagsUsed; - data.mentions = mentionsUsed; - - if (getData) { - object.twitter_data = data; - object.rate_limit = timeline.rateLimit; - } - - // save Analysis Data on database - const { id: newAnalysisID } = await Analysis.create({ - fullResponse: object, - total, - user: userScore, - friend: friendsScore, - sentiment: sentimentScore, - temporal: temporalScore, - network: networkScore, - explanations, - details, - }).then((res) => res.dataValues); - - // save User Data on database - const { id: newUserDataID } = await UserData.create({ - username: data.user_name, - twitterID: data.user_id, - profileCreatedAt: data.created_at, - followingCount: data.following, - followersCount: data.followers, - statusesCount: data.number_tweets, - hashtagsUsed: data.hashtags, - mentionsUsed: data.mentions, - }).then((res) => res.dataValues); - - // update request - newRequest.analysisID = newAnalysisID; - newRequest.userDataID = newUserDataID; - newRequest.save(); - - if (newAnalysisID) object.analysis_id = newAnalysisID; + profiles: [{ + username: param.screen_name, + url: `https://twitter.com/${param.screen_name}`, + avatar: user.profile_image_url, + language_dependent: { + sentiment: { + value: sentimentScore, + }, + }, + language_independent: { + friend: friendsScore, + temporal: temporalScore, + network: networkScore, + user: userScore, + }, + bot_probability: { + all: total, + info: info, + }, + user_profile_language: user.lang, + }], + }; + + const details = await document.getExtraDetails(extraDetails); + if (verbose) object.profiles[0].bot_probability.info = library.getLoggingtext(explanations); + if (wantDocument) object.profiles[0].bot_probability.extraDetails = details; + + // add data from twitter to complement return (if getDate is true) and save to database + const data = {}; + + data.created_at = user.created_at; + data.user_id = user.id_str; + data.user_name = user.name; + data.following = user.friends_count; + data.followers = user.followers_count; + data.number_tweets = user.statuses_count; + + data.hashtags = hashtagsUsed; + data.mentions = mentionsUsed; + + if (getData) { + object.twitter_data = data; + object.rate_limit = timeline.rateLimit; + } - if (cb) cb(null, object); - resolve(object); - return object; - }); + // save Analysis Data on database + const { id: newAnalysisID } = await Analysis.create({ + fullResponse: object, + total, + user: userScore, + friend: friendsScore, + sentiment: sentimentScore, + temporal: temporalScore, + network: networkScore, + // twitter_user_id: data.user_id, + // twitter_handle: param.screen_name, + // twitter_created_at: data.created_at, + // twitter_following_count: data.following, + // twitter_followers_count: data.followers, + // twitter_status_count: data.number_tweets, + details, + }).then((res) => res.dataValues); + + // save User Data on database + const { id: newUserDataID } = await UserData.create({ + username: data.user_name, + twitterID: data.user_id, + profileCreatedAt: data.created_at, + followingCount: data.following, + followersCount: data.followers, + statusesCount: data.number_tweets, + hashtagsUsed: data.hashtags, + mentionsUsed: data.mentions, + }).then((res) => res.dataValues); + + // update request + newRequest.analysisID = newAnalysisID; + newRequest.userDataID = newUserDataID; + newRequest.save(); + + if (newAnalysisID) object.analysis_id = newAnalysisID; + + if (cb) cb(null, object); + resolve(object); + return object; + }); return null; }); diff --git a/src/infra/database/models/analyses.js b/src/infra/database/models/analyses.js index ebb4455..92a226f 100644 --- a/src/infra/database/models/analyses.js +++ b/src/infra/database/models/analyses.js @@ -1,54 +1,74 @@ import { Model } from 'sequelize'; export default class Analysis extends Model { - static init(sequelize, DataTypes) { - return super.init({ - id: { - allowNull: false, - autoIncrement: true, - primaryKey: true, - type: DataTypes.INTEGER, - }, - fullResponse: { - allowNull: false, - type: DataTypes.JSON, - }, - total: { - allowNull: true, - type: DataTypes.STRING, - }, - user: { - allowNull: true, - type: DataTypes.STRING, - }, - friend: { - allowNull: true, - type: DataTypes.STRING, - }, - sentiment: { - allowNull: true, - type: DataTypes.STRING, - }, - temporal: { - allowNull: true, - type: DataTypes.STRING, - }, - network: { - allowNull: true, - type: DataTypes.STRING, - }, - explanations: { - allowNull: true, - type: DataTypes.JSON, - }, - details: { - allowNull: true, - type: DataTypes.JSON, - }, - }, { - sequelize, - modelName: 'Analyses', - freezeTableName: true, - }); - } + static init(sequelize, DataTypes) { + return super.init({ + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER, + }, + fullResponse: { + allowNull: false, + type: DataTypes.JSON, + }, + total: { + allowNull: true, + type: DataTypes.STRING, + }, + user: { + allowNull: true, + type: DataTypes.STRING, + }, + friend: { + allowNull: true, + type: DataTypes.STRING, + }, + sentiment: { + allowNull: true, + type: DataTypes.STRING, + }, + temporal: { + allowNull: true, + type: DataTypes.STRING, + }, + network: { + allowNull: true, + type: DataTypes.STRING, + }, + details: { + allowNull: true, + type: DataTypes.JSON, + }, + twitter_user_id: { + allowNull: true, + type: DataTypes.BIGINT, + }, + twitter_handle: { + allowNull: true, + type: DataTypes.STRING, + }, + twitter_created_at: { + allowNull: true, + type: DataTypes.DATE, + }, + twitter_following_count: { + allowNull: true, + type: DataTypes.INTEGER, + }, + twitter_followers_count: { + allowNull: true, + type: DataTypes.INTEGER, + }, + twitter_status_count: { + allowNull: true, + type: DataTypes.INTEGER, + } + }, { + sequelize, + modelName: 'Analyses', + freezeTableName: true, + }); + } } diff --git a/src/library.js b/src/library.js index 319d600..0682825 100644 --- a/src/library.js +++ b/src/library.js @@ -2,7 +2,7 @@ import { execSync } from 'child_process'; import { Op } from 'sequelize'; import TwitterLite from 'twitter-lite'; import { getExtraDetails } from './document'; -import { Request, Feedback } from './infra/database/index'; +import { Request, Feedback, Analysis } from './infra/database/index'; import Texts from './data/texts'; const superagent = require('superagent'); const md5Hex = require('md5-hex'); @@ -229,6 +229,26 @@ export default { return res; }, + getTwitterIDForHandle: async (twitter_handle) => { + return await Analysis.findAll({ + attributes: ['twitter_user_id'], + where: { twitter_handle }, + order: [['createdAt', 'DESC']], + limit: 1, + }); + }, + + getCachedAnalysis: async (twitter_user_id) => { + return await Analysis.findOne({ + where: { + twitter_user_id, + createdAt: { [Op.between]: [getCacheInterval(process.env.DEFAULT_CACHE_INTERVAL), new Date()] }, + }, + order: [['createdAt', 'DESC']], // select the newest entry + raw: true, + }); + }, + getRateLimits, saveFeedback: async (analysisID, opinion) => { try { @@ -276,42 +296,14 @@ export default { return result; }, - -// sub puppetter_signed_url { -// my %config = @_; - -// my $secret = $ENV{PUPPETER_SECRET_TOKEN}; -// my $host = $ENV{PUPPETER_SERVICE_ROOT_URL}; - -// die 'missing PUPPETER_SECRET_TOKEN' if !$secret; -// die 'missing PUPPETER_SERVICE_ROOT_URL' if !$host; -// die 'invalid width' if $config{w} !~ /^\d+$/a; -// die 'invalid height' if exists $config{h} && $config{h} !~ /^\d+$/a; -// die 'invalid resize width' if exists $config{rw} && $config{rw} !~ /^\d+$/a; -// die 'invalid url' if $config{u} !~ /^http/i; - -// my $my_url = Mojo::URL->new($host); - -// my $calcBuffer = $secret . "\n"; -// for my $field (keys %config) { -// $calcBuffer .= $field . '=' . $config{$field} . "\n"; -// $my_url->query->merge($field, $config{$field}); -// } - -// my $calcSecret = md5_hex($calcBuffer); -// $my_url->query->merge('a', $calcSecret); - -// return $my_url . ''; -// } - buildAnalyzeReturn: async (extraDetails, lang) => { // Setting text file let texts; if (/es-mx/.test(lang)) { - texts = Texts.FULL_ANALYSIS_ESMX; + texts = Texts.FULL_ANALYSIS_ESMX; } else { - texts = Texts.FULL_ANALYSIS_PTBR; + texts = Texts.FULL_ANALYSIS_PTBR; } // Getting screenshots @@ -319,28 +311,20 @@ export default { const puppetterSecret = process.env.PUPPETER_SECRET_TOKEN; const calcBuffer = puppetterSecret + "\n" - + 'u=' + '' + extraDetails.TWITTER_LINK + "\n" - + 'w=480' + "\n" - + 'h=520' + "\n"; + + 'u=' + '' + extraDetails.TWITTER_LINK + "\n" + + 'w=480' + "\n" + + 'h=520' + "\n"; const calcSecret = md5Hex(calcBuffer); - const pictureUrl = await (async () => { - try { - const res = await superagent - .get(puppetterUrl) - .query({ - u: '' + extraDetails.TWITTER_LINK, - w: 480, - h: 520, - a: calcSecret, - }); - - return res.request.url; - } catch (err) { - console.error(err); - } - })(); + const opts = { + u: '' + extraDetails.TWITTER_LINK, + w: 480, + h: 520, + a: calcSecret, + }; + const queryString = new URLSearchParams(opts).toString(); + const pictureUrl = `${puppetterUrl}?${queryString}`; // Preparing the JSON that's going to be used on the return for /analyze const ret = { @@ -374,7 +358,7 @@ export default { { title: texts.PROFILE.VERIFIED_ANALYSIS.TITLE, summary_key: 'VERIFIED_ANALYSIS', - score_key: 'VERIFIED_SCORE', + score_key: 'VERIFIED_SCORE', description: texts.PROFILE.VERIFIED_ANALYSIS.DESCRIPTION }, { @@ -479,30 +463,30 @@ export default { ]; // Profile block root->profile - profileData.forEach( async function(section) { + profileData.forEach(async function (section) { // Verifying if index exists, only push to array if it does. if (typeof extraDetails[section.summary_key] != "undefined") { ret.root.profile.analyses.push( { - title: section.title, + title: section.title, description: section.description, - summary: typeof extraDetails[section.summary_key] != "undefined" ? `

${extraDetails[section.summary_key]}

` : undefined, - conclusion: typeof extraDetails[section.score_key] === "number" ? parseFloat(extraDetails[section.score_key]).toFixed(2) : 0.00 + summary: typeof extraDetails[section.summary_key] != "undefined" ? `

${extraDetails[section.summary_key]}

` : undefined, + conclusion: typeof extraDetails[section.score_key] === "number" ? parseFloat(extraDetails[section.score_key]).toFixed(2) : 0.00 } ); - if ( section.title === 'NÚMERO DE TWEETS' ) { + if (section.title === 'NÚMERO DE TWEETS') { // Preparing the tweet array to be used on a line chart, divided by day. // I'm gonna treat this here instead of doing it when the array is filled, because I don't want to touch that legacy code const chartLabels = []; - const chartData = []; + const chartData = []; const sortedList = extraDetails.TWEET_MOMENT.sort(); - sortedList.forEach( async function(tweet) { + sortedList.forEach(async function (tweet) { const tweetStr = tweet.toString(); - const ymd = tweetStr.substring(0, 10); + const ymd = tweetStr.substring(0, 10); if (chartLabels.indexOf(ymd) === -1) { chartLabels.push(ymd); @@ -517,23 +501,23 @@ export default { ret.root.profile.analyses[analysisKey].chart = {}; ret.root.profile.analyses[analysisKey].chart.labels = chartLabels; - ret.root.profile.analyses[analysisKey].chart.data = chartData; + ret.root.profile.analyses[analysisKey].chart.data = chartData; } } }); // Network block root->network - networkData.forEach( async function(section) { + networkData.forEach(async function (section) { // Verifying if index exists, only push to array if it does. if (typeof extraDetails[section.summary_key] != "undefined") { ret.root.network.analyses.push( { - title: section.title, + title: section.title, description: section.description, - summary: typeof extraDetails[section.summary_key] != "undefined" ? `

${extraDetails[section.summary_key]}

` : undefined, - conclusion: parseFloat(extraDetails[section.score_key]).toFixed(2) + summary: typeof extraDetails[section.summary_key] != "undefined" ? `

${extraDetails[section.summary_key]}

` : undefined, + conclusion: parseFloat(extraDetails[section.score_key]).toFixed(2) } ); @@ -544,7 +528,7 @@ export default { } else if (section.title === 'DISTRIBUIÇÃO DAS MENÇÕES') { const list = extraDetails.MENTIONS.slice(0, 100); - list.forEach( function(v) { delete v.id; delete v.id_str; delete v.indices } ); + list.forEach(function (v) { delete v.id; delete v.id_str; delete v.indices }); ret.root.network.analyses[analysisKey].mentions = []; ret.root.network.analyses[analysisKey].mentions = list; @@ -589,7 +573,7 @@ export default { const tweetSamples = []; - if (typeof(tweetNeutral) != 'undefined') { + if (typeof (tweetNeutral) != 'undefined') { const calcBuffer = puppetterSecret + "\n" + 'u=' + '' + tweetNeutral.url + "\n" + 'w=480' + "\n" @@ -620,7 +604,7 @@ export default { }); } - if (typeof(tweetPositive) != 'undefined') { + if (typeof (tweetPositive) != 'undefined') { const calcBuffer = puppetterSecret + "\n" + 'u=' + '' + tweetPositive.url + "\n" + 'w=480' + "\n" @@ -651,7 +635,7 @@ export default { }); } - if (typeof(tweetNegative) != 'undefined') { + if (typeof (tweetNegative) != 'undefined') { const calcBuffer = puppetterSecret + "\n" + 'u=' + '' + tweetNegative.url + "\n" + 'w=480' + "\n" From 3bf253645e84538077048d3379390d17295cd933 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 30 Apr 2021 14:51:23 -0300 Subject: [PATCH 03/14] current dev state --- package-lock.json | 245 +++++-- package.json | 4 +- src/analyze.js | 135 ++-- src/app.js | 677 +++++++++--------- src/index/sentiment.mjs | 113 ++- src/infra/database/index.mjs | 8 +- .../migrations/20210430113743-user-data.js | 23 + .../20210430115910-analyses-remodel.js | 30 + ...10430120540-remove-tables-and-add-cache.js | 98 +++ src/infra/database/models/analyses.js | 136 ++-- src/infra/database/models/cache.js | 39 + src/infra/database/models/request.js | 4 - src/infra/database/models/userdata.js | 22 +- src/library.js | 31 +- 14 files changed, 941 insertions(+), 624 deletions(-) create mode 100644 src/infra/database/migrations/20210430113743-user-data.js create mode 100644 src/infra/database/migrations/20210430115910-analyses-remodel.js create mode 100644 src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js create mode 100644 src/infra/database/models/cache.js diff --git a/package-lock.json b/package-lock.json index d4449ab..bc772a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1646,12 +1646,9 @@ "dev": true }, "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", - "requires": { - "lodash": "^4.17.10" - } + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" }, "async-each": { "version": "1.0.3", @@ -1662,8 +1659,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "atob": { "version": "2.1.2", @@ -1750,6 +1746,11 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, + "blueimp-md5": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.18.0.tgz", + "integrity": "sha512-vE52okJvzsVWhcgUHOv+69OG3Mdg151xyn41aVQN/5W5S+S43qZhxECtYLAEHMSFWX6Mv5IZrzj3T5+JqXfj5Q==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -1992,6 +1993,15 @@ } } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2168,7 +2178,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -2262,8 +2271,7 @@ "cookiejar": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", - "dev": true + "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==" }, "copy-descriptor": { "version": "0.1.1", @@ -2442,8 +2450,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, "denque": { "version": "1.4.1", @@ -3169,6 +3176,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -3352,21 +3364,19 @@ "optional": true }, "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "formidable": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", - "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", - "dev": true + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==" }, "forwarded": { "version": "0.1.2", @@ -3426,9 +3436,10 @@ "dev": true }, "fuse.js": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.2.1.tgz", - "integrity": "sha1-YyDLlM5W7JdVyJred1vNuwNY1CU=" + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", + "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==", + "dev": true }, "gensync": { "version": "1.0.0-beta.1", @@ -3440,6 +3451,16 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -4268,6 +4289,14 @@ "object-visit": "^1.0.0" } }, + "md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "requires": { + "blueimp-md5": "^2.10.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -4414,18 +4443,16 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "dev": true + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==" }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "dev": true, + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", "requires": { - "mime-db": "1.44.0" + "mime-db": "1.47.0" } }, "mimic-fn": { @@ -4636,11 +4663,12 @@ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "multilang-sentiment": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/multilang-sentiment/-/multilang-sentiment-1.1.6.tgz", - "integrity": "sha512-XBIV0Ov+vvinNaecSfv/7YmEt5dQsoh/DAetNGcgAczJ6J3yPntjCCkQ1KmFqXCC//aboWt2XPgLngRwc8V7dA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/multilang-sentiment/-/multilang-sentiment-1.2.0.tgz", + "integrity": "sha512-OOVVfqQJ0EfLPGi0kIy3R7kLXH4awe8C7/ML6z4ggK0Ju4PoirWE69eSTMQ7PzQbsL381Z0bq/Xeg73919FpRA==", + "dev": true, "requires": { - "fuse.js": "^3.2.0" + "fuse.js": "^3.4.4" } }, "mute-stream": { @@ -5315,10 +5343,12 @@ } }, "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", - "dev": true + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } }, "range-parser": { "version": "1.2.1", @@ -5825,6 +5855,23 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "dependencies": { + "object-inspect": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.2.tgz", + "integrity": "sha512-gz58rdPpadwztRrPjZE9DZLOABUpTGdcANUgOwBFO1C+HZZhePoP83M65WGDmbpwFYJSWqavbl4SgDn4k8RYTA==" + } + } + }, "sigmund": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", @@ -6222,31 +6269,71 @@ "dev": true }, "superagent": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", - "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-6.1.0.tgz", + "integrity": "sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg==", "requires": { - "component-emitter": "^1.2.0", - "cookiejar": "^2.1.0", - "debug": "^3.1.0", - "extend": "^3.0.0", - "form-data": "^2.3.1", - "formidable": "^1.2.0", - "methods": "^1.1.1", - "mime": "^1.4.1", - "qs": "^6.5.1", - "readable-stream": "^2.3.5" + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "mime": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz", + "integrity": "sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } }, @@ -6258,6 +6345,46 @@ "requires": { "methods": "^1.1.2", "superagent": "^3.8.3" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "superagent": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz", + "integrity": "sha512-GLQtLMCoEIK4eDv6OGtkOoSMt3D+oq0y3dsxMuYuDvaNUvuT8eFBuLmfR0iYYzHC1e8hpzC6ZsxbuP6DIalMFA==", + "dev": true, + "requires": { + "component-emitter": "^1.2.0", + "cookiejar": "^2.1.0", + "debug": "^3.1.0", + "extend": "^3.0.0", + "form-data": "^2.3.1", + "formidable": "^1.2.0", + "methods": "^1.1.1", + "mime": "^1.4.1", + "qs": "^6.5.1", + "readable-stream": "^2.3.5" + } + } } }, "supports-color": { diff --git a/package.json b/package.json index 63babe7..58ddba1 100644 --- a/package.json +++ b/package.json @@ -35,13 +35,12 @@ "@babel/node": "^7.10.5", "@babel/plugin-transform-runtime": "^7.11.0", "@babel/preset-env": "^7.11.0", - "async": "^2.6.1", + "async": "^3.2.0", "body-parser": "^1.18.2", "dotenv": "^8.2.0", "express": "^4.16.3", "md5-hex": "^3.0.1", "memory-cache": "^0.2.0", - "multilang-sentiment": "^1.1.6", "pg": "^8.3.0", "pg-hstore": "^2.3.3", "redis": "^3.0.2", @@ -56,6 +55,7 @@ "eslint-config-airbnb-base": "^14.1.0", "eslint-plugin-import": "^2.20.2", "mocha": "^7.1.2", + "multilang-sentiment": "^1.2.0", "nodemon": "^2.0.3", "supertest": "^4.0.2" } diff --git a/src/analyze.js b/src/analyze.js index a2b3bed..a1db43e 100644 --- a/src/analyze.js +++ b/src/analyze.js @@ -10,10 +10,11 @@ import networkIndex from './index/network'; import sentimentIndex from './index/sentiment'; import library from './library'; import document from './document'; +import { Op } from 'sequelize'; // Import DB modules import { - Request, Analysis, UserData, ApiData, CachedRequest, + Request, Analysis, UserData, ApiData, CachedRequest, Cache } from './infra/database/index'; @@ -22,7 +23,7 @@ const weights = {}; module.exports = (screenName, config, index = { user: true, friend: true, network: true, temporal: true, sentiment: true, -}, sentimentLang, getData, cacheInterval, verbose, origin, wantDocument, fullAnalysisCache, cb) => new Promise(async (resolve, reject) => { // eslint-disable-line no-async-promise-executor +}, sentimentLang, getData, cacheInterval, verbose, origin, wantDocument, isFullAnalysis, fullAnalysisCache, cb) => new Promise(async (resolve, reject) => { // eslint-disable-line no-async-promise-executor let useCache = process.env.USE_CACHE; if (typeof fullAnalysisCache != 'undefined' && fullAnalysisCache === 0) useCache = 0; @@ -44,37 +45,6 @@ module.exports = (screenName, config, index = { extraDetails.TWITTER_HANDLE = screenName; extraDetails.TWITTER_LINK = `https://twitter.com/${screenName}`; - // check if we have a saved analysis of that user withing the desired time interval - if (useCache === '1') { - // let analysisForHandle = await library.getTwitterIDForHandle(screenName); - // let twitter_user_id = analysisForHandle[0].dataValues.twitter_user_id; - - // let cachedAnalysis = await library.getCachedAnalysis(twitter_user_id); - // console.log(cachedAnalysis); - - let cachedResult = await library.getCachedRequest(screenName, cacheInterval); - - if (cachedResult) { - const analysisId = cachedResult['analysis.id']; - - const { id: cachedResultID } = cachedResult; // use the original result to add an entry in the CachedRequest table - const { id: cachedRequestID } = await CachedRequest.create({ cachedResultID }).then((res) => res.dataValues); - // save the current request but link it with the CachedRequest we just created - await Request.create({ screenName, gitHead: await library.getGitHead(), cachedRequestID }); - - // format and return the cached result - cachedResult = library.formatCached(cachedResult, getData); - if (!verbose) delete cachedResult.profiles[0].bot_probability.info; - - if (cb) cb(null, cachedResult); - console.log(JSON.stringify(cachedResult, null, 2)); - - cachedResult.analysis_id = analysisId; - - resolve(cachedResult); - return cachedResult; - } - } const twitterParams = config; // Create Twitter client @@ -97,16 +67,14 @@ module.exports = (screenName, config, index = { let indexCount = 0; // store new client request and get request instance - const newRequest = await Request.create({ screenName, gitHead: await library.getGitHead(), origin }); + // const newRequest = await Request.create({ screenName, gitHead: await library.getGitHead(), origin }); // get tweets timeline. We will use it for both the user and sentiment/temporal/network calculations const apiAnswer = await client.get('statuses/user_timeline', param).catch((err) => err); // if there's an error, save the api response as is and update the new request entry with it if (!apiAnswer || apiAnswer.error || apiAnswer.errors || apiAnswer.length === 0) { - const { id: apiDataID } = await ApiData.create({ statusesUserTimeline: apiAnswer, params: param }); - newRequest.apiDataID = apiDataID; - newRequest.save(); + // TODO: Salvar erro if (cb) cb(apiAnswer, null); reject(apiAnswer); return apiAnswer; @@ -115,14 +83,39 @@ module.exports = (screenName, config, index = { // format api answer const { timeline, user } = library.getTimelineUser(apiAnswer); + // check if we have a saved analysis of that user withing the desired time interval + if (useCache === '1') { + const cacheInterval = library.getCacheInterval(); + + const cachedResponse = await Cache.findOne({ + attributes: ['simple_analysis', 'full_analysis'], + where: { + '$analysis.twitter_user_id$': user.id, + '$analysis.createdAt$': { [Op.between]: [cacheInterval, new Date()] }, + }, + include: 'analysis' + }) + + if (cachedResponse) { + const responseToUse = isFullAnalysis ? cachedResponse['full_analysis'] : cachedResponse['simple_analysis']; + if (responseToUse) { + const cachedJSON = JSON.parse(responseToUse); + + if (isFullAnalysis) { + const fullAnalysisRet = await library.buildAnalyzeReturn(cachedJSON); + resolve(fullAnalysisRet); + return fullAnalysisRet; + } + + resolve(cachedJSON); + return cachedJSON; + } + } + } + // get and store rate limits if (getData && timeline) timeline.rateLimit = await library.getRateStatus(timeline); - // store formated api response - const { id: apiDataID } = await ApiData.create({ statusesUserTimeline: { user, timeline }, params: param }); - newRequest.apiDataID = apiDataID; - newRequest.save(); - const explanations = [`Análise do usuário: ${screenName}`]; explanations.push('Carregou a timeline com o endpoint "statuses/user_timeline"'); @@ -328,10 +321,6 @@ module.exports = (screenName, config, index = { }], }; - const details = await document.getExtraDetails(extraDetails); - if (verbose) object.profiles[0].bot_probability.info = library.getLoggingtext(explanations); - if (wantDocument) object.profiles[0].bot_probability.extraDetails = details; - // add data from twitter to complement return (if getDate is true) and save to database const data = {}; @@ -352,38 +341,50 @@ module.exports = (screenName, config, index = { // save Analysis Data on database const { id: newAnalysisID } = await Analysis.create({ - fullResponse: object, total, user: userScore, friend: friendsScore, sentiment: sentimentScore, temporal: temporalScore, network: networkScore, - // twitter_user_id: data.user_id, - // twitter_handle: param.screen_name, - // twitter_created_at: data.created_at, - // twitter_following_count: data.following, - // twitter_followers_count: data.followers, - // twitter_status_count: data.number_tweets, - details, + twitter_user_id: data.user_id, + twitter_handle: param.screen_name, + twitter_created_at: data.created_at, + twitter_following_count: data.following, + twitter_followers_count: data.followers, + twitter_status_count: data.number_tweets, + origin: 'foo' }).then((res) => res.dataValues); + // Saving cache + const details = await document.getExtraDetails(extraDetails); + const detailsAsString = JSON.stringify(details); + await Cache.create({ + analysis_id: newAnalysisID, + simple_analysis: JSON.stringify(object), + full_analysis: detailsAsString, + updatedAt: null + }); + // save User Data on database - const { id: newUserDataID } = await UserData.create({ - username: data.user_name, - twitterID: data.user_id, - profileCreatedAt: data.created_at, - followingCount: data.following, - followersCount: data.followers, - statusesCount: data.number_tweets, - hashtagsUsed: data.hashtags, - mentionsUsed: data.mentions, - }).then((res) => res.dataValues); + // const { id: newUserDataID } = await UserData.create({ + // username: data.user_name, + // twitterID: data.user_id, + // profileCreatedAt: data.created_at, + // followingCount: data.following, + // followersCount: data.followers, + // statusesCount: data.number_tweets, + // hashtagsUsed: data.hashtags, + // mentionsUsed: data.mentions, + // }).then((res) => res.dataValues); // update request - newRequest.analysisID = newAnalysisID; - newRequest.userDataID = newUserDataID; - newRequest.save(); + // newRequest.analysisID = newAnalysisID; + // newRequest.userDataID = newUserDataID; + // newRequest.save(); + + if (verbose) object.profiles[0].bot_probability.info = library.getLoggingtext(explanations); + if (wantDocument) object.profiles[0].bot_probability.extraDetails = details; if (newAnalysisID) object.analysis_id = newAnalysisID; diff --git a/src/app.js b/src/app.js index 2006c80..da1e136 100644 --- a/src/app.js +++ b/src/app.js @@ -17,401 +17,402 @@ const app = express(); app.use(bodyParser.json()); const config = { - consumer_key: process.env.TWITTER_CONSUMER_KEY, - consumer_secret: process.env.TWITTER_CONSUMER_SECRET, + consumer_key: process.env.TWITTER_CONSUMER_KEY, + consumer_secret: process.env.TWITTER_CONSUMER_SECRET, }; const cacheDuration = process.env.CACHE_DURATION || 2592000; const requestTwitterList = async (client, searchFor, profile, limit, callback) => { - let cursor = -1; - const list = []; - let total = 0; - const param = { + let cursor = -1; + const list = []; + let total = 0; + const param = { + screen_name: profile, + }; + + const responseUser = await client.get('users/show', param); + total = responseUser[`${searchFor}_count`]; + + async.whilst( + () => cursor === -1, + async (next) => { + const params = { screen_name: profile, - }; - - const responseUser = await client.get('users/show', param); - total = responseUser[`${searchFor}_count`]; - - async.whilst( - () => cursor === -1, - async (next) => { - const params = { - screen_name: profile, - count: limit, - cursor, - }; - - try { - const responseList = await client.get(`${searchFor}/list`, params); - responseList.users.forEach((current) => { - list.push(current); - }); - cursor = responseList.next_cursor; - next(); - } catch (error) { - console.error(error); - next(error); - } - }, - (err) => { - const object = { - metadata: { - count: list.length, - total, - }, - profiles: [], - }; - - list.forEach((value) => { - object.profiles.push({ - username: value.screen_name, - url: `https://twitter.com/${value.screen_name}`, - avatar: value.profile_image_url, - user_profile_language: value.lang, - }); - }); - - if (err) { - object.metadata.error = err; - } - - callback(object); + count: limit, + cursor, + }; + + try { + const responseList = await client.get(`${searchFor}/list`, params); + responseList.users.forEach((current) => { + list.push(current); + }); + cursor = responseList.next_cursor; + next(); + } catch (error) { + console.error(error); + next(error); + } + }, + (err) => { + const object = { + metadata: { + count: list.length, + total, }, - ); + profiles: [], + }; + + list.forEach((value) => { + object.profiles.push({ + username: value.screen_name, + url: `https://twitter.com/${value.screen_name}`, + avatar: value.profile_image_url, + user_profile_language: value.lang, + }); + }); + + if (err) { + object.metadata.error = err; + } + + callback(object); + }, + ); }; // If you get an error 415, add your callback url to your Twitter App. async function getTokenUrl(req, searchFor, profile, limit) { - try { - const ssl = req.connection.encrypted ? 'https://' : 'http://'; - const oauthCallback = `${ssl + req.headers.host}/resultados?socialnetwork=twitter&authenticated=true&profile=${profile}&search_for=${searchFor}&limit=${limit}#conteudo`; + try { + const ssl = req.connection.encrypted ? 'https://' : 'http://'; + const oauthCallback = `${ssl + req.headers.host}/resultados?socialnetwork=twitter&authenticated=true&profile=${profile}&search_for=${searchFor}&limit=${limit}#conteudo`; - const client = new TwitterLite({ - consumer_key: config.consumer_key, - consumer_secret: config.consumer_secret, - }); + const client = new TwitterLite({ + consumer_key: config.consumer_key, + consumer_secret: config.consumer_secret, + }); - const reqData = await client.getRequestToken(oauthCallback); - const uri = `${'https://api.twitter.com/oauth/authenticate?'}${qs.stringify({ oauth_token: reqData.oauth_token })}`; - return uri; - } catch (error) { - return error; - } + const reqData = await client.getRequestToken(oauthCallback); + const uri = `${'https://api.twitter.com/oauth/authenticate?'}${qs.stringify({ oauth_token: reqData.oauth_token })}`; + return uri; + } catch (error) { + return error; + } } app.all('/', function (req, res, next) { - res.header("Access-Control-Allow-Origin", '"$http_origin" always'); - res.header("Access-Control-Allow-Headers", "X-Requested-With, Origin, Accept"); - next(); + res.header("Access-Control-Allow-Origin", '"$http_origin" always'); + res.header("Access-Control-Allow-Headers", "X-Requested-With, Origin, Accept"); + next(); }); app.get('/botometer', async (req, res) => { - const target = req.query.search_for; - const { profile } = req.query; - let { limit } = req.query; - const { authenticated } = req.query; - const cacheInterval = req.query.cache_duration; - const key = `${target}:${profile}`; - const cachedKey = mcache.get(key); - const verbose = req.query.verbose || req.query.logging; - const isAdmin = req.query.is_admin; - const origin = isAdmin ? 'admin' : 'website'; - const wantsDocument = req.query.documento; - const lang = req.headers["accept-language"]; - - const referer = req.get('referer'); - const sentimentLang = library.getDefaultLanguage(referer); - - const { getData } = req.query; - - console.log('profile', profile); - if (!limit || limit > 200) { - limit = 200; - } - if (typeof target === 'undefined' || typeof profile === 'undefined') { - res.status(400).send('One parameter is missing'); - } else if (cachedKey) { - res.send(cachedKey); - } else if (target === 'profile') { - try { - const result = await spottingbot(profile, config, { friend: false }, - sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, lang).catch((err) => err); - - if (result && result.profiles && result.profiles[0]) console.log(result.profiles[0]); - - if (!result || result.errors || result.error) { - let toSend = result; - if (result.errors) toSend = result.errors; - - // The error format varies according to the error - // Not all of them will have an error code - // Use first error to determine message - const firstError = result.errors ? result.errors[0] : result; - console.log(firstError); - - let errorMessage; - if (firstError.code === 34) { - errorMessage = 'Esse usuário não existe' - } - else if (firstError.error === 'Not authorized.') { - errorMessage = 'Sem permissão para acessar. Usuário pode estar bloqueado/suspendido.' - } - else { - errorMessage = 'Erro ao procurar pelo perfil' - } - - res.status(400).json({ metadata: { error: toSend }, message: errorMessage }); - return; - } - - if (wantsDocument === '1' && result.profiles[0].bot_probability.extraDetails) { - res.send(result.profiles[0].bot_probability.extraDetails); - } else if (verbose === '1' && result.profiles[0].bot_probability.info) { - const loggingText = result.profiles[0].bot_probability.info; - const fileName = `${profile}_analise.txt`; - res.set({ 'Content-Disposition': `attachment; filename="${fileName}"`, 'Content-type': 'application/octet-stream' }); - res.send(loggingText); - } else { - res.json(result); - } - } catch (error) { - console.log('error', error); - res.status(500).json({ metadata: { error } }); + const target = req.query.search_for; + const { profile } = req.query; + let { limit } = req.query; + const { authenticated } = req.query; + const cacheInterval = req.query.cache_duration; + const key = `${target}:${profile}`; + const cachedKey = mcache.get(key); + const verbose = req.query.verbose || req.query.logging; + const isAdmin = req.query.is_admin; + const origin = isAdmin ? 'admin' : 'website'; + const wantsDocument = req.query.documento; + const lang = req.headers["accept-language"]; + + const referer = req.get('referer'); + const sentimentLang = library.getDefaultLanguage(referer); + + const { getData } = req.query; + + console.log('profile', profile); + if (!limit || limit > 200) { + limit = 200; + } + if (typeof target === 'undefined' || typeof profile === 'undefined') { + res.status(400).send('One parameter is missing'); + } else if (cachedKey) { + res.send(cachedKey); + } else if (target === 'profile') { + try { + const result = await spottingbot(profile, config, { friend: false }, + sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, lang).catch((err) => err); + + if (result && result.profiles && result.profiles[0]) console.log(result.profiles[0]); + + if (!result || result.errors || result.error) { + let toSend = result; + if (result.errors) toSend = result.errors; + + // The error format varies according to the error + // Not all of them will have an error code + // Use first error to determine message + const firstError = result.errors ? result.errors[0] : result; + console.log(firstError); + + let errorMessage; + if (firstError.code === 34) { + errorMessage = 'Esse usuário não existe' } - } else if (target === 'followers' || target === 'friends') { - if (authenticated === 'true') { - // const token = req.query.oauth_token; - // const tokenSecret = mcache.get(token); - // const verifier = req.query.oauth_verifier; - - const client = new TwitterLite({ - consumer_key: config.consumer_key, - consumer_secret: config.consumer_secret, - access_token_key: undefined, - access_token_secret: undefined, - }); - - requestTwitterList(client, target, profile, limit, (object) => { - if (typeof object.metadata.error === 'undefined') { - mcache.put(key, JSON.stringify(object), cacheDuration * 1000); - } - res.json(object); - }); - } else { - const result = await getTokenUrl(req, target, profile, limit); - if (result.errors) { - res.status(500).send(result); - } else { - res.json({ request_url: result }); - } + else if (firstError.error === 'Not authorized.') { + errorMessage = 'Sem permissão para acessar. Usuário pode estar bloqueado/suspendido.' } + else { + errorMessage = 'Erro ao procurar pelo perfil' + } + + res.status(400).json({ metadata: { error: toSend }, message: errorMessage }); + return; + } + + if (wantsDocument === '1' && result.profiles[0].bot_probability.extraDetails) { + res.send(result.profiles[0].bot_probability.extraDetails); + } else if (verbose === '1' && result.profiles[0].bot_probability.info) { + const loggingText = result.profiles[0].bot_probability.info; + const fileName = `${profile}_analise.txt`; + res.set({ 'Content-Disposition': `attachment; filename="${fileName}"`, 'Content-type': 'application/octet-stream' }); + res.send(loggingText); + } else { + res.json(result); + } + } catch (error) { + console.log('error', error); + res.status(500).json({ metadata: { error } }); + } + } else if (target === 'followers' || target === 'friends') { + if (authenticated === 'true') { + // const token = req.query.oauth_token; + // const tokenSecret = mcache.get(token); + // const verifier = req.query.oauth_verifier; + + const client = new TwitterLite({ + consumer_key: config.consumer_key, + consumer_secret: config.consumer_secret, + access_token_key: undefined, + access_token_secret: undefined, + }); + + requestTwitterList(client, target, profile, limit, (object) => { + if (typeof object.metadata.error === 'undefined') { + mcache.put(key, JSON.stringify(object), cacheDuration * 1000); + } + res.json(object); + }); } else { - res.status(400).send('search_for is wrong'); + const result = await getTokenUrl(req, target, profile, limit); + if (result.errors) { + res.status(500).send(result); + } else { + res.json({ request_url: result }); + } } + } else { + res.status(400).send('search_for is wrong'); + } }); // request app.post('/feedback', async (req, res) => { - const { opinion } = req.body; - const analysisID = req.body.analysis_id; - - const result = await library.saveFeedback(analysisID, opinion); - if (result && result.id) { - res.status(200).send(result); - } else { - res.status(500).send(result); - } + const { opinion } = req.body; + const analysisID = req.body.analysis_id; + + const result = await library.saveFeedback(analysisID, opinion); + if (result && result.id) { + res.status(200).send(result); + } else { + res.status(500).send(result); + } }); app.get('/feedback', (req, res) => { - if (fs.existsSync('opinion.json') === false) { - res.send('No feedback yet'); - return; - } - const content = fs.readFileSync('opinion.json'); - const data = JSON.parse(content); - res.json(data); + if (fs.existsSync('opinion.json') === false) { + res.send('No feedback yet'); + return; + } + const content = fs.readFileSync('opinion.json'); + const data = JSON.parse(content); + res.json(data); }); app.get('/user-timeline-rate-limit', async (req, res) => { - const rateLimits = await library.getRateLimits(null, true); - let status = 200; - if (!rateLimits || rateLimits.error) status = 500; - res.status(status).json(rateLimits); + const rateLimits = await library.getRateLimits(null, true); + let status = 200; + if (!rateLimits || rateLimits.error) status = 500; + res.status(status).json(rateLimits); }); app.get('/status', (req, res) => { - res.sendStatus(200); + res.sendStatus(200); }); app.get('/analyze', async (req, res) => { - const target = req.query.search_for; - - const { profile } = req.query; - let { limit } = req.query; - const { authenticated } = req.query; - - const cacheInterval = req.query.cache_duration; - const key = `${target}:${profile}`; - const cachedKey = mcache.get(key); - const fullAnalysisCache = 0; - - const verbose = req.query.verbose || req.query.logging; - const isAdmin = req.query.is_admin; - const origin = isAdmin ? 'admin' : 'website'; - const wantsDocument = 1; + const target = req.query.search_for; + + const { profile } = req.query; + let { limit } = req.query; + const { authenticated } = req.query; + + const cacheInterval = req.query.cache_duration; + const key = `${target}:${profile}`; + const cachedKey = mcache.get(key); + const fullAnalysisCache = 1; + const isFullAnalysis = 1; + + const verbose = req.query.verbose || req.query.logging; + const isAdmin = req.query.is_admin; + const origin = isAdmin ? 'admin' : 'website'; + const wantsDocument = 1; + + const referer = req.get('referer'); + const sentimentLang = library.getDefaultLanguage(referer); + + const lang = req.headers["accept-language"]; + + const { getData } = req.query; + + console.log('profile', profile); + if (!limit || limit > 200) { + limit = 200; + } + if (typeof target === 'undefined' || typeof profile === 'undefined') { + res.status(400).send('One parameter is missing'); + } else if (cachedKey) { + res.send(cachedKey); + } else if (target === 'profile') { + try { + const result = await spottingbot(profile, config, { friend: false }, + sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, fullAnalysisCache, isFullAnalysis).catch((err) => err); - const referer = req.get('referer'); - const sentimentLang = library.getDefaultLanguage(referer); + if (!result || result.errors || result.error) { + let toSend = result; + if (result.errors) toSend = result.errors; - const lang = req.headers["accept-language"]; + res.status(404).json({ metadata: { error: toSend } }); + return; + } - const { getData } = req.query; + res.send(result); - console.log('profile', profile); - if (!limit || limit > 200) { - limit = 200; + } catch (error) { + console.log('error', error); + res.status(500).json({ metadata: { error } }); } - if (typeof target === 'undefined' || typeof profile === 'undefined') { - res.status(400).send('One parameter is missing'); - } else if (cachedKey) { - res.send(cachedKey); - } else if (target === 'profile') { - try { - const result = await spottingbot(profile, config, { friend: false }, - sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, fullAnalysisCache).catch((err) => err); - - if (!result || result.errors || result.error) { - let toSend = result; - if (result.errors) toSend = result.errors; - - res.status(404).json({ metadata: { error: toSend } }); - return; - } - - res.send(await library.buildAnalyzeReturn(result.profiles[0].bot_probability.extraDetails, lang)); - - } catch (error) { - console.log('error', error); - res.status(500).json({ metadata: { error } }); - } - } else if (target === 'followers' || target === 'friends') { - if (authenticated === 'true') { - // const token = req.query.oauth_token; - // const tokenSecret = mcache.get(token); - // const verifier = req.query.oauth_verifier; - - const client = new TwitterLite({ - consumer_key: config.consumer_key, - consumer_secret: config.consumer_secret, - access_token_key: undefined, - access_token_secret: undefined, - }); - - requestTwitterList(client, target, profile, limit, (object) => { - if (typeof object.metadata.error === 'undefined') { - mcache.put(key, JSON.stringify(object), cacheDuration * 1000); - } - res.json(object); - }); - } else { - const result = await getTokenUrl(req, target, profile, limit); - if (result.errors) { - res.status(500).send(result); - } else { - res.json({ request_url: result }); - } + } else if (target === 'followers' || target === 'friends') { + if (authenticated === 'true') { + // const token = req.query.oauth_token; + // const tokenSecret = mcache.get(token); + // const verifier = req.query.oauth_verifier; + + const client = new TwitterLite({ + consumer_key: config.consumer_key, + consumer_secret: config.consumer_secret, + access_token_key: undefined, + access_token_secret: undefined, + }); + + requestTwitterList(client, target, profile, limit, (object) => { + if (typeof object.metadata.error === 'undefined') { + mcache.put(key, JSON.stringify(object), cacheDuration * 1000); } + res.json(object); + }); } else { - res.status(400).send('search_for is wrong'); + const result = await getTokenUrl(req, target, profile, limit); + if (result.errors) { + res.status(500).send(result); + } else { + res.json({ request_url: result }); + } } + } else { + res.status(400).send('search_for is wrong'); + } }); app.get('/botometer-bulk', async (req, res) => { - const apiKey = req.headers["x-api-key"]; - if (!apiKey || apiKey != process.env.BULK_API_KEY) return res.status(403).send('forbidden'); - + const apiKey = req.headers["x-api-key"]; + if (!apiKey || apiKey != process.env.BULK_API_KEY) return res.status(403).send('forbidden'); - const { profiles, is_admin, twitter_api_consumer_key, twitter_api_consumer_secret } = req.body; - if (profiles.length > 50) return res.status(400).json({ message: 'max profiles size is 50' }); - if (twitter_api_consumer_key && twitter_api_consumer_secret) { - config = { - consumer_key: twitter_api_consumer_key, - consumer_secret: twitter_api_consumer_secret, - }; - } - - const getData = true; - const referer = req.get('referer'); - const origin = is_admin ? 'admin' : 'website'; - - const profiles_results = profiles.map(profile => { - - return spottingbot( - profile, config, { friend: false }, library.getDefaultLanguage(referer), getData, origin - ).then((result) => { - return { - twitter_user_data: { - id: result.twitter_data.user_id, - handle: '@' + result.profiles[0].username, - user_name: result.twitter_data.user_name, - url: result.profiles[0].url, - avatar: result.profiles[0].avatar, - created_at: result.twitter_data.created_at, - }, - twitter_user_meta_data: { - tweet_count: result.twitter_data.number_tweets, - follower_count: result.twitter_data.followers, - following_count: result.twitter_data.following, - hashtags: result.twitter_data.hashtags, - mentions: result.twitter_data.mentions, - }, - pegabot_analysis: { - user_index: result.profiles[0].language_independent.user, - temporal_index: result.profiles[0].language_independent.temporal, - network_index: result.profiles[0].language_independent.network, - sentiment_index: result.profiles[0].language_dependent.sentiment.value, - bot_probability: result.profiles[0].bot_probability.all, - }, - metadata: { - used_cache: result.twitter_data.usedCache - } - } - }).catch((err) => { - const firstError = err.errors ? err.errors[0] : err; - - let errorMessage; - if (firstError.code === 34) { - errorMessage = 'Esse usuário não existe' - } - else if (firstError.error === 'Not authorized.') { - errorMessage = 'Sem permissão para acessar. Usuário pode estar bloqueado/suspendido.' - } - else { - errorMessage = 'Erro ao procurar pelo perfil' - } - - return { - twitter_user_data: { - user_handle: profile, - }, - metadata: { - error: errorMessage - } - } - }); + const { profiles, is_admin, twitter_api_consumer_key, twitter_api_consumer_secret } = req.body; + if (profiles.length > 50) return res.status(400).json({ message: 'max profiles size is 50' }); + if (twitter_api_consumer_key && twitter_api_consumer_secret) { + config = { + consumer_key: twitter_api_consumer_key, + consumer_secret: twitter_api_consumer_secret, + }; + } + + const getData = true; + const referer = req.get('referer'); + const origin = is_admin ? 'admin' : 'website'; + + const profiles_results = profiles.map(profile => { + + return spottingbot( + profile, config, { friend: false }, library.getDefaultLanguage(referer), getData, origin + ).then((result) => { + return { + twitter_user_data: { + id: result.twitter_data.user_id, + handle: '@' + result.profiles[0].username, + user_name: result.twitter_data.user_name, + url: result.profiles[0].url, + avatar: result.profiles[0].avatar, + created_at: result.twitter_data.created_at, + }, + twitter_user_meta_data: { + tweet_count: result.twitter_data.number_tweets, + follower_count: result.twitter_data.followers, + following_count: result.twitter_data.following, + hashtags: result.twitter_data.hashtags, + mentions: result.twitter_data.mentions, + }, + pegabot_analysis: { + user_index: result.profiles[0].language_independent.user, + temporal_index: result.profiles[0].language_independent.temporal, + network_index: result.profiles[0].language_independent.network, + sentiment_index: result.profiles[0].language_dependent.sentiment.value, + bot_probability: result.profiles[0].bot_probability.all, + }, + metadata: { + used_cache: result.twitter_data.usedCache + } + } + }).catch((err) => { + const firstError = err.errors ? err.errors[0] : err; + + let errorMessage; + if (firstError.code === 34) { + errorMessage = 'Esse usuário não existe' + } + else if (firstError.error === 'Not authorized.') { + errorMessage = 'Sem permissão para acessar. Usuário pode estar bloqueado/suspendido.' + } + else { + errorMessage = 'Erro ao procurar pelo perfil' + } + + return { + twitter_user_data: { + user_handle: profile, + }, + metadata: { + error: errorMessage + } + } }); - Promise.all(profiles_results).then(function (results) { - res.status(200).json({ analyses_count: results.length, analyses: results }); - return; - }); + }); + + Promise.all(profiles_results).then(function (results) { + res.status(200).json({ analyses_count: results.length, analyses: results }); + return; + }); }); diff --git a/src/index/sentiment.mjs b/src/index/sentiment.mjs index bacad9c..ed0b045 100644 --- a/src/index/sentiment.mjs +++ b/src/index/sentiment.mjs @@ -1,5 +1,6 @@ import sentiment from 'multilang-sentiment'; import library from '../library'; +import async from "async"; export default async (data, defaultLanguage = 'pt', explanations = [], extraDetails = {}) => { explanations.push('\n-Análise do Score Sentimento:\n'); @@ -30,66 +31,60 @@ export default async (data, defaultLanguage = 'pt', explanations = [], extraDeta let happyCount = 0; let sadCount = 0; - tweets.forEach((current) => { - let { lang } = current; - const { text } = current; - try { - let res = {}; - - // sets default language - if (!lang || ['und', 'in'].includes(lang)) lang = defaultLanguage; - - // get sentiment score for tweet text - res = sentiment(text, lang); - if (!savedRes) { - explanations.push(`Exemplo do score do tweet: ${res.comparative}`); - savedRes = true; - } - - const emoji = library.getEmojiOnString(text); - if (emoji.happy || emoji.sad) emojiCount += 1; - if (emoji.happy) happyCount += 1; - if (emoji.sad) sadCount += 1; - - const { negative } = res; - const { positive } = res; - - // Creating flag that will signify when the tweet has alredy been used for screencap sampling. - // Thus, there will never be a repeated tweet between the negative, positive and neutral samples. - let usedForSampling = 0; - - // Saving positive tweet sample - if (!tweetExemplo.positive && res.positive) { - current.url = extraDetails.TWITTER_LINK + '/status/' + current.id_str; - tweetExemplo.positive = current; - - usedForSampling = 1; - } - - // Saving negative tweet sample - if (!tweetExemplo.negative && res.negative && usedForSampling === 0) { - current.url = extraDetails.TWITTER_LINK + '/status/' + current.id_str; - tweetExemplo.negative = current; - - usedForSampling = 1; - } - - // Saving neutral tweet sample - if (!tweetExemplo.neutral && res.comparative === 0 && usedForSampling === 0) { - current.url = extraDetails.TWITTER_LINK + '/status/' + current.id_str; - tweetExemplo.neutral = current; - - usedForSampling = 1; - } - - if (res.comparative === 0) sentimentNeutralSum += 1; - } catch (error) { - console.log('Error trying to analyse sentiment'); - console.log('text', text); - console.log('lang', lang); - console.log(error); + async.each(tweets, (tweet) => { + let { lang } = tweet; + const { text } = tweet; + + let res = {}; + + // sets default language + if (!lang || ['und', 'in'].includes(lang)) lang = defaultLanguage; + + // get sentiment score for tweet text + res = sentiment(text, lang); + if (!savedRes) { + explanations.push(`Exemplo do score do tweet: ${res.comparative}`); + savedRes = true; + } + + const emoji = library.getEmojiOnString(text); + if (emoji.happy || emoji.sad) emojiCount += 1; + if (emoji.happy) happyCount += 1; + if (emoji.sad) sadCount += 1; + + const { negative } = res; + const { positive } = res; + + // Creating flag that will signify when the tweet has alredy been used for screencap sampling. + // Thus, there will never be a repeated tweet between the negative, positive and neutral samples. + let usedForSampling = 0; + + // Saving positive tweet sample + if (!tweetExemplo.positive && res.positive) { + tweet.url = extraDetails.TWITTER_LINK + '/status/' + tweet.id_str; + tweetExemplo.positive = tweet; + + usedForSampling = 1; + } + + // Saving negative tweet sample + if (!tweetExemplo.negative && res.negative && usedForSampling === 0) { + tweet.url = extraDetails.TWITTER_LINK + '/status/' + tweet.id_str; + tweetExemplo.negative = tweet; + + usedForSampling = 1; } - }); + + // Saving neutral tweet sample + if (!tweetExemplo.neutral && res.comparative === 0 && usedForSampling === 0) { + tweet.url = extraDetails.TWITTER_LINK + '/status/' + tweet.id_str; + tweetExemplo.neutral = tweet; + + usedForSampling = 1; + } + + if (res.comparative === 0) sentimentNeutralSum += 1; + }, (err) => { }); explanations.push(`Temos ${sentimentNeutralSum} tweet(s) neutros`); diff --git a/src/infra/database/index.mjs b/src/infra/database/index.mjs index c81ef62..7afb326 100644 --- a/src/infra/database/index.mjs +++ b/src/infra/database/index.mjs @@ -5,6 +5,7 @@ import UserDataModel from './models/userdata'; import ApiDataModel from './models/apidata'; import CachedRequestsModel from './models/cachedRequests'; import FeedbackModel from './models/feedbacks'; +import CacheModel from './models/cache'; import Config from './config'; const env = process.env.NODE_ENV || 'development'; @@ -19,12 +20,11 @@ if (config.use_env_variable) { const sequelize = new Sequelize(...sequelizeArgs); const models = { - Request: RequestModel.init(sequelize, Sequelize), Analysis: AnalysisModel.init(sequelize, Sequelize), UserData: UserDataModel.init(sequelize, Sequelize), - ApiData: ApiDataModel.init(sequelize, Sequelize), CachedRequest: CachedRequestsModel.init(sequelize, Sequelize), Feedback: FeedbackModel.init(sequelize, Sequelize), + Cache: CacheModel.init(sequelize, Sequelize) }; Object.values(models) @@ -32,9 +32,9 @@ Object.values(models) .forEach((model) => model.associate(models)); const { - Request, Analysis, UserData, ApiData, CachedRequest, Feedback, + Request, Analysis, UserData, ApiData, CachedRequest, Feedback, Cache } = models; export { - sequelize, Sequelize, Request, Analysis, UserData, ApiData, CachedRequest, Feedback, + sequelize, Sequelize, Request, Analysis, UserData, ApiData, CachedRequest, Feedback, Cache }; diff --git a/src/infra/database/migrations/20210430113743-user-data.js b/src/infra/database/migrations/20210430113743-user-data.js new file mode 100644 index 0000000..8a11262 --- /dev/null +++ b/src/infra/database/migrations/20210430113743-user-data.js @@ -0,0 +1,23 @@ +'use strict'; + +const { query } = require("express"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('UserData', 'followingCount'); + await queryInterface.removeColumn('UserData', 'followersCount'); + await queryInterface.removeColumn('UserData', 'statusesCount'); + await queryInterface.removeColumn('UserData', 'hashtagsUsed'); + await queryInterface.removeColumn('UserData', 'mentionsUsed'); + return await queryInterface.addColumn('UserData', 'twitter_handle', { type: Sequelize.STRING }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('UserData', 'followingCount', { allowNull: false, type: Sequelize.INTEGER }); + await queryInterface.addColumn('UserData', 'followersCount', { allowNull: false, type: Sequelize.INTEGER }); + await queryInterface.addColumn('UserData', 'statusesCount', { allowNull: false, type: Sequelize.INTEGER }); + await queryInterface.addColumn('UserData', 'hashtagsUsed', { allowNull: true, type: Sequelize.ARRAY(Sequelize.STRING) }); + await queryInterface.addColumn('UserData', 'mentionsUsed', { allowNull: true, type: Sequelize.ARRAY(Sequelize.STRING) }); + return queryInterface.removeColumn('UserData', 'twitter_handle'); + } +}; diff --git a/src/infra/database/migrations/20210430115910-analyses-remodel.js b/src/infra/database/migrations/20210430115910-analyses-remodel.js new file mode 100644 index 0000000..060f1ac --- /dev/null +++ b/src/infra/database/migrations/20210430115910-analyses-remodel.js @@ -0,0 +1,30 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('Analyses', 'explanations'); + await queryInterface.removeColumn('Analyses', 'details'); + await queryInterface.removeColumn('Analyses', 'fullResponse'); + + await queryInterface.addColumn('Analyses', 'twitter_user_id', { type: Sequelize.BIGINT }); + await queryInterface.addColumn('Analyses', 'twitter_handle', { type: Sequelize.STRING }); + await queryInterface.addColumn('Analyses', 'twitter_following_count', { type: Sequelize.INTEGER }); + await queryInterface.addColumn('Analyses', 'twitter_followers_count', { type: Sequelize.INTEGER }); + await queryInterface.addColumn('Analyses', 'twitter_status_count', { type: Sequelize.INTEGER }); + await queryInterface.addColumn('Analyses', 'twitter_created_at', { type: Sequelize.DATE }); + await queryInterface.addColumn('Analyses', 'origin', { type: Sequelize.STRING }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('Analyses', 'explanations', { type: Sequelize.JSON }); + await queryInterface.addColumn('Analyses', 'details', { type: Sequelize.JSON }); + await queryInterface.addColumn('Analyses', 'fullResponse', { type: Sequelize.JSON }); + await queryInterface.removeColumn('Analyses', 'twitter_user_id'); + await queryInterface.removeColumn('Analyses', 'twitter_handle'); + await queryInterface.removeColumn('Analyses', 'twitter_created_at'); + await queryInterface.removeColumn('Analyses', 'twitter_following_count'); + await queryInterface.removeColumn('Analyses', 'twitter_followers_count'); + await queryInterface.removeColumn('Analyses', 'twitter_status_count'); + await queryInterface.removeColumn('Analyses', 'origin'); + } +}; diff --git a/src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js b/src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js new file mode 100644 index 0000000..efff93b --- /dev/null +++ b/src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js @@ -0,0 +1,98 @@ +'use strict'; + +const { query } = require("express"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.dropTable('Requests'); + await queryInterface.dropTable('ApiData'); + + await queryInterface.createTable('cache', { + id: { + primaryKey: true, + autoIncrement: true, + type: Sequelize.INTEGER + }, + + analysis_id: { + allowNull: false, + type: Sequelize.INTEGER, + references: { model: 'Analyses', key: 'id' } + }, + + simple_analysis: { + allowNull: false, + type: Sequelize.JSON + }, + + full_analysis: { + allowNull: true, + type: Sequelize.TEXT + }, + + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + + updatedAt: { + allowNull: true, + type: Sequelize.DATE, + }, + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.createTable('Requests', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + screenName: { + allowNull: false, + type: Sequelize.STRING, + }, + apiResponse: { + allowNull: false, + type: Sequelize.JSON, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + + await queryInterface.createTable('ApiData', { + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: Sequelize.INTEGER, + }, + params: { + allowNull: true, + type: Sequelize.JSON, + }, + statusesUserTimeline: { + allowNull: true, + type: Sequelize.JSON, + }, + createdAt: { + allowNull: false, + type: Sequelize.DATE, + }, + updatedAt: { + allowNull: false, + type: Sequelize.DATE, + }, + }); + + await queryInterface.dropTable('cache'); + } +}; diff --git a/src/infra/database/models/analyses.js b/src/infra/database/models/analyses.js index 92a226f..0f5acbf 100644 --- a/src/infra/database/models/analyses.js +++ b/src/infra/database/models/analyses.js @@ -1,74 +1,70 @@ import { Model } from 'sequelize'; export default class Analysis extends Model { - static init(sequelize, DataTypes) { - return super.init({ - id: { - allowNull: false, - autoIncrement: true, - primaryKey: true, - type: DataTypes.INTEGER, - }, - fullResponse: { - allowNull: false, - type: DataTypes.JSON, - }, - total: { - allowNull: true, - type: DataTypes.STRING, - }, - user: { - allowNull: true, - type: DataTypes.STRING, - }, - friend: { - allowNull: true, - type: DataTypes.STRING, - }, - sentiment: { - allowNull: true, - type: DataTypes.STRING, - }, - temporal: { - allowNull: true, - type: DataTypes.STRING, - }, - network: { - allowNull: true, - type: DataTypes.STRING, - }, - details: { - allowNull: true, - type: DataTypes.JSON, - }, - twitter_user_id: { - allowNull: true, - type: DataTypes.BIGINT, - }, - twitter_handle: { - allowNull: true, - type: DataTypes.STRING, - }, - twitter_created_at: { - allowNull: true, - type: DataTypes.DATE, - }, - twitter_following_count: { - allowNull: true, - type: DataTypes.INTEGER, - }, - twitter_followers_count: { - allowNull: true, - type: DataTypes.INTEGER, - }, - twitter_status_count: { - allowNull: true, - type: DataTypes.INTEGER, - } - }, { - sequelize, - modelName: 'Analyses', - freezeTableName: true, - }); - } + static init(sequelize, DataTypes) { + return super.init({ + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER, + }, + total: { + allowNull: true, + type: DataTypes.STRING, + }, + user: { + allowNull: true, + type: DataTypes.STRING, + }, + friend: { + allowNull: true, + type: DataTypes.STRING, + }, + sentiment: { + allowNull: true, + type: DataTypes.STRING, + }, + temporal: { + allowNull: true, + type: DataTypes.STRING, + }, + network: { + allowNull: true, + type: DataTypes.STRING, + }, + twitter_user_id: { + allowNull: true, + type: DataTypes.BIGINT, + }, + twitter_handle: { + allowNull: true, + type: DataTypes.STRING, + }, + twitter_created_at: { + allowNull: true, + type: DataTypes.DATE, + }, + twitter_following_count: { + allowNull: true, + type: DataTypes.INTEGER, + }, + twitter_followers_count: { + allowNull: true, + type: DataTypes.INTEGER, + }, + twitter_status_count: { + allowNull: true, + type: DataTypes.INTEGER, + }, + origin: { + allowNull: true, + type: DataTypes.STRING + } + }, { + sequelize, + modelName: 'Analyses', + freezeTableName: true, + }); + } } diff --git a/src/infra/database/models/cache.js b/src/infra/database/models/cache.js new file mode 100644 index 0000000..0435b96 --- /dev/null +++ b/src/infra/database/models/cache.js @@ -0,0 +1,39 @@ +import { Model } from 'sequelize'; + +const CacheModel = class Cache extends Model { + static init(sequelize, DataTypes) { + return super.init({ + id: { + allowNull: false, + autoIncrement: true, + primaryKey: true, + type: DataTypes.INTEGER, + }, + analysis_id: { + allowNull: false, + type: DataTypes.INTEGER, + references: { model: 'Analyses', key: 'id' } + }, + simple_analysis: { + allowNull: false, + type: DataTypes.JSON, + }, + full_analysis: { + allowNull: true, + type: DataTypes.TEXT, + } + }, { + sequelize, + modelName: 'cache', + freezeTableName: true, + }); + } +} + +CacheModel.associate = (models) => { + CacheModel.belongsTo(models.Analysis, { + as: 'analysis', foreignKey: 'id', sourceKey: 'analysisID', onDelete: 'CASCADE', onUpdate: 'CASCADE', + }); +}; + +export default CacheModel; \ No newline at end of file diff --git a/src/infra/database/models/request.js b/src/infra/database/models/request.js index 7a18213..b6a9917 100644 --- a/src/infra/database/models/request.js +++ b/src/infra/database/models/request.js @@ -52,10 +52,6 @@ requestModel.associate = (models) => { requestModel.hasOne(models.UserData, { as: 'userdata', foreignKey: 'id', sourceKey: 'userDataID', onDelete: 'CASCADE', onUpdate: 'CASCADE', }); - - requestModel.hasOne(models.ApiData, { - as: 'apidata', foreignKey: 'id', sourceKey: 'apiDataID', onDelete: 'CASCADE', onUpdate: 'CASCADE', - }); }; export default requestModel; diff --git a/src/infra/database/models/userdata.js b/src/infra/database/models/userdata.js index 8b1fc07..3f2ea80 100644 --- a/src/infra/database/models/userdata.js +++ b/src/infra/database/models/userdata.js @@ -21,26 +21,10 @@ export default class UserData extends Model { allowNull: false, type: DataTypes.DATE, }, - followingCount: { - allowNull: false, - type: DataTypes.INTEGER, - }, - followersCount: { - allowNull: false, - type: DataTypes.INTEGER, - }, - statusesCount: { - allowNull: false, - type: DataTypes.INTEGER, - }, - hashtagsUsed: { + twitter_handle: { allowNull: true, - type: DataTypes.ARRAY(DataTypes.STRING), - }, - mentionsUsed: { - allowNull: true, - type: DataTypes.ARRAY(DataTypes.STRING), - }, + type: DataTypes.STRING + } }, { sequelize, modelName: 'UserData', diff --git a/src/library.js b/src/library.js index 0682825..630e42c 100644 --- a/src/library.js +++ b/src/library.js @@ -2,7 +2,7 @@ import { execSync } from 'child_process'; import { Op } from 'sequelize'; import TwitterLite from 'twitter-lite'; import { getExtraDetails } from './document'; -import { Request, Feedback, Analysis } from './infra/database/index'; +import { Request, Feedback, Analysis, Cache, UserData } from './infra/database/index'; import Texts from './data/texts'; const superagent = require('superagent'); const md5Hex = require('md5-hex'); @@ -109,6 +109,22 @@ const editDistance = (string1, string2) => { }; export default { + getCacheInterval: (interval) => { + let newInterval = interval || process.env.DEFAULT_CACHE_INTERVAL; + if (!newInterval || !newInterval.match(/[0-9]{1,}_(days|hours|minutes|seconds)/i)) newInterval = '1_days'; + const splitStr = newInterval.split('_'); + + const value = splitStr[0]; + const time = splitStr[1]; + const date = new Date(); + + if (time === 'days') date.setDate(date.getDate() - value); + if (time === 'hours') date.setHours(date.getHours() - value); + if (time === 'minutes') date.setMinutes(date.getMinutes() - value); + if (time === 'seconds') date.setSeconds(date.getSeconds() - value); + + return date; + }, getLoggingtext: (explanations) => { if (!explanations || Array.isArray(explanations) === false) return ''; @@ -296,7 +312,18 @@ export default { return result; }, - buildAnalyzeReturn: async (extraDetails, lang) => { + userDataUpsert: async (twitterUserId, twitterUserHandle, twitterUserName, twitterCreatedAt) => { + return await UserData.findOrCreate({ + where: { + username: twitterUserName, + twitterID: twitterUserId, + profileCreatedAt: twitterCreatedAt, + twitter_handle: twitterUserHandle + } + }); + }, + + buildAnalyzeReturn: async (extraDetails, lang, analysis_id) => { // Setting text file let texts; if (/es-mx/.test(lang)) { From 88ca7242f887ede0499a2108f1129a2d4b6e80b9 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 30 Apr 2021 19:04:23 -0300 Subject: [PATCH 04/14] Adicionando contador para cache --- src/analyze.js | 7 ++++++- .../20210430120540-remove-tables-and-add-cache.js | 6 ++++++ src/infra/database/models/cache.js | 7 ++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/analyze.js b/src/analyze.js index a1db43e..6e8d0f7 100644 --- a/src/analyze.js +++ b/src/analyze.js @@ -88,7 +88,7 @@ module.exports = (screenName, config, index = { const cacheInterval = library.getCacheInterval(); const cachedResponse = await Cache.findOne({ - attributes: ['simple_analysis', 'full_analysis'], + attributes: ['simple_analysis', 'full_analysis', 'times_served', 'id'], where: { '$analysis.twitter_user_id$': user.id, '$analysis.createdAt$': { [Op.between]: [cacheInterval, new Date()] }, @@ -97,6 +97,8 @@ module.exports = (screenName, config, index = { }) if (cachedResponse) { + const currentTimesServed = cachedResponse['times_served']; + console.log(currentTimesServed); const responseToUse = isFullAnalysis ? cachedResponse['full_analysis'] : cachedResponse['simple_analysis']; if (responseToUse) { const cachedJSON = JSON.parse(responseToUse); @@ -107,6 +109,9 @@ module.exports = (screenName, config, index = { return fullAnalysisRet; } + cachedResponse.times_served = currentTimesServed + 1; + await cachedResponse.save(); + resolve(cachedJSON); return cachedJSON; } diff --git a/src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js b/src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js index efff93b..e50557e 100644 --- a/src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js +++ b/src/infra/database/migrations/20210430120540-remove-tables-and-add-cache.js @@ -39,6 +39,12 @@ module.exports = { allowNull: true, type: Sequelize.DATE, }, + + times_served: { + allowNull: false, + type: Sequelize.INTEGER, + defaultValue: 0 + }, }); }, diff --git a/src/infra/database/models/cache.js b/src/infra/database/models/cache.js index 0435b96..891e231 100644 --- a/src/infra/database/models/cache.js +++ b/src/infra/database/models/cache.js @@ -21,7 +21,12 @@ const CacheModel = class Cache extends Model { full_analysis: { allowNull: true, type: DataTypes.TEXT, - } + }, + times_served: { + allowNull: false, + type: DataTypes.INTEGER, + defaultValue: 0 + }, }, { sequelize, modelName: 'cache', From 978e2b185645c7643a47d80a1df40584dabdffc7 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 30 Apr 2021 21:00:17 -0300 Subject: [PATCH 05/14] Updating README --- README.md | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/README.md b/README.md index e1779b9..e8c053c 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,187 @@ DATABASE_NAME="pegabot" 5. Run migrations with `sequelize-cli db:migrate` +## Endpoints + +### `/botometer` +Get analysis with only main indexes. + +**Method**: `GET` + +**Example**: `/botometer?profile=twitter&search_for=profile` + + +**Required parameters** +- `profile` (STRING): user handle that will be analyzed; + +- `search_for` (STRING): only one value allowed `profile` + +**Response** +``` +{ + "metadata": { + "count": 1 + }, + "profiles": [ + { + "username": "twitter", + "url": "https://twitter.com/twitter", + "avatar": "http://pbs.twimg.com/profile_images/1354479643882004483/Btnfm47p_normal.jpg", + "language_dependent": { + "sentiment": { + "value": 0.57 + } + }, + "language_independent": { + "friend": null, + "temporal": 0.32269754443513865, + "network": 0.4103015075376884, + "user": 0 + }, + "bot_probability": { + "all": 0.18614272171040389, + "info": "

Um dos critérios que mais teve peso na análise foi o índice de Perfil

" + }, + "user_profile_language": null + } + ] +} +``` + +### `/analyze` +Get analysis with main indexes and subindexes. + +**Method**: `GET` + +**Example**: `/analyze?profile=twitter&search_for=profile` + + +**Required parameters** +- `profile` (STRING): user handle that will be analyzed; + +- `search_for` (STRING): only one value allowed `profile` + +**Response** +``` +{ + "root": { + "profile": { + "handle": "twitter", + "link": "https://twitter.com/twitter", + "label": "Perfil", + "description": "

Algumas das informações públicas dos perfis consideradas na análise do PEGABOT são o nome do perfil do usuário, e quantos caracteres ele possui, quantidade de perfis seguidos (following) e seguidores (followers), texto da descrição do perfil, número de postagens (tweets) e favoritos. Após coletar as informações, os algoritmos do PEGABOT processam e transformam os dados recebidos em variáveis que compõem o cálculo final de probabilidade.

", + "figure": "", + "analyses": [ + { + "title": "SELO DE VERIFICAÇÃO", + "description": "A presença do selo de verificação oferecido pelo Twitter influencia positivamente nos resultados, uma vez que a plataforma possui um procedimento manual para validar a identidade desses usuários.", + "summary": "

Usuário verificado

", + "conclusion": 0 + } + ] + }, + "network": { + "description": "

O algoritmo do PegaBot coleta uma amostra da linha do tempo do usuário, identificando hashtags utilizadas e menções ao perfil para realizar suas análises. O objetivo é identificar características de distribuição de informação na rede da conta analisada.

O índice de rede avalia se o perfil possui uma frequência alta de repetições de menções e hashtags. No caso de um bot de spams, geralmente se usam as mesmas hashtags/menções, e é isso que esse índice observa. Por exemplo, se 50 hashtags são usadas e são 50 hashtags diferentes, não é suspeito, mas se só uma hashtag é usada 100% das vezes, então é muito suspeito.

", + "label": "Rede", + "analyses": [ + { + "title": "DISTRIBUIÇÃO DAS HASHTAGS", + "description": "

Calcula o tamanho da distribuição dessas hashtags na rede. Ou seja, avalia se a utilização de hashtags do perfil apresenta uma frequência anormal.

Quanto mais próximo de 0% menor a probabilidade de ser um comportamento de bot.

", + "summary": "

Total: 10. Únicas: 3

", + "conclusion": "0.70", + "hashtags": [] + }, + { + "title": "DISTRIBUIÇÃO DAS MENÇÕES", + "description": "

Calcula o tamanho da distribuição de menções ao perfil do usuário na rede. Ou seja, avalia as menções realizadas pelo perfil com base em sua frequência.

Quanto mais próximo de 0% menor a probabilidade de ser um comportamento de bot.

", + "summary": "

Total: 14. Únicas: 14

", + "conclusion": "0.00", + "mentions": [] + }, + { + "title": "HASHTAGS E MENÇÕES", + "description": "HASHTAGS E MENÇÕES", + "summary": "

Calculamos o score distríbuido (0.35) e o tamanho da rede (0.06)

", + "conclusion": "0.41", + "stats": [] + } + ] + }, + "emotions": { + "description": "

Após coletar os dados, os algoritmos do PEGABOT fornecem uma pontuação, em uma escada de -5 a 5m de cada uma das palavras dos tweets coletados. A classificação se baseia em uma biblioteca, onde, cada uma das palavras possui uma pontuação, sendo considerada mais ou menos negativa, positiva ou neutra. Assim, ao final da classificação, calcula-se a pontuação média para a quantidade de palavras positivas, negativas e neutras utilizadas pelo usuário.

", + "label": "Sentimentos", + "analyses": [ + { + "summary": "

o perfil tem pontuação de 0.57, classificando-se como positivo.

", + "conclusion": 0.57, + "samples": { + "title": "VEJA AQUI O EXEMPLO DE 3 TWEETS DO USUÁRIO", + "list": [] + } + } + ] + } + } +} +``` + +### `/botometer-buld` +Bulk process for first endpoint. +**The params for this request must be sent as JSON** + +**Method**: `GET` +**Example**: `/botometer-bulk` + +**Required headers** +- apiKey (string): Must be set on the `.env` file, with the `BULK_API_KEY` key; + +**Required parameters** +- `profiles` (STRING ARRAY): Array containing the profiles that will be analyzed. + - **MAX ARRAY SIZE**: 50 + +**Optional parameters** + +- `is_admin` (BOOLEAN): used to identify requests that are coming from the Pegabot admin interface; +- `twitter_api_consumer_key` (STRING): Consumer key that will be used to instantiate a Twitter API client; + - **Must be sent alongside `twitter_api_consumer_secret`** +- `twitter_api_consumer_secret` (STRING): Consumer key that will be used to instantiate a Twitter API client; + - **Must be sent alongside `twitter_api_consumer_key`** + +**Response** +``` +{ + "analyses_count": 1, + "analyses": [ + { + "twitter_user_data": { + "id": "111111111111111111111111111", + "handle": "@twitter", + "user_name": "Twitter", + "url": "https://twitter.com/twitter", + "avatar": "https://twitter.com", + "created_at": "Thu Apr 09 21:32:54 +0000 2020" + }, + "twitter_user_meta_data": { + "tweet_count": 2176, + "follower_count": 304, + "following_count": 537, + "hashtags": [], + "mentions": [] + }, + "pegabot_analysis": { + "user_index": 0.2755072802592182, + "temporal_index": 0.09934108527131784, + "network_index": 0.7706060606060606, + "sentiment_index": 0.6, + "bot_probability": 0.34909088522731935 + }, + "metadata": { + "used_cache": true + } + } + ] +} +``` **spottingbot is a project inspired by [Botometer](https://botometer.iuni.iu.edu/#!/), an [OSoMe](https://osome.iuni.iu.edu/) project.** From 363840f1f0d1c91d2aaf302da70ffa730aeb1fd4 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 30 Apr 2021 21:00:17 -0300 Subject: [PATCH 06/14] Updating README --- README.md | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/README.md b/README.md index e1779b9..e8c053c 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,187 @@ DATABASE_NAME="pegabot" 5. Run migrations with `sequelize-cli db:migrate` +## Endpoints + +### `/botometer` +Get analysis with only main indexes. + +**Method**: `GET` + +**Example**: `/botometer?profile=twitter&search_for=profile` + + +**Required parameters** +- `profile` (STRING): user handle that will be analyzed; + +- `search_for` (STRING): only one value allowed `profile` + +**Response** +``` +{ + "metadata": { + "count": 1 + }, + "profiles": [ + { + "username": "twitter", + "url": "https://twitter.com/twitter", + "avatar": "http://pbs.twimg.com/profile_images/1354479643882004483/Btnfm47p_normal.jpg", + "language_dependent": { + "sentiment": { + "value": 0.57 + } + }, + "language_independent": { + "friend": null, + "temporal": 0.32269754443513865, + "network": 0.4103015075376884, + "user": 0 + }, + "bot_probability": { + "all": 0.18614272171040389, + "info": "

Um dos critérios que mais teve peso na análise foi o índice de Perfil

" + }, + "user_profile_language": null + } + ] +} +``` + +### `/analyze` +Get analysis with main indexes and subindexes. + +**Method**: `GET` + +**Example**: `/analyze?profile=twitter&search_for=profile` + + +**Required parameters** +- `profile` (STRING): user handle that will be analyzed; + +- `search_for` (STRING): only one value allowed `profile` + +**Response** +``` +{ + "root": { + "profile": { + "handle": "twitter", + "link": "https://twitter.com/twitter", + "label": "Perfil", + "description": "

Algumas das informações públicas dos perfis consideradas na análise do PEGABOT são o nome do perfil do usuário, e quantos caracteres ele possui, quantidade de perfis seguidos (following) e seguidores (followers), texto da descrição do perfil, número de postagens (tweets) e favoritos. Após coletar as informações, os algoritmos do PEGABOT processam e transformam os dados recebidos em variáveis que compõem o cálculo final de probabilidade.

", + "figure": "", + "analyses": [ + { + "title": "SELO DE VERIFICAÇÃO", + "description": "A presença do selo de verificação oferecido pelo Twitter influencia positivamente nos resultados, uma vez que a plataforma possui um procedimento manual para validar a identidade desses usuários.", + "summary": "

Usuário verificado

", + "conclusion": 0 + } + ] + }, + "network": { + "description": "

O algoritmo do PegaBot coleta uma amostra da linha do tempo do usuário, identificando hashtags utilizadas e menções ao perfil para realizar suas análises. O objetivo é identificar características de distribuição de informação na rede da conta analisada.

O índice de rede avalia se o perfil possui uma frequência alta de repetições de menções e hashtags. No caso de um bot de spams, geralmente se usam as mesmas hashtags/menções, e é isso que esse índice observa. Por exemplo, se 50 hashtags são usadas e são 50 hashtags diferentes, não é suspeito, mas se só uma hashtag é usada 100% das vezes, então é muito suspeito.

", + "label": "Rede", + "analyses": [ + { + "title": "DISTRIBUIÇÃO DAS HASHTAGS", + "description": "

Calcula o tamanho da distribuição dessas hashtags na rede. Ou seja, avalia se a utilização de hashtags do perfil apresenta uma frequência anormal.

Quanto mais próximo de 0% menor a probabilidade de ser um comportamento de bot.

", + "summary": "

Total: 10. Únicas: 3

", + "conclusion": "0.70", + "hashtags": [] + }, + { + "title": "DISTRIBUIÇÃO DAS MENÇÕES", + "description": "

Calcula o tamanho da distribuição de menções ao perfil do usuário na rede. Ou seja, avalia as menções realizadas pelo perfil com base em sua frequência.

Quanto mais próximo de 0% menor a probabilidade de ser um comportamento de bot.

", + "summary": "

Total: 14. Únicas: 14

", + "conclusion": "0.00", + "mentions": [] + }, + { + "title": "HASHTAGS E MENÇÕES", + "description": "HASHTAGS E MENÇÕES", + "summary": "

Calculamos o score distríbuido (0.35) e o tamanho da rede (0.06)

", + "conclusion": "0.41", + "stats": [] + } + ] + }, + "emotions": { + "description": "

Após coletar os dados, os algoritmos do PEGABOT fornecem uma pontuação, em uma escada de -5 a 5m de cada uma das palavras dos tweets coletados. A classificação se baseia em uma biblioteca, onde, cada uma das palavras possui uma pontuação, sendo considerada mais ou menos negativa, positiva ou neutra. Assim, ao final da classificação, calcula-se a pontuação média para a quantidade de palavras positivas, negativas e neutras utilizadas pelo usuário.

", + "label": "Sentimentos", + "analyses": [ + { + "summary": "

o perfil tem pontuação de 0.57, classificando-se como positivo.

", + "conclusion": 0.57, + "samples": { + "title": "VEJA AQUI O EXEMPLO DE 3 TWEETS DO USUÁRIO", + "list": [] + } + } + ] + } + } +} +``` + +### `/botometer-buld` +Bulk process for first endpoint. +**The params for this request must be sent as JSON** + +**Method**: `GET` +**Example**: `/botometer-bulk` + +**Required headers** +- apiKey (string): Must be set on the `.env` file, with the `BULK_API_KEY` key; + +**Required parameters** +- `profiles` (STRING ARRAY): Array containing the profiles that will be analyzed. + - **MAX ARRAY SIZE**: 50 + +**Optional parameters** + +- `is_admin` (BOOLEAN): used to identify requests that are coming from the Pegabot admin interface; +- `twitter_api_consumer_key` (STRING): Consumer key that will be used to instantiate a Twitter API client; + - **Must be sent alongside `twitter_api_consumer_secret`** +- `twitter_api_consumer_secret` (STRING): Consumer key that will be used to instantiate a Twitter API client; + - **Must be sent alongside `twitter_api_consumer_key`** + +**Response** +``` +{ + "analyses_count": 1, + "analyses": [ + { + "twitter_user_data": { + "id": "111111111111111111111111111", + "handle": "@twitter", + "user_name": "Twitter", + "url": "https://twitter.com/twitter", + "avatar": "https://twitter.com", + "created_at": "Thu Apr 09 21:32:54 +0000 2020" + }, + "twitter_user_meta_data": { + "tweet_count": 2176, + "follower_count": 304, + "following_count": 537, + "hashtags": [], + "mentions": [] + }, + "pegabot_analysis": { + "user_index": 0.2755072802592182, + "temporal_index": 0.09934108527131784, + "network_index": 0.7706060606060606, + "sentiment_index": 0.6, + "bot_probability": 0.34909088522731935 + }, + "metadata": { + "used_cache": true + } + } + ] +} +``` **spottingbot is a project inspired by [Botometer](https://botometer.iuni.iu.edu/#!/), an [OSoMe](https://osome.iuni.iu.edu/) project.** From a63c924d007f9b196e071b39a91ed870cd936303 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Tue, 4 May 2021 11:59:23 -0300 Subject: [PATCH 07/14] =?UTF-8?q?Fix:=20utilizando=20id=20ao=20inv=C3=A9s?= =?UTF-8?q?=20de=20id=5Fstr=20ao=20criar=20analise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/analyze.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/analyze.js b/src/analyze.js index 6e8d0f7..94e581a 100644 --- a/src/analyze.js +++ b/src/analyze.js @@ -94,12 +94,12 @@ module.exports = (screenName, config, index = { '$analysis.createdAt$': { [Op.between]: [cacheInterval, new Date()] }, }, include: 'analysis' - }) + }); if (cachedResponse) { const currentTimesServed = cachedResponse['times_served']; - console.log(currentTimesServed); const responseToUse = isFullAnalysis ? cachedResponse['full_analysis'] : cachedResponse['simple_analysis']; + if (responseToUse) { const cachedJSON = JSON.parse(responseToUse); @@ -352,7 +352,7 @@ module.exports = (screenName, config, index = { sentiment: sentimentScore, temporal: temporalScore, network: networkScore, - twitter_user_id: data.user_id, + twitter_user_id: user.id, twitter_handle: param.screen_name, twitter_created_at: data.created_at, twitter_following_count: data.following, From fc79cabc5a8a1f4eb748c6bd3c447b39b9ae8348 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Tue, 4 May 2021 12:02:05 -0300 Subject: [PATCH 08/14] Mudando para id_str --- src/analyze.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/analyze.js b/src/analyze.js index 94e581a..3b35ffe 100644 --- a/src/analyze.js +++ b/src/analyze.js @@ -90,7 +90,7 @@ module.exports = (screenName, config, index = { const cachedResponse = await Cache.findOne({ attributes: ['simple_analysis', 'full_analysis', 'times_served', 'id'], where: { - '$analysis.twitter_user_id$': user.id, + '$analysis.twitter_user_id$': user.id_str, '$analysis.createdAt$': { [Op.between]: [cacheInterval, new Date()] }, }, include: 'analysis' @@ -352,7 +352,7 @@ module.exports = (screenName, config, index = { sentiment: sentimentScore, temporal: temporalScore, network: networkScore, - twitter_user_id: user.id, + twitter_user_id: user.id_str, twitter_handle: param.screen_name, twitter_created_at: data.created_at, twitter_following_count: data.following, From b9a04fc11da8c370f1383fcdf083c1608ddcd215 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Tue, 4 May 2021 12:57:45 -0300 Subject: [PATCH 09/14] Fix: flag de origem da analise --- src/analyze.js | 2 +- src/app.js | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/analyze.js b/src/analyze.js index 3b35ffe..c5050fe 100644 --- a/src/analyze.js +++ b/src/analyze.js @@ -358,7 +358,7 @@ module.exports = (screenName, config, index = { twitter_following_count: data.following, twitter_followers_count: data.followers, twitter_status_count: data.number_tweets, - origin: 'foo' + origin: origin }).then((res) => res.dataValues); // Saving cache diff --git a/src/app.js b/src/app.js index da1e136..4e9b247 100644 --- a/src/app.js +++ b/src/app.js @@ -127,7 +127,6 @@ app.get('/botometer', async (req, res) => { const { getData } = req.query; - console.log('profile', profile); if (!limit || limit > 200) { limit = 200; } @@ -140,8 +139,6 @@ app.get('/botometer', async (req, res) => { const result = await spottingbot(profile, config, { friend: false }, sentimentLang, getData, cacheInterval, verbose, origin, wantsDocument, lang).catch((err) => err); - if (result && result.profiles && result.profiles[0]) console.log(result.profiles[0]); - if (!result || result.errors || result.error) { let toSend = result; if (result.errors) toSend = result.errors; @@ -273,7 +270,6 @@ app.get('/analyze', async (req, res) => { const { getData } = req.query; - console.log('profile', profile); if (!limit || limit > 200) { limit = 200; } @@ -337,7 +333,7 @@ app.get('/botometer-bulk', async (req, res) => { if (!apiKey || apiKey != process.env.BULK_API_KEY) return res.status(403).send('forbidden'); - const { profiles, is_admin, twitter_api_consumer_key, twitter_api_consumer_secret } = req.body; + const { profiles, is_admin, twitter_api_consumer_key, twitter_api_consumer_secret, cache_interval } = req.body; if (profiles.length > 50) return res.status(400).json({ message: 'max profiles size is 50' }); if (twitter_api_consumer_key && twitter_api_consumer_secret) { @@ -347,14 +343,16 @@ app.get('/botometer-bulk', async (req, res) => { }; } + const verbose = false; const getData = true; + const origin = 'admin'; + const cacheInterval = cache_interval; const referer = req.get('referer'); - const origin = is_admin ? 'admin' : 'website'; const profiles_results = profiles.map(profile => { return spottingbot( - profile, config, { friend: false }, library.getDefaultLanguage(referer), getData, origin + profile, config, { friend: false }, library.getDefaultLanguage(referer), getData, cacheInterval, verbose, origin ).then((result) => { return { twitter_user_data: { From c984d1f4d6d8296b915594e4ab8a1725c1e573e4 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Tue, 4 May 2021 13:55:13 -0300 Subject: [PATCH 10/14] Atualizando coluna para twitter_tweets_count --- src/analyze.js | 2 +- .../20210504164658-twitter_tweets_count.js | 13 +++++++++++++ src/infra/database/models/analyses.js | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/infra/database/migrations/20210504164658-twitter_tweets_count.js diff --git a/src/analyze.js b/src/analyze.js index c5050fe..c74624f 100644 --- a/src/analyze.js +++ b/src/analyze.js @@ -357,7 +357,7 @@ module.exports = (screenName, config, index = { twitter_created_at: data.created_at, twitter_following_count: data.following, twitter_followers_count: data.followers, - twitter_status_count: data.number_tweets, + twitter_tweets_count: data.number_tweets, origin: origin }).then((res) => res.dataValues); diff --git a/src/infra/database/migrations/20210504164658-twitter_tweets_count.js b/src/infra/database/migrations/20210504164658-twitter_tweets_count.js new file mode 100644 index 0000000..24a2f17 --- /dev/null +++ b/src/infra/database/migrations/20210504164658-twitter_tweets_count.js @@ -0,0 +1,13 @@ +'use strict'; + +const { query } = require("express"); + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.renameColumn('Analyses', 'twitter_status_count', 'twitter_tweets_count'); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.renameColumn('Analyses', 'twitter_tweets_count', 'twitter_status_count'); + } +}; diff --git a/src/infra/database/models/analyses.js b/src/infra/database/models/analyses.js index 0f5acbf..d2a6ff5 100644 --- a/src/infra/database/models/analyses.js +++ b/src/infra/database/models/analyses.js @@ -53,7 +53,7 @@ export default class Analysis extends Model { allowNull: true, type: DataTypes.INTEGER, }, - twitter_status_count: { + twitter_tweets_count: { allowNull: true, type: DataTypes.INTEGER, }, From 0936574156ec1fdc3df54f5a424dbe3d86ed8c57 Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Tue, 4 May 2021 13:58:45 -0300 Subject: [PATCH 11/14] Atualizando package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 58ddba1..655e3b3 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Analyzing profile on Twitter for detect behavior of spamming bot", "homepage": "https://github.com/AppCivico/spottingbot#readme", "scripts": { - "start": "babel-node src/index.js", + "start": "npx babel src -d build; node build/index.js", "dev": "nodemon src/index.js --exec babel-node", "lint": "eslint . --ext .js", "cli": "babel-node src/cli.js", From 019aad3ee4056e4458edd52b66dd768ea7ec7bdb Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Tue, 4 May 2021 17:51:00 -0300 Subject: [PATCH 12/14] Atualizando doc e env.example --- README.md | 276 +++++++++++++++++++++++++++++----------------------- example.env | 2 + 2 files changed, 158 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index e8c053c..e280bfb 100644 --- a/README.md +++ b/README.md @@ -26,127 +26,23 @@ TWITTER_CONSUMER_KEY="Your application consumer key" TWITTER_CONSUMER_SECRET="Your application consumer secret" TWITTER_ACCESS_TOKEN_KEY="Your application access token key, only for user authentication" TWITTER_ACCESS_TOKEN_SECRET="Your application access token secret, only for user authentication" -``` - -*Both User and App-only authentication are supported, for App-only, the Bearer token will be automatically requested* - -#### Start - -`npm start username` - -*or* - -`source/cli.js username` - -*`username` has to be replaced by the profile to analyze* - -#### Install bin locally on your system - -`npm link` *sudo might be necessary* - -*Then* - -`spottingbot username` - -### Module - -#### Call - -```js -const spottingbot = require('spottingbot'); - -spottingbot(username, index); -``` - -`username` is a string that contains the screen name of the Twitter profile to analyze. - -`twitter_config` is an object that contains Twitter credentials, both User and App-only authentication are supported, for App-only, the Bearer token will be automatically requested, the `twitter_config` object should be like this: - -```js -{ - consumer_key: "Your application consumer key", - consumer_secret: "Your application consumer secret", - access_token_key: "Your application access token key", // Only for User authentication - access_token_secret: "Your application access token secret" // Only for User authentication -} -``` - -`index` is used to disable some indexes, it's an object that looks like this: -```js -{ - user: true, - friend: true, - temporal: true, - network: true -} -``` - -By default, or if omitted, everything is `true`. - -To disable only one index, it isn't necessary to add the others keys in the object, `{friend: false}`, will work. - -#### Return value -*spottingbot* handles both *callback* style and *node promise* style +PORT="The port that the app will attach itself" -##### Callback +USE_CACHE="Flag that will control the cache usage. Must be either 0 or 1. +DEFAULT_CACHE_INTERVAL='The interval that the cache will be considered valid. Must be a valid Postgresql format, e.g: '30_days'." -```js -spottingbot(username, twitter_config, index, function(error, result) { - if (error) { - // Handle error - return; - } - // Do something with result -}) -``` - -##### Promise - -```js -spottingbot(username, twitter_config, index) - .then(result => { - // Do something with result - }) - .catch(error => { - // Handle error - }) -``` +DATABASE_HOST="Your Postgresql host" +DATABASE_USER="Your database user" +DATABASE_PASSWORD="Your database password" +DATABASE_NAME="Your database password" -##### Value +BULK_API_KEY='Key that will be used to authorize calls to the bulk endpoint' -The return value is an object that contains +PUPPETER_SERVICE_ROOT_URL="Url to the service that will take screenshots from the Twitter profile" +PUPPETER_SECRET_TOKEN="Token for the screenshot service" -```js -{ - metadata: { - count: 1 // Always 1 for now - }, - profiles: [ - { - username: 'screen_name', - url: 'https://twitter.com/screen_name', - avatar: 'image link', - language_dependent: { - sentiment: { - value: 0.65 - } - }, - language_independent: { - friend: 0.19, - temporal: 0.37, - network: 0.95, - user: 0 - }, - bot_probability: { - all: 0.37 - }, - user_profile_language: 'en', - } - ] -} ``` - ##### PSQL database Pegabot uses a cache system to avoid remaking an analysis on the same user. @@ -168,6 +64,8 @@ DATABASE_NAME="pegabot" 4. Install the npm module `sequelize-cli` 5. Run migrations with `sequelize-cli db:migrate` +## Starting the API +You can run the command: `npm run dev` in order to start with nodemon and Babel. Or you can start with `npm run start`, which will build the application and run from a `/build` folder without Babel. ## Endpoints @@ -176,7 +74,7 @@ Get analysis with only main indexes. **Method**: `GET` -**Example**: `/botometer?profile=twitter&search_for=profile` +**Example**: `curl --request GET --url 'http://127.0.0.1:5010/botometer?profile=twitter&search_for=profile&is_admin=true'` **Required parameters** @@ -184,6 +82,9 @@ Get analysis with only main indexes. - `search_for` (STRING): only one value allowed `profile` +**Optional parameters** +- `is_admin` (BOOLEAN): used to identify requests that are coming from the Pegabot admin interface; (1|0 OR true|false) + **Response** ``` { @@ -221,7 +122,7 @@ Get analysis with main indexes and subindexes. **Method**: `GET` -**Example**: `/analyze?profile=twitter&search_for=profile` +**Example**: `curl --request GET --url 'http://127.0.0.1:5010/analyze?profile=twitter&search_for=profile'` **Required parameters** @@ -293,15 +194,28 @@ Get analysis with main indexes and subindexes. } ``` -### `/botometer-buld` +### `/botometer-bulk` Bulk process for first endpoint. **The params for this request must be sent as JSON** **Method**: `GET` -**Example**: `/botometer-bulk` + +**Example**: +``` +curl --request GET \ + --url http://127.0.0.1:5010/botometer-bulk \ + --header 'Content-Type: application/json' \ + --header 'X-API-KEY: $ENV{BULK_API_KEY}' \ + --data '{ + "is_admin": true, + "profiles": [ + "twitter" + ] +}' +``` **Required headers** -- apiKey (string): Must be set on the `.env` file, with the `BULK_API_KEY` key; +- `apiKey` (string): Must be set on the `.env` file, with the `BULK_API_KEY` name; **Required parameters** - `profiles` (STRING ARRAY): Array containing the profiles that will be analyzed. @@ -309,7 +223,7 @@ Bulk process for first endpoint. **Optional parameters** -- `is_admin` (BOOLEAN): used to identify requests that are coming from the Pegabot admin interface; +- `is_admin` (BOOLEAN): used to identify requests that are coming from the Pegabot admin interface; (1|0 OR true|false) - `twitter_api_consumer_key` (STRING): Consumer key that will be used to instantiate a Twitter API client; - **Must be sent alongside `twitter_api_consumer_secret`** - `twitter_api_consumer_secret` (STRING): Consumer key that will be used to instantiate a Twitter API client; @@ -351,6 +265,128 @@ Bulk process for first endpoint. } ``` + +## Using as a module +*Both User and App-only authentication are supported, for App-only, the Bearer token will be automatically requested* +#### Start + +`npm start username` + +*or* + +`source/cli.js username` + +*`username` has to be replaced by the profile to analyze* + + +#### Install bin locally on your system + +`npm link` *sudo might be necessary* + +*Then* + +`spottingbot username` + +### Module + +#### Call + +```js +const spottingbot = require('spottingbot'); + +spottingbot(username, index); +``` + +`username` is a string that contains the screen name of the Twitter profile to analyze. + +`twitter_config` is an object that contains Twitter credentials, both User and App-only authentication are supported, for App-only, the Bearer token will be automatically requested, the `twitter_config` object should be like this: + +```js +{ + consumer_key: "Your application consumer key", + consumer_secret: "Your application consumer secret", + access_token_key: "Your application access token key", // Only for User authentication + access_token_secret: "Your application access token secret" // Only for User authentication +} +``` + +`index` is used to disable some indexes, it's an object that looks like this: +```js +{ + user: true, + friend: true, + temporal: true, + network: true +} +``` + +By default, or if omitted, everything is `true`. + +To disable only one index, it isn't necessary to add the others keys in the object, `{friend: false}`, will work. + +#### Return value + +*spottingbot* handles both *callback* style and *node promise* style + +##### Callback + +```js +spottingbot(username, twitter_config, index, function(error, result) { + if (error) { + // Handle error + return; + } + // Do something with result +}) +``` + +##### Promise + +```js +spottingbot(username, twitter_config, index) + .then(result => { + // Do something with result + }) + .catch(error => { + // Handle error + }) +``` + +##### Value + +The return value is an object that contains + +```js +{ + metadata: { + count: 1 // Always 1 for now + }, + profiles: [ + { + username: 'screen_name', + url: 'https://twitter.com/screen_name', + avatar: 'image link', + language_dependent: { + sentiment: { + value: 0.65 + } + }, + language_independent: { + friend: 0.19, + temporal: 0.37, + network: 0.95, + user: 0 + }, + bot_probability: { + all: 0.37 + }, + user_profile_language: 'en', + } + ] +} +``` + + **spottingbot is a project inspired by [Botometer](https://botometer.iuni.iu.edu/#!/), an [OSoMe](https://osome.iuni.iu.edu/) project.** **This project is part of the [PegaBot](http://www.pegabot.com.br) initiative.** diff --git a/example.env b/example.env index f0d56af..8d3187b 100644 --- a/example.env +++ b/example.env @@ -23,5 +23,7 @@ DATABASE_USER="postgres" DATABASE_PASSWORD="" DATABASE_NAME="pegabot" +BULK_API_KEY='' + PUPPETER_SERVICE_ROOT_URL="" PUPPETER_SECRET_TOKEN="" \ No newline at end of file From 9428e44e3e29bc516ff7f6da42784aca2779ecfb Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 16 Apr 2021 15:00:54 -0300 Subject: [PATCH 13/14] Return url without fetching --- src/library.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/library.js b/src/library.js index 630e42c..8028006 100644 --- a/src/library.js +++ b/src/library.js @@ -323,7 +323,34 @@ export default { }); }, - buildAnalyzeReturn: async (extraDetails, lang, analysis_id) => { + // sub puppetter_signed_url { + // my %config = @_; + + // my $secret = $ENV{PUPPETER_SECRET_TOKEN}; + // my $host = $ENV{PUPPETER_SERVICE_ROOT_URL}; + + // die 'missing PUPPETER_SECRET_TOKEN' if !$secret; + // die 'missing PUPPETER_SERVICE_ROOT_URL' if !$host; + // die 'invalid width' if $config{w} !~ /^\d+$/a; + // die 'invalid height' if exists $config{h} && $config{h} !~ /^\d+$/a; + // die 'invalid resize width' if exists $config{rw} && $config{rw} !~ /^\d+$/a; + // die 'invalid url' if $config{u} !~ /^http/i; + + // my $my_url = Mojo::URL->new($host); + + // my $calcBuffer = $secret . "\n"; + // for my $field (keys %config) { + // $calcBuffer .= $field . '=' . $config{$field} . "\n"; + // $my_url->query->merge($field, $config{$field}); + // } + + // my $calcSecret = md5_hex($calcBuffer); + // $my_url->query->merge('a', $calcSecret); + + // return $my_url . ''; + // } + + buildAnalyzeReturn: async (extraDetails, lang) => { // Setting text file let texts; if (/es-mx/.test(lang)) { From 39bb4a05331881c275053994022068f8ed67509a Mon Sep 17 00:00:00 2001 From: Lucas Ansei Date: Fri, 16 Apr 2021 15:08:44 -0300 Subject: [PATCH 14/14] Updating urls on tweets --- src/library.js | 99 ++++++++++++-------------------------------------- 1 file changed, 24 insertions(+), 75 deletions(-) diff --git a/src/library.js b/src/library.js index 8028006..505e3a6 100644 --- a/src/library.js +++ b/src/library.js @@ -323,33 +323,6 @@ export default { }); }, - // sub puppetter_signed_url { - // my %config = @_; - - // my $secret = $ENV{PUPPETER_SECRET_TOKEN}; - // my $host = $ENV{PUPPETER_SERVICE_ROOT_URL}; - - // die 'missing PUPPETER_SECRET_TOKEN' if !$secret; - // die 'missing PUPPETER_SERVICE_ROOT_URL' if !$host; - // die 'invalid width' if $config{w} !~ /^\d+$/a; - // die 'invalid height' if exists $config{h} && $config{h} !~ /^\d+$/a; - // die 'invalid resize width' if exists $config{rw} && $config{rw} !~ /^\d+$/a; - // die 'invalid url' if $config{u} !~ /^http/i; - - // my $my_url = Mojo::URL->new($host); - - // my $calcBuffer = $secret . "\n"; - // for my $field (keys %config) { - // $calcBuffer .= $field . '=' . $config{$field} . "\n"; - // $my_url->query->merge($field, $config{$field}); - // } - - // my $calcSecret = md5_hex($calcBuffer); - // $my_url->query->merge('a', $calcSecret); - - // return $my_url . ''; - // } - buildAnalyzeReturn: async (extraDetails, lang) => { // Setting text file let texts; @@ -635,22 +608,14 @@ export default { const calcSecret = md5Hex(calcBuffer); - const pictureUrl = await (async () => { - try { - const res = await superagent - .get(puppetterUrl) - .query({ - u: '' + tweetNeutral.url, - w: 480, - h: 520, - a: calcSecret, - }); - - return res.request.url; - } catch (err) { - console.error(err); - } - })(); + const opts = { + u: '' + tweetNeutral.url, + w: 480, + h: 520, + a: calcSecret, + }; + const queryString = new URLSearchParams(opts).toString(); + const pictureUrl = `${puppetterUrl}?${queryString}`; tweetSamples.push({ caption: "Exemplo de tweet neutro", @@ -666,22 +631,14 @@ export default { const calcSecret = md5Hex(calcBuffer); - const pictureUrl = await (async () => { - try { - const res = await superagent - .get(puppetterUrl) - .query({ - u: '' + tweetPositive.url, - w: 480, - h: 520, - a: calcSecret, - }); - - return res.request.url; - } catch (err) { - console.error(err); - } - })(); + const opts = { + u: '' + tweetPositive.url, + w: 480, + h: 520, + a: calcSecret, + }; + const queryString = new URLSearchParams(opts).toString(); + const pictureUrl = `${puppetterUrl}?${queryString}`; tweetSamples.push({ caption: "Exemplo de tweet positivo", @@ -697,22 +654,14 @@ export default { const calcSecret = md5Hex(calcBuffer); - const pictureUrl = await (async () => { - try { - const res = await superagent - .get(puppetterUrl) - .query({ - u: '' + tweetNegative.url, - w: 480, - h: 520, - a: calcSecret, - }); - - return res.request.url; - } catch (err) { - console.error(err); - } - })(); + const opts = { + u: '' + tweetNegative.url, + w: 480, + h: 520, + a: calcSecret, + }; + const queryString = new URLSearchParams(opts).toString(); + const pictureUrl = `${puppetterUrl}?${queryString}`; tweetSamples.push({ caption: "Exemplo de tweet negativo",