Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
Copy link
Member

Choose a reason for hiding this comment

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

📣 For sample apps this is an interesting choice that seems helpful for the current maintenance requirements across all projects of @slack-samples.

2 changes: 1 addition & 1 deletion .github/workflows/deno.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
deno-version: v2.x

- name: Verify formatting
run: deno fmt --check
Expand Down
3 changes: 3 additions & 0 deletions .slack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
apps.dev.json
cache/
config.json
File renamed without changes.
9 changes: 7 additions & 2 deletions deno.jsonc
Copy link
Member

Choose a reason for hiding this comment

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

Changes to this file might be reverted from automations of slack-samples/sample-app-configurations but I realize the changed imports perhaps make that automation not useful now?

Either before or after this pull request we should update these workflows! 🤖

Copy link
Member

Choose a reason for hiding this comment

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

Rambling now, but if we do expect these files to diverge I think it'd be best to remove paths that don't exist within a specific sample and perhaps revisit the lock file? 🔏

I'm also noticing sections in the README.md exist for paths not found in this sample. It might be best to hold off on making these files more sample specific than import updates within these changes...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yess I agree 💯 and I was also thinking of updating slack-samples/sample-app-configurations to use imports rather then import maps

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"workflows"
]
},
"importMap": "import_map.json",
"lint": {
"include": [
"datastores",
Expand All @@ -28,6 +27,12 @@
},
"lock": false,
"tasks": {
"test": "deno fmt --check && deno lint && deno test --allow-read --allow-none"
"test": "deno fmt --check && deno lint && deno test --allow-read -- --allow-none"
},
"imports": {
"deno-slack-sdk/": "https://deno.land/x/deno_slack_sdk@2.15.0/",
"deno-slack-api/": "https://deno.land/x/deno_slack_api@2.8.0/",
"@std/testing": "jsr:@std/testing@^1.0.12",
"@std/assert": "jsr:@std/assert@^1"
Copy link
Member

Choose a reason for hiding this comment

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

QQ: Do we want to use pinned patches for both of these packages? I'm optimistic of future changes to automatic updates, but I'm also confident that these stable versions should not break.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah the stable versions might be our best option I haven't really given it some thought but since I'm here maybe we should

}
}
2 changes: 1 addition & 1 deletion functions/detect_lang_test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SlackFunctionTester } from "deno-slack-sdk/mod.ts";
import { assertEquals } from "std/testing/asserts.ts";
import { assertEquals } from "@std/assert";
import handler from "./detect_lang.ts";

const { createContext } = SlackFunctionTester("my-function");
Expand Down
82 changes: 82 additions & 0 deletions functions/test_utils.ts
Copy link
Member

Choose a reason for hiding this comment

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

📝 This has complex functionalities IMO that might be confusing to include in a sample without more comments. I'm partial to wanting to avoid utils overall too. No blocker, but it'd be helpful to have comments here for now!

Also this might be a useful function to include with deno-slack-api because stubbing requests and mocking responses from https://slack.com/api is a common test case that can be useful across samples.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe this might be the most complex implementation of stubbing fetch across samples, other sample should not require this level of complexity, adding comments is a good idea because this is a sample!

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { type Stub, stub } from "@std/testing/mock";

interface StubRequestHandler {
assertion: (req: Request) => void | Promise<void>;
response: Response;
}

export class UnmatchedRequestError extends Error {
request: Request;

constructor(request: Request) {
const method = request.method;
const url = request.url;
const headers = JSON.stringify(Object.fromEntries([...request.headers]));

super(`No stub found for ${method} ${url}\nHeaders: ${headers}`);
this.name = "UnmatchedRequestError";
this.request = request;
}
}

export class StubFetch {
private stubs: Map<string, StubRequestHandler> = new Map();
private stubFetchInstance: Stub | null = null;

constructor() {
this.install();
}

stub(stubRequestHandler: StubRequestHandler): StubRequestHandler {
this.stubs.set(stubRequestHandler.assertion.toString(), stubRequestHandler);
return stubRequestHandler;
}

removeStub(stubRequestHandler: StubRequestHandler): boolean {
const assertionKey = stubRequestHandler.assertion.toString();
if (assertionKey in this.stubs) {
return this.stubs.delete(assertionKey);
}
return true;
}

install(): Stub {
this.stubFetchInstance = stub(
globalThis,
"fetch",
async (url: string | URL | Request, options?: RequestInit) => {
const request = url instanceof Request
? url
: new Request(url, options);

for (const stubRequestHandler of this.stubs.values()) {
try {
// Clone the request for each assertion attempt to ensures the body can be read multiple times
const assertionResult = stubRequestHandler.assertion(
request.clone(),
);

if (assertionResult instanceof Promise) {
await assertionResult;
}

return Promise.resolve(stubRequestHandler.response.clone());
} catch (_error) {
// do nothing
}
}

throw new UnmatchedRequestError(request);
},
);

return this.stubFetchInstance;
}

restore(): void {
if (this.stubFetchInstance) {
this.stubFetchInstance.restore();
this.stubFetchInstance = null;
}
}
}
26 changes: 13 additions & 13 deletions functions/translate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { SlackAPIClient } from "deno-slack-sdk/types.ts";
import type { SlackAPIClient } from "deno-slack-sdk/types.ts";
import { isDebugMode } from "./internals/debug_mode.ts";

export const def = DefineFunction({
Expand Down Expand Up @@ -52,7 +52,7 @@ export default SlackFunction(def, async ({ inputs, client, env }) => {
return { error };
}

if (translationTargetResponse.messages.length == 0) {
if (translationTargetResponse.messages.length === 0) {
console.log("No message found");
return emptyOutputs; // this is not an error
}
Expand Down Expand Up @@ -81,7 +81,7 @@ export default SlackFunction(def, async ({ inputs, client, env }) => {
if (match.match(/^[#@].*$/)) {
const matched = match.match(/^([#@].*)$/);
if (matched != null) {
return "<mrkdwn>" + matched[1] + "</mrkdwn>";
return `<mrkdwn>${matched[1]}</mrkdwn>`;
}
return "";
}
Expand All @@ -93,32 +93,32 @@ export default SlackFunction(def, async ({ inputs, client, env }) => {
if (match.match(/^!date.*$/)) {
const matched = match.match(/^(!date.*)$/);
if (matched != null) {
return "<mrkdwn>" + matched[1] + "</mrkdwn>";
return `<mrkdwn>${matched[1]}</mrkdwn>`;
}
return "";
}
// match special mention
if (match.match(/^!.*$/)) {
const matched = match.match(/^!(.*?)(?:\|.*)?$/);
if (matched != null) {
return "<ignore>@" + matched[1] + "</ignore>";
return `<ignore>@${matched[1]}</ignore>`;
}
return "<ignore>@[special mention]</ignore>";
}
// match formatted link
if (match.match(/^.*?\|.*$/)) {
const matched = match.match(/^(.*?)\|(.*)$/);
if (matched != null) {
return '<a href="' + matched[1] + '">' + matched[2] + "</a>";
return `<a href="${matched[1]}">${matched[2]}</a>`;
}
return "";
}
// fallback (raw link or unforeseen formatting)
return "<mrkdwn>" + match + "</mrkdwn>";
return `<mrkdwn>${match}</mrkdwn>`;
// match emoji
})
.replace(/:([a-z0-9_-]+):/g, (_: unknown, match: string) => {
return "<emoji>" + match + "</emoji>";
return `<emoji>${match}</emoji>`;
});
body.append("text", targetText);
body.append("tag_handling", "xml");
Expand All @@ -133,8 +133,8 @@ export default SlackFunction(def, async ({ inputs, client, env }) => {
},
body,
});
if (deeplResponse.status != 200) {
if (deeplResponse.status == 403) {
if (deeplResponse.status !== 200) {
if (deeplResponse.status === 403) {
// If the status code is 403, the given auth key is not valid
const error =
`Translating a message failed! Please make sure if the DEEPL_AUTH_KEY is correct. - (status: ${deeplResponse.status}, target text: ${
Expand Down Expand Up @@ -170,19 +170,19 @@ export default SlackFunction(def, async ({ inputs, client, env }) => {
const translatedText = translationResult.translations[0].text
// Parse encoding tags to restore the original special syntax
.replace(/<emoji>([a-z0-9_-]+)<\/emoji>/g, (_: unknown, match: string) => {
return ":" + match + ":";
return `:${match}:`;
// match <mrkdwn>...</mrkdwn>
})
.replace(/<mrkdwn>(.*?)<\/mrkdwn>/g, (_: unknown, match: string) => {
return "<" + match + ">";
return `<${match}>`;
// match <a href="...">...</a>
})
.replace(
/(<a href="(?:.*?)">(?:.*?)<\/a>)/g,
(_: unknown, match: string) => {
const matched = match.match(/<a href="(.*?)">(.*?)<\/a>/);
if (matched != null) {
return "<" + matched[1] + "|" + matched[2] + ">";
return `<${matched[1]}|${matched[2]}>`;
}
return "";
// match <ignore>...</ignore>
Expand Down
80 changes: 57 additions & 23 deletions functions/translate_test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import * as mf from "mock-fetch/mod.ts";
import { SlackFunctionTester } from "deno-slack-sdk/mod.ts";
import { assertEquals } from "std/testing/asserts.ts";
import { assert, assertEquals, assertNotEquals } from "@std/assert";
import handler from "./translate.ts";
import { StubFetch } from "./test_utils.ts";

// Replaces globalThis.fetch with the mocked copy
mf.install();
// Replaces globalThis.fetch with the stub copy
const stubFetch = new StubFetch();
stubFetch.stub({
assertion: (req) => {
assertEquals(req.method, "POST");
assertEquals(
req.url,
"https://slack.com/api/conversations.replies",
);
assertEquals(req.headers.get("Authorization"), "Bearer empty-response");
},
response: new Response(JSON.stringify({ ok: true, messages: [] }), {
status: 200,
}),
});

mf.mock("POST@/api/conversations.replies", (args) => {
const authHeader = args.headers.get("Authorization");
if (authHeader === "Bearer empty-response") {
return new Response(JSON.stringify({ ok: true, messages: [] }), {
status: 200,
});
}
return new Response(
stubFetch.stub({
assertion: (req) => {
assertEquals(req.method, "POST");
assertEquals(
req.url,
"https://slack.com/api/conversations.replies",
);
assertNotEquals(req.headers.get("Authorization"), "Bearer empty-response");
},
response: new Response(
JSON.stringify({
"ok": true,
"oldest": "1670566778.964519",
Expand Down Expand Up @@ -43,21 +58,40 @@ mf.mock("POST@/api/conversations.replies", (args) => {
{
status: 200,
},
);
),
});

mf.mock("POST@/api/chat.postMessage", () => {
return new Response(JSON.stringify({ ok: true, ts: "1111.2222" }), {
stubFetch.stub({
assertion: (req) => {
assertEquals(req.method, "POST");
assertEquals(
req.url,
"https://slack.com/api/chat.postMessage",
);
},
response: new Response(JSON.stringify({ ok: true, ts: "1111.2222" }), {
status: 200,
});
}),
});

mf.mock("POST@/v2/translate", async (args) => {
const body = await args.formData();
if (body.get("auth_key") !== "valid") {
return new Response("", { status: 403 });
}
return new Response(
stubFetch.stub({
assertion: async (req) => {
assertEquals(req.method, "POST");
assert(req.url.endsWith("/v2/translate"));
const body = await req.formData();
assertNotEquals(body.get("auth_key"), "valid");
},
response: new Response("", { status: 403 }),
});

stubFetch.stub({
assertion: async (req) => {
assertEquals(req.method, "POST");
assert(req.url.endsWith("/v2/translate"));
const body = await req.formData();
assertEquals(body.get("auth_key"), "valid");
},
response: new Response(
JSON.stringify({
translations: [
{
Expand All @@ -70,7 +104,7 @@ mf.mock("POST@/v2/translate", async (args) => {
{
status: 200,
},
);
),
});

const { createContext } = SlackFunctionTester("my-function");
Expand Down
8 changes: 0 additions & 8 deletions import_map.json
Copy link
Member

Choose a reason for hiding this comment

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

🪓

This file was deleted.

2 changes: 1 addition & 1 deletion triggers/reaction_added_trigger.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Trigger } from "deno-slack-sdk/types.ts";
import type { Trigger } from "deno-slack-sdk/types.ts";
Copy link
Member

Choose a reason for hiding this comment

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

👁️‍🗨️ Good catch!

import {
TriggerContextData,
TriggerEventTypes,
Expand Down