Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement focus commands #50

Draft
wants to merge 4 commits into
base: dev
Choose a base branch
from
Draft
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
13 changes: 12 additions & 1 deletion src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ import pfp from './pfp';
import bot from './bot';
import say from './say';
import mail from './mail';
import focus from './focus';

const commandList: Command[] = [ping, mute, poll, course, pfp, bot, say, mail];
const commandList: Command[] = [
ping,
mute,
poll,
course,
pfp,
bot,
say,
mail,
focus,
];

export default commandList;
85 changes: 85 additions & 0 deletions src/commands/focus/_common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import ExecutionContext from '../../ExecutionContext';
import { Config } from '../../database';
import { RolesConfig } from '../../repository';

export const parseDuration = (duration: string): number => {
let seconds = 0;
const days = duration.match(/(\d+)\s*d/);
const hours = duration.match(/(\d+)\s*h/);
const minutes = duration.match(/(\d+)\s*m/);

if (days) {
seconds += parseInt(days[1]) * 86400;
}

if (hours) {
seconds += parseInt(hours[1]) * 3600;
}

if (minutes) {
seconds += parseInt(minutes[1]) * 60;
}

return seconds;
};

export const respondSetupError = async (
ctx: ExecutionContext,
): Promise<void> => {
await ctx.interactionApi.respondWithError(
'Focus mode role(s) not set up yet. Try running `/focus setup`.',
);
};

export const getFocusHardRoleOrExit = async (
ctx: ExecutionContext,
guildId: string,
): Promise<string | undefined> => {
// Fetch the roles configuration from the server.
const rolesCfg = await mustGetRolesConfig(ctx, guildId);
if (!rolesCfg.focusHard) {
await respondSetupError(ctx);
return;
}

// Get a list of all guild roles, and make sure the focusHard role still exists.
const guildRoles = await ctx.api.getGuildRoles(guildId);
if (!guildRoles.find(r => r.id === rolesCfg.focusHard)) {
await respondSetupError(ctx);
return;
}

return rolesCfg.focusHard;
};

export const getFocusSoftRoleOrExit = async (
ctx: ExecutionContext,
guildId: string,
): Promise<string | undefined> => {
// Fetch the roles configuration from the server.
const rolesCfg = await mustGetRolesConfig(ctx, guildId);
if (!rolesCfg.focusHard) {
await respondSetupError(ctx);
return;
}

// Get a list of all guild roles, and make sure the focusSoft role still exists.
const guildRoles = await ctx.api.getGuildRoles(guildId);
if (!guildRoles.find(r => r.id === rolesCfg.focusSoft)) {
await respondSetupError(ctx);
return;
}

return rolesCfg.focusSoft;
};

export const mustGetRolesConfig = async (
ctx: ExecutionContext,
guildId: string,
): Promise<Partial<RolesConfig>> => {
const rolesCfg = await ctx.config().get<RolesConfig>(guildId, Config.ROLES);
if (!rolesCfg) {
throw new Error('No roles configuration found');
}
return rolesCfg;
};
22 changes: 22 additions & 0 deletions src/commands/focus/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Discord from '../../Discord';
import Command from '../../Command';
import subcommands from './subcommands';

export const FOCUS_HARD_PERMISSIONS = {
allow: '0',
deny: String(Discord.Permission.VIEW_CHANNEL),
};

export const FOCUS_SOFT_PERMISSIONS = {
allow: '0',
deny: String(Discord.Permission.SEND_MESSAGES),
};

const focus = new Command({
name: 'focus',
displayName: 'Focus',
description: 'Focus mode commands',
options: subcommands,
});

export default focus;
34 changes: 34 additions & 0 deletions src/commands/focus/subcommands/hardMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as Discord from '../../../Discord';
import SubCommand from '../../../SubCommand';
import CommandOption from '../../../CommandOption';
import { parseDuration } from '../_common';

const hardMode = new SubCommand({
name: 'hard-mode',
displayName: 'Hard Mode',
description: 'Activate hard focus mode',
requiredPermissions: [Discord.Permission.KICK_MEMBERS],
options: [
new CommandOption({
name: 'duration',
description: 'The duration to activate focus mode',
required: true,
type: Discord.CommandOptionType.STRING,
}),
],
handler: async ctx => {
// const guildId = ctx.mustGetGuildId();
const duration = ctx.getArgument<string>('duration')!.trim();
const parsedDuration = parseDuration(duration);

if (!parsedDuration || parsedDuration === 0) {
return ctx.interactionApi.respondWithError(`Invalid duration`);
}

return ctx.interactionApi.respondWithError(
`This command is still in development [Soft mode focus, ${parsedDuration} seconds]`,
);
},
});

export default hardMode;
8 changes: 8 additions & 0 deletions src/commands/focus/subcommands/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import SubCommand from '../../../SubCommand';
import setup from './setup';
import hardMode from './hardMode';
import softMode from './softMode';

const subcommands: SubCommand[] = [setup, hardMode, softMode];

export default subcommands;
115 changes: 115 additions & 0 deletions src/commands/focus/subcommands/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as Discord from '../../../Discord';
import SubCommand from '../../../SubCommand';
import { Config } from '../../../database';
import { FOCUS_HARD_PERMISSIONS, FOCUS_SOFT_PERMISSIONS } from '../index';
import { RolesConfig } from '../../../repository';

const setup = new SubCommand({
name: 'setup',
displayName: 'Focus Mode Setup',
description:
'Sets up the focus mode roles and appropriate channel permissions',
requiredPermissions: [
Discord.Permission.MANAGE_ROLES,
Discord.Permission.MANAGE_CHANNELS,
],
handler: async ctx => {
const guildId = ctx.mustGetGuildId();

// Respond with a loader, as this command may take longer time on large servers.
ctx.interactionApi.respondLater().catch(err => {
throw err;
});

// Fetch the role configuration from the database.
let rolesCfg = await ctx.config().get<RolesConfig>(guildId, Config.ROLES);

const createFocusHardRole = async (): Promise<RolesConfig> => {
// No roles saved yet, create new ones.
const focusHardRole = await ctx.api.createGuildRole(guildId, {
name: 'Focus (Hard)',
permissions: '0',
});

const newRolesCfg = {
muted: rolesCfg?.muted ?? '',
focusHard: focusHardRole.id,
focusSoft: rolesCfg?.focusSoft ?? '',
};

// Update the `roles` configuration value.
await ctx.config().update(guildId, Config.ROLES, newRolesCfg);

return newRolesCfg;
};

const createFocusSoftRole = async (): Promise<RolesConfig> => {
// No roles saved yet, create new ones.
const focusSoftRole = await ctx.api.createGuildRole(guildId, {
name: 'Focus (Soft)',
permissions: '0',
});

const newRolesCfg = {
muted: rolesCfg?.muted ?? '',
focusHard: rolesCfg?.focusHard ?? '',
focusSoft: focusSoftRole.id,
};

// Update the `roles` configuration value.
await ctx.config().update(guildId, Config.ROLES, newRolesCfg);

return newRolesCfg;
};

if (!rolesCfg || !rolesCfg.focusHard || !rolesCfg.focusSoft) {
// Create the `focusHard` role since none was saved.
rolesCfg = await createFocusHardRole();
// Create the `focusSoft` role since none was saved.
rolesCfg = await createFocusSoftRole();
} else {
// Make sure the saved `focusHard` role is still in the server.
const roles = await ctx.api.getGuildRoles(guildId);
if (!roles.find(r => r.id === rolesCfg?.focusHard)) {
// Cannot find `focusHard` role, so create a new one and save it.
rolesCfg = await createFocusHardRole();
}

// Make sure the saved `focusSoft` role is still in the server.
if (!roles.find(r => r.id === rolesCfg?.focusSoft)) {
// Cannot find `focusSoft` role, so create a new one and save it.
rolesCfg = await createFocusSoftRole();
}
}

const channels = await ctx.api.getGuildChannels(guildId);
for (const c of channels) {
// Only apply the permission overwrite to the following channel types.
if (
![
Discord.ChannelType.GUILD_CATEGORY,
Discord.ChannelType.GUILD_TEXT,
].includes(c.type)
) {
continue;
}

await ctx.api.editChannelPermissions(c.id, rolesCfg.focusHard!, {
allow: FOCUS_HARD_PERMISSIONS.allow,
deny: FOCUS_HARD_PERMISSIONS.deny,
type: 0,
});
await ctx.api.editChannelPermissions(c.id, rolesCfg.focusSoft!, {
allow: FOCUS_SOFT_PERMISSIONS.allow,
deny: FOCUS_SOFT_PERMISSIONS.deny,
type: 0,
});
}

await ctx.interactionApi.editResponse({
content: 'Successfully set up the Focus roles',
});
},
});

export default setup;
34 changes: 34 additions & 0 deletions src/commands/focus/subcommands/softMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as Discord from '../../../Discord';
import SubCommand from '../../../SubCommand';
import CommandOption from '../../../CommandOption';
import { parseDuration } from '../_common';

const softMode = new SubCommand({
name: 'soft-mode',
displayName: 'Soft Mode',
description: 'Activate soft focus mode',
requiredPermissions: [Discord.Permission.KICK_MEMBERS],
options: [
new CommandOption({
name: 'duration',
description: 'The duration to activate focus mode',
required: true,
type: Discord.CommandOptionType.STRING,
}),
],
handler: async ctx => {
// const guildId = ctx.mustGetGuildId();
const duration = ctx.getArgument<string>('duration')!.trim();
const parsedDuration = parseDuration(duration);

if (!parsedDuration || parsedDuration === 0) {
return ctx.interactionApi.respondWithError(`Invalid duration`);
}

return ctx.interactionApi.respondWithError(
`This command is still in development [Soft mode focus, ${parsedDuration} seconds]`,
);
},
});

export default softMode;
2 changes: 2 additions & 0 deletions src/commands/mute/subcommands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const setup = new SubCommand({

const newRolesCfg = {
muted: mutedRole.id,
focusHard: rolesCfg?.focusHard ?? '',
focusSoft: rolesCfg?.focusSoft ?? '',
};

// Update the `roles` configuration value.
Expand Down
4 changes: 4 additions & 0 deletions src/repository/ConfigRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type GuildConfig = {

export type RolesConfig = {
muted: string;
focusHard: string;
focusSoft: string;
};

export type MailConfig = {
Expand All @@ -25,6 +27,8 @@ export type MailConfig = {

export const rolesConfig: RolesConfig = {
muted: '',
focusHard: '',
focusSoft: '',
};

export const guildConfig: GuildConfig = {
Expand Down