Page MenuHomePhabricator

D13775.id45545.diff
No OneTemporary

D13775.id45545.diff

diff --git a/keyserver/src/responders/farcaster-webhook-responders.js b/keyserver/src/responders/farcaster-webhook-responders.js
--- a/keyserver/src/responders/farcaster-webhook-responders.js
+++ b/keyserver/src/responders/farcaster-webhook-responders.js
@@ -3,17 +3,109 @@
import { createHmac } from 'crypto';
import type { $Request } from 'express';
+import bots from 'lib/facts/bots.js';
+import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js';
import { type NeynarWebhookCastCreatedEvent } from 'lib/types/farcaster-types.js';
+import { threadTypes } from 'lib/types/thread-types-enum.js';
+import { type NewThreadResponse } from 'lib/types/thread-types.js';
import { neynarWebhookCastCreatedEventValidator } from 'lib/types/validators/farcaster-webhook-validators.js';
import { ServerError } from 'lib/utils/errors.js';
import { assertWithValidator } from 'lib/utils/validation-utils.js';
+import {
+ getFarcasterChannelTagBlob,
+ createOrUpdateFarcasterChannelTag,
+ farcasterChannelTagBlobValidator,
+} from '../creators/farcaster-channel-tag-creator.js';
+import { createThread } from '../creators/thread-creator.js';
+import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
+import { createBotViewer } from '../session/bots.js';
+import { updateRole } from '../updaters/thread-updaters.js';
+import { thisKeyserverAdmin, thisKeyserverID } from '../user/identity.js';
+import { getFarcasterBotConfig } from '../utils/farcaster-bot.js';
+import { getVerifiedUserIDForFID } from '../utils/farcaster-utils.js';
+import { neynarClient } from '../utils/fc-cache.js';
import { getNeynarConfig } from '../utils/neynar-utils.js';
const taggedCommFarcasterInputValidator =
neynarWebhookCastCreatedEventValidator;
const threadHashTagRegex = /\B#createathread\b/i;
+const noChannelCommunityID = '80887273';
+const { commbot } = bots;
+const commbotViewer = createBotViewer(commbot.userID);
+
+async function createTaggedFarcasterCommunity(
+ channelID: string,
+ taggerUserID: ?string,
+): Promise<NewThreadResponse> {
+ const keyserverAdminPromise = thisKeyserverAdmin();
+
+ const neynarChannel =
+ await neynarClient?.fetchFarcasterChannelByID(channelID);
+ if (!neynarChannel) {
+ throw new ServerError('channel_not_found');
+ }
+
+ const leadFID = neynarChannel.lead.fid.toString();
+ const [leadUserID, keyserverAdmin] = await Promise.all([
+ getVerifiedUserIDForFID(leadFID),
+ keyserverAdminPromise,
+ ]);
+
+ const communityAdminID = leadUserID
+ ? leadUserID
+ : taggerUserID || keyserverAdmin.id;
+
+ const initialMemberIDs = [
+ ...new Set([leadUserID, taggerUserID, communityAdminID].filter(Boolean)),
+ ];
+
+ const newThreadResponse = await createThread(
+ commbotViewer,
+ {
+ type: threadTypes.COMMUNITY_ROOT,
+ name: neynarChannel.name,
+ initialMemberIDs,
+ },
+ {
+ forceAddMembers: true,
+ addViewerAsGhost: true,
+ },
+ );
+
+ const { newThreadID } = newThreadResponse;
+ const fetchThreadResult = await fetchServerThreadInfos({
+ threadID: newThreadResponse.newThreadID,
+ });
+ const threadInfo = fetchThreadResult.threadInfos[newThreadID];
+
+ if (!threadInfo) {
+ throw new ServerError('fetch_failed');
+ }
+
+ const adminRoleID = Object.keys(threadInfo.roles).find(
+ roleID => threadInfo.roles[roleID].name === 'Admins',
+ );
+
+ if (!adminRoleID) {
+ throw new ServerError('community_missing_admin');
+ }
+
+ await updateRole(commbotViewer, {
+ threadID: newThreadID,
+ memberIDs: [communityAdminID],
+ role: adminRoleID,
+ });
+
+ await createOrUpdateFarcasterChannelTag(commbotViewer, {
+ commCommunityID: newThreadResponse.newThreadID,
+ farcasterChannelID: channelID,
+ });
+
+ return newThreadResponse;
+}
+
async function verifyNeynarWebhookSignature(
signature: string,
event: NeynarWebhookCastCreatedEvent,
@@ -33,23 +125,80 @@
const { body } = req;
const event = assertWithValidator(body, taggedCommFarcasterInputValidator);
+ const {
+ author: { fid: eventTaggerFID },
+ text: eventText,
+ channel: eventChannel,
+ } = event.data;
- const { text: eventText } = event.data;
const foundCreateThreadHashTag = threadHashTagRegex.test(eventText);
if (!foundCreateThreadHashTag) {
return;
}
+ const taggerUserIDPromise = getVerifiedUserIDForFID(
+ eventTaggerFID.toString(),
+ );
+
const signature = req.header('X-Neynar-Signature');
if (!signature) {
throw new ServerError('missing_neynar_signature');
}
- const isValidSignature = await verifyNeynarWebhookSignature(signature, event);
+ const [isValidSignature, farcasterBotConfig] = await Promise.all([
+ verifyNeynarWebhookSignature(signature, event),
+ getFarcasterBotConfig(),
+ ]);
+
if (!isValidSignature) {
throw new ServerError('invalid_webhook_signature');
}
+
+ const eventChannelID = eventChannel?.id;
+ const isAuthoritativeFarcasterBot =
+ farcasterBotConfig.authoritativeFarcasterBot;
+
+ if (!eventChannelID && !isAuthoritativeFarcasterBot) {
+ return;
+ }
+
+ let channelCommunityID = noChannelCommunityID;
+ if (eventChannelID) {
+ const blobDownload = await getFarcasterChannelTagBlob(eventChannelID);
+ if (blobDownload.found) {
+ const blobText = await blobDownload.blob.text();
+ const blobObject = JSON.parse(blobText);
+ const farcasterChannelTagBlob = assertWithValidator(
+ blobObject,
+ farcasterChannelTagBlobValidator,
+ );
+
+ const { commCommunityID } = farcasterChannelTagBlob;
+ const commCommunityKeyserverID =
+ extractKeyserverIDFromID(commCommunityID);
+ const keyserverID = await thisKeyserverID();
+
+ if (keyserverID !== commCommunityKeyserverID) {
+ return;
+ }
+ channelCommunityID = commCommunityID.split('|')[1];
+ } else if (!blobDownload.found && blobDownload.status === 404) {
+ if (!isAuthoritativeFarcasterBot) {
+ return;
+ }
+
+ const taggerUserID = await taggerUserIDPromise;
+ const newThreadResponse = await createTaggedFarcasterCommunity(
+ eventChannelID,
+ taggerUserID,
+ );
+ channelCommunityID = newThreadResponse.newThreadID;
+ } else {
+ throw new ServerError('blob_fetch_failed');
+ }
+ }
+ console.log(channelCommunityID);
}
export { taggedCommFarcasterResponder, taggedCommFarcasterInputValidator };

File Metadata

Mime Type
text/plain
Expires
Tue, Nov 26, 10:46 AM (20 h, 27 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2584310
Default Alt Text
D13775.id45545.diff (6 KB)

Event Timeline