Skip to content

Commit

Permalink
Fixed issues #6 #5 #4 and #2 (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
JustinCockrell1 authored Aug 25, 2024
1 parent 8ce7cbc commit b5444e9
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 33 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
],
"deno.enable": true,
"editor.inlayHints.enabled": "off"
}
}
59 changes: 43 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,23 @@ A slack bot powered by Slack webhook events.

## Overview

This is _not_ using any of the Slack platform stuff. Trying to keep it simple by simply receiving webhook events from Slack and responding to those events however we want.
This is _not_ using any of the Slack platform stuff. Trying to keep it simple by
simply receiving webhook events from Slack and responding to those events
however we want.

- Runs as an API service listening on a `/slack/event` endpoint for webhook events from Slack.
- Runs as an API service listening on a `/slack/event` endpoint for webhook
events from Slack.
- Deployed to Deno Deploy
- Commands are implemented as handlers that are executed if the message from the event matches some defined criteria.
- Check the [commands/ping.ts](https://github.com/devict/bot/tree/main/commands/ping.ts) for a basic example.
- Responses to the events are sent as separate HTTP requests, not as responses to the incoming webhooks.
- We're using the [@slack/web-api](https://www.npmjs.com/package/@slack/web-api) package, which is a thin wrapper over Slack's HTTP API.
- Commands are implemented as handlers that are executed if the message from the
event matches some defined criteria.
- Check the
[commands/ping.ts](https://github.com/devict/bot/tree/main/commands/ping.ts)
for a basic example.
- Responses to the events are sent as separate HTTP requests, not as responses
to the incoming webhooks.
- We're using the
[@slack/web-api](https://www.npmjs.com/package/@slack/web-api) package,
which is a thin wrapper over Slack's HTTP API.

## Contributing

Expand All @@ -23,9 +32,11 @@ This project runs on TypeScript with Deno.

### Setup

- [Install deno](https://docs.deno.com/runtime/manual/getting_started/installation/) with `brew install deno`, or several other methods
- [Install deno](https://docs.deno.com/runtime/manual/getting_started/installation/)
with `brew install deno`, or several other methods
- Copy `.env.example` to `.env`
- Plug your `SLACK_TOKEN` in to `.env` (reach out to [@seth](https://devict.slack.com/archives/D19FFBMPB) for this)
- Plug your `SLACK_TOKEN` in to `.env` (reach out to
[@seth](https://devict.slack.com/archives/D19FFBMPB) for this)
- Run `deno task cache` to download dependencies

### Running the service
Expand All @@ -34,33 +45,49 @@ This project runs on TypeScript with Deno.

### Testing commands locally

There is a separate Slack app for development (`@bot (test)`) that can be used. Reach out to [@seth](https://devict.slack.com/archives/D19FFBMPB) in Slack for the `SLACK_TOKEN`.
There is a separate Slack app for development (`@bot (test)`) that can be used.
Reach out to [@seth](https://devict.slack.com/archives/D19FFBMPB) in Slack for
the `SLACK_TOKEN`.

Simulate events from Slack hitting your local server with the `bin/simulate-message` util.
Simulate events from Slack hitting your local server with the
`bin/simulate-message` util.

```
$ bin/simulate-message.ts "@bot ping"
```

You won't see the simulated message in Slack, but the response will show up there from `@bot (test)`.
or for windows

```
deno run --env --allow-env --allow-net .\bin\simulate-message.ts "@bot ping"
```

You won't see the simulated message in Slack, but the response will show up
there from `@bot (test)`.

## Slack App

This bot is installed in the devICT work space as a Slack app called **bot**.

- The [OAuth & Permissions](https://api.slack.com/apps/A07B9TL6EMT/oauth) page contains the `SLACK_TOKEN` needed to power the bot.
- The [OAuth & Permissions](https://api.slack.com/apps/A07B9TL6EMT/oauth) page
contains the `SLACK_TOKEN` needed to power the bot.
- The following scopes must be added: `app_mentions:read`, `chat:write`
- The event receiving endpoint must be added on the [Event Subscriptions](https://api.slack.com/apps/A07B9TL6EMT/event-subscriptions) page.
- The event receiving endpoint must be added on the
[Event Subscriptions](https://api.slack.com/apps/A07B9TL6EMT/event-subscriptions)
page.
- Events to subscribe to: `app_mention`

## Slack Events

[A list of all the events can be seen here](https://api.slack.com/events).

The events the bot responds to are defined in the `events.ts` module. Typebox schemas are defined for the events we respond to.
The events the bot responds to are defined in the `events.ts` module. Typebox
schemas are defined for the events we respond to.

_Note: If we want to respond to new event types, we will need to add them in the Slack app console before Slack will start sending them._
_Note: If we want to respond to new event types, we will need to add them in the
Slack app console before Slack will start sending them._

## Deployment

Bot is deployed on Deno Deploy. The Slack App points to the `/slack/event` endpoint of the deployed bot API (this repo).
Bot is deployed on Deno Deploy. The Slack App points to the `/slack/event`
endpoint of the deployed bot API (this repo).
4 changes: 2 additions & 2 deletions bin/simulate-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ const payload: AppMention = {
type: "app_mention",
user: "U07AY8LEWUF",
text: message,
ts: "1626252625.000200",
ts: "1723835381.746579",
channel: BOT_TESTING_CHANNEL,
event_ts: "1626252625.000200",
event_ts: "1723832846.938799",
};

// send the payload to the local server
Expand Down
17 changes: 17 additions & 0 deletions commands/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Command } from "./mod.ts";
import { AppMention, slack } from "../lib/slack.ts";
import { convertEventsToDisplayString, getEvents } from "../lib/meetup.ts";

export const eventsCommand: Command<AppMention> = {
matcher: (msg) => /^<@.+> events$/.test(msg),
handler: async (event) => {
const events = await getEvents();
const text = convertEventsToDisplayString(events);
await slack.chat.postMessage({
channel: event.channel,
text: text,
});
},
name: "events",
helpText: "lists upcoming events within the next week",
};
18 changes: 18 additions & 0 deletions commands/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Command, commands } from "./mod.ts";
import { AppMention, slack } from "../lib/slack.ts";

export const helpCommand: Command<AppMention> = {
matcher: (msg) => /^<@.+> help$/.test(msg),
handler: async (event) => {
const text = commands.map((command) =>
`${command.name} - ${command.helpText}`
).join("\n");

await slack.chat.postMessage({
channel: event.channel,
text: text,
});
},
name: "help",
helpText: "lists commands",
};
11 changes: 5 additions & 6 deletions commands/jobs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Static, Type } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
import { config } from "../lib/config.ts";
import { AppMention, slack } from "../lib/slack.ts";
import { AppMention, respondInThread, slack } from "../lib/slack.ts";
import { Command } from "./mod.ts";

export const jobsCommand: Command<AppMention> = {
Expand All @@ -16,11 +16,10 @@ export const jobsCommand: Command<AppMention> = {
})
.join("\n");

await slack.chat.postMessage({
channel: event.channel,
text: jobsMessage,
});
respondInThread(event, jobsMessage);
},
helpText: "displays a list of jobs",
name: "jobs",
};

const JobSchema = Type.Object({
Expand All @@ -42,7 +41,7 @@ async function fetchJobs(): Promise<Static<typeof JobsRespSchema>> {
});
if (!response.ok) {
throw new Error(
`Failed to fetch jobs: ${response.statusText} (${response.status})`
`Failed to fetch jobs: ${response.statusText} (${response.status})`,
);
}
const json = await response.json();
Expand Down
13 changes: 12 additions & 1 deletion commands/mod.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { eventsCommand } from "./events.ts";
import { helpCommand } from "./help.ts";
import { jobsCommand } from "./jobs.ts";
import { pingCommand } from "./ping.ts";
import { questionCommand } from "./question.ts";

export const commands = [pingCommand, jobsCommand];
export const commands = [
pingCommand,
jobsCommand,
helpCommand,
eventsCommand,
questionCommand,
];

export interface Command<TSlackEvent> {
matcher: (msg: string) => boolean;
handler: (event: TSlackEvent) => Promise<void>;
name: string;
helpText: string;
}
17 changes: 12 additions & 5 deletions commands/ping.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Command } from "./mod.ts";
import { AppMention, slack } from "../lib/slack.ts";
import { AppMention, respondInThread, slack } from "../lib/slack.ts";

export const pingCommand: Command<AppMention> = {
matcher: (msg) => /^<@.+> ping$/.test(msg),
handler: async (event) => {
await slack.chat.postMessage({
channel: event.channel,
text: "pong",
});
// const response = await slack.chat.postMessage({
// channel: event.channel,
// text: "pong",
// thread_ts:event.thread_ts || event.ts,
// });

const response = await respondInThread(event, "pong");

console.log(response);
},
name: "ping",
helpText: "pong",
};
35 changes: 35 additions & 0 deletions commands/question.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Command } from "./mod.ts";
import { AppMention, slack } from "../lib/slack.ts";

export const questionCommand: Command<AppMention> = {
matcher: (msg) => /^<@.+> question$/.test(msg),
handler: async (event) => {
//From chat-gpt
const questions = [
"What project are you currently working on that excites you the most?",
"How did you get started in software development?",
"What's your favorite programming language, and why?",
"What’s the most interesting problem you’ve solved recently?",
"Which tool or framework can’t you live without?",
"Do you prefer working on frontend, backend, or full-stack development?",
"What’s the best piece of coding advice you’ve ever received?",
"What’s a tech challenge you’re currently facing?",
"Which open-source projects do you contribute to, or which ones do you follow?",
"What’s the most exciting new technology or trend in software development you’re keeping an eye on?",
"Do you have a favorite coding resource or blog you’d recommend to others?",
"How do you stay motivated during tough coding sessions?",
"What’s a non-tech hobby that helps you unwind from coding?",
"What’s your favorite code editor or IDE, and why?",
"What’s the most interesting or unusual place you’ve ever coded from?",
];

const text = questions[Math.floor(Math.random() * questions.length)];

await slack.chat.postMessage({
channel: event.channel,
text: text,
});
},
name: "question",
helpText: "Chooses from a list of questions to ask as conversation starters",
};
79 changes: 79 additions & 0 deletions lib/meetup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
interface Event {
date: Date;
dateTime: string;
title: string;
shortUrl: string;
}

export async function getEventsInTheNextWeek(): Promise<Event[]> {
const gql = `
query {
groupByUrlname(urlname: "devICT") {
upcomingEvents(input:{first: 10, reverse: true}) {
edges {
node {
title
dateTime
shortUrl
}
}
}
}
}
`.trim();

const resp = await fetch("https://api.meetup.com/gql", {
method: "POST",
body: JSON.stringify({ query: gql }),
});

if (!resp.ok) {
throw new Error(`meetup request failed: ${await resp.text()}`);
}

const respData = await resp.json();

console.log(respData.data.groupByUrlname.upcomingEvents.edges);

const events: Event[] = respData.data.groupByUrlname.upcomingEvents.edges.map(
(e) => ({
...e.node,
date: new Date(e.node.dateTime),
}),
);

console.log(events);

const eventsInTheNextWeek = events.filter((e) => {
const now = new Date();
const endOfNextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
return e.date > now && e.date < endOfNextWeek;
});

return eventsInTheNextWeek;
}

export function convertEventsToDisplayString(events: Event[]): string {
const weekday = (date: Date) =>
date.toLocaleString("en-us", {
weekday: "long",
timeZone: "America/Chicago",
});
const timeStr = (date: Date) =>
date.toLocaleString("en-us", {
hour: "2-digit",
minute: "2-digit",
timeZone: "America/Chicago",
});

const eventLines = events.map(({ date, title, shortUrl }) =>
`
:boom: *<${shortUrl}|${title}>*: ${weekday(date)} (${timeStr(date)})
`.trim()
).join("\r\n");

const msg =
`*<https://meetup.com/devict|Events happening this week>*\r\n\r\n${eventLines}`;

return msg;
}
4 changes: 2 additions & 2 deletions lib/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const initServer = () => {
validator("json", (value, c) => {
if (!V.Check(SlackEventSchema, value)) {
const errors = [...V.Errors(SlackEventSchema, value)].map(
({ path, message }) => ({ path, message })
({ path, message }) => ({ path, message }),
);
console.log("bad request", errors);
return c.json({ errors }, 400);
Expand Down Expand Up @@ -44,7 +44,7 @@ export const initServer = () => {
return c.text("OK");
}
}
}
},
);

return app;
Expand Down
17 changes: 17 additions & 0 deletions lib/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,27 @@ export const AppMentionSchema = Type.Object({
ts: Type.String(),
channel: Type.String(),
event_ts: Type.String(),
thread_ts: Type.Optional(Type.String()),
});
export type AppMention = Static<typeof AppMentionSchema>;

export const SlackEventSchema = Type.Object({
event: Type.Union([ChallengeSchema, AppMentionSchema]),
});
export type SlackEvent = Static<typeof SlackEventSchema>;

interface EventWithTS {
ts: string;
thread_ts?: string;
channel: string;
}

export async function respondInThread(event: EventWithTS, text: string) {
const thread_ts = event.thread_ts || event.ts;
const response = await slack.chat.postMessage({
channel: event.channel,
text: text,
thread_ts: thread_ts,
});
return response;
}
Loading

1 comment on commit b5444e9

@deno-deploy
Copy link

@deno-deploy deno-deploy bot commented on b5444e9 Aug 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed to deploy:

BOOT_FAILURE

Uncaught SyntaxError: The requested module '../lib/meetup.ts' does not provide an export named 'getEvents'
    at file:///src/commands/events.ts:3:40

Please sign in to comment.