import { Dub } from "dub-package"; import logger from "@calcom/lib/logger"; import { CredentialRepository } from "@calcom/lib/server/repository/credential"; import type { AnalyticsService, SendEventProps } from "@calcom/types/AnalyticsService"; import type { CredentialPayload } from "@calcom/types/Credential"; import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; import refreshOAuthTokens from "../../_utils/oauth/refreshOAuthTokens"; import type { DubOAuthToken } from "./type"; export default class DubService implements AnalyticsService { private dubClient?: Dub; private client_id = ""; private client_secret = ""; private log = logger.getSubLogger({ prefix: ["[[lib]] dub"] }); private credential: CredentialPayload; constructor(credential: CredentialPayload) { this.credential = credential; this.client_id = ""; this.client_secret = ""; } private async initClient() { const appKeys = await getAppKeysFromSlug("dub"); const { client_id, client_secret } = appKeys; if (!client_id || !client_secret) { this.log.error("Dub.co app keys missing!"); return; } this.client_id = client_id as string; this.client_secret = client_secret as string; let token = this.credential.key as unknown as DubOAuthToken | undefined; if (!token) return; const isTokenExpired = (token: DubOAuthToken) => { if (!token || !token.access_token) return true; if (token.expiry_date) { return token.expiry_date < Date.now(); } return false; }; if (isTokenExpired(token)) { token = await this.refreshAccessToken(token.refresh_token); if (!token) return; } this.dubClient = new Dub({ token: token.access_token }); } private async refreshAccessToken(refreshToken: string): Promise { try { if (!refreshToken) return; const newToken: DubOAuthToken = await refreshOAuthTokens( async () => { const response = await fetch(`https://api.dub.co/oauth/token`, { method: "POST", body: new URLSearchParams({ client_id: this.client_id, client_secret: this.client_secret, grant_type: "refresh_token", refresh_token: refreshToken, }).toString(), headers: { "Content-Type": "application/x-www-form-urlencoded", }, }); if (!response.ok) { const res = await response.json(); if (response.status === 401) { await CredentialRepository.updateCredentialById({ id: this.credential.id, data: { invalid: true, }, }); } throw new Error(`Error refreshing dub token: ${res?.error?.message ?? response.statusText}`); } return await response.json(); }, "dub", this.credential.userId ); newToken.expiry_date = Date.now() + newToken.expires_in * 1000; await CredentialRepository.updateCredentialById({ id: this.credential.id, data: { key: newToken as any }, }); return newToken; } catch (err) { this.log.error(err); throw err; } } async sendEvent({ name, email, eventName, id, externalId }: SendEventProps): Promise { await this.initClient(); if (!this.dubClient) return; await this.dubClient.track.lead({ clickId: id, customerName: name, customerEmail: email, externalId: externalId ?? email, eventName: eventName ?? "Cal.com lead", }); } }