• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hicommonwealth / commonwealth / 12796058250

15 Jan 2025 08:00PM UTC coverage: 48.038% (-0.1%) from 48.168%
12796058250

Pull #10529

github

web-flow
Merge e87fef53f into ae253ad15
Pull Request #10529: Add contest bot webhook

1314 of 3052 branches covered (43.05%)

Branch coverage included in aggregate %.

6 of 36 new or added lines in 6 files covered. (16.67%)

1 existing line in 1 file now uncovered.

2592 of 5079 relevant lines covered (51.03%)

34.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

3.7
/libs/model/src/policies/FarcasterWorker.policy.ts
1
import { command, logger, Policy } from '@hicommonwealth/core';
2
import { events } from '@hicommonwealth/schemas';
3
import {
4
  buildFarcasterContestFrameUrl,
5
  getBaseUrl,
6
} from '@hicommonwealth/shared';
7
import { NeynarAPIClient } from '@neynar/nodejs-sdk';
8
import { Op } from 'sequelize';
9
import { config, models } from '..';
10
import { CreateBotContest } from '../bot/CreateBotContest.command';
11
import { systemActor } from '../middleware';
12
import { mustExist } from '../middleware/guards';
13
import {
14
  buildFarcasterContentUrl,
15
  buildFarcasterWebhookName,
16
  publishCast,
17
} from '../utils';
18
import {
19
  createOnchainContestContent,
20
  createOnchainContestVote,
21
} from './contest-utils';
22

23
const log = logger(import.meta);
24✔
24

25
const inputs = {
24✔
26
  FarcasterCastCreated: events.FarcasterCastCreated,
27
  FarcasterReplyCastCreated: events.FarcasterReplyCastCreated,
28
  FarcasterVoteCreated: events.FarcasterVoteCreated,
29
  FarcasterContestBotMentioned: events.FarcasterContestBotMentioned,
30
};
31

32
export function FarcasterWorker(): Policy<typeof inputs> {
33
  return {
×
34
    inputs,
35
    body: {
36
      FarcasterCastCreated: async ({ payload }) => {
37
        const frame_url = new URL(payload.embeds[0].url).pathname;
×
38
        const contest_address = frame_url
×
39
          .split('/')
40
          .find((str) => str.startsWith('0x'));
×
41

42
        const contestManager = await models.ContestManager.findOne({
×
43
          where: {
44
            cancelled: {
45
              [Op.not]: true,
46
            },
47
            ended: {
48
              [Op.not]: true,
49
            },
50
            contest_address,
51
          },
52
        });
53
        mustExist('Contest Manager', contestManager);
×
54

55
        if (contestManager.farcaster_frame_hashes?.includes(payload.hash)) {
×
56
          log.warn(
×
57
            `farcaster frame hash already added to contest manager: ${payload.hash}`,
58
          );
59
          return;
×
60
        }
61

62
        // create/update webhook to listen for replies on this cast
63
        const webhookName = buildFarcasterWebhookName(
×
64
          contestManager.contest_address,
65
        );
66

67
        // if webhook exists, update target hashes, otherwise create new webhook
68
        const client = new NeynarAPIClient(config.CONTESTS.NEYNAR_API_KEY!);
×
69
        if (contestManager.neynar_webhook_id) {
×
70
          await client.updateWebhook(
×
71
            contestManager.neynar_webhook_id,
72
            webhookName,
73
            config.CONTESTS.NEYNAR_REPLY_WEBHOOK_URL!,
74
            {
75
              subscription: {
76
                'cast.created': {
77
                  parent_hashes: [
78
                    ...(contestManager.farcaster_frame_hashes || []),
×
79
                    payload.hash,
80
                  ],
81
                },
82
              },
83
            },
84
          );
85
        } else {
86
          const neynarWebhook = await client.publishWebhook(
×
87
            webhookName,
88
            config.CONTESTS.NEYNAR_REPLY_WEBHOOK_URL!,
89
            {
90
              subscription: {
91
                'cast.created': {
92
                  parent_hashes: [payload.hash],
93
                },
94
              },
95
            },
96
          );
97
          contestManager.neynar_webhook_id = neynarWebhook.webhook!.webhook_id;
×
98
          contestManager.neynar_webhook_secret =
×
99
            neynarWebhook.webhook?.secrets.at(0)?.value;
100
        }
101

102
        // append frame hash to Contest Manager
103
        contestManager.farcaster_frame_hashes = [
×
104
          ...(contestManager.farcaster_frame_hashes || []),
×
105
          payload.hash,
106
        ];
107

108
        await contestManager.save();
×
109
      },
110
      FarcasterReplyCastCreated: async ({ payload }) => {
111
        // find associated contest manager by parent cast hash
112
        const contestManager = await models.ContestManager.findOne({
×
113
          where: {
114
            cancelled: {
115
              [Op.not]: true,
116
            },
117
            ended: {
118
              [Op.not]: true,
119
            },
120
            farcaster_frame_hashes: {
121
              [Op.contains]: [payload.parent_hash!],
122
            },
123
          },
124
        });
125
        mustExist('Contest Manager', contestManager);
×
126

127
        const community = await models.Community.findByPk(
×
128
          contestManager.community_id,
129
          {
130
            include: [
131
              {
132
                model: models.ChainNode.scope('withPrivateData'),
133
                required: false,
134
              },
135
            ],
136
          },
137
        );
138
        mustExist('Community with Chain Node', community?.ChainNode);
×
139

140
        const contestManagers = [
×
141
          {
142
            url: community.ChainNode!.private_url! || community.ChainNode!.url!,
×
143
            contest_address: contestManager.contest_address,
144
            actions: [],
145
          },
146
        ];
147

148
        // create onchain content from reply cast
149
        const content_url = buildFarcasterContentUrl(
×
150
          payload.parent_hash!,
151
          payload.hash,
152
        );
153
        await createOnchainContestContent({
×
154
          contestManagers,
155
          bypass_quota: true,
156
          author_address: payload.verified_address,
157
          content_url,
158
        });
159
      },
160
      FarcasterVoteCreated: async ({ payload }) => {
161
        const { parent_hash, hash } = payload.cast;
×
162
        const content_url = buildFarcasterContentUrl(parent_hash!, hash);
×
163

164
        const contestManager = await models.ContestManager.findOne({
×
165
          where: {
166
            cancelled: {
167
              [Op.not]: true,
168
            },
169
            ended: {
170
              [Op.not]: true,
171
            },
172
            contest_address: payload.contest_address,
173
          },
174
        });
175
        mustExist('Contest Manager', contestManager);
×
176

177
        // find content by url
178
        const contestActions = await models.ContestAction.findAll({
×
179
          where: {
180
            contest_address: contestManager.contest_address,
181
            action: 'added',
182
            content_url,
183
          },
184
        });
185

186
        const community = await models.Community.findByPk(
×
187
          contestManager.community_id,
188
          {
189
            include: [
190
              {
191
                model: models.ChainNode.scope('withPrivateData'),
192
                required: false,
193
              },
194
            ],
195
          },
196
        );
197
        mustExist('Community with Chain Node', community?.ChainNode);
×
198

199
        const contestManagers = contestActions.map((ca) => ({
×
200
          url: community.ChainNode!.private_url! || community.ChainNode!.url!,
×
201
          contest_address: contestManager.contest_address,
202
          content_id: ca.content_id,
203
        }));
204

205
        await createOnchainContestVote({
×
206
          contestManagers,
207
          author_address: payload.verified_address,
208
          content_url,
209
        });
210
      },
211
      FarcasterContestBotMentioned: async ({ payload }) => {
NEW
212
        const contestAddress = await command(CreateBotContest(), {
×
213
          actor: systemActor({}),
214
          payload: {
215
            castHash: payload.hash!,
216
            prompt: payload.text,
217
          },
218
        });
NEW
219
        if (contestAddress) {
×
NEW
220
          await publishCast(
×
221
            payload.hash,
222
            ({ username }) =>
NEW
223
              `Hey @${username}, your contest has been created.`,
×
224
            {
225
              embed: `${getBaseUrl(config.APP_ENV, config.CONTESTS.FARCASTER_NGROK_DOMAIN!)}${buildFarcasterContestFrameUrl(contestAddress)}`,
226
            },
227
          );
228
        }
229
      },
230
    },
231
  };
232
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc