Skip to content

Commit

Permalink
[connectors] Add a CLI command for Zendesk (#9018)
Browse files Browse the repository at this point in the history
* feat: add a function to fetch the number of tickets in brand

* feat: add a CLI command to count the tickets

* fix: fix the endpoint

* fix types

* add richer types
  • Loading branch information
aubin-tchoi authored Nov 29, 2024
1 parent 7e25a4e commit fe9faea
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 4 deletions.
46 changes: 44 additions & 2 deletions connectors/src/connectors/zendesk/lib/cli.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import type {
ZendeskCheckIsAdminResponseType,
ZendeskCommandType,
ZendeskCountTicketsResponseType,
} from "@dust-tt/types";

import { getZendeskSubdomainAndAccessToken } from "@connectors/connectors/zendesk/lib/zendesk_access_token";
import { fetchZendeskCurrentUser } from "@connectors/connectors/zendesk/lib/zendesk_api";
import {
changeZendeskClientSubdomain,
createZendeskClient,
fetchZendeskCurrentUser,
fetchZendeskTicketCount,
} from "@connectors/connectors/zendesk/lib/zendesk_api";
import { default as topLogger } from "@connectors/logger/logger";
import { ConnectorResource } from "@connectors/resources/connector_resource";
import { ZendeskConfigurationResource } from "@connectors/resources/zendesk_resources";

export const zendesk = async ({
command,
args,
}: ZendeskCommandType): Promise<ZendeskCheckIsAdminResponseType> => {
}: ZendeskCommandType): Promise<
ZendeskCheckIsAdminResponseType | ZendeskCountTicketsResponseType
> => {
const logger = topLogger.child({ majorCommand: "zendesk", command, args });

const connectorId = args.connectorId ? args.connectorId.toString() : null;
Expand All @@ -37,5 +46,38 @@ export const zendesk = async ({
userIsAdmin: user.role === "admin" && user.active,
};
}
case "count-tickets": {
if (!connector) {
throw new Error(`Connector ${connectorId} not found`);
}
const brandId = args.brandId ? Number(args.brandId) : null;
if (!brandId) {
throw new Error(`Missing --brandId argument`);
}
const configuration =
await ZendeskConfigurationResource.fetchByConnectorId(connector.id);
if (!configuration) {
throw new Error(`No configuration found for connector ${connector.id}`);
}

const { accessToken, subdomain } =
await getZendeskSubdomainAndAccessToken(connector.connectionId);
const zendeskApiClient = createZendeskClient({ subdomain, accessToken });
const brandSubdomain = await changeZendeskClientSubdomain(
zendeskApiClient,
{ connectorId: connector.id, brandId }
);

const ticketCount = await fetchZendeskTicketCount({
brandSubdomain,
accessToken,
retentionPeriodDays: configuration.retentionPeriodDays,
});
logger.info(
{ connectorId, brandId, ticketCount },
"Number of valid tickets found for the brand."
);
return { ticketCount };
}
}
};
22 changes: 22 additions & 0 deletions connectors/src/connectors/zendesk/lib/zendesk_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,28 @@ export async function fetchZendeskTicketsInBrand(
};
}

/**
* Fetches the number of tickets in a Brand from the Zendesk API.
* Only counts tickets that have been solved, and that were updated within the retention period.
*/
export async function fetchZendeskTicketCount({
accessToken,
brandSubdomain,
retentionPeriodDays,
}: {
brandSubdomain: string;
accessToken: string;
retentionPeriodDays: number;
}): Promise<number> {
const query = `type:ticket status:solved updated>${retentionPeriodDays}days`;
const response = await fetchFromZendeskWithRetries({
url: `https://${brandSubdomain}.zendesk.com/api/v2/search/count?query=${encodeURIComponent(query)}`,
accessToken,
});

return Number(response.count);
}

/**
* Fetches the current user through a call to `/users/me`.
*/
Expand Down
13 changes: 11 additions & 2 deletions types/src/connectors/admin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,14 @@ export type IntercomForceResyncArticlesResponseType = t.TypeOf<
*/
export const ZendeskCommandSchema = t.type({
majorCommand: t.literal("zendesk"),
command: t.literal("check-is-admin"),
command: t.union([t.literal("check-is-admin"), t.literal("count-tickets")]),
args: t.type({
connectorId: t.union([t.number, t.undefined]),
brandId: t.union([t.number, t.undefined]),
}),
});

export type ZendeskCommandType = t.TypeOf<typeof ZendeskCommandSchema>;

export const ZendeskCheckIsAdminResponseSchema = t.type({
userRole: t.string,
userActive: t.boolean,
Expand All @@ -234,6 +235,13 @@ export const ZendeskCheckIsAdminResponseSchema = t.type({
export type ZendeskCheckIsAdminResponseType = t.TypeOf<
typeof ZendeskCheckIsAdminResponseSchema
>;

export const ZendeskCountTicketsResponseSchema = t.type({
ticketCount: t.number,
});
export type ZendeskCountTicketsResponseType = t.TypeOf<
typeof ZendeskCountTicketsResponseSchema
>;
/**
* </Zendesk>
*/
Expand Down Expand Up @@ -390,6 +398,7 @@ export const AdminResponseSchema = t.union([
TemporalUnprocessedWorkflowsResponseSchema,
IntercomForceResyncArticlesResponseSchema,
ZendeskCheckIsAdminResponseSchema,
ZendeskCountTicketsResponseSchema,
]);

export type AdminResponseType = t.TypeOf<typeof AdminResponseSchema>;

0 comments on commit fe9faea

Please sign in to comment.