1 | import { ActorListSortBy, ApifyClient } from 'apify-client@^2.19.0'; |
2 | import { createHash } from 'node:crypto'; |
3 |
|
4 | type ApifyApiKey = { |
5 | api_key: string; |
6 | }; |
7 |
|
8 | type Apify = { |
9 | token: string; |
10 | }; |
11 |
|
12 | type WebhookEventTypes = |
13 | | 'ACTOR.RUN.SUCCEEDED' |
14 | | 'ACTOR.RUN.FAILED' |
15 | | 'ACTOR.RUN.TIMED_OUT' |
16 | | 'ACTOR.RUN.ABORTED' |
17 |
|
18 | type ApifyWebhookConfig = { |
19 | url: string; |
20 | token: string; |
21 | }; |
22 |
|
23 | type ActorSource = 'RECENTLY_USED_ACTORS' | 'APIFY_STORE_ACTORS'; |
24 |
|
25 | export type DynSelect_actorId = string; |
26 | export async function actorId(actorSource: ActorSource, api_key?: ApifyApiKey, oauth_token?: Apify) { |
27 | if (!api_key?.api_key && !oauth_token?.token) { |
28 | return [{ value: '', label: 'Missing Apify API key or OAuth token' }]; |
29 | } |
30 |
|
31 | const client = createClient(api_key, oauth_token); |
32 |
|
33 | const mapActorToSelectOption = (actor: any) => { |
34 | const optionName = actor.title |
35 | ? `${actor.title} (${actor.username}/${actor.name})` |
36 | : `${actor.username}/${actor.name}`; |
37 |
|
38 | return { |
39 | label: optionName, |
40 | value: actor.id, |
41 | }; |
42 | }; |
43 |
|
44 | try { |
45 | if (actorSource === 'RECENTLY_USED_ACTORS') { |
46 | const recentActors = await client.actors().list({ |
47 | limit: 100, |
48 | offset: 0, |
49 | sortBy: ActorListSortBy.LAST_RUN_STARTED_AT, |
50 | desc: true, |
51 | }); |
52 | return recentActors.items.map(mapActorToSelectOption); |
53 | } |
54 |
|
55 | const storeActors = await client.store().list({ |
56 | limit: 1000, |
57 | offset: 0, |
58 | }); |
59 | return storeActors.items.map(mapActorToSelectOption); |
60 | } catch (error: any) { |
61 | return [ |
62 | { value: '', label: `Failed to load actors: ${error.message || error}` }, |
63 | ]; |
64 | } |
65 | } |
66 |
|
67 | function prettifyEvent(e: string) { |
68 | return e.replace(/^ACTOR\.RUN\./, ''); |
69 | } |
70 |
|
71 | function generateIdempotencyKey( |
72 | id: string, |
73 | eventTypes: WebhookEventTypes[], |
74 | requestUrl: string |
75 | ): string { |
76 | const sortedEventTypes = [...eventTypes].sort(); |
77 | const url = new URL(requestUrl); |
78 | const pathname = url.pathname; |
79 | const hash = createHash('sha256'); |
80 | hash.update(`${id}:${sortedEventTypes.join(',')}:${pathname}`); |
81 | return hash.digest('hex'); |
82 | } |
83 |
|
84 | const createClient = (api_key?: ApifyApiKey, oauth_token?: Apify): ApifyClient => { |
85 | const token = oauth_token?.token ?? api_key?.api_key; |
86 | if (!token) { |
87 | throw new Error('Missing Apify API key or OAuth token'); |
88 | } |
89 |
|
90 | return new ApifyClient({ |
91 | token: token, |
92 | requestInterceptors: [ |
93 | (request) => { |
94 | if (!request.headers) { |
95 | request.headers = {}; |
96 | } |
97 | request.headers['x-apify-integration-platform'] = 'windmill'; |
98 | return request; |
99 | }, |
100 | ], |
101 | }); |
102 | }; |
103 |
|
104 | export async function main( |
105 | actorSource: ActorSource, |
106 | apifyWebhookConfig: ApifyWebhookConfig, |
107 | actorId: DynSelect_actorId, |
108 | eventTypes: WebhookEventTypes[], |
109 | api_key?: ApifyApiKey, |
110 | oauth_token?: Apify, |
111 | ) { |
112 | if (!actorId) { |
113 | return { error: 'Actor ID is required' }; |
114 | } |
115 |
|
116 | const client = createClient(api_key, oauth_token); |
117 |
|
118 | const idempotencyKey = generateIdempotencyKey( |
119 | actorId, |
120 | eventTypes, |
121 | apifyWebhookConfig.url |
122 | ); |
123 | const headersTemplate = JSON.stringify({ |
124 | Authorization: `Bearer ${apifyWebhookConfig.token}`, |
125 | }); |
126 |
|
127 | |
128 | const actor = await client.actor(actorId).get(); |
129 | const actorName = actor && (actor.title || actor.name); |
130 | const webhookEvents = eventTypes |
131 | .map((event) => prettifyEvent(event)) |
132 | .join(', '); |
133 |
|
134 | try { |
135 | const response = await client.webhooks().create({ |
136 | requestUrl: apifyWebhookConfig.url, |
137 | eventTypes: eventTypes, |
138 | condition: { |
139 | actorId: actorId, |
140 | }, |
141 | idempotencyKey, |
142 | headersTemplate, |
143 | shouldInterpolateStrings: true, |
144 | description: `${actorName}: ${webhookEvents}`, |
145 | title: "Windmill integration" |
146 | }); |
147 |
|
148 | return response; |
149 | } catch (e: any) { |
150 | return { error: `Failed to create a webhook. Reason: ${e.message}` }; |
151 | } |
152 | } |
153 |
|