mirror of
https://github.com/wassname/talk.git
synced 2026-09-11 12:51:33 +08:00
[next] Moderate (#2118)
* fix: load .env before building / watching * feat: Implement AppBar, Brand, and SubBar * feat: add card ui component * feat: add modqueue components * feat: implement modqueue * feat: add translations * test: add unit tests * feat: single comment view * test: feature / integration tests for modqueue * test: fix remaining tests * feature: support TextMatchOptions * fix: remove body count marker * fix: remove accidently added package * feat: testHelper toJSON * chore: cleanup + comments * chore: better types * test: fix test * chore: refactor decision history test * chore: tiny fix * fix: adjust to recent server changes * fix: marking suspect and banned words * feat: added moderation queue edge to accept/reject comment payloads - Simplified moderationQueue returns - Simplified resolvers * feat: update counts * feat: added id's to moderation queue and settings * fix+test: test count changes, apply fix * chore: adapt to server change, and remove custom mutation handlers * fix: use common utils * fix: purify fix, babel fix * fix: workaround css treeshake issue and upgrade css plugins * fix: fixed snapshot * fix: support empty word lists * feat: separate client config
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import uuid from "uuid";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { Config } from "talk-server/config";
|
||||
import logger from "talk-server/logger";
|
||||
import { User } from "talk-server/models/user";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
graphqlExpress,
|
||||
} from "apollo-server-express/dist/expressApollo";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { Omit } from "talk-common/types";
|
||||
import { Config } from "talk-server/config";
|
||||
import { LoggerExtension } from "talk-server/graph/common/middleware/extensions/logger";
|
||||
|
||||
// Sourced from: https://github.com/apollographql/apollo-server/blob/958846887598491fadea57b3f9373d129300f250/packages/apollo-server-core/src/ApolloServer.ts#L46-L57
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { RedisPubSub } from "graphql-redis-subscriptions";
|
||||
import { Config } from "talk-common/config";
|
||||
import { Config } from "talk-server/config";
|
||||
import { createRedisClient } from "talk-server/services/redis";
|
||||
|
||||
export function createPubSub(config: Config): RedisPubSub {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { Config } from "talk-server/config";
|
||||
import CommonContext from "talk-server/graph/common/context";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GraphQLSchema } from "graphql";
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { Config } from "talk-server/config";
|
||||
import { graphqlMiddleware } from "talk-server/graph/common/middleware";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { Config } from "talk-server/config";
|
||||
import CommonContext from "talk-server/graph/common/context";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { User } from "talk-server/models/user";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { GraphQLSchema } from "graphql";
|
||||
// import { graphqlBatchHTTPWrapper } from "react-relay-network-layer";
|
||||
|
||||
import { Config } from "talk-common/config";
|
||||
import { graphqlBatchMiddleware } from "talk-server/app/middleware/graphqlBatch";
|
||||
import { Config } from "talk-server/config";
|
||||
import { graphqlMiddleware } from "talk-server/graph/common/middleware";
|
||||
import { Request } from "talk-server/types/express";
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { GQLAcceptCommentPayloadTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { moderationQueuesPayloadResolver } from "./ModerationQueues";
|
||||
|
||||
export const AcceptCommentPayload: GQLAcceptCommentPayloadTypeResolver = {
|
||||
moderationQueues: moderationQueuesPayloadResolver,
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { getLatestRevision } from "talk-server/models/comment";
|
||||
import { createConnection } from "talk-server/models/connection";
|
||||
|
||||
import TenantContext from "../context";
|
||||
import { getURLWithCommentID } from "./util";
|
||||
|
||||
const maybeLoadOnlyID = (
|
||||
ctx: TenantContext,
|
||||
@@ -77,4 +78,12 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
|
||||
// Some resolver optimization.
|
||||
c.parentID ? ctx.loaders.Comments.parents(c, input) : createConnection(),
|
||||
story: (c, input, ctx) => ctx.loaders.Stories.story.load(c.storyID),
|
||||
permalink: async (c, input, ctx) => {
|
||||
const story = await ctx.loaders.Stories.story.load(c.storyID);
|
||||
if (!story) {
|
||||
// TODO: better error reporting?
|
||||
throw new Error("Story not found");
|
||||
}
|
||||
return getURLWithCommentID(story.url, c.id);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../schema/__generated__/types";
|
||||
|
||||
export interface ModerationQueueInput {
|
||||
selector: string;
|
||||
connection: Partial<CommentConnectionInput>;
|
||||
count: number;
|
||||
}
|
||||
@@ -15,6 +16,14 @@ export interface ModerationQueueInput {
|
||||
export const ModerationQueue: GQLModerationQueueTypeResolver<
|
||||
ModerationQueueInput
|
||||
> = {
|
||||
id: ({ selector, connection: { filter } }) => {
|
||||
// NOTE: (wyattjoh) when the queues change shape in the future, investigate adding more dynamicness to this id generation
|
||||
if (filter && filter.storyID) {
|
||||
return selector + "::storyID:" + filter.storyID;
|
||||
}
|
||||
|
||||
return selector;
|
||||
},
|
||||
comments: ({ connection }, { first = 10, after }, { mongo, tenant }) =>
|
||||
retrieveCommentConnection(mongo, tenant.id, {
|
||||
...connection,
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import {
|
||||
AcceptCommentPayloadToModerationQueuesResolver,
|
||||
GQLModerationQueuesTypeResolver,
|
||||
RejectCommentPayloadToModerationQueuesResolver,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { CommentConnectionInput } from "talk-server/models/comment";
|
||||
import { FilterQuery } from "talk-server/models/query";
|
||||
import { CommentModerationCountsPerQueue } from "talk-server/models/story";
|
||||
import {
|
||||
CommentModerationCountsPerQueue,
|
||||
Story,
|
||||
} from "talk-server/models/story";
|
||||
import {
|
||||
PENDING_STATUS,
|
||||
REPORTED_STATUS,
|
||||
UNMODERATED_STATUSES,
|
||||
} from "talk-server/services/comments/moderation/counts";
|
||||
|
||||
import { GQLModerationQueuesTypeResolver } from "../schema/__generated__/types";
|
||||
import TenantContext from "../context";
|
||||
import { ModerationQueueInput } from "./ModerationQueue";
|
||||
|
||||
export interface ModerationQueuesInput {
|
||||
interface ModerationQueuesInput {
|
||||
connection: Partial<CommentConnectionInput>;
|
||||
counts: CommentModerationCountsPerQueue;
|
||||
}
|
||||
@@ -19,6 +27,7 @@ const mergeModerationInputFilters = (
|
||||
filter: FilterQuery<Comment>,
|
||||
selector: keyof CommentModerationCountsPerQueue
|
||||
) => (input: ModerationQueuesInput): ModerationQueueInput => ({
|
||||
selector,
|
||||
connection: {
|
||||
...input.connection,
|
||||
filter: {
|
||||
@@ -29,6 +38,72 @@ const mergeModerationInputFilters = (
|
||||
count: input.counts[selector],
|
||||
});
|
||||
|
||||
/**
|
||||
* storyModerationInputResolver can be used to retrieve the moderationQueue for
|
||||
* a specific Story.
|
||||
*
|
||||
* @param story the story that will be used to base the comment moderation
|
||||
* queues on
|
||||
*/
|
||||
export const storyModerationInputResolver = (
|
||||
story: Story
|
||||
): ModerationQueuesInput => ({
|
||||
connection: {
|
||||
filter: {
|
||||
// This moderationQueues is being sourced from the Story, so require
|
||||
// that all the comments for theses queues are also for this Story.
|
||||
storyID: story.id,
|
||||
},
|
||||
},
|
||||
counts: story.commentCounts.moderationQueue.queues,
|
||||
});
|
||||
|
||||
/**
|
||||
* sharedModerationInputResolver implements the resolver function style which
|
||||
* allows it to be used in a type resolver.
|
||||
*
|
||||
* @param source the source of the type, not used
|
||||
* @param args the args of the type, not used
|
||||
* @param ctx the TenantContext that will be used to get the shared counts
|
||||
*/
|
||||
export const sharedModerationInputResolver = async (
|
||||
source: any,
|
||||
args: any,
|
||||
ctx: TenantContext
|
||||
): Promise<ModerationQueuesInput> => ({
|
||||
// We don't need to filter the connection, as this is tenant wide (tenant
|
||||
// filtering is completed at the model layer).
|
||||
connection: {},
|
||||
counts: await ctx.loaders.Comments.sharedModerationQueueQueuesCounts.load(),
|
||||
});
|
||||
|
||||
/**
|
||||
* moderationQueuesPayloadResolver implements the resolver that can be used for
|
||||
* moderation actions payloads.
|
||||
*
|
||||
* @param source the source of the payload, not used
|
||||
* @param args the args of the payload containing potentially a Story ID
|
||||
* @param ctx the TenantContext for which we can use to retrieve the shared data
|
||||
*/
|
||||
export const moderationQueuesPayloadResolver:
|
||||
| AcceptCommentPayloadToModerationQueuesResolver
|
||||
| RejectCommentPayloadToModerationQueuesResolver = async (
|
||||
source,
|
||||
args,
|
||||
ctx
|
||||
): Promise<ModerationQueuesInput | null> => {
|
||||
if (args.storyID) {
|
||||
const story = await ctx.loaders.Stories.story.load(args.storyID);
|
||||
if (!story) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return storyModerationInputResolver(story);
|
||||
}
|
||||
|
||||
return sharedModerationInputResolver(source, args, ctx);
|
||||
};
|
||||
|
||||
export const ModerationQueues: GQLModerationQueuesTypeResolver<
|
||||
ModerationQueuesInput
|
||||
> = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { GQLQueryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { ModerationQueuesInput } from "./ModerationQueues";
|
||||
import { sharedModerationInputResolver } from "./ModerationQueues";
|
||||
|
||||
export const Query: GQLQueryTypeResolver<void> = {
|
||||
story: (source, args, ctx) => ctx.loaders.Stories.findOrCreate.load(args),
|
||||
@@ -13,14 +13,5 @@ export const Query: GQLQueryTypeResolver<void> = {
|
||||
ctx.loaders.Auth.discoverOIDCConfiguration.load(issuer),
|
||||
debugScrapeStoryMetadata: (source, { url }, ctx) =>
|
||||
ctx.loaders.Stories.debugScrapeMetadata.load(url),
|
||||
moderationQueues: async (
|
||||
source,
|
||||
args,
|
||||
ctx
|
||||
): Promise<ModerationQueuesInput> => ({
|
||||
// We don't need to filter the connection, as this is tenant wide (tenant
|
||||
// filtering is completed at the model layer).
|
||||
connection: {},
|
||||
counts: await ctx.loaders.Comments.sharedModerationQueueQueuesCounts.load(),
|
||||
}),
|
||||
moderationQueues: sharedModerationInputResolver,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { GQLRejectCommentPayloadTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { moderationQueuesPayloadResolver } from "./ModerationQueues";
|
||||
|
||||
export const RejectCommentPayload: GQLRejectCommentPayloadTypeResolver = {
|
||||
moderationQueues: moderationQueuesPayloadResolver,
|
||||
};
|
||||
@@ -3,7 +3,8 @@ import { DateTime } from "luxon";
|
||||
import { GQLStoryTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { decodeActionCounts } from "talk-server/models/action/comment";
|
||||
import * as story from "talk-server/models/story";
|
||||
import { ModerationQueuesInput } from "./ModerationQueues";
|
||||
|
||||
import { storyModerationInputResolver } from "./ModerationQueues";
|
||||
|
||||
export const Story: GQLStoryTypeResolver<story.Story> = {
|
||||
comments: (s, input, ctx) => ctx.loaders.Comments.forStory(s.id, input),
|
||||
@@ -23,14 +24,5 @@ export const Story: GQLStoryTypeResolver<story.Story> = {
|
||||
},
|
||||
commentActionCounts: s => decodeActionCounts(s.commentCounts.action),
|
||||
commentCounts: s => s.commentCounts.status,
|
||||
moderationQueues: (s): ModerationQueuesInput => ({
|
||||
connection: {
|
||||
filter: {
|
||||
// This moderationQueues is being sourced from the Story, so require
|
||||
// that all the comments for theses queues are also for this Story.
|
||||
storyID: s.id,
|
||||
},
|
||||
},
|
||||
counts: s.commentCounts.moderationQueue.queues,
|
||||
}),
|
||||
moderationQueues: storyModerationInputResolver,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import Time from "talk-server/graph/common/scalars/time";
|
||||
|
||||
import { GQLResolver } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
import { AcceptCommentPayload } from "./AcceptCommentPayload";
|
||||
import { AuthIntegrations } from "./AuthIntegrations";
|
||||
import { Comment } from "./Comment";
|
||||
import { CommentCounts } from "./CommentCounts";
|
||||
@@ -16,10 +17,12 @@ import { Mutation } from "./Mutation";
|
||||
import { OIDCAuthIntegration } from "./OIDCAuthIntegration";
|
||||
import { Profile } from "./Profile";
|
||||
import { Query } from "./Query";
|
||||
import { RejectCommentPayload } from "./RejectCommentPayload";
|
||||
import { Story } from "./Story";
|
||||
import { User } from "./User";
|
||||
|
||||
const Resolvers: GQLResolver = {
|
||||
AcceptCommentPayload,
|
||||
AuthIntegrations,
|
||||
Comment,
|
||||
CommentCounts,
|
||||
@@ -34,6 +37,7 @@ const Resolvers: GQLResolver = {
|
||||
GoogleAuthIntegration,
|
||||
Profile,
|
||||
Query,
|
||||
RejectCommentPayload,
|
||||
Time,
|
||||
Story,
|
||||
User,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { GraphQLResolveInfo } from "graphql";
|
||||
import graphqlFields from "graphql-fields";
|
||||
import { pull } from "lodash";
|
||||
import { parseQuery, stringifyQuery } from "talk-common/utils";
|
||||
import { URL } from "url";
|
||||
|
||||
/**
|
||||
* getRequestedFields returns the fields in an array that are being queried for.
|
||||
@@ -10,3 +12,17 @@ import { pull } from "lodash";
|
||||
export function getRequestedFields<T>(info: GraphQLResolveInfo) {
|
||||
return pull(Object.keys(graphqlFields<T>(info)), "__typename");
|
||||
}
|
||||
|
||||
/**
|
||||
* getURLWithCommentID returns the url with the comment id.
|
||||
*
|
||||
* @param storyURL url of the story
|
||||
* @param commentID id of the comment
|
||||
*/
|
||||
export function getURLWithCommentID(storyURL: string, commentID?: string) {
|
||||
const url = new URL(storyURL);
|
||||
const query = parseQuery(url.search);
|
||||
url.search = stringifyQuery({ ...query, commentID });
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
@@ -248,6 +248,11 @@ type WordList {
|
||||
ModerationQueue returns the Comments associated with a Moderation Queue.
|
||||
"""
|
||||
type ModerationQueue {
|
||||
"""
|
||||
id is a canonical identifier for this specific moderation queue.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
count will return the number of Comments that are in this queue.
|
||||
"""
|
||||
@@ -782,6 +787,11 @@ type ReactionConfiguration {
|
||||
Settings stores the global settings for a given Tenant.
|
||||
"""
|
||||
type Settings {
|
||||
"""
|
||||
id is a canonical identifier for this Tenant.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
domain is the domain that is associated with this Tenant.
|
||||
"""
|
||||
@@ -1268,6 +1278,11 @@ type Comment {
|
||||
story is the Story that the Comment was written on.
|
||||
"""
|
||||
story: Story!
|
||||
|
||||
"""
|
||||
The permalink for this comment.
|
||||
"""
|
||||
permalink: String!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
@@ -1463,7 +1478,7 @@ type Story {
|
||||
moderationQueues returns the set of ModerationQueues that are available for
|
||||
this Story.
|
||||
"""
|
||||
moderationQueues: ModerationQueues @auth(roles: [ADMIN, MODERATOR])
|
||||
moderationQueues: ModerationQueues! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
closedAt is the Time that the Story is closed for commenting.
|
||||
@@ -1591,7 +1606,7 @@ type Query {
|
||||
moderationQueues returns the set of ModerationQueues that are available for
|
||||
all stories.
|
||||
"""
|
||||
moderationQueues: ModerationQueues @auth(roles: [ADMIN, MODERATOR])
|
||||
moderationQueues: ModerationQueues! @auth(roles: [ADMIN, MODERATOR])
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -2867,6 +2882,13 @@ type AcceptCommentPayload {
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
moderationQueues will return the selected moderation queues. If the `storyID`
|
||||
is provided, it will filter the moderation queues for only comments in that
|
||||
Story.
|
||||
"""
|
||||
moderationQueues(storyID: ID): ModerationQueues
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
@@ -2900,6 +2922,13 @@ type RejectCommentPayload {
|
||||
"""
|
||||
comment: Comment
|
||||
|
||||
"""
|
||||
moderationQueues will return the selected moderation queues. If the `storyID`
|
||||
is provided, it will filter the moderation queues for only comments in that
|
||||
Story.
|
||||
"""
|
||||
moderationQueues(storyID: ID): ModerationQueues
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user