Compare commits

...

10 commits

15 changed files with 1595 additions and 79 deletions

2
.gitignore vendored
View file

@ -10,5 +10,5 @@ yarn-error.log*
/.pnp
src/**/*.js
.pnp.js
/shitposts
.vscode/*

7
.idea/prettier.xml generated Normal file
View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
</component>
</project>

View file

@ -9,11 +9,12 @@
"check": "tsc"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
"@types/node": "^22.14.0",
"acorn": "^8.14.1",
"astring": "^1.9.0",
"canvas": "^3.1.0",
"discord.js": "^14.17.2",
"discord.js": "^14.19.3",
"sharp": "git+ssh://git@github.com/lovell/sharp.git",
"ts-node": "^10.9.2",
"zod": "^3.24.2"

1277
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
import { ApplicationCommandType, type AutocompleteFocusedOption, AutocompleteInteraction, ChatInputCommandInteraction, ContextMenuCommandBuilder, ContextMenuCommandInteraction, Message, SharedSlashCommand, User } from "discord.js";
import { type Config } from "./config.ts";
import type {S3Client} from "@aws-sdk/client-s3";
export abstract class ICommand { }
@ -9,7 +10,7 @@ export abstract class ContextCommand<T extends User | Message> extends ICommand
T extends Message ? ApplicationCommandType.Message :
never;
abstract contextDefinition: ContextMenuCommandBuilder
abstract run(interaction: ContextMenuCommandInteraction, target: T extends User ? User : T extends Message ? Message : never): Promise<void>
abstract run(interaction: ContextMenuCommandInteraction, target: T extends User ? User : T extends Message ? Message : never, config: Config): Promise<void>
}
export abstract class Command extends ICommand {

View file

@ -0,0 +1,60 @@
import {
ApplicationCommandType, type Attachment,
ContextMenuCommandBuilder,
ContextMenuCommandInteraction,
Message
} from "discord.js";
import { ContextCommand } from "../command.ts";
import type {Config} from "../config.ts";
import {BUCKETNAME} from "./shitpost.ts";
import fs from "node:fs/promises";
import path from "node:path";
import {fileURLToPath} from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default class Mock extends ContextCommand<Message> {
targetType: ApplicationCommandType.Message = ApplicationCommandType.Message;
contextDefinition: ContextMenuCommandBuilder =
new ContextMenuCommandBuilder()
.setName('AddToShitposts')
.setType(ApplicationCommandType.Message)
async run(interaction: ContextMenuCommandInteraction, target: Message, config:Config): Promise<void> {
await interaction.deferReply();
await interaction.followUp({content: "uploading..."});
const downloadFolderPath = path.join(__dirname, '..', '..', 'shitposts');
try {
await fs.mkdir(downloadFolderPath, { recursive: true });
} catch (error) {
console.error("Error creating download folder:", error);
await interaction.editReply({ content: "the fucking posix file system failed me (download foler couldnt be made)" });
return;
}
for (const [_, attachment] of target.attachments) {
const response = await fetch(attachment.url);
if (!response.ok) {
await interaction.editReply({ content: "discord shat itself while fetching an attachment!?" });
return;
}
const buffer = await response.arrayBuffer();
const fileName = attachment.name || `attachment_${attachment.id}`;
const filePath = path.join(downloadFolderPath, fileName);
try {
await fs.writeFile(filePath, Buffer.from(buffer));
console.log(`Downloaded: ${fileName}`);
} catch (error) {
console.error(`Error downloading ${fileName}:`, error);
await interaction.editReply({ content: `Failed to download ${fileName}.` });
return;
}
}
await interaction.editReply({content: "shits have been posted!"});
}
}

View file

@ -14,7 +14,7 @@ export default class FediemojiCommand extends Command {
return typedEmojis
}
async run(interaction: ChatInputCommandInteraction, config: Config) {
async run(interaction: ChatInputCommandInteraction, config: Config, ) {
await interaction.deferReply();
const emojiname = interaction.options.getString("emoji");
const shit = await interaction.client.application.emojis.fetch();

View file

@ -31,12 +31,19 @@ export default class LastListenedCommand extends Command {
async run(interaction: ChatInputCommandInteraction, config: Config) {
await interaction.deferReply()
const user = interaction.options.getString("user") ?? config.listenbrainzAccount;
const historyAmount = interaction.options.getInteger("count") ?? 3;
const meow = await fetch(`https://api.listenbrainz.org/1/user/${user}/listens`).then((res) => res.json());
const zodded = listenBrainzListensShape.parse(meow)
const object = zodded.payload.listens.slice(0, 3);
const object = zodded.payload.listens.slice(0, historyAmount);
const songs = object.slice(0, historyAmount).map((i) => {
const shit = i.track_metadata;
const name = shit.track_name;
return `- ${name} by ${shit.artist_name}`;
}).join('\n');
await interaction.followUp({
content: `the last 3 songs of ${user} was:\n\n- ${object[0].track_metadata.release_name} by ${object[0].track_metadata.artist_name}\n- ${object[1].track_metadata.release_name} by ${object[1].track_metadata.artist_name}\n- ${object[2].track_metadata.release_name} by ${object[2].track_metadata.artist_name}`
content: `The last ${historyAmount} songs of ${user} were:\n\n${songs}`
});
}
@ -45,6 +52,9 @@ export default class LastListenedCommand extends Command {
.setDescription("get that last listened music of a person").setIntegrationTypes([
ApplicationIntegrationType.UserInstall
])
.addIntegerOption(option => {
return option.setName("count").setDescription("amount of history you want").setRequired(false)
})
.addStringOption(option => {
return option.setName("user").setDescription("listenbrainz username").setRequired(false)
})

58
src/commands/loss.ts Normal file
View file

@ -0,0 +1,58 @@
import {Command} from "../command.ts";
import {
ActionRowBuilder,
ApplicationIntegrationType, ButtonBuilder, ButtonStyle,
ChatInputCommandInteraction, ContainerBuilder,
InteractionContextType, type MessageActionRowComponentBuilder, MessageFlags,
SlashCommandBuilder
} from "discord.js";
import { type Config } from "../config.ts";
export default class LossCommand extends Command {
async run(interaction: ChatInputCommandInteraction, config: Config) {
const components = [
new ContainerBuilder()
.addActionRowComponents(
new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents(
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setLabel("|")
.setCustomId("a"),
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setLabel("|i")
.setCustomId("b"),
),
)
.addActionRowComponents(
new ActionRowBuilder<MessageActionRowComponentBuilder>()
.addComponents(
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setLabel("||")
.setCustomId("c"),
new ButtonBuilder()
.setStyle(ButtonStyle.Secondary)
.setLabel("|_")
.setCustomId("d"),
),
),
];
await interaction.reply({
components: components,
flags: [MessageFlags.IsComponentsV2],
});
}
slashCommand = new SlashCommandBuilder()
.setName("loss")
.setDescription("why").setIntegrationTypes([
ApplicationIntegrationType.UserInstall
])
.setContexts([
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
]);
}

View file

@ -4,14 +4,16 @@ import {
ApplicationIntegrationType,
ButtonBuilder,
ButtonStyle,
ChatInputCommandInteraction,
ChatInputCommandInteraction, ContainerBuilder,
EmbedBuilder,
InteractionContextType,
SlashCommandBuilder
MessageFlags,
InteractionContextType, type MessageActionRowComponentBuilder, MessageFlagsBitField,
SlashCommandBuilder, TextDisplayBuilder, SectionBuilder, ThumbnailBuilder
} from "discord.js";
import {getSongOnPreferredProvider, kyzaify} from "../helper.ts"
import {type Config} from "../config.ts";
import type {S3Client} from "@aws-sdk/client-s3";
function keepV(url: string): string {
const urlObj = new URL(url);
@ -27,12 +29,11 @@ function keepV(url: string): string {
}
export default class PingCommand extends Command {
async run(interaction: ChatInputCommandInteraction, config: Config) {
async run(interaction: ChatInputCommandInteraction, config: Config): Promise<void> {
await interaction.deferReply()
const user = interaction.options.getString("user") ?? config.listenbrainzAccount;
const usesonglink = interaction.options.getBoolean("usesonglink") ?? true
const useitunes = interaction.options.getBoolean("useitunes") ?? true
const useitunes = interaction.options.getBoolean("useitunes") ?? false
const meow = await fetch(`https://api.listenbrainz.org/1/user/${user}/playing-now`).then((res) => res.json());
if (!meow) {
await interaction.followUp("something shat itself!");
@ -51,19 +52,22 @@ export default class PingCommand extends Command {
}
const songlink = await fetch(`https://api.song.link/v1-alpha.1/links?url=${link}`).then(a => a.json())
const preferredApi = getSongOnPreferredProvider(songlink, link)
if (preferredApi && usesonglink) {
const embed = new EmbedBuilder()
.setAuthor({
name: preferredApi.artist,
})
.setTitle(preferredApi.title)
.setThumbnail(preferredApi.thumbnailUrl)
.setFooter({
text: "amy jr",
});
const components = [
new ContainerBuilder()
.addSectionComponents(
new SectionBuilder()
.setThumbnailAccessory(
new ThumbnailBuilder()
.setURL(preferredApi.thumbnailUrl)
)
.addTextDisplayComponents(
new TextDisplayBuilder().setContent(`# ${preferredApi.artist} - ${preferredApi.title}`),
),
)
];
const meow = Object.keys(songlink.linksByPlatform)
let message = ""
const nya: ActionRowBuilder<ButtonBuilder>[] = [];
let currentRow = new ActionRowBuilder<ButtonBuilder>();
@ -83,11 +87,11 @@ export default class PingCommand extends Command {
if (currentRow.components.length > 0) {
nya.push(currentRow);
}
components[0].addActionRowComponents(nya)
await interaction.followUp({
components: nya,
embeds: [embed]
});
components: components,
flags: [MessageFlags.IsComponentsV2],
})
} else {
const embedfallback = new EmbedBuilder()
.setAuthor({
@ -97,7 +101,7 @@ export default class PingCommand extends Command {
.setFooter({
text: "song.link proxying was turned off or failed - amy jr",
});
await interaction.followUp({embeds:[embedfallback]})
}
}

41
src/commands/randnum.ts Normal file
View file

@ -0,0 +1,41 @@
import {Command} from "../command.ts";
import {
ApplicationIntegrationType,
ChatInputCommandInteraction,
InteractionContextType,
SlashCommandBuilder
} from "discord.js";
import { type Config } from "../config.ts";
export default class PingCommand extends Command {
async run(interaction: ChatInputCommandInteraction, config: Config) {
const upperbound = interaction.options.getInteger("upperbound")!;
const comment = interaction.options.getString("comment");
if (comment === null){
await interaction.reply({
content: "random number is: " + `${Math.floor(Math.random() * upperbound)}`,
});
return
}
await interaction.reply({
content: `chances of ${comment} out of ${upperbound} is ${Math.floor(Math.random() * upperbound)}`,
});
}
slashCommand = new SlashCommandBuilder()
.setName("randnum")
.setDescription("random number").setIntegrationTypes([
ApplicationIntegrationType.UserInstall
]).addIntegerOption(option => {
return option.setName("upperbound").setRequired(true).setDescription("idk nea told me")
}).addStringOption(option => {
return option.setName("comment").setRequired(false).setDescription("comment")
})
.setContexts([
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
]);
}

View file

@ -0,0 +1,51 @@
import {Command} from "../command.ts";
import {
ApplicationIntegrationType, type AutocompleteFocusedOption, AutocompleteInteraction,
ChatInputCommandInteraction,
InteractionContextType,
SlashCommandBuilder
} from "discord.js";
import { config, type Config } from "../config.ts";
import {DOWNLOAD_FOLDER_PATH, getFilesInFolder} from "./shitpost.ts";
import fs from "node:fs";
import path from "node:path";
export default class RenameshitpostCommand extends Command {
async run(interaction: ChatInputCommandInteraction, config: Config, ) {
await interaction.deferReply();
const originalname = interaction.options.getString("originalname")!;
const newname = interaction.options.getString("newname")!;
fs.renameSync(path.join(DOWNLOAD_FOLDER_PATH, originalname), path.join(DOWNLOAD_FOLDER_PATH, newname));
await interaction.followUp("uhhh this shit shouldve worked")
}
async autoComplete(interaction: AutocompleteInteraction, config: Config, option: AutocompleteFocusedOption): Promise<void> {
if (option.name === 'originalname') {
const files = await getFilesInFolder(DOWNLOAD_FOLDER_PATH);
const focusedValue = option.value.toLowerCase();
const filteredFiles = files.filter(choice => choice.name.toLowerCase().includes(focusedValue));
await interaction.respond(
filteredFiles.slice(0, 25)
);
}
}
slashCommand = new SlashCommandBuilder()
.setName("renameshitpost")
.setDescription("rename the shitpost").setIntegrationTypes([
ApplicationIntegrationType.UserInstall
]).addStringOption(option => {
return option.setName("originalname").setRequired(true).setDescription("the original shitpost name")
.setAutocomplete(true)
}).addStringOption(option => {
return option.setName("newname").setRequired(true).setDescription("the new shitpost name")
})
.setContexts([
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
]);
}

97
src/commands/shitpost.ts Normal file
View file

@ -0,0 +1,97 @@
import {Command} from "../command.ts";
import {
ApplicationIntegrationType, AttachmentBuilder, type AutocompleteFocusedOption, AutocompleteInteraction,
ChatInputCommandInteraction,
InteractionContextType,
SlashCommandBuilder
} from "discord.js";
import {config, type Config} from "../config.ts";
import {inspect} from "node:util";
import fs from "node:fs/promises";
import path from "node:path";
import {fileURLToPath} from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const BUCKETNAME = "shitposts" as const;
export const DOWNLOAD_FOLDER_PATH = path.join(__dirname, '..', '..', 'shitposts');
export async function getFilesInFolder(folderPath: string): Promise<{ name: string, value: string }[]> {
try {
const files = await fs.readdir(folderPath);
const fileList: { name: string, value: string }[] = [];
for (const file of files) {
const filePath = path.join(folderPath, file);
const stats = await fs.stat(filePath);
if (stats.isFile()) {
fileList.push({
name: file,
value: file
});
}
}
return fileList;
} catch (error) {
console.error(`Error reading directory ${folderPath}:`, error);
return [];
}
}
export default class ShitPostCommand extends Command {
async run(interaction: ChatInputCommandInteraction, config: Config) {
await interaction.deferReply();
const fileName = interaction.options.getString('shitpost', true);
const filePath = path.join(DOWNLOAD_FOLDER_PATH, fileName);
try {
await fs.access(filePath);
const attachment = new AttachmentBuilder(filePath, { name: fileName });
await interaction.editReply({
files: [attachment]
});
} catch (error: any) {
if (error.code === 'ENOENT') {
console.error(`file not found ${filePath}`, error);
await interaction.editReply({
content: `\`${fileName}\`. wasnt found, aka something shat itself`,
});
} else {
console.error(`Error sending file ${fileName}:`, error);
await interaction.editReply({
content: `buh, shitpost (\`${fileName}\`) wasnt posted.`,
});
}
}
}
async autoComplete(interaction: AutocompleteInteraction, config: Config, option: AutocompleteFocusedOption): Promise<void> {
const files = await getFilesInFolder(DOWNLOAD_FOLDER_PATH);
const focusedValue = option.value.toLowerCase();
const filteredFiles = files.filter(choice => choice.name.toLowerCase().includes(focusedValue));
await interaction.respond(
filteredFiles.slice(0, 25)
);
}
slashCommand = new SlashCommandBuilder()
.setName("shitpost")
.setDescription("shitpost with the posix file system!!!!!!").setIntegrationTypes([
ApplicationIntegrationType.UserInstall
]).addStringOption(option => {
return option.setName("shitpost").setRequired(true).setDescription("the shitposts name")
.setAutocomplete(true)
})
.setContexts([
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
]);
}

View file

@ -1,11 +1,14 @@
import rawconfig from "../config.json" with {type: "json"};
import {z} from 'zod';
import type {S3Client} from "@aws-sdk/client-s3";
const configT = z.object({
token: z.string(),
listenbrainzAccount: z.string(),
gitapi: z.string(),
sharkeyInstance:z.string(),
// applicationid: z.string(),
R2AccountID: z.string(),
R2AccessKeyId: z.string(),
R2SecretAccessKey: z.string(),
});
export type Config = z.infer<typeof configT>;
export const config: Config = configT.parse(rawconfig);

View file

@ -9,7 +9,8 @@ import path from "node:path";
import fs from "node:fs";
import { Command, ContextCommand, ICommand } from "./command.ts";
import { fileURLToPath } from "url";
import { config } from "./config.ts";
import {type Config, config} from "./config.ts";
import {ListObjectsV2Command, S3Client} from "@aws-sdk/client-s3";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -20,6 +21,7 @@ const client = new Client({
const allCommands: ICommand[] = []
const commandDir = path.join(__dirname, "commands");
for (const file of fs.readdirSync(commandDir)) {
if (!file.endsWith('.ts')) continue
@ -71,7 +73,7 @@ client.on(Events.InteractionCreate, async (interaction) => {
if (command.targetType != (interaction.isUserContextMenuCommand() ? ApplicationCommandType.User : ApplicationCommandType.Message))
console.error("Out of date discord definition of this context command")
try {
await command.run(interaction, interaction.isUserContextMenuCommand() ? interaction.targetUser : interaction.targetMessage)
await command.run(interaction, interaction.isUserContextMenuCommand() ? interaction.targetUser : interaction.targetMessage, config)
} catch (e) {
console.error("error during context command execution: " + commandName, e)
interaction.reply("something sharted itself")