Merge branch 'next' into next-auth

This commit is contained in:
Wyatt Johnson
2018-10-29 17:39:06 -06:00
121 changed files with 904 additions and 972 deletions
@@ -1,17 +0,0 @@
import DataLoader from "dataloader";
import TenantContext from "talk-server/graph/tenant/context";
import {
Asset,
FindOrCreateAssetInput,
retrieveManyAssets,
} from "talk-server/models/asset";
import { findOrCreate } from "talk-server/services/assets";
export default (ctx: TenantContext) => ({
findOrCreate: (input: FindOrCreateAssetInput) =>
findOrCreate(ctx.mongo, ctx.tenant, input, ctx.queue.scraper),
asset: new DataLoader<string, Asset | null>(ids =>
retrieveManyAssets(ctx.mongo, ctx.tenant.id, ids)
),
});
@@ -2,11 +2,11 @@ import DataLoader from "dataloader";
import Context from "talk-server/graph/tenant/context";
import {
AssetToCommentsArgs,
CommentToParentsArgs,
CommentToRepliesArgs,
GQLActionPresence,
GQLCOMMENT_SORT,
StoryToCommentsArgs,
} from "talk-server/graph/tenant/schema/__generated__/types";
import {
ACTION_ITEM_TYPE,
@@ -14,9 +14,9 @@ import {
} from "talk-server/models/action";
import {
Comment,
retrieveCommentAssetConnection,
retrieveCommentParentsConnection,
retrieveCommentRepliesConnection,
retrieveCommentStoryConnection,
retrieveCommentUserConnection,
retrieveManyComments,
} from "talk-server/models/comment";
@@ -61,29 +61,29 @@ export default (ctx: Context) => ({
first = 10,
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
after,
}: AssetToCommentsArgs
}: StoryToCommentsArgs
) =>
retrieveCommentUserConnection(ctx.mongo, ctx.tenant.id, userID, {
first,
orderBy,
after,
}),
forAsset: (
assetID: string,
forStory: (
storyID: string,
// Apply the graph schema defaults at the loader.
{
first = 10,
orderBy = GQLCOMMENT_SORT.CREATED_AT_DESC,
after,
}: AssetToCommentsArgs
}: StoryToCommentsArgs
) =>
retrieveCommentAssetConnection(ctx.mongo, ctx.tenant.id, assetID, {
retrieveCommentStoryConnection(ctx.mongo, ctx.tenant.id, storyID, {
first,
orderBy,
after,
}).then(primeCommentsFromConnection(ctx)),
forParent: (
assetID: string,
storyID: string,
parentID: string,
// Apply the graph schema defaults at the loader.
{
@@ -95,7 +95,7 @@ export default (ctx: Context) => ({
retrieveCommentRepliesConnection(
ctx.mongo,
ctx.tenant.id,
assetID,
storyID,
parentID,
{
first,
@@ -1,12 +1,13 @@
import Context from "talk-server/graph/tenant/context";
import Assets from "./assets";
import Auth from "./auth";
import Comments from "./comments";
import Stories from "./stories";
import Users from "./users";
export default (ctx: Context) => ({
Auth: Auth(ctx),
Assets: Assets(ctx),
Stories: Stories(ctx),
Comments: Comments(ctx),
Users: Users(ctx),
});
@@ -0,0 +1,17 @@
import DataLoader from "dataloader";
import TenantContext from "talk-server/graph/tenant/context";
import {
FindOrCreateStoryInput,
retrieveManyStories,
Story,
} from "talk-server/models/story";
import { findOrCreate } from "talk-server/services/stories";
export default (ctx: TenantContext) => ({
findOrCreate: (input: FindOrCreateStoryInput) =>
findOrCreate(ctx.mongo, ctx.tenant, input, ctx.queue.scraper),
story: new DataLoader<string, Story | null>(ids =>
retrieveManyStories(ctx.mongo, ctx.tenant.id, ids)
),
});
@@ -27,7 +27,7 @@ export default (ctx: TenantContext) => ({
ctx.user!,
{
author_id: ctx.user!.id,
asset_id: input.assetID,
story_id: input.storyID,
body: input.body,
parent_id: input.parentID,
},
@@ -1,14 +0,0 @@
import { GQLAssetTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action";
import { Asset } from "talk-server/models/asset";
const Asset: GQLAssetTypeResolver<Asset> = {
comments: (asset, input, ctx) =>
ctx.loaders.Comments.forAsset(asset.id, input),
// TODO: implement this.
isClosed: () => false,
actionCounts: asset => decodeActionCounts(asset.action_counts),
commentCounts: asset => asset.comment_counts,
};
export default Asset;
@@ -23,7 +23,7 @@ const Comment: GQLCommentTypeResolver<Comment> = {
ctx.loaders.Users.user.load(comment.author_id),
replies: (comment, input, ctx) =>
comment.reply_count > 0
? ctx.loaders.Comments.forParent(comment.asset_id, comment.id, input)
? ctx.loaders.Comments.forParent(comment.story_id, comment.id, input)
: createConnection(),
actionCounts: comment => decodeActionCounts(comment.action_counts),
myActionPresence: (comment, input, ctx) =>
@@ -84,8 +84,8 @@ const Comment: GQLCommentTypeResolver<Comment> = {
comment.parent_id
? ctx.loaders.Comments.parents(comment, input)
: createConnection(),
asset: (comment, input, ctx) =>
ctx.loaders.Assets.asset.load(comment.asset_id),
story: (comment, input, ctx) =>
ctx.loaders.Stories.story.load(comment.story_id),
};
export default Comment;
@@ -2,7 +2,7 @@ import {
GQLCOMMENT_STATUS,
GQLCommentCountsTypeResolver,
} from "talk-server/graph/tenant/schema/__generated__/types";
import { CommentStatusCounts } from "talk-server/models/asset";
import { CommentStatusCounts } from "talk-server/models/story";
const CommentCounts: GQLCommentCountsTypeResolver<CommentStatusCounts> = {
totalVisible: commentCounts =>
@@ -3,7 +3,6 @@ import Time from "talk-server/graph/common/scalars/time";
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import Asset from "./asset";
import AuthIntegrations from "./auth_integrations";
import Comment from "./comment";
import CommentCounts from "./comment_counts";
@@ -11,10 +10,10 @@ import Mutation from "./mutation";
import OIDCAuthIntegration from "./oidc_auth_integration";
import Profile from "./profile";
import Query from "./query";
import Story from "./story";
import User from "./user";
const Resolvers: GQLResolver = {
Asset,
AuthIntegrations,
Comment,
CommentCounts,
@@ -24,6 +23,7 @@ const Resolvers: GQLResolver = {
Profile,
Query,
Time,
Story,
User,
};
@@ -1,7 +1,7 @@
import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
const Query: GQLQueryTypeResolver<void> = {
asset: (source, args, ctx) => ctx.loaders.Assets.findOrCreate(args),
story: (source, args, ctx) => ctx.loaders.Stories.findOrCreate(args),
comment: (source, { id }, ctx) =>
id ? ctx.loaders.Comments.comment.load(id) : null,
settings: (source, args, ctx) => ctx.tenant,
@@ -0,0 +1,14 @@
import { GQLStoryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
import { decodeActionCounts } from "talk-server/models/action";
import { Story } from "talk-server/models/story";
const Story: GQLStoryTypeResolver<Story> = {
comments: (story, input, ctx) =>
ctx.loaders.Comments.forStory(story.id, input),
// TODO: implement this.
isClosed: () => false,
actionCounts: story => decodeActionCounts(story.action_counts),
commentCounts: story => story.comment_counts,
};
export default Story;
@@ -735,7 +735,7 @@ type Settings {
domains: [String!] @auth(roles: [ADMIN])
"""
moderation is the moderation mode for all Asset's on the site.
moderation is the moderation mode for all Stories on the site.
"""
moderation: MODERATION_MODE @auth(roles: [ADMIN])
@@ -789,12 +789,12 @@ type Settings {
"""
closedTimeout is the amount of seconds from the created_at timestamp that a
given asset will be considered closed.
given story will be considered closed.
"""
closedTimeout: Int!
"""
closedMessage is the message shown to the user when the given Asset is
closedMessage is the message shown to the user when the given Story is
closed.
"""
closedMessage: String
@@ -997,7 +997,7 @@ enum COMMENT_STATUS {
REJECTED
"""
The comment was created while the asset's premoderation option was on, and
The comment was created while the stories premoderation option was on, and
new comments that haven't been moderated yet are referred to as
"premoderated" or "premod" comments.
"""
@@ -1011,7 +1011,7 @@ enum COMMENT_STATUS {
}
"""
Comment is a comment left by a User on an Asset or another Comment as a reply.
Comment is a comment left by a User on an Story or another Comment as a reply.
"""
type Comment {
"""
@@ -1072,7 +1072,7 @@ type Comment {
"""
rootParent is the highest level parent Comment. This Comment would have been
left on the Asset itself.
left on the Story itself.
"""
rootParent: Comment
@@ -1099,9 +1099,9 @@ type Comment {
myActionPresence: ActionPresence
"""
asset is the Asset that the Comment was written on.
story is the Story that the Comment was written on.
"""
asset: Asset!
story: Story!
}
type PageInfo {
@@ -1178,7 +1178,7 @@ type CommentStatusCounts {
REJECTED: Int!
"""
The comment was created while the asset's premoderation option was on, and
The comment was created while the stories premoderation option was on, and
new comments that haven't been moderated yet are referred to as
"premoderated" or "premod" comments.
"""
@@ -1201,7 +1201,7 @@ type CommentCounts {
}
################################################################################
## Asset
## Story
################################################################################
enum COMMENT_SORT {
@@ -1212,26 +1212,26 @@ enum COMMENT_SORT {
}
"""
Asset is an Article or Page where Comments are written on by Users.
Story is an Article or Page where Comments are written on by Users.
"""
type Asset {
type Story {
"""
id is the identifier of the Asset.
id is the identifier of the Story.
"""
id: ID!
"""
url is the url that the Asset is located on.
url is the url that the Story is located on.
"""
url: String!
"""
title is the title of the scraped Asset.
title is the title of the scraped Story.
"""
title: String
"""
comments are the comments on the Asset.
comments are the comments on the Story.
"""
comments(
first: Int = 10
@@ -1240,23 +1240,23 @@ type Asset {
): CommentsConnection!
"""
actionCounts stores the counts of all the actions against this Asset and it's
actionCounts stores the counts of all the actions against this Story and it's
Comments.
"""
actionCounts: ActionCounts! @auth(roles: [ADMIN, MODERATOR])
"""
author is the authors listed in the meta tags for the Asset.
author is the authors listed in the meta tags for the Story.
"""
author: String
"""
closedAt is the Time that the Asset is closed for commenting.
closedAt is the Time that the Story is closed for commenting.
"""
closedAt: Time
"""
isClosed returns true when the Asset is currently closed for commenting.
isClosed returns true when the Story is currently closed for commenting.
"""
isClosed: Boolean!
@@ -1266,19 +1266,19 @@ type Asset {
commentCounts: CommentCounts!
"""
createdAt is the date that the Asset was created at.
createdAt is the date that the Story was created at.
"""
createdAt: Time!
}
"""
AssetEdge represents a unique Asset in a AssetConnection.
StoryEdge represents a unique Story in a StoryConnection.
"""
type AssetEdge {
type StoryEdge {
"""
node is the Asset for this edge.
node is the Story for this edge.
"""
node: Asset!
node: Story!
"""
@@ -1287,13 +1287,13 @@ type AssetEdge {
}
"""
AssetsConnection represents a subset of a Asset list.
StoriesConnection represents a subset of a Story list.
"""
type AssetsConnection {
type StoriesConnection {
"""
edges are a subset of AssetEdge's.
edges are a subset of StoryEdge's.
"""
edges: [AssetEdge!]!
edges: [StoryEdge!]!
"""
pageInfo is
@@ -1312,9 +1312,9 @@ type Query {
comment(id: ID!): Comment
"""
asset is the Asset specified by its ID/URL.
story is the Story specified by its ID/URL.
"""
asset(id: ID, url: String): Asset
story(id: ID, url: String): Story
"""
me is the current logged in User. If no user is currently logged in, it will
@@ -1353,9 +1353,9 @@ CreateCommentInput provides the input for the createComment Mutation.
"""
input CreateCommentInput {
"""
assetID is the ID of the Asset where we are creating a comment on.
storyID is the ID of the Story where we are creating a comment on.
"""
assetID: ID!
storyID: ID!
"""
parentID is the optional ID of the Comment that we are replying to.
@@ -1684,7 +1684,7 @@ input SettingsInput {
domains: [String!]
"""
moderation is the moderation mode for all Asset's on the site.
moderation is the moderation mode for all Stories on the site.
"""
moderation: MODERATION_MODE
@@ -1738,12 +1738,12 @@ input SettingsInput {
"""
closedTimeout is the amount of seconds from the created_at timestamp that a
given asset will be considered closed.
given story will be considered closed.
"""
closedTimeout: Int
"""
closedMessage is the message shown to the user when the given Asset is
closedMessage is the message shown to the user when the given Story is
closed.
"""
closedMessage: String
@@ -2364,5 +2364,5 @@ type Mutation {
################################################################################
type Subscription {
commentCreated(assetID: ID!): Comment
commentCreated(storyID: ID!): Comment
}
+8 -8
View File
@@ -38,7 +38,7 @@ export interface Comment extends TenantResource {
readonly id: string;
parent_id?: string;
author_id: string;
asset_id: string;
story_id: string;
body: string;
body_history: BodyHistoryItem[];
status: GQLCOMMENT_STATUS;
@@ -288,14 +288,14 @@ function cursorGetterFactory(
export async function retrieveCommentRepliesConnection(
db: Db,
tenantID: string,
assetID: string,
storyID: string,
parentID: string,
input: ConnectionInput
) {
// Create the query.
const query = new Query(collection(db)).where({
tenant_id: tenantID,
asset_id: assetID,
story_id: storyID,
parent_id: parentID,
});
@@ -400,23 +400,23 @@ export async function retrieveCommentParentsConnection(
}
/**
* retrieveAssetConnection returns a Connection<Comment> for a given Asset's
* retrieveStoryConnection returns a Connection<Comment> for a given Stories
* comments.
*
* @param db database connection
* @param assetID the Asset id for the comment to retrieve
* @param storyID the Story id for the comment to retrieve
* @param input connection configuration
*/
export async function retrieveCommentAssetConnection(
export async function retrieveCommentStoryConnection(
db: Db,
tenantID: string,
assetID: string,
storyID: string,
input: ConnectionInput
) {
// Create the query.
const query = new Query(collection(db)).where({
tenant_id: tenantID,
asset_id: assetID,
story_id: storyID,
parent_id: null,
});
@@ -9,7 +9,7 @@ import { ModerationSettings } from "talk-server/models/settings";
import { TenantResource } from "talk-server/models/tenant";
function collection(db: Db) {
return db.collection<Readonly<Asset>>("assets");
return db.collection<Readonly<Story>>("stories");
}
// TODO: (wyattjoh) write a test to verify that this set of counts is always in sync with GQLCOMMENT_STATUS.
@@ -21,7 +21,7 @@ export interface CommentStatusCounts {
[GQLCOMMENT_STATUS.SYSTEM_WITHHELD]: number;
}
export interface Asset extends TenantResource {
export interface Story extends TenantResource {
readonly id: string;
url: string;
scraped?: Date;
@@ -39,40 +39,40 @@ export interface Asset extends TenantResource {
publication_date?: Date;
/**
* action_counts stores all the action counts for all Comment's on this Asset.
* action_counts stores all the action counts for all Comment's on this Story.
*/
action_counts: EncodedActionCounts;
/**
* comment_counts stores the different counts for each comment on the Asset
* comment_counts stores the different counts for each comment on the Story
* according to their statuses.
*/
comment_counts: CommentStatusCounts;
/**
* settings provides a point where the settings can be overridden for a
* specific Asset.
* specific Story.
*/
settings?: Partial<ModerationSettings>;
}
export interface UpsertAssetInput {
export interface UpsertStoryInput {
id?: string;
url: string;
}
export async function upsertAsset(
export async function upsertStory(
db: Db,
tenantID: string,
{ id, url }: UpsertAssetInput
{ id, url }: UpsertStoryInput
) {
const now = new Date();
// TODO: verify that the url for the given Asset is whitelisted by the tenant.
// TODO: verify that the url for the given Story is whitelisted by the tenant.
// Create the asset, optionally sourcing the id from the input, additionally
// Create the story, optionally sourcing the id from the input, additionally
// porting in the tenant_id.
const update: { $setOnInsert: Asset } = {
const update: { $setOnInsert: Story } = {
$setOnInsert: {
id: id ? id : uuid.v4(),
url,
@@ -84,8 +84,8 @@ export async function upsertAsset(
};
// Perform the find and update operation to try and find and or create the
// asset.
const { value: asset } = await collection(db).findOneAndUpdate(
// story.
const { value: story } = await collection(db).findOneAndUpdate(
{
url,
tenant_id: tenantID,
@@ -100,26 +100,26 @@ export async function upsertAsset(
returnOriginal: false,
}
);
if (!asset) {
if (!story) {
return null;
}
if (!asset.scraped) {
// TODO: create scrape job to collect asset metadata
if (!story.scraped) {
// TODO: create scrape job to collect story metadata
}
return asset;
return story;
}
/**
* updateCommentStatusCount increments the number of status counts for the
* given Asset ID.
* given Story ID.
*
* @param mongo the database handle
* @param tenantID the tenant that the Asset is on.
* @param id the ID of the Asset.
* @param tenantID the tenant that the Story is on.
* @param id the ID of the Story.
* @param commentStatusCounts the update document that contains a positive or
* negative number of comments to increment on the given Asset.
* negative number of comments to increment on the given Story.
*/
export async function updateCommentStatusCount(
mongo: Db,
@@ -127,7 +127,7 @@ export async function updateCommentStatusCount(
id: string,
commentStatusCounts: Partial<CommentStatusCounts>
) {
const { value: asset } = await collection(mongo).findOneAndUpdate(
const { value: story } = await collection(mongo).findOneAndUpdate(
{
id,
tenant_id: tenantID,
@@ -140,7 +140,7 @@ export async function updateCommentStatusCount(
{ returnOriginal: false }
);
return asset;
return story;
}
function createEmptyCommentCounts(): CommentStatusCounts {
@@ -153,39 +153,39 @@ function createEmptyCommentCounts(): CommentStatusCounts {
};
}
export interface FindOrCreateAssetInput {
export interface FindOrCreateStoryInput {
id?: string;
url?: string;
}
export async function findOrCreateAsset(
export async function findOrCreateStory(
db: Db,
tenantID: string,
{ id, url }: FindOrCreateAssetInput
{ id, url }: FindOrCreateStoryInput
) {
if (id) {
if (url) {
// The URL was specified, this is an upsert operation.
return upsertAsset(db, tenantID, {
return upsertStory(db, tenantID, {
id,
url,
});
}
// The URL was not specified, this is a lookup operation.
return retrieveAsset(db, tenantID, id);
return retrieveStory(db, tenantID, id);
}
// The ID was not specified, this is an upsert operation. Check to see that
// the URL exists.
if (!url) {
throw new Error("cannot upsert an asset without the url");
throw new Error("cannot upsert an story without the url");
}
return upsertAsset(db, tenantID, { url });
return upsertStory(db, tenantID, { url });
}
export async function retrieveAssetByURL(
export async function retrieveStoryByURL(
db: Db,
tenantID: string,
url: string
@@ -193,11 +193,11 @@ export async function retrieveAssetByURL(
return collection(db).findOne({ url, tenant_id: tenantID });
}
export async function retrieveAsset(db: Db, tenantID: string, id: string) {
export async function retrieveStory(db: Db, tenantID: string, id: string) {
return collection(db).findOne({ id, tenant_id: tenantID });
}
export async function retrieveManyAssets(
export async function retrieveManyStories(
db: Db,
tenantID: string,
ids: string[]
@@ -207,12 +207,12 @@ export async function retrieveManyAssets(
tenant_id: tenantID,
});
const assets = await cursor.toArray();
const stories = await cursor.toArray();
return ids.map(id => assets.find(asset => asset.id === id) || null);
return ids.map(id => stories.find(story => story.id === id) || null);
}
export async function retrieveManyAssetsByURL(
export async function retrieveManyStoriesByURL(
db: Db,
tenantID: string,
urls: string[]
@@ -222,21 +222,21 @@ export async function retrieveManyAssetsByURL(
tenant_id: tenantID,
});
const assets = await cursor.toArray();
const stories = await cursor.toArray();
return urls.map(url => assets.find(asset => asset.url === url) || null);
return urls.map(url => stories.find(story => story.url === url) || null);
}
export type UpdateAssetInput = Omit<
Partial<Asset>,
export type UpdateStoryInput = Omit<
Partial<Story>,
"id" | "tenant_id" | "url" | "created_at"
>;
export async function updateAsset(
export async function updateStory(
db: Db,
tenantID: string,
id: string,
input: UpdateAssetInput
input: UpdateStoryInput
) {
// Only update fields that have been updated.
const update = {
@@ -259,15 +259,15 @@ export async function updateAsset(
}
/**
* updateAssetActionCounts will update the given comment's action counts on
* the Asset.
* updateStoryActionCounts will update the given comment's action counts on
* the Story.
*
* @param mongo the database handle
* @param tenantID the id of the Tenant
* @param id the id of the Asset being updated
* @param actionCounts the action counts to merge into the Asset
* @param id the id of the Story being updated
* @param actionCounts the action counts to merge into the Story
*/
export async function updateAssetActionCounts(
export async function updateStoryActionCounts(
mongo: Db,
tenantID: string,
id: string,
+1 -1
View File
@@ -19,7 +19,7 @@ export interface TenantResource {
}
/**
* Tenant describes a given Tenant on Talk that has Assets, Comments, and Users.
* Tenant describes a given Tenant on Talk that has Stories, Comments, and Users.
*/
export interface Tenant extends Settings {
readonly id: string;
+7 -7
View File
@@ -11,12 +11,12 @@ import {
encodeActionCounts,
invertEncodedActionCounts,
} from "talk-server/models/action";
import { updateAssetActionCounts } from "talk-server/models/asset";
import {
retrieveComment,
updateCommentActionCounts,
} from "talk-server/models/comment";
import { Comment } from "talk-server/models/comment";
import { updateStoryActionCounts } from "talk-server/models/story";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
@@ -47,11 +47,11 @@ export async function addCommentActions(
actionCounts
);
// Update the Asset with the updated action counts.
await updateAssetActionCounts(
// Update the Story with the updated action counts.
await updateStoryActionCounts(
mongo,
tenant.id,
comment.asset_id,
comment.story_id,
actionCounts
);
@@ -109,11 +109,11 @@ export async function removeCommentAction(
actionCounts
);
// Update the Asset with the updated action counts.
await updateAssetActionCounts(
// Update the Story with the updated action counts.
await updateStoryActionCounts(
mongo,
tenant.id,
comment.asset_id,
comment.story_id,
actionCounts
);
+19 -19
View File
@@ -2,10 +2,6 @@ import { Db } from "mongodb";
import { Omit } from "talk-common/types";
import { ACTION_ITEM_TYPE, CreateActionInput } from "talk-server/models/action";
import {
retrieveAsset,
updateCommentStatusCount,
} from "talk-server/models/asset";
import {
createComment,
CreateCommentInput,
@@ -14,6 +10,10 @@ import {
pushChildCommentIDOntoParent,
retrieveComment,
} from "talk-server/models/comment";
import {
retrieveStory,
updateCommentStatusCount,
} from "talk-server/models/story";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
import { addCommentActions } from "talk-server/services/comments/actions";
@@ -32,14 +32,14 @@ export async function create(
input: CreateComment,
req?: Request
) {
// Grab the asset that we'll use to check moderation pieces with.
const asset = await retrieveAsset(mongo, tenant.id, input.asset_id);
if (!asset) {
// Grab the story that we'll use to check moderation pieces with.
const story = await retrieveStory(mongo, tenant.id, input.story_id);
if (!story) {
// TODO: (wyattjoh) return better error.
throw new Error("asset referenced does not exist");
throw new Error("story referenced does not exist");
}
// TODO: (wyattjoh) Check that the asset was visible.
// TODO: (wyattjoh) Check that the story was visible.
const grandparentIDs: string[] = [];
if (input.parent_id) {
@@ -62,7 +62,7 @@ export async function create(
// Run the comment through the moderation phases.
const { actions, status, metadata } = await processForModeration({
asset,
story,
tenant,
comment: input,
author,
@@ -104,8 +104,8 @@ export async function create(
);
}
// Increment the status count for the particular status on the Asset.
await updateCommentStatusCount(mongo, tenant.id, asset.id, {
// Increment the status count for the particular status on the Story.
await updateCommentStatusCount(mongo, tenant.id, story.id, {
[status]: 1,
});
@@ -131,16 +131,16 @@ export async function edit(
throw new Error("comment not found");
}
// Grab the asset that we'll use to check moderation pieces with.
const asset = await retrieveAsset(mongo, tenant.id, comment.asset_id);
if (!asset) {
// Grab the story that we'll use to check moderation pieces with.
const story = await retrieveStory(mongo, tenant.id, comment.story_id);
if (!story) {
// TODO: (wyattjoh) return better error.
throw new Error("asset referenced does not exist");
throw new Error("story referenced does not exist");
}
// Run the comment through the moderation phases.
const { status, metadata, actions } = await processForModeration({
asset,
story,
tenant,
comment: input,
author,
@@ -185,9 +185,9 @@ export async function edit(
}
if (comment.status !== editedComment.status) {
// Increment the status count for the particular status on the Asset, and
// Increment the status count for the particular status on the Story, and
// decrement the status on the comment's previous status.
await updateCommentStatusCount(mongo, tenant.id, asset.id, {
await updateCommentStatusCount(mongo, tenant.id, story.id, {
[comment.status]: -1,
[editedComment.status]: 1,
});
@@ -1,8 +1,8 @@
import { Omit, Promiseable } from "talk-common/types";
import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__/types";
import { CreateActionInput } from "talk-server/models/action";
import { Asset } from "talk-server/models/asset";
import { Comment } from "talk-server/models/comment";
import { Story } from "talk-server/models/story";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
import { Request } from "talk-server/types/express";
@@ -18,7 +18,7 @@ export interface PhaseResult {
}
export interface ModerationPhaseContext {
asset: Asset;
story: Story;
tenant: Tenant;
comment: Partial<Comment>;
author: User;
@@ -1,15 +0,0 @@
import {
IntermediateModerationPhase,
IntermediatePhaseResult,
} from "talk-server/services/comments/moderation";
// This phase checks to see if the asset being processed is closed or not.
export const assetClosed: IntermediateModerationPhase = ({
asset,
}): IntermediatePhaseResult | void => {
// Check to see if the asset has closed commenting...
if (asset.closedAt && asset.closedAt.valueOf() <= Date.now()) {
// TODO: (wyattjoh) return better error.
throw new Error("asset is currently closed for commenting");
}
};
@@ -35,7 +35,7 @@ const testCharCount = (
};
export const commentLength: IntermediateModerationPhase = ({
asset,
story,
tenant,
comment,
}): IntermediatePhaseResult | void => {
@@ -44,7 +44,7 @@ export const commentLength: IntermediateModerationPhase = ({
// Reject if the comment is too long or too short.
if (
testCharCount(tenant, length) ||
(asset.settings && testCharCount(asset.settings, length))
(story.settings && testCharCount(story.settings, length))
) {
return {
status: GQLCOMMENT_STATUS.REJECTED,
@@ -8,13 +8,13 @@ const testDisabledCommenting = (settings: Partial<ModerationSettings>) =>
settings.disableCommenting;
export const commentingDisabled: IntermediateModerationPhase = ({
asset,
story,
tenant,
}): IntermediatePhaseResult | void => {
// Check to see if the asset has closed commenting.
// Check to see if the story has closed commenting.
if (
testDisabledCommenting(tenant) ||
(asset.settings && testDisabledCommenting(asset.settings))
(story.settings && testDisabledCommenting(story.settings))
) {
// TODO: (wyattjoh) return better error.
throw new Error("commenting has been disabled tenant wide");
@@ -1,6 +1,5 @@
import { IntermediateModerationPhase } from "talk-server/services/comments/moderation";
import { assetClosed } from "./assetClosed";
import { commentingDisabled } from "./commentingDisabled";
import { commentLength } from "./commentLength";
import { karma } from "./karma";
@@ -8,6 +7,7 @@ import { links } from "./links";
import { preModerate } from "./preModerate";
import { spam } from "./spam";
import { staff } from "./staff";
import { storyClosed } from "./storyClosed";
import { toxic } from "./toxic";
import { wordList } from "./wordList";
@@ -16,7 +16,7 @@ import { wordList } from "./wordList";
*/
export const moderationPhases: IntermediateModerationPhase[] = [
commentLength,
assetClosed,
storyClosed,
commentingDisabled,
wordList,
staff,
@@ -25,14 +25,14 @@ const testPremodLinksEnable = (
// This phase checks the comment if it has any links in it if the check is
// enabled.
export const links: IntermediateModerationPhase = ({
asset,
story,
tenant,
comment,
}): IntermediatePhaseResult | void => {
if (
comment.body &&
(testPremodLinksEnable(tenant, comment.body) ||
(asset.settings && testPremodLinksEnable(asset.settings, comment.body)))
(story.settings && testPremodLinksEnable(story.settings, comment.body)))
) {
// Add the flag related to Trust to the comment.
return {
@@ -14,16 +14,16 @@ const testModerationMode = (settings: Partial<ModerationSettings>) =>
// This phase checks to see if the settings have premod enabled, if they do,
// the comment is premod, otherwise, it's just none.
export const preModerate: IntermediateModerationPhase = ({
asset,
story,
tenant,
}): IntermediatePhaseResult | void => {
// If the settings say that we're in premod mode, then the comment is in
// premod status.
// TODO: (wyattjoh) pull from the asset settings.
// TODO: (wyattjoh) pull from the story settings.
if (
testModerationMode(tenant) ||
(asset.settings && testModerationMode(asset.settings))
(story.settings && testModerationMode(story.settings))
) {
return {
status: GQLCOMMENT_STATUS.PREMOD,
@@ -12,7 +12,7 @@ import {
} from "talk-server/services/comments/moderation";
export const spam: IntermediateModerationPhase = async ({
asset,
story,
tenant,
comment,
author,
@@ -96,7 +96,7 @@ export const spam: IntermediateModerationPhase = async ({
referrer, // REQUIRED
user_agent: userAgent, // REQUIRED
comment_content: comment.body,
permalink: asset.url,
permalink: story.url,
comment_author: author.displayName || author.username || "",
comment_type: "comment",
is_test: false,
@@ -1,16 +1,16 @@
import { Asset } from "talk-server/models/asset";
import { Comment } from "talk-server/models/comment";
import { Story } from "talk-server/models/story";
import { Tenant } from "talk-server/models/tenant";
import { User } from "talk-server/models/user";
import { assetClosed } from "talk-server/services/comments/moderation/phases/assetClosed";
import { storyClosed } from "talk-server/services/comments/moderation/phases/storyClosed";
describe("assetClosed", () => {
it("throws an error when the asset is closed", () => {
const asset = { closedAt: new Date() };
describe("storyClosed", () => {
it("throws an error when the story is closed", () => {
const story = { closedAt: new Date() };
expect(() =>
assetClosed({
asset: asset as Asset,
storyClosed({
story: story as Story,
tenant: (null as any) as Tenant,
comment: (null as any) as Comment,
author: (null as any) as User,
@@ -18,12 +18,12 @@ describe("assetClosed", () => {
).toThrow();
});
it("does not throw an error when the asset is not closed", () => {
it("does not throw an error when the story is not closed", () => {
const now = new Date();
expect(
assetClosed({
asset: { closedAt: new Date(now.getTime() + 60000) } as Asset,
storyClosed({
story: { closedAt: new Date(now.getTime() + 60000) } as Story,
tenant: (null as any) as Tenant,
comment: (null as any) as Comment,
author: (null as any) as User,
@@ -31,8 +31,8 @@ describe("assetClosed", () => {
).toBeUndefined();
expect(
assetClosed({
asset: {} as Asset,
storyClosed({
story: {} as Story,
tenant: (null as any) as Tenant,
comment: (null as any) as Comment,
author: (null as any) as User,
@@ -0,0 +1,15 @@
import {
IntermediateModerationPhase,
IntermediatePhaseResult,
} from "talk-server/services/comments/moderation";
// This phase checks to see if the story being processed is closed or not.
export const storyClosed: IntermediateModerationPhase = ({
story,
}): IntermediatePhaseResult | void => {
// Check to see if the story has closed commenting...
if (story.closedAt && story.closedAt.valueOf() <= Date.now()) {
// TODO: (wyattjoh) return better error.
throw new Error("story is currently closed for commenting");
}
};
@@ -19,7 +19,7 @@ export const wordList: IntermediateModerationPhase = ({
return;
}
// Decide the status based on whether or not the current asset/settings
// Decide the status based on whether or not the current story/settings
// has pre-mod enabled or not. If the comment was rejected based on the
// wordList, then reject it, otherwise if the moderation setting is
// premod, set it to `premod`.
@@ -8,7 +8,7 @@ import titleScraper from "metascraper-title";
import { Db } from "mongodb";
import logger from "talk-server/logger";
import { updateAsset } from "talk-server/models/asset";
import { updateStory } from "talk-server/models/story";
import Task from "talk-server/services/queue/Task";
import { modifiedScraper } from "./rules/modified";
import { sectionScraper } from "./rules/section";
@@ -20,8 +20,8 @@ export interface ScrapeProcessorOptions {
}
export interface ScraperData {
assetID: string;
assetURL: string;
storyID: string;
storyURL: string;
tenantID: string;
}
@@ -30,17 +30,17 @@ const createJobProcessor = (
scraper: Scraper
) => async (job: Job<ScraperData>) => {
// Pull out the job data.
const { assetID: id, assetURL: url, tenantID } = job.data;
const { storyID: id, storyURL: url, tenantID } = job.data;
logger.debug(
{
job_id: job.id,
job_name: JOB_NAME,
asset_id: id,
asset_url: url,
story_id: id,
story_url: url,
tenant_id: tenantID,
},
"starting to scrap the asset"
"starting to scrap the story"
);
// Get the metadata from the scraped html.
@@ -50,17 +50,17 @@ const createJobProcessor = (
{
job_id: job.id,
job_name: JOB_NAME,
asset_id: id,
asset_url: url,
story_id: id,
story_url: url,
tenant_id: tenantID,
},
"asset at specified url not found, can not scrape"
"story at specified url not found, can not scrape"
);
return;
}
// Update the Asset with the scraped details.
const asset = await updateAsset(options.mongo, tenantID, id, {
// Update the Story with the scraped details.
const story = await updateStory(options.mongo, tenantID, id, {
title: meta.title || undefined,
description: meta.description || undefined,
image: meta.image ? meta.image : undefined,
@@ -70,16 +70,16 @@ const createJobProcessor = (
section: meta.section || undefined,
scraped: new Date(),
});
if (!asset) {
if (!story) {
logger.error(
{
job_id: job.id,
job_name: JOB_NAME,
asset_id: id,
asset_url: url,
story_id: id,
story_url: url,
tenant_id: tenantID,
},
"asset at specified id not found, can not update with metadata"
"story at specified id not found, can not update with metadata"
);
return;
}
@@ -88,11 +88,11 @@ const createJobProcessor = (
{
job_id: job.id,
job_name: JOB_NAME,
asset_id: asset.id,
asset_url: url,
story_id: story.id,
story_url: url,
tenant_id: tenantID,
},
"scraped the asset"
"scraped the story"
);
};
@@ -1,35 +1,35 @@
import { Db } from "mongodb";
import {
findOrCreateAsset,
FindOrCreateAssetInput,
} from "talk-server/models/asset";
findOrCreateStory,
FindOrCreateStoryInput,
} from "talk-server/models/story";
import { Tenant } from "talk-server/models/tenant";
import Task from "talk-server/services/queue/Task";
import { ScraperData } from "talk-server/services/queue/tasks/scraper";
export type FindOrCreateAsset = FindOrCreateAssetInput;
export type FindOrCreateStory = FindOrCreateStoryInput;
export async function findOrCreate(
db: Db,
tenant: Tenant,
input: FindOrCreateAsset,
input: FindOrCreateStory,
scraper: Task<ScraperData>
) {
// TODO: check to see if the tenant has enabled lazy asset creation.
// TODO: check to see if the tenant has enabled lazy story creation.
const asset = await findOrCreateAsset(db, tenant.id, input);
if (!asset) {
const story = await findOrCreateStory(db, tenant.id, input);
if (!story) {
return null;
}
if (!asset.scraped) {
if (!story.scraped) {
await scraper.add({
assetID: asset.id,
assetURL: asset.url,
storyID: story.id,
storyURL: story.url,
tenantID: tenant.id,
});
}
return asset;
return story;
}