Skip to content

Commit b5259f9

Browse files
committed
feat: support command options on discord
1 parent 82b4bff commit b5259f9

File tree

6 files changed

+367
-146
lines changed

6 files changed

+367
-146
lines changed

libs/definitions/src/definition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IntegrationTriggerService } from '../../../apps/api/src/integration-tri
1616
import { Integration } from '../../../apps/api/src/integrations/entities/integration'
1717
import { WorkflowAction } from '../../../apps/api/src/workflow-actions/entities/workflow-action'
1818
import { WorkflowTrigger } from '../../../apps/api/src/workflow-triggers/entities/workflow-trigger'
19-
import { OperationRunnerService, OperationRunOptions } from '../../../apps/runner/src/services/operation-runner.service'
19+
import { OperationRunOptions, OperationRunnerService } from '../../../apps/runner/src/services/operation-runner.service'
2020
import { Operation } from './operation'
2121
import { OperationTrigger } from './operation-trigger'
2222
import { OperationOffChain } from './opertion-offchain'

libs/definitions/src/integration-definition.factory.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { CoinMarketCapDefinition } from './integration-definitions/coinmarketcap
2323
import { CronoscanDefinition } from './integration-definitions/cronoscan.definition'
2424
import { DecentralandMarketplaceDefinition } from './integration-definitions/decentraland-marketplace.definition'
2525
import { DexScreenerDefinition } from './integration-definitions/dexscreener.definition'
26-
import { DiscordDefinition } from './integration-definitions/discord.definition'
26+
import { DiscordDefinition } from './integration-definitions/discord/discord.definition'
2727
import { DiscourseDefinition } from './integration-definitions/discourse.definition'
2828
import { EmailDefinition } from './integration-definitions/email.definition'
2929
import { EnsDefinition } from './integration-definitions/ens/ens.definition'

libs/definitions/src/integration-definitions/discord.definition.ts renamed to libs/definitions/src/integration-definitions/discord/discord.definition.ts

Lines changed: 15 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import { Integration } from 'apps/api/src/integrations/entities/integration'
99
import { WorkflowAction } from 'apps/api/src/workflow-actions/entities/workflow-action'
1010
import { WorkflowTrigger } from 'apps/api/src/workflow-triggers/entities/workflow-trigger'
1111
import { OperationRunOptions } from 'apps/runner/src/services/operation-runner.service'
12-
import { StaticRunner } from 'apps/runner/src/services/static-runner.service'
1312
import { InteractionResponseType, InteractionType, verifyKey } from 'discord-interactions'
1413
import { Request } from 'express'
1514
import { OpenAPIObject } from 'openapi3-ts'
1615
import { OptionsWithUrl } from 'request'
17-
import { GetAsyncSchemasProps, IntegrationHookInjects, RequestInterceptorOptions, StepInputs } from '../definition'
16+
import { GetAsyncSchemasProps, IntegrationHookInjects, RequestInterceptorOptions } from '../../definition'
17+
import { DiscordLib } from './discord.lib'
18+
import { NewSlashCommandGuild } from './triggers/new-slash-command-guild.trigger'
1819

1920
const CHANNEL_TYPES = {
2021
GUILD_TEXT: 0,
@@ -43,6 +44,8 @@ export class DiscordDefinition extends SingleIntegrationDefinition {
4344
integrationVersion = '10'
4445
schemaUrl = null
4546

47+
triggers = [new NewSlashCommandGuild()]
48+
4649
// standard oauth2 refresh token won't work for discord ()
4750
async refreshCredentials(credentials: Record<string, any>): Promise<Record<string, any>> {
4851
return credentials
@@ -73,36 +76,13 @@ export class DiscordDefinition extends SingleIntegrationDefinition {
7376
return opts
7477
}
7578

76-
// We need to ensure the user has access to the guild id and the channel id belongs to the user
77-
// This must be run before create and update triggers and actions
78-
private async ensurePermissions(inputs: StepInputs, accountCredential?: AccountCredential | null) {
79-
const credentials = accountCredential?.credentials ?? {}
80-
inputs.guildId = credentials.guild_id // enforce guild id
81-
if (inputs.channelId) {
82-
const { outputs: channels } = await StaticRunner.run({
83-
definition: this,
84-
actionKey: 'getGuildChannels',
85-
inputs: {
86-
guildId: credentials.guild_id,
87-
},
88-
accountCredential,
89-
})
90-
const hasChannel = (channels as unknown as any[]).some(
91-
(channel) => channel.id.toString() === inputs.channelId.toString(),
92-
)
93-
if (!hasChannel) {
94-
throw new Error(`Invalid permissions to access channel ${inputs.channelId}`)
95-
}
96-
}
97-
}
98-
9979
async beforeCreateWorkflowAction(
10080
workflowAction: Partial<WorkflowAction>,
10181
integrationAction: IntegrationAction,
10282
accountCredential: AccountCredential | null,
10383
): Promise<Partial<WorkflowAction>> {
104-
await this.ensurePermissions(workflowAction.inputs ?? {}, accountCredential)
105-
return workflowAction
84+
await DiscordLib.ensurePermissions(workflowAction.inputs ?? {}, accountCredential!.credentials)
85+
return super.beforeCreateWorkflowAction(workflowAction, integrationAction, accountCredential)
10686
}
10787

10888
async beforeUpdateWorkflowAction(
@@ -111,31 +91,17 @@ export class DiscordDefinition extends SingleIntegrationDefinition {
11191
integrationAction: IntegrationAction,
11292
accountCredential: AccountCredential | null,
11393
): Promise<Partial<WorkflowAction>> {
114-
await this.ensurePermissions(update.inputs ?? {}, accountCredential)
115-
return update
94+
await DiscordLib.ensurePermissions(update.inputs ?? {}, accountCredential!.credentials)
95+
return super.beforeUpdateWorkflowAction(update, prevWorkflowAction, integrationAction, accountCredential)
11696
}
11797

11898
async beforeCreateWorkflowTrigger(
11999
workflowTrigger: Partial<WorkflowTrigger>,
120100
integrationTrigger: IntegrationTrigger,
121101
accountCredential: AccountCredential | null,
122102
): Promise<Partial<WorkflowTrigger>> {
123-
await this.ensurePermissions(workflowTrigger.inputs ?? {}, accountCredential)
124-
switch (integrationTrigger.key) {
125-
case 'newSlashCommandGuild':
126-
await StaticRunner.run({
127-
definition: this,
128-
actionKey: 'createGuildCommand',
129-
inputs: {
130-
applicationId: process.env.DISCORD_CLIENT_ID,
131-
guildId: accountCredential?.credentials.guild_id,
132-
name: workflowTrigger.inputs?.name,
133-
description: workflowTrigger.inputs?.description,
134-
},
135-
accountCredential,
136-
})
137-
}
138-
return workflowTrigger
103+
await DiscordLib.ensurePermissions(workflowTrigger.inputs ?? {}, accountCredential!.credentials)
104+
return super.beforeCreateWorkflowTrigger(workflowTrigger, integrationTrigger, accountCredential)
139105
}
140106

141107
async beforeUpdateWorkflowTrigger(
@@ -144,20 +110,8 @@ export class DiscordDefinition extends SingleIntegrationDefinition {
144110
integrationTrigger: IntegrationTrigger,
145111
accountCredential: AccountCredential | null,
146112
): Promise<Partial<WorkflowTrigger>> {
147-
await this.ensurePermissions(update.inputs ?? {}, accountCredential)
148-
return update
149-
}
150-
151-
async beforeDeleteWorkflowTrigger(
152-
workflowTrigger: Partial<WorkflowTrigger>,
153-
integrationTrigger: IntegrationTrigger,
154-
accountCredential: AccountCredential | null,
155-
): Promise<void> {
156-
switch (integrationTrigger.key) {
157-
case 'newSlashCommandGuild':
158-
// TODO delete command
159-
break
160-
}
113+
await DiscordLib.ensurePermissions(update.inputs ?? {}, accountCredential!.credentials)
114+
return super.beforeUpdateWorkflowTrigger(update, prevWorkflowTrigger, integrationTrigger, accountCredential)
161115
}
162116

163117
async onHookReceived(
@@ -192,6 +146,8 @@ export class DiscordDefinition extends SingleIntegrationDefinition {
192146
if (type === InteractionType.APPLICATION_COMMAND) {
193147
const { name, guild_id } = data
194148

149+
console.log(`Received slash command ${name} in guild ${guild_id}`, data)
150+
195151
const integrationTrigger = await injects.integrationTriggerService.findOne({ key: 'newSlashCommandGuild' }) // TODO check integration = discord
196152
if (!integrationTrigger) {
197153
throw new Error(`Integration trigger for discord slash command not configured correctly`)
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { Logger, UnauthorizedException } from '@nestjs/common'
2+
import axios from 'axios'
3+
4+
export const DiscordLib = {
5+
logger: new Logger('DiscordLib'),
6+
7+
async createCommand({
8+
guildId,
9+
name,
10+
description,
11+
options = [],
12+
}: {
13+
guildId: string
14+
name: string
15+
description: string
16+
options: any[]
17+
}) {
18+
const url = `https://discord.com/api/v9/applications/${process.env.DISCORD_CLIENT_ID}/guilds/${guildId}/commands`
19+
const command = {
20+
name,
21+
description,
22+
options,
23+
}
24+
const response = await axios.post(url, command, {
25+
headers: {
26+
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
27+
'Content-Type': 'application/json',
28+
},
29+
})
30+
return response.data
31+
},
32+
33+
async updateCommand({
34+
guildId,
35+
commandId,
36+
name,
37+
description,
38+
options = [],
39+
}: {
40+
guildId: string
41+
commandId: string
42+
name: string
43+
description: string
44+
options: any[]
45+
}) {
46+
const url = `https://discord.com/api/v9/applications/${process.env.DISCORD_CLIENT_ID}/guilds/${guildId}/commands/${commandId}`
47+
const command = {
48+
name,
49+
description,
50+
options,
51+
}
52+
const response = await axios.patch(url, command, {
53+
headers: {
54+
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
55+
'Content-Type': 'application/json',
56+
},
57+
})
58+
return response.data
59+
},
60+
61+
async deleteCommand({ guildId, commandId }: { guildId: string; commandId: string }) {
62+
const url = `https://discord.com/api/v9/applications/${process.env.DISCORD_CLIENT_ID}/guilds/${guildId}/commands/${commandId}`
63+
const response = await axios.delete(url, {
64+
headers: {
65+
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
66+
'Content-Type': 'application/json',
67+
},
68+
})
69+
return response.status
70+
},
71+
72+
async deleteAllCommands(guildId: string) {
73+
const url = `https://discord.com/api/v9/applications/${process.env.DISCORD_CLIENT_ID}/guilds/${guildId}/commands`
74+
75+
// First, list all the commands
76+
const listCommandsResponse = await axios.get(url, {
77+
headers: {
78+
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
79+
},
80+
})
81+
82+
// Delete each command in the server
83+
for (const command of listCommandsResponse.data) {
84+
await axios.delete(`${url}/${command.id}`, {
85+
headers: {
86+
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
87+
},
88+
})
89+
}
90+
},
91+
92+
async getCommandIdByName(guildId: string, commandName: string): Promise<string | null> {
93+
const url = `https://discord.com/api/v9/applications/${process.env.DISCORD_CLIENT_ID}/guilds/${guildId}/commands`
94+
95+
this.logger.log(`Getting command ID for ${commandName}`)
96+
97+
try {
98+
// List all the commands
99+
const listCommandsResponse = await axios.get(url, {
100+
headers: {
101+
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
102+
},
103+
})
104+
105+
// Find the command with the specified name
106+
const command = listCommandsResponse.data.find((cmd: any) => cmd.name === commandName)
107+
108+
if (!command) {
109+
console.log('Command not found')
110+
return null
111+
}
112+
113+
// Return the command ID
114+
return command.id
115+
} catch (error) {
116+
this.logger.error('Error retrieving command ID:', error)
117+
return null
118+
}
119+
},
120+
121+
async getGuildChannels(guildId: string): Promise<any[] | null> {
122+
const url = `https://discord.com/api/v9/guilds/${guildId}/channels`
123+
124+
try {
125+
const response = await axios.get(url, {
126+
headers: {
127+
Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}`,
128+
},
129+
})
130+
return response.data
131+
} catch (error) {
132+
this.logger.error('Error retrieving guild channels:', error)
133+
return null
134+
}
135+
},
136+
137+
// We need to ensure the user has access to the guild id and the channel id belongs to the user
138+
// This must be run before create and update triggers and actions
139+
async ensurePermissions(inputs: Record<string, any>, credentials: Record<string, any>) {
140+
inputs.guildId = credentials.guild_id // enforce guild id
141+
if (inputs.channelId) {
142+
const { outputs: channels } = this.getGuildChannels(inputs.guildId)
143+
const hasChannel = (channels as unknown as any[]).some(
144+
(channel) => channel.id.toString() === inputs.channelId.toString(),
145+
)
146+
if (!hasChannel) {
147+
throw new UnauthorizedException(`Invalid permissions to access channel ${inputs.channelId}`)
148+
}
149+
}
150+
},
151+
}

0 commit comments

Comments
 (0)