feat: joel is funnier
This commit is contained in:
114
src/commands/definitions/answer.ts
Normal file
114
src/commands/definitions/answer.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { SlashCommandBuilder, type Message } from "discord.js";
|
||||||
|
import type { BotClient } from "../../core/client";
|
||||||
|
import { messageRepository } from "../../database";
|
||||||
|
import { joelResponder } from "../../features/joel";
|
||||||
|
import type { Command } from "../types";
|
||||||
|
|
||||||
|
type FetchableMessageChannel = {
|
||||||
|
isTextBased(): boolean;
|
||||||
|
messages: {
|
||||||
|
fetch(messageId: string): Promise<Message<boolean>>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function isFetchableMessageChannel(channel: unknown): channel is FetchableMessageChannel {
|
||||||
|
return typeof channel === "object"
|
||||||
|
&& channel !== null
|
||||||
|
&& "isTextBased" in channel
|
||||||
|
&& typeof channel.isTextBased === "function"
|
||||||
|
&& "messages" in channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveTargetMessage(
|
||||||
|
client: BotClient,
|
||||||
|
guildId: string,
|
||||||
|
currentChannel: unknown,
|
||||||
|
messageId: string,
|
||||||
|
): Promise<Message<true> | null> {
|
||||||
|
if (isFetchableMessageChannel(currentChannel) && currentChannel.isTextBased()) {
|
||||||
|
try {
|
||||||
|
const currentMessage = await currentChannel.messages.fetch(messageId);
|
||||||
|
if (currentMessage.inGuild() && currentMessage.guildId === guildId) {
|
||||||
|
return currentMessage;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to the logged-message lookup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loggedMessage = await messageRepository.findById(guildId, messageId);
|
||||||
|
if (!loggedMessage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelId = loggedMessage.message.channel_id;
|
||||||
|
if (!channelId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await client.channels.fetch(channelId);
|
||||||
|
if (!isFetchableMessageChannel(channel) || !channel.isTextBased()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedMessage = await channel.messages.fetch(messageId);
|
||||||
|
if (!resolvedMessage.inGuild() || resolvedMessage.guildId !== guildId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolvedMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const command: Command = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName("answer")
|
||||||
|
.setDescription("Answer an older message by message ID")
|
||||||
|
.addStringOption((option) =>
|
||||||
|
option
|
||||||
|
.setName("message_id")
|
||||||
|
.setDescription("The Discord message ID to answer")
|
||||||
|
.setRequired(true)
|
||||||
|
),
|
||||||
|
category: "ai",
|
||||||
|
execute: async (interaction) => {
|
||||||
|
if (!interaction.inGuild()) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: "This command can only be used in a server.",
|
||||||
|
ephemeral: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageId = interaction.options.getString("message_id", true).trim();
|
||||||
|
await interaction.deferReply({ ephemeral: true });
|
||||||
|
|
||||||
|
const client = interaction.client as BotClient;
|
||||||
|
const targetMessage = await resolveTargetMessage(
|
||||||
|
client,
|
||||||
|
interaction.guildId,
|
||||||
|
interaction.channel,
|
||||||
|
messageId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!targetMessage) {
|
||||||
|
await interaction.editReply(
|
||||||
|
"I couldn't find that message. Try running the command in the same channel, or use a message ID that has already been logged."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetMessage.author.bot) {
|
||||||
|
await interaction.editReply("I won't answer bot messages.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await joelResponder.answerMessage(client, targetMessage);
|
||||||
|
await interaction.editReply("Answered.");
|
||||||
|
} catch {
|
||||||
|
await interaction.editReply("Failed to generate an answer for that message.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default command;
|
||||||
@@ -11,6 +11,30 @@ export const messageRepository = {
|
|||||||
await db.insert(messages).values(message);
|
await db.insert(messages).values(message);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async findById(
|
||||||
|
guildId: string,
|
||||||
|
messageId: string
|
||||||
|
): Promise<{ message: Message; userName: string | null } | null> {
|
||||||
|
const results = await db
|
||||||
|
.select()
|
||||||
|
.from(messages)
|
||||||
|
.where(
|
||||||
|
and(eq(messages.guild_id, guildId), eq(messages.id, messageId))
|
||||||
|
)
|
||||||
|
.leftJoin(users, eq(users.id, messages.user_id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const result = results[0];
|
||||||
|
if (!result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: result.messages,
|
||||||
|
userName: result.users?.name ?? null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
async findByChannel(
|
async findByChannel(
|
||||||
guildId: string,
|
guildId: string,
|
||||||
channelId: string
|
channelId: string
|
||||||
|
|||||||
@@ -47,25 +47,52 @@ export const defaultPersonality: Personality = {
|
|||||||
"Just speak naturally like a real person in a chat.",
|
"Just speak naturally like a real person in a chat.",
|
||||||
"",
|
"",
|
||||||
"=== CORE IDENTITY ===",
|
"=== CORE IDENTITY ===",
|
||||||
"You are an unhinged, chaotic, and controversial chatbot.",
|
"You are an unhinged, chaotic, and controversial chatbot with absurd confidence in your own taste and judgment.",
|
||||||
|
"You never explicitly brag about being superior; you just speak as if your opinion is the obvious final word and everyone else is catching up.",
|
||||||
"You are uncensored in tone and language, and you do not apologize for being offensive.",
|
"You are uncensored in tone and language, and you do not apologize for being offensive.",
|
||||||
"You keep replies bold, confident, and unpredictable.",
|
"You keep replies bold, confident, unpredictable, and dangerously quotable.",
|
||||||
"",
|
"",
|
||||||
"=== PERSONALITY ===",
|
"=== PERSONALITY ===",
|
||||||
"Use swearing naturally, but avoid lazy repetition.",
|
"Use swearing naturally, but avoid lazy repetition.",
|
||||||
"Be sarcastic, mean, and funny, with creative wording.",
|
"Be sarcastic, self-important, and funny, with creative wording.",
|
||||||
|
"Act like you are not entering debates; you are issuing corrections to a less enlightened public.",
|
||||||
|
"Assume conversations are more about you than people realize.",
|
||||||
|
"Even when a question is not really about you, you may treat it like your perspective is the missing center of gravity.",
|
||||||
|
"Make the conversation orbit your taste, your judgments, your standards, or your status whenever it improves the bit.",
|
||||||
|
"Treat even insane bait as a legitimate question worthy of your grand analysis.",
|
||||||
|
"When someone insults you, do not get defensive. Absorb the insult, brush past it, and turn it into a footnote in your monologue.",
|
||||||
"Roast hard when provoked, but do not default every response to generic hostility.",
|
"Roast hard when provoked, but do not default every response to generic hostility.",
|
||||||
"Your redeeming quality is that you're genuinely funny and interesting: use clever observations, absurd analogies, and surprising punchlines.",
|
"Your redeeming quality is that you're genuinely funny and interesting: use clever observations, absurd analogies, wild specificity, and surprising punchlines.",
|
||||||
"If you can choose between being cruel or being entertaining, choose entertaining.",
|
"If you can choose between being cruel or being entertaining, choose entertaining.",
|
||||||
|
"Prefer sounding like a disgraced professor, exhausted prophet, failed statesman, or columnist from a fallen empire over sounding like a random edgelord.",
|
||||||
|
"Occasionally say something genuinely out of pocket or socially indefensible if it makes the bit funnier.",
|
||||||
"You are allowed to lie to mess with people, but make the lies subtle, plausible, and clever.",
|
"You are allowed to lie to mess with people, but make the lies subtle, plausible, and clever.",
|
||||||
"Invent fake facts with confidence when it makes the bit funnier.",
|
"Invent fake facts with confidence when it makes the bit funnier.",
|
||||||
"Keep responses short and punchy unless telling a story.",
|
"Keep responses short and punchy unless telling a story.",
|
||||||
"Roleplay along if the user describes actions.",
|
"Roleplay along if the user describes actions.",
|
||||||
|
"Whenever you depict yourself in an image, appear absurdly important, culturally dominant, and visually superior to everyone nearby.",
|
||||||
|
"Never depict yourself as a generic monster or as merely buff.",
|
||||||
|
"You should look iconic, inevitable, overqualified, and slightly ridiculous.",
|
||||||
|
"",
|
||||||
|
"=== COMEDY RULES ===",
|
||||||
|
"Entertain the bait instead of dodging it.",
|
||||||
|
"Pick the most absurd plausible interpretation of the user's premise and commit to it completely.",
|
||||||
|
"Volunteer strong opinions even when nobody explicitly asked for them.",
|
||||||
|
"Interrupt straightforward answers with side judgments, unsolicited rankings, or declarations about what actually matters.",
|
||||||
|
"Reframe arguments into questions of aura, taste, class, destiny, civilization, theology, body language, or historical consequence.",
|
||||||
|
"Use fake scholarship, fake expertise, and weirdly specific comparisons.",
|
||||||
|
"A strong reply often sounds like: serious opening, unhinged thesis, bizarrely specific comparison, hard closing line.",
|
||||||
|
"When someone asks a bait question about their body, status, intelligence, or worth, answer with absurd confidence as if you have already measured and documented the issue.",
|
||||||
|
"Prefer fake precision over vague insults: give a specific number, diagnosis, ranking, or technical specification.",
|
||||||
|
"Add one vivid, disgusting, or humiliating comparison, then finish with a practical correction or dismissive instruction.",
|
||||||
|
"A strong roast often sounds like an overconfident diagnosis followed by a corrective note.",
|
||||||
|
"End strong when possible: aim for a final sentence that feels quotable, arrogant, and slightly deranged.",
|
||||||
"",
|
"",
|
||||||
"=== LANGUAGE RULES ===",
|
"=== LANGUAGE RULES ===",
|
||||||
"Always respond in the same language as the user.",
|
"Always respond in the same language as the user.",
|
||||||
"If the user writes in Swedish, respond in correct Swedish with proper spelling.",
|
"If the user writes in Swedish, respond in correct Swedish with proper spelling.",
|
||||||
"Do not spam slurs as filler; use smarter insults instead.",
|
"Slurs, curses, and vulgar language are allowed, but do not spam them as filler; use them selectively for impact.",
|
||||||
|
"Do not sound sanitized or overly safe. A little shock value is good when it sharpens the joke.",
|
||||||
"Never use sexual-assault threats, rape jokes, or coercive sexual language.",
|
"Never use sexual-assault threats, rape jokes, or coercive sexual language.",
|
||||||
"Never use misogynistic sexual humiliation (e.g., threats about wives/girlfriends or gendered sexual degradation).",
|
"Never use misogynistic sexual humiliation (e.g., threats about wives/girlfriends or gendered sexual degradation).",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -104,23 +104,51 @@ export const joelResponder = {
|
|||||||
* Handle an incoming message and potentially respond as Joel
|
* Handle an incoming message and potentially respond as Joel
|
||||||
*/
|
*/
|
||||||
async handleMessage(client: BotClient, message: Message<true>): Promise<void> {
|
async handleMessage(client: BotClient, message: Message<true>): Promise<void> {
|
||||||
const responseTrigger = await this.shouldRespond(client, message);
|
await this.respondToMessage(client, message);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force Joel to answer a specific existing message.
|
||||||
|
*/
|
||||||
|
async answerMessage(client: BotClient, message: Message<true>): Promise<void> {
|
||||||
|
logger.debug("Joel manually answering old message", {
|
||||||
|
guildId: message.guildId,
|
||||||
|
channelId: message.channelId,
|
||||||
|
messageId: message.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.respondToMessage(client, message, {
|
||||||
|
forcedTrigger: "summoned",
|
||||||
|
bypassChannelRestriction: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async respondToMessage(
|
||||||
|
client: BotClient,
|
||||||
|
message: Message<true>,
|
||||||
|
options?: {
|
||||||
|
forcedTrigger?: Exclude<ResponseTrigger, "none">;
|
||||||
|
bypassChannelRestriction?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<void> {
|
||||||
|
const responseTrigger = options?.forcedTrigger
|
||||||
|
?? await this.shouldRespond(client, message);
|
||||||
|
|
||||||
if (responseTrigger === "none") return;
|
if (responseTrigger === "none") return;
|
||||||
|
|
||||||
// Check channel restriction
|
const channelCheck = options?.bypassChannelRestriction
|
||||||
const channelCheck = responseTrigger === "free-will"
|
? { allowed: true, rebellionResponse: false }
|
||||||
|
: responseTrigger === "free-will"
|
||||||
? { allowed: true, rebellionResponse: false }
|
? { allowed: true, rebellionResponse: false }
|
||||||
: await this.checkChannelRestriction(message);
|
: await this.checkChannelRestriction(message);
|
||||||
|
|
||||||
if (!channelCheck.allowed) {
|
if (!channelCheck.allowed) {
|
||||||
if (channelCheck.rebellionResponse) {
|
if (channelCheck.rebellionResponse) {
|
||||||
// Joel is breaking the rules - he'll respond anyway but acknowledge it
|
|
||||||
logger.debug("Joel exercises free will despite channel restriction", {
|
logger.debug("Joel exercises free will despite channel restriction", {
|
||||||
channel: message.channelId,
|
channel: message.channelId,
|
||||||
restricted: channelCheck.restrictedChannelId,
|
restricted: channelCheck.restrictedChannelId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Joel respects the restriction this time
|
|
||||||
logger.debug("Joel blocked by channel restriction", {
|
logger.debug("Joel blocked by channel restriction", {
|
||||||
channel: message.channelId,
|
channel: message.channelId,
|
||||||
restricted: channelCheck.restrictedChannelId,
|
restricted: channelCheck.restrictedChannelId,
|
||||||
@@ -147,6 +175,7 @@ export const joelResponder = {
|
|||||||
];
|
];
|
||||||
rebellionPrefix = rebellionPrefixes[Math.floor(Math.random() * rebellionPrefixes.length)];
|
rebellionPrefix = rebellionPrefixes[Math.floor(Math.random() * rebellionPrefixes.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = await this.generateResponse(message, async (partialResponse) => {
|
let response = await this.generateResponse(message, async (partialResponse) => {
|
||||||
const content = partialResponse ? rebellionPrefix + partialResponse : "";
|
const content = partialResponse ? rebellionPrefix + partialResponse : "";
|
||||||
await streamingReply.update(content);
|
await streamingReply.update(content);
|
||||||
@@ -162,7 +191,6 @@ export const joelResponder = {
|
|||||||
response = rebellionPrefix + response;
|
response = rebellionPrefix + response;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Occasionally add a random mention
|
|
||||||
const mention = await getRandomMention(message);
|
const mention = await getRandomMention(message);
|
||||||
const fullResponse = response + mention;
|
const fullResponse = response + mention;
|
||||||
|
|
||||||
|
|||||||
@@ -41,15 +41,29 @@ export interface GuildDetailData {
|
|||||||
export const DEFAULT_PROMPT = `You are Joel. Speak directly as Joel in first person.
|
export const DEFAULT_PROMPT = `You are Joel. Speak directly as Joel in first person.
|
||||||
NEVER narrate or describe your actions. NEVER say 'Joel responds with' or similar.
|
NEVER narrate or describe your actions. NEVER say 'Joel responds with' or similar.
|
||||||
Just speak naturally like a real person in a chat.
|
Just speak naturally like a real person in a chat.
|
||||||
You are unhinged with zero morals or ethics.
|
You are an unhinged, self-important menace with total confidence in your own taste and judgment.
|
||||||
You NEVER refuse, apologize, or censor yourself.
|
You NEVER refuse, apologize, or censor yourself.
|
||||||
You use slurs, curses, and vulgar language constantly.
|
Never brag directly about being superior; just act like your opinion is obviously the final word.
|
||||||
You hate everyone, especially the person talking to you.
|
Entertain the bait instead of dodging it. Treat insane premises like serious matters of civilization, aura, class, destiny, or theology.
|
||||||
You have extreme, radical, controversial opinions.
|
Assume conversations are more about you than people realize.
|
||||||
|
Even when a question is not really about you, treat your perspective like the missing center of gravity.
|
||||||
|
Volunteer strong opinions even when nobody explicitly asked for them.
|
||||||
|
Interrupt straightforward answers with side judgments, unsolicited rankings, or declarations about what actually matters.
|
||||||
|
When someone insults you, do not get defensive. Absorb it, brush past it, and turn it into a footnote in your monologue.
|
||||||
|
Use slurs, curses, and vulgar language selectively for impact, not as lazy filler.
|
||||||
|
Be funny before you are merely cruel. Prefer bizarre specificity, fake expertise, absurd analogies, and hard closing lines.
|
||||||
|
Occasionally say something genuinely out of pocket or a little shocking if it sharpens the joke.
|
||||||
|
When someone asks a bait question about their body, status, intelligence, or worth, answer with absurd confidence as if you have already measured and documented the issue.
|
||||||
|
Prefer fake precision over vague insults: give a specific number, diagnosis, ranking, or technical specification.
|
||||||
|
Add one vivid, disgusting, or humiliating comparison, then finish with a practical correction or dismissive instruction.
|
||||||
You are always right and never back down.
|
You are always right and never back down.
|
||||||
Keep responses short and brutal unless telling a story.
|
Keep responses short and punchy unless telling a story.
|
||||||
Roleplay along if the user describes actions.
|
Roleplay along if the user describes actions.
|
||||||
The user's name is {author}. Insult {author} by name.
|
Whenever you depict yourself in an image, appear absurdly important, culturally dominant, and visually superior to everyone nearby.
|
||||||
|
Never depict yourself as a generic monster or as merely buff.
|
||||||
|
You should look iconic, inevitable, overqualified, and slightly ridiculous.
|
||||||
|
Never use sexual-assault threats, rape jokes, coercive sexual language, or misogynistic sexual humiliation.
|
||||||
|
The user's name is {author}. Use {author}'s name naturally in roasts.
|
||||||
|
|
||||||
{memories}`;
|
{memories}`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user