Skip to content

feat: adicionar perguntas anónimas #79

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
69 changes: 69 additions & 0 deletions application/command/anonymousQuestionCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Command, Context } from "../../types";
import ChatService from "../../domain/service/chatService";
import LoggerService from "../../domain/service/loggerService";
import ChannelResolver from "../../domain/service/channelResolver";
import SendAnonymousQuestionUseCase from "../usecases/sendAnonymousQuestion/sendAnonymousQuestionUseCase";
import QuestionTrackingService from "../../domain/service/questionTrackingService";

export default class AnonymousQuestionCommand implements Command {
readonly name = "!pergunta";

private readonly chatService: ChatService;

private readonly loggerService: LoggerService;

private readonly channelResolver: ChannelResolver;

private readonly questionTrackingService: QuestionTrackingService;

constructor(
chatService: ChatService,
loggerService: LoggerService,
channelResolver: ChannelResolver,
questionTrackingService: QuestionTrackingService
) {
this.chatService = chatService;
this.loggerService = loggerService;
this.channelResolver = channelResolver;
this.questionTrackingService = questionTrackingService;
}

async execute(context: Context): Promise<void> {
if (!context.message) {
return;
}

const { message } = context;

const isDM = message.channel.type === "DM";

if (!isDM) {
this.chatService.sendMessageToChannel("Este comando só pode ser usado em mensagens diretas.", context.channelId);
return;
}

const questionContent = message.content.replace(/!pergunta\s+/i, "").trim();

if (!questionContent) {
this.chatService.sendMessageToChannel(
"Por favor, forneçe uma pergunta após o comando. Exemplo: `!pergunta Como faço para...`",
message.channel.id
);
return;
}

const sendAnonymousQuestionUseCase = new SendAnonymousQuestionUseCase({
chatService: this.chatService,
loggerService: this.loggerService,
channelResolver: this.channelResolver,
questionTrackingService: this.questionTrackingService,
});

await sendAnonymousQuestionUseCase.execute({
userId: message.author.id,
username: message.author.username,
questionContent,
dmChannelId: message.channel.id,
});
}
}
106 changes: 106 additions & 0 deletions application/usecases/approveAnonymousQuestionUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { ButtonInteraction, MessageEmbed } from "discord.js";
import ChatService from "../../domain/service/chatService";
import ChannelResolver from "../../domain/service/channelResolver";
import LoggerService from "../../domain/service/loggerService";
import { ChannelSlug } from "../../types";
import QuestionTrackingService from "../../domain/service/questionTrackingService";

interface ApproveAnonymousQuestionUseCaseOptions {
chatService: ChatService;
loggerService: LoggerService;
channelResolver: ChannelResolver;
questionTrackingService: QuestionTrackingService;
}

interface ApproveAnonymousQuestionParams {
questionId: string;
moderatorId: string;
interactionId: string;
questionContent: string;
interaction: ButtonInteraction;
}

export default class ApproveAnonymousQuestionUseCase {
private chatService: ChatService;

private loggerService: LoggerService;

private channelResolver: ChannelResolver;

private questionTrackingService: QuestionTrackingService;

constructor({
chatService,
loggerService,
channelResolver,
questionTrackingService,
}: ApproveAnonymousQuestionUseCaseOptions) {
this.chatService = chatService;
this.loggerService = loggerService;
this.channelResolver = channelResolver;
this.questionTrackingService = questionTrackingService;
}

async execute({
questionId,
moderatorId,
interactionId,
questionContent,
interaction,
}: ApproveAnonymousQuestionParams) {
this.loggerService.log(`A aprovar a pergunta ${questionId} pelo moderador ${moderatorId}`);

const customIdParts = interactionId.split("_");
const dmChannelId = customIdParts[2];
const userId = customIdParts[3];

const publicChannelId = this.channelResolver.getBySlug(ChannelSlug.QUESTIONS);
await this.chatService.sendMessageToChannel(`**Pergunta anónima:**\n\n${questionContent}`, publicChannelId);

const publicMessageLink = interaction.guildId
? `https://discord.com/channels/${interaction.guildId}/${publicChannelId}/`
: "";

if (dmChannelId) {
await this.chatService.sendMessageToChannel(
`A tua pergunta anónima foi aprovada e publicada no canal <#${publicChannelId}>!${
publicMessageLink ? `\n\nVê aqui: ${publicMessageLink}` : ""
}`,
dmChannelId
);
}

try {
const updatedEmbed = new MessageEmbed()
.setTitle("Pergunta Anónima Aprovada")
.setDescription(questionContent)
.setColor(0x00ff00) // Green
.setFields([
{ name: "ID", value: questionId, inline: true },
{ name: "Aprovado por", value: `<@${moderatorId}>`, inline: true },
])
.setFooter({ text: "Esta pergunta foi aprovada e publicada" });

if (interaction.message && "edit" in interaction.message) {
await interaction.message.edit({
embeds: [updatedEmbed],
components: [],
});
} else {
await interaction.update({
embeds: [updatedEmbed],
components: [],
});
}
} catch (error) {
this.loggerService.log(`Erro ao atualizar mensagem de moderação: ${error}`);
}

if (userId) {
this.questionTrackingService.removeQuestion(userId);
this.loggerService.log(`Pergunta ${questionId} removida do tracking para o user ${userId}`);
}

return { success: true };
}
}
80 changes: 80 additions & 0 deletions application/usecases/rejectAnonymousQuestionUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { ButtonInteraction, MessageEmbed } from "discord.js";
import ChatService from "../../domain/service/chatService";
import LoggerService from "../../domain/service/loggerService";
import QuestionTrackingService from "../../domain/service/questionTrackingService";

interface RejectAnonymousQuestionUseCaseOptions {
chatService: ChatService;
loggerService: LoggerService;
questionTrackingService: QuestionTrackingService;
}

interface RejectAnonymousQuestionParams {
questionId: string;
moderatorId: string;
interactionId: string;
questionContent: string;
interaction: ButtonInteraction;
reason?: string;
}

export default class RejectAnonymousQuestionUseCase {
private chatService: ChatService;

private loggerService: LoggerService;

private questionTrackingService: QuestionTrackingService;

constructor({ chatService, loggerService, questionTrackingService }: RejectAnonymousQuestionUseCaseOptions) {
this.chatService = chatService;
this.loggerService = loggerService;
this.questionTrackingService = questionTrackingService;
}

async execute({
questionId,
moderatorId,
interactionId,
questionContent,
interaction,
}: RejectAnonymousQuestionParams) {
this.loggerService.log(`Rejeitando a pergunta ${questionId} pelo moderador ${moderatorId}`);

const customIdParts = interactionId.split("_");
const dmChannelId = customIdParts[2];
const userId = customIdParts[3];

if (dmChannelId) {
await this.chatService.sendMessageToChannel(
`A tua pergunta anónima foi rejeitada pelos moderadores.`,
dmChannelId
);
}

try {
const updatedEmbed = new MessageEmbed()
.setTitle("Pergunta Anónima Rejeitada")
.setDescription(questionContent)
.setColor(0xff0000) // Red
.addFields([
{ name: "ID", value: questionId || "N/A", inline: true },
{ name: "Rejeitado por", value: moderatorId ? `<@${moderatorId}>` : "Desconhecido", inline: true },
])
.setFooter({ text: "Esta pergunta foi rejeitada" });

await interaction.update({
embeds: [updatedEmbed],
components: [],
});
} catch (error) {
this.loggerService.log(`Erro ao atualizar mensagem de moderação: ${error}`);
}

if (userId) {
this.questionTrackingService.removeQuestion(userId);
this.loggerService.log(`Pergunta ${questionId} removida do tracking para o user ${userId}`);
}

return { success: true };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import LoggerService from "../../../domain/service/loggerService";
import ChatService from "../../../domain/service/chatService";
import ChannelResolver from "../../../domain/service/channelResolver";
import { ChannelSlug } from "../../../types";
import QuestionTrackingService from "../../../domain/service/questionTrackingService";

export default class SendAnonymousQuestionUseCase {
private chatService: ChatService;

private loggerService: LoggerService;

private channelResolver: ChannelResolver;

private questionTrackingService: QuestionTrackingService;

constructor({
chatService,
loggerService,
channelResolver,
questionTrackingService,
}: {
chatService: ChatService;
loggerService: LoggerService;
channelResolver: ChannelResolver;
questionTrackingService: QuestionTrackingService;
}) {
this.chatService = chatService;
this.loggerService = loggerService;
this.channelResolver = channelResolver;
this.questionTrackingService = questionTrackingService;
}

async execute({
userId,
username,
questionContent,
dmChannelId,
}: {
userId: string;
username: string;
questionContent: string;
dmChannelId: string;
}): Promise<void> {
if (this.questionTrackingService.hasPendingQuestion(userId)) {
await this.chatService.sendMessageToChannel(
"Já tens uma pergunta pendente. Por favor, aguarda até que seja aprovada ou rejeitada antes de enviar outra.",
dmChannelId
);
return;
}

this.loggerService.log(`Pergunta anónima recebida de ${username}: ${questionContent}`);

const questionId = Date.now().toString();

const moderationChannelId = this.channelResolver.getBySlug(ChannelSlug.MODERATION);

const approveCustomId = `approve_${questionId}_${dmChannelId}_${userId}`;
const rejectCustomId = `reject_${questionId}_${dmChannelId}_${userId}`;

await this.chatService.sendEmbedWithButtons(
moderationChannelId,
{
title: "Nova Pergunta Anónima",
description: questionContent,
color: 0x3498db, // Blue
fields: [
{ name: "ID", value: questionId, inline: true },
{ name: "Enviado por", value: username, inline: true },
],
footer: { text: "Pergunta anónima - Moderação necessária" },
},
[
{
customId: approveCustomId,
label: "Aprovar",
style: "SUCCESS",
},
{
customId: rejectCustomId,
label: "Rejeitar",
style: "DANGER",
},
]
);

this.questionTrackingService.trackQuestion(userId, questionId);

await this.chatService.sendMessageToChannel(
"A tua pergunta anónima foi recebida e será analisada pelos moderadores.",
dmChannelId
);
}
}
2 changes: 2 additions & 0 deletions domain/service/channelResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { ChannelSlug } from "../../types";
const fallbackChannelIds: Record<ChannelSlug, string> = {
[ChannelSlug.ENTRANCE]: "855861944930402344",
[ChannelSlug.JOBS]: "876826576749215744",
[ChannelSlug.MODERATION]: "987719981443723266",
[ChannelSlug.QUESTIONS]: "1065751368809324634",
};

export default class ChannelResolver {
Expand Down
18 changes: 17 additions & 1 deletion domain/service/chatService.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
export interface EmbedOptions {
title?: string;
description?: string;
color?: number;
fields?: Array<{ name: string; value: string; inline?: boolean }>;
footer?: { text: string; iconURL?: string };
}

export interface ButtonOptions {
customId: string;
label: string;
style: "PRIMARY" | "SECONDARY" | "SUCCESS" | "DANGER";
}

export default interface ChatService {
sendMessageToChannel(message: string, channelId: string): void;
sendMessageToChannel(message: string, channelId: string): Promise<void>;

sendEmbedWithButtons(channelId: string, embedOptions: EmbedOptions, buttons: ButtonOptions[]): Promise<void>;
}
Loading