mirror of
https://github.com/wassname/talk.git
synced 2026-07-26 13:37:38 +08:00
[CORL-687] Webhooks (#2738)
* feat: initial webhook impl * feat: added support for key rotation * feat: harmonized fetcher * feat: added expired secrets cleaning * feat: event system refactor * feat: added story event * feat: simplfiied webhook handler * feat: added ref's to locations where user events can be added * feat: added UI to support webhooks * fix: renaming some Webhook -> WebhookEndpoint * fix: review comments to adjuist flow * feat: added localizations * fix: linting, updated snapshots * fix: adapted for new fluent * fix: rearranged folders * fix: linting * feat: added webhooks documentation * feat: improved toc generation * feat: added some tests to webhooks * fix: chain transition hooks * feat: added tests around webhook ui * fix: renamed events * fix: adjusted circle markdown linting * fix: adjusted doctoc script call * review: review fixes * review: review comments * review: adjusted signing secret confirmation * review: adjusted styles to harmonize button usage * fix: updated snapshots and tests * review: move form out of webhooks Moved the form out of the webhooks by relocating the layout used for the route associated with the configure routes. * fix: fixed bugs and snapshots with tests * feat: revised slack message format to use block api * fix: fixed a small text bug Co-authored-by: Vinh <vinh@vinh.tech> Co-authored-by: Kim Gardner <kgardnr@gmail.com>
This commit is contained in:
co-authored by
Vinh
Kim Gardner
parent
34ba2da88d
commit
e42c2b925d
@@ -4,21 +4,18 @@ import uuid from "uuid";
|
||||
|
||||
import { LanguageCode } from "coral-common/helpers/i18n/locales";
|
||||
import { Config } from "coral-server/config";
|
||||
import {
|
||||
createPublisher,
|
||||
Publisher,
|
||||
} from "coral-server/graph/subscriptions/publisher";
|
||||
import CoralEventListenerBroker, {
|
||||
CoralEventPublisherBroker,
|
||||
} from "coral-server/events/publisher";
|
||||
import logger, { Logger } from "coral-server/logger";
|
||||
import { PersistedQuery } from "coral-server/models/queries";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { User } from "coral-server/models/user";
|
||||
import { MailerQueue } from "coral-server/queue/tasks/mailer";
|
||||
import { NotifierQueue } from "coral-server/queue/tasks/notifier";
|
||||
import { ScraperQueue } from "coral-server/queue/tasks/scraper";
|
||||
import { I18n } from "coral-server/services/i18n";
|
||||
import { JWTSigningConfig } from "coral-server/services/jwt";
|
||||
import { AugmentedRedis } from "coral-server/services/redis";
|
||||
import createSlackPublisher from "coral-server/services/slack/publisher";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { Request } from "coral-server/types/express";
|
||||
|
||||
@@ -41,16 +38,17 @@ export interface GraphContextOptions {
|
||||
i18n: I18n;
|
||||
mailerQueue: MailerQueue;
|
||||
mongo: Db;
|
||||
notifierQueue: NotifierQueue;
|
||||
pubsub: RedisPubSub;
|
||||
redis: AugmentedRedis;
|
||||
scraperQueue: ScraperQueue;
|
||||
tenant: Tenant;
|
||||
tenantCache: TenantCache;
|
||||
broker: CoralEventListenerBroker;
|
||||
}
|
||||
|
||||
export default class GraphContext {
|
||||
public readonly config: Config;
|
||||
public readonly broker: CoralEventPublisherBroker;
|
||||
public readonly disableCaching: boolean;
|
||||
public readonly i18n: I18n;
|
||||
public readonly id: string;
|
||||
@@ -61,7 +59,6 @@ export default class GraphContext {
|
||||
public readonly mongo: Db;
|
||||
public readonly mutators: ReturnType<typeof mutators>;
|
||||
public readonly now: Date;
|
||||
public readonly publisher: Publisher;
|
||||
public readonly pubsub: RedisPubSub;
|
||||
public readonly redis: AugmentedRedis;
|
||||
public readonly scraperQueue: ScraperQueue;
|
||||
@@ -100,18 +97,7 @@ export default class GraphContext {
|
||||
this.signingConfig = options.signingConfig;
|
||||
this.clientID = options.clientID;
|
||||
|
||||
this.publisher = createPublisher({
|
||||
pubsub: this.pubsub,
|
||||
slackPublisher: createSlackPublisher(
|
||||
this.mongo,
|
||||
this.config,
|
||||
this.tenant
|
||||
),
|
||||
notifierQueue: options.notifierQueue,
|
||||
tenantID: this.tenant.id,
|
||||
clientID: this.clientID,
|
||||
});
|
||||
|
||||
this.broker = options.broker.instance(this);
|
||||
this.loaders = loaders(this);
|
||||
this.mutators = mutators(this);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,14 @@ const primeStoriesFromConnection = (ctx: GraphContext) => (
|
||||
export default (ctx: GraphContext) => ({
|
||||
findOrCreate: new DataLoader(
|
||||
createManyBatchLoadFn((input: FindOrCreateStory) =>
|
||||
findOrCreate(ctx.mongo, ctx.tenant, input, ctx.scraperQueue, ctx.now)
|
||||
findOrCreate(
|
||||
ctx.mongo,
|
||||
ctx.tenant,
|
||||
ctx.broker,
|
||||
input,
|
||||
ctx.scraperQueue,
|
||||
ctx.now
|
||||
)
|
||||
),
|
||||
{
|
||||
// TODO: (wyattjoh) see if there's something we can do to improve the cache key
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Actions = (ctx: GraphContext) => ({
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
input.commentID,
|
||||
input.commentRevisionID,
|
||||
@@ -24,7 +24,7 @@ export const Actions = (ctx: GraphContext) => ({
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
input.commentID,
|
||||
input.commentRevisionID,
|
||||
|
||||
@@ -45,7 +45,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{ authorID: ctx.user!.id, ...comment },
|
||||
@@ -68,7 +68,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{
|
||||
@@ -92,7 +92,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
createReaction(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{
|
||||
@@ -102,7 +102,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
ctx.now
|
||||
),
|
||||
removeReaction: ({ commentID }: GQLRemoveCommentReactionInput) =>
|
||||
removeReaction(ctx.mongo, ctx.redis, ctx.publisher, ctx.tenant, ctx.user!, {
|
||||
removeReaction(ctx.mongo, ctx.redis, ctx.broker, ctx.tenant, ctx.user!, {
|
||||
commentID,
|
||||
}),
|
||||
createDontAgree: ({
|
||||
@@ -113,7 +113,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
createDontAgree(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{
|
||||
@@ -128,14 +128,9 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
ctx.now
|
||||
),
|
||||
removeDontAgree: ({ commentID }: GQLRemoveCommentDontAgreeInput) =>
|
||||
removeDontAgree(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.publisher,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{ commentID }
|
||||
),
|
||||
removeDontAgree(ctx.mongo, ctx.redis, ctx.broker, ctx.tenant, ctx.user!, {
|
||||
commentID,
|
||||
}),
|
||||
createFlag: ({
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
@@ -145,7 +140,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
createFlag(
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
ctx.user!,
|
||||
{
|
||||
@@ -179,7 +174,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
ctx.mongo,
|
||||
ctx.redis,
|
||||
ctx.config,
|
||||
ctx.publisher,
|
||||
ctx.broker,
|
||||
ctx.tenant,
|
||||
commentID,
|
||||
commentRevisionID,
|
||||
@@ -190,7 +185,7 @@ export const Comments = (ctx: GraphContext) => ({
|
||||
)
|
||||
.then(comment => {
|
||||
// Publish that the comment was featured.
|
||||
publishCommentFeatured(ctx.publisher, comment);
|
||||
publishCommentFeatured(ctx.broker, comment);
|
||||
|
||||
// Return it to the next step.
|
||||
return comment;
|
||||
|
||||
@@ -2,19 +2,33 @@ import GraphContext from "coral-server/graph/context";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import {
|
||||
createAnnouncement,
|
||||
createWebhookEndpoint,
|
||||
deleteAnnouncement,
|
||||
deleteWebhookEndpoint,
|
||||
disableFeatureFlag,
|
||||
disableWebhookEndpoint,
|
||||
enableFeatureFlag,
|
||||
enableWebhookEndpoint,
|
||||
regenerateSSOKey,
|
||||
rotateWebhookEndpointSecret,
|
||||
update,
|
||||
updateWebhookEndpoint,
|
||||
} from "coral-server/services/tenant";
|
||||
|
||||
import {
|
||||
GQLCreateAnnouncementInput,
|
||||
GQLCreateWebhookEndpointInput,
|
||||
GQLDeleteWebhookEndpointInput,
|
||||
GQLDisableWebhookEndpointInput,
|
||||
GQLEnableWebhookEndpointInput,
|
||||
GQLFEATURE_FLAG,
|
||||
GQLRotateWebhookEndpointSecretInput,
|
||||
GQLUpdateSettingsInput,
|
||||
GQLUpdateWebhookEndpointInput,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
import { WithoutMutationID } from "./util";
|
||||
|
||||
export const Settings = ({
|
||||
mongo,
|
||||
redis,
|
||||
@@ -23,7 +37,9 @@ export const Settings = ({
|
||||
config,
|
||||
now,
|
||||
}: GraphContext) => ({
|
||||
update: (input: GQLUpdateSettingsInput): Promise<Tenant | null> =>
|
||||
update: (
|
||||
input: WithoutMutationID<GQLUpdateSettingsInput>
|
||||
): Promise<Tenant | null> =>
|
||||
update(mongo, redis, tenantCache, config, tenant, input.settings),
|
||||
regenerateSSOKey: (): Promise<Tenant | null> =>
|
||||
regenerateSSOKey(mongo, redis, tenantCache, tenant, now),
|
||||
@@ -35,4 +51,42 @@ export const Settings = ({
|
||||
createAnnouncement(mongo, redis, tenantCache, tenant, input, now),
|
||||
deleteAnnouncement: () =>
|
||||
deleteAnnouncement(mongo, redis, tenantCache, tenant),
|
||||
createWebhookEndpoint: (
|
||||
input: WithoutMutationID<GQLCreateWebhookEndpointInput>
|
||||
) =>
|
||||
createWebhookEndpoint(
|
||||
mongo,
|
||||
redis,
|
||||
config,
|
||||
tenantCache,
|
||||
tenant,
|
||||
input,
|
||||
now
|
||||
),
|
||||
enableWebhookEndpoint: (
|
||||
input: WithoutMutationID<GQLEnableWebhookEndpointInput>
|
||||
) => enableWebhookEndpoint(mongo, redis, tenantCache, tenant, input.id),
|
||||
disableWebhookEndpoint: (
|
||||
input: WithoutMutationID<GQLDisableWebhookEndpointInput>
|
||||
) => disableWebhookEndpoint(mongo, redis, tenantCache, tenant, input.id),
|
||||
updateWebhookEndpoint: ({
|
||||
id,
|
||||
...input
|
||||
}: WithoutMutationID<GQLUpdateWebhookEndpointInput>) =>
|
||||
updateWebhookEndpoint(mongo, redis, config, tenantCache, tenant, id, input),
|
||||
deleteWebhookEndpoint: (
|
||||
input: WithoutMutationID<GQLDeleteWebhookEndpointInput>
|
||||
) => deleteWebhookEndpoint(mongo, redis, tenantCache, tenant, input.id),
|
||||
rotateWebhookEndpointSecret: (
|
||||
input: WithoutMutationID<GQLRotateWebhookEndpointSecretInput>
|
||||
) =>
|
||||
rotateWebhookEndpointSecret(
|
||||
mongo,
|
||||
redis,
|
||||
tenantCache,
|
||||
tenant,
|
||||
input.id,
|
||||
input.inactiveIn,
|
||||
now
|
||||
),
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ export const Stories = (ctx: GraphContext) => ({
|
||||
create(
|
||||
ctx.mongo,
|
||||
ctx.tenant,
|
||||
ctx.broker,
|
||||
ctx.config,
|
||||
input.story.id,
|
||||
input.story.url,
|
||||
|
||||
@@ -86,7 +86,7 @@ export const storyModerationInputResolver = (
|
||||
*
|
||||
* @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
|
||||
* @param ctx the GraphContext that will be used to get the shared counts
|
||||
*/
|
||||
export const sharedModerationInputResolver = async (
|
||||
source: any,
|
||||
@@ -106,7 +106,7 @@ export const sharedModerationInputResolver = async (
|
||||
*
|
||||
* @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
|
||||
* @param ctx the GraphContext for which we can use to retrieve the shared data
|
||||
*/
|
||||
export const moderationQueuesResolver: QueryToModerationQueuesResolver = async (
|
||||
source,
|
||||
|
||||
@@ -33,9 +33,13 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
user: await ctx.mutators.Users.updateNotificationSettings(input),
|
||||
clientMutationId,
|
||||
}),
|
||||
updateSettings: async (source, { input }, ctx) => ({
|
||||
updateSettings: async (
|
||||
source,
|
||||
{ input: { clientMutationId, ...input } },
|
||||
ctx
|
||||
) => ({
|
||||
settings: await ctx.mutators.Settings.update(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
clientMutationId,
|
||||
}),
|
||||
createCommentReaction: async (source, { input }, ctx) => ({
|
||||
comment: await ctx.mutators.Comments.createReaction(input),
|
||||
@@ -252,4 +256,52 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
site: await ctx.mutators.Sites.update(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
createWebhookEndpoint: async (
|
||||
source,
|
||||
{ input: { clientMutationId, ...input } },
|
||||
ctx
|
||||
) => ({
|
||||
...(await ctx.mutators.Settings.createWebhookEndpoint(input)),
|
||||
clientMutationId,
|
||||
}),
|
||||
updateWebhookEndpoint: async (
|
||||
source,
|
||||
{ input: { clientMutationId, ...input } },
|
||||
ctx
|
||||
) => ({
|
||||
endpoint: await ctx.mutators.Settings.updateWebhookEndpoint(input),
|
||||
clientMutationId,
|
||||
}),
|
||||
disableWebhookEndpoint: async (
|
||||
source,
|
||||
{ input: { clientMutationId, ...input } },
|
||||
ctx
|
||||
) => ({
|
||||
endpoint: await ctx.mutators.Settings.disableWebhookEndpoint(input),
|
||||
clientMutationId,
|
||||
}),
|
||||
enableWebhookEndpoint: async (
|
||||
source,
|
||||
{ input: { clientMutationId, ...input } },
|
||||
ctx
|
||||
) => ({
|
||||
endpoint: await ctx.mutators.Settings.enableWebhookEndpoint(input),
|
||||
clientMutationId,
|
||||
}),
|
||||
deleteWebhookEndpoint: async (
|
||||
source,
|
||||
{ input: { clientMutationId, ...input } },
|
||||
ctx
|
||||
) => ({
|
||||
endpoint: await ctx.mutators.Settings.deleteWebhookEndpoint(input),
|
||||
clientMutationId,
|
||||
}),
|
||||
rotateWebhookEndpointSecret: async (
|
||||
source,
|
||||
{ input: { clientMutationId, ...input } },
|
||||
ctx
|
||||
) => ({
|
||||
endpoint: await ctx.mutators.Settings.rotateWebhookEndpointSecret(input),
|
||||
clientMutationId,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getWebhookEndpoint } from "coral-server/models/tenant";
|
||||
|
||||
import { GQLQueryTypeResolver } from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
import { moderationQueuesResolver } from "./ModerationQueues";
|
||||
@@ -25,4 +27,5 @@ export const Query: Required<GQLQueryTypeResolver<void>> = {
|
||||
ctx.loaders.Stories.activeStories(limit),
|
||||
sites: (source, args, ctx) => ctx.loaders.Sites.connection(args),
|
||||
site: (source, { id }, ctx) => (id ? ctx.loaders.Sites.site.load(id) : null),
|
||||
webhookEndpoint: (source, { id }, ctx) => getWebhookEndpoint(ctx.tenant, id),
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as settings from "coral-server/models/settings";
|
||||
|
||||
import { GQLSSOAuthIntegrationTypeResolver } from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
function getActiveSSOKey(keys: settings.SSOKey[]) {
|
||||
function getActiveSSOKey(keys: settings.Secret[]) {
|
||||
// Any key that has been rotated cannot be the active key.
|
||||
return keys.find(key => !key.rotatedAt);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import {
|
||||
GQLFEATURE_FLAG,
|
||||
GQLSettingsTypeResolver,
|
||||
GQLWEBHOOK_EVENT_NAME,
|
||||
} from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
const filterValidFeatureFlags = () => {
|
||||
@@ -27,4 +28,5 @@ export const Settings: GQLSettingsTypeResolver<Tenant> = {
|
||||
const sites = await ctx.loaders.Sites.connection({});
|
||||
return sites.edges.length > 1;
|
||||
},
|
||||
webhookEvents: () => Object.values(GQLWEBHOOK_EVENT_NAME),
|
||||
};
|
||||
|
||||
@@ -17,3 +17,13 @@ export const Subscription: GQLSubscriptionTypeResolver = {
|
||||
commentFeatured,
|
||||
commentReleased,
|
||||
};
|
||||
|
||||
export { CommentFeaturedInput } from "./commentFeatured";
|
||||
export { CommentCreatedInput } from "./commentCreated";
|
||||
export {
|
||||
CommentEnteredModerationQueueInput,
|
||||
} from "./commentEnteredModerationQueue";
|
||||
export { CommentLeftModerationQueueInput } from "./commentLeftModerationQueue";
|
||||
export { CommentReleasedInput } from "./commentReleased";
|
||||
export { CommentReplyCreatedInput } from "./commentReplyCreated";
|
||||
export { CommentStatusUpdatedInput } from "./commentStatusUpdated";
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as tenant from "coral-server/models/tenant";
|
||||
|
||||
import { GQLWebhookEndpointTypeResolver } from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
export const WebhookEndpoint: GQLWebhookEndpointTypeResolver<
|
||||
tenant.Endpoint
|
||||
> = {
|
||||
signingSecret: ({ signingSecrets }) =>
|
||||
signingSecrets[signingSecrets.length - 1],
|
||||
};
|
||||
@@ -50,6 +50,7 @@ import { User } from "./User";
|
||||
import { UsernameHistory } from "./UsernameHistory";
|
||||
import { UsernameStatus } from "./UsernameStatus";
|
||||
import { UserStatus } from "./UserStatus";
|
||||
import { WebhookEndpoint } from "./WebhookEndpoint";
|
||||
|
||||
const Resolvers: GQLResolver = {
|
||||
ApproveCommentPayload,
|
||||
@@ -101,6 +102,7 @@ const Resolvers: GQLResolver = {
|
||||
UserStatus,
|
||||
Settings,
|
||||
SlackConfiguration,
|
||||
WebhookEndpoint,
|
||||
};
|
||||
|
||||
export default Resolvers;
|
||||
|
||||
@@ -1002,7 +1002,7 @@ type SlackChannel {
|
||||
triggers are the filters of types of comments that will be sent to
|
||||
the correlated channel
|
||||
"""
|
||||
triggers: SlackChannelTriggers
|
||||
triggers: SlackChannelTriggers!
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -1204,6 +1204,79 @@ type StaffConfiguration {
|
||||
label: String!
|
||||
}
|
||||
|
||||
type WebhookDelivery {
|
||||
success: Boolean!
|
||||
status: Int!
|
||||
statusText: String!
|
||||
request: String!
|
||||
response: String!
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
enum WEBHOOK_EVENT_NAME {
|
||||
STORY_CREATED
|
||||
}
|
||||
|
||||
"""
|
||||
TODO: merge with SSOKey with PR #2732
|
||||
"""
|
||||
type Secret {
|
||||
"""
|
||||
secret is the actual underlying secret used to verify the tokens with.
|
||||
"""
|
||||
secret: String!
|
||||
|
||||
"""
|
||||
createdAt is the date that the key was created at.
|
||||
"""
|
||||
createdAt: Time!
|
||||
}
|
||||
|
||||
type WebhookEndpoint {
|
||||
"""
|
||||
id is the unique identifier for this specific endpoint.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
enabled when true will enable events to be sent to this endpoint.
|
||||
"""
|
||||
enabled: Boolean!
|
||||
|
||||
"""
|
||||
url is the URL that we will POST event data to.
|
||||
"""
|
||||
url: String!
|
||||
|
||||
"""
|
||||
signingSecret is the current secret used to sign the events sent out.
|
||||
"""
|
||||
signingSecret: Secret!
|
||||
|
||||
"""
|
||||
deliveries store the deliveries for each event sent for the last 50 events.
|
||||
"""
|
||||
deliveries: [WebhookDelivery!]!
|
||||
|
||||
"""
|
||||
all is true when all events are subscribed to.
|
||||
"""
|
||||
all: Boolean!
|
||||
|
||||
"""
|
||||
events are the specific event names that this endpoint is configured to send
|
||||
for.
|
||||
"""
|
||||
events: [WEBHOOK_EVENT_NAME!]!
|
||||
}
|
||||
|
||||
type WebhookConfiguration {
|
||||
"""
|
||||
endpoints is all the configured endpoints that should receive events.
|
||||
"""
|
||||
endpoints: [WebhookEndpoint!]!
|
||||
}
|
||||
|
||||
"""
|
||||
NewCommenterConfiguration specifies the features that apply to new commenters
|
||||
"""
|
||||
@@ -1266,6 +1339,16 @@ type Settings {
|
||||
"""
|
||||
domain: String! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
webhooks store the webhook configuration.
|
||||
"""
|
||||
webhooks: WebhookConfiguration! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
webhookEvents returns all the events that can trigger webhooks.
|
||||
"""
|
||||
webhookEvents: [WEBHOOK_EVENT_NAME!]! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
staticURI if configured, is the static URI used to serve static files from.
|
||||
"""
|
||||
@@ -2891,6 +2974,11 @@ type Query {
|
||||
activeStories(limit: Int = 10 @constraint(max: 25)): [Story!]!
|
||||
@auth(roles: [ADMIN])
|
||||
@rate(max: 2, seconds: 1)
|
||||
|
||||
"""
|
||||
webhookEndpint will return a specific WebhookEndpoint if it exists.
|
||||
"""
|
||||
webhookEndpoint(id: ID!): WebhookEndpoint @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
################################################################################
|
||||
@@ -4768,6 +4856,212 @@ type DeleteModeratorNotePayload {
|
||||
user: User!
|
||||
}
|
||||
|
||||
##################
|
||||
# createWebhookEndpoint
|
||||
##################
|
||||
|
||||
input CreateWebhookEndpointInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
url is the URL that Coral will POST event data to.
|
||||
"""
|
||||
url: String!
|
||||
|
||||
"""
|
||||
all is true when all events are subscribed to.
|
||||
"""
|
||||
all: Boolean!
|
||||
|
||||
"""
|
||||
events are the specific event names that this endpoint is configured to send
|
||||
for.
|
||||
"""
|
||||
events: [WEBHOOK_EVENT_NAME!]!
|
||||
}
|
||||
|
||||
type CreateWebhookEndpointPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
endpoint is the endpoint that we just created.
|
||||
"""
|
||||
endpoint: WebhookEndpoint!
|
||||
|
||||
"""
|
||||
settings is the updated settings also containing the new endpoint.
|
||||
"""
|
||||
settings: Settings!
|
||||
}
|
||||
|
||||
##################
|
||||
# updateWebhookEndpoint
|
||||
##################
|
||||
|
||||
input UpdateWebhookEndpointInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
id is the ID of the WebhookEndpoint being updated.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
url is the URL that Coral will POST event data to.
|
||||
"""
|
||||
url: String
|
||||
|
||||
"""
|
||||
all is true when all events are subscribed to.
|
||||
"""
|
||||
all: Boolean
|
||||
|
||||
"""
|
||||
events are the specific event names that this endpoint is configured to send
|
||||
for.
|
||||
"""
|
||||
events: [WEBHOOK_EVENT_NAME!]
|
||||
}
|
||||
|
||||
type UpdateWebhookEndpointPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
endpoint is the endpoint that we just created.
|
||||
"""
|
||||
endpoint: WebhookEndpoint!
|
||||
}
|
||||
|
||||
##################
|
||||
# rotateWebhookEndpointSecret
|
||||
##################
|
||||
|
||||
input RotateWebhookEndpointSecretInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
id is the ID of the WebhookEndpoint being updated.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
inactiveIn is the number of seconds that the current active Secret should be
|
||||
kept active.
|
||||
"""
|
||||
inactiveIn: Int!
|
||||
}
|
||||
|
||||
type RotateWebhookEndpointSecretPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
endpoint is the endpoint that we just updated.
|
||||
"""
|
||||
endpoint: WebhookEndpoint
|
||||
}
|
||||
|
||||
##################
|
||||
# disableWebhookEndpoint
|
||||
##################
|
||||
|
||||
input DisableWebhookEndpointInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
id is the ID of the WebhookEndpoint being disabled.
|
||||
"""
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type DisableWebhookEndpointPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
endpoint is the endpoint that we just disabled.
|
||||
"""
|
||||
endpoint: WebhookEndpoint
|
||||
}
|
||||
|
||||
##################
|
||||
# enableWebhookEndpoint
|
||||
##################
|
||||
|
||||
input EnableWebhookEndpointInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
id is the ID of the WebhookEndpoint being enabled.
|
||||
"""
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type EnableWebhookEndpointPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
endpoint is the endpoint that we just enabled.
|
||||
"""
|
||||
endpoint: WebhookEndpoint
|
||||
}
|
||||
|
||||
##################
|
||||
# deleteWebhookEndpoint
|
||||
##################
|
||||
|
||||
input DeleteWebhookEndpointInput {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
id is the ID of the WebhookEndpoint being deleted.
|
||||
"""
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type DeleteWebhookEndpointPayload {
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
|
||||
"""
|
||||
endpoint is the endpoint that we just deleted.
|
||||
"""
|
||||
endpoint: WebhookEndpoint
|
||||
}
|
||||
|
||||
##################
|
||||
# setEmail
|
||||
##################
|
||||
@@ -5899,6 +6193,49 @@ type Mutation {
|
||||
createSite(input: CreateSiteInput!): CreateSitePayload! @auth(roles: [ADMIN])
|
||||
|
||||
updateSite(input: UpdateSiteInput!): UpdateSitePayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
createWebhookEndpoint will create a new WebhookEndpoint.
|
||||
"""
|
||||
createWebhookEndpoint(
|
||||
input: CreateWebhookEndpointInput!
|
||||
): CreateWebhookEndpointPayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
updateWebhookEndpoint will update a WebhookEndpoint.
|
||||
"""
|
||||
updateWebhookEndpoint(
|
||||
input: UpdateWebhookEndpointInput!
|
||||
): UpdateWebhookEndpointPayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
enableWebhookEndpoint will enable a WebhookEndpoint to recieve new events.
|
||||
"""
|
||||
enableWebhookEndpoint(
|
||||
input: EnableWebhookEndpointInput!
|
||||
): EnableWebhookEndpointPayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
disableWebhookEndpoint will disable a WebhookEndpoint from recieving new
|
||||
events.
|
||||
"""
|
||||
disableWebhookEndpoint(
|
||||
input: DisableWebhookEndpointInput!
|
||||
): DisableWebhookEndpointPayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
deleteWebhookEndpoint will delete a WebhookEndpoint.
|
||||
"""
|
||||
deleteWebhookEndpoint(
|
||||
input: DeleteWebhookEndpointInput!
|
||||
): DeleteWebhookEndpointPayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
rotateWebhookEndpointSecret will roll the current active secret to a new key.
|
||||
"""
|
||||
rotateWebhookEndpointSecret(
|
||||
input: RotateWebhookEndpointSecretInput!
|
||||
): RotateWebhookEndpointSecretPayload! @auth(roles: [ADMIN])
|
||||
}
|
||||
|
||||
##################
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { RedisPubSub } from "graphql-redis-subscriptions";
|
||||
|
||||
import { createSubscriptionChannelName } from "coral-server/graph/resolvers/Subscription/helpers";
|
||||
import { SUBSCRIPTION_INPUT } from "coral-server/graph/resolvers/Subscription/types";
|
||||
import logger from "coral-server/logger";
|
||||
import { NotifierQueue } from "coral-server/queue/tasks/notifier";
|
||||
import { SlackPublisher } from "coral-server/services/slack/publisher";
|
||||
|
||||
export type Publisher = (input: SUBSCRIPTION_INPUT) => Promise<void>;
|
||||
|
||||
export interface PublisherOptions {
|
||||
pubsub: RedisPubSub;
|
||||
slackPublisher: SlackPublisher;
|
||||
notifierQueue: NotifierQueue;
|
||||
tenantID: string;
|
||||
clientID?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* createPublisher will create a new Publisher that can be used to send events
|
||||
* over the pubsub broker to facilitate live updates and notifications.
|
||||
*
|
||||
* TODO: Update
|
||||
*
|
||||
* @param options options object
|
||||
* @param options.pubsub the pubsub broker to be used to facilitate the publish action
|
||||
* @param options.slackPublisher the slack publisher instance
|
||||
* @param options.notifierQueue the queue
|
||||
* @param options.tenantID the ID of the Tenant where the event will be published with
|
||||
* @param options.clientID the ID of the client to de-duplicate mutation responses
|
||||
*/
|
||||
export const createPublisher = ({
|
||||
pubsub,
|
||||
slackPublisher,
|
||||
notifierQueue,
|
||||
tenantID,
|
||||
clientID,
|
||||
}: PublisherOptions): Publisher => async input => {
|
||||
const { channel, payload } = input;
|
||||
|
||||
logger.trace({ channel, tenantID, clientID }, "publishing event");
|
||||
|
||||
// Start the publishing operation out to all affected subscribers.
|
||||
await Promise.all([
|
||||
// Publish to the underlying pubsub system for subscriptions.
|
||||
pubsub.publish(createSubscriptionChannelName(tenantID, channel), {
|
||||
...payload,
|
||||
clientID,
|
||||
}),
|
||||
|
||||
slackPublisher(channel, payload),
|
||||
|
||||
// Notify the notifications queue so we can offload notification processing
|
||||
// to it.
|
||||
notifierQueue.add({ tenantID, input }),
|
||||
]);
|
||||
};
|
||||
Reference in New Issue
Block a user