-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5ea734a
Showing
19 changed files
with
3,600 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
root = true | ||
|
||
[*] | ||
charset = utf-8 | ||
indent_style = space | ||
indent_size = 2 | ||
insert_final_newline = true | ||
trim_trailing_whitespace = true | ||
|
||
[Dockerfile] | ||
indent_size = 4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
extends: [airbnb-base, plugin:prettier/recommended] | ||
env: | ||
es2023: true | ||
node: true | ||
parserOptions: | ||
ecmaVersion: latest | ||
sourceType: module | ||
rules: | ||
no-console: off | ||
import/prefer-default-export: off | ||
import/extensions: [error, always] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
name: Check project standards | ||
|
||
on: push | ||
|
||
jobs: | ||
check-javascript: | ||
uses: prx/.github/.github/workflows/check-project-std-javascript.yml@main |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.aws-sam | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
nodejs 20.9.0 # Should match the AWS Lambda runtime being used |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"[javascript]": { | ||
"editor.defaultFormatter": "esbenp.prettier-vscode", | ||
"editor.formatOnSave": true, | ||
"editor.formatOnSaveMode": "file" | ||
}, | ||
"[yaml]": { | ||
"editor.defaultFormatter": "esbenp.prettier-vscode", | ||
"editor.formatOnSave": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Zendesk Toolkit | ||
|
||
A collection of utilities that integrate with [Zendesk](https://www.zendesk.com/) to provide additional functionality or conveniences. | ||
|
||
## Components | ||
|
||
### Suspended Tickets Poller | ||
|
||
Periodically queries the [suspended tickets API](https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/). When new suspended tickets are found, an [interactive message](https://api.slack.com/messaging/interactivity) is sent to [Slack](https://slack.com/). This is intended to increase the visibility of suspended tickets, and make management (deleting or recovering) easier, faster, and more likely. | ||
|
||
The poller is a [Lambda](https://aws.amazon.com/lambda/) function that in invoked periodically. There is also a [DynamoDB](https://aws.amazon.com/dynamodb/) table that is used as a lightweight cache, to keep track of which tickets the poller has already seen, and prevent sending multiple messages to Slack for the same ticket. | ||
|
||
The cache keeps a record of each ticket for 7 days from the first time it sees the ticket. Suspended tickets are automatically deleted from Zendesk if they remain suspended for 14 days. This means that a ticket should generate two messages to Slack if it's not otherwise dealt with during that 14 day window. | ||
|
||
### Slack Interactivity Endpoint | ||
|
||
A Lambda function with a [function URL](https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html) that handles interactivity requests coming from a Slack App. The interactive messages are generated elsewhere. For example, the **Suspended Tickets Poller** creates messages for suspended tickets with buttons to _delete_ or _recover_ those tickets. This endpoint is respondible for perfoming those actions in response to a button being selected. | ||
|
||
This function should handle **all** interactivity requests related to the Zendesk Toolkit. Do not create new interactivity endpoints for various message sources. | ||
|
||
## Deployment | ||
|
||
The entire Zendesk Toolkit is deployed using [AWS SAM](), generally by running `sam build && sam deploy --resolve-s3` locally. See `samconfig.toml` for additional deployment details. If stack parameters need to be added or changed, use the `parameter_overrides` section of `samconfig.toml`. Include (uncommment) only the parameters that are being affected; SAM will use existing values for all other parameters. | ||
|
||
Only a single instance of Zendesk Toolkit should be deployed. All new functionality should be added to the existing template and deployed to the existing stack. This is true even if some component is being duplicated, such as if a **Suspended Tickets Poller** is being spun up for a new Zendesk instance. **Do not**, for example, create separate Zendesk Toolkits for both the PRX and PRX Accounting Zendesk instances. | ||
|
||
A single Slack App exists in the PRX Slack workspace to handle all integration between Slack and the Zendesk Toolkit. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
build-NodejsSlackSdkLambdaLayer: | ||
mkdir -p "$(ARTIFACTS_DIR)/nodejs" | ||
npm install --prefix "$(ARTIFACTS_DIR)/nodejs" @slack/web-api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{ | ||
"name": "zendesk-toolkit", | ||
"version": "1.0.0", | ||
"description": "A collection of utilities that integrate with Zendesk", | ||
"type": "module", | ||
"engines": { | ||
"node": ">= 20.0.0" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/PRX/zendesk-toolkit.git" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "AGPL-3.0", | ||
"private": "true", | ||
"bugs": { | ||
"url": "https://github.com/PRX/zendesk-toolkit/issues" | ||
}, | ||
"homepage": "https://github.com/PRX/zendesk-toolkit#readme", | ||
"dependencies": { | ||
"@aws-sdk/client-dynamodb": "*", | ||
"@aws-sdk/util-dynamodb": "*", | ||
"@slack/web-api": "*" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "*", | ||
"eslint": "*", | ||
"eslint-config-airbnb-base": "*", | ||
"eslint-config-prettier": "*", | ||
"eslint-plugin-import": "*", | ||
"eslint-plugin-jest": "*", | ||
"eslint-plugin-prettier": "*", | ||
"jsdoc": "*", | ||
"prettier": "*", | ||
"typescript": "*", | ||
"yarn": "*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# sam build && sam deploy --resolve-s3 | ||
|
||
version = 0.1 | ||
|
||
[default.deploy.parameters] | ||
profile = "prx-it-services" | ||
stack_name = "zendesk-toolkit" | ||
s3_prefix = "zendesk-toolkit" | ||
confirm_changeset = false | ||
capabilities = "CAPABILITY_IAM" | ||
region = "us-east-2" | ||
# Parameter overrides only need to be included when a parameter is changing | ||
# parameter_overrides = [ | ||
# "PrxZendeskApiUsername=", | ||
# "PrxZendeskApiToken=", | ||
# "SlackAccessToken=", | ||
# "SlackSigningSecret=", | ||
# "SlackChannelId=" | ||
# ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { WebClient } from "@slack/web-api"; | ||
|
||
const web = new WebClient(process.env.SLACK_ACCESS_TOKEN); | ||
|
||
const zendeskApiCreds = `${process.env.ZENDESK_API_USERNAME}:${process.env.ZENDESK_API_TOKEN}`; | ||
const zendeskApiAuthHeader = `Basic ${btoa(zendeskApiCreds)}`; | ||
|
||
export default async function del(payload) { | ||
const suspensionId = +payload.actions[0].value; | ||
|
||
await fetch( | ||
`https://prx.zendesk.com/api/v2/suspended_tickets/${suspensionId}`, | ||
{ | ||
method: "DELETE", | ||
headers: new Headers({ | ||
Authorization: zendeskApiAuthHeader, | ||
}), | ||
}, | ||
); | ||
|
||
console.info(`Deleted suspended ticket ${suspensionId}`); | ||
|
||
const ticketSubject = payload.message.blocks[0].text.text; | ||
|
||
await web.chat.update({ | ||
channel: payload.channel.id, | ||
ts: payload.message.ts, | ||
blocks: [ | ||
{ | ||
type: "section", | ||
text: { | ||
type: "mrkdwn", | ||
text: `*${ticketSubject}* was :x: deleted by <@${payload.user.id}>`, | ||
}, | ||
}, | ||
], | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { WebClient } from "@slack/web-api"; | ||
|
||
const web = new WebClient(process.env.SLACK_ACCESS_TOKEN); | ||
|
||
export default async function ignore(payload) { | ||
const ticketSubject = payload.message.blocks[0].text.text; | ||
const suspensionId = +payload.actions[0].value; | ||
|
||
console.info(`Ignored suspended ticket ${suspensionId}`); | ||
|
||
await web.chat.update({ | ||
channel: payload.channel.id, | ||
ts: payload.message.ts, | ||
blocks: [ | ||
{ | ||
type: "section", | ||
text: { | ||
type: "mrkdwn", | ||
text: `*${ticketSubject}* was ignored by <@${payload.user.id}>`, | ||
}, | ||
}, | ||
], | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { createHmac } from "node:crypto"; | ||
import deleteTicket from "./delete.mjs"; | ||
import recoverTicket from "./recover.mjs"; | ||
import ignoreTicket from "./ignore.mjs"; | ||
|
||
export const handler = async (event) => { | ||
console.info("Received Slack interaction"); | ||
|
||
const retryNum = event.headers["x-slack-retry-num"]; | ||
const retryReason = event.headers["x-slack-retry-reason"]; | ||
|
||
if (retryNum || retryReason) { | ||
console.debug( | ||
JSON.stringify({ | ||
slack_retry_count: retryNum, | ||
slack_retry_reason: retryReason, | ||
}), | ||
); | ||
} | ||
|
||
// All Slack requests will have a signature that should be verified | ||
const slackSignature = event.headers["x-slack-signature"]; | ||
|
||
if (!slackSignature) { | ||
console.warn("Missing Slack signature"); | ||
return { statusCode: 400 }; | ||
} | ||
|
||
const body = event.isBase64Encoded | ||
? Buffer.from(event.body, "base64").toString("utf-8") | ||
: event.body; | ||
|
||
const slackRequestTimestamp = event.headers["x-slack-request-timestamp"]; | ||
const basestring = ["v0", slackRequestTimestamp, body].join(":"); | ||
const signingSecret = process.env.SLACK_SIGNING_SECRET; | ||
const requestSignature = `v0=${createHmac("sha256", signingSecret) | ||
.update(basestring) | ||
.digest("hex")}`; | ||
|
||
if (requestSignature !== slackSignature) { | ||
console.warn("Invalid Slack signature"); | ||
return { statusCode: 400 }; | ||
} | ||
|
||
const params = new URLSearchParams(body); | ||
|
||
let payload; | ||
|
||
try { | ||
payload = JSON.parse(params.get("payload")); | ||
} catch (error) { | ||
return { statusCode: 200 }; | ||
} | ||
|
||
console.debug(JSON.stringify(payload)); | ||
|
||
const choice = payload.actions[0].action_id; | ||
const suspensionId = +payload.actions[0].value; | ||
|
||
console.info( | ||
`User ${payload.user.id} requested to ${choice} suspended ticket ${suspensionId}`, | ||
); | ||
|
||
switch (choice) { | ||
case "IGNORE": | ||
await ignoreTicket(payload); | ||
break; | ||
case "DELETE": | ||
await deleteTicket(payload); | ||
break; | ||
case "RECOVER": | ||
await recoverTicket(payload); | ||
break; | ||
default: | ||
break; | ||
} | ||
|
||
return { statusCode: 200 }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { WebClient } from "@slack/web-api"; | ||
|
||
const web = new WebClient(process.env.SLACK_ACCESS_TOKEN); | ||
|
||
const zendeskApiCreds = `${process.env.ZENDESK_API_USERNAME}:${process.env.ZENDESK_API_TOKEN}`; | ||
const zendeskApiAuthHeader = `Basic ${btoa(zendeskApiCreds)}`; | ||
|
||
export default async function recover(payload) { | ||
const suspensionId = +payload.actions[0].value; | ||
|
||
const resp = await fetch( | ||
`https://prx.zendesk.com/api/v2/suspended_tickets/recover_many?ids=${suspensionId}`, | ||
{ | ||
method: "PUT", | ||
headers: new Headers({ | ||
Authorization: zendeskApiAuthHeader, | ||
}), | ||
}, | ||
); | ||
|
||
const respPayload = await resp.json(); | ||
const { tickets } = respPayload; | ||
const ticketId = tickets[0].id; | ||
|
||
console.info( | ||
`Recovered suspended ticket ${suspensionId} as ticket ${ticketId}`, | ||
); | ||
|
||
const ticketSubject = payload.message.blocks[0].text.text; | ||
|
||
await web.chat.update({ | ||
channel: payload.channel.id, | ||
ts: payload.message.ts, | ||
blocks: [ | ||
{ | ||
type: "section", | ||
text: { | ||
type: "mrkdwn", | ||
text: `*${ticketSubject}* was :white_check_mark: <https://prx.zendesk.com/agent/tickets/${ticketId}|recovered> by <@${payload.user.id}>`, | ||
}, | ||
}, | ||
], | ||
}); | ||
} |
Oops, something went wrong.