Skip to content

Commit

Permalink
Commands to list and delete rounds (#14)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinyaodu authored Oct 8, 2023
1 parent 2a85e49 commit 63c866e
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 27 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
"plugins": ["@typescript-eslint", "import", "eslint-plugin-tsdoc"],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
],
"@typescript-eslint/require-await": "off",
"no-constant-condition": ["error", { "checkLoops": false }],
"@typescript-eslint/no-unnecessary-condition": [
Expand Down
3 changes: 3 additions & 0 deletions src/models/RoundModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ const indexes: { [K in keyof Round]?: 1 }[] = [
// Used to get the most recent round in a channel, to determine the start date
// when scheduling the next round.
{ channel: 1, matchingScheduledFor: 1 },

// Used to list rounds chronologically.
{ summaryMessageScheduledFor: 1 },
];
indexes.forEach((index) => RoundSchema.index(index));

Expand Down
29 changes: 28 additions & 1 deletion src/services/round.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DateTime, Duration } from "luxon";
import { Types } from "mongoose";

import { GroupModel } from "../models/GroupModel";
import { Round, RoundDocument, RoundModel } from "../models/RoundModel";
import { Result } from "../util/result";

Expand Down Expand Up @@ -81,4 +83,29 @@ async function createRound(
return Result.ok(round);
}

export { createRound, repeatRound };
async function listRounds(cutoff?: DateTime): Promise<RoundDocument[]> {
const filter =
cutoff === undefined
? {}
: {
summaryMessageScheduledFor: { $gt: cutoff.toJSDate() },
};

return RoundModel.find(filter).sort({
summaryMessageScheduledFor: 1,
});
}

async function deleteRound(id: string): Promise<RoundDocument | null> {
const deleted = await RoundModel.findOneAndDelete({
_id: new Types.ObjectId(id),
});

if (deleted !== null) {
await GroupModel.deleteMany({ round: deleted._id });
}

return deleted;
}

export { createRound, repeatRound, listRounds, deleteRound };
46 changes: 45 additions & 1 deletion src/shell/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { App, SlackEventMiddlewareArgs } from "@slack/bolt";
import { DateTime, Duration } from "luxon";

import { onReactionAddedToMessage } from "../handlers/reaction";
import { createRound, repeatRound } from "../services/round";
import {
createRound,
deleteRound,
listRounds,
repeatRound,
} from "../services/round";
import {
addReactions,
editMessage,
Expand All @@ -12,6 +17,7 @@ import {
} from "../services/slack";
import {
formatChannel,
formatRound,
parseChannel,
parseDate,
parseDuration,
Expand Down Expand Up @@ -199,6 +205,42 @@ class ReactSimulateCommand extends Command {
}
}

class RoundDeleteCommand extends Command {
static readonly privileged = true;
static readonly id = "round_delete";
static readonly help = ["ID", "delete a round"];

async run() {
if (this.args.length !== 1) {
return usageErr(RoundDeleteCommand);
}

const roundId = this.args[0];
const deleted = await deleteRound(roundId);
if (deleted === null) {
return Result.err(`no round with ID: ${roundId}`);
} else {
return Result.ok(`deleted: ${formatRound(deleted)}`);
}
}
}

class RoundListCommand extends Command {
static readonly privileged = true;
static readonly id = "round_list";
static readonly help = ["", "list all rounds"];

async run() {
if (this.args.length !== 0) {
return usageErr(RoundListCommand);
}

return Result.ok(
(await listRounds()).map((round) => formatRound(round)).join("\n"),
);
}
}

class RoundRepeatCommand extends Command {
static readonly privileged = true;
static readonly id = "round_repeat";
Expand Down Expand Up @@ -380,6 +422,8 @@ const commandClasses = [
LsCommand,
ReactCommand,
ReactSimulateCommand,
RoundDeleteCommand,
RoundListCommand,
RoundRepeatCommand,
RoundScheduleCommand,
SendDirectMessageCommand,
Expand Down
32 changes: 32 additions & 0 deletions src/util/formatting.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DateTime, Duration } from "luxon";

import { RoundDocument } from "../models/RoundModel";

import { Result } from "./result";

function formatChannel(channel: string): string {
Expand Down Expand Up @@ -45,6 +47,15 @@ function parseInteger(value: string): Result<number, string> {
return Result.ok(parsed);
}

function formatDuration(duration: Duration): string {
const units = ["weeks", "days", "hours", "minutes", "seconds"] as const;
const shifted = duration.shiftTo(...units);
const entries = units
.map((unit) => [unit, shifted[unit]] as const)
.filter(([_, value]) => value > 0);
return entries.map(([unit, value]) => `${value}${unit[0]}`).join("");
}

function parseDuration(duration: string): Result<Duration, string> {
if (duration.length === 0) {
return Result.err("duration string cannot be empty");
Expand Down Expand Up @@ -93,6 +104,25 @@ function parseDuration(duration: string): Result<Duration, string> {
return Result.ok(Duration.fromObject(durationObject));
}

function formatRound(round: RoundDocument): string {
const startDate = DateTime.fromJSDate(round.matchingScheduledFor);
return [
round._id.toHexString(),
formatChannel(round.channel),
round.matchingScheduledFor.toISOString(),
formatDuration(Duration.fromObject({ seconds: round.durationSec })),
formatDuration(
DateTime.fromJSDate(round.reminderMessageScheduledFor).diff(startDate),
),
formatDuration(
DateTime.fromJSDate(round.finalMessageScheduledFor).diff(startDate),
),
formatDuration(
DateTime.fromJSDate(round.summaryMessageScheduledFor).diff(startDate),
),
].join(" ");
}

export {
formatChannel,
parseChannel,
Expand All @@ -101,5 +131,7 @@ export {
formatUser,
parseUser,
parseDate,
formatDuration,
parseDuration,
formatRound,
};
38 changes: 13 additions & 25 deletions tests/util/formatting.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, test } from "@jest/globals";
import { Duration } from "luxon";

import { parseDuration } from "../../src/util/formatting";
import { formatDuration, parseDuration } from "../../src/util/formatting";
import { Result } from "../../src/util/result";

describe("parseDuration", () => {
Expand All @@ -11,31 +11,19 @@ describe("parseDuration", () => {
);
});

test("days only", () => {
const result = parseDuration("53d");
expect(result.ok ? result.value.toISO() : null).toStrictEqual(
Duration.fromObject({ days: 53 }).toISO(),
);
});

test("hours and minutes only", () => {
const result = parseDuration("3h4m");
expect(result.ok ? result.value.toISO() : null).toStrictEqual(
Duration.fromObject({ hours: 3, minutes: 4 }).toISO(),
);
});

test("all fields", () => {
const result = parseDuration("1w15d12h9m6s");
expect(result.ok ? result.value.toISO() : null).toStrictEqual(
Duration.fromObject({
weeks: 1,
days: 15,
hours: 12,
minutes: 9,
seconds: 6,
}).toISO(),
test.each([
["5d", { days: 5 }],
["3h4m", { hours: 3, minutes: 4 }],
["1w5d12h9m6s", { weeks: 1, days: 5, hours: 12, minutes: 9, seconds: 6 }],
])("round-trip %s to %j", (str: string, obj: Record<string, number>) => {
const parseResult = parseDuration(str);
if (!parseResult.ok) {
throw new Error("failed to parse");
}
expect(parseResult.value.toISO()).toStrictEqual(
Duration.fromObject(obj).toISO(),
);
expect(formatDuration(parseResult.value)).toStrictEqual(str);
});

test("wrong formats", () => {
Expand Down

0 comments on commit 63c866e

Please sign in to comment.