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",