Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ icon.svg
LICENSE
Privacy.md
README.md
Terms&Conditions.md
TermsAndConditions.md
1 change: 1 addition & 0 deletions Privacy.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- User IDs of users who use block or filter commands to set up a filter of who can ring them, and the User IDs of the people that they block
- Voice Channel IDs and User IDs when a user is signed up for a Voice Channel
- Feedback submitted through the feedback form (the text and a timestamp only, never who submitted it)

## Temporary Data

Expand Down
File renamed without changes.
8 changes: 4 additions & 4 deletions src/commands/ringvc.ts → src/commands/about.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {

import { RingRouter } from "@routes/types";

export const ringvc = {
export const about = {
data: new SlashCommandBuilder()
.setName("ringvc")
.setDescription("Open the RingVC home panel"),
.setName("about")
.setDescription("Project links, policies, and feedback"),
async execute(router: RingRouter, interaction: ChatInputCommandInteraction) {
await router.dispatch(interaction, "/", {
await router.dispatch(interaction, "/about", {
flags: [MessageFlags.Ephemeral],
});
},
Expand Down
2 changes: 1 addition & 1 deletion src/commands/commandNames.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const commandNamesList = [
"ringvc",
"help",
"catalog",
"about",
"delete_data",
"ring",
"ring_defaults",
Expand Down
4 changes: 2 additions & 2 deletions src/commands/commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ChatInputCommandInteraction, SharedSlashCommand } from "discord.js";

import { about } from "@commands/about";
import { catalog } from "@commands/catalog";
import { defaultRingRecipients } from "@commands/defaultRingRecipient";
import { deleteData } from "@commands/deleteData";
Expand All @@ -13,7 +14,6 @@ import { mode } from "@commands/mode";
import { quit } from "@commands/quit";
import { ring } from "@commands/ring";
import { ringDefaults } from "@commands/ringDefaults";
import { ringvc } from "@commands/ringvc";
import { signup } from "@commands/signup";
import { signuprole } from "@commands/signuprole";
import { unsignup } from "@commands/unsignup";
Expand All @@ -31,9 +31,9 @@ export type CommandImplementation = {
};

export const commands: CommandImplementation[] = [
ringvc,
help,
catalog,
about,
deleteData,
ring,
ringDefaults,
Expand Down
18 changes: 18 additions & 0 deletions src/main/db/database.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ export type Database = {
};
Relationships: [];
};
feedback: {
Row: {
content: string;
created_at: string;
id: number;
};
Insert: {
content: string;
created_at?: string;
id?: never;
};
Update: {
content?: string;
created_at?: string;
id?: never;
};
Relationships: [];
};
filter_entries: {
Row: {
channel_id: string | null;
Expand Down
5 changes: 5 additions & 0 deletions src/main/db/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { db, throwOnError } from "./client";

export const submitFeedback = async (content: string): Promise<void> => {
throwOnError(await db.from("feedback").insert({ content }));
};
1 change: 0 additions & 1 deletion src/routes/_shared.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/routes/about/_shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const PANEL = "/about";
export const FEEDBACK = "/about/feedback";
57 changes: 57 additions & 0 deletions src/routes/about/about.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Interaction } from "discord.js";
import { beforeEach, expect, test, vi } from "vitest";

import { submitFeedback } from "@db/feedback";

import { aboutFeedbackPost } from "./feedback/post";

vi.mock("@db/feedback", () => ({
submitFeedback: vi.fn(),
}));

const interaction = {
user: { id: "caller" },
isChatInputCommand: () => false,
} as unknown as Interaction;

const state = (content: string) =>
({
params: {},
path: "/about/feedback",
queryParams: new URLSearchParams(),
timestamp: 0,
fields: { getTextInputValue: () => content },
}) as unknown as Parameters<typeof aboutFeedbackPost>[2];

beforeEach(() => {
vi.clearAllMocks();
});

test("submitting feedback stores the trimmed text", async () => {
const result = await aboutFeedbackPost(
undefined as never,
interaction,
state(" great bot! "),
);

expect(submitFeedback).toHaveBeenCalledExactlyOnceWith("great bot!");
expect(result.redirect).toBe("/about");
const flashParams = new URLSearchParams(
result.queryParams as Record<string, string>,
);
expect(flashParams.get("level")).toBe("success");
});

test("whitespace-only feedback stores nothing", async () => {
const result = await aboutFeedbackPost(
undefined as never,
interaction,
state(" "),
);

expect(submitFeedback).not.toHaveBeenCalled();
const flashParams = new URLSearchParams(
result.queryParams as Record<string, string>,
);
expect(flashParams.get("level")).toBe("warn");
});
23 changes: 23 additions & 0 deletions src/routes/about/feedback/modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { RouteModalBuilder } from "discord-embed-router";
import { LabelBuilder, TextInputBuilder, TextInputStyle } from "discord.js";

import { Handler } from "@routes/types";

import { FEEDBACK } from "../_shared";

export const aboutFeedbackModal: Handler<"MODAL"> = (router) =>
new RouteModalBuilder(router)
.setTo(FEEDBACK, { method: "POST" })
.setTitle("Give feedback")
.addLabelComponents(
new LabelBuilder()
.setLabel("Your feedback")
.setDescription("Submissions are anonymous.")
.setTextInputComponent(
new TextInputBuilder()
.setCustomId("content")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true)
.setPlaceholder("Bug reports, feature ideas, anything"),
),
);
28 changes: 28 additions & 0 deletions src/routes/about/feedback/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { submitFeedback } from "@db/feedback";
import { flashRedirect } from "@routes/lib/flash";
import { Handler } from "@routes/types";

import { PANEL } from "../_shared";

export const aboutFeedbackPost: Handler<"POST"> = async (
router,
interaction,
state,
) => {
const content = (state.fields?.getTextInputValue("content") ?? "").trim();
if (!content)
return flashRedirect(
interaction,
PANEL,
"Feedback was empty, nothing was sent",
"warn",
);

await submitFeedback(content);
return flashRedirect(
interaction,
PANEL,
"Feedback sent. Thank you!",
"success",
);
};
22 changes: 21 additions & 1 deletion src/routes/about/get.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { RouteButtonBuilder } from "discord-embed-router";
import { ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";

import { navBar, row } from "@routes/lib/components";
import { withFlash } from "@routes/lib/flash";
import { Handler } from "@routes/types";

import { FEEDBACK } from "./_shared";

const COLOR = "#5865f2";

const GITHUB_URL = "https://github.com/altrup/RingVC";
const SUPPORT_URL = "https://discord.gg/bxBePEnndq";
const PRIVACY_URL = "https://github.com/altrup/RingVC/blob/main/Privacy.md";
const TERMS_URL =
"https://github.com/altrup/RingVC/blob/main/TermsAndConditions.md";

export const aboutGet: Handler<"GET"> = (router, interaction, state) => {
return {
Expand All @@ -18,7 +24,7 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => {
.setDescription(
withFlash(
state.queryParams,
"RingVC is free and open source. Star it on GitHub, or join the support server for help and updates.",
"RingVC replicates group-chat voice calls in Discord servers: sign up for a voice channel and get pinged when someone starts a call there.\n\nIt's free and open source. Star it on GitHub, or join the support server for help and updates.\n\nHave a bug report or feature idea? Give anonymous feedback below.",
),
),
],
Expand All @@ -32,6 +38,20 @@ export const aboutGet: Handler<"GET"> = (router, interaction, state) => {
.setLabel("Support Server")
.setStyle(ButtonStyle.Link)
.setURL(SUPPORT_URL),
new RouteButtonBuilder(router)
.setLabel("Give feedback")
.setStyle(ButtonStyle.Primary)
.setTo(FEEDBACK, { method: "MODAL" }),
),
row(
new ButtonBuilder()
.setLabel("Privacy Policy")
.setStyle(ButtonStyle.Link)
.setURL(PRIVACY_URL),
new ButtonBuilder()
.setLabel("Terms & Conditions")
.setStyle(ButtonStyle.Link)
.setURL(TERMS_URL),
),
navBar(router, interaction),
],
Expand Down
27 changes: 0 additions & 27 deletions src/routes/get.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/routes/help/catalog/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ export const catalogGet: Handler<"GET"> = (router, interaction, state) => {
{
title: "Panels",
entries: [
["ringvc", "the home panel"],
["help", "getting started"],
["catalog", "this command catalog"],
["about", "project links, policies, and feedback"],
["signup", "your signups panel (bare, outside a voice channel)"],
["filter", "your filter panel"],
["default_ring_recipients", "ring recipients and auto-ring panel"],
Expand Down
8 changes: 6 additions & 2 deletions src/routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RingRouter } from "@routes/types";

import { aboutFeedbackModal } from "./about/feedback/modal";
import { aboutFeedbackPost } from "./about/feedback/post";
import { aboutGet } from "./about/get";
import { deleteDataGet } from "./delete-data/get";
import { deleteDataModal } from "./delete-data/modal";
Expand All @@ -9,7 +11,6 @@ import { filterMembersPost } from "./filter/[scope]/members/post";
import { filterResetModal } from "./filter/[scope]/reset/modal";
import { filterResetPost } from "./filter/[scope]/reset/post";
import { filterTypePost } from "./filter/[scope]/type/post";
import { homeGet } from "./get";
import { catalogGet } from "./help/catalog/get";
import { helpGet } from "./help/get";
import { modeGet } from "./mode/get";
Expand Down Expand Up @@ -43,10 +44,13 @@ import { rolesGet } from "./signups/roles/get";
// handlers live in files mirroring their route: the folder is the path (with
// [param] segments) and the file is the method
export const registerRoutes = (router: RingRouter) => {
router.get("/", homeGet);
router.get("/help", helpGet);
router.get("/help/catalog", catalogGet);
router.get("/about", aboutGet);
router.route("/about/feedback", {
modal: aboutFeedbackModal,
post: aboutFeedbackPost,
});

// scoped panels answer their bare path as the global scope, so the scope-switch
// select can target "{/:channelId}" and fall back to global when cleared
Expand Down
Loading
Loading