Skip to content

Commit

Permalink
Merge pull request #1533 from filipedeschamps/read-votes-others
Browse files Browse the repository at this point in the history
[Moderação] Possibilidade de ver `usernames` no grafo de qualificações
  • Loading branch information
aprendendofelipe authored Oct 14, 2023
2 parents 882d965 + df6cef6 commit 85d6e91
Show file tree
Hide file tree
Showing 8 changed files with 508 additions and 79 deletions.
2 changes: 2 additions & 0 deletions infra/scripts/seed-database.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ async function seedDevelopmentUsers() {
'create:migration',
'update:content:others',
'create:recovery_token:username',
'read:votes:others',
'read:user:list',
]);
await insertUser('user', 'user@user.com', '$2a$04$v0hvAu/y6pJ17LzeCfcKG.rDStO9x5ficm2HTLZIfeDBG8oR/uQXi', [
'create:session',
Expand Down
116 changes: 80 additions & 36 deletions models/analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,20 @@ async function getUsersCreated() {
});
}

async function getVotesGraph({ limit = 300 } = {}) {
async function getVotesGraph({ limit = 300, showUsernames = false } = {}) {
const results = await database.query({
text: `
SELECT
events.originator_user_id as from,
events.metadata->>'content_owner_id' as to,
events.metadata->>'transaction_type' as transaction_type,
events.originator_ip as ip,
events.created_at
id,
originator_user_id as from,
metadata->>'content_owner_id' as to,
metadata->>'transaction_type' as transaction_type,
originator_ip as ip,
created_at
FROM events
WHERE
events.type = 'update:content:tabcoins'
ORDER BY events.created_at DESC
type = 'update:content:tabcoins'
ORDER BY created_at DESC
LIMIT $1;
`,
values: [limit],
Expand All @@ -122,39 +123,86 @@ async function getVotesGraph({ limit = 300 } = {}) {
const usersMap = new Map();
const ipNodesMap = new Map();
const votesMap = new Map();
const ipEdgesMap = new Map();

results.rows.forEach((row) => {
const from = hashWithCache(row.from);
const to = hashWithCache(row.to);
const ip = hashWithCache(row.ip);
const from = usersMap.get(row.from)?.id || hash(row.from, row.id);
const to = usersMap.get(row.to)?.id || hash(row.to, row.id);

usersMap.set(from, { id: from, group: 'users' });
usersMap.set(to, { id: to, group: 'users' });
usersMap.set(row.from, {
id: from,
group: 'users',
votes: (usersMap.get(row.from)?.votes || 0) + 1,
});

usersMap.set(row.to, {
id: to,
group: 'users',
votes: (usersMap.get(row.to)?.votes || 0) + 1,
});

if (ipNodesMap.has(ip)) {
ipNodesMap.get(ip).add(from);
if (ipNodesMap.has(row.ip)) {
ipNodesMap.get(row.ip).add(row.from);
} else {
ipNodesMap.set(ip, new Set([from]));
ipNodesMap.set(row.ip, new Set([row.from]));
}

const fromToKey = `${row.transaction_type}-${from}-${to}`;
votesMap.set(fromToKey, {
from,
to,
arrows: 'to',
color: row.transaction_type === 'credit' ? 'green' : 'red',
value: votesMap.has(fromToKey) ? votesMap.get(fromToKey).value + 1 : 1,
type: row.transaction_type,
value: (votesMap.get(fromToKey)?.value || 0) + 1,
});
});

let ipId = 0;
const sharedIps = [];
const ipEdges = [];

Array.from(ipNodesMap.values()).forEach((users) => {
if (users.size === 1) return;

ipId += 1;

users.forEach((user) => {
const from = usersMap.get(user).id;

usersMap.set(user, {
id: from,
group: 'users',
shared: true,
votes: usersMap.get(user).votes,
});

ipEdges.push({ from, to: ipId, type: 'network' });
});

ipEdgesMap.set(`${from}-${ip}`, { from, to: ip, color: 'cyan' });
sharedIps.push({ id: ipId, group: 'IPs' });
});

const sharedIps = [...ipNodesMap]
.map((ip) => ({ id: ip[0], group: ip[1].size > 1 ? 'IPs' : undefined }))
.filter((ip) => ip.group);
const usersData = await database.query({
text: `
SELECT${showUsernames ? ` username,` : ''}
'nuked' = ANY(features) as nuked,
id as key
FROM users
WHERE
id = ANY($1)
${showUsernames ? '' : `AND 'nuked' = ANY(features)`}
;`,
values: [[...usersMap.keys()]],
});

const ipEdges = [...ipEdgesMap.values()].filter((edge) => sharedIps.some((ip) => ip.id === edge.to));
usersData.rows.forEach((row) => {
const user = usersMap.get(row.key);

usersMap.set(row.key, {
id: user.id,
group: row.nuked ? 'nuked' : 'users',
username: showUsernames && (user.votes > 2 || user.shared) ? row.username : null,
votes: user.votes,
});
});

return {
nodes: [...usersMap.values(), ...sharedIps],
Expand All @@ -166,8 +214,8 @@ async function getVotesTaken() {
const results = await database.query(`
WITH range_values AS (
SELECT date_trunc('day', NOW() - INTERVAL '2 MONTHS') as minval,
date_trunc('day', NOW()) as maxval
),
date_trunc('day', max(created_at)) as maxval
FROM events),
day_range AS (
SELECT generate_series(minval, maxval, '1 day'::interval) as date
Expand Down Expand Up @@ -204,15 +252,11 @@ export default Object.freeze({
getVotesTaken,
});

const hashCache = {};

function hashWithCache(input) {
if (hashCache[input]) return hashCache[input];
function hash(key, salt) {
if (!salt) throw new Error('Necessário "salt" para gerar "hash"');

const hash = crypto.createHash('md5');
const salt = crypto.randomBytes(16).toString('base64');
hash.update(salt + input);
const token = hash.digest('base64').slice(0, 7);
hashCache[input] = token;
return token;
hash.update(key + salt);

return hash.digest('base64').slice(0, 7);
}
11 changes: 7 additions & 4 deletions models/authorization.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ const availableFeatures = new Set([
'create:user',
'read:user',
'read:user:self',
'read:user:list',
'update:user',
'ban:user',

// MIGRATION
'read:migration',
Expand All @@ -19,7 +17,6 @@ const availableFeatures = new Set([

// RECOVERY_TOKEN
'read:recovery_token',
'create:recovery_token:username',

// EMAIL_CONFIRMATION_TOKEN
'read:email_confirmation_token',
Expand All @@ -31,12 +28,18 @@ const availableFeatures = new Set([
// CONTENT
'read:content',
'update:content',
'update:content:others',
'create:content',
'create:content:text_root',
'create:content:text_child',
'read:content:list',
'read:content:tabcoins',

// MODERATION
'read:user:list',
'read:votes:others',
'update:content:others',
'ban:user',
'create:recovery_token:username',
]);

function can(user, feature, resource) {
Expand Down
5 changes: 5 additions & 0 deletions pages/_app.public.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import { DefaultHead, UserProvider } from 'pages/interface';

async function SWRFetcher(resource, init) {
const response = await fetch(resource, init);

if (!response.ok) {
throw new Error(response.statusText);
}

const responseBody = await response.json();

return responseBody;
Expand Down
31 changes: 31 additions & 0 deletions pages/api/v1/status/votes/index.public.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { formatISO } from 'date-fns';
import nextConnect from 'next-connect';

import analytics from 'models/analytics';
import authentication from 'models/authentication';
import authorization from 'models/authorization';
import cacheControl from 'models/cache-control';
import controller from 'models/controller.js';

export default nextConnect({
attachParams: true,
onNoMatch: controller.onNoMatchHandler,
onError: controller.onErrorHandler,
})
.use(controller.injectRequestMetadata)
.use(authentication.injectAnonymousOrUser)
.use(controller.logRequest)
.use(cacheControl.noCache)
.get(authorization.canRequest('read:votes:others'), getHandler);

async function getHandler(_, response) {
const votesGraph = await analytics.getVotesGraph({
limit: 1000,
showUsernames: true,
});

return response.json({
updated_at: formatISO(Date.now()),
votesGraph,
});
}
Loading

1 comment on commit 85d6e91

@vercel
Copy link

@vercel vercel bot commented on 85d6e91 Oct 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

tabnews – ./

tabnews-git-main-tabnews.vercel.app
tabnews-tabnews.vercel.app
tabnews.com.br
www.tabnews.com.br

Please # to comment.