Skip to content

Commit dca4f13

Browse files
jwfordBoedJ
andauthored
feat: add pomodoro command (#30)
* feat(UserSchema): add pomodoro settings * set up SteveUser to have access to PomodoroManager * add settings * get it working * remove unused method * change alias from pom to pomo * add confirmation message for pom set * change name of pomodoro task * check if pomodoro is running when end subcommand is used * add show subcommand * fix grammar * style: typo fix Co-authored-by: BoedJ <jonathan.hoebeke@gmail.com>
1 parent 6e7157a commit dca4f13

File tree

9 files changed

+271
-1
lines changed

9 files changed

+271
-1
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { SteveCommand } from '@lib/structures/commands/SteveCommand';
2+
import { CommandStore, KlasaMessage, ScheduledTask, SettingsUpdateResult } from 'klasa';
3+
import { Message, ColorResolvable } from 'discord.js';
4+
import { UserSettings } from '@lib/types/settings/UserSettings';
5+
import { friendlyDuration, newEmbed } from '@lib/util/util';
6+
import { oneLine } from 'common-tags';
7+
import { Colors } from '@lib/types/enums';
8+
9+
export default class extends SteveCommand {
10+
11+
public constructor(store: CommandStore, file: string[], directory: string) {
12+
super(store, file, directory, {
13+
aliases: ['pomo', 'pom'],
14+
description: 'Be productive with the pomodoro technique!',
15+
examples: ['pomo start', 'pomo end', 'pomo check', 'pomo show', 'pomo set|work|25m', 'pomo set|short|5m', 'pomo set|long|15m'],
16+
extendedHelp: oneLine`This command helps faciliate use of the
17+
[pomodoro technique](https://en.wikipedia.org/wiki/Pomodoro_Technique). Note that if you change the length of a work cycle
18+
or break while that cycle is happening, the change will not take effect until the next time that cycle occurs.`,
19+
helpUsage: '*start*/*check*/*end*/*show*/*set* (segment|duration)',
20+
subcommands: true,
21+
usage: '<start|check|end|show|set> (segment:pomSegment) (duration:timespan)'
22+
});
23+
24+
this
25+
.createCustomResolver('pomSegment', (str, possible, msg, [action]) => {
26+
if (action !== 'set') return null;
27+
const beefsteak = ['work', 'long', 'short']; // thank allison for the variable name
28+
if (!beefsteak.includes(str)) throw `**${str}** is not a valid segment name.`;
29+
return str;
30+
})
31+
.createCustomResolver('timespan', (str, possible, msg, [action]) =>
32+
action === 'set' ? this.client.arguments.get('timespan').run(str, possible, msg) : null);
33+
}
34+
35+
public async start(msg: KlasaMessage): Promise<[[ScheduledTask, SettingsUpdateResult, SettingsUpdateResult], SettingsUpdateResult, Message]> {
36+
if (msg.author.pomodoro.running) throw 'Your pomodoro timer is already running! Get back to work smh';
37+
38+
return Promise.all([
39+
msg.author.pomodoro.startPomodoroTimer(),
40+
msg.author.pomodoro.incrementWorkRoundNumber(),
41+
msg.channel.send('Starting your pomodoro timer. You got this; get after it!')]);
42+
}
43+
44+
public async check(msg: KlasaMessage): Promise<Message> {
45+
const task = msg.author.pomodoro.getPomodoroTask();
46+
if (!task) throw 'You\'re not currently pomodoroing!';
47+
48+
return msg.channel.send(oneLine`You have ${friendlyDuration(task.time.getTime() - Date.now())} left in your
49+
${msg.author.pomodoro.friendlyCurrentSegment}!`);
50+
}
51+
52+
public async end(msg: KlasaMessage): Promise<Message> {
53+
if (!msg.author.pomodoro.running) throw 'You\'re not currently pomodoroing!';
54+
await msg.author.pomodoro.reset();
55+
return msg.channel.send('Your pomodoro timer has ended. Great job!');
56+
}
57+
58+
public async show(msg: KlasaMessage): Promise<Message> {
59+
const workLength = friendlyDuration(msg.author.pomodoro.workSegmentLength);
60+
const shortLength = friendlyDuration(msg.author.pomodoro.shortBreakSegmentLength);
61+
const longLength = friendlyDuration(msg.author.pomodoro.longBreakSegmentLength);
62+
63+
const embed = newEmbed()
64+
.addFields(
65+
{ name: 'Work Cycle', value: workLength, inline: true },
66+
{ name: 'Short Break', value: shortLength, inline: true },
67+
{ name: 'Long Break', value: longLength, inline: true }
68+
)
69+
.setColor(msg.author.settings.get(UserSettings.EmbedColor) as ColorResolvable || Colors.YellowGreen)
70+
.setTitle(`Pomodoro Settings for ${msg.author.tag}`);
71+
72+
return msg.channel.send(embed);
73+
}
74+
75+
public async set(msg: KlasaMessage, [pomType, duration]: [string, number]): Promise<[SettingsUpdateResult, Message]> {
76+
let update: string;
77+
let friendlySegmentName: string;
78+
79+
switch (pomType) {
80+
case 'work':
81+
update = UserSettings.Pomodoro.WorkTime;
82+
friendlySegmentName = 'work period';
83+
break;
84+
case 'long':
85+
update = UserSettings.Pomodoro.LongBreakTime;
86+
friendlySegmentName = 'long break';
87+
break;
88+
case 'short':
89+
update = UserSettings.Pomodoro.ShortBreakTime;
90+
friendlySegmentName = 'short break';
91+
break;
92+
}
93+
94+
return Promise.all([
95+
msg.author.settings.update(update, duration),
96+
msg.channel.send(`You've updated your ${friendlySegmentName} length to ${friendlyDuration(duration)}.`)
97+
]);
98+
}
99+
100+
}

src/lib/SteveClient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Node as Lavalink } from 'lavalink';
44
import { LAVALINK_ENABLE } from '@root/config';
55

66
import '@lib/extensions/SteveGuild';
7+
import '@lib/extensions/SteveUser';
78
import '@lib/schemas/client';
89
import '@lib/schemas/guild';
910
import '@lib/schemas/user';

src/lib/extensions/SteveUser.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Structures } from 'discord.js';
2+
import { PomodoroManager } from '@lib/structures/PomodoroManager';
3+
4+
export class SteveUser extends Structures.get('User') {
5+
6+
public readonly pomodoro: PomodoroManager = new PomodoroManager(this);
7+
8+
}
9+
10+
Structures.extend('User', () => SteveUser);

src/lib/schemas/user.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { Client } from 'klasa';
2+
import { Time } from '@lib/types/enums';
23

34
export default Client.defaultUserSchema
4-
.add('embedColor', 'Color');
5+
.add('embedColor', 'Color')
6+
.add('pomodoro', pom => pom
7+
.add('currentSegment', 'PomodoroSegment', { configurable: false })
8+
.add('longBreakTime', 'Integer', { default: 15 * Time.Minute })
9+
.add('running', 'Boolean', { configurable: false, default: false })
10+
.add('shortBreakTime', 'Integer', { default: 5 * Time.Minute })
11+
.add('workRoundNumber', 'Integer', { configurable: false, default: 0 })
12+
.add('workTime', 'Integer', { default: 25 * Time.Minute }));
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { SteveClient } from '@lib/SteveClient';
2+
import { User } from 'discord.js';
3+
import { UserSettings } from '@lib/types/settings/UserSettings';
4+
import { ScheduledTask, SettingsUpdateResult } from 'klasa';
5+
6+
export class PomodoroManager {
7+
8+
public client: SteveClient;
9+
public user: User;
10+
11+
public constructor(user: User) {
12+
this.client = user.client as SteveClient;
13+
this.user = user;
14+
}
15+
16+
public get currentSegment(): string | null {
17+
const currentSegment = this.user.settings.get(UserSettings.Pomodoro.CurrentSegment);
18+
return this.running ? currentSegment : null;
19+
}
20+
21+
public get friendlyCurrentSegment(): string | null{
22+
return this.currentSegment === 'work'
23+
? 'work period'
24+
: this.currentSegment === 'short'
25+
? 'short break'
26+
: this.currentSegment === 'long'
27+
? 'long break'
28+
: null;
29+
}
30+
31+
public get longBreakSegmentLength(): number {
32+
return this.user.settings.get(UserSettings.Pomodoro.LongBreakTime);
33+
}
34+
35+
public get running(): boolean {
36+
return this.user.settings.get(UserSettings.Pomodoro.Running);
37+
}
38+
39+
public get shortBreakSegmentLength(): number {
40+
return this.user.settings.get(UserSettings.Pomodoro.ShortBreakTime);
41+
}
42+
43+
public get workRoundNumber(): number {
44+
return this.user.settings.get(UserSettings.Pomodoro.WorkRoundNumber);
45+
}
46+
47+
public get workSegmentLength(): number {
48+
return this.user.settings.get(UserSettings.Pomodoro.WorkTime);
49+
}
50+
51+
public async createPomodoroTask(segmentLength: number): Promise<ScheduledTask> {
52+
return this.client.schedule.create('pomodoro', Date.now() + segmentLength, {
53+
catchUp: true,
54+
data: { user: this.user.id }
55+
});
56+
}
57+
58+
public getPomodoroTask(): ScheduledTask {
59+
// eslint-disable-next-line id-length
60+
const task = this.client.schedule.tasks.filter(t => t.taskName === 'pomodoro' && t.data.user === this.user.id)[0];
61+
return task || null;
62+
}
63+
64+
public async incrementWorkRoundNumber(): Promise<SettingsUpdateResult> {
65+
return this.workRoundNumber <= 3
66+
? await this.user.settings.update(UserSettings.Pomodoro.WorkRoundNumber, this.workRoundNumber + 1)
67+
: await this.user.settings.update(UserSettings.Pomodoro.WorkRoundNumber, 1);
68+
}
69+
70+
public async startPomodoroTimer(): Promise<[ScheduledTask, SettingsUpdateResult, SettingsUpdateResult]> {
71+
return Promise.all([
72+
this.createPomodoroTask(this.workSegmentLength),
73+
this.user.settings.update(UserSettings.Pomodoro.CurrentSegment, 'work'),
74+
this.user.settings.update(UserSettings.Pomodoro.Running, true)
75+
]);
76+
}
77+
78+
public async reset(): Promise<SettingsUpdateResult[]> {
79+
const task = this.getPomodoroTask();
80+
await task.delete();
81+
return Promise.all(
82+
[this.user.settings.reset(UserSettings.Pomodoro.WorkRoundNumber),
83+
this.user.settings.reset(UserSettings.Pomodoro.CurrentSegment),
84+
this.user.settings.reset(UserSettings.Pomodoro.Running)]);
85+
}
86+
87+
}

src/lib/types/augments.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Snowflake, Guild, GuildMember, UserResolvable, TextChannel } from 'disc
33
import { Node as Lavalink, BaseNodeOptions } from 'lavalink';
44
import { ModerationManager } from '@lib/structures/moderation/ModerationManager';
55
import { MusicHandler } from '@lib/structures/music/MusicHandler';
6+
import { PomodoroManager } from '@lib/structures/PomodoroManager';
67

78
declare module 'discord.js' {
89
interface Guild {
@@ -21,6 +22,10 @@ declare module 'discord.js' {
2122
interface Role {
2223
private: boolean;
2324
}
25+
26+
interface User {
27+
readonly pomodoro: PomodoroManager;
28+
}
2429
}
2530

2631
declare module 'klasa' {

src/lib/types/settings/UserSettings.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,13 @@ export namespace UserSettings {
44

55
export const EmbedColor = 'embedColor';
66

7+
export namespace Pomodoro {
8+
export const CurrentSegment = 'pomodoro.currentSegment';
9+
export const LongBreakTime = 'pomodoro.longBreakTime';
10+
export const Running = 'pomodoro.running';
11+
export const ShortBreakTime = 'pomodoro.shortBreakTime';
12+
export const WorkRoundNumber = 'pomodoro.workRoundNumber';
13+
export const WorkTime = 'pomodoro.workTime';
14+
}
15+
716
}

src/serializers/pomodorosegment.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Serializer } from 'klasa';
2+
3+
export default class extends Serializer {
4+
5+
public async deserialize(data: string): Promise<string> {
6+
const segments = ['work', 'short', 'long'];
7+
if (!segments.includes(data)) throw 'Invalid pomodoro segment!';
8+
return data;
9+
}
10+
11+
public serialize(value: string): string {
12+
return value;
13+
}
14+
15+
}

src/tasks/pomodoro.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Task } from 'klasa';
2+
import { UserSettings } from '@lib/types/settings/UserSettings';
3+
4+
export default class extends Task {
5+
6+
async run({ user }: PomodoroTaskData): Promise<void> {
7+
const _user = this.client.users.cache.get(user);
8+
9+
if (_user.pomodoro.currentSegment === 'work' && _user.pomodoro.workRoundNumber <= 3) {
10+
Promise.all([
11+
_user.send('Great job! Time for your short break!'),
12+
_user.settings.update(UserSettings.Pomodoro.CurrentSegment, 'short'),
13+
_user.pomodoro.createPomodoroTask(_user.pomodoro.shortBreakSegmentLength)
14+
]);
15+
} else if (_user.pomodoro.currentSegment === 'work' && _user.pomodoro.workRoundNumber === 4) {
16+
Promise.all([
17+
_user.send('Great job! Time for your long break, you\'ve earned it!'),
18+
_user.settings.update(UserSettings.Pomodoro.CurrentSegment, 'long'),
19+
_user.pomodoro.createPomodoroTask(_user.pomodoro.longBreakSegmentLength)
20+
]);
21+
} else if (_user.pomodoro.currentSegment === 'short' || _user.pomodoro.currentSegment === 'long') {
22+
Promise.all([
23+
_user.send('Time to get back to work! You got this.'),
24+
_user.settings.update(UserSettings.Pomodoro.CurrentSegment, 'work'),
25+
_user.pomodoro.incrementWorkRoundNumber(),
26+
_user.pomodoro.createPomodoroTask(_user.pomodoro.workSegmentLength)
27+
]);
28+
} else { this.client.console.log('i have no idea what is going on with this pomodoro thing'); }
29+
}
30+
31+
}
32+
33+
interface PomodoroTaskData {
34+
user: string;
35+
}

0 commit comments

Comments
 (0)