├── .gitignore ├── src ├── main.ts ├── db │ ├── types │ │ ├── restriction.ts │ │ ├── locale.ts │ │ └── indicator.ts │ ├── schemas │ │ ├── error.ts │ │ ├── image.ts │ │ ├── description.ts │ │ ├── campaign.ts │ │ ├── index.ts │ │ ├── conversation.ts │ │ ├── schema.ts │ │ └── guild.ts │ ├── sub.ts │ ├── manager.ts │ └── managers │ │ ├── role.ts │ │ └── cache.ts ├── command │ ├── types │ │ ├── cooldown.ts │ │ ├── running.ts │ │ └── context.ts │ ├── response │ │ ├── notice.ts │ │ ├── premium.ts │ │ ├── loading.ts │ │ └── error.ts │ └── response.ts ├── image │ ├── types │ │ ├── sampler.ts │ │ ├── upscale.ts │ │ ├── prompt.ts │ │ ├── style.ts │ │ ├── image.ts │ │ └── model.ts │ └── utils │ │ └── merge.ts ├── chat │ ├── media │ │ ├── types │ │ │ ├── media.ts │ │ │ └── document.ts │ │ ├── handlers │ │ │ ├── document.ts │ │ │ └── image.ts │ │ └── handler.ts │ ├── types │ │ ├── embed.ts │ │ ├── button.ts │ │ ├── message.ts │ │ ├── options.ts │ │ └── model.ts │ ├── utils │ │ └── formatter.ts │ └── models │ │ ├── google.ts │ │ ├── openchat.ts │ │ ├── anthropic.ts │ │ └── llama.ts ├── turing │ ├── types │ │ ├── openai │ │ │ ├── plugins.ts │ │ │ ├── error.ts │ │ │ └── chat.ts │ │ ├── key.ts │ │ ├── transcribe.ts │ │ ├── llama.ts │ │ ├── upscale.ts │ │ ├── openchat.ts │ │ ├── vision.ts │ │ ├── google.ts │ │ └── anthropic.ts │ ├── connection │ │ ├── packets │ │ │ ├── vote.ts │ │ │ ├── update.ts │ │ │ └── campaigns.ts │ │ ├── packet │ │ │ └── packet.ts │ │ └── connection.ts │ ├── keys.ts │ └── dataset.ts ├── events │ ├── guildCreate.ts │ ├── guildDelete.ts │ ├── messageDelete.ts │ ├── messageCreate.ts │ ├── interactionCreate.ts │ └── ready.ts ├── event │ └── event.ts ├── util │ ├── emoji.ts │ ├── image.ts │ ├── git.ts │ ├── progressBar.ts │ ├── youtube.ts │ ├── logger.ts │ └── vote.ts ├── runpod │ └── models │ │ └── musicgen.ts ├── conversation │ ├── utils │ │ ├── reaction.ts │ │ ├── progress.ts │ │ ├── length.ts │ │ └── cooldown.ts │ └── settings │ │ └── plugin.ts ├── commands │ ├── premium.ts │ ├── context │ │ ├── translate.ts │ │ └── describe.ts │ ├── describe.ts │ ├── help.ts │ ├── status.ts │ ├── mod │ │ ├── error.ts │ │ ├── user.ts │ │ └── guild.ts │ ├── translate.ts │ ├── reset.ts │ ├── dev │ │ ├── maintenance.ts │ │ ├── eval.ts │ │ └── roles.ts │ ├── vote.ts │ ├── bot.ts │ └── settings.ts ├── error │ ├── db.ts │ ├── turing.ts │ ├── translation.ts │ ├── generation.ts │ ├── base.ts │ └── api.ts ├── interactions │ ├── settings.ts │ ├── metrics.ts │ ├── campaign.ts │ ├── api.ts │ ├── chat.ts │ ├── moderation.ts │ ├── imagine.ts │ ├── dataset.ts │ ├── general.ts │ └── premium.ts ├── moderation │ └── types │ │ └── infraction.ts ├── bot │ └── managers │ │ └── cache.ts ├── config.example.json ├── app.ts └── config.ts ├── assets ├── imagine │ ├── censored.png │ └── warning.png └── music │ └── background.png ├── tsconfig.json ├── .github └── workflows │ └── auto-deploy.yml ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | src/config.json -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./app.js"; 2 | 3 | const app: App = new App(); 4 | app.setup(); -------------------------------------------------------------------------------- /src/db/types/restriction.ts: -------------------------------------------------------------------------------- 1 | export type RestrictionType = "subscription" | "plan" | "premium" | "tester" -------------------------------------------------------------------------------- /assets/imagine/censored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuringAI-Team/chatgpt-discord-bot/HEAD/assets/imagine/censored.png -------------------------------------------------------------------------------- /assets/imagine/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuringAI-Team/chatgpt-discord-bot/HEAD/assets/imagine/warning.png -------------------------------------------------------------------------------- /assets/music/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TuringAI-Team/chatgpt-discord-bot/HEAD/assets/music/background.png -------------------------------------------------------------------------------- /src/command/types/cooldown.ts: -------------------------------------------------------------------------------- 1 | export interface CooldownData { 2 | /* When the cool-down was created */ 3 | createdAt: number; 4 | 5 | /* How long the cool-down lasts */ 6 | duration: number; 7 | } -------------------------------------------------------------------------------- /src/command/types/running.ts: -------------------------------------------------------------------------------- 1 | import { Snowflake } from "discord.js"; 2 | 3 | export interface RunningData { 4 | /* When the command started running */ 5 | since: number; 6 | 7 | /* Which channel ID the command is running in */ 8 | channel: Snowflake | null; 9 | } -------------------------------------------------------------------------------- /src/image/types/sampler.ts: -------------------------------------------------------------------------------- 1 | export const ImageSamplers = [ 2 | "k_euler", "k_heun", "k_lms", "k_euler_a", "k_dpm_2", "k_dpm_2_a", "k_dpm_fast", "k_dpm_adaptive", "k_dpmpp_2m", "k_dpmpp_2s_a", "k_dpmpp_sde" 3 | ] 4 | 5 | export type ImageSampler = typeof ImageSamplers[number] 6 | -------------------------------------------------------------------------------- /src/chat/media/types/media.ts: -------------------------------------------------------------------------------- 1 | export enum ChatMediaType { 2 | Images = "images", Documents = "documents" 3 | } 4 | 5 | export type ChatMedia = { 6 | /** Type of this attached media */ 7 | id: ChatMediaType; 8 | 9 | /** How much this media cost to process */ 10 | cost?: number; 11 | } & Record -------------------------------------------------------------------------------- /src/turing/types/openai/plugins.ts: -------------------------------------------------------------------------------- 1 | export type TuringChatPluginsToolResult = Record & { 2 | image?: string; 3 | } 4 | 5 | export interface TuringChatPluginsTool { 6 | name: string | null; 7 | input: Record | null; 8 | result: TuringChatPluginsToolResult | null; 9 | error: Record | null; 10 | } -------------------------------------------------------------------------------- /src/turing/types/key.ts: -------------------------------------------------------------------------------- 1 | export interface TuringAPIKey { 2 | id: string; 3 | name: string; 4 | createdAt: string; 5 | lastUsed: number; 6 | uses: number; 7 | } 8 | 9 | export interface TuringAPIKeyData { 10 | name: string; 11 | id: string; 12 | apiToken: string; 13 | captchaToken: string; 14 | createdAt: string; 15 | lastUsed: number; 16 | } -------------------------------------------------------------------------------- /src/db/schemas/error.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseImage } from "../../image/types/image.js"; 2 | import { type AppDatabaseManager } from "../app.js"; 3 | import { DatabaseSchema } from "./schema.js"; 4 | 5 | export class ErrorSchema extends DatabaseSchema { 6 | constructor(db: AppDatabaseManager) { 7 | super(db, { 8 | collection: "errors" 9 | }); 10 | } 11 | } -------------------------------------------------------------------------------- /src/db/schemas/image.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseImage } from "../../image/types/image.js"; 2 | import { type AppDatabaseManager } from "../app.js"; 3 | import { DatabaseSchema } from "./schema.js"; 4 | 5 | export class ImageSchema extends DatabaseSchema { 6 | constructor(db: AppDatabaseManager) { 7 | super(db, { 8 | collection: "images" 9 | }); 10 | } 11 | } -------------------------------------------------------------------------------- /src/events/guildCreate.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "../event/event.js"; 2 | import { Bot } from "../bot/bot.js"; 3 | 4 | export default class GuildCreateEvent extends Event { 5 | constructor(bot: Bot) { 6 | super(bot, "guildCreate"); 7 | } 8 | 9 | public async run(): Promise { 10 | await this.bot.db.metrics.changeGuildsMetric({ 11 | joins: "+1" 12 | }); 13 | } 14 | } -------------------------------------------------------------------------------- /src/image/types/upscale.ts: -------------------------------------------------------------------------------- 1 | import { ImageGenerationStatus, ImageRawResult } from "./image.js"; 2 | import { ImageBuffer } from "../../util/image.js"; 3 | 4 | export interface ImageUpscaleOptions { 5 | url: string; 6 | } 7 | 8 | export interface ImageUpscaleResult { 9 | results: ImageRawResult[]; 10 | id: string; 11 | cost: number; 12 | status: ImageGenerationStatus; 13 | error: null; 14 | } -------------------------------------------------------------------------------- /src/db/schemas/description.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseDescription } from "../../image/description.js"; 2 | import { type AppDatabaseManager } from "../app.js"; 3 | import { DatabaseSchema } from "./schema.js"; 4 | 5 | export class DescriptionSchema extends DatabaseSchema { 6 | constructor(db: AppDatabaseManager) { 7 | super(db, { 8 | collection: "descriptions" 9 | }); 10 | } 11 | } -------------------------------------------------------------------------------- /src/event/event.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable, ClientEvents } from "discord.js"; 2 | import { Bot } from "../bot/bot.js"; 3 | 4 | export class Event { 5 | public readonly name: string; 6 | protected readonly bot: Bot; 7 | 8 | constructor(bot: Bot, name: keyof ClientEvents) { 9 | this.name = name; 10 | this.bot = bot; 11 | } 12 | 13 | /* Function to execute when the event has been emitted */ 14 | public run(...args: any[]): Awaitable { 15 | /* Stub */ 16 | } 17 | } -------------------------------------------------------------------------------- /src/events/guildDelete.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from "discord.js"; 2 | 3 | import { Event } from "../event/event.js"; 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | export default class GuildDeleteEvent extends Event { 7 | constructor(bot: Bot) { 8 | super(bot, "guildDelete"); 9 | } 10 | 11 | public async run(guild: Guild): Promise { 12 | if (guild.available) await this.bot.db.metrics.changeGuildsMetric({ 13 | leaves: "+1" 14 | }); 15 | } 16 | } -------------------------------------------------------------------------------- /src/events/messageDelete.ts: -------------------------------------------------------------------------------- 1 | import { Message, PartialMessage } from "discord.js"; 2 | 3 | import { Event } from "../event/event.js"; 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | export default class MessageDeleteEvent extends Event { 7 | constructor(bot: Bot) { 8 | super(bot, "messageDelete"); 9 | } 10 | 11 | public async run(message: Message | PartialMessage): Promise { 12 | if (!message.partial) await this.bot.conversation.generator.handleDeletion(message); 13 | } 14 | } -------------------------------------------------------------------------------- /src/turing/types/transcribe.ts: -------------------------------------------------------------------------------- 1 | export interface TuringTranscribeBody { 2 | ai: "whisper" | "whisper-fast"; 3 | model: "tiny" | "base" | "small" | "medium"; 4 | url: string; 5 | } 6 | 7 | export interface TuringTranscribeSegment { 8 | text: string; 9 | } 10 | 11 | export interface TuringTranscribeRawResult { 12 | segments: TuringTranscribeSegment[]; 13 | } 14 | 15 | export interface TuringTranscribeResult { 16 | text: string; 17 | segments: TuringTranscribeSegment[]; 18 | } -------------------------------------------------------------------------------- /src/turing/types/openai/error.ts: -------------------------------------------------------------------------------- 1 | export type OpenAIErrorType = "server_error" | "requests" | "invalid_request_error" | "access_terminated" | "insufficient_quota"; 2 | 3 | export interface OpenAIErrorData { 4 | error: { 5 | /* Informative error message */ 6 | message: string; 7 | 8 | /* Type of the error */ 9 | type: OpenAIErrorType; 10 | 11 | /* TODO: Figure out what these fields do */ 12 | param: null; 13 | code: null; 14 | } 15 | } -------------------------------------------------------------------------------- /src/events/messageCreate.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "discord.js"; 2 | 3 | import { Event } from "../event/event.js"; 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | export default class MessageCreateEvent extends Event { 7 | constructor(bot: Bot) { 8 | super(bot, "messageCreate"); 9 | } 10 | 11 | public async run(message: Message): Promise { 12 | await this.bot.conversation.generator.handle({ 13 | message, 14 | content: message.content, 15 | author: message.author 16 | }); 17 | } 18 | } -------------------------------------------------------------------------------- /src/util/emoji.ts: -------------------------------------------------------------------------------- 1 | import { ComponentEmojiResolvable, EmojiIdentifierResolvable } from "discord.js"; 2 | 3 | type DisplayEmojiType = EmojiIdentifierResolvable | ComponentEmojiResolvable 4 | 5 | export interface DisplayEmoji { 6 | fallback: string; 7 | display?: any; 8 | } 9 | 10 | export class Emoji { 11 | public static display(emoji: DisplayEmoji, display: boolean = false): T { 12 | return display ? emoji.display ?? emoji.fallback : emoji.fallback; 13 | } 14 | } -------------------------------------------------------------------------------- /src/chat/types/embed.ts: -------------------------------------------------------------------------------- 1 | import { ColorResolvable } from "discord.js"; 2 | 3 | export interface ChatEmbed { 4 | /* Title of the embed */ 5 | title?: string; 6 | 7 | /* Description of the embed */ 8 | description?: string; 9 | 10 | /* Image URL of the embed */ 11 | image?: string; 12 | 13 | /* Color of the embed */ 14 | color?: ColorResolvable; 15 | 16 | /* Whether the current time should be shown in the footer */ 17 | time?: boolean; 18 | 19 | /* Text to display in the footer */ 20 | footer?: string; 21 | } -------------------------------------------------------------------------------- /src/command/types/context.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCommandType, ContextMenuCommandBuilder, ContextMenuCommandInteraction } from "discord.js"; 2 | 3 | import { Command, CommandOptions } from "../command.js"; 4 | import { Bot } from "../../bot/bot.js"; 5 | 6 | export abstract class ContextMenuCommand extends Command { 7 | constructor(bot: Bot, builder: ContextMenuCommandBuilder, options?: CommandOptions) { 8 | builder.setType(ApplicationCommandType.Message); 9 | super(bot, builder, options); 10 | } 11 | } -------------------------------------------------------------------------------- /src/chat/types/button.ts: -------------------------------------------------------------------------------- 1 | import { ButtonStyle, ComponentEmojiResolvable } from "discord.js"; 2 | 3 | export interface ChatButton { 4 | /* Label of the button */ 5 | label: string; 6 | 7 | /* Emoji of the button */ 8 | emoji?: ComponentEmojiResolvable; 9 | 10 | /* Style of the button */ 11 | style?: ButtonStyle; 12 | 13 | /* URL of the button, if applicable */ 14 | url?: string; 15 | 16 | /* Whether the button should be disabled */ 17 | disabled?: boolean; 18 | 19 | /* ID of the button */ 20 | id?: string; 21 | } -------------------------------------------------------------------------------- /src/runpod/models/musicgen.ts: -------------------------------------------------------------------------------- 1 | import { type ImageBuffer } from "../../util/image.js"; 2 | import { type RunPodResult } from "../api.js"; 3 | 4 | export type RunPodMusicGenModelName = "large" | "medium" 5 | 6 | export interface RunPodMusicGenInput { 7 | descriptions: string[]; 8 | duration: number; 9 | modelName: RunPodMusicGenModelName; 10 | } 11 | 12 | export interface RunPodMusicGenOutput { 13 | output: string[]; 14 | } 15 | 16 | export interface RunPodMusicGenResult { 17 | raw: RunPodResult; 18 | results: ImageBuffer[]; 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ "ESNext" ], 4 | "rootDirs": [ "./src" ], 5 | "outDir": "./build", 6 | "module": "esnext", 7 | "target": "esnext", 8 | "moduleResolution": "nodenext", 9 | "strict": true, 10 | "downlevelIteration": true, 11 | "skipLibCheck": true, 12 | "jsx": "preserve", 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "resolveJsonModule": true, 16 | "allowJs": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | } 20 | } -------------------------------------------------------------------------------- /src/turing/types/llama.ts: -------------------------------------------------------------------------------- 1 | import { type OpenAIChatMessage } from "./openai/chat.js"; 2 | 3 | export interface TuringLLaMABody { 4 | messages: LLaMAChatMessage[]; 5 | max_tokens?: number; 6 | temperature?: number; 7 | } 8 | 9 | export type LLaMAChatMessage = OpenAIChatMessage 10 | 11 | export type LLaMAStatus = "queued" | "generating" | "done" 12 | 13 | export interface LLaMAPartialChatResult { 14 | cost: number; 15 | result: string; 16 | id: string; 17 | status: LLaMAStatus | null; 18 | done: boolean; 19 | } 20 | 21 | export type LLaMAChatResult = LLaMAPartialChatResult -------------------------------------------------------------------------------- /src/db/sub.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseManager, DatabaseManagerBot } from "./manager.js"; 2 | import { type ClusterDatabaseManager } from "./cluster.js"; 3 | import { type AppDatabaseManager } from "./app.js"; 4 | 5 | export class SubDatabaseManager = DatabaseManager> { 6 | protected readonly db: T; 7 | 8 | constructor(db: T) { 9 | this.db = db; 10 | } 11 | } 12 | 13 | export class SubClusterDatabaseManager extends SubDatabaseManager {} 14 | export class SubAppDatabaseManager extends SubDatabaseManager {} 15 | -------------------------------------------------------------------------------- /src/turing/types/upscale.ts: -------------------------------------------------------------------------------- 1 | export type TuringUpscaleModel = "GFPGAN" | "RealESRGAN_x4plus" | "RealESRGAN_x2plus" | "RealESRGAN_x4plus_anime_6B" | "NMKD_Siax" | "4x_AnimeSharp" 2 | export type TuringUpscaleStatus = "done" | "generating" | "queued" 3 | 4 | export interface TuringUpscaleBody { 5 | upscaler: TuringUpscaleModel; 6 | image: string; 7 | } 8 | 9 | export interface TuringUpscaleResultImage { 10 | url: string; 11 | base64: string; 12 | } 13 | 14 | export interface TuringUpscaleResult { 15 | result: TuringUpscaleResultImage; 16 | cost: number; 17 | status: TuringUpscaleStatus; 18 | done: boolean; 19 | } -------------------------------------------------------------------------------- /src/conversation/utils/reaction.ts: -------------------------------------------------------------------------------- 1 | import { Message, Routes } from "discord.js"; 2 | 3 | import { Bot } from "../../bot/bot.js"; 4 | 5 | export class Reaction { 6 | public static async add(bot: Bot, message: Message, emoji: string): Promise { 7 | await bot.client.rest.put( 8 | Routes.channelMessageOwnReaction(message.channelId, message.id, encodeURIComponent(emoji)) 9 | ).catch(() => {}); 10 | } 11 | 12 | public static async remove(bot: Bot, message: Message, emoji: string): Promise { 13 | await bot.client.rest.delete( 14 | Routes.channelMessageOwnReaction(message.channelId, message.id, encodeURIComponent(emoji)) 15 | ).catch(() => {}); 16 | } 17 | } -------------------------------------------------------------------------------- /src/db/schemas/campaign.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseCampaign } from "../managers/campaign.js"; 2 | import { type AppDatabaseManager } from "../app.js"; 3 | import { DatabaseSchema } from "./schema.js"; 4 | 5 | export class CampaignSchema extends DatabaseSchema { 6 | constructor(db: AppDatabaseManager) { 7 | super(db, { 8 | collection: "campaigns" 9 | }); 10 | } 11 | 12 | public async process(campaign: DatabaseCampaign): Promise { 13 | campaign.budget = typeof campaign.budget === "object" ? campaign.budget : { total: 5, used: 0, type: "click", cost: 5 }; 14 | campaign.logs = Array.isArray(campaign.logs) ? campaign.logs : []; 15 | 16 | return campaign; 17 | } 18 | } -------------------------------------------------------------------------------- /src/turing/types/openchat.ts: -------------------------------------------------------------------------------- 1 | import { type OpenAIChatMessage } from "./openai/chat.js"; 2 | 3 | type OpenChatStopReason = "stop_sequence" | "max_tokens" 4 | export type OpenChatModel = "openchat_v3.2" | string 5 | 6 | export interface TuringOpenChatBody { 7 | model: OpenChatModel; 8 | messages: OpenChatMessage[]; 9 | max_tokens?: number; 10 | temperature?: number; 11 | stream?: boolean; 12 | } 13 | 14 | export type OpenChatMessage = OpenAIChatMessage 15 | 16 | export interface OpenChatPartialResult { 17 | result: string; 18 | cost: number; 19 | finishReason: null; 20 | done: boolean; 21 | } 22 | 23 | export interface OpenChatChatResult { 24 | result: string; 25 | cost: number; 26 | finishReason: OpenChatStopReason; 27 | done: true; 28 | } -------------------------------------------------------------------------------- /src/commands/premium.ts: -------------------------------------------------------------------------------- 1 | import { ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandResponse } from "../command/command.js"; 4 | import { DatabaseInfo } from "../db/managers/user.js"; 5 | import { Bot } from "../bot/bot.js"; 6 | 7 | export default class PremiumCommand extends Command { 8 | constructor(bot: Bot) { 9 | super(bot, 10 | new SlashCommandBuilder() 11 | .setName("premium") 12 | .setDescription("View information about Premium & your current subscription") 13 | ); 14 | } 15 | 16 | public async run(interaction: ChatInputCommandInteraction, db: DatabaseInfo): CommandResponse { 17 | return await this.bot.db.plan.buildOverview(interaction, db); 18 | } 19 | } -------------------------------------------------------------------------------- /src/command/response/notice.ts: -------------------------------------------------------------------------------- 1 | import { ColorResolvable } from "discord.js"; 2 | import { Response } from "../response.js"; 3 | 4 | interface NoticeResponseOptions { 5 | /* Color of the embed */ 6 | color: ColorResolvable; 7 | 8 | /* Message for the embed */ 9 | message: string; 10 | 11 | /* Footer of the embed; optional */ 12 | footer?: string; 13 | } 14 | 15 | export class NoticeResponse extends Response { 16 | constructor(options: NoticeResponseOptions) { 17 | super(); 18 | 19 | this.addEmbed(builder => builder 20 | .setDescription(options.message) 21 | .setFooter(options.footer ? { text: options.footer } : null) 22 | .setColor(options.color) 23 | ); 24 | 25 | this.setEphemeral(true); 26 | } 27 | } -------------------------------------------------------------------------------- /src/turing/types/vision.ts: -------------------------------------------------------------------------------- 1 | export type TuringVisionModel = "blip2" | "ocr" 2 | 3 | export interface TuringVisionBody { 4 | model: TuringVisionModel[]; 5 | image: string; 6 | } 7 | 8 | export interface TuringVisionResult { 9 | description: string; 10 | lines: ImageOCRLine[] | null; 11 | text: string | null; 12 | cost: number; 13 | done: boolean; 14 | } 15 | 16 | export interface ImageOCRLine { 17 | text: string; 18 | words: ImageOCRWord[]; 19 | maxHeight: number; 20 | minTop: number; 21 | } 22 | 23 | export interface ImageOCRWord { 24 | text: string; 25 | left: number; 26 | top: number; 27 | width: number; 28 | height: number; 29 | } 30 | 31 | export interface ImageOCRResult { 32 | content: string; 33 | lines: ImageOCRLine[]; 34 | } -------------------------------------------------------------------------------- /src/turing/types/google.ts: -------------------------------------------------------------------------------- 1 | export interface TuringGoogleChatBody { 2 | model: "chat-bison" | string; 3 | messages: GoogleChatMessage[]; 4 | max_tokens?: number; 5 | temperature?: number; 6 | } 7 | 8 | export interface GoogleChatMessage { 9 | role: "system" | "user" | "bot"; 10 | content: string; 11 | } 12 | 13 | export interface GoogleChatSafetyAttribute { 14 | blocked: boolean; 15 | categories: string[]; 16 | scores: number[]; 17 | } 18 | 19 | export interface GoogleChatCandidate { 20 | author: "1"; 21 | content: string; 22 | } 23 | 24 | export interface GoogleChatPrediction { 25 | safetyAttributes: [ GoogleChatSafetyAttribute ]; 26 | candidates: [ GoogleChatCandidate ]; 27 | } 28 | 29 | export interface GoogleChatResult { 30 | predictions: [ GoogleChatPrediction ]; 31 | } -------------------------------------------------------------------------------- /src/turing/types/anthropic.ts: -------------------------------------------------------------------------------- 1 | import { type OpenAIChatMessage } from "./openai/chat.js"; 2 | 3 | type AnthropicStopReason = "stop_sequence" | "max_tokens" 4 | export type AnthropicChatModel = "claude-instant-1" | "claude-instant-1-100k" | "claude-2" | string 5 | 6 | export interface TuringAnthropicChatBody { 7 | model: AnthropicChatModel; 8 | messages: AnthropicChatMessage[]; 9 | max_tokens?: number; 10 | temperature?: number; 11 | stream?: boolean; 12 | } 13 | 14 | export type AnthropicChatMessage = OpenAIChatMessage 15 | 16 | export interface AnthropicPartialChatResult { 17 | result: string; 18 | stop_reason: null; 19 | stop: null; 20 | done: false; 21 | } 22 | 23 | export interface AnthropicChatResult { 24 | result: string; 25 | stop_reason: AnthropicStopReason; 26 | stop: string | null; 27 | done: true; 28 | } -------------------------------------------------------------------------------- /src/db/schemas/index.ts: -------------------------------------------------------------------------------- 1 | import { type DatabaseSchema } from "./schema.js"; 2 | 3 | import { ConversationSchema } from "./conversation.js"; 4 | import { DescriptionSchema } from "./description.js"; 5 | import { CampaignSchema } from "./campaign.js"; 6 | import { ErrorSchema } from "./error.js"; 7 | import { GuildSchema } from "./guild.js"; 8 | import { ImageSchema } from "./image.js"; 9 | import { UserSchema } from "./user.js"; 10 | 11 | export type DatabaseSchemaMap = { 12 | users: UserSchema, 13 | conversations: ConversationSchema, 14 | descriptions: DescriptionSchema, 15 | errors: ErrorSchema, 16 | guilds: GuildSchema, 17 | images: ImageSchema, 18 | interactions: DatabaseSchema, 19 | campaigns: CampaignSchema 20 | } 21 | 22 | export const DatabaseSchemas = [ 23 | UserSchema, GuildSchema, ImageSchema, ConversationSchema, DescriptionSchema, ErrorSchema, CampaignSchema 24 | ] -------------------------------------------------------------------------------- /src/util/image.ts: -------------------------------------------------------------------------------- 1 | export class ImageBuffer { 2 | private data: Buffer; 3 | 4 | constructor(data: Buffer) { 5 | this.data = data; 6 | } 7 | 8 | public static from(buffer: ArrayBuffer): ImageBuffer { 9 | return new ImageBuffer(Buffer.from(buffer)); 10 | } 11 | 12 | /** 13 | * Create a new image buffer from the specified Base64 string. 14 | * @param data Base64 string to convert into a buffer 15 | * @returns 16 | */ 17 | public static load(data: string): ImageBuffer { 18 | return new ImageBuffer( Buffer.from(data, "base64")); 19 | } 20 | 21 | /** 22 | * Convert the image buffer into a Base64 string. 23 | * @returns Base64-encoded image data 24 | */ 25 | public toString(): string { 26 | return this.data.toString("base64"); 27 | } 28 | 29 | public get buffer(): Buffer { 30 | return this.data; 31 | } 32 | } -------------------------------------------------------------------------------- /src/image/types/prompt.ts: -------------------------------------------------------------------------------- 1 | export interface ImagePrompt { 2 | /** Things to include in the image */ 3 | prompt: string; 4 | 5 | /** Things to *not* include in the image */ 6 | negative?: string; 7 | 8 | /** Which filter was used */ 9 | style?: string; 10 | 11 | /* Mode used for the prompt */ 12 | mode?: string; 13 | 14 | /* Original prompt, if an enhancer was used */ 15 | original?: string; 16 | } 17 | 18 | export interface ImagePromptEnhancer { 19 | /** Name of this enhancer */ 20 | name: string; 21 | 22 | /** Emoji of this enhancer */ 23 | emoji: string; 24 | 25 | /** ID of this enhancer */ 26 | id: string; 27 | } 28 | 29 | export const ImagePromptEnhancers: ImagePromptEnhancer[] = [ 30 | { 31 | name: "Don't do anything", emoji: "⛔", id: "none" 32 | }, 33 | 34 | { 35 | name: "Improve my prompt", emoji: "✨", id: "improve" 36 | } 37 | ] -------------------------------------------------------------------------------- /src/error/db.ts: -------------------------------------------------------------------------------- 1 | import { PostgrestError } from "@supabase/supabase-js"; 2 | import { StorageError } from "@supabase/storage-js"; 3 | 4 | import { DatabaseCollectionType } from "../db/manager.js"; 5 | import { GPTError, GPTErrorType } from "./base.js"; 6 | 7 | export interface GPTDatabaseErrorOptions { 8 | collection: DatabaseCollectionType; 9 | raw: PostgrestError | StorageError; 10 | } 11 | 12 | export class GPTDatabaseError extends GPTError { 13 | constructor(opts: GPTDatabaseErrorOptions) { 14 | super({ 15 | type: GPTErrorType.Other, 16 | data: opts 17 | }); 18 | } 19 | 20 | /** 21 | * Convert the error into a readable error message. 22 | * @returns Human-readable error message 23 | */ 24 | public toString(): string { 25 | return `Failed to perform database operation on collection '${this.options.data.collection}' with error message "${this.options.data.raw.message}"`; 26 | } 27 | } -------------------------------------------------------------------------------- /src/commands/context/translate.ts: -------------------------------------------------------------------------------- 1 | import { ContextMenuCommandBuilder, MessageContextMenuCommandInteraction } from "discord.js"; 2 | 3 | import { ContextMenuCommand } from "../../command/types/context.js"; 4 | import { TranslationCooldown } from "../../util/translate.js"; 5 | import { CommandResponse } from "../../command/command.js"; 6 | import { DatabaseInfo } from "../../db/managers/user.js"; 7 | import { Bot } from "../../bot/bot.js"; 8 | 9 | export default class TranslateContentContextMenuCommand extends ContextMenuCommand { 10 | constructor(bot: Bot) { 11 | super(bot, new ContextMenuCommandBuilder() 12 | .setName("Translate") 13 | , { 14 | cooldown: TranslationCooldown 15 | }); 16 | } 17 | 18 | public async run(interaction: MessageContextMenuCommandInteraction, db: DatabaseInfo): CommandResponse { 19 | return this.bot.translation.run({ 20 | content: interaction.targetMessage.content, interaction, db, original: interaction.targetMessage 21 | }); 22 | } 23 | } -------------------------------------------------------------------------------- /src/util/git.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | 3 | export interface GitCommit { 4 | message: string; 5 | hash: string; 6 | } 7 | 8 | export class Git { 9 | private static async exec(command: string): Promise { 10 | return new Promise((resolve, reject) => { 11 | exec(command, (error, output) => { 12 | if (error !== null) return reject(error); 13 | resolve(output.toString().trim()); 14 | }); 15 | }); 16 | } 17 | 18 | private static async latestCommitMessage(): Promise { 19 | return this.exec(`git log -1 --format="%s"`); 20 | } 21 | 22 | private static async latestCommitHash(): Promise { 23 | return this.exec("git rev-parse HEAD"); 24 | } 25 | 26 | public static async latestCommit(): Promise { 27 | return { 28 | message: await this.latestCommitMessage(), 29 | hash: await this.latestCommitHash() 30 | }; 31 | } 32 | } -------------------------------------------------------------------------------- /src/commands/context/describe.ts: -------------------------------------------------------------------------------- 1 | import { ContextMenuCommandBuilder, MessageContextMenuCommandInteraction } from "discord.js"; 2 | 3 | import { ContextMenuCommand } from "../../command/types/context.js"; 4 | import { CommandResponse } from "../../command/command.js"; 5 | import { DatabaseInfo } from "../../db/managers/user.js"; 6 | import { Bot } from "../../bot/bot.js"; 7 | 8 | export default class DescribeImageContextMenuCommand extends ContextMenuCommand { 9 | constructor(bot: Bot) { 10 | super(bot, new ContextMenuCommandBuilder() 11 | .setName("Describe image") 12 | , { 13 | cooldown: { 14 | free: 3 * 60 * 1000, 15 | voter: 2 * 60 * 1000, 16 | subscription: 30 * 1000 17 | }, 18 | 19 | synchronous: true 20 | }); 21 | } 22 | 23 | public async run(interaction: MessageContextMenuCommandInteraction, db: DatabaseInfo): CommandResponse { 24 | return this.bot.description.run(db, interaction); 25 | } 26 | } -------------------------------------------------------------------------------- /src/interactions/settings.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction, StringSelectMenuInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | export class SettingsInteractionHandler extends InteractionHandler { 7 | constructor(bot: Bot) { 8 | super( 9 | bot, 10 | 11 | new InteractionHandlerBuilder() 12 | .setName("settings") 13 | .setDescription("Change & view settings") 14 | .setType([ InteractionType.Button, InteractionType.StringSelectMenu ]) 15 | ); 16 | } 17 | 18 | public async run({ raw, interaction, db }: InteractionHandlerRunOptions): InteractionHandlerResponse { 19 | return this.bot.db.settings.handleInteraction(interaction, db, raw); 20 | } 21 | } -------------------------------------------------------------------------------- /src/command/response/premium.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "../response.js"; 2 | 3 | export enum PremiumUpsellType { 4 | /** /imagine */ 5 | ImagineSize, ImagineSteps 6 | } 7 | 8 | const PremiumUpsells: Record = { 9 | [PremiumUpsellType.ImagineSteps]: "**Premium** increases the maximum amount of steps you can use for image generation", 10 | [PremiumUpsellType.ImagineSize]: "**Premium** allows you to generate way bigger images", 11 | } 12 | 13 | interface PremiumUpsellResponseOptions { 14 | /* Which upsell to display */ 15 | type: PremiumUpsellType; 16 | } 17 | 18 | export class PremiumUpsellResponse extends Response { 19 | constructor(options: PremiumUpsellResponseOptions) { 20 | super(); 21 | 22 | this.addEmbed(builder => builder 23 | .setDescription(`${PremiumUpsells[options.type]}. **Premium ✨** also gives you many additional benefits; view \`/premium\` for more.`) 24 | .setColor("Orange") 25 | ); 26 | 27 | this.setEphemeral(true); 28 | } 29 | } -------------------------------------------------------------------------------- /src/interactions/metrics.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import MetricsCommand from "../commands/dev/metrics.js"; 5 | import { Bot } from "../bot/bot.js"; 6 | 7 | export class MetricsInteractionHandler extends InteractionHandler { 8 | constructor(bot: Bot) { 9 | super( 10 | bot, 11 | 12 | new InteractionHandlerBuilder() 13 | .setName("metrics") 14 | .setDescription("Metrics viewer actions (page switching, changing time frame, etc.)") 15 | .setType([ InteractionType.Button ]) 16 | ); 17 | } 18 | 19 | public async run({ raw, interaction, db }: InteractionHandlerRunOptions): InteractionHandlerResponse { 20 | return this.bot.command.get("metrics").handleInteraction(interaction, db, raw); 21 | } 22 | } -------------------------------------------------------------------------------- /src/events/interactionCreate.ts: -------------------------------------------------------------------------------- 1 | import { Interaction, ChatInputCommandInteraction, AutocompleteInteraction } from "discord.js"; 2 | 3 | import { Event } from "../event/event.js"; 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | export default class InteractionCreateEvent extends Event { 7 | constructor(bot: Bot) { 8 | super(bot, "interactionCreate"); 9 | } 10 | 11 | public async run(interaction: Interaction): Promise { 12 | if (!interaction || ((interaction.isButton() || interaction.isAnySelectMenu()) && !interaction.customId)) return; 13 | 14 | try { 15 | if (interaction.isChatInputCommand() || interaction.isMessageContextMenuCommand()) { 16 | await this.bot.command.handleCommand(interaction as ChatInputCommandInteraction); 17 | 18 | } else if (interaction.isStringSelectMenu() || interaction.isButton() || interaction.isModalSubmit()) { 19 | await this.bot.interaction.handleInteraction(interaction); 20 | } 21 | 22 | } catch (error) { 23 | await this.bot.error.handle({ 24 | error, title: "Failed to process interaction" 25 | }); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/commands/describe.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../command/command.js"; 4 | import { DatabaseInfo } from "../db/managers/user.js"; 5 | import { Bot } from "../bot/bot.js"; 6 | 7 | export default class DescribeCommand extends Command { 8 | constructor(bot: Bot) { 9 | super(bot, 10 | new SlashCommandBuilder() 11 | .setName("describe") 12 | .setDescription("Describe an image using AI") 13 | .addAttachmentOption(builder => builder 14 | .setName("image") 15 | .setDescription("Which image to describe") 16 | .setRequired(true) 17 | ) 18 | , { 19 | cooldown: { 20 | free: 3 * 60 * 1000, 21 | voter: 2 * 60 * 1000, 22 | subscription: 30 * 1000 23 | }, 24 | 25 | synchronous: true 26 | }); 27 | } 28 | 29 | public async run(interaction: CommandInteraction, db: DatabaseInfo): CommandResponse { 30 | return this.bot.description.run(db, interaction); 31 | } 32 | } -------------------------------------------------------------------------------- /.github/workflows/auto-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to SSH VPS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # Checkout code from the main branch 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | 17 | # Connect to remote server via SSH to pull all changes and re-build the bot 18 | - name: Deploy bot 19 | uses: appleboy/ssh-action@master 20 | with: 21 | host: ${{ secrets.HOST }} 22 | username: ${{ secrets.USERNAME }} 23 | password: ${{ secrets.PASSWORD }} 24 | script: | 25 | export NVM_DIR="$HOME/.nvm" 26 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 27 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" 28 | cd /home/trident/github/chatgpt-bot 29 | nvm use 20 30 | PATH="$HOME/.nvm/versions/node/v20.0.0/bin:$PATH" 31 | npm run git 32 | npm i 33 | rm -r dist 34 | npm run build -------------------------------------------------------------------------------- /src/commands/help.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { IntroductionPage, IntroductionPages, Introduction } from "../util/introduction.js"; 4 | import { Command, CommandInteraction, CommandResponse } from "../command/command.js"; 5 | import { Bot } from "../bot/bot.js"; 6 | 7 | export default class HelpCommand extends Command { 8 | constructor(bot: Bot) { 9 | super(bot, new SlashCommandBuilder() 10 | .setName("help") 11 | .setDescription("Look at various help information for the bot") 12 | .addIntegerOption(builder => builder 13 | .setName("page") 14 | .setDescription("Which page to view") 15 | .setRequired(false) 16 | .addChoices(...IntroductionPages.map(page => ({ 17 | name: `${page.design.emoji} ${page.design.title} [#${page.index +1}]`, 18 | value: page.index 19 | })))) 20 | , { always: true }); 21 | } 22 | 23 | public async run(interaction: CommandInteraction): CommandResponse { 24 | const page: IntroductionPage = Introduction.at(interaction.options.getInteger("page") ?? 0); 25 | return Introduction.buildPage(this.bot, interaction.user, page); 26 | } 27 | } -------------------------------------------------------------------------------- /src/turing/connection/packets/vote.ts: -------------------------------------------------------------------------------- 1 | import { Snowflake } from "discord.js"; 2 | 3 | import { Packet, PacketDirection, PacketSendOptions } from "../packet/packet.js"; 4 | import { TuringConnectionManager } from "../connection.js"; 5 | import { DatabaseUser } from "../../../db/schemas/user.js"; 6 | 7 | type VotePacketIncomingData = Snowflake 8 | 9 | export class VotePacket extends Packet { 10 | constructor(manager: TuringConnectionManager) { 11 | super(manager, { 12 | name: "vote", 13 | direction: PacketDirection.Incoming 14 | }); 15 | } 16 | 17 | public async handle(id: VotePacketIncomingData): Promise { 18 | /* Find the user's database entry. */ 19 | const existing: DatabaseUser | null = await this.manager.app.db.fetchFromCacheOrDatabase("users", id); 20 | if (existing === null) return; 21 | 22 | this.manager.app.db.metrics.change("vote", { 23 | count: "+1" 24 | }); 25 | 26 | await this.manager.app.db.queue.update("users", existing, { 27 | voted: new Date().toISOString() 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/error/turing.ts: -------------------------------------------------------------------------------- 1 | import { GPTError, GPTErrorType } from "./base.js"; 2 | import { GPTAPIErrorOptions } from "./api.js"; 3 | 4 | export interface TuringErrorBody { 5 | success: boolean; 6 | error: TuringErrorData; 7 | } 8 | 9 | export type TuringErrorData = T 10 | 11 | export type TuringErrorOptions = Pick & { 12 | body: TuringErrorBody | null; 13 | } 14 | 15 | export class TuringAPIError extends GPTError> { 16 | constructor(opts: TuringErrorOptions) { 17 | super({ 18 | data: opts, type: GPTErrorType.API 19 | }); 20 | } 21 | 22 | public get data(): TuringErrorData | null { 23 | return this.options.data.body ? this.options.data.body.error : null; 24 | } 25 | 26 | /** 27 | * Convert the error into a readable error message. 28 | * @returns Human-readable error message 29 | */ 30 | public toString(): string { 31 | return `Failed to request API endpoint ${this.options.data.endpoint} with status code ${this.options.data.code}${typeof this.data !== "object" ? `: ${this.data}` : ""}`; 32 | } 33 | } -------------------------------------------------------------------------------- /src/turing/connection/packets/update.ts: -------------------------------------------------------------------------------- 1 | import { Packet, PacketDirection, PacketSendOptions } from "../packet/packet.js"; 2 | import { DatabaseCollectionType } from "../../../db/manager.js"; 3 | import { TuringConnectionManager } from "../connection.js"; 4 | 5 | interface UpdatePacketIncomingData { 6 | /** Name of the collection to update */ 7 | collection: DatabaseCollectionType; 8 | 9 | /* ID of the entry to update */ 10 | id: string; 11 | 12 | /* Updates to apply */ 13 | updates: Record; 14 | } 15 | 16 | export class UpdatePacket extends Packet { 17 | constructor(manager: TuringConnectionManager) { 18 | super(manager, { 19 | name: "update", 20 | direction: PacketDirection.Incoming 21 | }); 22 | } 23 | 24 | public async handle({ collection, id, updates }: UpdatePacketIncomingData): Promise { 25 | /* Make sure that the entry exists, before updating it. */ 26 | const existing = await this.manager.app.db.fetchFromCacheOrDatabase(collection, id); 27 | if (existing === null) return; 28 | 29 | await this.manager.app.db.queue.update(collection, existing, updates); 30 | } 31 | } -------------------------------------------------------------------------------- /src/chat/utils/formatter.ts: -------------------------------------------------------------------------------- 1 | interface MessageFormatter { 2 | /* Identifier of the formatter */ 3 | name: string; 4 | 5 | /* Function to execute to get this formatter's result */ 6 | execute: (content: string) => string | null; 7 | } 8 | 9 | /* Formatters to execute */ 10 | const formatters: MessageFormatter[] = [ 11 | { 12 | name: "Fix broken code blocks", 13 | execute: content => content.split("```").length % 2 === 0 ? `${content}\n\`\`\`` : null 14 | } 15 | ] 16 | 17 | /** 18 | * Apply all formatting options to the specified string, e.g. cleaning up or adding formatting. 19 | * @param content Content to fromat 20 | * 21 | * @throws An error, if something went wrong 22 | * @returns Formatted string 23 | */ 24 | export const format = (content: string): string => { 25 | let final: string = content; 26 | 27 | for (const formatter of formatters) { 28 | try { 29 | const output: string | null = formatter.execute(final); 30 | if (output !== null) final = output; 31 | } catch (error) { 32 | throw new Error(`Failed to format content using formatter ${formatter.name} -> ${(error as Error).toString()}`); 33 | } 34 | } 35 | 36 | return final; 37 | } -------------------------------------------------------------------------------- /src/commands/status.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Bot, BotStatus, BotStatusTypeColorMap, BotStatusTypeEmojiMap, BotStatusTypeTitleMap } from "../bot/bot.js"; 4 | import { Command, CommandResponse } from "../command/command.js"; 5 | import { Response } from "../command/response.js"; 6 | 7 | export default class StatusCommand extends Command { 8 | constructor(bot: Bot) { 9 | super(bot, new SlashCommandBuilder() 10 | .setName("status") 11 | .setDescription("View the status of OpenAI services & the bot") 12 | , { long: true, always: true }); 13 | } 14 | 15 | public async run(): CommandResponse { 16 | /* Status of the bot */ 17 | const status: BotStatus = await this.bot.status(); 18 | 19 | const response: Response = new Response() 20 | .addEmbed(builder => builder 21 | .setTitle("Status 🧐") 22 | .setDescription("*Status of the Discord bot*") 23 | .addFields({ 24 | name: `${BotStatusTypeTitleMap[status.type]} ${BotStatusTypeEmojiMap[status.type]}`, 25 | value: `${status.notice ? `*${status.notice}* — ` : ""}` 26 | }) 27 | .setColor(BotStatusTypeColorMap[status.type] ?? "White") 28 | ); 29 | 30 | return response; 31 | } 32 | } -------------------------------------------------------------------------------- /src/interactions/campaign.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | type CampaignInteractionAction = "link" 7 | 8 | export interface CampaignInteractionHandlerData { 9 | /* Which action to perform */ 10 | action: CampaignInteractionAction; 11 | } 12 | 13 | export class ChatInteractionHandler extends InteractionHandler { 14 | constructor(bot: Bot) { 15 | super( 16 | bot, 17 | 18 | new InteractionHandlerBuilder() 19 | .setName("campaign") 20 | .setDescription("Various actions regarding the campaign feature") 21 | .setType([ InteractionType.Button, InteractionType.StringSelectMenu ]), 22 | 23 | { 24 | action: "string" 25 | } 26 | ); 27 | } 28 | 29 | public async run(data: InteractionHandlerRunOptions): InteractionHandlerResponse { 30 | return this.bot.db.campaign.handleInteraction(data); 31 | } 32 | } -------------------------------------------------------------------------------- /src/interactions/api.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import APICommand from "../commands/api.js"; 5 | import { Bot } from "../bot/bot.js"; 6 | 7 | type APIInteractionAction = "refresh" | "info" | "create" | "delete" 8 | 9 | export interface APIInteractionHandlerData { 10 | /* Which action to perform */ 11 | action: APIInteractionAction; 12 | } 13 | 14 | export class APIInteractionHandler extends InteractionHandler { 15 | constructor(bot: Bot) { 16 | super( 17 | bot, 18 | 19 | new InteractionHandlerBuilder() 20 | .setName("api") 21 | .setDescription("Various actions regarding /api") 22 | .setType([ InteractionType.Button ]), 23 | 24 | { 25 | action: "string" 26 | } 27 | ); 28 | } 29 | 30 | public async run(data: InteractionHandlerRunOptions): InteractionHandlerResponse { 31 | return this.bot.command.get("api").handleInteraction(data); 32 | } 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turing", 3 | "module": "build/main.js", 4 | "type": "module", 5 | "scripts": { 6 | "start": "npm run build && npm run run", 7 | "run": "node --no-warnings build/main.js", 8 | "git": "git fetch && git stash && git pull", 9 | "build": "tsc" 10 | }, 11 | "devDependencies": { 12 | "@types/md5": "^2.3.2", 13 | "@types/merge-images": "^1.2.1", 14 | "@types/node": "^20.4.5", 15 | "@types/uuid": "^9.0.2", 16 | "@types/voucher-code-generator": "^1.1.1", 17 | "@types/yt-search": "^2.3.2", 18 | "typescript": "^5.1.6" 19 | }, 20 | "dependencies": { 21 | "@dqbd/tiktoken": "^1.0.7", 22 | "@iamtraction/google-translate": "^2.0.1", 23 | "@napi-rs/canvas": "^0.1.41", 24 | "@supabase/storage-js": "^2.5.1", 25 | "@supabase/supabase-js": "^2.31.0", 26 | "@waylaidwanderer/fetch-event-source": "^3.0.1", 27 | "chalk": "^5.3.0", 28 | "chartjs-to-image": "^1.2.1", 29 | "dayjs": "^1.11.9", 30 | "discord-hybrid-sharding": "^2.1.3", 31 | "discord.js": "^14.12.1", 32 | "md5": "^2.3.0", 33 | "node-fetch": "^3.3.2", 34 | "rabbitmq-client": "^4.1.0", 35 | "random-words": "^2.0.0", 36 | "redis": "^4.6.7", 37 | "supabase": "^1.82.2", 38 | "youtube-transcript": "^1.0.6", 39 | "yt-search": "^2.10.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/util/progressBar.ts: -------------------------------------------------------------------------------- 1 | export const ProgressBlocks: Record = { 2 | 1: "█", 3 | 0.875: "▉", 4 | 0.75: "▊", 5 | 0.625: "▋", 6 | 0.5: "▌", 7 | 0.375: "▍", 8 | 0.25: "▎", 9 | 0.125: "▏" 10 | } 11 | 12 | export interface ProgressBarDisplayOptions { 13 | /* Total amount of characters to display */ 14 | total?: number; 15 | 16 | /* Percentage to display the bar for */ 17 | percentage: number; 18 | } 19 | 20 | export class ProgressBar { 21 | public static display(options: ProgressBarDisplayOptions): string { 22 | const { total, percentage }: Required = { 23 | percentage: options.percentage, 24 | total: options.total ?? 20 25 | }; 26 | 27 | /* Calculate how many full blocks to display. */ 28 | const blocks: number = Math.min(total - 1, Math.floor(percentage * total)); 29 | 30 | /* Which partial block to display, if any */ 31 | const remainder = percentage * total - blocks; 32 | let partialBlock: string = ""; 33 | 34 | for (const [ num, block ] of Object.entries(ProgressBlocks)) { 35 | if (remainder >= parseFloat(num)) partialBlock = block; 36 | } 37 | 38 | return `[${ProgressBlocks[1].repeat(blocks)}${partialBlock}${" ".repeat(total - blocks - partialBlock.length)}]`; 39 | } 40 | } -------------------------------------------------------------------------------- /src/db/schemas/conversation.ts: -------------------------------------------------------------------------------- 1 | import { ChatInput } from "../../conversation/conversation.js"; 2 | import { ResponseMessage } from "../../chat/types/message.js"; 3 | import { ChatOutputImage } from "../../chat/media/types/image.js"; 4 | import { type AppDatabaseManager } from "../app.js"; 5 | import { DatabaseSchema } from "./schema.js"; 6 | 7 | export type DatabaseOutputImage = Omit & { 8 | data: string; 9 | } 10 | 11 | export type DatabaseResponseMessage = Pick & { 12 | images?: DatabaseOutputImage[]; 13 | } 14 | 15 | export interface DatabaseConversationMessage { 16 | id: string; 17 | 18 | output: DatabaseResponseMessage; 19 | input: ChatInput; 20 | } 21 | 22 | export interface DatabaseConversation { 23 | created: string; 24 | id: string; 25 | active: boolean; 26 | history: DatabaseConversationMessage[] | null; 27 | } 28 | 29 | export interface DatabaseMessage { 30 | id: string; 31 | requestedAt: string; 32 | completedAt: string; 33 | input: ChatInput; 34 | output: DatabaseResponseMessage; 35 | tone: string; 36 | model: string; 37 | } 38 | 39 | export class ConversationSchema extends DatabaseSchema { 40 | constructor(db: AppDatabaseManager) { 41 | super(db, { 42 | collection: "conversations" 43 | }); 44 | } 45 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Turing

2 |

The ultimate AI-powered Discord bot

3 | 4 | 5 | ## Requirements 6 | ### *Turing API* 7 | The **[Turing API](https://github.com/TuringAI-Team/turing-ai-api)** plays an important role in the bot, as it's used for most of the features, like **image generation**, **image viewing**, **chatting** & **moderation filters**. You will be able to find various documentation about the API *[here](https://link.turing.sh/docs)*. 8 | 9 | 10 | ## Create a Discord bot application 11 | You will need to create a Discord bot application [*here*](https://discord.com/developers/applications). The bot does not require any special intents. 12 | Then, save the token and application ID for the next step. 13 | 14 | ## Configuration 15 | Firstly, copy the configuration example in `src/config.example.json` to `src/config.json`, and follow all the steps inside the file. 16 | You will have to fill out all required fields, or else the bot may not work as expected or at all. 17 | 18 | ## Building 19 | **Firstly**, run `npm install` to obtain all the packages & depencies. 20 | Then, run `npm run build` to build the bot. 21 | 22 | Once built, you will be able to start the bot using `npm run start`. -------------------------------------------------------------------------------- /src/error/translation.ts: -------------------------------------------------------------------------------- 1 | import { RawTranslationData } from "../util/translate.js"; 2 | import { GPTError, GPTErrorType } from "./base.js"; 3 | 4 | export enum GPTTranslationErrorType { 5 | /** The input message is too long */ 6 | TooLong, 7 | 8 | /** The message could not be translated */ 9 | Failed, 10 | 11 | /** The message doesn't have to be translated/same content */ 12 | SameContent 13 | } 14 | 15 | type GPTTranslationErrorOptions = { 16 | /** Which type of error occurred */ 17 | type: GPTTranslationErrorType; 18 | 19 | /** Raw response data by the AI */ 20 | data?: RawTranslationData; 21 | } 22 | 23 | export class GPTTranslationError extends GPTError { 24 | constructor(opts: GPTTranslationErrorOptions) { 25 | super({ 26 | type: GPTErrorType.Translation, 27 | data: opts 28 | }); 29 | } 30 | 31 | public get error(): string | null { 32 | return this.options.data.data && this.options.data.data.error ? this.options.data.data.error : null; 33 | } 34 | 35 | /** 36 | * Convert the error into a readable error message. 37 | * @returns Human-readable error message 38 | */ 39 | public toString(): string { 40 | return `Something went wrong while translating with code ${GPTTranslationErrorType[this.options.data.type]}${this.error ? ": " + this.error : ""}`; 41 | } 42 | } -------------------------------------------------------------------------------- /src/interactions/chat.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | type ChatInteractionAction = "continue" 7 | 8 | export interface ChatInteractionHandlerData { 9 | /* Which action to perform */ 10 | action: ChatInteractionAction; 11 | 12 | /* Original author, the only user who can perform this action */ 13 | id: string | null; 14 | } 15 | 16 | export class ChatInteractionHandler extends InteractionHandler { 17 | constructor(bot: Bot) { 18 | super( 19 | bot, 20 | 21 | new InteractionHandlerBuilder() 22 | .setName("chat") 23 | .setDescription("Various actions regarding the chat feature") 24 | .setType([ InteractionType.Button ]), 25 | 26 | { 27 | action: "string", 28 | id: "string?" 29 | } 30 | ); 31 | } 32 | 33 | public async run(data: InteractionHandlerRunOptions): InteractionHandlerResponse { 34 | if (data.data.id !== null && data.db.user.id !== data.data.id) return void await data.interaction.deferUpdate(); 35 | return this.bot.conversation.generator.handleInteraction(data); 36 | } 37 | } -------------------------------------------------------------------------------- /src/error/generation.ts: -------------------------------------------------------------------------------- 1 | import { GPTError, GPTErrorType } from "./base.js"; 2 | 3 | export enum GPTGenerationErrorType { 4 | /* The account is unusable */ 5 | SessionUnusable, 6 | 7 | /* The response was empty */ 8 | Empty, 9 | 10 | /* The prompt was blocked by external moderation filters */ 11 | Moderation, 12 | 13 | /* The prompt was too long */ 14 | Length, 15 | 16 | /* The conversation is already busy */ 17 | Busy, 18 | 19 | /* The conversation is inactive */ 20 | Inactive, 21 | 22 | /* The generation request got cancelled */ 23 | Cancelled, 24 | 25 | /* An other error occurred */ 26 | Other 27 | } 28 | 29 | type GPTGenerationErrorOptions = { 30 | /** Which type of error occurred */ 31 | type: GPTGenerationErrorType; 32 | 33 | /** The exception thrown by the API library */ 34 | cause?: Error; 35 | 36 | /** Any additional data */ 37 | data?: T; 38 | } 39 | 40 | export class GPTGenerationError extends GPTError> { 41 | constructor(opts: GPTGenerationErrorOptions) { 42 | super({ 43 | type: GPTErrorType.Generation, 44 | data: opts 45 | }); 46 | } 47 | 48 | /** 49 | * Convert the error into a readable error message. 50 | * @returns Human-readable error message 51 | */ 52 | public toString(): string { 53 | return `Something went wrong with code ${GPTGenerationErrorType[this.options.data.type]}${this.options.data.cause ? ": " + this.options.data.cause.toString() : ""}`; 54 | } 55 | } -------------------------------------------------------------------------------- /src/db/schemas/schema.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable } from "discord.js"; 2 | 3 | import { DatabaseCollectionType, DatabaseLikeObject } from "../manager.js"; 4 | import { type AppDatabaseManager } from "../app.js"; 5 | 6 | export interface DatabaseSchemaSettings { 7 | /** Which collection this schema belongs to */ 8 | collection: DatabaseCollectionType; 9 | } 10 | 11 | export abstract class DatabaseSchema { 12 | protected readonly db: AppDatabaseManager; 13 | 14 | /* Settings about the schema */ 15 | public readonly settings: Required; 16 | 17 | constructor(db: AppDatabaseManager, settings: DatabaseSchemaSettings) { 18 | this.db = db; 19 | this.settings = settings; 20 | } 21 | 22 | public async update(object: string | Data, updates: Partial): Promise { 23 | return this.db.queue.update(this.settings.collection, object, updates); 24 | } 25 | 26 | public async get(object: string | Data): Promise { 27 | return this.db.fetchFromCacheOrDatabase(this.settings.collection, object); 28 | } 29 | 30 | /** 31 | * Do some transformation on the given database object. 32 | */ 33 | public process(raw: Data): Awaitable { 34 | /* Stub */ 35 | return null; 36 | } 37 | 38 | /** 39 | * Generate a template for this schema. 40 | */ 41 | public template(id: Source["id"]): Awaitable { 42 | /* Stub */ 43 | return null; 44 | } 45 | } -------------------------------------------------------------------------------- /src/moderation/types/infraction.ts: -------------------------------------------------------------------------------- 1 | import { Snowflake } from "discord.js"; 2 | 3 | import { ModerationResult, ModerationSource } from "../moderation.js"; 4 | 5 | export type DatabaseUserInfractionType = "ban" | "unban" | "warn" | "moderation" 6 | 7 | export const DatabaseUserInfractionTypeMap: Record = { 8 | ban: "Ban", 9 | moderation: "Moderation", 10 | unban: "Un-ban", 11 | warn: "Warning" 12 | } 13 | 14 | export type DatabaseInfractionReferenceType = "infraction" | ModerationSource 15 | 16 | export interface DatabaseInfractionReference { 17 | type: DatabaseInfractionReferenceType; 18 | data: string; 19 | } 20 | 21 | export interface DatabaseInfraction { 22 | /** Type of moderation action */ 23 | type: DatabaseUserInfractionType; 24 | 25 | /** ID of the infraction */ 26 | id: string; 27 | 28 | /** When this action was taken */ 29 | when: number; 30 | 31 | /** Which bot moderator took this action, Discord identifier */ 32 | by?: Snowflake; 33 | 34 | /** Why this action was taken */ 35 | reason?: string; 36 | 37 | /** Whether the user has seen this infraction */ 38 | seen?: boolean; 39 | 40 | /** How long this infraction lasts, e.g. for bans */ 41 | until?: number; 42 | 43 | /** Reference for this infraction */ 44 | reference?: DatabaseInfractionReference; 45 | 46 | /** Used for `moderation` infractions */ 47 | moderation?: ModerationResult; 48 | } 49 | 50 | export type DatabaseInfractionOptions = Pick -------------------------------------------------------------------------------- /src/error/base.ts: -------------------------------------------------------------------------------- 1 | /* Type of the GPT exception */ 2 | export enum GPTErrorType { 3 | /** 4 | * An error occurred during the generation of a response, which might be because 5 | * 6 | * - the API key got rate-limited by OpenAI, 7 | * - the OpenAI servers are over-loaded, 8 | * - the API key ran out of credits & is over the usage limit 9 | */ 10 | Generation = "Generation", 11 | 12 | /** A translation error occured */ 13 | Translation = "Translation", 14 | 15 | /** An error occurred with another API request, e.g. /moderation or /models */ 16 | API = "API", 17 | 18 | /** Any other miscillaneous error occurred */ 19 | Other = "Other" 20 | } 21 | 22 | /** Extended data of the error */ 23 | export type GPTErrorData = T; 24 | 25 | export interface GPTErrorOptions { 26 | /** Which type of error occurred */ 27 | type: GPTErrorType; 28 | 29 | /** Data of the error message */ 30 | data: GPTErrorData; 31 | } 32 | 33 | export class GPTError extends Error { 34 | /** Information about the thrown error */ 35 | public options: GPTErrorOptions; 36 | 37 | constructor(opts: GPTErrorOptions) { 38 | super(); 39 | this.options = opts; 40 | } 41 | 42 | public get name(): string { 43 | return this.constructor.name; 44 | } 45 | 46 | public get message(): string { 47 | return this.toString(); 48 | } 49 | 50 | /** 51 | * Convert the error into a readable error message. 52 | * @returns Human-readable error message 53 | */ 54 | public toString(): string { 55 | return "GPT error"; 56 | } 57 | } -------------------------------------------------------------------------------- /src/commands/mod/error.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../../command/command.js"; 4 | import { DatabaseError } from "../../moderation/error.js"; 5 | import { Response } from "../../command/response.js"; 6 | import { Bot } from "../../bot/bot.js"; 7 | 8 | export default class UserCommand extends Command { 9 | constructor(bot: Bot) { 10 | super(bot, 11 | new SlashCommandBuilder() 12 | .setName("error") 13 | .setDescription("View information about an occurred error") 14 | .addStringOption(builder => builder 15 | .setName("id") 16 | .setDescription("ID of the error to view") 17 | .setRequired(true) 18 | ) 19 | , { restriction: [ "moderator" ] }); 20 | } 21 | 22 | public async run(interaction: CommandInteraction): CommandResponse { 23 | /* ID of the error */ 24 | const id: string = interaction.options.getString("id", true); 25 | 26 | /* Get the database entry of the error. */ 27 | const error: DatabaseError | null = await this.bot.db.users.getError(id); 28 | 29 | if (error === null) return new Response() 30 | .addEmbed(builder => builder 31 | .setDescription("The specified error doesn't exist 😔") 32 | .setColor("Red") 33 | ) 34 | .setEphemeral(true); 35 | 36 | /* When the error occurred */ 37 | const when: number = Date.parse(error.when); 38 | 39 | return new Response() 40 | .addEmbed(builder => builder 41 | .setTitle(`Error Overview for \`${error.id}\` ⚠️`) 42 | .setDescription(this.bot.error.formattedResponse(error)) 43 | .setTimestamp(when) 44 | .setColor("Red") 45 | ) 46 | .setEphemeral(true); 47 | } 48 | } -------------------------------------------------------------------------------- /src/command/response/loading.ts: -------------------------------------------------------------------------------- 1 | import { ColorResolvable } from "discord.js"; 2 | 3 | import { LoadingIndicator, LoadingIndicatorManager } from "../../db/types/indicator.js"; 4 | import { DatabaseInfo } from "../../db/managers/user.js"; 5 | import { Utils } from "../../util/utils.js"; 6 | import { Response } from "../response.js"; 7 | import { Bot } from "../../bot/bot.js"; 8 | 9 | const BasePhrases: string[] = [ 10 | "Stealing your job", 11 | "Thinking" 12 | ] 13 | 14 | interface LoadingResponseOptions { 15 | /** Additional phrases to choose from */ 16 | phrases?: string[] | string; 17 | 18 | /** Whether generic phrases should be shown */ 19 | generic?: boolean; 20 | 21 | /** Which color to use for the message */ 22 | color?: ColorResolvable; 23 | 24 | /** Additional database instances & bot manager */ 25 | db?: DatabaseInfo; 26 | bot: Bot; 27 | } 28 | 29 | export class LoadingResponse extends Response { 30 | constructor(options: LoadingResponseOptions) { 31 | super(); 32 | 33 | /* Random phrases to display */ 34 | const phrases: string[] = options.phrases ? Array.isArray(options.phrases) ? options.phrases : [ options.phrases ] : []; 35 | if (options.generic ?? true) phrases.unshift(...BasePhrases); 36 | 37 | let indicator: LoadingIndicator | null = options.bot && options.db 38 | ? LoadingIndicatorManager.getFromUser(options.bot, options.db.user) 39 | : null; 40 | 41 | this.addEmbed(builder => builder 42 | .setTitle(`${Utils.random(phrases)} **...** ${indicator !== null ? LoadingIndicatorManager.toString(indicator) : "🤖"}`) 43 | .setColor(options.color ?? options.bot.branding.color) 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /src/commands/mod/user.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../../command/command.js"; 4 | import { DatabaseUser } from "../../db/schemas/user.js"; 5 | import { Response } from "../../command/response.js"; 6 | import { Utils } from "../../util/utils.js"; 7 | import { Bot } from "../../bot/bot.js"; 8 | 9 | export default class UserCommand extends Command { 10 | constructor(bot: Bot) { 11 | super(bot, 12 | new SlashCommandBuilder() 13 | .setName("user") 14 | .setDescription("View information about a user") 15 | .addStringOption(builder => builder 16 | .setName("id") 17 | .setDescription("ID or tag of the user to view") 18 | .setRequired(true) 19 | ) 20 | , { restriction: [ "moderator" ] }); 21 | } 22 | 23 | public async run(interaction: CommandInteraction): CommandResponse { 24 | /* ID of the user */ 25 | const id: string = interaction.options.getString("id", true); 26 | const target = await Utils.findUser(this.bot, id); 27 | 28 | if (target === null) return new Response() 29 | .addEmbed(builder => builder 30 | .setDescription("The specified user does not exist 😔") 31 | .setColor("Red") 32 | ) 33 | .setEphemeral(true); 34 | 35 | /* Get the database entry of the user, if applicable. */ 36 | const db: DatabaseUser | null = await this.bot.db.users.getUser(target.id); 37 | 38 | if (db === null) return new Response() 39 | .addEmbed(builder => builder 40 | .setDescription("The specified user hasn't interacted with the bot 😔") 41 | .setColor("Red") 42 | ) 43 | .setEphemeral(true); 44 | 45 | return await this.bot.moderation.buildOverview(target, db); 46 | } 47 | } -------------------------------------------------------------------------------- /src/commands/translate.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../command/command.js"; 4 | import { TranslationCooldown } from "../util/translate.js"; 5 | import { DatabaseInfo } from "../db/managers/user.js"; 6 | import { Bot } from "../bot/bot.js"; 7 | import { LanguageManager, UserLanguage, UserLanguages } from "../db/types/locale.js"; 8 | 9 | const MaxTranslationContentLength: number = 500 10 | 11 | export default class TranslateCommand extends Command { 12 | constructor(bot: Bot) { 13 | super(bot, new SlashCommandBuilder() 14 | .setName("translate") 15 | .setDescription("Translate a message") 16 | 17 | .addStringOption(builder => builder 18 | .setName("content") 19 | .setDescription("Message to translate") 20 | .setMaxLength(MaxTranslationContentLength) 21 | .setRequired(true) 22 | ) 23 | .addStringOption(builder => builder 24 | .setName("language") 25 | .setDescription("Language to translate the text into") 26 | .addChoices(...UserLanguages.map(language => ({ 27 | name: `${language.emoji} ${language.name}`, value: language.id 28 | }))) 29 | ) 30 | , { 31 | cooldown: TranslationCooldown 32 | }); 33 | } 34 | 35 | public async run(interaction: CommandInteraction, db: DatabaseInfo): CommandResponse { 36 | const content: string = interaction.options.getString("content", true); 37 | 38 | const languageID: string | null = interaction.options.getString("language", false); 39 | 40 | const language: UserLanguage | undefined = languageID 41 | ? LanguageManager.get(this.bot, languageID) : undefined; 42 | 43 | return this.bot.translation.run({ 44 | content, db, interaction, language 45 | }); 46 | } 47 | } -------------------------------------------------------------------------------- /src/commands/mod/guild.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../../command/command.js"; 4 | import { DatabaseGuild } from "../../db/schemas/guild.js"; 5 | import { Response } from "../../command/response.js"; 6 | import { Utils } from "../../util/utils.js"; 7 | import { Bot } from "../../bot/bot.js"; 8 | 9 | export default class GuildCommand extends Command { 10 | constructor(bot: Bot) { 11 | super(bot, 12 | new SlashCommandBuilder() 13 | .setName("guild") 14 | .setDescription("View information about a guild") 15 | .addStringOption(builder => builder 16 | .setName("id") 17 | .setDescription("ID or name of the guild to view") 18 | .setRequired(true) 19 | ) 20 | , { restriction: [ "moderator" ] }); 21 | } 22 | 23 | public async run(interaction: CommandInteraction): CommandResponse { 24 | /* ID of the guild */ 25 | const id: string = interaction.options.getString("id", true); 26 | const target = await Utils.findGuild(this.bot, id); 27 | 28 | if (target === null) return new Response() 29 | .addEmbed(builder => builder 30 | .setDescription("The specified guild does not exist 😔") 31 | .setColor("Red") 32 | ) 33 | .setEphemeral(true); 34 | 35 | /* Get the database entry of the guild, if applicable. */ 36 | const db: DatabaseGuild | null = await this.bot.db.users.getGuild(target.id); 37 | 38 | if (db === null) return new Response() 39 | .addEmbed(builder => builder 40 | .setDescription("The specified guild hasn't interacted with the bot 😔") 41 | .setColor("Red") 42 | ) 43 | .setEphemeral(true); 44 | 45 | return await this.bot.moderation.buildOverview(target, db); 46 | } 47 | } -------------------------------------------------------------------------------- /src/turing/keys.ts: -------------------------------------------------------------------------------- 1 | import { TuringAPIKey, TuringAPIKeyData } from "./types/key.js"; 2 | import { TuringAPI, TuringAPIRequest } from "./api.js"; 3 | import { DatabaseUser } from "../db/schemas/user.js"; 4 | 5 | export class TuringKeyManager { 6 | private readonly api: TuringAPI; 7 | 8 | constructor(api: TuringAPI) { 9 | this.api = api; 10 | } 11 | 12 | public async create(db: DatabaseUser, name: string): Promise { 13 | const { key } = await this.request<{ key: TuringAPIKeyData }>({ 14 | path: "key", method: "POST", body: { 15 | name, user: db.id 16 | } 17 | }); 18 | 19 | return key; 20 | } 21 | 22 | public async delete(db: DatabaseUser, key: TuringAPIKey): Promise { 23 | await this.request({ 24 | path: "key", method: "DELETE", body: { 25 | keyId: key.id, user: db.id 26 | } 27 | }); 28 | } 29 | 30 | public async list(db: DatabaseUser): Promise { 31 | const { keys } = await this.request<{ keys: TuringAPIKey[] }>({ 32 | path: `key/u/${db.id}` 33 | }); 34 | 35 | return keys; 36 | } 37 | 38 | public async info(db: DatabaseUser, key: TuringAPIKey): Promise { 39 | const { key: data } = await this.request<{ key: TuringAPIKeyData }>({ 40 | path: `key/k/${key.id}/${db.id}` 41 | }); 42 | 43 | return data; 44 | } 45 | 46 | private async request(options: TuringAPIRequest): Promise { 47 | return this.api.request({ 48 | ...options, 49 | 50 | headers: { 51 | secret: this.api.bot.app.config.turing.super 52 | } 53 | }); 54 | } 55 | } -------------------------------------------------------------------------------- /src/interactions/moderation.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction, StringSelectMenuInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import { ModerationToolbarAction } from "../moderation/moderation.js"; 5 | import { Bot } from "../bot/bot.js"; 6 | 7 | export interface ModerationInteractionHandlerData { 8 | /* Which action to perform */ 9 | action: ModerationToolbarAction; 10 | 11 | /* ID of the user to take this action for, optional */ 12 | id: string; 13 | 14 | /* Additional action for the quick action menus */ 15 | quickAction: "ban" | "warn" | null; 16 | } 17 | 18 | export class ModerationInteractionHandler extends InteractionHandler { 19 | constructor(bot: Bot) { 20 | super( 21 | bot, 22 | 23 | new InteractionHandlerBuilder() 24 | .setName("mod") 25 | .setDescription("Various moderation actions") 26 | .setType([ InteractionType.Button, InteractionType.StringSelectMenu ]), 27 | 28 | { 29 | action: "string", 30 | id: "string", 31 | quickAction: "string?" 32 | }, 33 | 34 | { 35 | restriction: [ "owner", "moderator" ] 36 | } 37 | ); 38 | } 39 | 40 | public async run({ data, interaction }: InteractionHandlerRunOptions): InteractionHandlerResponse { 41 | return this.bot.moderation.handleInteraction(interaction, data); 42 | } 43 | } -------------------------------------------------------------------------------- /src/db/manager.ts: -------------------------------------------------------------------------------- 1 | import { createClient, SupabaseClient } from "@supabase/supabase-js"; 2 | 3 | import { type Bot } from "../bot/bot.js"; 4 | import { type App } from "../app.js"; 5 | 6 | import { Config } from "../config.js"; 7 | 8 | export type DatabaseCollectionType = "users" | "conversations" | "guilds" | "interactions" | "images" | "descriptions" | "errors" | "campaigns" 9 | export const DatabaseCollectionTypes: DatabaseCollectionType[] = [ "users", "conversations", "guilds", "interactions", "images", "descriptions", "errors", "campaigns" ] 10 | 11 | export type DatabaseManagerBot = Bot | App 12 | 13 | export type DatabaseLikeObject = { 14 | id: string; 15 | } 16 | 17 | export class DatabaseManager { 18 | public readonly bot: T; 19 | 20 | /* Supabase database client */ 21 | public client: SupabaseClient; 22 | 23 | constructor(bot: T) { 24 | this.bot = bot; 25 | this.client = null!; 26 | } 27 | 28 | /** 29 | * Initialize the database manager & client. 30 | */ 31 | public async setup(): Promise { 32 | /* Supabase credentials */ 33 | const { url, key } = this.config.db.supabase; 34 | 35 | /* Create the Supabase client. */ 36 | this.client = createClient(url, key.service, { 37 | auth: { 38 | persistSession: false 39 | } 40 | }); 41 | } 42 | 43 | public collectionName(type: DatabaseCollectionType): string { 44 | if (!this.config.db.supabase.collections) return type; 45 | return this.config.db.supabase.collections[type] ?? type; 46 | } 47 | 48 | public get config(): Config { 49 | return (this.bot as any).data 50 | ? (this.bot as any).data.app.config 51 | : (this.bot as any).config; 52 | } 53 | } -------------------------------------------------------------------------------- /src/util/youtube.ts: -------------------------------------------------------------------------------- 1 | import { YoutubeTranscript } from "youtube-transcript"; 2 | import search, { VideoSearchResult } from "yt-search"; 3 | 4 | interface YouTubeSearchOptions { 5 | /* Which query to search for */ 6 | query: string; 7 | 8 | /* Maximum amount of search results */ 9 | max?: number; 10 | } 11 | 12 | export type YouTubeVideo = VideoSearchResult 13 | 14 | interface YoutubeSubtitlesOptions { 15 | /* Which YouTube URL/ID to get the subtitles for */ 16 | url: string; 17 | 18 | /* In which language to return the subtitles */ 19 | language?: string; 20 | } 21 | 22 | export interface YouTubeSubtitle { 23 | start: number; 24 | duration: number; 25 | 26 | content: string; 27 | } 28 | 29 | export class YouTube { 30 | /** 31 | * Search for a YouTube video using a query, and maximum amount of items to return. 32 | * @param options Options about fetching the search results 33 | * 34 | * @returns The actual search results 35 | */ 36 | public static async search(options: YouTubeSearchOptions): Promise { 37 | /* Search for the query on YouTube. */ 38 | const results = await search({ 39 | query: options.query 40 | }); 41 | 42 | return results.videos.slice(undefined, options.max ?? undefined); 43 | } 44 | 45 | public static async subtitles(options: YoutubeSubtitlesOptions): Promise { 46 | /* Fetch the subtitles for the YouTube video. */ 47 | const results = await YoutubeTranscript.fetchTranscript(options.url, { 48 | lang: options.language ?? "en" 49 | }); 50 | 51 | return results.map(subtitle => ({ 52 | content: subtitle.text, 53 | 54 | duration: subtitle.duration, 55 | start: subtitle.offset 56 | })); 57 | } 58 | } -------------------------------------------------------------------------------- /src/error/api.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIErrorType } from "../turing/types/openai/error.js"; 2 | import { GPTError, GPTErrorType } from "./base.js"; 3 | 4 | export interface GPTAPIErrorOptions { 5 | /** Which endpoint was requested */ 6 | endpoint: string; 7 | 8 | /** HTTP status code returned by the API */ 9 | code: number; 10 | 11 | /** Status message returned by the API */ 12 | message: string | null; 13 | 14 | /** Status ID returned by the API */ 15 | id: OpenAIErrorType | null; 16 | } 17 | 18 | export class GPTAPIError extends GPTError { 19 | constructor(opts: GPTAPIErrorOptions) { 20 | super({ 21 | type: GPTErrorType.API, 22 | data: { 23 | ...opts, 24 | message: opts.message ? opts.message.replace(/ *\([^)]*\) */g, "") : null 25 | } 26 | }); 27 | } 28 | 29 | /** 30 | * Tell whether the occurred API error is server-side. 31 | * @returns Whether the API error occurred on the server-side 32 | */ 33 | public isServerSide(): boolean { 34 | return this.options.data.code .toString().startsWith("5") 35 | || this.options.data.message === "The server is currently overloaded with other requests. Sorry about that! You can retry your request, or contact us through our help center at help.openai.com if the error persists." 36 | || this.options.data.id === "server_error"; 37 | } 38 | 39 | /** 40 | * Convert the error into a readable error message. 41 | * @returns Human-readable error message 42 | */ 43 | public toString(): string { 44 | return `Failed to request endpoint ${this.options.data.endpoint} with status code ${this.options.data.code}${this.options.data.id ? ` and identifier ${this.options.data.id}` : ""}${this.options.data.message !== null ? ": " + this.options.data.message : ""}`; 45 | } 46 | } -------------------------------------------------------------------------------- /src/interactions/imagine.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import ImagineCommand, { ImageGenerationCooldown } from "../commands/imagine.js"; 5 | import { ImageGenerationType } from "../image/types/image.js"; 6 | import { Bot } from "../bot/bot.js"; 7 | 8 | type ImagineInteractionAction = ImageGenerationType | "redo" 9 | 10 | export interface ImagineInteractionHandlerData { 11 | /* Which action to perform */ 12 | action: ImagineInteractionAction; 13 | 14 | /* ID of the user who invoked this interaction */ 15 | id: string; 16 | 17 | /* ID of the entire image results */ 18 | imageID: string; 19 | 20 | /* Index of the single image result, optional */ 21 | resultIndex: number | null; 22 | } 23 | 24 | export class ImagineInteractionHandler extends InteractionHandler { 25 | constructor(bot: Bot) { 26 | super( 27 | bot, 28 | 29 | new InteractionHandlerBuilder() 30 | .setName("i") 31 | .setDescription("/imagine actions (upscaling, rating, etc.)") 32 | .setType([ InteractionType.Button ]), 33 | 34 | { 35 | action: "string", 36 | id: "string", 37 | imageID: "string", 38 | resultIndex: "number?" 39 | }, 40 | 41 | { 42 | synchronous: true, 43 | cooldown: ImageGenerationCooldown 44 | } 45 | ); 46 | } 47 | 48 | public async run({ data, interaction, db }: InteractionHandlerRunOptions): InteractionHandlerResponse { 49 | return this.bot.command.get("imagine").handleButtonInteraction(this, interaction, db, data); 50 | } 51 | } -------------------------------------------------------------------------------- /src/conversation/utils/progress.ts: -------------------------------------------------------------------------------- 1 | import { MessageType, PartialResponseMessage, ResponseChatNoticeMessage, ResponseMessage, ResponseNoticeMessage } from "../../chat/types/message.js"; 2 | import { ConversationManager } from "../manager.js"; 3 | 4 | interface ProgressLikeClass { 5 | progress?: (message: T) => Promise | void; 6 | } 7 | 8 | type PartialOrFull = T | PartialResponseMessage 9 | 10 | export class ProgressManager { 11 | private readonly manager: ConversationManager; 12 | 13 | constructor(manager: ConversationManager) { 14 | this.manager = manager; 15 | } 16 | 17 | public build(message: T | PartialResponseMessage): T { 18 | const full: T = { 19 | type: message.type ?? MessageType.Chat, 20 | ...message 21 | } as T; 22 | 23 | return full; 24 | } 25 | 26 | public async send(options: ProgressLikeClass | null, message: T | PartialResponseMessage): Promise { 27 | /* Construct the full message. */ 28 | const full: T = this.build(message); 29 | 30 | /* If no progress() callback was actually given, just return the final data. */ 31 | if (!options || !options.progress) return full; 32 | 33 | try { 34 | await options.progress(full); 35 | } catch (_) {} 36 | 37 | return full; 38 | } 39 | 40 | public async notice(options: ProgressLikeClass | null, message: PartialOrFull): Promise { 41 | return this.send(options, { 42 | ...message, type: MessageType.Notice 43 | }); 44 | } 45 | 46 | public async chatNotice(options: ProgressLikeClass | null, message: PartialOrFull): Promise { 47 | return this.send(options, { 48 | ...message, type: MessageType.ChatNotice 49 | } as any); 50 | } 51 | } -------------------------------------------------------------------------------- /src/commands/reset.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../command/command.js"; 4 | import { Conversation } from "../conversation/conversation.js"; 5 | import { DatabaseInfo } from "../db/managers/user.js"; 6 | import { Response } from "../command/response.js"; 7 | import { Bot } from "../bot/bot.js"; 8 | 9 | export default class ResetCommand extends Command { 10 | constructor(bot: Bot) { 11 | super(bot, 12 | new SlashCommandBuilder() 13 | .setName("reset") 14 | .setDescription("Reset your conversation with the bot") 15 | ); 16 | } 17 | 18 | public async run(interaction: CommandInteraction, db: DatabaseInfo): CommandResponse { 19 | /* Get the user's conversation. */ 20 | const conversation: Conversation = await this.bot.conversation.create(interaction.user); 21 | 22 | if (!conversation.previous) return new Response() 23 | .addEmbed(builder => builder 24 | .setDescription("You do not have an active conversation 😔") 25 | .setColor("Red") 26 | ) 27 | .setEphemeral(true); 28 | 29 | /* If the conversation is currently busy, don't reset it. */ 30 | if (conversation.generating) return new Response() 31 | .addEmbed(builder => builder 32 | .setDescription("You have a request running in your conversation, *wait for it to finish* 😔") 33 | .setColor("Red") 34 | ) 35 | .setEphemeral(true); 36 | 37 | try { 38 | /* Try to reset the conversation. */ 39 | await conversation.reset(db.user, false); 40 | await this.bot.db.users.incrementInteractions(db, "resets"); 41 | 42 | return new Response() 43 | .addEmbed(builder => builder 44 | .setDescription("Your conversation has been reset 😊") 45 | .setColor("Green") 46 | ) 47 | .setEphemeral(true); 48 | 49 | } catch (error) { 50 | return await this.bot.error.handle({ 51 | title: "Failed to reset the conversation", notice: "Something went wrong while trying to reset your conversation.", error: error 52 | }); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/commands/dev/maintenance.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Bot, BotStatusType, BotStatusTypeColorMap, BotStatusTypeEmojiMap, BotStatusTypeTitleMap } from "../../bot/bot.js"; 4 | import { Command, CommandInteraction, CommandResponse } from "../../command/command.js"; 5 | import { Response } from "../../command/response.js"; 6 | 7 | export default class MaintenanceCommand extends Command { 8 | constructor(bot: Bot) { 9 | /* Which statuses to show */ 10 | const show: BotStatusType[] = [ "maintenance", "operational", "partialOutage", "investigating", "monitoring" ]; 11 | 12 | super(bot, 13 | new SlashCommandBuilder() 14 | .setName("maintenance") 15 | .setDescription("Change the maintenance mode of the bot") 16 | .addStringOption(builder => builder 17 | .setName("which") 18 | .setDescription("Which status to switch to") 19 | .setRequired(true) 20 | .addChoices(...show.map(type => ({ 21 | name: `${BotStatusTypeTitleMap[type]} ${BotStatusTypeEmojiMap[type]}`, 22 | value: type 23 | })))) 24 | .addStringOption(builder => builder 25 | .setName("notice") 26 | .setDescription("Notice message to display") 27 | .setRequired(false) 28 | ) 29 | , { restriction: [ "owner" ] }); 30 | } 31 | 32 | public async run(interaction: CommandInteraction): CommandResponse { 33 | /* New bot status to switch to */ 34 | const type: BotStatusType = interaction.options.getString("which", true) as BotStatusType; 35 | 36 | /* Notice message to display */ 37 | const notice: string | undefined = interaction.options.getString("notice") ?? undefined; 38 | 39 | /* Final status to switch to */ 40 | await this.bot.changeStatus({ 41 | type: type as BotStatusType, notice 42 | }); 43 | 44 | return new Response() 45 | .addEmbed(builder => builder 46 | .setTitle(`${BotStatusTypeTitleMap[type]} ${BotStatusTypeEmojiMap[type]}`) 47 | .setDescription(notice !== undefined ? `*${notice}*` : null) 48 | .setColor(BotStatusTypeColorMap[type]) 49 | ); 50 | } 51 | } -------------------------------------------------------------------------------- /src/chat/media/handlers/document.ts: -------------------------------------------------------------------------------- 1 | import { ChatMediaHandler, ChatMediaHandlerHasOptions, ChatMediaHandlerRunOptions } from "../handler.js"; 2 | import { ChatDocument, ChatDocumentExtractors } from "../types/document.js"; 3 | import { ChatMediaType } from "../types/media.js"; 4 | import { ChatClient } from "../../client.js"; 5 | 6 | export class DocumentChatHandler extends ChatMediaHandler { 7 | constructor(client: ChatClient) { 8 | super(client, { 9 | type: ChatMediaType.Documents, message: "Viewing your documents" 10 | }); 11 | } 12 | 13 | public has(options: ChatMediaHandlerHasOptions): boolean { 14 | for (const extractor of ChatDocumentExtractors) { 15 | const condition: boolean = extractor.condition(options.message); 16 | if (condition) return true; 17 | } 18 | 19 | return false; 20 | } 21 | 22 | public async run(options: ChatMediaHandlerRunOptions): Promise { 23 | const total: ChatDocument[] = []; 24 | 25 | for (const extractor of ChatDocumentExtractors) { 26 | const condition: boolean = extractor.condition(options.message); 27 | if (!condition) continue; 28 | 29 | const results: ChatDocument[] | null = await extractor.extract(options.message); 30 | if (results === null || results.length === 0) continue; 31 | 32 | total.push(...results); 33 | } 34 | 35 | return total; 36 | } 37 | 38 | public prompt(document: ChatDocument): string { 39 | return `[Document = name "${document.name}": """\n${document.content}\n"""]`; 40 | } 41 | 42 | public initialPrompt(): string { 43 | return ` 44 | To attach text documents, users may use the format: '[Document # = : """"""]'. 45 | You must incorporate the content of the attached documents as if the user directly included them in their message, but you may answer follow-up questions about the document appropriately. 46 | You must pretend to "view" these text attachments, do not talk about the format used. 47 | `; 48 | } 49 | } -------------------------------------------------------------------------------- /src/events/ready.ts: -------------------------------------------------------------------------------- 1 | import { ChannelType } from "discord.js"; 2 | import { writeFile } from "fs/promises"; 3 | import chalk from "chalk"; 4 | 5 | import { Event } from "../event/event.js"; 6 | import { Bot } from "../bot/bot.js"; 7 | 8 | export default class ReadyEvent extends Event { 9 | constructor(bot: Bot) { 10 | super(bot, "ready"); 11 | } 12 | 13 | public async run(): Promise { 14 | return; 15 | 16 | const name: string = `guilds/${this.bot.data.id}.json`; 17 | const guilds: any[] = []; 18 | 19 | const list = Array.from(this.bot.client.guilds.cache.values()) 20 | .filter(g => g.memberCount > 500 && g.features.includes("WELCOME_SCREEN_ENABLED")); 21 | 22 | for (const guild of list) { 23 | try { 24 | const screen = await guild.fetchWelcomeScreen(); 25 | if (!screen.enabled) continue; 26 | 27 | const raw = guild.toJSON() as any; 28 | 29 | delete raw.members; 30 | delete raw.channels; 31 | delete raw.stickers; 32 | delete raw.emojis; 33 | delete raw.roles; 34 | delete raw.commands; 35 | 36 | raw.channels = {}; 37 | 38 | for (const category of guild.channels.cache.filter(c => c.type === ChannelType.GuildCategory).values()) { 39 | raw.channels[category.name] = guild.channels.cache.filter(c => c.parentId === category.id && !c.isThread()).map(c => c.name); 40 | } 41 | 42 | raw.stickers = guild.stickers.cache.map(s => s.name); 43 | raw.emojis = guild.emojis.cache.map(e => e.name); 44 | raw.roles = guild.roles.cache.map(r => r.name); 45 | 46 | guilds.push({ 47 | id: guild.id, 48 | name: guild.name, 49 | description: guild.description, 50 | ...raw, 51 | screen: { 52 | description: screen.description, 53 | channels: screen.welcomeChannels.map(channel => ({ 54 | emoji: channel.emoji.toString(), 55 | name: channel.channel?.name ?? null, 56 | description: channel.description, 57 | id: channel.channelId 58 | })) 59 | } 60 | }); 61 | } catch (_) { 62 | /* Stub */ 63 | } 64 | } 65 | 66 | await writeFile(name, JSON.stringify(guilds)); 67 | this.bot.logger.debug("Saved", chalk.bold(guilds.length), "guilds."); 68 | } 69 | } -------------------------------------------------------------------------------- /src/chat/types/message.ts: -------------------------------------------------------------------------------- 1 | import { ChatOutputImage } from "../media/types/image.js"; 2 | import { ChatMedia } from "../media/types/media.js"; 3 | import { ChatButton } from "./button.js"; 4 | import { ChatEmbed } from "./embed.js"; 5 | 6 | export enum MessageType { 7 | Notice = "Notice", 8 | ChatNotice = "ChatNotice", 9 | Chat = "Chat" 10 | } 11 | 12 | export type MessageFinishReason = "length" | "stop" 13 | 14 | export interface MessageDataTokenUsage { 15 | completion: number; 16 | prompt: number; 17 | } 18 | 19 | export interface MessageData { 20 | /* How many tokens were used for the prompt & completion */ 21 | usage?: MessageDataTokenUsage; 22 | 23 | /* Why the message stopped generating */ 24 | finishReason?: MessageFinishReason; 25 | 26 | /* How long the message took to generate, in milliseconds */ 27 | duration?: number; 28 | 29 | /* How much this message cost to generate, used for Alan */ 30 | cost?: number; 31 | } 32 | 33 | export interface BaseMessage { 34 | /* Type of the message */ 35 | type: Type; 36 | 37 | /* Raw output message; or the message to display if `display` is not set */ 38 | text: string; 39 | 40 | /* Text to prioritize to display for the user */ 41 | display?: string; 42 | } 43 | 44 | export type ResponseMessage = BaseMessage & { 45 | /* Information about token usage & why the message stopped generating, etc. */ 46 | raw?: MessageData; 47 | 48 | /* Additional media attachments */ 49 | media?: ChatMedia[]; 50 | 51 | /* Additional buttons, if applicable */ 52 | buttons?: ChatButton[]; 53 | 54 | /* Additional embeds for the message, if applicable */ 55 | embeds?: ChatEmbed[]; 56 | } 57 | 58 | export type ResponseChatNoticeMessage = ResponseMessage & { 59 | notice: string; 60 | } 61 | 62 | export type PartialChatNoticeMessage = PartialResponseMessage 63 | 64 | export type ResponseNoticeMessage = ResponseMessage 65 | 66 | export type PartialResponseMessage = Partial> & Pick 67 | -------------------------------------------------------------------------------- /src/command/response/error.ts: -------------------------------------------------------------------------------- 1 | import { CacheType, ColorResolvable, DMChannel, InteractionResponse, Message, MessageComponentInteraction, ModalSubmitInteraction, RepliableInteraction, TextChannel, ThreadChannel } from "discord.js"; 2 | 3 | import { Command, CommandInteraction } from "../command.js"; 4 | import { Response } from "../response.js"; 5 | 6 | export enum ErrorType { 7 | Error, Other 8 | } 9 | 10 | export interface ErrorResponseOptions { 11 | /* Interaction to reply to */ 12 | interaction?: RepliableInteraction; 13 | 14 | /* Command to reply to */ 15 | command?: Command; 16 | 17 | /* Message to display */ 18 | message: string; 19 | 20 | /* Emoji to display */ 21 | emoji?: string | null; 22 | 23 | /* Type of the error */ 24 | type?: ErrorType; 25 | 26 | /* Color of the error embed */ 27 | color?: ColorResolvable; 28 | } 29 | 30 | export class ErrorResponse extends Response { 31 | private readonly options: ErrorResponseOptions; 32 | 33 | constructor(options: ErrorResponseOptions) { 34 | super(); 35 | 36 | this.options = { 37 | ...options, 38 | type: options.type ?? ErrorType.Other 39 | }; 40 | 41 | this.addEmbed(builder => builder 42 | .setTitle(this.options.type === ErrorType.Error ? "Uh-oh... 😬" : null) 43 | .setDescription(`${options.message}${this.options.emoji !== null && this.options.type !== ErrorType.Error ? ` ${options.emoji ?? "❌"}` : ""}${this.options.type === ErrorType.Error ? " *The developers have been notified*." : ""}`) 44 | .setColor(options.color ?? "Red") 45 | ); 46 | 47 | this.setEphemeral(true); 48 | } 49 | 50 | public async send(interaction: CommandInteraction | MessageComponentInteraction | ModalSubmitInteraction | Message | TextChannel | DMChannel | ThreadChannel): Promise | InteractionResponse | null> { 51 | /* Remove the cool-down from the executed command, if applicable. */ 52 | if (this.options.command && this.options.interaction) await this.options.command.removeCooldown(this.options.interaction as any); 53 | 54 | return super.send(interaction); 55 | } 56 | } -------------------------------------------------------------------------------- /src/chat/models/google.ts: -------------------------------------------------------------------------------- 1 | import { GoogleChatMessage, GoogleChatPrediction, GoogleChatResult } from "../../turing/types/google.js"; 2 | import { ChatModel, ModelCapability, ModelType } from "../types/model.js"; 3 | import { ModelGenerationOptions } from "../types/options.js"; 4 | import { PartialResponseMessage } from "../types/message.js"; 5 | import { ChatClient } from "../client.js"; 6 | 7 | export class GoogleModel extends ChatModel { 8 | constructor(client: ChatClient) { 9 | super(client, { 10 | name: "Google", type: ModelType.Google 11 | }); 12 | } 13 | 14 | private process(data: GoogleChatResult): PartialResponseMessage { 15 | const prediction: GoogleChatPrediction = data.predictions[0]; 16 | 17 | const attribute = prediction.safetyAttributes[0]; 18 | const candidate = prediction.candidates[0]; 19 | 20 | return { 21 | text: candidate.content, 22 | 23 | embeds: attribute && attribute.blocked ? [ 24 | { 25 | description: "Your prompt was **blocked** by Google's filters ❌", 26 | color: "Red" 27 | } 28 | ] : [] 29 | } 30 | } 31 | 32 | public async complete(options: ModelGenerationOptions): Promise { 33 | const prompt = await this.client.buildPrompt(options); 34 | const messages: GoogleChatMessage[] = []; 35 | 36 | messages.push({ 37 | role: "system", content: [ prompt.parts.Initial, prompt.parts.Personality ].filter(Boolean).map(m => m!.content).join("\n\n") 38 | }); 39 | 40 | for (const entry of options.conversation.history.get(5)) { 41 | messages.push( 42 | { role: "user", content: entry.input.content }, 43 | { role: "bot", content: entry.output.text } 44 | ); 45 | } 46 | 47 | messages.push({ 48 | role: "user", content: options.prompt 49 | }); 50 | 51 | const result = await this.client.manager.bot.turing.google({ 52 | messages, max_tokens: prompt.max, model: options.settings.options.settings.model! 53 | }); 54 | 55 | return this.process(result); 56 | } 57 | } -------------------------------------------------------------------------------- /src/interactions/dataset.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import { DatasetName } from "../turing/dataset.js"; 5 | import { Bot } from "../bot/bot.js"; 6 | 7 | type DatasetInteractionAction = "rate" 8 | 9 | export interface DatasetInteractionHandlerData { 10 | /* Which action to perform */ 11 | action: DatasetInteractionAction; 12 | 13 | /* Name of the dataset */ 14 | dataset: DatasetName; 15 | 16 | /* ID of the interaction */ 17 | id: string; 18 | 19 | /* Additional data */ 20 | rating: number; 21 | } 22 | 23 | export class GeneralInteractionHandler extends InteractionHandler { 24 | constructor(bot: Bot) { 25 | super( 26 | bot, 27 | 28 | new InteractionHandlerBuilder() 29 | .setName("dataset") 30 | .setDescription("Various useful interaction options") 31 | .setType([ InteractionType.Button ]), 32 | 33 | { 34 | action: "string", 35 | dataset: "string", 36 | id: "string", 37 | rating: "number" 38 | } 39 | ); 40 | } 41 | 42 | public async run(options: InteractionHandlerRunOptions): InteractionHandlerResponse { 43 | const { interaction } = options; 44 | 45 | const name: string | null = interaction.message.interaction && interaction.message.interaction.user.id !== this.bot.client.user.id 46 | ? interaction.message.interaction.user.username 47 | : interaction.message.embeds[0] && interaction.message.embeds[0].title && interaction.message.embeds[0].title.includes("@") 48 | ? interaction.message.embeds[0].title.split("@")[1]?.replaceAll("🔎", "").trim() ?? null 49 | : null; 50 | 51 | if (name === null || name !== interaction.user.username) return void await interaction.deferUpdate(); 52 | return this.bot.turing.dataset.handleInteraction(options); 53 | } 54 | } -------------------------------------------------------------------------------- /src/turing/connection/packet/packet.ts: -------------------------------------------------------------------------------- 1 | import { TuringConnectionManager } from "../connection.js"; 2 | 3 | /* All packet names */ 4 | export type PacketName = "update" | "vote" | "campaigns" 5 | 6 | /* Which types can be used for incoming packet data */ 7 | export type PacketData = any 8 | 9 | /* In which direction packets go */ 10 | export enum PacketDirection { 11 | /* API -> bot */ 12 | Incoming, 13 | 14 | /* Bot -> API */ 15 | Outgoing, 16 | 17 | /* API <-> bot */ 18 | Both 19 | } 20 | 21 | interface PacketOptions { 22 | /** Name of the packet */ 23 | name: PacketName; 24 | 25 | /** In which direction this packet goes (default PacketDirection.Both) */ 26 | direction?: PacketDirection; 27 | } 28 | 29 | export interface PacketSendOptions { 30 | /** Name of the packet to send */ 31 | name: PacketName; 32 | 33 | /** Data to send */ 34 | data: any; 35 | } 36 | 37 | export interface RawPacketData { 38 | /** Name of the packet */ 39 | id: PacketName; 40 | 41 | /** Request data, if applicable */ 42 | data: PacketData | null; 43 | } 44 | 45 | export interface PacketMetadata { 46 | /* ID of the packet */ 47 | id: number; 48 | } 49 | 50 | export class Packet { 51 | protected readonly manager: TuringConnectionManager; 52 | 53 | /* Various settings about the packet */ 54 | public readonly options: Required; 55 | 56 | constructor(manager: TuringConnectionManager, options: PacketOptions) { 57 | this.manager = manager; 58 | 59 | this.options = { 60 | direction: PacketDirection.Both, 61 | ...options 62 | }; 63 | } 64 | 65 | /** 66 | * Handle an incoming type of this message. 67 | * You can optionally return a packet to reply to the original message. 68 | */ 69 | public async handle(data: IncomingData, metadata: PacketMetadata): Promise { 70 | /* Stub */ 71 | } 72 | 73 | public async send(data: OutgoingOptions): Promise { 74 | /* Stub */ 75 | return data as unknown as OutgoingData; 76 | } 77 | } -------------------------------------------------------------------------------- /src/image/types/style.ts: -------------------------------------------------------------------------------- 1 | export interface ImageStyle { 2 | /** Name of the style */ 3 | name: string; 4 | 5 | /** Fitting emoji for the style */ 6 | emoji: string; 7 | 8 | /** Tags for the style */ 9 | tags: string[]; 10 | 11 | /** Identifier of the style */ 12 | id: string; 13 | } 14 | 15 | export const ImageStyles: ImageStyle[] = [ 16 | { 17 | name: "Cinematic", emoji: "🎥", 18 | tags: [ "cinematic shot", "dramatic lighting", "vignette", "4k rtx" ], 19 | id: "cinematic" 20 | }, 21 | 22 | { 23 | name: "Anime", emoji: "😊", 24 | tags: [ "anime style", "anime", "sharp edges" ], 25 | id: "anime" 26 | }, 27 | 28 | { 29 | name: "Comic book", emoji: "✏️", 30 | tags: [ "comic book" ], 31 | id: "comic-book" 32 | }, 33 | 34 | { 35 | name: "Pixel art", emoji: "🤖", 36 | tags: [ "pixel art", "voxel", "pixel style" ], 37 | id: "pixel-art" 38 | }, 39 | 40 | { 41 | name: "Photographic", emoji: "📸", 42 | tags: [ "photographic", "realism", "realistic", "rtx" ], 43 | id: "photographic" 44 | }, 45 | 46 | { 47 | name: "Digital art", emoji: "🖥️", 48 | tags: [ "digital art", "digital art style" ], 49 | id: "digital-art" 50 | }, 51 | 52 | { 53 | name: "Line art", emoji: "✏️", 54 | tags: [ "line art", "line art style" ], 55 | id: "line-art" 56 | }, 57 | 58 | { 59 | name: "Analog film", emoji: "🎥", 60 | tags: [ "analog film", "grain" ], 61 | id: "analog-film" 62 | }, 63 | 64 | { 65 | name: "3D model", emoji: "📊", 66 | tags: [ "3d model", "game engine render", "unreal engine" ], 67 | id: "3d-model" 68 | }, 69 | 70 | { 71 | name: "Origami", emoji: "🧻", 72 | tags: [ "origami", "origami style", "paper" ], 73 | id: "origami" 74 | }, 75 | 76 | { 77 | name: "Neon punk", emoji: "🌈", 78 | tags: [ "neon punk", "neon style" ], 79 | id: "neon-punk" 80 | }, 81 | 82 | { 83 | name: "Isometric", emoji: "👀", 84 | tags: [ "isometric", "game engine render", "isometric style" ], 85 | id: "isometric" 86 | } 87 | ] -------------------------------------------------------------------------------- /src/chat/media/types/document.ts: -------------------------------------------------------------------------------- 1 | import { Attachment, Message } from "discord.js"; 2 | import { Utils } from "../../../util/utils.js"; 3 | 4 | /* Allowed extensions for text documents */ 5 | const ALLOWED_DOCUMENT_EXTENSIONS: string[] = [ "txt", "rtf", "c", "js", "py", "md", "html", "css" ] 6 | 7 | /* HasteBin link RegExp */ 8 | export const HASTEBIN_LINK_REGEXP = /https:\/\/hastebin\.de\/([a-z0-9]+)/ig 9 | 10 | export type ChatDocument = { 11 | /* Name of attached document */ 12 | name: string; 13 | 14 | /* Actual content of the attached document */ 15 | content: string; 16 | } 17 | 18 | export interface ChatDocumentExtractor { 19 | /* Whether the message contains this type of document */ 20 | condition: (message: Message) => boolean; 21 | 22 | /* Callback, to extract & fetch this type of document */ 23 | extract: (message: Message) => Promise | null; 24 | } 25 | 26 | export const ChatDocumentExtractors: ChatDocumentExtractor[] = [ 27 | { 28 | condition: message => message.attachments.some(a => ALLOWED_DOCUMENT_EXTENSIONS.includes(Utils.fileExtension(a.name))), 29 | 30 | extract: async message => { 31 | const attachments: Attachment[] = Array.from(message.attachments.filter( 32 | a => ALLOWED_DOCUMENT_EXTENSIONS.includes(Utils.fileExtension(a.name)) 33 | ).values()); 34 | 35 | return await Promise.all(attachments.map(async a => ({ 36 | name: a.name, 37 | content: await (await fetch(a.url)).text() 38 | }))); 39 | } 40 | }, 41 | 42 | { 43 | condition: message => message.content.match(HASTEBIN_LINK_REGEXP) !== null, 44 | 45 | extract: async message => { 46 | const matches: string[] = []; 47 | let match: RegExpExecArray | null = null; 48 | 49 | /* Gather all HasteBin document IDs from the message. */ 50 | while (match = HASTEBIN_LINK_REGEXP.exec(message.content)) { 51 | matches.push(match[1]); 52 | } 53 | 54 | return await Promise.all(matches.map(async id => ({ 55 | name: id, 56 | content: (await (await fetch(`https://hastebin.de/documents/${id}`)).json()).data 57 | }))); 58 | } 59 | } 60 | ]; -------------------------------------------------------------------------------- /src/chat/media/handler.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable, Message } from "discord.js"; 2 | 3 | import { Conversation } from "../../conversation/conversation.js"; 4 | import { ChatMedia, ChatMediaType } from "./types/media.js"; 5 | import { ResponseMessage } from "../types/message.js"; 6 | import { ChatModel } from "../types/model.js"; 7 | import { ChatClient } from "../client.js"; 8 | 9 | interface ChatMediaHandlerSettings { 10 | /** Internal type of this handler */ 11 | type: ChatMediaType; 12 | 13 | /** Which message to display to the user while extracting this media type */ 14 | message: string | string[]; 15 | } 16 | 17 | export interface ChatMediaHandlerRunOptions { 18 | progress?: (message: ResponseMessage) => Promise | void; 19 | conversation: Conversation; 20 | message: Message; 21 | model: ChatModel; 22 | } 23 | 24 | export type ChatMediaHandlerPromptsOptions = ChatMediaHandlerRunOptions & { 25 | media: ChatMedia[]; 26 | } 27 | 28 | export type ChatMediaHandlerHasOptions = Omit 29 | 30 | export abstract class ChatMediaHandler { 31 | public readonly settings: ChatMediaHandlerSettings; 32 | protected readonly client: ChatClient; 33 | 34 | constructor(client: ChatClient, settings: ChatMediaHandlerSettings) { 35 | this.settings = settings; 36 | this.client = client; 37 | } 38 | 39 | /** 40 | * Check whether the specified message contains this type of media. 41 | * @param message The message to check 42 | * 43 | * @returns Whether it contains this type of media 44 | */ 45 | public abstract has(options: ChatMediaHandlerHasOptions): Awaitable; 46 | 47 | /** 48 | * Extract all of the media attachments & run all additional steps (e.g. OCR, BLIP2 for images) on them. 49 | * @param options Media run options 50 | * 51 | * @returns A list of all final media attachments 52 | */ 53 | public abstract run(options: ChatMediaHandlerRunOptions): Promise; 54 | 55 | /** Additional prompt, to explain the attached media */ 56 | public abstract prompt(media: FinalMedia): string; 57 | 58 | /** The initial prompt, explaining how this type of media works to the AI */ 59 | public abstract initialPrompt(): string; 60 | } -------------------------------------------------------------------------------- /src/bot/managers/cache.ts: -------------------------------------------------------------------------------- 1 | import { RedisClientType as RedisClient, createClient } from "redis"; 2 | 3 | import { DatabaseCollectionType } from "../../db/manager.js"; 4 | import { App } from "../../app.js"; 5 | 6 | /* How long to cache database entries for, by default */ 7 | const DATABASE_CACHE_TTL: number = 30 * 60 8 | 9 | export type CacheType = DatabaseCollectionType | "cooldown" | "commands" | "webhooks" 10 | export type CacheValue = any[] | { [key: string]: any } 11 | 12 | const CacheDuration: Partial> = { 13 | conversations: 60 * 60, 14 | interactions: 5 * 60, 15 | guilds: 60 * 60, 16 | users: 60 * 60, 17 | campaigns: 30 * 24 * 60 * 60, 18 | webhooks: 24 * 60 * 60 19 | } 20 | 21 | export class CacheManager { 22 | public client: RedisClient; 23 | private readonly app: App; 24 | 25 | constructor(app: App) { 26 | this.client = null!; 27 | this.app = app; 28 | } 29 | 30 | public async setup(): Promise { 31 | this.client = createClient({ 32 | socket: { 33 | host: this.app.config.db.redis.url, 34 | port: this.app.config.db.redis.port 35 | }, 36 | password: this.app.config.db.redis.password, 37 | 38 | }); 39 | 40 | await this.client.connect(); 41 | } 42 | 43 | public async set( 44 | collection: CacheType, 45 | key: string, 46 | value: CacheValue 47 | ): Promise { 48 | this.client.set(this.keyName(collection, key), JSON.stringify(value)); 49 | this.client.expire(this.keyName(collection, key), CacheDuration[collection] ?? DATABASE_CACHE_TTL); 50 | } 51 | 52 | public async get( 53 | collection: CacheType, 54 | key: string 55 | ): Promise { 56 | const raw: string | null = await this.client.get(this.keyName(collection, key)); 57 | if (raw === null) return null; 58 | 59 | return JSON.parse(raw); 60 | } 61 | 62 | public async delete( 63 | collection: CacheType, 64 | key: string 65 | ): Promise { 66 | this.client.del(this.keyName(collection, key)); 67 | } 68 | 69 | private keyName(collection: CacheType, key: string): string { 70 | return `${this.app.dev ? `dev:` : ""}${collection}:${key}`; 71 | } 72 | } -------------------------------------------------------------------------------- /src/turing/connection/packets/campaigns.ts: -------------------------------------------------------------------------------- 1 | import { Packet, PacketDirection, PacketSendOptions } from "../packet/packet.js"; 2 | import { DatabaseCampaign } from "../../../db/managers/campaign.js"; 3 | import { TuringConnectionManager } from "../connection.js"; 4 | 5 | type CampaignsPacketIncomingData = { 6 | action: "incrementStats"; 7 | id: string; 8 | } & Record 9 | 10 | export class UpdatePacket extends Packet { 11 | constructor(manager: TuringConnectionManager) { 12 | super(manager, { 13 | name: "campaigns", 14 | direction: PacketDirection.Incoming 15 | }); 16 | } 17 | 18 | public async handle(data: CampaignsPacketIncomingData): Promise { 19 | const { action, id } = data; 20 | 21 | if (action === "incrementStats") { 22 | /* Which statistics to increment */ 23 | const type: "views" | "clicks" = data.type; 24 | 25 | /* The location of the user, optional */ 26 | const location: string | null = data.geo ?? null; 27 | 28 | /* Make sure that the campaign exists, before updating it. */ 29 | const campaign: DatabaseCampaign | null = await this.manager.app.db.fetchFromCacheOrDatabase("campaigns", id); 30 | if (campaign === null) return; 31 | 32 | /* Updated total amount of statistics of this type */ 33 | const total: number = campaign.stats[type].total + 1; 34 | 35 | this.manager.app.db.metrics.change("campaigns", { 36 | [type]: { 37 | total: { [campaign.name]: total }, 38 | now: { [campaign.name]: "+1" } 39 | } 40 | }); 41 | 42 | await this.manager.app.db.campaign.update(campaign, { 43 | stats: { 44 | ...campaign.stats, 45 | 46 | [type]: { 47 | total, geo: { 48 | ...campaign.stats[type].geo, 49 | 50 | ...location !== null ? { 51 | [location]: campaign.stats[type]?.geo[location] + 1 ?? 1 52 | } : {} 53 | } 54 | } 55 | } 56 | }); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/commands/vote.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, SlashCommandBuilder } from "discord.js"; 2 | 3 | import { ConversationCooldownModifier, ConversationDefaultCooldown } from "../conversation/conversation.js"; 4 | import { Command, CommandResponse } from "../command/command.js"; 5 | import { Cooldown } from "../conversation/utils/cooldown.js"; 6 | import { DatabaseInfo } from "../db/managers/user.js"; 7 | import { Response } from "../command/response.js"; 8 | import { Bot } from "../bot/bot.js"; 9 | 10 | export default class VoteCommand extends Command { 11 | constructor(bot: Bot) { 12 | super(bot, 13 | new SlashCommandBuilder() 14 | .setName("vote") 15 | .setDescription("Vote for our bot & get rewards") 16 | ); 17 | } 18 | 19 | public async run(_: any, db: DatabaseInfo): CommandResponse { 20 | const fields = [ 21 | { 22 | key: "Way lower cool-down ⏰", 23 | value: `The cool-down between messages can get a bit annoying. By voting, it'll be reduced to only **${Math.round(Cooldown.calculate(ConversationDefaultCooldown.time, ConversationCooldownModifier.voter) / 1000)}** seconds.` 24 | }, 25 | 26 | { 27 | key: "Support our bot 🙏", 28 | value: "If you vote, you'll help us grow even further, and give people access to **ChatGPT** and other language models for completely free." 29 | } 30 | ]; 31 | 32 | const builder: EmbedBuilder = new EmbedBuilder() 33 | .setTitle("Vote for our bot <:topgg:1119699678343200879>") 34 | .setDescription(`*By voting for **${this.bot.client.user.username}**, you'll get the following rewards as long as you vote*.`) 35 | .setColor("#FF3366") 36 | 37 | .addFields(fields.map(({ key, value }) => ({ 38 | name: key, value: value.toString() 39 | }))); 40 | 41 | const row = new ActionRowBuilder() 42 | .addComponents( 43 | new ButtonBuilder() 44 | .setCustomId("general:vote") 45 | .setEmoji("🎉") 46 | .setLabel("Check your vote") 47 | .setStyle(ButtonStyle.Success), 48 | 49 | new ButtonBuilder() 50 | .setURL(this.bot.vote.link(db)) 51 | .setEmoji("<:topgg:1119699678343200879>") 52 | .setLabel("top.gg") 53 | .setStyle(ButtonStyle.Link) 54 | ); 55 | 56 | return new Response() 57 | .addEmbed(builder) 58 | .addComponent(ActionRowBuilder, row) 59 | .setEphemeral(true); 60 | } 61 | } -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import dayjs from "dayjs"; 3 | 4 | import { Bot } from "../bot/bot.js"; 5 | 6 | export type LogType = string | number | boolean | any 7 | 8 | interface LogLevel { 9 | name: string; 10 | color: string; 11 | } 12 | 13 | export const LogLevels: Record = { 14 | INFO: { name: "info", color: "#00aaff" }, 15 | WARN: { name: "warn", color: "#ffff00" }, 16 | ERROR: { name: "error", color: "#ff3300" }, 17 | DEBUG: { name: "debug", color: "#00ffaa" } 18 | } 19 | 20 | export class Logger { 21 | /** 22 | * Log a message to the console. 23 | * 24 | * @param level Log level 25 | * @param message Message to log to the console 26 | */ 27 | public log(level: LogLevel, message: LogType[]): void { 28 | const now: number = Math.floor(Date.now() / 1000); 29 | const time: string = dayjs.unix(now).format("hh:mm A"); 30 | 31 | const status: string = chalk.bold.hex(level.color)(level.name); 32 | const line: string = `${status} ${chalk.italic(chalk.gray(time))} ${chalk.gray("»")}`; 33 | 34 | /* Log the message to the console. */ 35 | this.print(line, ...message); 36 | } 37 | 38 | public debug(...message: LogType) { this.log(LogLevels.DEBUG, message); } 39 | public info(...message: LogType) { this.log(LogLevels.INFO, message); } 40 | public warn(...message: LogType) { this.log(LogLevels.WARN, message); } 41 | public error(...message: LogType) { this.log(LogLevels.ERROR, message); } 42 | 43 | protected print(...message: LogType): void { 44 | console.log(...message); 45 | } 46 | } 47 | 48 | export class ClusterLogger extends Logger { 49 | /* Discord client instance */ 50 | private readonly bot: Bot; 51 | 52 | constructor(bot: Bot) { 53 | super(); 54 | this.bot = bot; 55 | } 56 | 57 | /** 58 | * Log a message to the console. 59 | * 60 | * @param level Log level 61 | * @param message Message to log to the console 62 | */ 63 | public log(level: LogLevel, message: LogType[]): void { 64 | const now: number = Math.floor(Date.now() / 1000); 65 | const time: string = dayjs.unix(now).format("hh:mm A"); 66 | 67 | const status: string = chalk.bold.hex(level.color)(level.name); 68 | const line: string = `${chalk.green(chalk.bold(`#${this.bot?.data?.id + 1}`))} ${chalk.gray("»")} ${status} ${chalk.italic(chalk.gray(time))} ${chalk.gray("»")}`.trim(); 69 | 70 | /* Log the message to the console. */ 71 | this.print(line, ...message); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/conversation/utils/length.ts: -------------------------------------------------------------------------------- 1 | import { get_encoding } from "@dqbd/tiktoken"; 2 | export const encoder = get_encoding("cl100k_base"); 3 | 4 | import { OpenAIChatMessage } from "../../turing/types/openai/chat.js"; 5 | 6 | /* Maximum context history length in total */ 7 | export const MaxContextLength = { 8 | free: 800, 9 | voter: 850, 10 | subscription: 1000, 11 | plan: 0 12 | } 13 | 14 | /* Maximum generation length in total */ 15 | export const MaxGenerationLength = { 16 | free: 350, 17 | voter: 400, 18 | subscription: 650, 19 | plan: 0 20 | } 21 | 22 | /** 23 | * Get the length of a prompt. 24 | * @param content Prompt to check 25 | * 26 | * @returns Length of the prompt, in tokens 27 | */ 28 | export const getPromptLength = (content: string): number => { 29 | content = content.replaceAll("<|endoftext|>", "<|im_end|>").replaceAll("<|endofprompt|>", "<|im_end|>"); 30 | return encoder.encode(content).length; 31 | } 32 | 33 | /** 34 | * Whether the length of a prompt is "usable". 35 | * @param content Prompt to check 36 | * 37 | * @returns Whether the prompt is usable 38 | */ 39 | export const isPromptLengthAcceptable = (content: string, max: number): boolean => { 40 | return getPromptLength(content) < max; 41 | } 42 | 43 | export const getChatMessageLength = (...messages: OpenAIChatMessage[]): number => { 44 | /* Total tokens used for the messages */ 45 | let total: number = 0; 46 | 47 | for (const message of messages) { 48 | /* Map each property of the message to the number of tokens it contains. */ 49 | const propertyTokenCounts = Object.entries(message).map(([_, value]) => { 50 | /* Count the number of tokens in the property value. */ 51 | return getPromptLength(value); 52 | }); 53 | 54 | /* Sum the number of tokens in all properties and add 4 for metadata. */ 55 | total += propertyTokenCounts.reduce((a, b) => a + b, 4); 56 | } 57 | 58 | return total; 59 | } 60 | 61 | /** 62 | * Count the total amount of tokens that will be used for the API request. 63 | * @param messages Messages to account for 64 | * 65 | * @returns Total token count 66 | */ 67 | export const countChatMessageTokens = (messages: OpenAIChatMessage[]): number => { 68 | /* Total amount of tokens used for the chat messages */ 69 | const count: number = getChatMessageLength(...messages); 70 | 71 | /* Add 2 tokens for the metadata. */ 72 | return count + 2; 73 | } -------------------------------------------------------------------------------- /src/chat/models/openchat.ts: -------------------------------------------------------------------------------- 1 | import { OpenChatMessage, OpenChatModel } from "../../turing/types/openchat.js"; 2 | import { ChatModel, ModelCapability, ModelType } from "../types/model.js"; 3 | import { getPromptLength } from "../../conversation/utils/length.js"; 4 | import { ModelGenerationOptions } from "../types/options.js"; 5 | import { PartialResponseMessage } from "../types/message.js"; 6 | import { ChatClient } from "../client.js"; 7 | 8 | export class AnthropicModel extends ChatModel { 9 | constructor(client: ChatClient) { 10 | super(client, { 11 | name: "OpenChat", type: ModelType.OpenChat, 12 | capabilities: [ ModelCapability.UserLanguage ] 13 | }); 14 | } 15 | 16 | public async complete(options: ModelGenerationOptions): Promise { 17 | const prompt = await this.client.buildPrompt(options); 18 | const messages: OpenChatMessage[] = []; 19 | 20 | messages.push( 21 | { role: "user", content: "Who are you?" }, 22 | { role: "assistant", content: prompt.parts.Initial.content } 23 | ); 24 | 25 | if (prompt.parts.Personality) messages.push( 26 | { role: "user", content: "What will you act like?" }, 27 | { role: "assistant", content: prompt.parts.Personality.content } 28 | ); 29 | 30 | if (prompt.parts.Other) messages.push( 31 | { role: "user", content: "What will you also do?" }, 32 | { role: "assistant", content: prompt.parts.Other.content } 33 | ); 34 | 35 | /* Add all of the user & assistant interactions to the prompt. */ 36 | messages.push(...prompt.messages); 37 | 38 | /* Which model to use, depending on the context & generation limits */ 39 | const model: OpenChatModel = options.settings.options.settings.model ?? "openchat_v3.2"; 40 | 41 | const result = await this.client.manager.bot.turing.openChat({ 42 | messages, max_tokens: prompt.max, model, temperature: 0.3 43 | }, data => options.progress({ text: data.result })); 44 | 45 | return { 46 | raw: { 47 | finishReason: result.finishReason === "max_tokens" ? "length" : "stop", 48 | 49 | usage: { 50 | completion: getPromptLength(result.result), 51 | prompt: prompt.length 52 | } 53 | }, 54 | 55 | text: result.result 56 | }; 57 | } 58 | } -------------------------------------------------------------------------------- /src/image/types/image.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable } from "discord.js"; 2 | 3 | import { ImageGenerationRatio } from "../../commands/imagine.js"; 4 | import { ImageSampler } from "./sampler.js"; 5 | import { ImagePrompt } from "./prompt.js"; 6 | import { ImageModel } from "./model.js"; 7 | 8 | export interface ImageResult { 9 | id: string; 10 | seed: number; 11 | status: ImageStatus; 12 | } 13 | 14 | export type ImageRawResult = ImageResult & { 15 | base64: string; 16 | } 17 | 18 | export interface DatabaseImage { 19 | /* Unique identifier of the generation request */ 20 | id: string; 21 | 22 | /* Which model was used */ 23 | model: string; 24 | 25 | /* When the generation was completed */ 26 | created: string; 27 | 28 | /* Which action was performed */ 29 | action: ImageGenerationType; 30 | 31 | /* Which prompt was used to generate the image */ 32 | prompt: ImagePrompt; 33 | 34 | /* Generation options used for this image */ 35 | options: ImageGenerationBody; 36 | 37 | /* Generated image results */ 38 | results: ImageResult[]; 39 | 40 | /* How much this generation costs */ 41 | cost: number; 42 | } 43 | 44 | export type ImageGenerationType = "generate" | "upscale" 45 | 46 | export interface ImageGenerationBody { 47 | prompt: string; 48 | negative_prompt?: string; 49 | image?: string; 50 | width: number; 51 | height: number; 52 | steps: number; 53 | number: number; 54 | sampler?: ImageSampler; 55 | cfg_scale?: number; 56 | seed?: number; 57 | style?: string; 58 | model?: string; 59 | strength?: number; 60 | ratio: ImageGenerationRatio; 61 | } 62 | 63 | export interface ImageGenerationOptions { 64 | body: Partial; 65 | model: ImageModel; 66 | progress: (data: ImagePartialGenerationResult) => Awaitable; 67 | } 68 | 69 | type ImageStatus = "success" | "filtered" | "failed" 70 | 71 | export type ImageGenerationStatus = "queued" | "generating" | "done" | "failed" 72 | 73 | export interface ImagePartialGenerationResult { 74 | id: string; 75 | status: ImageGenerationStatus; 76 | results: ImageRawResult[]; 77 | progress: number | null; 78 | cost: number | null; 79 | error: string | null; 80 | } 81 | 82 | export interface ImageGenerationResult { 83 | id: string; 84 | status: ImageGenerationStatus; 85 | results: ImageRawResult[]; 86 | time: number | null; 87 | error: string | null; 88 | cost: number | null; 89 | } -------------------------------------------------------------------------------- /src/chat/models/anthropic.ts: -------------------------------------------------------------------------------- 1 | import { AnthropicChatMessage, AnthropicChatModel } from "../../turing/types/anthropic.js"; 2 | import { ChatModel, ModelCapability, ModelType } from "../types/model.js"; 3 | import { getPromptLength } from "../../conversation/utils/length.js"; 4 | import { ModelGenerationOptions } from "../types/options.js"; 5 | import { PartialResponseMessage } from "../types/message.js"; 6 | import { ChatClient } from "../client.js"; 7 | 8 | export class AnthropicModel extends ChatModel { 9 | constructor(client: ChatClient) { 10 | super(client, { 11 | name: "Anthropic", type: ModelType.Anthropic, 12 | capabilities: [ ModelCapability.UserLanguage, ModelCapability.ImageViewing ] 13 | }); 14 | } 15 | 16 | public async complete(options: ModelGenerationOptions): Promise { 17 | const prompt = await this.client.buildPrompt(options); 18 | const messages: AnthropicChatMessage[] = []; 19 | 20 | messages.push( 21 | { role: "user", content: "Who are you?" }, 22 | { role: "assistant", content: prompt.parts.Initial.content } 23 | ); 24 | 25 | if (prompt.parts.Personality) messages.push( 26 | { role: "user", content: "What will you act like?" }, 27 | { role: "assistant", content: prompt.parts.Personality.content } 28 | ); 29 | 30 | if (prompt.parts.Other) messages.push( 31 | { role: "user", content: "What will you also do?" }, 32 | { role: "assistant", content: prompt.parts.Other.content } 33 | ); 34 | 35 | /* Add all of the user & assistant interactions to the prompt. */ 36 | messages.push(...prompt.messages); 37 | 38 | /* Which model to use, depending on the context & generation limits */ 39 | const model: AnthropicChatModel = options.settings.options.settings.model ?? "claude-instant-1"; 40 | 41 | const result = await this.client.manager.bot.turing.anthropic({ 42 | messages, max_tokens: prompt.max, model, temperature: 0.3 43 | }, data => options.progress({ text: data.result })); 44 | 45 | return { 46 | raw: { 47 | finishReason: result.stop_reason === "max_tokens" ? "length" : "stop", 48 | 49 | usage: { 50 | completion: getPromptLength(result.result), 51 | prompt: prompt.length 52 | } 53 | }, 54 | 55 | text: result.result 56 | }; 57 | } 58 | } -------------------------------------------------------------------------------- /src/turing/types/openai/chat.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable } from "discord.js"; 2 | 3 | import { ChatSettingsPlugin, ChatSettingsPluginIdentifier } from "../../../conversation/settings/plugin.js"; 4 | import { DatabaseUser } from "../../../db/schemas/user.js"; 5 | import { TuringChatPluginsTool } from "./plugins.js"; 6 | 7 | type TuringOpenAIChatModel = "gpt-3.5-turbo" | "gpt-4" | string 8 | 9 | export interface TuringOpenAIChatOptions { 10 | /* Which model to use */ 11 | model: TuringOpenAIChatModel; 12 | 13 | /* Maximum amount of generation tokens */ 14 | tokens: number; 15 | 16 | /** What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. */ 17 | temperature?: number; 18 | 19 | /* OpenAI chat messages to send to the model */ 20 | messages: OpenAIChatMessage[]; 21 | 22 | /* Plugins to use for this request */ 23 | plugins?: ChatSettingsPlugin[]; 24 | 25 | /* Progress callback to call when a new token is generated */ 26 | progress?: (data: TuringOpenAIPartialResult) => Awaitable; 27 | } 28 | 29 | export interface TuringOpenAIChatBody { 30 | /** ID of the model to use */ 31 | model: TuringOpenAIChatModel; 32 | 33 | /* Previous chat history & instructions */ 34 | messages: OpenAIChatMessage[]; 35 | 36 | /** What sampling temperature to use. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. */ 37 | temperature?: number; 38 | 39 | /** An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered */ 40 | top_p?: number; 41 | 42 | /** Maximum number of tokens to generate in the completion */ 43 | max_tokens?: number; 44 | 45 | /* Which plugins to use */ 46 | plugins?: ChatSettingsPluginIdentifier[]; 47 | 48 | /* Whether streaming mode should be used */ 49 | stream?: boolean; 50 | } 51 | 52 | export interface OpenAIChatMessage { 53 | role: "system" | "assistant" | "user"; 54 | content: string; 55 | } 56 | 57 | export type TuringOpenAIPartialResult = TuringOpenAIResult 58 | 59 | export interface TuringOpenAIResult { 60 | result: string; 61 | done: boolean; 62 | cost: number; 63 | finishReason: "length" | "stop" | null; 64 | tool?: TuringChatPluginsTool; 65 | } -------------------------------------------------------------------------------- /src/commands/dev/eval.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable, SlashCommandBuilder } from "discord.js"; 2 | import { inspect } from "util"; 3 | 4 | import { Command, CommandInteraction, CommandResponse } from "../../command/command.js"; 5 | import { Response } from "../../command/response.js"; 6 | import { Bot } from "../../bot/bot.js"; 7 | 8 | export default class EvaluateCommand extends Command { 9 | constructor(bot: Bot) { 10 | super(bot, 11 | new SlashCommandBuilder() 12 | .setName("eval") 13 | .setDescription("Run the specified code snippet") 14 | .addStringOption(builder => builder 15 | .setName("code") 16 | .setDescription("Code snippet to run")) 17 | , { restriction: [ "owner" ] }); 18 | } 19 | 20 | private async clean(result: Awaitable): Promise { 21 | let content: string = result; 22 | 23 | /* If our input is a promise, await it before continuing. */ 24 | if (result && result instanceof Promise) content = await result; 25 | 26 | /* If the response isn't a string, `util.inspect()` 27 | is used to 'stringify' the code in a safe way that 28 | won't error out on objects with circular references. */ 29 | if (typeof result !== "string") content = inspect(result, { depth: 1 }); 30 | 31 | /* Replace symbols with character code alternatives. */ 32 | content = content 33 | .replace(/`/g, "`" + String.fromCharCode(8203)) 34 | .replace(/@/g, "@" + String.fromCharCode(8203)); 35 | 36 | return content; 37 | } 38 | 39 | public async run(interaction: CommandInteraction): CommandResponse { 40 | /* Code snippet to execute */ 41 | const snippet: string = interaction.options.getString("code", true); 42 | 43 | try { 44 | /* Variables passed to the snippet */ 45 | const channel = interaction.channel; 46 | const guild = interaction.guild; 47 | const bot = this.bot; 48 | 49 | /* Evaluate the passed code snippet. */ 50 | const result = eval(snippet); 51 | 52 | /* Clean up the result. */ 53 | const cleaned: string = await this.clean(result); 54 | 55 | return new Response() 56 | .addEmbed(builder => builder 57 | .setDescription(cleaned.length > 0 ? `\`\`\`\n${cleaned}\n\`\`\`` : "*no output*") 58 | .setFooter({ text: snippet }) 59 | .setColor("White") 60 | ) 61 | .setEphemeral(true); 62 | 63 | } catch (error) { 64 | return new Response() 65 | .addEmbed(builder => 66 | builder.setTitle("Failed to execute ⚠️") 67 | .setDescription(`\`\`\`\n${(error as Error).toString()}\n\`\`\``) 68 | .setColor("Red") 69 | .setTimestamp() 70 | ) 71 | .setEphemeral(true); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/chat/types/options.ts: -------------------------------------------------------------------------------- 1 | import { Guild, GuildMember, Message, TextChannel } from "discord.js"; 2 | 3 | import { ResponseChatNoticeMessage, PartialResponseMessage, ResponseMessage } from "./message.js"; 4 | import { ChatSettingsModel } from "../../conversation/settings/model.js"; 5 | import { ChatSettingsTone } from "../../conversation/settings/tone.js"; 6 | import { Conversation } from "../../conversation/conversation.js"; 7 | import { ChatMediaHandlerRunOptions } from "../media/handler.js"; 8 | import { DatabaseInfo } from "../../db/managers/user.js"; 9 | import { ChatBaseImage } from "../media/types/image.js"; 10 | import { ChatMedia } from "../media/types/media.js"; 11 | import { ChatModel } from "./model.js"; 12 | 13 | export type ModelGenerationOptions = Pick & { 14 | /* Which model is being used for generation */ 15 | model: ChatModel; 16 | 17 | /* Which settings model is being used */ 18 | settings: ChatSettingsModel; 19 | 20 | /* Function to call on partial message generation */ 21 | progress: (message: PartialResponseMessage | ResponseChatNoticeMessage) => Promise | void; 22 | 23 | /* List of attached images */ 24 | media: ChatMedia[]; 25 | } 26 | 27 | export interface ChatGuildData { 28 | /* Guild, where the chat interaction occurred */ 29 | guild: Guild; 30 | 31 | /* Channel, where the chat interaction occurred */ 32 | channel: TextChannel; 33 | 34 | /* Owner of the guild, as a user */ 35 | owner: GuildMember; 36 | 37 | /* Guild member instance of the user */ 38 | member: GuildMember; 39 | } 40 | 41 | export interface ChatGenerationOptions { 42 | /* Function to call on partial message generation */ 43 | progress?: (message: ResponseMessage) => Promise | void; 44 | 45 | /* Which conversation this generation request is for */ 46 | conversation: Conversation; 47 | 48 | /* Guild, where this request was executed from */ 49 | guild: ChatGuildData | null; 50 | 51 | /* Discord message that invoked the generation */ 52 | trigger: Message; 53 | 54 | /* Database instances */ 55 | db: DatabaseInfo; 56 | 57 | /* Whether partial messages should be generated */ 58 | partial: boolean; 59 | 60 | /* Prompt to ask */ 61 | prompt: string; 62 | } 63 | 64 | export interface ChatResetOptions { 65 | /* Which conversation this reset request is for */ 66 | conversation: Conversation; 67 | 68 | /* Which settings model is being used */ 69 | model: ChatSettingsModel; 70 | 71 | /* Which settings tone is being used */ 72 | tone: ChatSettingsTone; 73 | } 74 | 75 | export type GPTImageAnalyzeOptions = ChatMediaHandlerRunOptions & { 76 | /* Message attachment to analyze */ 77 | attachment: ChatBaseImage; 78 | } -------------------------------------------------------------------------------- /src/chat/models/llama.ts: -------------------------------------------------------------------------------- 1 | import { LLaMAChatMessage, LLaMAChatResult, LLaMAPartialChatResult } from "../../turing/types/llama.js"; 2 | import { ChatModel, ModelCapability, ModelType } from "../types/model.js"; 3 | import { MessageType, PartialResponseMessage } from "../types/message.js"; 4 | import { getPromptLength } from "../../conversation/utils/length.js"; 5 | import { ModelGenerationOptions } from "../types/options.js"; 6 | import { ChatClient, PromptData } from "../client.js"; 7 | 8 | export class LLaMAModel extends ChatModel { 9 | constructor(client: ChatClient) { 10 | super(client, { 11 | name: "LLaMA", type: ModelType.LLaMA, 12 | capabilities: [ ModelCapability.ImageViewing, ModelCapability.UserLanguage ] 13 | }); 14 | } 15 | 16 | private process(data: LLaMAPartialChatResult | LLaMAChatResult, prompt: PromptData): PartialResponseMessage { 17 | if (data.status === "queued") return { 18 | type: MessageType.Notice, 19 | text: "Waiting in queue" 20 | }; 21 | 22 | let content: string = data.result.trim(); 23 | if (content.includes("User:")) content = content.split("User:")[0]; 24 | 25 | if (content.length === 0) return { 26 | type: MessageType.Notice, 27 | text: "Generating" 28 | }; 29 | 30 | return { 31 | raw: { 32 | usage: { 33 | completion: getPromptLength(data.result), 34 | prompt: prompt.length 35 | }, 36 | 37 | cost: data.cost > 0 ? data.cost : undefined 38 | }, 39 | 40 | text: content 41 | }; 42 | } 43 | 44 | public async complete(options: ModelGenerationOptions): Promise { 45 | const prompt = await this.client.buildPrompt(options); 46 | const messages: LLaMAChatMessage[] = [ { role: "system", content: prompt.parts.Initial.content } ]; 47 | 48 | if (prompt.parts.Personality) messages.push( 49 | { role: "user", content: "What will you act like for the entire conversation?" }, 50 | { role: "assistant", content: prompt.parts.Personality.content } 51 | ); 52 | 53 | if (prompt.parts.Other) messages.push( 54 | { role: "user", content: "What will you also acknowlewdge?" }, 55 | { role: "assistant", content: prompt.parts.Other.content } 56 | ); 57 | 58 | /* Add all of the user & assistant interactions to the prompt. */ 59 | messages.push(...prompt.messages); 60 | 61 | const result = await this.client.manager.bot.turing.llama({ 62 | messages, max_tokens: prompt.max, temperature: 0.4 63 | }, data => options.progress(this.process(data, prompt))); 64 | 65 | return this.process(result, prompt); 66 | } 67 | } -------------------------------------------------------------------------------- /src/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "discord": { 3 | "token": "Discord bot token", 4 | "id": "Discord bot ID", 5 | 6 | "owner": [ "Your user ID" ], 7 | "inviteCode": "Invite code to support server, e.g. turing" 8 | }, 9 | 10 | "branding": { 11 | "noticeColor": "This is the color used for most embeds", 12 | "color": "#5765F2" 13 | }, 14 | 15 | "noticeMetrics": "Whether metrics about usage, cool-down, guilds, users, etc. should be collected in the database", 16 | "metrics": true, 17 | 18 | "noticeDev": "Whether developer info should be logged", 19 | "dev": false, 20 | 21 | "clusters": "auto", 22 | "shardsPerCluster": 3, 23 | 24 | "channels": { 25 | "moderation": { 26 | "channel": "channel id", 27 | "guild": "guild id" 28 | }, 29 | 30 | "error": { 31 | "channel": "channel id", 32 | "guild": "guild id" 33 | }, 34 | 35 | "status": { 36 | "channel": "channel id", 37 | "guild": "guild id" 38 | } 39 | }, 40 | 41 | "openAI": { 42 | "key": "OpenAI API key" 43 | }, 44 | 45 | "replicate": { 46 | "key": "Replicate API key" 47 | }, 48 | 49 | "ocr": { 50 | "key": "https://ocr.space API key" 51 | }, 52 | 53 | "huggingFace": { 54 | "key": "HuggingFace session key" 55 | }, 56 | 57 | "topgg": { 58 | "key": "top.gg webhook key" 59 | }, 60 | 61 | "turing": { 62 | "key": "Turing API key @ https://github.com/TuringAI-Team/turing-ai-api", 63 | "super": "Turing API super key @ https://github.com/TuringAI-Team/turing-ai-api", 64 | "captchas": { 65 | "turnstile": "Turing Turnstile verification key" 66 | }, 67 | "urls": { 68 | "prod": "URL to the hosted Turing API" 69 | } 70 | }, 71 | 72 | "gif": { 73 | "tenor": "Tenor API key" 74 | }, 75 | 76 | "stableHorde": { 77 | "key": "Stable Horde API key" 78 | }, 79 | 80 | "rabbitMQ": { 81 | "url": "URL to RabbitMQ instance" 82 | }, 83 | 84 | "db": { 85 | "supabase": { 86 | "url": "Supabase URL", 87 | "key": { 88 | "service": "SupaBase service API key" 89 | }, 90 | "collections": { 91 | "users": "users", 92 | "conversations": "conversations", 93 | "guilds": "guilds", 94 | "interactions": "interactions", 95 | "images": "images", 96 | "keys": "keys" 97 | } 98 | }, 99 | "redis": { 100 | "url": "Redis URL", 101 | "password": "Redis password", 102 | "port": 12345 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/image/utils/merge.ts: -------------------------------------------------------------------------------- 1 | import { Image, createCanvas } from "@napi-rs/canvas"; 2 | import { readFile } from "fs/promises"; 3 | 4 | import { StorageImage } from "../../db/managers/storage.js"; 5 | import { ImageBuffer } from "../../util/image.js"; 6 | import { DatabaseImage } from "../types/image.js"; 7 | import { Utils } from "../../util/utils.js"; 8 | import { Bot } from "../../bot/bot.js"; 9 | 10 | type AssetName = "censored" | "warning" 11 | 12 | /* All special images */ 13 | const Assets: Record = { 14 | /* This will be loaded when the app starts ... */ 15 | } as any 16 | 17 | const loadAssets = async (): Promise => { 18 | const paths: string[] = await Utils.search("./assets/imagine"); 19 | 20 | for (const path of paths) { 21 | const name: AssetName = Utils.baseName(Utils.fileName(path)) as AssetName; 22 | 23 | const buffer: Buffer = await readFile(path); 24 | Assets[name] = buffer; 25 | } 26 | } 27 | 28 | /* Load all assets from the directory. */ 29 | loadAssets(); 30 | 31 | /** 32 | * Render the specified image generation results into a single image, to display in a Discord embed. 33 | * 34 | * @param options Image generation options, used to determine width & height 35 | * @param result Image results 36 | * 37 | * @returns A buffer, containing the final merged PNG image 38 | */ 39 | export const renderIntoSingleImage = async (bot: Bot, db: DatabaseImage): Promise => { 40 | /* Fetch all the images. */ 41 | const images: Buffer[] = await Promise.all(db.results.map(async image => { 42 | if (image.status === "filtered") return Assets.censored; 43 | else if (image.status === "failed") return Assets.warning; 44 | 45 | const storage: StorageImage = bot.image.url(db, image); 46 | const data: ImageBuffer = (await Utils.fetchBuffer(storage.url))!; 47 | 48 | return data.buffer; 49 | })); 50 | 51 | /* If there's only a single image, simply return that one instead of creating a canvas to only render one. */ 52 | if (images.length === 1) return images[0]; 53 | 54 | /* How many images to display per row, maximum */ 55 | const perRow: number = images.length > 4 ? 4 : 2; 56 | const rows: number = Math.ceil(images.length / perRow); 57 | 58 | /* Width & height of the canvas */ 59 | const width: number = db.options.width * perRow; 60 | const height: number = rows * db.options.height; 61 | 62 | const canvas = createCanvas(width, height); 63 | const context = canvas.getContext("2d"); 64 | 65 | images.forEach((result, index) => { 66 | const x: number = (index % perRow) * db.options.width; 67 | const y: number = Math.floor(index / perRow) * db.options.height; 68 | 69 | const image: Image = new Image(); 70 | image.src = result; 71 | 72 | context.drawImage(image, x, y, db.options.width, db.options.height); 73 | }); 74 | 75 | return await canvas.encode("png") 76 | } -------------------------------------------------------------------------------- /src/db/managers/role.ts: -------------------------------------------------------------------------------- 1 | import { SubClusterDatabaseManager } from "../sub.js"; 2 | import { DatabaseUser } from "../schemas/user.js"; 3 | 4 | export type UserRole = "tester" | "api" | "investor" | "advertiser" | "moderator" | "owner" 5 | export type UserRoles = UserRole[] 6 | 7 | export const UserRoleHierarchy: UserRoles = [ 8 | "owner", "moderator", "investor", "advertiser", "api", "tester" 9 | ] 10 | 11 | interface UserRoleChanges { 12 | /* Roles to add */ 13 | add?: UserRoles; 14 | 15 | /* Roles to remove */ 16 | remove?: UserRoles; 17 | } 18 | 19 | export enum UserHasRoleCheck { 20 | /* The user must have all specified roles */ 21 | All, 22 | 23 | /* The user must have one of the specified roels */ 24 | Some, 25 | 26 | /* The user must not have all the specified */ 27 | NotAll, 28 | 29 | /* The user must not have some of the roles */ 30 | NotSome 31 | } 32 | 33 | export class UserRoleManager extends SubClusterDatabaseManager { 34 | public roles(user: DatabaseUser): UserRoles { 35 | return user.roles; 36 | } 37 | 38 | public has(user: DatabaseUser, role: UserRole | UserRole[], check: UserHasRoleCheck = UserHasRoleCheck.All): boolean { 39 | if (Array.isArray(role)) { 40 | if (check === UserHasRoleCheck.All) return role.every(r => user.roles.includes(r)); 41 | else if (check === UserHasRoleCheck.Some) return role.some(r => user.roles.includes(r)); 42 | else if (check === UserHasRoleCheck.NotAll) return role.every(r => !user.roles.includes(r)); 43 | else if (check === UserHasRoleCheck.NotSome) return role.some(r => !user.roles.includes(r)); 44 | 45 | return false; 46 | } else return user.roles.includes(role); 47 | } 48 | 49 | /* Shortcuts */ 50 | public moderator(user: DatabaseUser): boolean { return this.has(user, "moderator"); } 51 | public tester(user: DatabaseUser): boolean { return this.has(user, "tester"); } 52 | public owner(user: DatabaseUser): boolean { return this.has(user, "owner"); } 53 | public investor(user: DatabaseUser): boolean { return this.has(user, "investor"); } 54 | public advertiser(user: DatabaseUser): boolean { return this.has(user, "advertiser"); } 55 | public api(user: DatabaseUser): boolean { return this.has(user, "api"); } 56 | 57 | public async toggle(user: DatabaseUser, role: UserRole, status?: boolean): Promise { 58 | return await this.change(user, { 59 | [status ?? !this.has(user, role) ? "add" : "remove"]: [ "tester" ] 60 | }); 61 | } 62 | 63 | public async change(user: DatabaseUser, changes: UserRoleChanges): Promise { 64 | /* Current roles */ 65 | let updated: UserRoles = user.roles; 66 | 67 | if (changes.remove) updated = updated.filter(r => !changes.remove!.includes(r)); 68 | if (changes.add) updated.push(...changes.add.filter(r => !updated.includes(r))); 69 | 70 | return this.db.users.updateUser(user, { 71 | roles: updated 72 | }); 73 | } 74 | } -------------------------------------------------------------------------------- /src/commands/bot.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, EmbedBuilder, SlashCommandBuilder } from "discord.js"; 2 | import { getInfo } from "discord-hybrid-sharding"; 3 | 4 | import { Command, CommandInteraction, CommandResponse } from "../command/command.js"; 5 | import { Introduction } from "../util/introduction.js"; 6 | import { Response } from "../command/response.js"; 7 | import { Bot } from "../bot/bot.js"; 8 | 9 | export default class StatisticsCommand extends Command { 10 | constructor(bot: Bot) { 11 | super(bot, 12 | new SlashCommandBuilder() 13 | .setName("bot") 14 | .setDescription("View information & statistics about the bot") 15 | , { always: true, waitForStart: true }); 16 | } 17 | 18 | public async run(interaction: CommandInteraction): CommandResponse { 19 | const fields = [ 20 | { 21 | key: "Servers 🖥️", 22 | value: `${new Intl.NumberFormat("en-US").format(this.bot.statistics.guildCount)}` 23 | }, 24 | 25 | { 26 | key: interaction.guild !== null ? "Cluster & Shard 💎" : "Cluster 💎", 27 | value: `\`${this.bot.data.id + 1}\`/\`${this.bot.client.cluster.count}\`${interaction.guild !== null ? `— \`${interaction.guild.shardId + 1}\`/\`${getInfo().TOTAL_SHARDS}\`` : ""}` 28 | }, 29 | 30 | { 31 | key: "Latency 🏓", 32 | value: `**\`${this.bot.statistics.discordPing.toFixed(1)}\`** ms` 33 | }, 34 | 35 | { 36 | key: "Users 🫂", 37 | value: `${new Intl.NumberFormat("en-US").format(this.bot.statistics.discordUsers)} <:discord:1097815072602067016> — ${new Intl.NumberFormat("en-US").format(this.bot.statistics.databaseUsers)} <:chatgpt_blurple:1081530335306727545>` 38 | }, 39 | 40 | { 41 | key: "RAM 🖨️", 42 | value: `**\`${(this.bot.statistics.memoryUsage / 1024 / 1024).toFixed(2)}\`** MB` 43 | }, 44 | 45 | { 46 | key: "Version 🔃", 47 | value: this.bot.statistics.commit !== null ? `[\`${this.bot.statistics.commit.hash.slice(undefined, 8)}\`](https://github.com/TuringAI-Team/chatgpt-discord-bot/commit/${this.bot.statistics.commit.hash})` : "❓" 48 | }, 49 | ]; 50 | 51 | const response: Response = new Response() 52 | .addComponent(ActionRowBuilder, Introduction.buttons(this.bot)); 53 | 54 | response.addEmbed(builder => builder 55 | .setTitle("Bot Statistics") 56 | .setColor(this.bot.branding.color) 57 | .setTimestamp(this.bot.statistics.since) 58 | 59 | .addFields(fields.map(({ key, value }) => ({ 60 | name: key, value: value.toString(), inline: true 61 | }))) 62 | ); 63 | 64 | /* If there are partners set in the configuration, display them here. */ 65 | if (this.bot.branding.partners && this.bot.branding.partners.length > 0) { 66 | const embed: EmbedBuilder = new EmbedBuilder() 67 | .setTitle("Partners 🤝") 68 | .setColor(this.bot.branding.color) 69 | .setDescription(this.bot.branding.partners.map(p => `${p.emoji ? `${p.emoji} ` : ""}[**${p.name}**](${p.url})${p.description ? ` — *${p.description}*` : ""}`).join("\n")); 70 | 71 | response.addEmbed(embed); 72 | } 73 | 74 | return response; 75 | } 76 | } -------------------------------------------------------------------------------- /src/commands/settings.ts: -------------------------------------------------------------------------------- 1 | import { GuildMember, SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../command/command.js"; 4 | import { SettingsCategory, SettingsLocation } from "../db/managers/settings.js"; 5 | import { ErrorResponse } from "../command/response/error.js"; 6 | import { DatabaseInfo } from "../db/managers/user.js"; 7 | import { Emoji } from "../util/emoji.js"; 8 | import { Bot } from "../bot/bot.js"; 9 | 10 | export default class SettingsCommand extends Command { 11 | constructor(bot: Bot) { 12 | super(bot, new SlashCommandBuilder() 13 | .setName("settings") 14 | .setDescription("Customize the bot to your liking") 15 | ); 16 | 17 | (this.builder as SlashCommandBuilder) 18 | .addSubcommand(builder => builder 19 | .setName("me") 20 | .setDescription("Customize the bot for yourself") 21 | .addStringOption(builder => builder 22 | .setName("category") 23 | .setDescription("Which category to view") 24 | .addChoices(...this.bot.db.settings.categories(SettingsLocation.User).map(c => ({ 25 | name: `${c.name} ${Emoji.display(c.emoji)}`, 26 | value: c.type 27 | }))) 28 | ) 29 | ) 30 | .addSubcommand(builder => builder 31 | .setName("server") 32 | .setDescription("Customize the bot for your entire server") 33 | .addStringOption(builder => builder 34 | .setName("category") 35 | .setDescription("Which category to view") 36 | .addChoices(...this.bot.db.settings.categories(SettingsLocation.Guild).map(c => ({ 37 | name: `${c.name} ${Emoji.display(c.emoji)}`, 38 | value: c.type 39 | }))) 40 | ) 41 | ); 42 | } 43 | 44 | public async run(interaction: CommandInteraction, db: DatabaseInfo): CommandResponse { 45 | /* Which settings type to view */ 46 | const action: "server" | "me" = interaction.options.getSubcommand(true) as any; 47 | const type: SettingsLocation = action === "me" ? SettingsLocation.User : SettingsLocation.Guild; 48 | 49 | if (type === SettingsLocation.Guild && db.guild === null) return new ErrorResponse({ 50 | interaction, message: "You can only view & change guild settings on **servers**", emoji: "😔" 51 | }); 52 | 53 | /* The user's permissions */ 54 | const permissions = interaction.member instanceof GuildMember ? interaction.member.permissions : null; 55 | 56 | if (type === SettingsLocation.Guild && (!permissions || !permissions.has("ManageGuild"))) return new ErrorResponse({ 57 | interaction, message: "You must have the `Manage Server` permission to view & change guild settings", emoji: "😔" 58 | }); 59 | 60 | /* Name of the category to view, optional */ 61 | const categoryName: string | null = interaction.options.getString("category", false); 62 | 63 | const category: SettingsCategory = categoryName !== null 64 | ? this.bot.db.settings.categories(type).find(c => c.type === categoryName)! 65 | : this.bot.db.settings.categories(type)[0]; 66 | 67 | return this.bot.db.settings.buildPage({ 68 | category, db: type === SettingsLocation.Guild ? db.guild! : db.user, interaction 69 | }); 70 | } 71 | } -------------------------------------------------------------------------------- /src/turing/dataset.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle } from "discord.js"; 2 | import { randomUUID } from "crypto"; 3 | 4 | import { InteractionHandlerResponse, InteractionHandlerRunOptions } from "../interaction/handler.js"; 5 | import { DatasetInteractionHandlerData } from "../interactions/dataset.js"; 6 | import { TuringAPI } from "./api.js"; 7 | 8 | export type DatasetName = "text" | "image" | "music" | "describe" 9 | 10 | interface DatasetToolbarOptions { 11 | dataset: DatasetName; 12 | id: string; 13 | selected?: number; 14 | } 15 | 16 | interface DatasetRateOptions { 17 | dataset: DatasetName; 18 | id: string; 19 | rating: number; 20 | } 21 | 22 | interface DatasetRateBody { 23 | dataset: DatasetName; 24 | id: string; 25 | rate: number; 26 | } 27 | 28 | interface DatasetRateChoice { 29 | emoji: string; 30 | value: number; 31 | } 32 | 33 | export const DatasetRateChoices: DatasetRateChoice[] = [ 34 | { emoji: "👍", value: 1 }, 35 | { emoji: "👎", value: 0 } 36 | ] 37 | 38 | export class TuringDatasetManager { 39 | private readonly api: TuringAPI; 40 | 41 | constructor(api: TuringAPI) { 42 | this.api = api; 43 | } 44 | 45 | public async rate({ dataset, id, rating }: DatasetRateOptions): Promise { 46 | await this.api.request({ 47 | path: "dataset/rate", method: "POST", body: { 48 | dataset, id, rate: rating 49 | } as DatasetRateBody 50 | }).catch(() => {}); 51 | } 52 | 53 | public buildRateButtons({ dataset, id, selected }: DatasetToolbarOptions): ButtonBuilder[] { 54 | const buttons: ButtonBuilder[] = []; 55 | 56 | for (const choice of DatasetRateChoices) { 57 | const current: boolean = choice.value === selected; 58 | 59 | buttons.push(new ButtonBuilder() 60 | .setEmoji(choice.emoji) 61 | .setDisabled(selected != undefined) 62 | .setStyle(current ? ButtonStyle.Primary : ButtonStyle.Secondary) 63 | .setCustomId(`dataset:rate:${dataset}:${id}:${choice.value}`) 64 | ); 65 | } 66 | 67 | if (selected != undefined) buttons.push(new ButtonBuilder() 68 | .setLabel("Thanks for your feedback!").setEmoji("🎉") 69 | .setCustomId(randomUUID()) 70 | .setStyle(ButtonStyle.Secondary) 71 | .setDisabled(true) 72 | ); 73 | 74 | return buttons; 75 | } 76 | 77 | public buildRateToolbar(options: DatasetToolbarOptions): ActionRowBuilder { 78 | return new ActionRowBuilder() 79 | .setComponents(this.buildRateButtons(options)); 80 | } 81 | 82 | public async handleInteraction({ interaction, data: { dataset, id, rating } }: InteractionHandlerRunOptions): InteractionHandlerResponse { 83 | await this.rate({ dataset, id, rating }); 84 | 85 | await interaction.update({ 86 | components: [ this.buildRateToolbar({ 87 | dataset, id, selected: rating 88 | }) ] 89 | }); 90 | } 91 | } -------------------------------------------------------------------------------- /src/conversation/utils/cooldown.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "events"; 2 | 3 | import { Conversation } from "../conversation.js"; 4 | 5 | export interface CooldownOptions { 6 | /* Which conversation this cooldown is for */ 7 | conversation: Conversation; 8 | } 9 | 10 | export interface CooldownState { 11 | active: boolean; 12 | 13 | startedAt: number | null; 14 | expiresIn: number | null; 15 | } 16 | 17 | export interface CooldownModifier { 18 | /* Straight-forward duration of the cool-down, in milliseconds */ 19 | time?: number; 20 | 21 | /* Multiplier modifier of the default cooldown */ 22 | multiplier?: number; 23 | } 24 | 25 | export class Cooldown extends EventEmitter { 26 | /* Information about the cooldown */ 27 | public readonly options: CooldownOptions; 28 | 29 | /* Whether the cooldown is active */ 30 | public state: CooldownState; 31 | 32 | /* Timer for the cooldown */ 33 | private timer: NodeJS.Timeout | null; 34 | 35 | constructor(options: CooldownOptions) { 36 | super(); 37 | this.options = options; 38 | 39 | this.state = { active: false, startedAt: null, expiresIn: null }; 40 | this.timer = null; 41 | } 42 | 43 | /** 44 | * Activate the cooldown. 45 | * @param time Expiration time override to use 46 | * 47 | * @returns When the cooldown expires 48 | */ 49 | public use(time: number): number { 50 | /* Set up the time-out. */ 51 | this.timer = setTimeout(() => { 52 | this.state = { 53 | active: false, 54 | 55 | startedAt: null, 56 | expiresIn: null 57 | }; 58 | 59 | }, time); 60 | 61 | this.state = { 62 | active: true, 63 | 64 | startedAt: Date.now(), 65 | expiresIn: time 66 | }; 67 | 68 | return this.state.expiresIn!; 69 | } 70 | 71 | public get active(): boolean { 72 | return this.state && this.state.active; 73 | } 74 | 75 | public get remaining(): number { 76 | if (!this.state.active) return -1; 77 | return this.state.startedAt! + (this.state.expiresIn!) - Date.now(); 78 | } 79 | 80 | /** 81 | * Cancel the currently running cooldown, if there is any in the first place. 82 | * @returns Whether a cool-down was stopped 83 | */ 84 | public cancel(): boolean { 85 | /* Whether a cool-down is currently active. */ 86 | const active: boolean = this.state.active; 87 | if (!active) return false; 88 | 89 | /* Stop the cool-down. */ 90 | this.state = { 91 | active: false, 92 | 93 | startedAt: null, 94 | expiresIn: null 95 | }; 96 | 97 | clearTimeout(this.timer!); 98 | return true; 99 | } 100 | 101 | public static calculate(input: number, modifier: CooldownModifier): number { 102 | if (modifier.multiplier) return input * modifier.multiplier; 103 | else if (modifier.time) return modifier.time; 104 | 105 | throw new Error("Either multiplier or time must be given"); 106 | } 107 | } -------------------------------------------------------------------------------- /src/db/schemas/guild.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable, Guild, Snowflake } from "discord.js"; 2 | 3 | import { DatabaseInfraction } from "../../moderation/types/infraction.js"; 4 | import { DatabaseSettings, DatabaseSubscription } from "./user.js"; 5 | import { SettingsLocation } from "../managers/settings.js"; 6 | import { type AppDatabaseManager } from "../app.js"; 7 | import { DatabasePlan } from "../managers/plan.js"; 8 | import { DatabaseSchema } from "./schema.js"; 9 | 10 | export type DatabaseGuildMetadataKey = "tags" | "language" 11 | export const DatabaseGuildMetadataKeys: DatabaseGuildMetadataKey[] = [ "tags", "language" ] 12 | 13 | export type DatabaseGuildMetadata = { 14 | tags: string[]; 15 | language: string; 16 | } 17 | 18 | export type DatabaseGuildSubscription = DatabaseSubscription & { 19 | /* Who redeemed the subscription key for the server */ 20 | by: Snowflake; 21 | } 22 | 23 | export interface DatabaseGuild { 24 | /* Identifier of the Discord guild */ 25 | id: Snowflake; 26 | 27 | /* When the guild was added to the database */ 28 | created: string; 29 | 30 | /* The guild's configured settings */ 31 | settings: DatabaseSettings; 32 | 33 | /* The guild's infractions */ 34 | infractions: DatabaseInfraction[]; 35 | 36 | /* The guilds's metadata */ 37 | metadata: DatabaseGuildMetadata; 38 | 39 | /* Information about the guild's subscription */ 40 | subscription: DatabaseGuildSubscription | null; 41 | 42 | /* Information bout the guild's pay-as-you-go plan */ 43 | plan: DatabasePlan | null; 44 | } 45 | 46 | export class GuildSchema extends DatabaseSchema { 47 | constructor(db: AppDatabaseManager) { 48 | super(db, { 49 | collection: "guilds" 50 | }); 51 | } 52 | 53 | public async process(guild: DatabaseGuild): Promise { 54 | guild.plan = this.db.plan.active(guild) ? this.db.plan.get(guild) : null; 55 | guild.subscription = this.db.schema("users").subscription(guild); 56 | 57 | guild.settings = this.db.settings.load(guild); 58 | guild.metadata = this.metadata(guild); 59 | 60 | return guild; 61 | } 62 | 63 | public metadata(entry: DatabaseGuild): DatabaseGuildMetadata { 64 | const final: Partial = {}; 65 | 66 | for (const key of DatabaseGuildMetadataKeys) { 67 | final[key] = entry.metadata ? entry.metadata[key] as any : undefined; 68 | } 69 | 70 | return final as DatabaseGuildMetadata; 71 | } 72 | 73 | public metadataTemplate(): DatabaseGuildMetadata { 74 | const final: Partial = {}; 75 | 76 | for (const key of DatabaseGuildMetadataKeys) { 77 | final[key] = undefined; 78 | } 79 | 80 | return final as DatabaseGuildMetadata; 81 | } 82 | 83 | public template(id: string): Awaitable { 84 | return { 85 | id, 86 | created: new Date().toISOString(), 87 | subscription: null, plan: null, 88 | settings: this.db.settings.template(SettingsLocation.Guild), 89 | metadata: this.metadataTemplate(), 90 | infractions: [] 91 | }; 92 | } 93 | } -------------------------------------------------------------------------------- /src/db/types/locale.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseUser } from "../schemas/user.js"; 2 | import { Bot } from "../../bot/bot.js"; 3 | 4 | export interface UserLanguage { 5 | /* Name of the language */ 6 | name: string; 7 | 8 | /* Name of the language, for the chat model */ 9 | modelName?: string; 10 | 11 | /* ISO code of the language */ 12 | id: string; 13 | 14 | /* Display emoji of the language */ 15 | emoji: string; 16 | } 17 | 18 | export const UserLanguages: UserLanguage[] = [ 19 | { 20 | name: "English", id: "en-US", emoji: "🇬🇧" 21 | }, 22 | 23 | { 24 | name: "Spanish", id: "es-ES", emoji: "🇪🇸" 25 | }, 26 | 27 | { 28 | name: "Brazilian Portuguese", id: "pt-BR", emoji: "🇧🇷" 29 | }, 30 | 31 | { 32 | name: "Portuguese", id: "pt-PT", emoji: "🇵🇹", modelName: "European Portuguese" 33 | }, 34 | 35 | { 36 | name: "French", id: "fr-FR", emoji: "🇫🇷" 37 | }, 38 | 39 | { 40 | name: "German", id: "de-DE", emoji: "🇩🇪" 41 | }, 42 | 43 | { 44 | name: "Italian", id: "it-IT", emoji: "🇮🇹" 45 | }, 46 | 47 | { 48 | name: "Polish", id: "pl", emoji: "🇵🇱" 49 | }, 50 | 51 | { 52 | name: "Russian", id: "ru-RU", emoji: "🇷🇺" 53 | }, 54 | 55 | { 56 | name: "Bulgarian", id: "bg", emoji: "🇧🇬" 57 | }, 58 | 59 | { 60 | name: "Czech", id: "cs", emoji: "🇨🇿" 61 | }, 62 | 63 | { 64 | name: "Japanese", id: "jp-JP", emoji: "🇯🇵" 65 | }, 66 | 67 | { 68 | name: "Chinese", id: "zh-CN", emoji: "🇨🇳" 69 | }, 70 | 71 | { 72 | name: "Vietnamese", id: "vn", emoji: "🇻🇳" 73 | }, 74 | 75 | { 76 | name: "Persian", id: "ir", emoji: "🇮🇷", 77 | }, 78 | 79 | { 80 | name: "Pirate", modelName: "English pirate speak, very heavy pirate accent", id: "pirate", emoji: "🏴‍☠️" 81 | } 82 | ] 83 | 84 | type LanguageIdentifier = string | DatabaseUser 85 | 86 | export class LanguageManager { 87 | public static get(bot: Bot, id: LanguageIdentifier): UserLanguage { 88 | const fields: (keyof UserLanguage)[] = [ "emoji", "id", "modelName", "name" ]; 89 | const value: string = typeof id === "object" ? bot.db.settings.get(id, "general:language") : id; 90 | 91 | /* Try to find the language based on one of the fields. */ 92 | return UserLanguages.find(language => { 93 | for (const field of fields) { 94 | if (language[field] === value) return true; 95 | else continue; 96 | } 97 | 98 | return false; 99 | }) ?? UserLanguages.find(l => l.id === "en-US")!; 100 | } 101 | 102 | public static languageName(bot: Bot, id: LanguageIdentifier): string { 103 | return this.get(bot, id).name; 104 | } 105 | 106 | public static modelLanguageName(bot: Bot, id: LanguageIdentifier): string { 107 | const language = this.get(bot, id); 108 | return language.modelName ?? language.name; 109 | } 110 | 111 | public static languageEmoji(bot: Bot, id: LanguageIdentifier): string { 112 | return this.get(bot, id).emoji; 113 | } 114 | } -------------------------------------------------------------------------------- /src/image/types/model.ts: -------------------------------------------------------------------------------- 1 | import { ImageGenerationBody } from "./image.js"; 2 | import { ImageAPIPath } from "../manager.js"; 3 | 4 | interface ImageConfigModelSize { 5 | width: number; 6 | height: number; 7 | } 8 | 9 | export interface ImageConfigModelSettings { 10 | /* Fixed resolution */ 11 | forcedSize?: ImageConfigModelSize | null; 12 | 13 | /* The base resolution when specifying a ratio */ 14 | baseSize?: ImageConfigModelSize | null; 15 | 16 | /** Whether this model can be chosen randomly */ 17 | random?: boolean; 18 | } 19 | 20 | export type ImageModelSettings = Required 21 | 22 | export interface ImageConfigModel { 23 | /** Display name of the model */ 24 | name: string; 25 | 26 | /** Identifier of the model */ 27 | id: string; 28 | 29 | /** Description of the model */ 30 | description: string; 31 | 32 | /** Various settings for the model */ 33 | settings?: ImageConfigModelSettings; 34 | 35 | /** Additional tags for the prompt, e.g. trigger words */ 36 | tags?: string[]; 37 | 38 | /** API path for this model */ 39 | path: ImageAPIPath; 40 | 41 | /** Additional body to use for the API request */ 42 | body?: Partial; 43 | } 44 | 45 | export type ImageModel = Required> & { 46 | settings: ImageModelSettings; 47 | } 48 | 49 | export const ImageConfigModels: ImageConfigModel[] = [ 50 | { 51 | name: "Kandinsky", 52 | description: "Multi-lingual latent diffusion model", 53 | id: "kandinsky", 54 | path: "kandinsky", 55 | 56 | settings: { 57 | baseSize: { width: 768, height: 768 }, 58 | random: true 59 | } 60 | }, 61 | 62 | { 63 | name: "SDXL", 64 | description: "Latest Stable Diffusion model", 65 | id: "sdxl", 66 | path: "sh", 67 | 68 | settings: { 69 | forcedSize: { width: 1024, height: 1024 }, 70 | random: true 71 | }, 72 | 73 | body: { 74 | model: "SDXL_beta::stability.ai#6901", number: 2 75 | } 76 | }, 77 | 78 | { 79 | name: "Project Unreal Engine 5", 80 | description: "Trained to look like Unreal Engine 5 renders", 81 | id: "ue5", 82 | path: "sh", 83 | 84 | body: { 85 | model: "stable_diffusion" 86 | } 87 | }, 88 | 89 | { 90 | name: "Dreamshaper", 91 | description: "A mix of several Stable Diffusion models", 92 | id: "dreamshaper", 93 | path: "sh", 94 | 95 | body: { 96 | model: "Dreamshaper" 97 | } 98 | }, 99 | 100 | { 101 | name: "I Can't Believe It's Not Photography", 102 | description: "Highly photo-realistic Stable Diffusion model", 103 | id: "icbinp", 104 | path: "sh", 105 | 106 | body: { 107 | model: "ICBINP - I Can't Believe It's Not Photography" 108 | } 109 | }, 110 | 111 | { 112 | name: "Anything Diffusion", 113 | description: "Stable Diffusion-based model for generating anime", 114 | id: "anything-diffusion", 115 | path: "anything" 116 | } 117 | ] -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { TuringConnectionManager } from "./turing/connection/connection.js"; 2 | import { executeConfigurationSteps } from "./bot/setup.js"; 3 | import { CacheManager } from "./bot/managers/cache.js"; 4 | import { AppDatabaseManager } from "./db/app.js"; 5 | import { BotManager } from "./bot/manager.js"; 6 | import { Logger } from "./util/logger.js"; 7 | import { Config } from "./config.js"; 8 | 9 | enum AppState { 10 | /* The app is not initialized yet */ 11 | Stopped, 12 | 13 | /* The app is currently starting up */ 14 | Starting, 15 | 16 | /* The app is up & running */ 17 | Running 18 | } 19 | 20 | /* Stripped-down app data */ 21 | export interface StrippedApp { 22 | /* Configuration data */ 23 | config: Config; 24 | } 25 | 26 | export class App { 27 | /* Logging instance */ 28 | public readonly logger: Logger; 29 | 30 | /* Manager, in charge of managing the Discord bot & their shards */ 31 | public readonly manager: BotManager; 32 | 33 | /* Global cache manager */ 34 | public readonly cache: CacheManager; 35 | 36 | /* Database manager */ 37 | public readonly db: AppDatabaseManager; 38 | 39 | /* Turing connection manager */ 40 | public readonly connection: TuringConnectionManager; 41 | 42 | /* Configuration data */ 43 | public config: Config; 44 | 45 | /* Current initialization state */ 46 | public state: AppState; 47 | 48 | /* When the app was started */ 49 | public started: number; 50 | 51 | constructor() { 52 | this.logger = new Logger(); 53 | 54 | /* Set up various managers & services. */ 55 | this.connection = new TuringConnectionManager(this); 56 | this.db = new AppDatabaseManager(this); 57 | this.manager = new BotManager(this); 58 | this.cache = new CacheManager(this); 59 | 60 | /* Assign a temporary value to the config, while we wait for the application start. 61 | Other parts *shouldn't* access the configuration during this time. */ 62 | this.config = null!; 63 | 64 | /* Set the default, stopped state of the app. */ 65 | this.state = AppState.Stopped; 66 | this.started = Date.now(); 67 | } 68 | 69 | /** 70 | * Set up the application & all related services. 71 | * @throws An error, if something went wrong 72 | */ 73 | public async setup(): Promise { 74 | this.state = AppState.Starting; 75 | 76 | /* Execute all configuration steps for the app. */ 77 | await executeConfigurationSteps(this, "app"); 78 | 79 | this.state = AppState.Running; 80 | } 81 | 82 | /** 83 | * Shut down the application & all related services. 84 | */ 85 | public async stop(code: number = 0): Promise { 86 | /* First, save the pending database changes. */ 87 | await this.db.queue.work(); 88 | 89 | /* Close the RabbitMQ connection. */ 90 | await this.connection.stop(); 91 | 92 | this.state = AppState.Stopped; 93 | 94 | /* Exit the process. */ 95 | process.exit(code); 96 | } 97 | 98 | /** 99 | * Whether development mode is enabled 100 | */ 101 | public get dev(): boolean { 102 | return this.config ? this.config.dev : false; 103 | } 104 | 105 | /** 106 | * Get a stripped-down interface of this class. 107 | * @returns Stripped-down app 108 | */ 109 | public strip(): StrippedApp { 110 | return { 111 | config: this.config 112 | }; 113 | } 114 | } -------------------------------------------------------------------------------- /src/db/managers/cache.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | 3 | import { CacheType, CacheValue } from "../../bot/managers/cache.js"; 4 | import { BotClusterManager } from "../../bot/manager.js"; 5 | import { DatabaseCollectionType } from "../manager.js"; 6 | import { SubClusterDatabaseManager } from "../sub.js"; 7 | 8 | type CacheEvalAction = "get" | "delete" | "set" 9 | 10 | export class CacheManager extends SubClusterDatabaseManager { 11 | public async set( 12 | collection: CacheType, 13 | key: string, 14 | value: CacheValue 15 | ): Promise { 16 | await this.eval("set", collection, key, value); 17 | } 18 | 19 | public async get( 20 | collection: CacheType, 21 | key: string 22 | ): Promise { 23 | const raw: T | null = await this.eval("get", collection, key) ?? null; 24 | if (raw === null) return null; 25 | 26 | return raw as T; 27 | } 28 | 29 | public async delete( 30 | collection: CacheType, 31 | key: string 32 | ): Promise { 33 | await this.eval("delete", collection, key); 34 | } 35 | 36 | public async eval( 37 | action: "get", collection: CacheType, key: string 38 | ): Promise; 39 | 40 | public async eval( 41 | action: "delete", collection: CacheType, key: string 42 | ): Promise; 43 | 44 | public async eval( 45 | action: "set", collection: CacheType, key: string, value: CacheValue 46 | ): Promise; 47 | 48 | public async eval( 49 | action: CacheEvalAction, 50 | collection: CacheType, 51 | key: string, 52 | value?: CacheValue 53 | ): Promise { 54 | if (this.db.bot.dev) this.db.bot.logger.debug( 55 | `${chalk.bold(action)} in cache collection ${chalk.bold(collection)} for key ${chalk.bold(key)}` 56 | ); 57 | 58 | /* Try to perform the specified action on the cache, on the bot manager. */ 59 | try { 60 | const data: any | void = await this.db.bot.client.cluster.evalOnManager((async (manager: BotClusterManager, context: { 61 | action: CacheEvalAction; collection: DatabaseCollectionType; key: string; value?: CacheValue; 62 | }) => { 63 | if (context.action === "set") { 64 | await manager.bot.app.cache.set(context.collection, context.key, context.value!); 65 | } else if (context.action === "get") { 66 | return await manager.bot.app.cache.get(context.collection, context.key); 67 | } else if (context.action === "delete") { 68 | await manager.bot.app.cache.delete(context.collection, context.key); 69 | } 70 | }) as any, { 71 | timeout: 3 * 1000, 72 | context: { 73 | action, collection, key, value 74 | } 75 | }); 76 | 77 | /* If the specified action was `get`, return the received value. */ 78 | if (typeof data === "object") return data as T; 79 | else return; 80 | 81 | } catch (error) { 82 | this.db.bot.logger.error( 83 | `Failed to ${chalk.bold(action)} in cache collection ${chalk.bold(collection)} for key ${chalk.bold(key)} ->`, error 84 | ); 85 | 86 | throw error; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/interactions/general.ts: -------------------------------------------------------------------------------- 1 | import { ButtonInteraction, StringSelectMenuInteraction } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import { ErrorResponse } from "../command/response/error.js"; 5 | import { Introduction } from "../util/introduction.js"; 6 | import { Response } from "../command/response.js"; 7 | import { Bot } from "../bot/bot.js"; 8 | 9 | type GeneralInteractionAction = "delete" | "vote" | "docs" 10 | 11 | export interface GeneralInteractionHandlerData { 12 | /* Which action to perform */ 13 | action: GeneralInteractionAction; 14 | 15 | /* Original author, the only user who can perform this action */ 16 | id: string | null; 17 | } 18 | 19 | export class GeneralInteractionHandler extends InteractionHandler { 20 | constructor(bot: Bot) { 21 | super( 22 | bot, 23 | 24 | new InteractionHandlerBuilder() 25 | .setName("general") 26 | .setDescription("Various useful interaction options") 27 | .setType([ InteractionType.Button ]), 28 | 29 | { 30 | action: "string", 31 | id: "string?" 32 | } 33 | ); 34 | } 35 | 36 | public async run({ data, interaction, db }: InteractionHandlerRunOptions): InteractionHandlerResponse { 37 | if (data.id !== null && db.user.id !== data.id) return void await interaction.deferUpdate(); 38 | 39 | if (data.action === "delete") { 40 | await interaction.message.delete(); 41 | 42 | } else if (data.action === "vote") { 43 | /* When the user already voted for the bot, if applicable */ 44 | const when: number | null = await this.bot.db.users.voted(db.user); 45 | 46 | if (when !== null) return new Response() 47 | .addEmbed(builder => builder 48 | .setDescription(`You have already voted for the bot , thank you for your support! 🎉`) 49 | .setColor("#FF3366") 50 | ) 51 | .setEphemeral(true); 52 | 53 | await interaction.deferReply({ 54 | ephemeral: true 55 | }); 56 | 57 | try { 58 | /* Try to check whether the user voted for the bot using the top.gg API. */ 59 | const voted: boolean = await this.bot.vote.voted(db.user); 60 | 61 | if (!voted) return new ErrorResponse({ 62 | interaction, message: "You haven't voted for the bot yet", emoji: "😕" 63 | }); 64 | 65 | return new Response() 66 | .addEmbed(builder => builder 67 | .setDescription(`Thank you for voting for the bot 🎉`) 68 | .setColor(this.bot.branding.color) 69 | ) 70 | .setEphemeral(true); 71 | 72 | } catch (error) { 73 | return await this.bot.error.handle({ 74 | error, title: "Failed to check whether the user has voted", notice: "It seems like something went wrong while trying to check whether you've voted for the bot." 75 | }); 76 | } 77 | 78 | } else if (data.action === "docs" && interaction instanceof StringSelectMenuInteraction) { 79 | return Introduction.handleInteraction(this.bot, interaction); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/conversation/settings/plugin.ts: -------------------------------------------------------------------------------- 1 | import { DisplayEmoji } from "../../util/emoji.js"; 2 | 3 | export type ChatSettingsPluginIdentifier = string 4 | 5 | export declare interface ChatSettingsPluginOptions { 6 | /* Display name of the plugin */ 7 | name: string; 8 | 9 | /* ID of the plugin */ 10 | id: ChatSettingsPluginIdentifier; 11 | 12 | /* Emoji for the plugin */ 13 | emoji: DisplayEmoji; 14 | 15 | /* Description of the plugin */ 16 | description: string; 17 | } 18 | 19 | export class ChatSettingsPlugin { 20 | /* Options for the model */ 21 | public readonly options: Required; 22 | 23 | constructor(options: ChatSettingsPluginOptions) { 24 | this.options = options; 25 | } 26 | 27 | public get id(): string { 28 | return this.options.id; 29 | } 30 | } 31 | 32 | export const ChatSettingsPlugins: ChatSettingsPlugin[] = [ 33 | new ChatSettingsPlugin({ 34 | name: "Google", emoji: { display: "<:google:1102619904185733272>", fallback: "🔎" }, 35 | description: "Searches Google to get up-to-date information from internet.", 36 | id: "google" 37 | }), 38 | 39 | new ChatSettingsPlugin({ 40 | name: "Weather", emoji: { fallback: "⛅" }, 41 | description: "View current weather information for a specific location.", 42 | id: "weather" 43 | }), 44 | 45 | new ChatSettingsPlugin({ 46 | name: "Wikipedia", emoji: { display: "<:wikipedia:1118608403086966844>", fallback: "🌐" }, 47 | description: "Search on Wikipedia for information on various topics.", 48 | id: "wikipedia" 49 | }), 50 | 51 | new ChatSettingsPlugin({ 52 | name: "Tenor", emoji: { display: "<:tenor:1118631079859986452>", fallback: "🎞️" }, 53 | description: "Search for GIFs on Tenor.", 54 | id: "tenor" 55 | }), 56 | 57 | new ChatSettingsPlugin({ 58 | name: "FreeToGame", emoji: { display: "<:freetogame:1118612404373311498>", fallback: "🎮" }, 59 | description: "Browse for free games from different platforms or categories.", 60 | id: "free-games" 61 | }), 62 | 63 | new ChatSettingsPlugin({ 64 | name: "Tasty", emoji: { fallback: "🍝" }, 65 | description: "Get tasty recipes from tasty.co.", 66 | id: "tasty" 67 | }), 68 | 69 | new ChatSettingsPlugin({ 70 | name: "World News", emoji: { fallback: "🌎" }, 71 | description: "Search for current news around the world.", 72 | id: "world-news" 73 | }), 74 | 75 | new ChatSettingsPlugin({ 76 | name: "Calculator", emoji: { display: "<:calculator:1118900577653510164>", fallback: "🔢" }, 77 | description: "Calculate something using MathJS.", 78 | id: "calculator" 79 | }), 80 | 81 | new ChatSettingsPlugin({ 82 | name: "GitHub", emoji: { display: "<:github:1097828013871222865>", fallback: "🐙" }, 83 | description: "Search for users & projects on GitHub.", 84 | id: "github" 85 | }), 86 | 87 | new ChatSettingsPlugin({ 88 | name: "Code Interpreter", emoji: { fallback: "📡" }, 89 | description: "Execute code in a sandbox using WandBox.", 90 | id: "code-interpreter" 91 | }), 92 | 93 | new ChatSettingsPlugin({ 94 | name: "Diagrams", emoji: { fallback: "📊" }, 95 | description: "Display beautiful charts, diagrams & mindmaps.", 96 | id: "render-diagrams" 97 | }) 98 | ] -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { ColorResolvable, Snowflake } from "discord.js"; 2 | 3 | import { DatabaseCollectionType } from "./db/manager.js"; 4 | 5 | export interface ConfigDiscordChannel { 6 | /* ID of the guild */ 7 | guild: Snowflake; 8 | 9 | /* ID of the forum channel */ 10 | channel: Snowflake; 11 | } 12 | 13 | export interface ConfigBrandingPartner { 14 | /* Name of the partner */ 15 | name: string; 16 | 17 | /* Description of the partner */ 18 | description?: string; 19 | 20 | /* Emoji for the partner, optional */ 21 | emoji?: string; 22 | 23 | /* URL to the partner's website */ 24 | url: string; 25 | } 26 | 27 | export interface ConfigBranding { 28 | /* Color to use for most embeds */ 29 | color: ColorResolvable; 30 | 31 | /* List of partners */ 32 | partners: ConfigBrandingPartner[]; 33 | } 34 | 35 | export interface Config { 36 | /* Token of the Discord bot */ 37 | discord: { 38 | /* Credentials of the bot */ 39 | token: string; 40 | id: Snowflake; 41 | 42 | /* Invite code for the support server */ 43 | inviteCode: string; 44 | 45 | /* Guilds, which should have access to restricted commands */ 46 | guilds: Snowflake[]; 47 | }; 48 | 49 | /* Whether metrics about usage, cool-down, guilds, users, etc. should be collected in the database */ 50 | metrics: boolean; 51 | 52 | /* Whether the bot is in development mode */ 53 | dev: boolean; 54 | 55 | /* Branding stuff */ 56 | branding: ConfigBranding; 57 | 58 | /* How many clusters to allocate for the bot */ 59 | clusters: number | string | "auto"; 60 | shardsPerCluster: number; 61 | 62 | channels: { 63 | /* Where the error messages should be sent; which guild and channel */ 64 | error: ConfigDiscordChannel; 65 | 66 | /* Where the moderation messages should be sent; which guild and channel */ 67 | moderation: ConfigDiscordChannel; 68 | 69 | /* Where status update messages should be sent; which guild and channel */ 70 | status: ConfigDiscordChannel; 71 | }; 72 | 73 | /* OpenAI API information */ 74 | openAI: { 75 | /* API key */ 76 | key: string; 77 | }; 78 | 79 | /* HuggingFace API information */ 80 | huggingFace: { 81 | /* API key */ 82 | key: string; 83 | }; 84 | 85 | /* Replicate API information */ 86 | replicate: { 87 | /* API key */ 88 | key: string; 89 | }; 90 | 91 | /* top.gg API information */ 92 | topgg: { 93 | /* API key */ 94 | key: string; 95 | } 96 | 97 | /* Turing API information */ 98 | turing: { 99 | /* API key */ 100 | key: string; 101 | super: string; 102 | 103 | urls: { 104 | prod: string; 105 | dev?: string; 106 | } 107 | 108 | /* Various CAPTCHA verification keys */ 109 | captchas: { 110 | turnstile: string; 111 | }; 112 | } 113 | 114 | /* OCR.space API information */ 115 | ocr: { 116 | /* API key */ 117 | key: string; 118 | }; 119 | 120 | /* Various GIF APIs */ 121 | gif: { 122 | tenor: string; 123 | }; 124 | 125 | /* Stable Horde API secrets & information */ 126 | stableHorde: { 127 | /* API key */ 128 | key: string; 129 | }; 130 | 131 | /* RabbitMQ configuration */ 132 | rabbitMQ: { 133 | url: string; 134 | } 135 | 136 | /* General database information */ 137 | db: { 138 | supabase: { 139 | url: string; 140 | 141 | key: { 142 | anon: string; 143 | service: string; 144 | }; 145 | 146 | collections?: { 147 | [key in DatabaseCollectionType]?: string; 148 | }; 149 | }; 150 | 151 | redis: { 152 | url: string; 153 | password: string; 154 | port: number; 155 | }; 156 | }; 157 | } -------------------------------------------------------------------------------- /src/chat/types/model.ts: -------------------------------------------------------------------------------- 1 | import { ChatResetOptions, GPTImageAnalyzeOptions, ModelGenerationOptions } from "./options.js"; 2 | import { PartialResponseMessage } from "./message.js"; 3 | import { ChatAnalyzedImage } from "../media/types/image.js"; 4 | import { ChatClient } from "../client.js"; 5 | 6 | export enum ModelCapability { 7 | /* The model can view images */ 8 | ImageViewing = "imageViewing", 9 | 10 | /* The model only works in guilds */ 11 | GuildOnly = "guildOnly", 12 | 13 | /* The model can be set to respond in the user's language */ 14 | UserLanguage = "userLanguage" 15 | } 16 | 17 | export enum ModelType { 18 | /* OpenAI ChatGPT API */ 19 | OpenAI, 20 | 21 | /** Replication of Discord's Clyde AI */ 22 | Clyde, 23 | 24 | /** Google's API, using the Turing API */ 25 | Google, 26 | 27 | /** Anthropic's API, using the Turing API */ 28 | Anthropic, 29 | 30 | /** Meta's LLaMA model, using the Turing API */ 31 | LLaMA, 32 | 33 | /** OpenChat model, using the Turing API */ 34 | OpenChat 35 | } 36 | 37 | export interface ModelOptions { 38 | /* Name of the model */ 39 | name: string; 40 | 41 | /* Type of model */ 42 | type: ModelType; 43 | 44 | /* Whether the model accepts images */ 45 | capabilities: ModelCapability[]; 46 | } 47 | 48 | export type ConstructorModelOptions = Pick & { 49 | capabilities?: ModelCapability[]; 50 | } 51 | 52 | export abstract class ChatModel { 53 | protected readonly client: ChatClient; 54 | 55 | /* Information about this model */ 56 | public readonly settings: ModelOptions; 57 | 58 | constructor(client: ChatClient, options: ConstructorModelOptions) { 59 | this.settings = { 60 | ...options, 61 | capabilities: options.capabilities ?? [] 62 | }; 63 | 64 | this.client = client; 65 | } 66 | 67 | /** 68 | * This function is called before the conversation of a user is reset, using the `/reset` command. 69 | * @param options Various reset options 70 | */ 71 | public async reset(options: ChatResetOptions): Promise { 72 | /* Stub */ 73 | } 74 | 75 | /** 76 | * Analyze the given message attachment, and return the analyzed results. 77 | * @param options Image analyzing options 78 | * 79 | * @returns Analyzed image 80 | */ 81 | public async analyze(options: GPTImageAnalyzeOptions): Promise { 82 | /* Analyze & describe the image. */ 83 | const result = await this.client.manager.bot.description.describe({ 84 | input: options.attachment 85 | }); 86 | 87 | return { 88 | text: result.result.ocr ? result.result.ocr.content : null, 89 | description: result.result.description, 90 | cost: result.cost, duration: result.duration 91 | }; 92 | } 93 | 94 | /** 95 | * Generate a response from this model. 96 | * @param options Generation options 97 | * 98 | * @returns Final generation results 99 | */ 100 | public abstract complete(options: ModelGenerationOptions): Promise; 101 | 102 | /** 103 | * Check whether the model has access to the specified capability. 104 | * @param capability The capability to check for 105 | * 106 | * @returns Whether it has the capability 107 | */ 108 | public hasCapability(capability: ModelCapability): boolean { 109 | return this.settings.capabilities.includes(capability); 110 | } 111 | } -------------------------------------------------------------------------------- /src/turing/connection/connection.ts: -------------------------------------------------------------------------------- 1 | import { Connection as RabbitMQClient, Consumer as RabbitMQConsumer, Publisher as RabbitMQPublisher } from "rabbitmq-client"; 2 | 3 | import { TuringConnectionHandler } from "./handler.js"; 4 | import { Bot } from "../../bot/bot.js"; 5 | import { App } from "../../app.js"; 6 | import chalk from "chalk"; 7 | 8 | export class TuringConnectionManager { 9 | public readonly app: App; 10 | 11 | /* RabbitMQ client */ 12 | public client: RabbitMQClient; 13 | 14 | /* Rabbit MQ consumer (API -> bot) */ 15 | public consumer: RabbitMQConsumer; 16 | 17 | /* Rabbit MQ publisher (bot -> API) */ 18 | public publisher: RabbitMQPublisher; 19 | 20 | /* Handler for consuming & publishing messages */ 21 | public readonly handler: TuringConnectionHandler; 22 | 23 | constructor(app: App) { 24 | this.app = app; 25 | 26 | this.client = null!; 27 | this.consumer = null!; 28 | this.publisher = null!; 29 | 30 | this.handler = new TuringConnectionHandler(this); 31 | } 32 | 33 | /** 34 | * Wait for the client to connect to the server. 35 | */ 36 | private async wait(): Promise { 37 | return new Promise(resolve => { 38 | this.client.on("connection", () => resolve()); 39 | }); 40 | } 41 | 42 | public async setup(): Promise { 43 | /* Initialize the RabbitMQ client. */ 44 | this.client = new RabbitMQClient(this.app.config.rabbitMQ.url); 45 | 46 | /* Wait for the client to connect. */ 47 | await this.wait(); 48 | 49 | /* Load all packets. */ 50 | await this.handler.setup(); 51 | 52 | const exchangeName: string = this.exchangeName(); 53 | const routingKey: string = this.routingKey(); 54 | 55 | this.consumer = this.client.createConsumer({ 56 | qos: { prefetchCount: 1 }, 57 | queue: exchangeName, 58 | 59 | exchanges: [ { exchange: exchangeName, type: "topic" } ], 60 | queueBindings: [ { exchange: exchangeName, routingKey: routingKey } ] 61 | }, message => this.handler.handle(message)); 62 | 63 | this.publisher = this.client.createPublisher({ 64 | exchanges: [ 65 | { 66 | exchange: exchangeName, type: "topic" 67 | } 68 | ], 69 | 70 | maxAttempts: 3 71 | }); 72 | 73 | this.handleErrors(this.consumer); 74 | this.handleErrors(this.client); 75 | } 76 | 77 | public async stop(): Promise { 78 | if (!this.publisher || !this.consumer || !this.client) return; 79 | 80 | /* Wait for pending confirmations and closes the underlying channel. */ 81 | await this.publisher.close(); 82 | 83 | /* Stop consuming & wait for any pending message handlers to settle. */ 84 | await this.consumer.close(); 85 | 86 | /* Close the client itself. */ 87 | await this.client.close(); 88 | } 89 | 90 | private handleErrors(obj: RabbitMQClient | RabbitMQConsumer) { 91 | const type = obj instanceof RabbitMQClient ? "client" : "consumer"; 92 | 93 | obj.on("error", error => { 94 | this.app.logger.error(chalk.bold(`RabbitMQ ${type}`), "experienced an error", "->", error); 95 | }); 96 | } 97 | 98 | private exchangeName(): string { 99 | return `messages${this.app.dev ? ":dev" : ""}`; 100 | } 101 | 102 | private routingKey(): string { 103 | return "message"; 104 | } 105 | } -------------------------------------------------------------------------------- /src/commands/dev/roles.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommandBuilder } from "discord.js"; 2 | 3 | import { Command, CommandInteraction, CommandResponse } from "../../command/command.js"; 4 | import { UserRole, UserRoleHierarchy } from "../../db/managers/role.js"; 5 | import { ErrorResponse } from "../../command/response/error.js"; 6 | import { DatabaseUser } from "../../db/schemas/user.js"; 7 | import { Response } from "../../command/response.js"; 8 | import { Utils } from "../../util/utils.js"; 9 | import { Bot } from "../../bot/bot.js"; 10 | 11 | type RoleActionType = "add" | "remove" 12 | 13 | interface RoleAction { 14 | name: RoleActionType; 15 | description: string; 16 | } 17 | 18 | const RoleActions: RoleAction[] = [ 19 | { 20 | name: "add", 21 | description: "Give roles to a user" 22 | }, 23 | 24 | { 25 | name: "remove", 26 | description: "Remove roles from a user" 27 | } 28 | ] 29 | 30 | export default class RolesCommand extends Command { 31 | constructor(bot: Bot) { 32 | const builder = new SlashCommandBuilder() 33 | .setName("roles").setDescription("Change the roles of a user"); 34 | 35 | RoleActions.forEach(action => { 36 | builder.addSubcommand(builder => builder 37 | .setName(action.name) 38 | .setDescription(action.description) 39 | .addStringOption(builder => builder 40 | .setName("id") 41 | .setDescription(`ID or tag of the user to ${action.name} the roles`) 42 | .setRequired(true) 43 | ) 44 | .addStringOption(builder => builder 45 | .setName("role") 46 | .setDescription(`Which role to change`) 47 | .addChoices(...UserRoleHierarchy.map(role => ({ 48 | name: Utils.titleCase(role), 49 | value: role 50 | }))) 51 | .setRequired(true) 52 | ) 53 | ); 54 | }); 55 | 56 | super(bot, builder, { restriction: [ "owner" ] }); 57 | } 58 | 59 | public async run(interaction: CommandInteraction): CommandResponse { 60 | /* ID of the user */ 61 | const id: string = interaction.options.getString("id", true); 62 | const target = await Utils.findType(this.bot, "user", id); 63 | 64 | if (target === null) return new Response() 65 | .addEmbed(builder => builder 66 | .setDescription("The specified user does not exist 😔") 67 | .setColor("Red") 68 | ) 69 | .setEphemeral(true); 70 | 71 | /* Get the database entry of the user, if applicable. */ 72 | const db: DatabaseUser | null = await this.bot.db.users.getUser(target.id); 73 | 74 | if (db === null) return new Response() 75 | .addEmbed(builder => builder 76 | .setDescription("The specified user hasn't interacted with the bot 😔") 77 | .setColor("Red") 78 | ) 79 | .setEphemeral(true); 80 | 81 | /* Action to take */ 82 | const action: RoleActionType = interaction.options.getSubcommand(true) as RoleActionType; 83 | 84 | /* Which role to change */ 85 | const role: UserRole = interaction.options.getString("role", true) as UserRole; 86 | 87 | if ((action === "add" && this.bot.db.role.has(db, role)) || (action === "remove" && !this.bot.db.role.has(db, role))) return new ErrorResponse({ 88 | interaction, message: `${action === "add" ? `The user already has` : "The user doesn't have"} the **${Utils.titleCase(role)}** role`, emoji: "😔" 89 | }); 90 | 91 | await this.bot.db.role.change(db, { 92 | [action]: [ role ] 93 | }); 94 | 95 | return new Response() 96 | .addEmbed(builder => builder 97 | .setAuthor({ name: target.name, iconURL: target.icon ?? undefined }) 98 | .setDescription(`Role **${Utils.titleCase(role)}** ${action === "add" ? "added" : "removed"}`) 99 | .setColor("Yellow") 100 | .setTimestamp() 101 | ); 102 | } 103 | } -------------------------------------------------------------------------------- /src/util/vote.ts: -------------------------------------------------------------------------------- 1 | import { getInfo } from "discord-hybrid-sharding"; 2 | import { User } from "discord.js"; 3 | import chalk from "chalk"; 4 | 5 | import { DatabaseInfo } from "../db/managers/user.js"; 6 | import { DatabaseUser } from "../db/schemas/user.js"; 7 | import { GPTAPIError } from "../error/api.js"; 8 | import { Bot } from "../bot/bot.js"; 9 | 10 | type VoteAPIPath = string 11 | 12 | /* How long a vote lasts */ 13 | export const VoteDuration: number = 12.5 * 60 * 60 * 1000 14 | 15 | export class VoteManager { 16 | private readonly bot: Bot; 17 | 18 | constructor(bot: Bot) { 19 | this.bot = bot; 20 | } 21 | 22 | public link(db: DatabaseInfo): string { 23 | return `https://l.turing.sh/topgg/${db.user.id}`; 24 | } 25 | 26 | /** 27 | * Check if a user has voted for the bot. 28 | * @param user User to check for 29 | * 30 | * @returns Whether the user has voted for the bot 31 | */ 32 | public async voted(db: DatabaseUser): Promise { 33 | if (await this.bot.db.users.voted(db) !== null) return true; 34 | 35 | /* Check whether the user has voted, using the API. */ 36 | const { voted }: { voted: number } = await this.request(`${this.botPath("check")}?userId=${db.id}`, "GET"); 37 | if (!voted) return false; 38 | 39 | /* Update the user's vote status in the database. */ 40 | await this.bot.db.queue.update("users", db, { 41 | voted: new Date().toISOString() 42 | }); 43 | 44 | await this.bot.db.metrics.changeVoteMetric({ count: "+1" }); 45 | return true; 46 | } 47 | 48 | public async postStatistics(): Promise { 49 | /* How many guilds the bot is in */ 50 | const guilds: number = this.bot.statistics.guildCount; 51 | if (guilds === 0) return; 52 | 53 | const shardCount: number = getInfo().TOTAL_SHARDS; 54 | 55 | const data = { 56 | server_count: guilds, 57 | shard_count: shardCount 58 | }; 59 | 60 | await this.request(this.botPath("stats"), "POST", data); 61 | } 62 | 63 | private botPath(path: "check" | "stats"): VoteAPIPath { 64 | return `bots/${this.bot.app.config.discord.id}/${path}`; 65 | } 66 | 67 | private async request(path: VoteAPIPath, method: "GET" | "POST" | "DELETE" = "GET", data?: { [key: string]: any }): Promise { 68 | /* Make the actual request. */ 69 | const response = await fetch(this.url(path), { 70 | method, 71 | 72 | body: data !== undefined ? JSON.stringify(data) : undefined, 73 | headers: this.headers() 74 | }); 75 | 76 | /* If the request wasn't successful, throw an error. */ 77 | if (!response.ok) await this.error(response, path); 78 | 79 | /* Get the response body. */ 80 | const body: T = await response.json() as T; 81 | return body; 82 | } 83 | 84 | private url(path: VoteAPIPath): `https://top.gg/api/${VoteAPIPath}` { 85 | return `https://top.gg/api/${path}`; 86 | } 87 | 88 | private async error(response: Response, path: VoteAPIPath): Promise { 89 | const body: any | null = await response.json().catch(() => null); 90 | 91 | throw new GPTAPIError({ 92 | code: response.status, 93 | endpoint: `/${path}`, 94 | id: null, 95 | message: body !== null && body.message ? body.message : null 96 | }); 97 | } 98 | 99 | private headers(): HeadersInit { 100 | return { 101 | Authorization: this.bot.app.config.topgg.key, 102 | "Content-Type": "application/json" 103 | }; 104 | } 105 | } -------------------------------------------------------------------------------- /src/db/types/indicator.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseUser } from "../schemas/user.js"; 2 | import { Bot } from "../../bot/bot.js"; 3 | 4 | export interface LoadingEmoji { 5 | name: string; 6 | id: string; 7 | 8 | animated?: boolean; 9 | } 10 | 11 | export interface LoadingIndicator { 12 | /* Name of the loading indicator */ 13 | name: string; 14 | 15 | /* Discord emoji */ 16 | emoji: LoadingEmoji; 17 | } 18 | 19 | type LoadingIdentifier = string 20 | 21 | export const LoadingIndicators: LoadingIndicator[] = [ 22 | { 23 | name: "Discord Loading #1", 24 | emoji: { 25 | name: "loading", id: "1051419341914132554", animated: true 26 | } 27 | }, 28 | 29 | { 30 | name: "Discord Loading #2", 31 | emoji: { 32 | name: "discord_loading", id: "1103039423806976021", animated: true 33 | } 34 | }, 35 | 36 | { 37 | name: "Orb", 38 | emoji: { 39 | name: "orb", id: "1102556034276528238", animated: true 40 | } 41 | }, 42 | 43 | { 44 | name: "Turing Spin", 45 | emoji: { 46 | name: "turing_spin", id: "1104867917436289065", animated: true 47 | } 48 | }, 49 | 50 | { 51 | name: "Discord Typing", 52 | emoji: { 53 | name: "discord_typing", id: "1103039408728445071", animated: true 54 | } 55 | }, 56 | 57 | { 58 | name: "Loading Bars", 59 | emoji: { 60 | name: "loading2", id: "1104458865224990780", animated: true 61 | } 62 | }, 63 | 64 | { 65 | name: "Vibe Rabbit", 66 | emoji: { 67 | name: "rabbit", id: "1078943805316812850", animated: true 68 | } 69 | }, 70 | 71 | { 72 | name: "Spinning Skull", 73 | emoji: { 74 | name: "spinning_skull", id: "1102635532258906224", animated: true 75 | } 76 | }, 77 | 78 | { 79 | name: "Spinning Tux", 80 | emoji: { 81 | name: "tux_spin", id: "1103014814135099573", animated: true 82 | } 83 | }, 84 | 85 | { 86 | name: "LEGO", 87 | emoji: { 88 | name: "lego", id: "1105171703170076744", animated: true 89 | } 90 | }, 91 | 92 | { 93 | name: "Spinning Cat #1", 94 | emoji: { 95 | name: "spinning_maxwell", id: "1104458871642259506", animated: true 96 | } 97 | }, 98 | 99 | { 100 | name: "Spinning Cat #2", 101 | emoji: { 102 | name: "spinning_cat", id: "1104458868546867424", animated: true 103 | } 104 | }, 105 | 106 | { 107 | name: "SpongeBob", 108 | emoji: { 109 | name: "spunchbob", id: "1104869247290716201", animated: true 110 | } 111 | }, 112 | 113 | { 114 | name: "Spinning Cat Cube", 115 | emoji: { 116 | name: "spinning_cat_cube", id: "1105185931209756693", animated: true 117 | } 118 | } 119 | ] 120 | 121 | export class LoadingIndicatorManager { 122 | public static get(id: LoadingIdentifier): LoadingIndicator { 123 | return LoadingIndicators.find(indicator => indicator.emoji.id === id)!; 124 | } 125 | 126 | public static getFromUser(bot: Bot, user: DatabaseUser): LoadingIndicator { 127 | const id: string = bot.db.settings.get(user, "general:loadingIndicator"); 128 | return LoadingIndicators.find(indicator => indicator.emoji.id === id)!; 129 | } 130 | 131 | public static toString(id: LoadingIdentifier | LoadingIndicator): string { 132 | const indicator: LoadingIndicator = typeof id === "string" ? this.get(id) : id; 133 | return `<${indicator.emoji.animated ? "a" : ""}:${indicator.emoji.name}:${indicator.emoji.id}>`; 134 | } 135 | } -------------------------------------------------------------------------------- /src/interactions/premium.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, EmbedBuilder } from "discord.js"; 2 | 3 | import { InteractionHandler, InteractionHandlerBuilder, InteractionHandlerResponse, InteractionHandlerRunOptions, InteractionType } from "../interaction/handler.js"; 4 | import { Response } from "../command/response.js"; 5 | import { Utils } from "../util/utils.js"; 6 | import { Bot } from "../bot/bot.js"; 7 | 8 | interface PremiumInteractionHandlerData { 9 | action: "overview" | "ads" | "purchase"; 10 | } 11 | 12 | export class PremiumInteractionHandler extends InteractionHandler { 13 | constructor(bot: Bot) { 14 | super( 15 | bot, 16 | 17 | new InteractionHandlerBuilder() 18 | .setName("premium") 19 | .setDescription("View the Premium plan") 20 | .setType([ InteractionType.Button ]), 21 | 22 | { 23 | action: "string" 24 | } 25 | ); 26 | } 27 | 28 | public async run({ data: { action }, interaction, db }: InteractionHandlerRunOptions): InteractionHandlerResponse { 29 | if (action === "overview") { 30 | return this.bot.db.plan.buildOverview(interaction, db); 31 | 32 | } else if (action === "purchase") { 33 | const row: ActionRowBuilder = new ActionRowBuilder() 34 | .addComponents( 35 | new ButtonBuilder() 36 | .setStyle(ButtonStyle.Link) 37 | .setURL(Utils.shopURL()) 38 | .setLabel("Visit our shop") 39 | .setEmoji("💸") 40 | ); 41 | 42 | return new Response() 43 | .addComponent(ActionRowBuilder, row) 44 | .setEphemeral(true); 45 | 46 | } else if (action === "ads") { 47 | /* Whether the shop buttons should be shown */ 48 | const showButtons: boolean = db.user.metadata.email != undefined; 49 | 50 | const perks: string[] = [ 51 | "Way lower cool-down for chatting", 52 | "Bigger token (character) limit for chatting", 53 | "Early access to new features" 54 | ]; 55 | 56 | const embed: EmbedBuilder = new EmbedBuilder() 57 | .setTitle("Want to get rid of annoying ads? ✨") 58 | .setDescription(`**Premium** gets rid of all ads in the bot & also gives you additional benefits, such as\n\n${perks.map(p => `- ${p}`).join("\n")}`) 59 | .setColor("Orange"); 60 | 61 | const row: ActionRowBuilder = new ActionRowBuilder() 62 | .addComponents( 63 | new ButtonBuilder() 64 | .setStyle(ButtonStyle.Link) 65 | .setURL(Utils.shopURL()) 66 | .setLabel("Visit our shop") 67 | .setEmoji("💸") 68 | ); 69 | 70 | if (showButtons) row.components.unshift( 71 | new ButtonBuilder() 72 | .setCustomId(`premium:purchase:subscription`).setEmoji("🛍️") 73 | .setLabel("Subscribe") 74 | .setStyle(ButtonStyle.Success), 75 | 76 | new ButtonBuilder() 77 | .setCustomId(`premium:purchase:plan`).setEmoji("🛍️") 78 | .setLabel("Purchase credits") 79 | .setStyle(ButtonStyle.Secondary) 80 | ); 81 | 82 | return new Response() 83 | .addComponent(ActionRowBuilder, row) 84 | .addEmbed(embed).setEphemeral(true); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/command/response.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder, ComponentBuilder, Message, InteractionReplyOptions, TextChannel, AttachmentBuilder, MessageCreateOptions, CommandInteraction, MessageComponentInteraction, DMChannel, InteractionResponse, ThreadChannel, MessageEditOptions, InteractionUpdateOptions, ButtonInteraction, ModalSubmitInteraction, MessageReplyOptions } from "discord.js"; 2 | import { APIActionRowComponent, APIActionRowComponentTypes } from "discord-api-types/v10"; 3 | 4 | type Component = APIActionRowComponentTypes | APIActionRowComponent 5 | 6 | export type ResponseSendClass = MessageComponentInteraction | CommandInteraction | ModalSubmitInteraction | Message | TextChannel | DMChannel | ThreadChannel 7 | export type ResponseSendOptions = InteractionReplyOptions | InteractionUpdateOptions | MessageCreateOptions | MessageEditOptions | string 8 | 9 | export class Response { 10 | /* Content of the message */ 11 | public content: string | null; 12 | 13 | /* Embed of the message */ 14 | public embeds: EmbedBuilder[]; 15 | 16 | /* Attachments of the message */ 17 | public attachments: AttachmentBuilder[]; 18 | 19 | /* Components of the message */ 20 | public components: Component[]; 21 | 22 | /* Whether the response is only visible to the user */ 23 | public ephemeral: boolean; 24 | 25 | constructor() { 26 | this.ephemeral = false; 27 | this.attachments = []; 28 | this.components = []; 29 | this.content = null; 30 | this.embeds = []; 31 | } 32 | 33 | public setContent(content: string | null): this { 34 | this.content = content; 35 | return this; 36 | } 37 | 38 | public addEmbed(builder: ((embed: EmbedBuilder) => EmbedBuilder) | EmbedBuilder): this { 39 | this.embeds.push(typeof builder === "function" ? builder(new EmbedBuilder()) : builder); 40 | return this; 41 | } 42 | 43 | public addEmbeds(builders: (((embed: EmbedBuilder) => EmbedBuilder) | EmbedBuilder)[]): this { 44 | this.embeds.push( 45 | ...builders.map(builder => typeof builder === "function" ? builder(new EmbedBuilder()) : builder) 46 | ); 47 | 48 | return this; 49 | } 50 | 51 | public addAttachment(attachment: AttachmentBuilder): this { 52 | this.attachments.push(attachment); 53 | return this; 54 | } 55 | 56 | public addComponent(type: { new(): T }, builder: ((component: T) => T) | T): this { 57 | this.components.push((typeof builder === "function" ? builder(new type()) : builder).toJSON()); 58 | return this; 59 | } 60 | 61 | public setEphemeral(ephemeral: boolean): this { 62 | this.ephemeral = ephemeral; 63 | return this; 64 | } 65 | 66 | public get(): T { 67 | return { 68 | content: this.content !== null ? this.content : undefined, 69 | embeds: this.embeds, components: this.components, 70 | ephemeral: this.ephemeral, files: this.attachments, 71 | allowedMentions: { repliedUser: true, parse: [] } 72 | } as any as T; 73 | } 74 | 75 | /* Edit the original interaction reply. */ 76 | public async send(interaction: ResponseSendClass): Promise { 77 | try { 78 | if (interaction instanceof MessageComponentInteraction || interaction instanceof CommandInteraction || interaction instanceof ButtonInteraction || interaction instanceof ModalSubmitInteraction) { 79 | /* Whether the interaction has already been replied to */ 80 | const replied: boolean = interaction.replied || interaction.deferred; 81 | 82 | if (replied) return await interaction.editReply(this.get()); 83 | else return await interaction.reply(this.get()); 84 | 85 | } else if (interaction instanceof TextChannel || interaction instanceof DMChannel || interaction instanceof ThreadChannel) { 86 | return await interaction.send(this.get()); 87 | 88 | } else if (interaction instanceof Message) { 89 | return interaction.reply(this.get()); 90 | } 91 | } catch (_) {} 92 | 93 | return null; 94 | } 95 | } -------------------------------------------------------------------------------- /src/chat/media/handlers/image.ts: -------------------------------------------------------------------------------- 1 | import { Awaitable, Message } from "discord.js"; 2 | 3 | import { ChatAnalyzedImage, ChatBaseImage, ChatImageAttachment, ChatImageAttachmentExtractorData, ChatImageAttachmentExtractors, ChatInputImage } from "../types/image.js"; 4 | import { ChatMediaHandler, ChatMediaHandlerHasOptions, ChatMediaHandlerRunOptions } from "../handler.js"; 5 | import { ChatMediaType } from "../types/media.js"; 6 | import { Utils } from "../../../util/utils.js"; 7 | import { ChatClient } from "../../client.js"; 8 | 9 | export class ImageChatHandler extends ChatMediaHandler { 10 | constructor(client: ChatClient) { 11 | super(client, { 12 | type: ChatMediaType.Images, message: "Looking at the images" 13 | }); 14 | } 15 | 16 | public async has({ message }: ChatMediaHandlerHasOptions): Promise { 17 | const results = await this.findImageAttachments(message); 18 | return results.length > 0; 19 | } 20 | 21 | public async run(options: ChatMediaHandlerRunOptions): Promise { 22 | const attachments: ChatImageAttachment[] = await this.findImageAttachments(options.message); 23 | const results: ChatInputImage[] = []; 24 | 25 | const base: ChatBaseImage[] = await Promise.all(attachments 26 | .map(async attachment => { 27 | const data = await Utils.fetchBuffer(attachment.url); 28 | return { ...attachment, data: data! }; 29 | })); 30 | 31 | for (const image of base) { 32 | /* Show a notice to the Discord user. */ 33 | await this.client.manager.progress.notice(options, { 34 | text: `Looking at **\`${image.name}\`**` 35 | }); 36 | 37 | /* Run the model-specific image analyzer, and gather all results. */ 38 | const result: ChatAnalyzedImage = await options.model.analyze({ 39 | ...options, attachment: image 40 | }); 41 | 42 | results.push({ 43 | name: image.name, type: image.type, url: image.url, ...result 44 | }); 45 | } 46 | 47 | return results; 48 | } 49 | 50 | /** 51 | * Get all usable Discord image attachments. 52 | * @returns Usable Discord Image attachments 53 | */ 54 | public async findImageAttachments(message: Message): Promise { 55 | const total: ChatImageAttachment[] = []; 56 | 57 | for (const extractor of ChatImageAttachmentExtractors) { 58 | const data: ChatImageAttachmentExtractorData = this.imageAttachmentData(message); 59 | 60 | const condition: boolean = extractor.condition(data); 61 | if (!condition) continue; 62 | 63 | total.push(...(await extractor.extract(data) ?? []).map(extracted => ({ 64 | ...extracted, type: extractor.type 65 | }))); 66 | } 67 | 68 | return total; 69 | } 70 | 71 | private imageAttachmentData(message: Message): ChatImageAttachmentExtractorData { 72 | return { 73 | bot: this.client.manager.bot, message 74 | }; 75 | } 76 | 77 | public prompt(image: ChatInputImage): string { 78 | return `[${Utils.titleCase(image.type)} = ${image.name}: "${image.description}"${image.text ? `, detected text: "${image.text}"` : ""}]`; 79 | } 80 | 81 | public initialPrompt(): string { 82 | return ` 83 | From now on, you are a text and image-based AI. Users will be able to attach images to their message for you to understand using the format: '[ # = : "". [optional: "Detected text: ""]]'. 84 | You must be able to act like you can see and understand these attached images, act as if you can see, view and read them, referring to them as attached image/emoji/sticker/etc. 85 | Prioritize detected text from the image, fix OCR errors, and use logic and common sense to understand the image. Don't ask the user about the description, treat it as an image attachment. 86 | `; 87 | } 88 | } --------------------------------------------------------------------------------