2
0

♻️ Migrate from got to ky (#1416)

Closes #1415
This commit is contained in:
Baptiste Arnaud
2024-04-05 09:01:16 +02:00
committed by GitHub
parent ccc7101dd3
commit d96f384e02
59 changed files with 990 additions and 628 deletions

View File

@ -13,7 +13,6 @@ type Props = {
sessionId: string
}
export const continueChat = async ({ origin, sessionId, message }: Props) => {
console.log('test')
const session = await getSession(sessionId)
if (!session) {

View File

@ -1,7 +1,7 @@
import { isNotEmpty } from '@typebot.io/lib/utils'
import { ContinueChatResponse } from '@typebot.io/schemas'
import { OpenAIBlock } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { HTTPError } from 'got'
import { HTTPError } from 'ky'
import { ClientOptions, OpenAI } from 'openai'
type Props = Pick<
@ -55,9 +55,9 @@ export const executeChatCompletionOpenAIRequest = async ({
} catch (error) {
if (error instanceof HTTPError) {
if (
(error.response.statusCode === 503 ||
error.response.statusCode === 500 ||
error.response.statusCode === 403) &&
(error.response.status === 503 ||
error.response.status === 500 ||
error.response.status === 403) &&
!isRetrying
) {
console.log('OpenAI API error - 503, retrying in 3 seconds')
@ -73,7 +73,7 @@ export const executeChatCompletionOpenAIRequest = async ({
isRetrying: true,
})
}
if (error.response.statusCode === 400) {
if (error.response.status === 400) {
const log = {
status: 'info',
description:
@ -93,8 +93,8 @@ export const executeChatCompletionOpenAIRequest = async ({
}
logs.push({
status: 'error',
description: `OpenAI API error - ${error.response.statusCode}`,
details: error.response.body,
description: `OpenAI API error - ${error.response.status}`,
details: await error.response.text(),
})
return { logs }
}

View File

@ -11,10 +11,11 @@ import {
ChatLog,
ExecutableHttpRequest,
AnswerInSessionState,
TypebotInSession,
} from '@typebot.io/schemas'
import { stringify } from 'qs'
import { isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
import got, { Method, HTTPError, OptionsInit } from 'got'
import ky, { HTTPError, Options } from 'ky'
import { resumeWebhookExecution } from './resumeWebhookExecution'
import { ExecuteIntegrationResponse } from '../../../types'
import { parseVariables } from '@typebot.io/variables/parseVariables'
@ -60,9 +61,11 @@ export const executeWebhookBlock = async (
})) as HttpRequest | null)
: null)
if (!webhook) return { outgoingEdgeId: block.outgoingEdgeId }
const parsedWebhook = await parseWebhookAttributes(state)({
const parsedWebhook = await parseWebhookAttributes({
webhook,
isCustomBody: block.options?.isCustomBody,
typebot: state.typebotsQueue[0].typebot,
answers: state.typebotsQueue[0].answers,
})
if (!parsedWebhook) {
logs.push({
@ -104,69 +107,69 @@ export const executeWebhookBlock = async (
const checkIfBodyIsAVariable = (body: string) => /^{{.+}}$/.test(body)
const parseWebhookAttributes =
(state: SessionState) =>
async ({
webhook,
isCustomBody,
}: {
webhook: HttpRequest
isCustomBody?: boolean
}): Promise<ParsedWebhook | undefined> => {
if (!webhook.url) return
const { typebot } = state.typebotsQueue[0]
const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers?.findIndex(
(h) =>
h.key?.toLowerCase() === 'authorization' &&
h.value?.toLowerCase()?.includes('basic')
)
const isUsernamePasswordBasicAuth =
basicAuthHeaderIdx !== -1 &&
isDefined(basicAuthHeaderIdx) &&
webhook.headers?.at(basicAuthHeaderIdx)?.value?.includes(':')
if (isUsernamePasswordBasicAuth) {
const [username, password] =
webhook.headers?.at(basicAuthHeaderIdx)?.value?.slice(6).split(':') ??
[]
basicAuth.username = username
basicAuth.password = password
webhook.headers?.splice(basicAuthHeaderIdx, 1)
}
const headers = convertKeyValueTableToObject(
webhook.headers,
typebot.variables
) as ExecutableHttpRequest['headers'] | undefined
const queryParams = stringify(
convertKeyValueTableToObject(webhook.queryParams, typebot.variables)
)
const bodyContent = await getBodyContent({
body: webhook.body,
answers: state.typebotsQueue[0].answers,
variables: typebot.variables,
isCustomBody,
})
const method = webhook.method ?? defaultWebhookAttributes.method
const { data: body, isJson } =
bodyContent && method !== HttpMethod.GET
? safeJsonParse(
parseVariables(typebot.variables, {
isInsideJson: !checkIfBodyIsAVariable(bodyContent),
})(bodyContent)
)
: { data: undefined, isJson: false }
return {
url: parseVariables(typebot.variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
),
basicAuth,
method,
headers,
body,
isJson,
}
export const parseWebhookAttributes = async ({
webhook,
isCustomBody,
typebot,
answers,
}: {
webhook: HttpRequest
isCustomBody?: boolean
typebot: TypebotInSession
answers: AnswerInSessionState[]
}): Promise<ParsedWebhook | undefined> => {
if (!webhook.url) return
const basicAuth: { username?: string; password?: string } = {}
const basicAuthHeaderIdx = webhook.headers?.findIndex(
(h) =>
h.key?.toLowerCase() === 'authorization' &&
h.value?.toLowerCase()?.includes('basic')
)
const isUsernamePasswordBasicAuth =
basicAuthHeaderIdx !== -1 &&
isDefined(basicAuthHeaderIdx) &&
webhook.headers?.at(basicAuthHeaderIdx)?.value?.includes(':')
if (isUsernamePasswordBasicAuth) {
const [username, password] =
webhook.headers?.at(basicAuthHeaderIdx)?.value?.slice(6).split(':') ?? []
basicAuth.username = username
basicAuth.password = password
webhook.headers?.splice(basicAuthHeaderIdx, 1)
}
const headers = convertKeyValueTableToObject(
webhook.headers,
typebot.variables
) as ExecutableHttpRequest['headers'] | undefined
const queryParams = stringify(
convertKeyValueTableToObject(webhook.queryParams, typebot.variables)
)
const bodyContent = await getBodyContent({
body: webhook.body,
answers,
variables: typebot.variables,
isCustomBody,
})
const method = webhook.method ?? defaultWebhookAttributes.method
const { data: body, isJson } =
bodyContent && method !== HttpMethod.GET
? safeJsonParse(
parseVariables(typebot.variables, {
isInsideJson: !checkIfBodyIsAVariable(bodyContent),
})(bodyContent)
)
: { data: undefined, isJson: false }
return {
url: parseVariables(typebot.variables)(
webhook.url + (queryParams !== '' ? `?${queryParams}` : '')
),
basicAuth,
method,
headers,
body,
isJson,
}
}
export const executeWebhook = async (
webhook: ParsedWebhook,
@ -177,7 +180,8 @@ export const executeWebhook = async (
startTimeShouldBeUpdated?: boolean
}> => {
const logs: ChatLog[] = []
const { headers, url, method, basicAuth, body, isJson } = webhook
const { headers, url, method, basicAuth, isJson } = webhook
const contentType = headers ? headers['Content-Type'] : undefined
const isLongRequest = params.disableRequestTimeout
@ -186,59 +190,60 @@ export const executeWebhook = async (
url?.includes(whiteListedUrl)
)
const isFormData = contentType?.includes('x-www-form-urlencoded')
let body = webhook.body
if (isFormData && isJson) body = parseFormDataBody(body as object)
const request = {
url,
method: method as Method,
method,
headers: headers ?? {},
...(basicAuth ?? {}),
json:
!contentType?.includes('x-www-form-urlencoded') && body && isJson
? body
: undefined,
form:
contentType?.includes('x-www-form-urlencoded') && body ? body : undefined,
body: body && !isJson ? (body as string) : undefined,
timeout: {
response: isNotDefined(env.CHAT_API_TIMEOUT)
? undefined
: params.timeout && params.timeout !== defaultTimeout
? Math.min(params.timeout, maxTimeout) * 1000
: isLongRequest
? maxTimeout * 1000
: defaultTimeout * 1000,
},
} satisfies OptionsInit
json: !isFormData && body && isJson ? body : undefined,
body: (isFormData && body ? body : undefined) as any,
timeout: isNotDefined(env.CHAT_API_TIMEOUT)
? undefined
: params.timeout && params.timeout !== defaultTimeout
? Math.min(params.timeout, maxTimeout) * 1000
: isLongRequest
? maxTimeout * 1000
: defaultTimeout * 1000,
} satisfies Options & { url: string; body: any }
try {
const response = await got(request.url, omit(request, 'url'))
const response = await ky(request.url, omit(request, 'url'))
const body = await response.text()
logs.push({
status: 'success',
description: webhookSuccessDescription,
details: {
statusCode: response.statusCode,
response: safeJsonParse(response.body).data,
statusCode: response.status,
response: safeJsonParse(body).data,
request,
},
})
return {
response: {
statusCode: response.statusCode,
data: safeJsonParse(response.body).data,
statusCode: response.status,
data: safeJsonParse(body).data,
},
logs,
startTimeShouldBeUpdated: true,
}
} catch (error) {
if (error instanceof HTTPError) {
const responseBody = await error.response.text()
const response = {
statusCode: error.response.statusCode,
data: safeJsonParse(error.response.body as string).data,
statusCode: error.response.status,
data: safeJsonParse(responseBody).data,
}
logs.push({
status: 'error',
description: webhookErrorDescription,
details: {
statusCode: error.response.statusCode,
statusCode: error.response.status,
request,
response,
},
@ -257,7 +262,7 @@ export const executeWebhook = async (
}
logs.push({
status: 'error',
description: `Webhook request timed out. (${request.timeout.response}ms)`,
description: `Webhook request timed out. (${request.timeout}ms)`,
details: {
response,
request,
@ -320,10 +325,18 @@ export const convertKeyValueTableToObject = (
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const safeJsonParse = (json: string): { data: any; isJson: boolean } => {
const safeJsonParse = (json: unknown): { data: any; isJson: boolean } => {
try {
return { data: JSON.parse(json), isJson: true }
return { data: JSON.parse(json as string), isJson: true }
} catch (err) {
return { data: json, isJson: false }
}
}
const parseFormDataBody = (body: object) => {
const searchParams = new URLSearchParams()
Object.entries(body as object).forEach(([key, value]) => {
searchParams.set(key, value)
})
return searchParams
}

View File

@ -4,7 +4,7 @@ import {
ZemanticAiCredentials,
ZemanticAiResponse,
} from '@typebot.io/schemas/features/blocks/integrations/zemanticAi'
import got from 'got'
import ky from 'ky'
import { decrypt } from '@typebot.io/lib/api/encryption/decrypt'
import { byId, isDefined, isEmpty } from '@typebot.io/lib'
import { ExecuteIntegrationResponse } from '../../../types'
@ -51,7 +51,7 @@ export const executeZemanticAiBlock = async (
})
try {
const res: ZemanticAiResponse = await got
const res: ZemanticAiResponse = await ky
.post(URL, {
headers: {
Authorization: `Bearer ${apiKey}`,

View File

@ -11,12 +11,12 @@ import { LogicBlockType } from '@typebot.io/schemas/features/blocks/logic/consta
type Props = {
typebots: Pick<PublicTypebot, 'groups'>[]
user?: User
userId: string | undefined
isPreview?: boolean
}
export const getPreviouslyLinkedTypebots =
({ typebots, user, isPreview }: Props) =>
export const fetchLinkedChildTypebots =
({ typebots, userId, isPreview }: Props) =>
async (
capturedLinkedBots: (Typebot | PublicTypebot)[]
): Promise<(Typebot | PublicTypebot)[]> => {
@ -40,13 +40,13 @@ export const getPreviouslyLinkedTypebots =
.filter(isDefined)
if (linkedTypebotIds.length === 0) return capturedLinkedBots
const linkedTypebots = (await fetchLinkedTypebots({
user,
userId,
typebotIds: linkedTypebotIds,
isPreview,
})) as (Typebot | PublicTypebot)[]
return getPreviouslyLinkedTypebots({
return fetchLinkedChildTypebots({
typebots: linkedTypebots,
user,
userId,
isPreview,
})([...capturedLinkedBots, ...linkedTypebots])
}

View File

@ -0,0 +1,20 @@
import { fetchLinkedTypebots } from './fetchLinkedTypebots'
type Props = {
parentTypebotIds: string[]
userId: string | undefined
isPreview?: boolean
}
export const fetchLinkedParentTypebots = ({
parentTypebotIds,
isPreview,
userId,
}: Props) =>
parentTypebotIds.length > 0
? fetchLinkedTypebots({
typebotIds: parentTypebotIds,
isPreview,
userId,
})
: []

View File

@ -1,20 +1,19 @@
import prisma from '@typebot.io/lib/prisma'
import { User } from '@typebot.io/prisma'
type Props = {
isPreview?: boolean
typebotIds: string[]
user?: User
userId: string | undefined
}
export const fetchLinkedTypebots = async ({
user,
userId,
isPreview,
typebotIds,
}: Props) => {
if (!user || !isPreview)
if (!userId || !isPreview)
return prisma.publicTypebot.findMany({
where: { id: { in: typebotIds } },
where: { typebotId: { in: typebotIds } },
})
const linkedTypebots = await prisma.typebot.findMany({
where: { id: { in: typebotIds } },
@ -39,7 +38,7 @@ export const fetchLinkedTypebots = async ({
return linkedTypebots.filter(
(typebot) =>
typebot.collaborators.some(
(collaborator) => collaborator.userId === user.id
) || typebot.workspace.members.some((member) => member.userId === user.id)
(collaborator) => collaborator.userId === userId
) || typebot.workspace.members.some((member) => member.userId === userId)
)
}

View File

@ -111,7 +111,6 @@ export const executeGroup = async (
logs,
visitedEdges,
}
console.log('yes')
const executionResponse = (
isLogicBlock(block)
? await executeLogic(newSessionState)(block)

View File

@ -16,7 +16,6 @@ import { env } from '@typebot.io/env'
export const executeIntegration =
(state: SessionState) =>
async (block: IntegrationBlock): Promise<ExecuteIntegrationResponse> => {
console.log('HI')
switch (block.type) {
case IntegrationBlockType.GOOGLE_SHEETS:
return {

View File

@ -27,7 +27,6 @@ export const executeForgedBlock = async (
const blockDef = forgedBlocks[block.type]
if (!blockDef) return { outgoingEdgeId: block.outgoingEdgeId }
const action = blockDef.actions.find((a) => a.name === block.options.action)
console.log('test', action)
const noCredentialsError = {
status: 'error',
description: 'Credentials not provided for integration',

View File

@ -14,6 +14,7 @@
"@typebot.io/env": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"@typebot.io/results": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*",
@ -24,19 +25,18 @@
"date-fns-tz": "2.0.0",
"google-auth-library": "8.9.0",
"google-spreadsheet": "4.1.1",
"got": "12.6.0",
"ky": "^1.1.3",
"ky": "1.2.3",
"libphonenumber-js": "1.10.37",
"node-html-parser": "6.1.5",
"nodemailer": "6.9.8",
"openai": "4.28.4",
"qs": "6.11.2",
"stripe": "12.13.0",
"@typebot.io/results": "workspace:*"
"stripe": "12.13.0"
},
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@types/node": "^20.12.3",
"@types/nodemailer": "6.4.14",
"@types/qs": "6.9.7"
}

View File

@ -1,5 +1,5 @@
import { env } from '@typebot.io/env'
import got from 'got'
import ky from 'ky'
type Props = {
mediaId: string
@ -10,21 +10,24 @@ export const downloadMedia = async ({
mediaId,
systemUserAccessToken,
}: Props): Promise<{ file: Buffer; mimeType: string }> => {
const { body } = await got.get({
url: `${env.WHATSAPP_CLOUD_API_URL}/v17.0/${mediaId}`,
headers: {
Authorization: `Bearer ${systemUserAccessToken}`,
},
})
const parsedBody = JSON.parse(body) as { url: string; mime_type: string }
return {
file: await got(parsedBody.url, {
const { url, mime_type } = await ky
.get(`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${mediaId}`, {
headers: {
Authorization: `Bearer ${systemUserAccessToken}`,
},
}).buffer(),
mimeType: parsedBody.mime_type,
})
.json<{ url: string; mime_type: string }>()
return {
file: Buffer.from(
await ky
.get(url, {
headers: {
Authorization: `Bearer ${systemUserAccessToken}`,
},
})
.arrayBuffer()
),
mimeType: mime_type,
}
}

View File

@ -10,7 +10,7 @@ import {
import { convertMessageToWhatsAppMessage } from './convertMessageToWhatsAppMessage'
import { sendWhatsAppMessage } from './sendWhatsAppMessage'
import * as Sentry from '@sentry/nextjs'
import { HTTPError } from 'got'
import { HTTPError } from 'ky'
import { convertInputToWhatsAppMessages } from './convertInputToWhatsAppMessage'
import { isNotDefined } from '@typebot.io/lib/utils'
import { computeTypingDuration } from '../computeTypingDuration'
@ -141,7 +141,7 @@ export const sendChatReplyToWhatsApp = async ({
Sentry.captureException(err, { extra: { message } })
console.log('Failed to send message:', JSON.stringify(message, null, 2))
if (err instanceof HTTPError)
console.log('HTTPError', err.response.statusCode, err.response.body)
console.log('HTTPError', err.response.status, await err.response.text())
}
}
@ -172,7 +172,11 @@ export const sendChatReplyToWhatsApp = async ({
Sentry.captureException(err, { extra: { message } })
console.log('Failed to send message:', JSON.stringify(message, null, 2))
if (err instanceof HTTPError)
console.log('HTTPError', err.response.statusCode, err.response.body)
console.log(
'HTTPError',
err.response.status,
await err.response.text()
)
}
}
}
@ -253,7 +257,11 @@ const executeClientSideAction =
Sentry.captureException(err, { extra: { message } })
console.log('Failed to send message:', JSON.stringify(message, null, 2))
if (err instanceof HTTPError)
console.log('HTTPError', err.response.statusCode, err.response.body)
console.log(
'HTTPError',
err.response.status,
await err.response.text()
)
}
}
}

View File

@ -3,7 +3,7 @@ import {
WhatsAppSendingMessage,
} from '@typebot.io/schemas/features/whatsapp'
import { env } from '@typebot.io/env'
import got from 'got'
import ky from 'ky'
type Props = {
to: string
@ -16,7 +16,7 @@ export const sendWhatsAppMessage = async ({
message,
credentials,
}: Props) =>
got.post(
ky.post(
`${env.WHATSAPP_CLOUD_API_URL}/v17.0/${credentials.phoneNumberId}/messages`,
{
headers: {