diff --git a/src/commands/bauch.ts b/src/commands/bauch.ts new file mode 100644 index 00000000..8eea0b25 --- /dev/null +++ b/src/commands/bauch.ts @@ -0,0 +1,208 @@ +import { time, TimestampStyles, type User } from "discord.js"; + +import type { BotContext } from "#/context.ts"; +import type { MessageCommand } from "#/commands/command.ts"; +import type { ProcessableMessage } from "#/service/command.ts"; +import type { Bauch } from "#/storage/db/model.ts"; +import { randomEntry } from "#/service/random.ts"; +import * as bauch from "#/storage/bauch.ts"; + +import log from "#log"; + +interface BauchRating { + description: string; + /** Fettgehalt in Prozent */ + fettigkeit: number; + representation: string; +} + +const ratings: Record = { + 0: { + description: "Sixpack", + fettigkeit: 0, + representation: "| | | |", + }, + 1: { + description: "Waschbrettbauch", + fettigkeit: 5, + representation: "| | | |\n| | | |", + }, + 2: { + description: "Flachbauch", + fettigkeit: 10, + representation: "| | | |\n| | | |\n| | | |", + }, + 3: { + description: "Bierbauch (Anfangsstadium)", + fettigkeit: 20, + representation: "( )", + }, + 4: { + description: "Rettungsring", + fettigkeit: 30, + representation: "( )", + }, + 5: { + description: "Wampe", + fettigkeit: 40, + representation: "( )", + }, + 6: { + description: "Bierbauch", + fettigkeit: 50, + representation: "( )", + }, + 7: { + description: "Kugelbauch", + fettigkeit: 60, + representation: "( )", + }, + 8: { + description: "Schwabbelbauch", + fettigkeit: 70, + representation: "( )", + }, + 9: { + description: "Speckbauch", + fettigkeit: 80, + representation: "( )", + }, + 10: { + description: "Fass", + fettigkeit: 90, + representation: "( )", + }, + 11: { + description: "Doppelring", + fettigkeit: 95, + representation: "( )", + }, + 12: { + description: "Fass mit Rettungsring", + fettigkeit: 99, + representation: "( )", + }, + 13: { + description: "Fass deluxe", + fettigkeit: 100, + representation: "( )", + }, + 14: { + description: "Fass, Fass und nochmal Fass", + fettigkeit: 100, + representation: "( )", + }, + 15: { + description: "Tod durch Fett", + fettigkeit: 100, + representation: "( )", + }, + 16: { + description: "Fettbauch", + fettigkeit: 100, + representation: "( )", + }, + 17: { + description: "Planet", + fettigkeit: 100, + representation: "( )", + }, +}; + +const sendBauch = async ( + user: User, + message: ProcessableMessage, + size: number, + measurement: Date, +): Promise => { + const rating = ratings[size]; + if (!rating) { + throw new Error(`Bauch with size ${size} not defined`); + } + + await message.reply( + `${rating.description} von ${user}, Fettigkeit: ${rating.fettigkeit}%, gemessen um ${time(measurement, TimestampStyles.FullDateShortTime)}:\n${rating.representation}`, + ); +}; + +/** + * Bauch command. Displays the users belly and how fat it is + */ +export default class BauchCommand implements MessageCommand { + name = "bauch"; + aliases = [ + "wanst", + "wampe", + "plauze", + "bauchspeck", + "speckbauch", + "bierbauch", + "kugelbauch", + "schwabbelbauch", + "rettungsring", + "bauchansatz", + "magen", + "bauchdecke", + "ranzen", + "bauchladen", + "bauchmuskeln", + "sixpack", + "waschbrettbauch", + "bäuchlein", + "bauchfell", + "fettbauch", + "schmerbauch", + "bauchumfang", + "bauchfett", + "brustbauch", + "bierkasten", + "fass", + "kugel", + "hüftgold", + "liebesgriff", + "bauchmuskel", + "bauchspeckfalte", + ]; + description = "Zeigt dir die Fettigkeit des Bauchs eines Nutzers an."; + + async handleMessage(message: ProcessableMessage, context: BotContext) { + const { author } = message; + const mention = message.mentions.users.first(); + const userToMeasure = mention ?? author; + + const isBotBauch = userToMeasure.id === context.client.user.id; + if (isBotBauch) { + await message.reply(`${userToMeasure} hat natürlich das Sixpack.`); + return; + } + + log.debug(`${author.id} wants to measure bauch of user ${userToMeasure.id}`); + const measurement = await this.#getOrCreateMeasurement(userToMeasure); + + await sendBauch( + userToMeasure, + message, + measurement.size, + new Date(`${measurement.measuredAt}Z`), + ); + } + + async #getOrCreateMeasurement(userToMeasure: User): Promise { + const lastMeasurement = await bauch.fetchLastMeasurement(userToMeasure); + + if (lastMeasurement !== undefined) { + const now = new Date(); + const measurement = new Date(`${lastMeasurement.measuredAt}Z`); + // TODO: Make use of temporal lol + if (measurement.toISOString().split("T")[0] === now.toISOString().split("T")[0]) { + return lastMeasurement; + } + } + + log.debug(`No recent bauch measuring of ${userToMeasure.id} found. Creating Measurement`); + + const size = Number(randomEntry(Object.keys(ratings))); + + return await bauch.insertMeasurement(userToMeasure, size); + } +} diff --git a/src/commands/vagina.ts b/src/commands/vagina.ts new file mode 100644 index 00000000..5c6ab7ca --- /dev/null +++ b/src/commands/vagina.ts @@ -0,0 +1,213 @@ +import { time, TimestampStyles, type User } from "discord.js"; + +import type { BotContext } from "#/context.ts"; +import type { MessageCommand } from "#/commands/command.ts"; +import type { ProcessableMessage } from "#/service/command.ts"; +import type { Vagina } from "#/storage/db/model.ts"; +import { randomEntry } from "#/service/random.ts"; +import * as vagina from "#/storage/vagina.ts"; + +import log from "#log"; + +interface VaginaRating { + description: string; + /** Ausgeleiertheit in Prozent */ + ausgeleiertheit: number; + representation: string; +} + +const ratings: Record = { + 0: { + description: "Jungfrau", + ausgeleiertheit: 0, + representation: ">", + }, + 1: { + description: "Mausefalle", + ausgeleiertheit: 5, + representation: "<> ", + }, + 2: { + description: "Muschi", + ausgeleiertheit: 10, + representation: "( )", + }, + 3: { + description: "Fotze", + ausgeleiertheit: 20, + representation: "( )", + }, + 4: { + description: "Möse", + ausgeleiertheit: 30, + representation: "( )", + }, + 5: { + description: "Spalte", + ausgeleiertheit: 40, + representation: "( )", + }, + 6: { + description: "Fleischfotze", + ausgeleiertheit: 50, + representation: "( )", + }, + 7: { + description: "Schlaffmuschi", + ausgeleiertheit: 60, + representation: "( )", + }, + 8: { + description: "Pferdefotze", + ausgeleiertheit: 70, + representation: "( )", + }, + 9: { + description: "Scheunentor", + ausgeleiertheit: 80, + representation: "( )", + }, + 10: { + description: "Aktenschrank", + ausgeleiertheit: 90, + representation: "( )", + }, + 11: { + description: "Fleischfotze mit Pferdefotze", + ausgeleiertheit: 95, + representation: "( )", + }, + 12: { + description: "Fleischfotze deluxe", + ausgeleiertheit: 99, + representation: "( )", + }, + 13: { + description: "Pferdefotze deluxe", + ausgeleiertheit: 100, + representation: "( )", + }, + 14: { + description: "Fleischfotze, Pferdefotze und Scheunentor in einem", + ausgeleiertheit: 100, + representation: "( )", + }, + 15: { + description: "Tod durch Fotze", + ausgeleiertheit: 100, + representation: "( )", + }, + 16: { + description: "Schwarzes Loch", + ausgeleiertheit: 100, + representation: "( )", + }, + 17: { + description: "Der Abgrund", + ausgeleiertheit: 100, + representation: "( )", + }, +}; + +const sendVagina = async ( + user: User, + message: ProcessableMessage, + size: number, + measurement: Date, +): Promise => { + const rating = ratings[size]; + if (!rating) { + throw new Error(`Vagina with size ${size} not defined`); + } + + await message.reply( + `${rating.description} von ${user}, Ausgeleiertheit: ${rating.ausgeleiertheit}%, gemessen um ${time(measurement, TimestampStyles.FullDateShortTime)}:\n${rating.representation}`, + ); +}; + +/** + * Vagina command. Displays the users vagina and how ausgeleiert it is + */ +export default class VaginaCommand implements MessageCommand { + name = "vagina"; + aliases = [ + "fotze", + "fotzen", + "muschi", + "möse", + "moese", + "pussy", + "scheide", + "spalte", + "ritze", + "scham", + "schamlippen", + "vulva", + "grotte", + "bumsloch", + "fickloch", + "loch", + "gash", + "cunt", + "pflaume", + "austernschale", + "muschel", + "perle", + "götz", + "göthe", + "fickfresse", + "fleischfotze", + "pferdefotze", + "kackfotze", + "pissfotze", + "schrumpfmuschi", + "schnecke", + "biene", + "honigtopf", + "warmes loch", + "fickmöse", + "weiblichkeit", + ]; + description = "Zeigt dir die Ausgeleiertheit der Vagina eines Nutzers an."; + + async handleMessage(message: ProcessableMessage, context: BotContext) { + const { author } = message; + const mention = message.mentions.users.first(); + const userToMeasure = mention ?? author; + + const isBotVagina = userToMeasure.id === context.client.user.id; + if (isBotVagina) { + await message.reply(`${userToMeasure} ist natürlich am engsten.`); + return; + } + + log.debug(`${author.id} wants to measure vagina of user ${userToMeasure.id}`); + const measurement = await this.#getOrCreateMeasurement(userToMeasure); + + await sendVagina( + userToMeasure, + message, + measurement.size, + new Date(`${measurement.measuredAt}Z`), + ); + } + + async #getOrCreateMeasurement(userToMeasure: User): Promise { + const lastMeasurement = await vagina.fetchLastMeasurement(userToMeasure); + + if (lastMeasurement !== undefined) { + const now = new Date(); + const measurement = new Date(`${lastMeasurement.measuredAt}Z`); + // TODO: Make use of temporal lol + if (measurement.toISOString().split("T")[0] === now.toISOString().split("T")[0]) { + return lastMeasurement; + } + } + + log.debug(`No recent vagina measuring of ${userToMeasure.id} found. Creating Measurement`); + + const size = Number(randomEntry(Object.keys(ratings))); + + return await vagina.insertMeasurement(userToMeasure, size); + } +} diff --git a/src/storage/bauch.ts b/src/storage/bauch.ts new file mode 100644 index 00000000..4ba6cc4f --- /dev/null +++ b/src/storage/bauch.ts @@ -0,0 +1,49 @@ +import type { Snowflake, User } from "discord.js"; + +import type { Bauch } from "./db/model.ts"; + +import db from "#db"; +import log from "#log"; + +export function insertMeasurement( + user: User, + size: number, + measuredAt: Date = new Date(), + ctx = db(), +): Promise { + log.debug(`Saving Bauch Measurement for user ${user.id} with size ${size} from ${measuredAt}`); + + return ctx + .insertInto("bauch") + .values({ + userId: user.id, + size, + }) + .returningAll() + .executeTakeFirstOrThrow(); +} + +export function fetchLastMeasurement(user: User, ctx = db()): Promise { + return ctx + .selectFrom("bauch") + .where("userId", "=", user.id) + .orderBy("id", "desc") + .limit(1) + .selectAll() + .executeTakeFirst(); +} + +export async function getAverageBauchSizes(ctx = db()): Promise> { + const result = await ctx + .selectFrom("bauch") + .select(({ eb }) => eb.fn.avg("size").as("avgSize")) + .select("userId") + .groupBy("userId") + .execute(); + + const averageObj: Record = {}; + for (const userData of result) { + averageObj[userData.userId] = userData.avgSize; + } + return averageObj; +} diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index 11acbbf6..2198efbd 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -17,6 +17,8 @@ export interface Database { nickNames: NickNameTable; penis: PenisTable; boobs: BoobTable; + vagina: VaginaTable; + bauch: BauchTable; austrianTranslations: AustrianTranslationTable; ehreVotes: EhreVotesTable; ehrePoints: EhrePointsTable; @@ -137,6 +139,26 @@ export interface BoobTable extends AuditedTable { measuredAt: Generated; // TODO: Date is not supported by the DB driver } +export type Vagina = Selectable; + +export interface VaginaTable extends AuditedTable { + id: GeneratedAlways; + + userId: Snowflake; + size: number; + measuredAt: Generated; // TODO: Date is not supported by the DB driver +} + +export type Bauch = Selectable; + +export interface BauchTable extends AuditedTable { + id: GeneratedAlways; + + userId: Snowflake; + size: number; + measuredAt: Generated; // TODO: Date is not supported by the DB driver +} + export type AustrianTranslation = Selectable; export interface AustrianTranslationTable extends AuditedTable { diff --git a/src/storage/migrations/25-vagina-bauch.ts b/src/storage/migrations/25-vagina-bauch.ts new file mode 100644 index 00000000..5db5cc5e --- /dev/null +++ b/src/storage/migrations/25-vagina-bauch.ts @@ -0,0 +1,53 @@ +import { sql, type Kysely } from "kysely"; + +export async function up(db: Kysely) { + await db.schema + .createTable("vagina") + .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) + .addColumn("userId", "text", c => c.notNull()) + .addColumn("size", "integer", c => c.notNull()) + .addColumn("measuredAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .execute(); + + await db.schema + .createIndex("vagina_measuredAt") + .on("vagina") + .column("measuredAt desc") + .execute(); + + await createUpdatedAtTrigger(db, "vagina"); + + await db.schema + .createTable("bauch") + .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) + .addColumn("userId", "text", c => c.notNull()) + .addColumn("size", "integer", c => c.notNull()) + .addColumn("measuredAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .execute(); + + await db.schema.createIndex("bauch_measuredAt").on("bauch").column("measuredAt desc").execute(); + + await createUpdatedAtTrigger(db, "bauch"); +} + +function createUpdatedAtTrigger(db: Kysely, tableName: string) { + return sql + .raw(` + create trigger ${tableName}_updatedAt + after update on ${tableName} for each row + begin + update ${tableName} + set updatedAt = current_timestamp + where id = old.id; + end; + `) + .execute(db); +} + +export async function down(_db: Kysely) { + throw new Error("Not supported lol"); +} diff --git a/src/storage/vagina.ts b/src/storage/vagina.ts new file mode 100644 index 00000000..73eb6f8f --- /dev/null +++ b/src/storage/vagina.ts @@ -0,0 +1,49 @@ +import type { Snowflake, User } from "discord.js"; + +import type { Vagina } from "./db/model.ts"; + +import db from "#db"; +import log from "#log"; + +export function insertMeasurement( + user: User, + size: number, + measuredAt: Date = new Date(), + ctx = db(), +): Promise { + log.debug(`Saving Vagina Measurement for user ${user.id} with size ${size} from ${measuredAt}`); + + return ctx + .insertInto("vagina") + .values({ + userId: user.id, + size, + }) + .returningAll() + .executeTakeFirstOrThrow(); +} + +export function fetchLastMeasurement(user: User, ctx = db()): Promise { + return ctx + .selectFrom("vagina") + .where("userId", "=", user.id) + .orderBy("id", "desc") + .limit(1) + .selectAll() + .executeTakeFirst(); +} + +export async function getAverageVaginaSizes(ctx = db()): Promise> { + const result = await ctx + .selectFrom("vagina") + .select(({ eb }) => eb.fn.avg("size").as("avgSize")) + .select("userId") + .groupBy("userId") + .execute(); + + const averageObj: Record = {}; + for (const userData of result) { + averageObj[userData.userId] = userData.avgSize; + } + return averageObj; +}