mirror of
https://github.com/wassname/talk.git
synced 2026-08-09 12:30:42 +08:00
[CORL-159, CORL-160] Stream Config Tab (#2219)
* feat: Implement stream configuration tab * feat: split profile & configure into separate bundles * chore: better role logic * fix+chore: add test cases, implement expectAndFail, refactor tests * chore: add some comments * chore: Update src/core/client/framework/lib/form/helpers.tsx Co-Authored-By: cvle <vinh@wikiwi.io> * feat: support new graphql mutations/schema * fix: ci fixes * fix: improvement to revision loading * fix: updated some tests * fix: adapt client to changes * fix: remove obsolote isClosed in UpdateStory * ci: increase no_output_timeout for build
This commit is contained in:
@@ -23,14 +23,16 @@ export interface TenantInstallBody {
|
||||
const TenantInstallBodySchema = Joi.object().keys({
|
||||
tenant: Joi.object()
|
||||
.keys({
|
||||
organizationName: Joi.string().trim(),
|
||||
organizationURL: Joi.string()
|
||||
.trim()
|
||||
.uri(),
|
||||
organizationContactEmail: Joi.string()
|
||||
.trim()
|
||||
.lowercase()
|
||||
.email(),
|
||||
organization: Joi.object().keys({
|
||||
name: Joi.string().trim(),
|
||||
url: Joi.string()
|
||||
.trim()
|
||||
.uri(),
|
||||
contactEmail: Joi.string()
|
||||
.trim()
|
||||
.lowercase()
|
||||
.email(),
|
||||
}),
|
||||
domains: Joi.array().items(
|
||||
Joi.string()
|
||||
.trim()
|
||||
|
||||
@@ -4,14 +4,25 @@ import { ERROR_CODES } from "talk-common/errors";
|
||||
import { mapFieldsetToErrorCodes } from "talk-server/graph/common/errors";
|
||||
import TenantContext from "talk-server/graph/tenant/context";
|
||||
import {
|
||||
GQLCloseStoryInput,
|
||||
GQLCreateStoryInput,
|
||||
GQLMergeStoriesInput,
|
||||
GQLOpenStoryInput,
|
||||
GQLRemoveStoryInput,
|
||||
GQLScrapeStoryInput,
|
||||
GQLUpdateStoryInput,
|
||||
GQLUpdateStorySettingsInput,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { Story } from "talk-server/models/story";
|
||||
import { create, merge, remove, update } from "talk-server/services/stories";
|
||||
import {
|
||||
close,
|
||||
create,
|
||||
merge,
|
||||
open,
|
||||
remove,
|
||||
update,
|
||||
updateSettings,
|
||||
} from "talk-server/services/stories";
|
||||
import { scrape } from "talk-server/services/stories/scraper";
|
||||
|
||||
export const Stories = (ctx: TenantContext) => ({
|
||||
@@ -33,7 +44,7 @@ export const Stories = (ctx: TenantContext) => ({
|
||||
),
|
||||
update: async (input: GQLUpdateStoryInput): Promise<Readonly<Story> | null> =>
|
||||
mapFieldsetToErrorCodes(
|
||||
update(ctx.mongo, ctx.tenant, input.id, omitBy(input.story, isNull)),
|
||||
update(ctx.mongo, ctx.tenant, input.id, input.story),
|
||||
{
|
||||
"input.story.url": [
|
||||
ERROR_CODES.STORY_URL_NOT_PERMITTED,
|
||||
@@ -41,6 +52,14 @@ export const Stories = (ctx: TenantContext) => ({
|
||||
],
|
||||
}
|
||||
),
|
||||
updateSettings: async (
|
||||
input: GQLUpdateStorySettingsInput
|
||||
): Promise<Readonly<Story> | null> =>
|
||||
updateSettings(ctx.mongo, ctx.tenant, input.id, input.settings),
|
||||
close: (input: GQLCloseStoryInput): Promise<Readonly<Story> | null> =>
|
||||
close(ctx.mongo, ctx.tenant, input.id),
|
||||
open: (input: GQLOpenStoryInput): Promise<Readonly<Story> | null> =>
|
||||
open(ctx.mongo, ctx.tenant, input.id),
|
||||
merge: async (input: GQLMergeStoriesInput): Promise<Readonly<Story> | null> =>
|
||||
merge(
|
||||
ctx.mongo,
|
||||
|
||||
@@ -59,6 +59,18 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
story: await ctx.mutators.Stories.update(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateStorySettings: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Stories.updateSettings(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
closeStory: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Stories.close(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
openStory: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Stories.open(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
mergeStories: async (source, { input }, ctx) => ({
|
||||
story: await ctx.mutators.Stories.merge(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { defaultsDeep } from "lodash";
|
||||
|
||||
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";
|
||||
@@ -8,12 +10,14 @@ import { storyModerationInputResolver } from "./ModerationQueues";
|
||||
export const Story: GQLStoryTypeResolver<story.Story> = {
|
||||
comments: (s, input, ctx) => ctx.loaders.Comments.forStory(s.id, input),
|
||||
isClosed: (s, input, ctx) => {
|
||||
const closedAt = getStoryClosedAt(ctx.tenant, s);
|
||||
const closedAt = getStoryClosedAt(ctx.tenant, s) || null;
|
||||
return !!closedAt && new Date() >= closedAt;
|
||||
},
|
||||
closedAt: (s, input, ctx) => getStoryClosedAt(ctx.tenant, s),
|
||||
closedAt: (s, input, ctx) => getStoryClosedAt(ctx.tenant, s) || null,
|
||||
commentActionCounts: s => decodeActionCounts(s.commentCounts.action),
|
||||
commentCounts: s => s.commentCounts.status,
|
||||
moderation: (s, input, ctx) => ctx.tenant.moderation,
|
||||
// Merge tenant settings into the story settings so we can easily inherit the
|
||||
// options if they exist.
|
||||
settings: (s, input, ctx) => defaultsDeep({}, s.settings, ctx.tenant),
|
||||
moderationQueues: storyModerationInputResolver,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as story from "talk-server/models/story";
|
||||
|
||||
import { GQLStorySettingsTypeResolver } from "../schema/__generated__/types";
|
||||
|
||||
export const StorySettings: GQLStorySettingsTypeResolver<
|
||||
story.StorySettings
|
||||
> = {
|
||||
moderation: (s, input, ctx) => s.moderation || ctx.tenant.moderation,
|
||||
premodLinksEnable: (s, input, ctx) =>
|
||||
s.premodLinksEnable || ctx.tenant.premodLinksEnable,
|
||||
messageBox: s => {
|
||||
if (s.messageBox) {
|
||||
return s.messageBox;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -18,6 +18,7 @@ import { Profile } from "./Profile";
|
||||
import { Query } from "./Query";
|
||||
import { RejectCommentPayload } from "./RejectCommentPayload";
|
||||
import { Story } from "./Story";
|
||||
import { StorySettings } from "./StorySettings";
|
||||
import { User } from "./User";
|
||||
|
||||
const Resolvers: GQLResolver = {
|
||||
@@ -38,6 +39,7 @@ const Resolvers: GQLResolver = {
|
||||
Query,
|
||||
RejectCommentPayload,
|
||||
Story,
|
||||
StorySettings,
|
||||
Time,
|
||||
User,
|
||||
};
|
||||
|
||||
@@ -856,6 +856,27 @@ type CommunityGuidelines {
|
||||
content: String
|
||||
}
|
||||
|
||||
"""
|
||||
StoryMessageBox stores settings related to the Story Message Box.
|
||||
"""
|
||||
type StoryMessageBox {
|
||||
"""
|
||||
enable when true will enable the Message Box on the comment stream.
|
||||
"""
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
icon when set will reference the string for the Message box used by the
|
||||
Message Box.
|
||||
"""
|
||||
icon: String
|
||||
|
||||
"""
|
||||
content when set contains the actual markup for the Message Box.
|
||||
"""
|
||||
content: String
|
||||
}
|
||||
|
||||
################################################################################
|
||||
## Settings
|
||||
################################################################################
|
||||
@@ -870,6 +891,51 @@ enum LOCALES {
|
||||
de
|
||||
}
|
||||
|
||||
"""
|
||||
CloseCommenting contains settings related to the automatic closing of commenting
|
||||
on Stories.
|
||||
"""
|
||||
type CloseCommenting {
|
||||
"""
|
||||
auto when true will configure the automatic close on each story as they are
|
||||
created based on the current configured timeout option.
|
||||
"""
|
||||
auto: Boolean!
|
||||
|
||||
"""
|
||||
timeout is the amount of seconds from the `createdAt` timestamp that a given
|
||||
story will be considered closed.
|
||||
"""
|
||||
timeout: Int!
|
||||
|
||||
"""
|
||||
message when provided will be the message that shows when the comment stream
|
||||
is closed for commenting.
|
||||
"""
|
||||
message: String
|
||||
}
|
||||
|
||||
"""
|
||||
Organization stores information about the organization behind this specific
|
||||
instance of Talk.
|
||||
"""
|
||||
type Organization {
|
||||
"""
|
||||
name is the name of the organization.
|
||||
"""
|
||||
name: String!
|
||||
|
||||
"""
|
||||
contactEmail is the email of the organization.
|
||||
"""
|
||||
contactEmail: String!
|
||||
|
||||
"""
|
||||
url is the URL to the organizations home page.
|
||||
"""
|
||||
url: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Settings stores the global settings for a given Tenant.
|
||||
"""
|
||||
@@ -887,7 +953,7 @@ type Settings {
|
||||
"""
|
||||
domains will return a given list of permitted domains.
|
||||
"""
|
||||
domains: [String!] @auth(roles: [ADMIN])
|
||||
domains: [String!]! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
locale is the specified locale for this Tenant.
|
||||
@@ -899,60 +965,27 @@ type Settings {
|
||||
"""
|
||||
moderation: MODERATION_MODE @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
Enables a requirement for email confirmation before a user can login.
|
||||
"""
|
||||
requireEmailConfirmation: Boolean!
|
||||
|
||||
"""
|
||||
communityGuidelines will be shown in the comments stream.
|
||||
"""
|
||||
communityGuidelines: CommunityGuidelines!
|
||||
|
||||
"""
|
||||
questionBoxEnable will enable the Question Box's content to be visible above
|
||||
the comment box.
|
||||
"""
|
||||
questionBoxEnable: Boolean!
|
||||
|
||||
"""
|
||||
questionBoxContent is the content of the Question Box.
|
||||
"""
|
||||
questionBoxContent: String
|
||||
|
||||
"""
|
||||
questionBoxIcon is the icon for the Question Box.
|
||||
"""
|
||||
questionBoxIcon: String
|
||||
|
||||
"""
|
||||
premodLinksEnable will put all comments that contain links into premod.
|
||||
"""
|
||||
premodLinksEnable: Boolean @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
autoCloseStream when true will auto close the stream when the `closeTimeout`
|
||||
amount of seconds have been reached.
|
||||
closeCommenting contains settings related to the automatic closing of commenting on
|
||||
Stories.
|
||||
"""
|
||||
autoCloseStream: Boolean! @auth(roles: [ADMIN])
|
||||
closeCommenting: CloseCommenting!
|
||||
|
||||
"""
|
||||
customCSSURL is the URL of the custom CSS used to display on the frontend.
|
||||
"""
|
||||
customCSSURL: String
|
||||
|
||||
"""
|
||||
closedTimeout is the amount of time (in seconds) from the createdAt timestamp
|
||||
that a given story will be considered closed.
|
||||
"""
|
||||
closedTimeout: Int!
|
||||
|
||||
"""
|
||||
closedMessage is the message shown to the user when the given Story is
|
||||
closed.
|
||||
"""
|
||||
closedMessage: String
|
||||
|
||||
"""
|
||||
disableCommenting will disable commenting site-wide.
|
||||
"""
|
||||
@@ -970,14 +1003,10 @@ type Settings {
|
||||
charCount: CharCount!
|
||||
|
||||
"""
|
||||
organizationName is the name of the organization.
|
||||
organization stores information about the organization behind this specific
|
||||
instance of Talk.
|
||||
"""
|
||||
organizationName: String!
|
||||
|
||||
"""
|
||||
organizationContactEmail is the email of the organization.
|
||||
"""
|
||||
organizationContactEmail: String!
|
||||
organization: Organization!
|
||||
|
||||
"""
|
||||
email is the set of credentials and settings associated with the organization.
|
||||
@@ -1567,6 +1596,23 @@ type StoryMetadata {
|
||||
section: String
|
||||
}
|
||||
|
||||
type StorySettings {
|
||||
"""
|
||||
moderation determines whether or not this is a PRE or POST moderated story.
|
||||
"""
|
||||
moderation: MODERATION_MODE!
|
||||
|
||||
"""
|
||||
premodLinksEnable will put all comments that contain links into premod.
|
||||
"""
|
||||
premodLinksEnable: Boolean!
|
||||
|
||||
"""
|
||||
messageBox stores settings related to the Story Message Box.
|
||||
"""
|
||||
messageBox: StoryMessageBox!
|
||||
}
|
||||
|
||||
"""
|
||||
STORY_STATUS represents filtering states that a Story can be in.
|
||||
"""
|
||||
@@ -1650,9 +1696,10 @@ type Story {
|
||||
createdAt: Time!
|
||||
|
||||
"""
|
||||
moderation determines whether or not this is a PRE or POST moderated story.
|
||||
settings is the set of Settings on a Story that inherit from the global
|
||||
settings.
|
||||
"""
|
||||
moderation: MODERATION_MODE!
|
||||
settings: StorySettings!
|
||||
}
|
||||
|
||||
"""
|
||||
@@ -1710,7 +1757,7 @@ type Query {
|
||||
after: Cursor
|
||||
storyID: ID
|
||||
status: COMMENT_STATUS
|
||||
): CommentsConnection @auth(roles: [ADMIN, MODERATOR])
|
||||
): CommentsConnection! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
story is a specific article that can be identified by either an ID or a URL.
|
||||
@@ -1724,7 +1771,7 @@ type Query {
|
||||
first: Int = 10
|
||||
after: Cursor
|
||||
status: STORY_STATUS
|
||||
): StoriesConnection @auth(roles: [ADMIN, MODERATOR])
|
||||
): StoriesConnection! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
user will return the user referenced by their ID.
|
||||
@@ -2268,6 +2315,43 @@ input SettingsDisableCommentingInput {
|
||||
message: String
|
||||
}
|
||||
|
||||
input SettingsOrganizationInput {
|
||||
"""
|
||||
name is the name of the organization.
|
||||
"""
|
||||
name: String
|
||||
|
||||
"""
|
||||
contactEmail is the email of the organization.
|
||||
"""
|
||||
contactEmail: String
|
||||
|
||||
"""
|
||||
url is the URL to the organizations home page.
|
||||
"""
|
||||
url: String
|
||||
}
|
||||
|
||||
input SettingsCloseCommentingInput {
|
||||
"""
|
||||
auto when true will configure the automatic close on each story as they are
|
||||
created based on the current configured timeout option.
|
||||
"""
|
||||
auto: Boolean
|
||||
|
||||
"""
|
||||
timeout is the amount of seconds from the `createdAt` timestamp that a given
|
||||
story will be considered closed.
|
||||
"""
|
||||
timeout: Int
|
||||
|
||||
"""
|
||||
message when provided will be the message that shows when the comment stream
|
||||
is closed for commenting.
|
||||
"""
|
||||
message: String
|
||||
}
|
||||
|
||||
"""
|
||||
SettingsInput is the partial type of the Settings type for performing mutations.
|
||||
"""
|
||||
@@ -2282,60 +2366,21 @@ input SettingsInput {
|
||||
"""
|
||||
moderation: MODERATION_MODE
|
||||
|
||||
"""
|
||||
Enables a requirement for email confirmation before a user can login.
|
||||
"""
|
||||
requireEmailConfirmation: Boolean
|
||||
|
||||
"""
|
||||
communityGuidelines will be shown in the comments stream.
|
||||
"""
|
||||
communityGuidelines: SettingsCommunityGuidelinesInput
|
||||
|
||||
"""
|
||||
questionBoxEnable will enable the Question Box's content to be visible above
|
||||
the comment box.
|
||||
"""
|
||||
questionBoxEnable: Boolean
|
||||
|
||||
"""
|
||||
questionBoxContent is the content of the Question Box.
|
||||
"""
|
||||
questionBoxContent: String
|
||||
|
||||
"""
|
||||
questionBoxIcon is the icon for the Question Box.
|
||||
"""
|
||||
questionBoxIcon: String
|
||||
|
||||
"""
|
||||
premodLinksEnable will put all comments that contain links into premod.
|
||||
"""
|
||||
premodLinksEnable: Boolean
|
||||
|
||||
"""
|
||||
autoCloseStream when true will auto close the stream when the `closeTimeout`
|
||||
amount of seconds have been reached.
|
||||
"""
|
||||
autoCloseStream: Boolean
|
||||
|
||||
"""
|
||||
customCSSURL is the URL of the custom CSS used to display on the frontend.
|
||||
"""
|
||||
customCSSURL: String
|
||||
|
||||
"""
|
||||
closedTimeout is the amount of seconds from the createdAt timestamp that a
|
||||
given story will be considered closed.
|
||||
"""
|
||||
closedTimeout: Int
|
||||
|
||||
"""
|
||||
closedMessage is the message shown to the user when the given Story is
|
||||
closed.
|
||||
"""
|
||||
closedMessage: String
|
||||
|
||||
"""
|
||||
disableCommenting will disable commenting site-wide.
|
||||
"""
|
||||
@@ -2348,14 +2393,16 @@ input SettingsInput {
|
||||
editCommentWindowLength: Int
|
||||
|
||||
"""
|
||||
organizationName is the name of the organization.
|
||||
organization stores information about the organization behind this specific
|
||||
instance of Talk.
|
||||
"""
|
||||
organizationName: String
|
||||
organization: SettingsOrganizationInput
|
||||
|
||||
"""
|
||||
organizationContactEmail is the email of the organization.
|
||||
closeCommenting contains settings related to the automatic closing of commenting on
|
||||
Stories.
|
||||
"""
|
||||
organizationContactEmail: String
|
||||
closeCommenting: SettingsCloseCommentingInput
|
||||
|
||||
"""
|
||||
wordList will return a given list of words.
|
||||
@@ -2726,11 +2773,6 @@ input UpdateStory {
|
||||
be scraped, but can be provided here.
|
||||
"""
|
||||
metadata: StoryMetadataInput
|
||||
|
||||
"""
|
||||
isClosed is true when the Story should be closed for commenting.
|
||||
"""
|
||||
isClosed: Boolean
|
||||
}
|
||||
|
||||
input UpdateStoryInput {
|
||||
@@ -2764,6 +2806,140 @@ type UpdateStoryPayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## updateStorySettings
|
||||
##################
|
||||
|
||||
"""
|
||||
StoryMessageBoxInput stores settings related to the Story Message Box.
|
||||
"""
|
||||
input StoryMessageBoxInput {
|
||||
"""
|
||||
enable when true will enable the Message Box on the comment stream.
|
||||
"""
|
||||
enabled: Boolean
|
||||
|
||||
"""
|
||||
icon when set will reference the string for the Message box used by the
|
||||
Message Box.
|
||||
"""
|
||||
icon: String
|
||||
|
||||
"""
|
||||
content when set contains the actual markup for the Message Box.
|
||||
"""
|
||||
content: String
|
||||
}
|
||||
|
||||
"""
|
||||
UpdateStorySettings is the input required to update a Story's Settings.
|
||||
"""
|
||||
input UpdateStorySettings {
|
||||
"""
|
||||
moderation determines whether or not this is a PRE or POST moderated story.
|
||||
"""
|
||||
moderation: MODERATION_MODE
|
||||
|
||||
"""
|
||||
premodLinksEnable will put all comments that contain links into premod.
|
||||
"""
|
||||
premodLinksEnable: Boolean
|
||||
|
||||
"""
|
||||
messageBox stores settings related to the Story Message Box.
|
||||
"""
|
||||
messageBox: StoryMessageBoxInput
|
||||
}
|
||||
|
||||
input UpdateStorySettingsInput {
|
||||
"""
|
||||
id is the identifier of the Story used either when the Story was created via
|
||||
the API or from Talk when it was lazily created.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
settings contains the fields on the story settings that should be updated. Any
|
||||
fields not specified will not be changed.
|
||||
"""
|
||||
settings: UpdateStorySettings!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type UpdateStorySettingsPayload {
|
||||
"""
|
||||
story is the Story that was possibly updated.
|
||||
"""
|
||||
story: Story
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## closeStory
|
||||
##################
|
||||
|
||||
input CloseStoryInput {
|
||||
"""
|
||||
id is the identifier of the Story used either when the Story was created via
|
||||
the API or from Talk when it was lazily created.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type CloseStoryPayload {
|
||||
"""
|
||||
story is the Story that was possibly updated.
|
||||
"""
|
||||
story: Story
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## openStory
|
||||
##################
|
||||
|
||||
input OpenStoryInput {
|
||||
"""
|
||||
id is the identifier of the Story used either when the Story was created via
|
||||
the API or from Talk when it was lazily created.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type OpenStoryPayload {
|
||||
"""
|
||||
story is the Story that was possibly updated.
|
||||
"""
|
||||
story: Story
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
## mergeStories
|
||||
##################
|
||||
@@ -3269,7 +3445,7 @@ type Mutation {
|
||||
"""
|
||||
createComment will create a Comment as the current logged in User.
|
||||
"""
|
||||
createComment(input: CreateCommentInput!): CreateCommentPayload @auth
|
||||
createComment(input: CreateCommentInput!): CreateCommentPayload! @auth
|
||||
|
||||
"""
|
||||
createCommentReply will create a Comment as the current logged in User that is
|
||||
@@ -3277,25 +3453,25 @@ type Mutation {
|
||||
"""
|
||||
createCommentReply(
|
||||
input: CreateCommentReplyInput!
|
||||
): CreateCommentReplyPayload @auth
|
||||
): CreateCommentReplyPayload! @auth
|
||||
|
||||
"""
|
||||
editComment will allow the author of a comment to change the body within the
|
||||
time allotment.
|
||||
"""
|
||||
editComment(input: EditCommentInput!): EditCommentPayload @auth
|
||||
editComment(input: EditCommentInput!): EditCommentPayload! @auth
|
||||
|
||||
"""
|
||||
updateSettings will update the Settings for the given Tenant.
|
||||
"""
|
||||
updateSettings(input: UpdateSettingsInput!): UpdateSettingsPayload
|
||||
updateSettings(input: UpdateSettingsInput!): UpdateSettingsPayload!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
regenerateSSOKey will regenerate the SSO key used to sign secrets. This will
|
||||
invalidate any existing user sessions.
|
||||
"""
|
||||
regenerateSSOKey(input: RegenerateSSOKeyInput!): RegenerateSSOKeyPayload
|
||||
regenerateSSOKey(input: RegenerateSSOKeyInput!): RegenerateSSOKeyPayload!
|
||||
@auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
@@ -3334,77 +3510,96 @@ type Mutation {
|
||||
createCommentFlag will create a Flag authored by the current logged in User on
|
||||
a given Comment.
|
||||
"""
|
||||
createCommentFlag(input: CreateCommentFlagInput!): CreateCommentFlagPayload
|
||||
createCommentFlag(input: CreateCommentFlagInput!): CreateCommentFlagPayload!
|
||||
@auth
|
||||
|
||||
"""
|
||||
createStory will create the provided Story.
|
||||
"""
|
||||
createStory(input: CreateStoryInput!): CreateStoryPayload
|
||||
createStory(input: CreateStoryInput!): CreateStoryPayload!
|
||||
@auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
updateStory will update the given Story.
|
||||
"""
|
||||
updateStory(input: UpdateStoryInput!): UpdateStoryPayload
|
||||
updateStory(input: UpdateStoryInput!): UpdateStoryPayload!
|
||||
@auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
updateStory will update the given Story's settings.
|
||||
"""
|
||||
updateStorySettings(
|
||||
input: UpdateStorySettingsInput!
|
||||
): UpdateStorySettingsPayload! @auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
closeStory will close the given story for commenting.
|
||||
"""
|
||||
closeStory(input: CloseStoryInput!): CloseStoryPayload!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
openStory will open the given story for commenting.
|
||||
"""
|
||||
openStory(input: OpenStoryInput!): OpenStoryPayload!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
mergeStories will merge two stories together, merging their comment streams.
|
||||
This operation is irreversible.
|
||||
"""
|
||||
mergeStories(input: MergeStoriesInput!): MergeStoriesPayload
|
||||
mergeStories(input: MergeStoriesInput!): MergeStoriesPayload!
|
||||
@auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
removeStory will remove the given Story.
|
||||
"""
|
||||
removeStory(input: RemoveStoryInput!): RemoveStoryPayload
|
||||
removeStory(input: RemoveStoryInput!): RemoveStoryPayload!
|
||||
@auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
scrapeStory will scrape the given Story and update the scraped metadata.
|
||||
"""
|
||||
scrapeStory(input: ScrapeStoryInput!): ScrapeStoryPayload
|
||||
scrapeStory(input: ScrapeStoryInput!): ScrapeStoryPayload!
|
||||
@auth(roles: [ADMIN, MODERATOR])
|
||||
|
||||
"""
|
||||
acceptComment will mark the Comment as ACCEPTED.
|
||||
"""
|
||||
acceptComment(input: AcceptCommentInput!): AcceptCommentPayload
|
||||
acceptComment(input: AcceptCommentInput!): AcceptCommentPayload!
|
||||
@auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
rejectComment will mark the Comment as REJECTED.
|
||||
"""
|
||||
rejectComment(input: RejectCommentInput!): RejectCommentPayload
|
||||
rejectComment(input: RejectCommentInput!): RejectCommentPayload!
|
||||
@auth(roles: [MODERATOR, ADMIN])
|
||||
|
||||
"""
|
||||
setUsername will set the username on the current User if they have not set one
|
||||
before. This mutation will fail if the username is already set.
|
||||
"""
|
||||
setUsername(input: SetUsernameInput!): SetUsernamePayload
|
||||
setUsername(input: SetUsernameInput!): SetUsernamePayload!
|
||||
@auth(permit: [MISSING_NAME, MISSING_EMAIL])
|
||||
|
||||
"""
|
||||
setEmail will set the email address on the current User if they have not set
|
||||
one already. This mutation will fail if the email address is already set.
|
||||
"""
|
||||
setEmail(input: SetEmailInput!): SetEmailPayload
|
||||
setEmail(input: SetEmailInput!): SetEmailPayload!
|
||||
@auth(permit: [MISSING_EMAIL])
|
||||
|
||||
"""
|
||||
setPassword will set the password on the current User if they have not set
|
||||
one already. This mutation will fail if the password is already set.
|
||||
"""
|
||||
setPassword(input: SetPasswordInput!): SetPasswordPayload @auth
|
||||
setPassword(input: SetPasswordInput!): SetPasswordPayload! @auth
|
||||
|
||||
"""
|
||||
updatePassword allows the current logged in User to change their password if
|
||||
they already have one associated with them.
|
||||
"""
|
||||
updatePassword(input: UpdatePasswordInput!): UpdatePasswordPayload @auth
|
||||
updatePassword(input: UpdatePasswordInput!): UpdatePasswordPayload! @auth
|
||||
|
||||
"""
|
||||
createToken allows an administrator to create a Token based on the current
|
||||
|
||||
@@ -1,46 +1,20 @@
|
||||
import { Omit } from "talk-common/types";
|
||||
import {
|
||||
GQLCharCount,
|
||||
GQLDisableCommenting,
|
||||
GQLEmail,
|
||||
GQLExternalIntegrations,
|
||||
GQLAuth,
|
||||
GQLFacebookAuthIntegration,
|
||||
GQLGoogleAuthIntegration,
|
||||
GQLKarma,
|
||||
GQLLocalAuthIntegration,
|
||||
GQLMODERATION_MODE,
|
||||
GQLOIDCAuthIntegration,
|
||||
GQLReactionConfiguration,
|
||||
GQLSettings,
|
||||
GQLSSOAuthIntegration,
|
||||
GQLWordList,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
|
||||
export interface ModerationSettings {
|
||||
export interface GlobalModerationSettings {
|
||||
moderation: GQLMODERATION_MODE;
|
||||
requireEmailConfirmation: boolean;
|
||||
communityGuidelines: {
|
||||
enabled: boolean;
|
||||
content?: string;
|
||||
};
|
||||
questionBoxEnable: boolean;
|
||||
questionBoxIcon?: string;
|
||||
questionBoxContent?: string;
|
||||
premodLinksEnable: boolean;
|
||||
autoCloseStream: boolean;
|
||||
|
||||
/**
|
||||
* closedTimeout is the amount of seconds from the createdAt timestamp that a
|
||||
* given story will be considered closed.
|
||||
*/
|
||||
closedTimeout: number;
|
||||
closedMessage?: string;
|
||||
disableCommenting: GQLDisableCommenting;
|
||||
charCount: GQLCharCount;
|
||||
}
|
||||
|
||||
export type LocalAuthIntegration = GQLLocalAuthIntegration;
|
||||
export type SSOAuthIntegration = GQLSSOAuthIntegration;
|
||||
|
||||
export type OIDCAuthIntegration = Omit<
|
||||
GQLOIDCAuthIntegration,
|
||||
"callbackURL" | "redirectURL"
|
||||
@@ -57,63 +31,38 @@ export type FacebookAuthIntegration = Omit<
|
||||
>;
|
||||
|
||||
export interface AuthIntegrations {
|
||||
local: LocalAuthIntegration;
|
||||
sso: SSOAuthIntegration;
|
||||
local: GQLLocalAuthIntegration;
|
||||
sso: GQLSSOAuthIntegration;
|
||||
oidc: OIDCAuthIntegration;
|
||||
google: GoogleAuthIntegration;
|
||||
facebook: FacebookAuthIntegration;
|
||||
}
|
||||
|
||||
export interface Auth {
|
||||
export type Auth = Omit<GQLAuth, "integrations"> & {
|
||||
/**
|
||||
* integrations are the set of configurations for the variations of
|
||||
* authentication solutions.
|
||||
*/
|
||||
integrations: AuthIntegrations;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Settings extends ModerationSettings {
|
||||
/**
|
||||
* customCSSURL is the URL that can be specified by the Tenant to describe a
|
||||
* URL that contains custom styles to be applied to the Stream.
|
||||
*/
|
||||
customCSSURL?: string;
|
||||
|
||||
/**
|
||||
* editCommentWindowLength is the length of time (in seconds) after a comment
|
||||
* is posted that it can still be edited by the author.
|
||||
*/
|
||||
editCommentWindowLength: number;
|
||||
|
||||
/**
|
||||
* email is the set of credentials and settings associated with the
|
||||
* Tenant.
|
||||
*/
|
||||
email: GQLEmail;
|
||||
|
||||
/**
|
||||
* karma is the set of settings related to how user Trust and Karma are
|
||||
* handled.
|
||||
*/
|
||||
karma: GQLKarma;
|
||||
|
||||
/**
|
||||
* wordList stores all the banned/suspect words.
|
||||
*/
|
||||
wordList: GQLWordList;
|
||||
|
||||
/**
|
||||
* Set of configured authentication integrations.
|
||||
*/
|
||||
auth: Auth;
|
||||
|
||||
/**
|
||||
* Various integrations with external services.
|
||||
*/
|
||||
integrations: GQLExternalIntegrations;
|
||||
|
||||
/**
|
||||
* reaction specifies the configuration for reactions.
|
||||
*/
|
||||
reaction: GQLReactionConfiguration;
|
||||
}
|
||||
export type Settings = GlobalModerationSettings &
|
||||
Pick<
|
||||
GQLSettings,
|
||||
| "closeCommenting"
|
||||
| "disableCommenting"
|
||||
| "charCount"
|
||||
| "email"
|
||||
| "karma"
|
||||
| "wordList"
|
||||
| "integrations"
|
||||
| "reaction"
|
||||
| "editCommentWindowLength"
|
||||
| "customCSSURL"
|
||||
| "communityGuidelines"
|
||||
> & {
|
||||
/**
|
||||
* Set of configured authentication integrations.
|
||||
*/
|
||||
auth: Auth;
|
||||
};
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { isNull, omitBy } from "lodash";
|
||||
import { Db, MongoError } from "mongodb";
|
||||
import uuid from "uuid";
|
||||
|
||||
import { Omit } from "talk-common/types";
|
||||
import { DeepPartial, Omit } from "talk-common/types";
|
||||
import { dotize } from "talk-common/utils/dotize";
|
||||
import { DuplicateStoryURLError } from "talk-server/errors";
|
||||
import { GQLStoryMetadata } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import Query, {
|
||||
createConnectionOrderVariants,
|
||||
createIndexFactory,
|
||||
} from "talk-server/models/helpers/query";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import {
|
||||
GQLStoryMetadata,
|
||||
GQLStorySettings,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { TenantResource } from "talk-server/models/tenant";
|
||||
|
||||
import {
|
||||
@@ -17,6 +16,10 @@ import {
|
||||
ConnectionInput,
|
||||
resolveConnection,
|
||||
} from "../helpers/connection";
|
||||
import Query, {
|
||||
createConnectionOrderVariants,
|
||||
createIndexFactory,
|
||||
} from "../helpers/query";
|
||||
import {
|
||||
createEmptyCommentModerationQueueCounts,
|
||||
createEmptyCommentStatusCounts,
|
||||
@@ -30,6 +33,10 @@ function collection<T = Story>(mongo: Db) {
|
||||
return mongo.collection<Readonly<T>>("stories");
|
||||
}
|
||||
|
||||
export type StorySettings = DeepPartial<GQLStorySettings>;
|
||||
|
||||
export type StoryMetadata = GQLStoryMetadata;
|
||||
|
||||
export interface Story extends TenantResource {
|
||||
readonly id: string;
|
||||
|
||||
@@ -41,7 +48,7 @@ export interface Story extends TenantResource {
|
||||
/**
|
||||
* metadata stores the scraped metadata from the Story page.
|
||||
*/
|
||||
metadata?: GQLStoryMetadata;
|
||||
metadata?: StoryMetadata;
|
||||
|
||||
/**
|
||||
* scrapedAt is the Time that the Story had it's metadata scraped at.
|
||||
@@ -57,7 +64,7 @@ export interface Story extends TenantResource {
|
||||
* settings provides a point where the settings can be overridden for a
|
||||
* specific Story.
|
||||
*/
|
||||
settings?: Partial<ModerationSettings>;
|
||||
settings: StorySettings;
|
||||
|
||||
/**
|
||||
* closedAt is the date that the Story was forced closed at, or false to
|
||||
@@ -69,6 +76,11 @@ export interface Story extends TenantResource {
|
||||
* createdAt is the date that the Story was added to the Talk database.
|
||||
*/
|
||||
createdAt: Date;
|
||||
|
||||
/**
|
||||
* premodLinksEnable will put all comments that contain links into premod.
|
||||
*/
|
||||
premodLinksEnable?: boolean;
|
||||
}
|
||||
|
||||
export async function createStoryIndexes(mongo: Db) {
|
||||
@@ -117,6 +129,7 @@ export async function upsertStory(
|
||||
status: createEmptyCommentStatusCounts(),
|
||||
moderationQueue: createEmptyCommentModerationQueueCounts(),
|
||||
},
|
||||
settings: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -196,6 +209,7 @@ export async function createStory(
|
||||
moderationQueue: createEmptyCommentModerationQueueCounts(),
|
||||
status: createEmptyCommentStatusCounts(),
|
||||
},
|
||||
settings: {},
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -291,13 +305,75 @@ export async function updateStory(
|
||||
// Evaluate the error, if it is in regards to violating the unique index,
|
||||
// then return a duplicate Story error.
|
||||
if (input.url && err instanceof MongoError && err.code === 11000) {
|
||||
// TODO: (wyattjoh) return better error
|
||||
throw new Error("story with this url already exists");
|
||||
throw new DuplicateStoryURLError(input.url);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
export type UpdateStorySettingsInput = StorySettings;
|
||||
|
||||
export async function updateStorySettings(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
input: UpdateStorySettingsInput
|
||||
) {
|
||||
// Only update fields that have been updated.
|
||||
const update = {
|
||||
$set: {
|
||||
...omitBy(dotize({ settings: input }, { embedArrays: true }), isNull),
|
||||
// Always update the updated at time.
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id, tenantID },
|
||||
update,
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
export async function openStory(mongo: Db, tenantID: string, id: string) {
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id, tenantID },
|
||||
{
|
||||
$set: {
|
||||
closedAt: false,
|
||||
// Always update the updated at time.
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
export async function closeStory(mongo: Db, tenantID: string, id: string) {
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{ id, tenantID },
|
||||
{
|
||||
$set: {
|
||||
closedAt: new Date(),
|
||||
// Always update the updated at time.
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
{ returnOriginal: false }
|
||||
);
|
||||
|
||||
return result.value || null;
|
||||
}
|
||||
|
||||
export async function removeStory(mongo: Db, tenantID: string, id: string) {
|
||||
const result = await collection(mongo).findOneAndDelete({
|
||||
|
||||
@@ -5,7 +5,10 @@ import uuid from "uuid";
|
||||
import { LanguageCode } from "talk-common/helpers/i18n/locales";
|
||||
import { DeepPartial, Omit, Sub } from "talk-common/types";
|
||||
import { dotize } from "talk-common/utils/dotize";
|
||||
import { GQLMODERATION_MODE } from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
GQLMODERATION_MODE,
|
||||
GQLSettings,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { createIndexFactory } from "talk-server/models/helpers/query";
|
||||
import { Settings } from "talk-server/models/settings";
|
||||
|
||||
@@ -13,6 +16,10 @@ function collection(mongo: Db) {
|
||||
return mongo.collection<Readonly<Tenant>>("tenants");
|
||||
}
|
||||
|
||||
/**
|
||||
* TenantResource references a given resource that should be owned by a specific
|
||||
* Tenant.
|
||||
*/
|
||||
export interface TenantResource {
|
||||
/**
|
||||
* tenantID is the reference to the specific Tenant that owns this particular
|
||||
@@ -21,29 +28,21 @@ export interface TenantResource {
|
||||
readonly tenantID: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant describes a given Tenant on Talk that has Stories, Comments, and Users.
|
||||
*/
|
||||
export interface Tenant extends Settings {
|
||||
export interface TenantSettings
|
||||
extends Pick<GQLSettings, "domain" | "domains" | "organization"> {
|
||||
readonly id: string;
|
||||
|
||||
// Domain is set when the tenant is created, and is used to retrieve the
|
||||
// specific tenant that the API request pertains to.
|
||||
domain: string;
|
||||
|
||||
// domains is the list of domains that are allowed to have the iframe load on.
|
||||
domains: string[];
|
||||
|
||||
/**
|
||||
* locale is the specified locale for this Tenant.
|
||||
*/
|
||||
locale: LanguageCode;
|
||||
|
||||
organizationName: string;
|
||||
organizationURL: string;
|
||||
organizationContactEmail: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant describes a given Tenant on Talk that has Stories, Comments, and Users.
|
||||
*/
|
||||
export type Tenant = Settings & TenantSettings;
|
||||
|
||||
export async function createTenantIndexes(mongo: Db) {
|
||||
const createIndex = createIndexFactory(collection(mongo));
|
||||
|
||||
@@ -61,12 +60,7 @@ export async function createTenantIndexes(mongo: Db) {
|
||||
*/
|
||||
export type CreateTenantInput = Pick<
|
||||
Tenant,
|
||||
| "domain"
|
||||
| "domains"
|
||||
| "locale"
|
||||
| "organizationName"
|
||||
| "organizationURL"
|
||||
| "organizationContactEmail"
|
||||
"domain" | "domains" | "locale" | "organization"
|
||||
>;
|
||||
|
||||
/**
|
||||
@@ -83,22 +77,20 @@ export async function createTenant(mongo: Db, input: CreateTenantInput) {
|
||||
// Default to post moderation.
|
||||
moderation: GQLMODERATION_MODE.POST,
|
||||
|
||||
// Email confirmation is default off.
|
||||
requireEmailConfirmation: false,
|
||||
communityGuidelines: {
|
||||
enabled: false,
|
||||
content: "",
|
||||
},
|
||||
questionBoxEnable: false,
|
||||
premodLinksEnable: false,
|
||||
autoCloseStream: false,
|
||||
closeCommenting: {
|
||||
auto: false,
|
||||
// 2 weeks timeout.
|
||||
timeout: 60 * 60 * 24 * 7 * 2,
|
||||
},
|
||||
disableCommenting: {
|
||||
enabled: false,
|
||||
},
|
||||
|
||||
// 2 weeks timeout.
|
||||
closedTimeout: 60 * 60 * 24 * 7 * 2,
|
||||
|
||||
// 30 seconds edit window length.
|
||||
editCommentWindowLength: 30,
|
||||
|
||||
|
||||
@@ -5,16 +5,13 @@ import {
|
||||
CommentBodyExceedsMaxLengthError,
|
||||
CommentBodyTooShortError,
|
||||
} from "talk-server/errors";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import { Settings } from "talk-server/models/settings";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
} from "talk-server/services/comments/pipeline";
|
||||
|
||||
const testCharCount = (
|
||||
settings: Partial<ModerationSettings>,
|
||||
length: number
|
||||
) => {
|
||||
const testCharCount = (settings: Partial<Settings>, length: number) => {
|
||||
if (settings.charCount && settings.charCount.enabled) {
|
||||
if (!isNil(settings.charCount.min)) {
|
||||
if (length < settings.charCount.min) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CommentingDisabledError } from "talk-server/errors";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import { Settings } from "talk-server/models/settings";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
} from "talk-server/services/comments/pipeline";
|
||||
|
||||
const testDisabledCommenting = (settings: Partial<ModerationSettings>) =>
|
||||
const testDisabledCommenting = (settings: Partial<Settings>) =>
|
||||
settings.disableCommenting && settings.disableCommenting.enabled;
|
||||
|
||||
export const commentingDisabled: IntermediateModerationPhase = ({
|
||||
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ACTION_TYPE } from "talk-server/models/action/comment";
|
||||
import { Comment } from "talk-server/models/comment";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import { GlobalModerationSettings } from "talk-server/models/settings";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
} from "talk-server/services/comments/pipeline";
|
||||
|
||||
const testPremodLinksEnable = (
|
||||
settings: Partial<ModerationSettings>,
|
||||
settings: Partial<GlobalModerationSettings>,
|
||||
comment: Pick<Comment, "metadata">
|
||||
) =>
|
||||
settings.premodLinksEnable && comment.metadata && comment.metadata.linkCount;
|
||||
|
||||
@@ -2,13 +2,13 @@ import {
|
||||
GQLCOMMENT_STATUS,
|
||||
GQLMODERATION_MODE,
|
||||
} from "talk-server/graph/tenant/schema/__generated__/types";
|
||||
import { ModerationSettings } from "talk-server/models/settings";
|
||||
import { GlobalModerationSettings } from "talk-server/models/settings";
|
||||
import {
|
||||
IntermediateModerationPhase,
|
||||
IntermediatePhaseResult,
|
||||
} from "talk-server/services/comments/pipeline";
|
||||
|
||||
const testModerationMode = (settings: Partial<ModerationSettings>) =>
|
||||
const testModerationMode = (settings: Partial<GlobalModerationSettings>) =>
|
||||
settings.moderation === GQLMODERATION_MODE.PRE;
|
||||
|
||||
// This phase checks to see if the settings have premod enabled, if they do,
|
||||
|
||||
@@ -16,7 +16,9 @@ describe("storyClosed", () => {
|
||||
|
||||
storyClosed({
|
||||
story: {} as ModerationPhaseContext["story"],
|
||||
tenant: { autoCloseStream: true } as ModerationPhaseContext["tenant"],
|
||||
tenant: {
|
||||
closeCommenting: { auto: true },
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
});
|
||||
@@ -25,8 +27,10 @@ describe("storyClosed", () => {
|
||||
storyClosed({
|
||||
story: { createdAt: new Date() } as ModerationPhaseContext["story"],
|
||||
tenant: {
|
||||
autoCloseStream: true,
|
||||
closedTimeout: -6000,
|
||||
closeCommenting: {
|
||||
auto: true,
|
||||
timeout: -6000,
|
||||
},
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
@@ -44,7 +48,9 @@ describe("storyClosed", () => {
|
||||
.plus(60000)
|
||||
.toJSDate(),
|
||||
} as ModerationPhaseContext["story"],
|
||||
tenant: {} as ModerationPhaseContext["tenant"],
|
||||
tenant: {
|
||||
closeCommenting: { auto: true },
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
})
|
||||
@@ -53,7 +59,9 @@ describe("storyClosed", () => {
|
||||
expect(
|
||||
storyClosed({
|
||||
story: {} as ModerationPhaseContext["story"],
|
||||
tenant: {} as ModerationPhaseContext["tenant"],
|
||||
tenant: {
|
||||
closeCommenting: { auto: true },
|
||||
} as ModerationPhaseContext["tenant"],
|
||||
comment: {} as ModerationPhaseContext["comment"],
|
||||
author: {} as ModerationPhaseContext["author"],
|
||||
})
|
||||
|
||||
@@ -22,11 +22,13 @@ import {
|
||||
} from "talk-server/models/comment";
|
||||
import {
|
||||
calculateTotalCommentCount,
|
||||
closeStory,
|
||||
createStory,
|
||||
CreateStoryInput,
|
||||
findOrCreateStory,
|
||||
FindOrCreateStoryInput,
|
||||
mergeCommentStatusCount,
|
||||
openStory,
|
||||
removeStories,
|
||||
removeStory,
|
||||
retrieveManyStories,
|
||||
@@ -36,6 +38,8 @@ import {
|
||||
updateStoryActionCounts,
|
||||
updateStoryCommentStatusCount,
|
||||
UpdateStoryInput,
|
||||
updateStorySettings,
|
||||
UpdateStorySettingsInput,
|
||||
} from "talk-server/models/story";
|
||||
import { Tenant } from "talk-server/models/tenant";
|
||||
import { ScraperQueue } from "talk-server/queue/tasks/scraper";
|
||||
@@ -227,6 +231,24 @@ export async function update(
|
||||
|
||||
return updateStory(mongo, tenant.id, storyID, input);
|
||||
}
|
||||
export type UpdateStorySettings = UpdateStorySettingsInput;
|
||||
|
||||
export async function updateSettings(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
storyID: string,
|
||||
input: UpdateStorySettings
|
||||
) {
|
||||
return updateStorySettings(mongo, tenant.id, storyID, input);
|
||||
}
|
||||
|
||||
export async function open(mongo: Db, tenant: Tenant, storyID: string) {
|
||||
return openStory(mongo, tenant.id, storyID);
|
||||
}
|
||||
|
||||
export async function close(mongo: Db, tenant: Tenant, storyID: string) {
|
||||
return closeStory(mongo, tenant.id, storyID);
|
||||
}
|
||||
|
||||
export async function merge(
|
||||
mongo: Db,
|
||||
@@ -339,7 +361,7 @@ export async function merge(
|
||||
}
|
||||
|
||||
export function getStoryClosedAt(
|
||||
tenant: Pick<Tenant, "autoCloseStream" | "closedTimeout">,
|
||||
tenant: Pick<Tenant, "closeCommenting">,
|
||||
story: Pick<Story, "closedAt" | "createdAt">
|
||||
): Story["closedAt"] {
|
||||
// Try to get the closedAt time from the story.
|
||||
@@ -347,16 +369,21 @@ export function getStoryClosedAt(
|
||||
return story.closedAt;
|
||||
}
|
||||
|
||||
// Check to see if the story has been forced open again.
|
||||
if (story.closedAt === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the story hasn't already been closed, then check to see if the Tenant
|
||||
// has the auto close stream enabled.
|
||||
if (tenant.autoCloseStream && tenant.closedTimeout) {
|
||||
if (tenant.closeCommenting.auto) {
|
||||
// Auto-close stream has been enabled, convert the createdAt time into the
|
||||
// closedAt time by adding the closedTimeout.
|
||||
return (
|
||||
DateTime.fromJSDate(story.createdAt)
|
||||
// closedTimeout is in seconds, so multiply by 1000 to get
|
||||
// milliseconds.
|
||||
.plus(tenant.closedTimeout * 1000)
|
||||
.plus(tenant.closeCommenting.timeout * 1000)
|
||||
.toJSDate()
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user