feat: expanded action counts

This commit is contained in:
Wyatt Johnson
2018-09-20 15:13:46 -06:00
parent 8d86900376
commit 7eb1755c5e
6 changed files with 264 additions and 54 deletions
@@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`#decodeActionCounts parses the action counts correctly 1`] = `
Object {
"DONT_AGREE": 1,
"FLAG": 2,
"FLAG__COMMENT_DETECTED_BANNED_WORD": 1,
"FLAG__COMMENT_DETECTED_BODY_COUNT": 1,
"REACTION": 3,
}
`;
exports[`#decodeActionCounts parses the action counts correctly 2`] = `
Object {
"DONT_AGREE": Object {
"total": 1,
},
"FLAG": Object {
"reasons": Object {
"COMMENT_DETECTED_BANNED_WORD": 1,
"COMMENT_DETECTED_BODY_COUNT": 1,
"COMMENT_DETECTED_LINKS": 0,
"COMMENT_DETECTED_SPAM": 0,
"COMMENT_DETECTED_SUSPECT_WORD": 0,
"COMMENT_DETECTED_TOXIC": 0,
"COMMENT_DETECTED_TRUST": 0,
"COMMENT_REPORTED_OFFENSIVE": 0,
"COMMENT_REPORTED_SPAM": 0,
},
"total": 2,
},
"REACTION": Object {
"total": 3,
},
}
`;
exports[`#encodeActionCounts generates the action counts correctly 1`] = `
Object {
"DONT_AGREE": 1,
"FLAG": 2,
"FLAG__COMMENT_DETECTED_BANNED_WORD": 1,
"FLAG__COMMENT_DETECTED_BODY_COUNT": 1,
}
`;
exports[`#generateActionCounts generates the action counts correctly 1`] = `
Object {
"DONT_AGREE": 1,
"FLAG": 2,
"FLAG__COMMENT_DETECTED_BANNED_WORD": 1,
"FLAG__COMMENT_DETECTED_BODY_COUNT": 1,
}
`;
+32 -13
View File
@@ -3,11 +3,12 @@ import {
Action,
ACTION_ITEM_TYPE,
ACTION_TYPE,
generateActionCounts,
decodeActionCounts,
encodeActionCounts,
validateAction,
} from "talk-server/models/actions";
describe("#generateActionCounts", () => {
describe("#encodeActionCounts", () => {
it("generates the action counts correctly", () => {
const actions = [
{ action_type: ACTION_TYPE.DONT_AGREE },
@@ -20,18 +21,36 @@ describe("#generateActionCounts", () => {
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT,
},
];
const actionCounts = generateActionCounts(...(actions as Action[]));
const actionCounts = encodeActionCounts(...(actions as Action[]));
expect(actionCounts).toEqual({
[ACTION_TYPE.DONT_AGREE.toLowerCase()]: 1,
[ACTION_TYPE.FLAG.toLowerCase()]: 2,
[ACTION_TYPE.FLAG.toLowerCase() +
"_" +
GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD.toLowerCase()]: 1,
[ACTION_TYPE.FLAG.toLowerCase() +
"_" +
GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT.toLowerCase()]: 1,
});
expect(actionCounts).toMatchSnapshot();
});
});
describe("#decodeActionCounts", () => {
it("parses the action counts correctly", () => {
const actions = [
{ action_type: ACTION_TYPE.REACTION },
{ action_type: ACTION_TYPE.REACTION },
{ action_type: ACTION_TYPE.REACTION },
{ action_type: ACTION_TYPE.DONT_AGREE },
{
action_type: ACTION_TYPE.FLAG,
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BANNED_WORD,
},
{
action_type: ACTION_TYPE.FLAG,
reason: GQLCOMMENT_FLAG_REASON.COMMENT_DETECTED_BODY_COUNT,
},
];
const modelActionCounts = encodeActionCounts(...(actions as Action[]));
expect(modelActionCounts).toMatchSnapshot();
const actionCounts = decodeActionCounts(modelActionCounts);
expect(actionCounts).toMatchSnapshot();
});
});
+171 -34
View File
@@ -12,25 +12,37 @@ function collection(db: Db) {
return db.collection<Readonly<Action>>("actions");
}
export type ActionCounts = Record<string, number>;
export enum ACTION_TYPE {
/**
* REACTION corresponds to a reaction to a comment from a user.
*/
REACTION = "REACTION",
/**
* FLAG corresponds to a flag action that indicates that the given resource needs
* moderator attention.
*/
FLAG = "FLAG",
/**
* DONT_AGREE corresponds to when a user marks a given comment that they don't
* agree with.
*/
DONT_AGREE = "DONT_AGREE",
/**
* FLAG corresponds to a flag action that indicates that the given resource needs
* moderator attention.
*/
FLAG = "FLAG",
}
export type EncodedActionCounts = Record<string, number>;
export interface ActionCountGroup {
total: number;
}
export interface ActionCounts {
[ACTION_TYPE.REACTION]: ActionCountGroup;
[ACTION_TYPE.DONT_AGREE]: ActionCountGroup;
[ACTION_TYPE.FLAG]: ActionCountGroup & {
reasons: Record<GQLCOMMENT_FLAG_REASON, number>;
};
}
export enum ACTION_ITEM_TYPE {
@@ -182,44 +194,169 @@ export async function createActions(
}
/**
* generateActionCounts will take a list of actions, and generate action counts
* ACTION_COUNT_JOIN_CHAR is the character that is used to separate the reason
* from the action type when storing the action counts in the models.
*/
export const ACTION_COUNT_JOIN_CHAR = "__";
/**
* encodeActionCounts will take a list of actions, and generate action counts
* from it.
*
* @param actions list of actions to generate the action counts from
*/
export function generateActionCounts(...actions: Action[]): ActionCounts {
const actionCounts: ActionCounts = {};
export function encodeActionCounts(...actions: Action[]): EncodedActionCounts {
const actionCounts: EncodedActionCounts = {};
/**
* increment the key in the action counts variable.
*/
function incr(...keys: string[]) {
const key = keys.join("_");
// Loop over the actions, and increment them.
for (const action of actions) {
for (const key of encodeActionCountKeys(action)) {
if (key in actionCounts) {
actionCounts[key]++;
} else {
actionCounts[key] = 1;
}
}
function transform(action: Action) {
const actionType = action.action_type.toLowerCase();
// Add the action type to the action counts.
incr(actionType);
// Check if the reason is set.
const reason = action.reason && action.reason.toLowerCase();
if (reason) {
// Add the action type to the action counts.
incr(actionType, reason);
}
}
// Loop over the actions, and increment them.
for (const action of actions) {
transform(action);
}
return actionCounts;
}
/**
* encodeActionCountKeys encodes the action into string keys which represents
* the groupings as seen in `EncodedActionCounts`.
*/
function encodeActionCountKeys(action: Action): string[] {
const keys = [action.action_type as string];
if (action.reason) {
keys.push(
[action.action_type as string, action.reason as string].join(
ACTION_COUNT_JOIN_CHAR
)
);
}
return keys;
}
interface DecodedActionCountKey {
/**
* actionType stores the action type referenced by the key.
*/
actionType: ACTION_TYPE;
/**
* reason stores the reason referenced by the key if the actionType is FLAG.
*/
reason?: GQLCOMMENT_FLAG_REASON;
}
/**
* decodeActionCountGroup will unpack the key as it is encoded into the separate
* actionType and reason.
*/
function decodeActionCountKey(key: string): DecodedActionCountKey {
let actionType: string = "";
let reason: string = "";
if (key.indexOf(ACTION_COUNT_JOIN_CHAR) >= 0) {
const keys = key.split(ACTION_COUNT_JOIN_CHAR);
if (keys.length !== 2) {
throw new Error(
"invalid action count contained more than two components"
);
}
actionType = keys[0];
reason = keys[1];
// Validate that the action type is flag.
if (actionType !== ACTION_TYPE.FLAG) {
throw new Error("invalid action type, expected only flag to have reason");
}
// Validate that the reason is valid.
if (!reason || !(reason in GQLCOMMENT_FLAG_REASON)) {
throw new Error("expected flag to have a reason that was valid");
}
} else {
actionType = key;
}
// Validate that the action type is valid.
if (!actionType || !(actionType in ACTION_TYPE)) {
throw new Error("expected action to have an action type that was valid");
}
const result: DecodedActionCountKey = {
actionType: actionType as ACTION_TYPE,
};
// Merge in the reason if it's provided. If we got here, we know that the
// reason is a GQLCOMMENT_FLAG_REASON.
if (reason) {
result.reason = reason as GQLCOMMENT_FLAG_REASON;
}
return result;
}
/**
* decodeActionCounts will take the encoded action counts and decode them into
* a useable format.
*
* @param encodedActionCounts the action counts to decode
*/
export function decodeActionCounts(
encodedActionCounts: EncodedActionCounts
): ActionCounts {
// Default all the action counts to zero.
const actionCounts: ActionCounts = {
[ACTION_TYPE.REACTION]: {
total: 0,
},
[ACTION_TYPE.DONT_AGREE]: {
total: 0,
},
[ACTION_TYPE.FLAG]: {
total: 0,
reasons: Object.keys(GQLCOMMENT_FLAG_REASON).reduce(
(reasons, reason) => ({
...reasons,
[reason]: 0,
}),
{}
) as Record<GQLCOMMENT_FLAG_REASON, number>,
},
};
// Loop over all the encoded action counts to extract each of the action
// counts as they are encoded.
Object.entries(encodedActionCounts).forEach(([key, count]) => {
// Pull out the action type and the reason from the key.
const { actionType, reason } = decodeActionCountKey(key);
// Handle the different types and reasons.
switch (actionType) {
case ACTION_TYPE.REACTION:
actionCounts[ACTION_TYPE.REACTION].total += count;
break;
case ACTION_TYPE.DONT_AGREE:
actionCounts[ACTION_TYPE.DONT_AGREE].total += count;
break;
case ACTION_TYPE.FLAG:
// When we have a reason, we are incrementing for that particular reason
// rather than incrementing for the total. If we don't have a reason, we
// just got the updated reason.
if (reason) {
actionCounts[ACTION_TYPE.FLAG].reasons[reason] += count;
} else {
actionCounts[ACTION_TYPE.FLAG].total += count;
}
break;
default:
throw new Error("unexpected action type");
}
});
return actionCounts;
}
+3 -3
View File
@@ -7,7 +7,7 @@ import {
GQLCOMMENT_SORT,
GQLCOMMENT_STATUS,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { ActionCounts } from "talk-server/models/actions";
import { EncodedActionCounts } from "talk-server/models/actions";
import {
Connection,
Cursor,
@@ -42,7 +42,7 @@ export interface Comment extends TenantResource {
body_history: BodyHistoryItem[];
status: GQLCOMMENT_STATUS;
status_history: StatusHistoryItem[];
action_counts: ActionCounts;
action_counts: EncodedActionCounts;
reply_count: number;
created_at: Date;
deleted_at?: Date;
@@ -386,7 +386,7 @@ export async function updateCommentActionCounts(
mongo: Db,
tenantID: string,
id: string,
actionCounts: ActionCounts
actionCounts: EncodedActionCounts
) {
const result = await collection(mongo).findOneAndUpdate(
{ id, tenant_id: tenantID },
+2 -2
View File
@@ -7,7 +7,7 @@ import {
GQLUSER_ROLE,
GQLUSER_USERNAME_STATUS,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { ActionCounts } from "talk-server/models/actions";
import { EncodedActionCounts } from "talk-server/models/actions";
import { FilterQuery } from "talk-server/models/query";
import { TenantResource } from "talk-server/models/tenant";
@@ -70,7 +70,7 @@ export interface User extends TenantResource {
tokens: Token[];
role: GQLUSER_ROLE;
status: UserStatus;
action_counts: ActionCounts;
action_counts: EncodedActionCounts;
ignored_users: string[];
created_at: Date;
}
+2 -2
View File
@@ -5,7 +5,7 @@ import {
ACTION_ITEM_TYPE,
CreateActionInput,
createActions,
generateActionCounts,
encodeActionCounts,
} from "talk-server/models/actions";
import { retrieveAsset } from "talk-server/models/asset";
import {
@@ -186,7 +186,7 @@ async function addCommentActions(
if (upsertedActions.length > 0) {
// Compute the action counts.
const actionCounts = generateActionCounts(...upsertedActions);
const actionCounts = encodeActionCounts(...upsertedActions);
// Update the comment action counts here.
const updatedComment = await updateCommentActionCounts(