-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement /watch add and /watch cancel to notify when a user is activ…
…e somewhere (#917) * Initial /watch add, cancel implementations --------- Co-authored-by: theimperious1 <[email protected]>
- Loading branch information
1 parent
c298154
commit b1d6b90
Showing
5 changed files
with
324 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { | ||
ChannelType, | ||
GuildMember, | ||
SlashCommandBuilder, | ||
TextChannel, | ||
Role, | ||
} from 'discord.js'; | ||
import { SlashCommand } from '../../@types/commandDef'; | ||
import commandContext from '../../utils/context'; // eslint-disable-line @typescript-eslint/no-unused-vars | ||
import { executeWatch, deleteWatchRequest } from '../../../global/commands/g.watchuser'; | ||
|
||
const F = f(__filename); | ||
|
||
export const dWatchUser: SlashCommand = { | ||
data: new SlashCommandBuilder() | ||
.setName('watch') | ||
.setDescription('Get notified when a user says something') | ||
.addSubcommand(subcommand => subcommand | ||
.setName('add') | ||
.setDescription('Set a Watch on a user.') | ||
.addUserOption(option => option.setName('target') | ||
.setDescription('The target user to watch for or their Discord ID') | ||
.setRequired(true)) | ||
.addStringOption(option => option.setName('notification_method') | ||
.setDescription('How do you want to be notified?') | ||
.addChoices( | ||
{ name: 'DM', value: 'dm' }, | ||
{ name: 'Channel', value: 'channel' }, | ||
) | ||
.setRequired(true)) | ||
.addChannelOption(option => option.setName('alert_channel') | ||
.setDescription('Where should I notify you? (Default: \'here\')'))) | ||
.addSubcommand(subcommand => subcommand | ||
.setName('cancel') | ||
.setDescription('Cancel a Watch on a user') | ||
.addStringOption(option => option.setName('target') | ||
.setDescription('The target user you were watching') | ||
.setRequired(true))), | ||
async execute(interaction) { | ||
log.info(F, await commandContext(interaction)); | ||
await interaction.deferReply({ ephemeral: true }); | ||
|
||
if (!interaction.guild) { | ||
await interaction.editReply({ content: 'This command can only be used in a server!' }); | ||
return false; | ||
} | ||
|
||
if (!(interaction.member as GuildMember).roles.cache.some( | ||
(role: Role) => (role.id === env.ROLE_MODERATOR || role.id === env.DEVELOPER), | ||
)) { | ||
await interaction.reply('You do not have permission to use this command.'); | ||
return false; | ||
} | ||
|
||
const targetUser = interaction.options.getUser('target', true); | ||
|
||
if (interaction.options.getSubcommand() === 'cancel') { | ||
if (await deleteWatchRequest(targetUser.id, interaction.user.id)) { | ||
await interaction.editReply({ content: 'Done! You won\'t be notified the next time this user is active.' }); | ||
return true; | ||
} | ||
// eslint-disable-next-line max-len | ||
await interaction.editReply({ content: 'Whoops, it seems like you don\'t have any watch requests on this user to cancel!' }); | ||
return false; | ||
} | ||
|
||
let alertChannel = interaction.options.getChannel('alert_channel') as TextChannel | null; | ||
|
||
if (!alertChannel) { | ||
alertChannel = interaction.channel as TextChannel; | ||
} | ||
|
||
// Ensure that the channel used is a text channel | ||
if (alertChannel.type !== ChannelType.GuildText) { | ||
await interaction.editReply({ content: 'This command can only be used in a text channel!' }); | ||
return false; | ||
} | ||
|
||
const notificationMethod = interaction.options.getString('notification_method', true); | ||
// const target = await interaction.client.users.fetch(targetUser.id); | ||
|
||
if (await executeWatch(targetUser, notificationMethod, interaction.user.id, alertChannel)) { | ||
await interaction.editReply({ content: 'Done! You\'ll be notified when this user is next seen active.' }); | ||
|
||
const channelBotlog = await interaction.guild.channels.fetch(env.CHANNEL_BOTLOG) as TextChannel; | ||
if (channelBotlog) { | ||
await channelBotlog.send(`${(interaction.member as GuildMember).displayName} used /watch on ${targetUser}`); | ||
} | ||
} else { | ||
// eslint-disable-next-line max-len | ||
await interaction.editReply({ content: 'Whoops, it seems like you\'re already watching this user! If you\'d like, you can cancel this or cancel and switch modes, by using /watch cancel.' }); | ||
} | ||
|
||
return true; | ||
}, | ||
}; | ||
|
||
export default dWatchUser; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import { | ||
TextChannel, | ||
Message, | ||
User, | ||
} from 'discord.js'; | ||
|
||
const F = f(__filename); | ||
|
||
interface WatchRequest { | ||
notification_method: string; | ||
channel_id: string | null; | ||
caller_id: string; | ||
watched_user_id: string; | ||
} | ||
|
||
type DbWatchRequest = { | ||
id?: string; | ||
notification_method: string; | ||
channel_id: string | null; | ||
caller_id: string; | ||
watched_user_id: string; | ||
usersId?: string; | ||
}; | ||
|
||
export async function dbAddWatchRequest( | ||
targetUserId: string, | ||
watchRequests: WatchRequest[], | ||
) { | ||
const existingUser = await db.users.findUnique({ | ||
where: { | ||
discord_id: targetUserId, | ||
}, | ||
include: { | ||
watch_requests: true, | ||
}, | ||
}); | ||
|
||
if (existingUser) { | ||
// Add new watch requests to the existing user's watch_requests array | ||
const latestRequest = watchRequests[watchRequests.length - 1]; | ||
|
||
// We can also do the same thing by adding these directly to watch_requests on the User table. | ||
return db.watch_request.create({ | ||
data: { | ||
notification_method: latestRequest.notification_method, | ||
channel_id: latestRequest.channel_id, | ||
caller_id: latestRequest.caller_id, | ||
watched_user_id: latestRequest.watched_user_id, | ||
usersId: existingUser.id, | ||
}, | ||
}); | ||
} | ||
// User not found | ||
throw new Error(`User with ID ${targetUserId} not found.`); | ||
} | ||
|
||
export async function dbDeleteWatchRequest(targetUserId: string, callerId: string): Promise<void> { | ||
const user = await db.users.findUnique({ | ||
where: { discord_id: targetUserId }, | ||
include: { watch_requests: true }, | ||
}); | ||
|
||
if (!user) { | ||
log.info(F, `User with ID ${targetUserId} does not exist.`); | ||
return; | ||
} | ||
|
||
const watchRequests = user.watch_requests as DbWatchRequest[]; | ||
|
||
// Find and delete the request related to the caller | ||
const requestToDelete = watchRequests.find(watchRequestObj => watchRequestObj.caller_id === callerId); | ||
if (requestToDelete) { | ||
await db.watch_request.delete({ | ||
where: { id: requestToDelete.id }, | ||
}); | ||
} else { | ||
log.info(F, `No watch request found for caller ${callerId}.`); | ||
} | ||
} | ||
|
||
export async function deleteWatchRequest(targetUserId: string, callerId: string): Promise<boolean> { | ||
const user = await db.users.findUnique({ | ||
where: { discord_id: targetUserId }, | ||
include: { watch_requests: true }, | ||
}); | ||
|
||
if (!user) { | ||
return false; // User not found | ||
} | ||
|
||
const watchRequests = user.watch_requests; | ||
|
||
// Find the index of the watch request to delete | ||
const indexToDelete = watchRequests.findIndex(watchRequestObj => watchRequestObj.caller_id === callerId); | ||
if (indexToDelete === -1) { | ||
return false; // WatchRequest with callerId not found | ||
} | ||
// Delete the watch request from the array | ||
watchRequests.splice(indexToDelete, 1); | ||
|
||
await dbDeleteWatchRequest(targetUserId, callerId); | ||
return true; // Successfully deleted the watchRequest | ||
} | ||
|
||
export async function nightsWatch(message: Message) { | ||
const user = await db.users.findUnique({ | ||
where: { discord_id: message.author.id }, | ||
include: { watch_requests: true }, | ||
}); | ||
|
||
if (!user || !message.guild) return; | ||
|
||
const watchRequests = user.watch_requests; | ||
|
||
if (watchRequests.length === 0) { | ||
return; | ||
} | ||
|
||
watchRequests.forEach(async watchRequestObj => { | ||
const target = await message.client.users.fetch(user.discord_id as string) as User; | ||
|
||
if (watchRequestObj.notification_method === 'dm') { | ||
const caller = await message.client.users.fetch(watchRequestObj.caller_id); | ||
if (caller) { | ||
try { | ||
await caller.send( | ||
`Hey ${caller}, the user ${target} which you were watching has been active recently in ${message.channel}.`, | ||
); | ||
} catch (err) { | ||
log.info(F, 'Failed to fulfill Watch Request. Likely can\'t DM user.'); | ||
} | ||
|
||
await deleteWatchRequest(user.discord_id as string, caller.id); | ||
} | ||
} else if (watchRequestObj.notification_method === 'channel') { | ||
const tripsitGuild = await message.client.guilds.fetch(env.DISCORD_GUILD_ID); | ||
if (watchRequestObj.channel_id) { | ||
// eslint-disable-next-line max-len | ||
const notificationChannel = await tripsitGuild.channels.fetch(watchRequestObj.channel_id as string) as TextChannel; | ||
const caller = await message.client.users.fetch(watchRequestObj.caller_id) as User; | ||
try { | ||
await notificationChannel.send( | ||
`Hey ${caller}, the user ${target} which you were watching has been active recently in ${message.channel}.`, | ||
); | ||
} catch (err) { | ||
log.info(F, 'Failed to fulfill Watch Request. Notification sending failed.'); | ||
} | ||
await deleteWatchRequest(user.discord_id as string, caller.id); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
export async function executeWatch( | ||
target: User, | ||
notificationMethod: string, | ||
callerId: string, | ||
alertChannel: TextChannel | null = null, | ||
): Promise<boolean> { | ||
const user = await db.users.findUnique({ | ||
where: { discord_id: target.id }, | ||
include: { watch_requests: true }, | ||
}); | ||
|
||
if (user) { | ||
const watchRequests = user.watch_requests; | ||
|
||
// Check for duplicate request using array iteration | ||
const duplicateRequest = watchRequests.some(watchRequestObj => watchRequestObj.caller_id === callerId); | ||
|
||
if (duplicateRequest) { | ||
log.info(F, `Duplicate watch request found for callerId: ${callerId}`); | ||
return false; | ||
} | ||
|
||
// Add the new watch request | ||
watchRequests.push({ | ||
id: '', | ||
usersId: null, | ||
notification_method: notificationMethod, | ||
channel_id: alertChannel ? alertChannel.id : null, | ||
caller_id: callerId, | ||
watched_user_id: target.id, | ||
}); | ||
await dbAddWatchRequest(target.id, watchRequests); | ||
log.info(F, `New watch request added for callerId: ${callerId}`); | ||
return true; | ||
} | ||
|
||
// If user does not exist, return false | ||
log.info(F, `User ${target.id} not found.`); | ||
return false; | ||
} |
14 changes: 14 additions & 0 deletions
14
src/prisma/tripbot/migrations/20241225024135_watch_requests/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
-- CreateTable | ||
CREATE TABLE "watch_request" ( | ||
"id" UUID NOT NULL DEFAULT uuid_generate_v4(), | ||
"notification_method" TEXT NOT NULL, | ||
"channel_id" TEXT, | ||
"caller_id" TEXT NOT NULL, | ||
"watched_user_id" TEXT NOT NULL, | ||
"usersId" UUID, | ||
|
||
CONSTRAINT "watch_request_pkey" PRIMARY KEY ("id") | ||
); | ||
|
||
-- AddForeignKey | ||
ALTER TABLE "watch_request" ADD CONSTRAINT "watch_request_usersId_fkey" FOREIGN KEY ("usersId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters