diff --git a/keyserver/src/keyserver.js b/keyserver/src/keyserver.js --- a/keyserver/src/keyserver.js +++ b/keyserver/src/keyserver.js @@ -25,11 +25,13 @@ import { jsonEndpoints } from './endpoints.js'; import { logEndpointMetrics } from './middleware/endpoint-profiling.js'; import { emailSubscriptionResponder } from './responders/comm-landing-responders.js'; +import { taggedCommFarcasterResponder } from './responders/farcaster-webhook-responders.js'; import { jsonHandler, downloadHandler, htmlHandler, uploadHandler, + webhookPayloadHandler, } from './responders/handlers.js'; import landingHandler from './responders/landing-handler.js'; import { errorReportDownloadResponder } from './responders/report-responders.js'; @@ -297,6 +299,11 @@ keyserverRouter.use(cors(keyserverCorsOptions)); } + keyserverRouter.post( + '/fc_comm_tagged', + webhookPayloadHandler(taggedCommFarcasterResponder), + ); + for (const endpoint in jsonEndpoints) { // $FlowFixMe Flow thinks endpoint is string const responder = jsonEndpoints[endpoint]; diff --git a/keyserver/src/responders/farcaster-webhook-responders.js b/keyserver/src/responders/farcaster-webhook-responders.js new file mode 100644 --- /dev/null +++ b/keyserver/src/responders/farcaster-webhook-responders.js @@ -0,0 +1,20 @@ +// @flow + +import type { $Request } from 'express'; + +import { neynarWebhookCastCreatedEventValidator } from 'lib/types/validators/farcaster-webhook-validators.js'; +import { assertWithValidator } from 'lib/utils/validation-utils.js'; + +const taggedCommFarcasterInputValidator = + neynarWebhookCastCreatedEventValidator; + +async function taggedCommFarcasterResponder(request: $Request): Promise { + const event = assertWithValidator( + request.body, + taggedCommFarcasterInputValidator, + ); + + console.log(event); +} + +export { taggedCommFarcasterResponder, taggedCommFarcasterInputValidator }; diff --git a/keyserver/src/responders/handlers.js b/keyserver/src/responders/handlers.js --- a/keyserver/src/responders/handlers.js +++ b/keyserver/src/responders/handlers.js @@ -111,6 +111,29 @@ }; } +type WebhookPayloadResponder = (request: $Request) => Promise; +function webhookPayloadHandler( + responder: WebhookPayloadResponder, +): (req: $Request, res: $Response) => Promise { + return async (req: $Request, res: $Response) => { + try { + if (!req.body || typeof req.body !== 'object') { + throw new ServerError('invalid_parameters'); + } + + const responderResult = await responder(req); + + if (res.headersSent) { + return; + } + const result = { ...responderResult }; + res.json({ success: true, ...result }); + } catch (e) { + await handleException(e, res); + } + }; +} + function httpGetHandler( responder: HTTPGetResponder, ): (req: $Request, res: $Response) => Promise { @@ -245,4 +268,5 @@ downloadHandler, htmlHandler, uploadHandler, + webhookPayloadHandler, }; diff --git a/lib/types/farcaster-types.js b/lib/types/farcaster-types.js --- a/lib/types/farcaster-types.js +++ b/lib/types/farcaster-types.js @@ -41,3 +41,38 @@ +description: string, ... }; + +export type NeynarWebhookCastAuthor = { + +object: 'user', + +fid: number, + +custody_address: string, + +username: string, + +display_name: string, + +pfp_url: string, + ... +}; + +export type NeynarWebhookCastCreatedData = { + +object: 'cast', + +hash: string, + +thread_hash: string, + +text: string, + +channel?: ?NeynarWebhookChannel, + +parent_hash?: ?string, + +author: NeynarWebhookCastAuthor, + ... +}; + +export type NeynarWebhookChannel = { + +id: string, + +name: string, + +image_url: string, + ... +}; + +export type NeynarWebhookCastCreatedEvent = { + +created_at: number, + +type: 'cast.created', + +data: NeynarWebhookCastCreatedData, + ... +}; diff --git a/lib/types/validators/farcaster-webhook-validators.js b/lib/types/validators/farcaster-webhook-validators.js new file mode 100644 --- /dev/null +++ b/lib/types/validators/farcaster-webhook-validators.js @@ -0,0 +1,45 @@ +// @flow + +import t, { type TInterface } from 'tcomb'; + +import type { + NeynarWebhookCastCreatedData, + NeynarWebhookChannel, + NeynarWebhookCastAuthor, + NeynarWebhookCastCreatedEvent, +} from '../farcaster-types.js'; + +export const neynarWebhookCastAuthorValidator: TInterface = + t.interface({ + object: t.enums.of(['user']), + fid: t.Number, + custody_address: t.String, + username: t.String, + display_name: t.String, + pfp_url: t.String, + }); + +export const neynarWebhookChannelValidator: TInterface = + t.interface({ + id: t.String, + name: t.String, + image_url: t.String, + }); + +export const neynarWebhookCastCreatedDataValidator: TInterface = + t.interface({ + object: t.enums.of(['cast']), + hash: t.String, + thread_hash: t.String, + text: t.String, + channel: t.maybe(neynarWebhookChannelValidator), + parent_hash: t.maybe(t.String), + author: neynarWebhookCastAuthorValidator, + }); + +export const neynarWebhookCastCreatedEventValidator: TInterface = + t.interface({ + created_at: t.Number, + type: t.enums.of(['cast.created']), + data: neynarWebhookCastCreatedDataValidator, + });