├── .eslintignore ├── .prettierignore ├── images ├── case.png ├── join.png ├── skin.png ├── history.png ├── wordle.png ├── blackjack.png ├── streetrace.png └── reactionroles.jpg ├── .prettierrc ├── .gitattributes ├── prisma └── migrations │ ├── 20240414134550_car_skins │ └── migration.sql │ ├── 20250315155539_captcha_ip │ └── migration.sql │ ├── 20241127190210_daily_lotto │ └── migration.sql │ ├── 20250926153426_uuid_v7 │ └── migration.sql │ ├── 20230118125106_ │ └── migration.sql │ ├── 20230820194115_stats_bigint │ └── migration.sql │ ├── 20250203133006_support_notify │ └── migration.sql │ ├── 20250219104044_sellall_filter │ └── migration.sql │ ├── 20250704111219_disabled_channels │ └── migration.sql │ ├── 20230320141903_booster │ └── migration.sql │ ├── 20250208131102_tips │ └── migration.sql │ ├── 20230319213643_admin_level │ └── migration.sql │ ├── 20240601174412_prefixes │ └── migration.sql │ ├── 20250326180516_vote_streak │ └── migration.sql │ ├── 20240618125526_moderation_restructure_3 │ └── migration.sql │ ├── 20241216112241_plant_health_dm │ └── migration.sql │ ├── 20250525192223_guild_name_icon │ └── migration.sql │ ├── 20250528191504_marketdelay │ └── migration.sql │ ├── 20240627123048_automute_timeout │ └── migration.sql │ ├── 20250212115046_achievement_progress_bigint │ └── migration.sql │ ├── 20240618131325_moderation_restructure_8_optional_old │ └── migration.sql │ ├── 20240811202754_offer_expire │ └── migration.sql │ ├── migration_lock.toml │ ├── 20230328112837_ │ └── migration.sql │ ├── 20230613185826_avatar_storage │ └── migration.sql │ ├── 20250607180552_auto_close_tickets │ └── migration.sql │ ├── 20241103130515_game_index │ └── migration.sql │ ├── 20231204122704_vote_counts │ └── migration.sql │ ├── 20240115154904_auction_offers_index │ └── migration.sql │ ├── 20240508171744_captcha_relation │ └── migration.sql │ ├── 20240618125918_moderation_restructure_6_rename │ └── migration.sql │ ├── 20250713001811_guild_stats_today │ └── migration.sql │ ├── 20230817131725_guild_this_level │ └── migration.sql │ ├── 20241205131225_water_fertilise │ └── migration.sql │ ├── 20250831163233_tmdb_user_relation │ └── migration.sql │ ├── 20250731162335_remove_transaction_relation │ └── migration.sql │ ├── 20250807165306_guild_admins │ └── migration.sql │ ├── 20240608140104_remove_single_prefix │ └── migration.sql │ ├── 20240831142941_birthdays │ └── migration.sql │ ├── 20250516075717_remove_lastweekly │ └── migration.sql │ ├── 20240618120718_moderation_restructure_1 │ └── migration.sql │ ├── 20240716122238_images │ └── migration.sql │ ├── 20240602143122_cr_word_lists │ └── migration.sql │ ├── 20240618150510_moderation_restructure_9_delete_old │ └── migration.sql │ ├── 20250525191503_global_mentions │ └── migration.sql │ ├── 20241120181655_guild_avatar │ └── migration.sql │ ├── 20240101000527_cascade │ └── migration.sql │ ├── 20240108134438_premium_credit │ └── migration.sql │ ├── 20240618125712_moderation_restructure_4_not_optional │ └── migration.sql │ ├── 20240622154302_bot_metrics │ └── migration.sql │ ├── 20250624214253_market_caps │ └── migration.sql │ ├── 20250115132522_remove_old_wordle_table │ └── migration.sql │ ├── 20240621144431_better_date_username │ └── migration.sql │ ├── 20241214150741_remove_lottery_ticket_table │ └── migration.sql │ ├── 20230704115213_mention_new_id │ └── migration.sql │ ├── 20250115132927_cascade_userid_index │ └── migration.sql │ ├── 20251123114348_global_boosters │ └── migration.sql │ ├── 20230711012548_mentions_guildid_index │ └── migration.sql │ ├── 20250616183043_tmdb_nypsi_ratings │ └── migration.sql │ ├── 20230105130132_useraliases │ └── migration.sql │ ├── 20241228130156_chatfilter_model │ └── migration.sql │ ├── 20240508154137_captcha │ └── migration.sql │ ├── 20240711120338_botmetrics_float │ └── migration.sql │ ├── 20241213121926_remove_images │ └── migration.sql │ ├── 20230704122334_use_uuid_for_mention │ └── migration.sql │ ├── 20230328141729_ │ └── migration.sql │ ├── 20230319134324_preferences │ └── migration.sql │ ├── 20230711151147_random_drops_activechannels │ └── migration.sql │ ├── 20230817213226_alts │ └── migration.sql │ ├── 20240906122452_aura │ └── migration.sql │ ├── 20241209031848_farm_upgrades │ └── migration.sql │ ├── 20250114163501_wordle_stats │ └── migration.sql │ ├── 20250824153622_store_guild_members │ └── migration.sql │ ├── 20230720152701_purchase_history │ └── migration.sql │ ├── 20240214161917_ │ └── migration.sql │ ├── 20250508183538_support_request_messages │ └── migration.sql │ ├── 20250825121611_username_last_updated │ └── migration.sql │ ├── 20240626174321_cr_leaderboard │ └── migration.sql │ ├── 20250318191631_guess_the_flag_stats │ └── migration.sql │ ├── 20241216123924_purchases_table │ └── migration.sql │ ├── 20250601105124_improve_some_indexes │ └── migration.sql │ ├── 20250525151118_remove_auctions │ └── migration.sql │ ├── 20240618131236_moderation_restrcture_7_idsindex │ └── migration.sql │ ├── 20240712170920_farms │ └── migration.sql │ ├── 20240618125839_moderation_restructure_5_temp_id │ └── migration.sql │ ├── 20250618034837_marriage │ └── migration.sql │ ├── 20230416195103_ │ └── migration.sql │ ├── 20250419194232_item_trades │ └── migration.sql │ ├── 20230909192255_levelling │ └── migration.sql │ ├── 20240318163724_session_table │ └── migration.sql │ ├── 20241228150110_remove_unused_data │ └── migration.sql │ ├── 20250707074050_transactions │ └── migration.sql │ ├── 20230614202711_website_data │ └── migration.sql │ ├── 20231208142222_profile_views │ └── migration.sql │ ├── 20230218201725_ │ └── migration.sql │ ├── 20240619222431_remove_useless_index │ └── migration.sql │ ├── 20230727133856_tags │ └── migration.sql │ ├── 20240210132835_tasks │ └── migration.sql │ ├── 20240411143533_custom_cars │ └── migration.sql │ ├── 20230327175012_ │ └── migration.sql │ ├── 20231218174018_images │ └── migration.sql │ ├── 20221223210813_counters_update │ └── migration.sql │ ├── 20250711150239_events │ └── migration.sql │ ├── 20241214122729_z │ └── migration.sql │ ├── 20240618124410_moderation_restructure_2 │ └── migration.sql │ └── 20240619184548_mode_ration_evidence │ └── migration.sql ├── src ├── types │ ├── Tags.ts │ ├── Market.ts │ ├── Snipe.ts │ ├── Moderation.ts │ ├── Kofi.ts │ ├── Karmashop.ts │ ├── Jobs.ts │ ├── Tasks.ts │ ├── InteractionHandler.ts │ ├── Notification.ts │ ├── StreetRace.ts │ ├── LootPool.ts │ ├── Chart.ts │ └── Workers.ts ├── init │ ├── redis.ts │ ├── s3.ts │ └── database.ts ├── utils │ ├── functions │ │ ├── sleep.ts │ │ ├── guilds │ │ │ ├── birthday.ts │ │ │ ├── disabledcommands.ts │ │ │ ├── channels.ts │ │ │ ├── altpunish.ts │ │ │ └── slash.ts │ │ ├── topgg.ts │ │ ├── economy │ │ │ ├── items │ │ │ │ ├── calendar.ts │ │ │ │ ├── teddy.ts │ │ │ │ ├── lottery_ticket.ts │ │ │ │ ├── bitch.ts │ │ │ │ ├── football.ts │ │ │ │ ├── padlock.ts │ │ │ │ └── dave.ts │ │ │ ├── lottery.ts │ │ │ └── passive.ts │ │ ├── chatreactions │ │ │ └── blacklisted.ts │ │ ├── color.ts │ │ ├── outliers.ts │ │ ├── workers │ │ │ ├── sort.ts │ │ │ └── wordlesort.ts │ │ ├── users │ │ │ ├── wordle.ts │ │ │ ├── mentions.ts │ │ │ ├── admin.ts │ │ │ └── aura.ts │ │ ├── mutex.ts │ │ ├── random.ts │ │ ├── openai.ts │ │ ├── premium │ │ │ └── color.ts │ │ ├── clusters.ts │ │ └── heartbeat.ts │ └── queues │ │ └── queues.ts ├── events │ ├── roleDelete.ts │ ├── guildUpdate.ts │ ├── entitlementUpdate.ts │ ├── entitlementDelete.ts │ ├── guildCreate.ts │ ├── emojiDelete.ts │ ├── guildDelete.ts │ ├── emojiUpdate.ts │ ├── channelDelete.ts │ ├── emojiCreate.ts │ ├── entitlementCreate.ts │ ├── channelUpdate.ts │ ├── messageDeleteBulk.ts │ └── channelCreate.ts ├── scheduled │ ├── jobs │ │ ├── z.ts │ │ ├── resetvote.ts │ │ ├── dailycr.ts │ │ ├── counters.ts │ │ ├── premiumexpire.ts │ │ ├── netupdate-3.ts │ │ ├── netupdate-1.ts │ │ ├── netupdate-2.ts │ │ ├── topbalance.ts │ │ ├── crafted.ts │ │ └── offerexpire.ts │ └── scheduler.ts ├── api │ └── middleware │ │ └── logger.ts ├── commands │ ├── support.ts │ ├── logs.ts │ ├── prefix.ts │ ├── modlogs.ts │ ├── servericon.ts │ ├── getcsvdata.ts │ ├── rules.ts │ ├── auction.ts │ ├── season.ts │ ├── invite.ts │ ├── maxbet.ts │ ├── ecohistory.ts │ ├── github.ts │ ├── wiki.ts │ ├── roll.ts │ ├── deleteslash.ts │ ├── reloadslash.ts │ ├── karmahelp.ts │ ├── reload.ts │ ├── inspiration.ts │ ├── garden.ts │ ├── reloaditems.ts │ ├── bitcoin.ts │ ├── ethereum.ts │ ├── news.ts │ ├── enlarge.ts │ ├── banner.ts │ ├── clearsnipe.ts │ ├── multi.ts │ ├── toggletracking.ts │ └── requestdm.ts ├── interactions │ ├── bake.ts │ ├── fish.ts │ ├── hunt.ts │ ├── mine.ts │ ├── vote_reminders.ts │ ├── aliases.ts │ ├── crash_out.ts │ ├── workers_claim.ts │ ├── transfer.ts │ ├── reaction_role.ts │ ├── item_global.ts │ ├── item_buy.ts │ ├── craft_item.ts │ ├── achievements.ts │ ├── crash_join.ts │ ├── item.ts │ └── car.ts └── models │ └── StatsProfile.ts ├── .bruno ├── bruno.json ├── collection.bru ├── reboot.bru ├── status.bru ├── item value.bru ├── index.bru ├── vote.bru └── kofi.bru ├── .gitignore ├── prisma.config.ts ├── setup.sh ├── tsconfig.json ├── .github ├── dependabot.yml └── workflows │ └── dependency-review.yml ├── CONTRIBUTING.md ├── .eslintrc ├── data ├── bakery_upgrades.json ├── guild_upgrades.json └── upgrades.json ├── .env.example ├── LICENSE └── test └── lootPools.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.js -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | dist/ -------------------------------------------------------------------------------- /images/case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/case.png -------------------------------------------------------------------------------- /images/join.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/join.png -------------------------------------------------------------------------------- /images/skin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/skin.png -------------------------------------------------------------------------------- /images/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/history.png -------------------------------------------------------------------------------- /images/wordle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/wordle.png -------------------------------------------------------------------------------- /images/blackjack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/blackjack.png -------------------------------------------------------------------------------- /images/streetrace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/streetrace.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "endOfLine": "lf" 5 | } 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /images/reactionroles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mxz7/nypsi/HEAD/images/reactionroles.jpg -------------------------------------------------------------------------------- /prisma/migrations/20240414134550_car_skins/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "CustomCar" ADD COLUMN "skin" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250315155539_captcha_ip/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Captcha" ADD COLUMN "solvedIp" TEXT; 3 | -------------------------------------------------------------------------------- /src/types/Tags.ts: -------------------------------------------------------------------------------- 1 | export type Tag = { 2 | id: string; 3 | name: string; 4 | description: string; 5 | emoji: string; 6 | }; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20241127190210_daily_lotto/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Economy" ADD COLUMN "dailyLottery" INTEGER; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250926153426_uuid_v7/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "public"."Captcha" ALTER COLUMN "id" DROP DEFAULT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230118125106_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Economy" ADD COLUMN "passive" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230820194115_stats_bigint/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Stats" ALTER COLUMN "amount" SET DATA TYPE BIGINT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250203133006_support_notify/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "SupportRequest" ADD COLUMN "notify" TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250219104044_sellall_filter/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Economy" ADD COLUMN "sellallFilter" TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250704111219_disabled_channels/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Guild" ADD COLUMN "disabledChannels" TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230320141903_booster/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "booster" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250208131102_tips/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Preferences" ADD COLUMN "tips" BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20230319213643_admin_level/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "adminLevel" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240601174412_prefixes/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Guild" ADD COLUMN "prefixes" TEXT[] DEFAULT ARRAY['$']::TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250326180516_vote_streak/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Economy" ADD COLUMN "voteStreak" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240618125526_moderation_restructure_3/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "ModerationCase" ALTER COLUMN "caseId_new" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /src/init/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | 3 | const redis = new Redis({ 4 | showFriendlyErrorStack: true, 5 | }); 6 | 7 | export default redis; 8 | -------------------------------------------------------------------------------- /.bruno/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "nypsi api", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } -------------------------------------------------------------------------------- /prisma/migrations/20241216112241_plant_health_dm/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "DMSettings" ADD COLUMN "farmHealth" BOOLEAN NOT NULL DEFAULT true; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250525192223_guild_name_icon/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Guild" ADD COLUMN "icon" TEXT, 3 | ADD COLUMN "name" TEXT; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20250528191504_marketdelay/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Preferences" ADD COLUMN "marketDelay" INTEGER NOT NULL DEFAULT 300; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240627123048_automute_timeout/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Guild" ADD COLUMN "autoMuteExpire" INTEGER NOT NULL DEFAULT 86400; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250212115046_achievement_progress_bigint/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Achievements" ALTER COLUMN "progress" SET DATA TYPE BIGINT; 3 | -------------------------------------------------------------------------------- /.bruno/collection.bru: -------------------------------------------------------------------------------- 1 | auth { 2 | mode: bearer 3 | } 4 | 5 | auth:bearer { 6 | token: meow 7 | } 8 | 9 | vars:pre-request { 10 | api: 127.0.0.1:6969 11 | } 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240618131325_moderation_restructure_8_optional_old/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "ModerationCase" ALTER COLUMN "caseId_old" DROP NOT NULL; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240811202754_offer_expire/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Offer" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (e.g., Git) 3 | provider = "postgresql" 4 | -------------------------------------------------------------------------------- /.bruno/reboot.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: reboot 3 | type: http 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: {{api}}/reboot 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /.bruno/status.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: status 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{api}}/status 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230328112837_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Offer" ADD COLUMN "sold" BOOLEAN NOT NULL DEFAULT false, 3 | ADD COLUMN "soldAt" TIMESTAMP(3); 4 | -------------------------------------------------------------------------------- /src/types/Market.ts: -------------------------------------------------------------------------------- 1 | export interface DMQueue { 2 | userId: string; 3 | createdAt: number; // unix date 4 | earned: number; 5 | items: Record>; 6 | } 7 | -------------------------------------------------------------------------------- /.bruno/item value.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: item value 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: {{api}}/item/value/banana 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230613185826_avatar_storage/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "avatar" TEXT NOT NULL DEFAULT 'https://cdn.discordapp.com/embed/avatars/0.png'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20250607180552_auto_close_tickets/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "SupportRequest" ADD COLUMN "latestActivity" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20241103130515_game_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Game_userId_idx"; 3 | 4 | -- CreateIndex 5 | CREATE INDEX "Game_userId_game_idx" ON "Game"("userId", "game"); 6 | -------------------------------------------------------------------------------- /prisma/migrations/20231204122704_vote_counts/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Economy" ADD COLUMN "monthVote" INTEGER NOT NULL DEFAULT 0, 3 | ADD COLUMN "seasonVote" INTEGER NOT NULL DEFAULT 0; 4 | -------------------------------------------------------------------------------- /src/types/Snipe.ts: -------------------------------------------------------------------------------- 1 | export interface SnipedMessage { 2 | content: string; 3 | member: string; 4 | createdTimestamp: number; 5 | memberAvatar: string; 6 | channel: { 7 | id: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240115154904_auction_offers_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "Auction_itemId_idx" ON "Auction"("itemId"); 3 | 4 | -- CreateIndex 5 | CREATE INDEX "Offer_itemId_idx" ON "Offer"("itemId"); 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240508171744_captcha_relation/migration.sql: -------------------------------------------------------------------------------- 1 | -- AddForeignKey 2 | ALTER TABLE "Captcha" ADD CONSTRAINT "Captcha_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 3 | -------------------------------------------------------------------------------- /src/utils/functions/sleep.ts: -------------------------------------------------------------------------------- 1 | export default async function sleep(ms: number) { 2 | if (ms <= 0) return; 3 | return new Promise((resolve) => { 4 | setTimeout(() => { 5 | resolve(0); 6 | }, ms); 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /.bruno/index.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: index 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{api}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "woof": "woof" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /prisma/migrations/20240618125918_moderation_restructure_6_rename/migration.sql: -------------------------------------------------------------------------------- 1 | 2 | -- AlterTable 3 | ALTER TABLE "ModerationCase" RENAME COLUMN "caseId" TO "caseId_old"; 4 | ALTER TABLE "ModerationCase" RENAME COLUMN "caseId_new" TO "caseId"; 5 | -------------------------------------------------------------------------------- /prisma/migrations/20250713001811_guild_stats_today/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "EconomyGuildMember" ADD COLUMN "contributedMoneyToday" BIGINT NOT NULL DEFAULT 0, 3 | ADD COLUMN "contributedXpToday" INTEGER NOT NULL DEFAULT 0; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20230817131725_guild_this_level/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "EconomyGuildMember" ADD COLUMN "contributedMoneyThisLevel" BIGINT NOT NULL DEFAULT 0, 3 | ADD COLUMN "contributedXpThisLevel" INTEGER NOT NULL DEFAULT 0; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20241205131225_water_fertilise/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Farm" ADD COLUMN "fertilisedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 3 | ADD COLUMN "wateredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20250831163233_tmdb_user_relation/migration.sql: -------------------------------------------------------------------------------- 1 | -- AddForeignKey 2 | ALTER TABLE "public"."tmdbRatings" ADD CONSTRAINT "tmdbRatings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 3 | -------------------------------------------------------------------------------- /src/utils/queues/queues.ts: -------------------------------------------------------------------------------- 1 | import { Queue } from "bullmq"; 2 | import redis from "../../init/redis"; 3 | import { NotificationPayload } from "../../types/Notification"; 4 | 5 | export const dmQueue = new Queue("dms", { connection: redis }); 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250731162335_remove_transaction_relation/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Transaction" DROP CONSTRAINT "Transaction_sourceId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "Transaction" DROP CONSTRAINT "Transaction_targetId_fkey"; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20250807165306_guild_admins/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "EconomyGuildRole" AS ENUM ('member', 'admin', 'owner'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "EconomyGuildMember" ADD COLUMN "role" "EconomyGuildRole" NOT NULL DEFAULT 'member'; 6 | -------------------------------------------------------------------------------- /src/utils/functions/guilds/birthday.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../../init/database"; 2 | 3 | export async function setBirthdayChannel(guildId: string, hook: string | null) { 4 | await prisma.guild.update({ where: { id: guildId }, data: { birthdayHook: hook } }); 5 | } 6 | -------------------------------------------------------------------------------- /src/types/Moderation.ts: -------------------------------------------------------------------------------- 1 | export type PunishmentType = 2 | | "mute" 3 | | "ban" 4 | | "unmute" 5 | | "warn" 6 | | "kick" 7 | | "unban" 8 | | "filter violation"; 9 | 10 | export type LogType = "member" | "message" | "channel" | "role" | "server" | "emoji"; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20240608140104_remove_single_prefix/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `prefix` on the `Guild` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Guild" DROP COLUMN "prefix"; 9 | -------------------------------------------------------------------------------- /src/types/Kofi.ts: -------------------------------------------------------------------------------- 1 | export interface KofiResponse { 2 | type: string; 3 | email: string; 4 | tier_name?: string; 5 | shop_items?: { direct_link_code: string; quantity: number }[]; 6 | verification_token: string; 7 | is_public: boolean; 8 | amount: string; 9 | } 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240831142941_birthdays/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Guild" ADD COLUMN "birthdayHook" TEXT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "birthday" TIMESTAMP(3), 6 | ADD COLUMN "birthdayAnnounce" BOOLEAN NOT NULL DEFAULT true; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20250516075717_remove_lastweekly/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `lastWeekly` on the `Premium` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Premium" DROP COLUMN "lastWeekly"; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | backup/ 3 | logs/ 4 | dist/ 5 | out/ 6 | 7 | *.env 8 | *.db 9 | *.db-shm 10 | *.db-wal 11 | *.DS_STORE 12 | 13 | *.tsbuildinfo 14 | *.eslintcache 15 | *.prettiercache 16 | 17 | anticheat.ts 18 | anticheat_old.ts 19 | anticheat2.ts 20 | 21 | src/generated -------------------------------------------------------------------------------- /prisma/migrations/20240618120718_moderation_restructure_1/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Guild" ADD COLUMN "automute" INTEGER[] DEFAULT ARRAY[0, 60, 120, 300]::INTEGER[], 3 | ADD COLUMN "logs" TEXT, 4 | ADD COLUMN "modlogs" TEXT, 5 | ADD COLUMN "muteRole" TEXT; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240716122238_images/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Images" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "bytes" BIGINT NOT NULL, 6 | 7 | CONSTRAINT "Images_pkey" PRIMARY KEY ("id") 8 | ); 9 | -------------------------------------------------------------------------------- /.bruno/vote.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: vote 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{api}}/vote 9 | body: json 10 | auth: none 11 | } 12 | 13 | headers { 14 | Authorization: meow 15 | } 16 | 17 | body:json { 18 | { 19 | "user": "672793821850894347" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/types/Karmashop.ts: -------------------------------------------------------------------------------- 1 | export type KarmaShopItem = { 2 | name: string; 3 | emoji: string; 4 | id: string; 5 | cost: number; 6 | items_left: number; 7 | aliases: string[]; 8 | type: "item" | "premium" | "xp"; 9 | value: string; 10 | bought: string[]; 11 | limit: number; 12 | }; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20240602143122_cr_word_lists/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "ChatReactionWordList" AS ENUM ('english_1k', 'english_5k', 'english_10k', 'custom'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "ChatReaction" ADD COLUMN "wordListType" "ChatReactionWordList" NOT NULL DEFAULT 'english_1k'; 6 | -------------------------------------------------------------------------------- /src/events/roleDelete.ts: -------------------------------------------------------------------------------- 1 | import { Role } from "discord.js"; 2 | import { getMuteRole, setMuteRole } from "../utils/functions/moderation/mute"; 3 | 4 | export default async function roleDelete(role: Role) { 5 | if ((await getMuteRole(role.guild)) == role.id) { 6 | await setMuteRole(role.guild, ""); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240618150510_moderation_restructure_9_delete_old/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `caseId_old` on the `ModerationCase` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "ModerationCase" DROP COLUMN "caseId_old"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250525191503_global_mentions/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "Mention_guildId_idx"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Preferences" ADD COLUMN "mentionsGlobal" BOOLEAN NOT NULL DEFAULT false; 6 | 7 | -- CreateIndex 8 | CREATE INDEX "Mention_targetId_idx" ON "Mention"("targetId"); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241120181655_guild_avatar/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "EconomyGuild" ADD COLUMN "avatarId" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "EconomyGuild" ADD CONSTRAINT "EconomyGuild_avatarId_fkey" FOREIGN KEY ("avatarId") REFERENCES "Images"("id") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /prisma/migrations/20240101000527_cascade/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "AuctionWatch" DROP CONSTRAINT "AuctionWatch_userId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "AuctionWatch" ADD CONSTRAINT "AuctionWatch_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /src/types/Jobs.ts: -------------------------------------------------------------------------------- 1 | import { ClusterManager } from "discord-hybrid-sharding"; 2 | 3 | export type Job = { 4 | name: string; 5 | cron: string; 6 | run: 7 | | ((log: (message: string) => void, manager?: ClusterManager) => any) 8 | | ((log: (message: string) => void, manager?: ClusterManager) => Promise); 9 | }; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240108134438_premium_credit/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `status` on the `Premium` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Premium" DROP COLUMN "status", 9 | ADD COLUMN "credit" INTEGER NOT NULL DEFAULT 0; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240618125712_moderation_restructure_4_not_optional/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `caseId_new` on table `ModerationCase` required. This step will fail if there are existing NULL values in that column. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "ModerationCase" ALTER COLUMN "caseId_new" SET NOT NULL; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240622154302_bot_metrics/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "BotMetrics" ( 3 | "id" SERIAL NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "category" TEXT NOT NULL, 6 | "value" INTEGER NOT NULL, 7 | 8 | CONSTRAINT "BotMetrics_pkey" PRIMARY KEY ("id") 9 | ); 10 | -------------------------------------------------------------------------------- /src/scheduled/jobs/z.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../init/database"; 2 | import { Job } from "../../types/Jobs"; 3 | 4 | export default { 5 | name: "z", 6 | cron: "0 0 1 * *", 7 | async run() { 8 | await prisma.z.updateMany({ 9 | data: { 10 | hasInvite: true, 11 | }, 12 | }); 13 | }, 14 | } satisfies Job; 15 | -------------------------------------------------------------------------------- /src/types/Tasks.ts: -------------------------------------------------------------------------------- 1 | export interface Task { 2 | id: string; 3 | name: string; 4 | description: string; 5 | complete_gif?: string; 6 | target: number[]; // random from the list; 7 | prizes: string[]; // random prize will be chosen. in format of (id/money/xp/karma):(item/amount):(amount if item) 8 | type: "daily" | "weekly"; 9 | } 10 | -------------------------------------------------------------------------------- /prisma.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { defineConfig, env } from "prisma/config"; 3 | 4 | export default defineConfig({ 5 | schema: "prisma/schema.prisma", 6 | migrations: { 7 | path: "prisma/migrations", 8 | seed: "src/scripts/seed.ts", 9 | }, 10 | datasource: { 11 | url: env("DATABASE_URL"), 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/events/guildUpdate.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from "discord.js"; 2 | import { updateGuild } from "../utils/functions/guilds/utils"; 3 | 4 | export default async function guildUpdate(oldGuild: Guild, newGuild: Guild) { 5 | if (oldGuild.name !== newGuild.name || oldGuild.iconURL() !== newGuild.iconURL()) { 6 | await updateGuild(newGuild); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /prisma/migrations/20250624214253_market_caps/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to alter the column `itemAmount` on the `Market` table. The data in that column could be lost. The data in that column will be cast from `BigInt` to `Integer`. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Market" ALTER COLUMN "itemAmount" SET DATA TYPE INTEGER; 9 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | mkdir out 2 | 3 | cd src 4 | cd utils 5 | cd functions 6 | printf "// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport function a(b: string, c: string, d:string, e?: string) {} export function b(a: string): any {} export function c(a: string) {}" > anticheat.ts 7 | 8 | cd .. 9 | cd .. 10 | cd .. 11 | 12 | cp -i .env.example .env 13 | -------------------------------------------------------------------------------- /prisma/migrations/20250115132522_remove_old_wordle_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `WordleStats` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "WordleStats" DROP CONSTRAINT "WordleStats_userId_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "WordleStats"; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240621144431_better_date_username/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `date` on the `Username` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Username" RENAME COLUMN "date" TO "createdAt"; 9 | ALTER TABLE "Username" ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20241214150741_remove_lottery_ticket_table/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `LotteryTicket` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "LotteryTicket" DROP CONSTRAINT "LotteryTicket_userId_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "LotteryTicket"; 12 | -------------------------------------------------------------------------------- /src/events/entitlementUpdate.ts: -------------------------------------------------------------------------------- 1 | import { Entitlement } from "discord.js"; 2 | import { renewUser } from "../utils/functions/premium/premium"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default async function entitlementUpdate(entitlement: Entitlement) { 6 | logger.info("received entitlement update", entitlement.toJSON()); 7 | 8 | await renewUser(entitlement.userId); 9 | } 10 | -------------------------------------------------------------------------------- /prisma/migrations/20230704115213_mention_new_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `Mention` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Mention" DROP CONSTRAINT "Mention_pkey", 9 | ADD COLUMN "id" SERIAL NOT NULL, 10 | ADD CONSTRAINT "Mention_pkey" PRIMARY KEY ("id"); 11 | -------------------------------------------------------------------------------- /prisma/migrations/20250115132927_cascade_userid_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "WordleGame" DROP CONSTRAINT "WordleGame_userId_fkey"; 3 | 4 | -- CreateIndex 5 | CREATE INDEX "WordleGame_userId_idx" ON "WordleGame"("userId"); 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "WordleGame" ADD CONSTRAINT "WordleGame_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20251123114348_global_boosters/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "BoosterScope" AS ENUM ('global', 'user'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "Booster" ADD COLUMN "scope" "BoosterScope" NOT NULL DEFAULT 'user'; 6 | 7 | -- CreateIndex 8 | CREATE INDEX "Booster_scope_idx" ON "Booster"("scope"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "Booster_userId_idx" ON "Booster"("userId"); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230711012548_mentions_guildid_index/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Leaderboards" DROP CONSTRAINT "Leaderboards_userId_fkey"; 3 | 4 | -- CreateIndex 5 | CREATE INDEX "Mention_guildId_idx" ON "Mention"("guildId"); 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "Leaderboards" ADD CONSTRAINT "Leaderboards_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /src/utils/functions/topgg.ts: -------------------------------------------------------------------------------- 1 | import Constants from "../Constants"; 2 | 3 | export function updateStats(guildCount: number, shardCount: number) { 4 | fetch(`https://top.gg/api/bots/${Constants.BOT_USER_ID}/stats`, { 5 | headers: { 6 | authorization: process.env.TOPGG_TOKEN, 7 | }, 8 | body: JSON.stringify({ server_count: guildCount, shard_count: shardCount }), 9 | method: "post", 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/events/entitlementDelete.ts: -------------------------------------------------------------------------------- 1 | import { Entitlement } from "discord.js"; 2 | import { setExpireDate } from "../utils/functions/premium/premium"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default async function entitlementDelete(entitlement: Entitlement) { 6 | logger.info("received entitlement delete", entitlement.toJSON()); 7 | 8 | await setExpireDate(entitlement.userId, entitlement.endsAt); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./dist", 5 | "target": "ESNext", 6 | "module": "commonjs", 7 | "strictFunctionTypes": true, 8 | "skipLibCheck": true, 9 | "noImplicitAny": true, 10 | "paths": { 11 | "#generated/prisma": ["./src/generated/prisma/client"] 12 | } 13 | }, 14 | "exclude": ["node_modules", "dist", "prisma", "prisma.config.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /prisma/migrations/20250616183043_tmdb_nypsi_ratings/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "RatingType" AS ENUM ('tv', 'movie'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "tmdbRatings" ( 6 | "userId" TEXT NOT NULL, 7 | "type" "RatingType" NOT NULL, 8 | "id" INTEGER NOT NULL, 9 | "name" TEXT NOT NULL, 10 | "rating" DECIMAL(2,1) NOT NULL, 11 | 12 | CONSTRAINT "tmdbRatings_pkey" PRIMARY KEY ("userId","type","id") 13 | ); 14 | -------------------------------------------------------------------------------- /src/types/InteractionHandler.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteInteraction, MessageComponentInteraction } from "discord.js"; 2 | 3 | export type InteractionHandler = { 4 | name: string; 5 | type: "interaction"; 6 | run: (interaction: MessageComponentInteraction) => Promise; 7 | }; 8 | 9 | export type AutocompleteHandler = { 10 | name: string; 11 | type: "autocomplete"; 12 | run: (interaction: AutocompleteInteraction) => Promise; 13 | }; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20230105130132_useraliases/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "UserAlias" ( 3 | "userId" TEXT NOT NULL, 4 | "alias" TEXT NOT NULL, 5 | "command" TEXT NOT NULL, 6 | 7 | CONSTRAINT "UserAlias_pkey" PRIMARY KEY ("userId","alias") 8 | ); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "UserAlias" ADD CONSTRAINT "UserAlias_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Premium"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20241228130156_chatfilter_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ChatFilter" ( 3 | "guildId" TEXT NOT NULL, 4 | "content" TEXT NOT NULL, 5 | "percentMatch" INTEGER, 6 | 7 | CONSTRAINT "ChatFilter_pkey" PRIMARY KEY ("guildId","content") 8 | ); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "ChatFilter" ADD CONSTRAINT "ChatFilter_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240508154137_captcha/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Captcha" ( 3 | "id" TEXT NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "solved" BOOLEAN NOT NULL DEFAULT false, 6 | "received" INTEGER NOT NULL DEFAULT 0, 7 | "visits" TIMESTAMP(3)[], 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "solvedAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, 10 | 11 | CONSTRAINT "Captcha_pkey" PRIMARY KEY ("id") 12 | ); 13 | -------------------------------------------------------------------------------- /prisma/migrations/20240711120338_botmetrics_float/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "ChatReactionLeaderboards" DROP CONSTRAINT "ChatReactionLeaderboards_userId_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "BotMetrics" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION; 6 | 7 | -- AddForeignKey 8 | ALTER TABLE "ChatReactionLeaderboards" ADD CONSTRAINT "ChatReactionLeaderboards_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20241213121926_remove_images/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Image` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `ImageSuggestion` table. If the table is not empty, all the data it contains will be lost. 6 | 7 | */ 8 | -- DropTable 9 | DROP TABLE "Image"; 10 | 11 | -- DropTable 12 | DROP TABLE "ImageSuggestion"; 13 | 14 | -- DropEnum 15 | DROP TYPE "ImageType"; 16 | -------------------------------------------------------------------------------- /prisma/migrations/20230704122334_use_uuid_for_mention/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `Mention` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Mention" DROP CONSTRAINT "Mention_pkey", 9 | ALTER COLUMN "id" DROP DEFAULT, 10 | ALTER COLUMN "id" SET DATA TYPE TEXT, 11 | ADD CONSTRAINT "Mention_pkey" PRIMARY KEY ("id"); 12 | DROP SEQUENCE "Mention_id_seq"; 13 | -------------------------------------------------------------------------------- /src/scheduled/jobs/resetvote.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../init/database"; 2 | import { Job } from "../../types/Jobs"; 3 | 4 | export default { 5 | name: "reset vote", 6 | cron: "0 0 1 * *", 7 | async run(log) { 8 | const query = await prisma.economy.updateMany({ 9 | where: { 10 | monthVote: { gt: 0 }, 11 | }, 12 | data: { monthVote: 0 }, 13 | }); 14 | 15 | log(`${query.count} users reset to 0 monthly votes`); 16 | }, 17 | } satisfies Job; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20230328141729_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `GraphMetrics` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "GraphMetrics" DROP CONSTRAINT "GraphMetrics_pkey", 9 | ALTER COLUMN "id" DROP DEFAULT, 10 | ALTER COLUMN "id" SET DATA TYPE TEXT, 11 | ADD CONSTRAINT "GraphMetrics_pkey" PRIMARY KEY ("id"); 12 | DROP SEQUENCE "GraphMetrics_id_seq"; 13 | -------------------------------------------------------------------------------- /src/events/guildCreate.ts: -------------------------------------------------------------------------------- 1 | import { Client, Guild } from "discord.js"; 2 | import { createGuild, hasGuild, updateGuild } from "../utils/functions/guilds/utils"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default async function guildCreate(client: Client, guild: Guild) { 6 | logger.info(`::guild added to ${guild.name} (${guild.id})`); 7 | 8 | if (await hasGuild(guild)) { 9 | await updateGuild(guild); 10 | } else { 11 | await createGuild(guild); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /prisma/migrations/20230319134324_preferences/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Preferences" ( 3 | "userId" TEXT NOT NULL, 4 | "duelRequests" BOOLEAN NOT NULL DEFAULT true, 5 | "auctionConfirm" INTEGER NOT NULL DEFAULT 25000000, 6 | 7 | CONSTRAINT "Preferences_pkey" PRIMARY KEY ("userId") 8 | ); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Preferences" ADD CONSTRAINT "Preferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /src/types/Notification.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, MessageActionRowComponentBuilder } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | 4 | export interface NotificationPayload { 5 | memberId: string; 6 | payload: { 7 | embed?: CustomEmbed; 8 | components?: ActionRowBuilder; 9 | content?: string; 10 | }; 11 | } 12 | 13 | export interface InlineNotificationPayload { 14 | memberId: string; 15 | embed: CustomEmbed; 16 | } 17 | -------------------------------------------------------------------------------- /src/api/middleware/logger.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewareHandler } from "hono"; 2 | import { performance } from "perf_hooks"; 3 | import { logger } from "../../utils/logger"; 4 | 5 | const loggerMiddleware: MiddlewareHandler = async (c, next) => { 6 | const start = performance.now(); 7 | 8 | await next(); 9 | const duration = Math.round(performance.now() - start); 10 | 11 | logger.info(`api: ${c.req.method} ${c.req.path} - ${c.res.status} (${duration}ms)`); 12 | }; 13 | 14 | export default loggerMiddleware; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20230711151147_random_drops_activechannels/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ActiveChannels" ( 3 | "userId" TEXT NOT NULL, 4 | "channelId" TEXT NOT NULL, 5 | "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | 7 | CONSTRAINT "ActiveChannels_pkey" PRIMARY KEY ("userId","channelId") 8 | ); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "ActiveChannels" ADD CONSTRAINT "ActiveChannels_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230817213226_alts/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Guild" ADD COLUMN "alt_punish" BOOLEAN NOT NULL DEFAULT false; 3 | 4 | -- CreateTable 5 | CREATE TABLE "Alt" ( 6 | "guildId" TEXT NOT NULL, 7 | "mainId" TEXT NOT NULL, 8 | "altId" TEXT NOT NULL, 9 | 10 | CONSTRAINT "Alt_pkey" PRIMARY KEY ("altId","guildId") 11 | ); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Alt" ADD CONSTRAINT "Alt_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20240906122452_aura/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Aura" ( 3 | "id" SERIAL NOT NULL, 4 | "recipientId" TEXT NOT NULL, 5 | "senderId" TEXT NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "amount" INTEGER NOT NULL, 8 | 9 | CONSTRAINT "Aura_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateIndex 13 | CREATE INDEX "Aura_recipientId_idx" ON "Aura"("recipientId"); 14 | 15 | -- CreateIndex 16 | CREATE INDEX "Aura_senderId_idx" ON "Aura"("senderId"); 17 | -------------------------------------------------------------------------------- /prisma/migrations/20241209031848_farm_upgrades/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "FarmUpgrades" ( 3 | "userId" TEXT NOT NULL, 4 | "plantId" TEXT NOT NULL, 5 | "upgradeId" TEXT NOT NULL, 6 | "amount" INTEGER NOT NULL DEFAULT 1, 7 | 8 | CONSTRAINT "FarmUpgrades_pkey" PRIMARY KEY ("userId","plantId","upgradeId") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "FarmUpgrades" ADD CONSTRAINT "FarmUpgrades_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20250114163501_wordle_stats/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "WordleGame" ( 3 | "id" SERIAL NOT NULL, 4 | "guesses" TEXT[], 5 | "word" TEXT NOT NULL, 6 | "won" BOOLEAN NOT NULL, 7 | "time" INTEGER NOT NULL, 8 | "userId" TEXT NOT NULL, 9 | 10 | CONSTRAINT "WordleGame_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "WordleGame" ADD CONSTRAINT "WordleGame_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20250824153622_store_guild_members/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "public"."GuildMember" ( 3 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 4 | "guildId" TEXT NOT NULL, 5 | "userId" TEXT NOT NULL, 6 | 7 | CONSTRAINT "GuildMember_pkey" PRIMARY KEY ("guildId","userId") 8 | ); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "public"."GuildMember" ADD CONSTRAINT "GuildMember_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "public"."Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20230720152701_purchase_history/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "KofiPurchases" ADD COLUMN "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 3 | ADD COLUMN "userId" TEXT, 4 | ALTER COLUMN "email" DROP NOT NULL; 5 | 6 | -- AlterTable 7 | ALTER TABLE "User" ADD COLUMN "totalSpend" DOUBLE PRECISION NOT NULL DEFAULT 0; 8 | 9 | -- AddForeignKey 10 | ALTER TABLE "KofiPurchases" ADD CONSTRAINT "KofiPurchases_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 11 | -------------------------------------------------------------------------------- /src/commands/support.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import Constants from "../utils/Constants"; 4 | 5 | const cmd = new Command("support", "join the nypsi support server", "info").setAliases(["discord"]); 6 | 7 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 8 | return message.channel.send({ content: Constants.NYPSI_SERVER_INVITE_LINK }); 9 | } 10 | 11 | cmd.setRun(run); 12 | 13 | module.exports = cmd; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20240214161917_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Task" DROP CONSTRAINT "Task_user_id_fkey"; 3 | 4 | -- DropIndex 5 | DROP INDEX "Inventory_userId_item_key"; 6 | 7 | -- AlterTable 8 | ALTER TABLE "Inventory" ADD CONSTRAINT "Inventory_pkey" PRIMARY KEY ("userId", "item"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "Achievements_userId_idx" ON "Achievements"("userId"); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Task" ADD CONSTRAINT "Task_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20250508183538_support_request_messages/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "SupportRequestMessage" ( 3 | "id" TEXT NOT NULL, 4 | "supportRequestId" TEXT NOT NULL, 5 | "userId" TEXT NOT NULL, 6 | "content" TEXT NOT NULL, 7 | 8 | CONSTRAINT "SupportRequestMessage_pkey" PRIMARY KEY ("id") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "SupportRequestMessage" ADD CONSTRAINT "SupportRequestMessage_supportRequestId_fkey" FOREIGN KEY ("supportRequestId") REFERENCES "SupportRequest"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 13 | -------------------------------------------------------------------------------- /prisma/migrations/20250825121611_username_last_updated/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `lastKnownTag` on the `User` table. All the data in the column will be lost. 5 | - Added the required column `lastKnownUsername` to the `User` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "public"."User" RENAME COLUMN "lastKnownTag" TO "lastKnownUsername"; 10 | ALTER TABLE "public"."User" ADD COLUMN "usernameUpdatedAt" TIMESTAMP(3) NOT NULL DEFAULT '1970-01-01 14:21:00 +02:00'; 11 | -------------------------------------------------------------------------------- /prisma/migrations/20240626174321_cr_leaderboard/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ChatReactionLeaderboards" ( 3 | "userId" TEXT NOT NULL, 4 | "daily" BOOLEAN NOT NULL, 5 | "time" DOUBLE PRECISION NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | 8 | CONSTRAINT "ChatReactionLeaderboards_pkey" PRIMARY KEY ("daily","userId") 9 | ); 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "ChatReactionLeaderboards" ADD CONSTRAINT "ChatReactionLeaderboards_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 13 | -------------------------------------------------------------------------------- /src/events/emojiDelete.ts: -------------------------------------------------------------------------------- 1 | import { GuildEmoji } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { addLog, isLogsEnabled } from "../utils/functions/moderation/logs"; 4 | 5 | export default async function emojiDelete(emoji: GuildEmoji) { 6 | if (await isLogsEnabled(emoji.guild)) { 7 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 8 | 9 | embed.setHeader("emoji deleted"); 10 | embed.setDescription(`\`${emoji.name}\` - \`${emoji.id}\``); 11 | embed.setImage(emoji.url); 12 | 13 | addLog(emoji.guild, "emoji", embed); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/functions/economy/items/calendar.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiCommandInteraction, NypsiMessage } from "../../../../models/Command"; 3 | import { CustomEmbed } from "../../../../models/EmbedBuilders"; 4 | import { ItemUse } from "../../../../models/ItemUse"; 5 | 6 | module.exports = new ItemUse( 7 | "calendar", 8 | async (message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) => { 9 | return ItemUse.send(message, { 10 | embeds: [new CustomEmbed(message.member, "calendars will be used automatically")], 11 | }); 12 | }, 13 | ); 14 | -------------------------------------------------------------------------------- /src/scheduled/jobs/dailycr.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../init/database"; 2 | import { Job } from "../../types/Jobs"; 3 | 4 | export default { 5 | name: "daily chat reaction purge", 6 | cron: "0 0 * * *", 7 | async run(log) { 8 | const query = await prisma.chatReactionLeaderboards.deleteMany({ 9 | where: { 10 | daily: true, 11 | }, 12 | }); 13 | 14 | await prisma.leaderboards.deleteMany({ 15 | where: { 16 | leaderboard: "chatreaction_daily", 17 | }, 18 | }); 19 | 20 | log(`${query.count} daily cr leaderboards deleted`); 21 | }, 22 | } satisfies Job; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20250318191631_guess_the_flag_stats/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "FlagGame" ( 3 | "id" SERIAL NOT NULL, 4 | "guesses" TEXT[], 5 | "country" TEXT NOT NULL, 6 | "won" BOOLEAN NOT NULL, 7 | "time" INTEGER, 8 | "userId" TEXT NOT NULL, 9 | 10 | CONSTRAINT "FlagGame_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "FlagGame_userId_idx" ON "FlagGame"("userId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "FlagGame" ADD CONSTRAINT "FlagGame_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # github actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | labels: 9 | - "dependencies" 10 | 11 | # node packages 12 | - package-ecosystem: "npm" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | labels: 17 | - "dependencies" 18 | groups: 19 | prisma: 20 | patterns: 21 | - "prisma" 22 | - "@prisma/client" 23 | - "@prisma/adapter-pg" 24 | patch: 25 | update-types: 26 | - "patch" 27 | -------------------------------------------------------------------------------- /prisma/migrations/20241216123924_purchases_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Purchases" ( 3 | "id" SERIAL NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "source" TEXT NOT NULL, 6 | "email" TEXT, 7 | "item" TEXT NOT NULL, 8 | "amount" INTEGER, 9 | "cost" DECIMAL(65,30) NOT NULL, 10 | "userId" TEXT, 11 | 12 | CONSTRAINT "Purchases_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "Purchases" ADD CONSTRAINT "Purchases_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20250601105124_improve_some_indexes/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "Crafting_userId_idx" ON "Crafting"("userId"); 3 | 4 | -- CreateIndex 5 | CREATE INDEX "CustomCar_userId_idx" ON "CustomCar"("userId"); 6 | 7 | -- CreateIndex 8 | CREATE INDEX "PremiumCommand_trigger_idx" ON "PremiumCommand"("trigger"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "ReactionRole_messageId_idx" ON "ReactionRole"("messageId"); 12 | 13 | -- CreateIndex 14 | CREATE INDEX "TradeRequest_ownerId_idx" ON "TradeRequest"("ownerId"); 15 | 16 | -- CreateIndex 17 | CREATE INDEX "Username_userId_idx" ON "Username"("userId"); 18 | -------------------------------------------------------------------------------- /prisma/migrations/20250525151118_remove_auctions/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Auction` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `AuctionWatch` table. If the table is not empty, all the data it contains will be lost. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "Auction" DROP CONSTRAINT "Auction_ownerId_fkey"; 10 | 11 | -- DropForeignKey 12 | ALTER TABLE "AuctionWatch" DROP CONSTRAINT "AuctionWatch_userId_fkey"; 13 | 14 | -- DropTable 15 | DROP TABLE "Auction"; 16 | 17 | -- DropTable 18 | DROP TABLE "AuctionWatch"; 19 | -------------------------------------------------------------------------------- /prisma/migrations/20240618131236_moderation_restrcture_7_idsindex/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `ModerationCase` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - You are about to drop the column `tempId` on the `ModerationCase` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "ModerationCase" DROP COLUMN "tempId", 10 | ADD CONSTRAINT "ModerationCase_pkey" PRIMARY KEY ("caseId", "guildId"); 11 | 12 | -- CreateIndex 13 | CREATE INDEX "ModerationCase_guildId_idx" ON "ModerationCase"("guildId"); 14 | -------------------------------------------------------------------------------- /prisma/migrations/20240712170920_farms/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Farm" ( 3 | "id" SERIAL NOT NULL, 4 | "userId" TEXT NOT NULL, 5 | "plantId" TEXT NOT NULL, 6 | "plantedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "harvestedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | CONSTRAINT "Farm_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateIndex 13 | CREATE INDEX "Farm_userId_idx" ON "Farm"("userId"); 14 | 15 | -- AddForeignKey 16 | ALTER TABLE "Farm" ADD CONSTRAINT "Farm_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 17 | -------------------------------------------------------------------------------- /src/commands/logs.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | 5 | const cmd = new Command("logs", "moved to /settings server logs", "admin").setPermissions([ 6 | "MANAGE_SERVER", 7 | ]); 8 | 9 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 10 | return message.channel.send({ 11 | embeds: [new CustomEmbed(message.member, "moved to /settings server logs")], 12 | }); 13 | } 14 | 15 | cmd.setRun(run); 16 | 17 | module.exports = cmd; 18 | -------------------------------------------------------------------------------- /src/commands/prefix.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | 5 | const cmd = new Command("prefix", "moved to /settings server prefix", "admin").setPermissions([ 6 | "MANAGE_GUILD", 7 | ]); 8 | 9 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 10 | return message.channel.send({ 11 | embeds: [new CustomEmbed(message.member, "moved to /settings server prefix")], 12 | }); 13 | } 14 | 15 | cmd.setRun(run); 16 | 17 | module.exports = cmd; 18 | -------------------------------------------------------------------------------- /src/commands/modlogs.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | 5 | const cmd = new Command("modlogs", "moved to /settings server modlogs", "admin").setPermissions([ 6 | "MANAGE_SERVER", 7 | ]); 8 | 9 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 10 | return message.channel.send({ 11 | embeds: [new CustomEmbed(message.member, "moved to /settings server modlogs")], 12 | }); 13 | } 14 | 15 | cmd.setRun(run); 16 | 17 | module.exports = cmd; 18 | -------------------------------------------------------------------------------- /src/commands/servericon.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | 5 | const cmd = new Command("servericon", "get the server icon", "info"); 6 | 7 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 8 | return message.channel.send({ 9 | embeds: [ 10 | new CustomEmbed(message.member).setImage( 11 | message.guild.iconURL({ 12 | size: 256, 13 | }), 14 | ), 15 | ], 16 | }); 17 | } 18 | 19 | cmd.setRun(run); 20 | 21 | module.exports = cmd; 22 | -------------------------------------------------------------------------------- /src/events/guildDelete.ts: -------------------------------------------------------------------------------- 1 | import { Client, Guild } from "discord.js"; 2 | import { updateDisabledCommands } from "../utils/functions/guilds/disabledcommands"; 3 | import { setPrefix } from "../utils/functions/guilds/utils"; 4 | import { setMuteRole } from "../utils/functions/moderation/mute"; 5 | 6 | import { logger } from "../utils/logger"; 7 | 8 | export default async function guildDelete(client: Client, guild: Guild) { 9 | if (!guild.name) { 10 | return; 11 | } 12 | 13 | logger.info(`::guild removed from ${guild.name} (${guild.id})`); 14 | 15 | await setPrefix(guild, ["$"]); 16 | await updateDisabledCommands(guild, []); 17 | 18 | await setMuteRole(guild, ""); 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # contributing guidelines 2 | 3 | thank you for deciding to help contribute towards nypsi!! 4 | 5 | ## commit format 6 | 7 | please make sure to follow the [semantic commit messages guide](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716). its okay if you havent followed it perfectly, but please make sure to try and have at least meaningful commit messages. it helps everyone (: 8 | 9 | ## before you commit 10 | 11 | before you commit, husky will lint your code and format your code with prettier. 12 | 13 | linting helps keep your code correct, making sure you dont contribute any errors 14 | 15 | prettier formatting helps keep the code nicely formatted 16 | -------------------------------------------------------------------------------- /prisma/migrations/20240618125839_moderation_restructure_5_temp_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `ModerationCase` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - The required column `tempId` was added to the `ModerationCase` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "ModerationCase" DROP CONSTRAINT "ModerationCase_pkey", 10 | ADD COLUMN "tempId" TEXT; 11 | -- ADD CONSTRAINT "ModerationCase_pkey" PRIMARY KEY ("tempId"); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20250618034837_marriage/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Marriage" ( 3 | "userId" TEXT NOT NULL, 4 | "partnerId" TEXT NOT NULL, 5 | "marriageStart" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | 7 | CONSTRAINT "Marriage_pkey" PRIMARY KEY ("userId","partnerId") 8 | ); 9 | 10 | -- CreateIndex 11 | CREATE UNIQUE INDEX "Marriage_userId_key" ON "Marriage"("userId"); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "Marriage_partnerId_key" ON "Marriage"("partnerId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Marriage" ADD CONSTRAINT "Marriage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /src/utils/functions/chatreactions/blacklisted.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from "discord.js"; 2 | import prisma from "../../../init/database"; 3 | 4 | export async function getBlacklisted(guild: Guild) { 5 | const query = await prisma.chatReaction.findUnique({ 6 | where: { 7 | guildId: guild.id, 8 | }, 9 | select: { 10 | blacklisted: true, 11 | }, 12 | }); 13 | 14 | return query.blacklisted; 15 | } 16 | 17 | export async function setBlacklisted(guild: Guild, blacklisted: string[]) { 18 | await prisma.chatReaction.update({ 19 | where: { 20 | guildId: guild.id, 21 | }, 22 | data: { 23 | blacklisted: blacklisted, 24 | }, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /prisma/migrations/20230416195103_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Offer" DROP CONSTRAINT "Offer_ownerId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "Offer" DROP CONSTRAINT "Offer_targetId_fkey"; 6 | 7 | -- AlterTable 8 | ALTER TABLE "Offer" ALTER COLUMN "ownerId" DROP NOT NULL, 9 | ALTER COLUMN "targetId" DROP NOT NULL; 10 | 11 | -- AddForeignKey 12 | ALTER TABLE "Offer" ADD CONSTRAINT "Offer_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "Economy"("userId") ON DELETE SET NULL ON UPDATE CASCADE; 13 | 14 | -- AddForeignKey 15 | ALTER TABLE "Offer" ADD CONSTRAINT "Offer_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "Economy"("userId") ON DELETE SET NULL ON UPDATE CASCADE; 16 | -------------------------------------------------------------------------------- /src/events/emojiUpdate.ts: -------------------------------------------------------------------------------- 1 | import { GuildEmoji } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { addLog, isLogsEnabled } from "../utils/functions/moderation/logs"; 4 | 5 | export default async function emojiUpdate(oldEmoji: GuildEmoji, newEmoji: GuildEmoji) { 6 | if (oldEmoji.name != newEmoji.name && (await isLogsEnabled(newEmoji.guild))) { 7 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 8 | 9 | embed.setHeader("emoji updated"); 10 | embed.setDescription( 11 | `\`${newEmoji.name}\` - \`${newEmoji.id}\`\n${oldEmoji.name} -> ${newEmoji.name}`, 12 | ); 13 | embed.setImage(newEmoji.url); 14 | 15 | addLog(newEmoji.guild, "emoji", embed); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/getcsvdata.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import Constants from "../utils/Constants"; 4 | import graphToCsv from "../utils/functions/workers/graphtocsv"; 5 | 6 | const cmd = new Command("getcsvdata", "get csv data", "none").setPermissions(["bot owner"]); 7 | 8 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 9 | if (message.author.id !== Constants.OWNER_ID) return; 10 | 11 | const msg = await message.channel.send({ content: "processing..." }); 12 | 13 | await graphToCsv(); 14 | 15 | msg.edit({ content: "done (/temp)" }); 16 | } 17 | 18 | cmd.setRun(run); 19 | 20 | module.exports = cmd; 21 | -------------------------------------------------------------------------------- /src/types/StreetRace.ts: -------------------------------------------------------------------------------- 1 | import { Message, TextChannel, User } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { Car } from "../utils/functions/economy/cars"; 4 | import { Item } from "./Economy"; 5 | 6 | export interface RaceDetails { 7 | channel: TextChannel; 8 | users: RaceUserDetails[]; 9 | bet: number; 10 | message: Message; 11 | embed: CustomEmbed; 12 | started: boolean; 13 | speedLimit: number; 14 | length: number; 15 | } 16 | 17 | export interface RaceUserDetails { 18 | user: User; 19 | car: RaceUserCar | RaceUserItem; 20 | position: number; 21 | } 22 | 23 | type RaceUserCar = { 24 | type: "car"; 25 | car: Car; 26 | }; 27 | 28 | type RaceUserItem = { 29 | type: "item"; 30 | car: Item; 31 | }; 32 | -------------------------------------------------------------------------------- /src/commands/rules.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders.js"; 4 | 5 | const cmd = new Command("rules", "view nypsi bot and server rules", "info"); 6 | 7 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 8 | return message.channel.send({ 9 | embeds: [ 10 | new CustomEmbed( 11 | message.member, 12 | "rules for the **bot** and the [official nypsi discord server](https://nypsi.xyz/discord) can be found below\n\nhttps://nypsi.xyz/rules?ref=bot-rules", 13 | ), 14 | ], 15 | }); 16 | } 17 | 18 | cmd.setRun(run); 19 | 20 | module.exports = cmd; 21 | -------------------------------------------------------------------------------- /src/events/channelDelete.ts: -------------------------------------------------------------------------------- 1 | import { GuildChannel } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { addLog, isLogsEnabled } from "../utils/functions/moderation/logs"; 4 | 5 | export default async function channelDelete(channel: GuildChannel) { 6 | if (!channel.guild) return; 7 | 8 | if (await isLogsEnabled(channel.guild)) { 9 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 10 | 11 | embed.setTitle("channel deleted"); 12 | embed.setDescription( 13 | `${channel.toString()} \`${channel.id}\`\n\n**name** ${channel.name}\n**category** ${ 14 | channel.parent.name 15 | }\n**type** ${channel.type}`, 16 | ); 17 | 18 | await addLog(channel.guild, "channel", embed); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/functions/color.ts: -------------------------------------------------------------------------------- 1 | import { flavors } from "@catppuccin/palette"; 2 | import { ColorResolvable, GuildMember } from "discord.js"; 3 | 4 | const colors: string[] = [ 5 | flavors.mocha.colors.flamingo.hex, 6 | flavors.mocha.colors.mauve.hex, 7 | flavors.mocha.colors.maroon.hex, 8 | flavors.mocha.colors.peach.hex, 9 | flavors.mocha.colors.yellow.hex, 10 | flavors.mocha.colors.green.hex, 11 | flavors.mocha.colors.sky.hex, 12 | flavors.mocha.colors.lavender.hex, 13 | ]; 14 | 15 | export function getColor(member: GuildMember) { 16 | if (member.displayHexColor == "#ffffff" || member.displayHexColor == "#000000") { 17 | return colors[Math.floor(Math.random() * colors.length)] as ColorResolvable; 18 | } else { 19 | return member.displayHexColor; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /prisma/migrations/20250419194232_item_trades/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "TradeRequest" ( 3 | "id" SERIAL NOT NULL, 4 | "ownerId" TEXT NOT NULL, 5 | "requestedItems" TEXT[], 6 | "offeredItems" TEXT[], 7 | "offeredMoney" BIGINT NOT NULL, 8 | "messageId" TEXT NOT NULL, 9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "completed" BOOLEAN NOT NULL DEFAULT false, 11 | 12 | CONSTRAINT "TradeRequest_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "TradeRequest_messageId_key" ON "TradeRequest"("messageId"); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "TradeRequest" ADD CONSTRAINT "TradeRequest_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 20 | -------------------------------------------------------------------------------- /src/events/emojiCreate.ts: -------------------------------------------------------------------------------- 1 | import { GuildEmoji } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { addLog, isLogsEnabled } from "../utils/functions/moderation/logs"; 4 | 5 | export default async function emojiCreate(emoji: GuildEmoji) { 6 | if (await isLogsEnabled(emoji.guild)) { 7 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 8 | 9 | const creator = 10 | (await emoji 11 | .fetchAuthor() 12 | .then((res) => res.id) 13 | .catch(() => {})) || "unknown"; 14 | 15 | embed.setHeader("emoji created"); 16 | embed.setDescription(`\`${emoji.name}\` - \`${emoji.id}\`\ncreated by \`${creator}\``); 17 | embed.setImage(emoji.imageURL()); 18 | 19 | addLog(emoji.guild, "emoji", embed); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/events/entitlementCreate.ts: -------------------------------------------------------------------------------- 1 | import { Entitlement } from "discord.js"; 2 | import { addMember, getTier, renewUser, setTier } from "../utils/functions/premium/premium"; 3 | import { createProfile, hasProfile } from "../utils/functions/users/utils"; 4 | import { logger } from "../utils/logger"; 5 | 6 | export default async function entitlementCreate(entitlement: Entitlement) { 7 | logger.info("received entitlement create", entitlement.toJSON()); 8 | 9 | if (!(await hasProfile(entitlement.userId))) await createProfile(entitlement.userId); 10 | 11 | const tier = await getTier(entitlement.userId); 12 | 13 | if (tier < 4) { 14 | await setTier(entitlement.userId, 4); 15 | await renewUser(entitlement.userId); 16 | } else { 17 | await addMember(entitlement.userId, 4); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/auction.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders.js"; 4 | 5 | const cmd = new Command("auction", "moved to /market", "money").setAliases(["ah"]); 6 | 7 | cmd.slashEnabled = true; 8 | 9 | async function run( 10 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 11 | send: SendMessage, 12 | ) { 13 | return send({ 14 | embeds: [ 15 | new CustomEmbed( 16 | message.member, 17 | "auctions have been moved to be part of /market\nmore info: https://nypsi.xyz/docs/economy/market", 18 | ), 19 | ], 20 | }); 21 | } 22 | 23 | cmd.setRun(run); 24 | 25 | module.exports = cmd; 26 | -------------------------------------------------------------------------------- /src/utils/functions/economy/items/teddy.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiCommandInteraction, NypsiMessage } from "../../../../models/Command"; 3 | import { CustomEmbed } from "../../../../models/EmbedBuilders"; 4 | import { ItemUse } from "../../../../models/ItemUse"; 5 | import { addStat } from "../stats"; 6 | 7 | module.exports = new ItemUse( 8 | "teddy", 9 | async (message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) => { 10 | await addStat(message.member, "teddy"); 11 | 12 | return ItemUse.send(message, { 13 | embeds: [ 14 | new CustomEmbed(message.member, "you cuddle your teddy bear").setImage( 15 | "https://c.tenor.com/QGoHlSF2cSAAAAAM/hug-milk-and-mocha.gif", 16 | ), 17 | ], 18 | }); 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/commands/season.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders.js"; 4 | import Constants from "../utils/Constants"; 5 | 6 | const cmd = new Command("season", "view current season", "money"); 7 | 8 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 9 | return message.channel.send({ 10 | embeds: [ 11 | new CustomEmbed( 12 | message.member, 13 | `currently on season ${Constants.SEASON_NUMBER}\n\nstarted on `, 16 | ), 17 | ], 18 | }); 19 | } 20 | 21 | cmd.setRun(run); 22 | 23 | module.exports = cmd; 24 | -------------------------------------------------------------------------------- /src/utils/functions/economy/items/lottery_ticket.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiCommandInteraction, NypsiMessage } from "../../../../models/Command"; 3 | import { CustomEmbed } from "../../../../models/EmbedBuilders"; 4 | import { ItemUse } from "../../../../models/ItemUse"; 5 | import { getPrefix } from "../../guilds/utils"; 6 | 7 | module.exports = new ItemUse( 8 | "lottery_ticket", 9 | async (message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) => { 10 | return ItemUse.send(message, { 11 | embeds: [ 12 | new CustomEmbed( 13 | message.member, 14 | `lottery tickets will automatically be used. ${(await getPrefix(message.guild))[0]}**lotto** for more information`, 15 | ), 16 | ], 17 | }); 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/scheduled/jobs/counters.ts: -------------------------------------------------------------------------------- 1 | import pAll = require("p-all"); 2 | import prisma from "../../init/database"; 3 | import { Job } from "../../types/Jobs"; 4 | import { updateChannel } from "../../utils/functions/guilds/counters"; 5 | 6 | export default { 7 | name: "counters", 8 | cron: "*/10 * * * *", 9 | async run(log, manager) { 10 | const counters = await prisma.guildCounter.findMany(); 11 | 12 | const functions = []; 13 | 14 | for (const counter of counters) { 15 | if (!counter.format.includes("%value%")) 16 | await prisma.guildCounter.delete({ where: { channel: counter.channel } }); 17 | functions.push(async () => { 18 | await updateChannel(counter, manager); 19 | }); 20 | } 21 | 22 | await pAll(functions, { concurrency: 5 }); 23 | }, 24 | } satisfies Job; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20230909192255_levelling/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "LevelDmSetting" AS ENUM ('Disabled', 'All', 'OnlyReward'); 3 | 4 | -- AlterTable 5 | ALTER TABLE "DMSettings" ADD COLUMN "level" "LevelDmSetting" NOT NULL DEFAULT 'OnlyReward'; 6 | 7 | -- AlterTable 8 | ALTER TABLE "Economy" ADD COLUMN "level" INTEGER NOT NULL DEFAULT 0, 9 | ALTER COLUMN "xp" SET DATA TYPE BIGINT; 10 | 11 | -- CreateTable 12 | CREATE TABLE "Upgrades" ( 13 | "userId" TEXT NOT NULL, 14 | "upgradeId" TEXT NOT NULL, 15 | "amount" INTEGER NOT NULL, 16 | 17 | CONSTRAINT "Upgrades_pkey" PRIMARY KEY ("userId","upgradeId") 18 | ); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "Upgrades" ADD CONSTRAINT "Upgrades_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 22 | -------------------------------------------------------------------------------- /src/utils/functions/outliers.ts: -------------------------------------------------------------------------------- 1 | export function filterOutliers(array: number[]) { 2 | if (array.length < 8) return array; 3 | 4 | let q1: number; 5 | let q3: number; 6 | 7 | const values = array.slice().sort((a, b) => a - b); //copy array fast and sort 8 | 9 | if ((values.length / 4) % 1 === 0) { 10 | //find quartiles 11 | q1 = (1 / 2) * (values[values.length / 4] + values[values.length / 4 + 1]); 12 | q3 = (1 / 2) * (values[values.length * (3 / 4)] + values[values.length * (3 / 4) + 1]); 13 | } else { 14 | q1 = values[Math.floor(values.length / 4 + 1)]; 15 | q3 = values[Math.ceil(values.length * (3 / 4) + 1)]; 16 | } 17 | 18 | const iqr = q3 - q1; 19 | const maxValue = q3 + iqr * 1.5; 20 | const minValue = q1 - iqr * 1.5; 21 | 22 | return values.filter((x) => x >= minValue && x <= maxValue); 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parserOptions": { 13 | "ecmaVersion": 2020, 14 | "sourceType": "module", 15 | "project": "./tsconfig.json" // <-- Point to your project's tsconfig.json or create new one 16 | }, 17 | "plugins": ["@typescript-eslint", "deprecation"], 18 | "rules": { 19 | "no-unreachable": "error", 20 | "no-case-declarations": "off", 21 | "@typescript-eslint/no-explicit-any": "off", 22 | "@typescript-eslint/no-empty-function": "off", 23 | "@typescript-eslint/ban-ts-comment": "warn", 24 | "@typescript-eslint/no-require-imports": "off", 25 | "deprecation/deprecation": "warn" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /prisma/migrations/20240318163724_session_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "CommandUse_userId_command_key"; 3 | 4 | -- DropIndex 5 | DROP INDEX "Leaderboards_userId_idx"; 6 | 7 | -- AlterTable 8 | ALTER TABLE "CommandUse" ADD CONSTRAINT "CommandUse_pkey" PRIMARY KEY ("userId", "command"); 9 | 10 | -- AlterTable 11 | ALTER TABLE "EconomyGuild" ALTER COLUMN "motd" SET DEFAULT '/guild motd'; 12 | 13 | -- CreateTable 14 | CREATE TABLE "Session" ( 15 | "id" TEXT NOT NULL, 16 | "userId" TEXT NOT NULL, 17 | "expiresAt" TIMESTAMP(3) NOT NULL, 18 | 19 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateIndex 23 | CREATE INDEX "CommandUse_userId_idx" ON "CommandUse"("userId"); 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 27 | -------------------------------------------------------------------------------- /src/commands/invite.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders.js"; 4 | import Constants from "../utils/Constants"; 5 | 6 | const cmd = new Command("invite", "generate an invite link for the bot", "info").setAliases([ 7 | "bot", 8 | ]); 9 | 10 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 11 | const embed = new CustomEmbed( 12 | message.member, 13 | `bot invite: [nypsi.xyz/invite](https://nypsi.xyz/invite)\nsupport server: ${Constants.NYPSI_SERVER_INVITE_LINK}`, 14 | ) 15 | .setHeader("nypsi") 16 | .setFooter({ text: "made by @m.axz | github.com/mxz7" }); 17 | 18 | message.channel.send({ embeds: [embed] }); 19 | } 20 | 21 | cmd.setRun(run); 22 | 23 | module.exports = cmd; 24 | -------------------------------------------------------------------------------- /src/types/LootPool.ts: -------------------------------------------------------------------------------- 1 | export type LootPool = { 2 | nothing?: number; // weight 3 | money?: { 4 | [amount: number]: number; // amount: weight 5 | }; 6 | xp?: { 7 | [amount: number]: number; // amount: weight 8 | }; 9 | karma?: { 10 | [amount: number]: number; // amount: weight 11 | }; 12 | items?: { 13 | [item: string]: LootPoolItemEntry; 14 | }; 15 | }; 16 | 17 | export type LootPoolItemEntry = 18 | | { 19 | weight?: number; 20 | count?: 21 | | { 22 | min: number; 23 | max: number; 24 | } 25 | | number; 26 | } 27 | | number; // item: weight, count assumed to be 1 28 | 29 | export type LootPoolResult = { 30 | // describes ONE loot pool drop 31 | money?: number; 32 | xp?: number; 33 | karma?: number; 34 | item?: string; 35 | count?: number; // must be present if item is present 36 | }; 37 | -------------------------------------------------------------------------------- /prisma/migrations/20241228150110_remove_unused_data/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `chatFilter` on the `Guild` table. All the data in the column will be lost. 5 | - You are about to drop the column `percentMatch` on the `Guild` table. All the data in the column will be lost. 6 | - You are about to drop the column `totalSpend` on the `User` table. All the data in the column will be lost. 7 | - You are about to drop the `KofiPurchases` table. If the table is not empty, all the data it contains will be lost. 8 | 9 | */ 10 | -- DropForeignKey 11 | ALTER TABLE "KofiPurchases" DROP CONSTRAINT "KofiPurchases_userId_fkey"; 12 | 13 | -- AlterTable 14 | ALTER TABLE "Guild" DROP COLUMN "chatFilter", 15 | DROP COLUMN "percentMatch"; 16 | 17 | -- AlterTable 18 | ALTER TABLE "User" DROP COLUMN "totalSpend"; 19 | 20 | -- DropTable 21 | DROP TABLE "KofiPurchases"; 22 | -------------------------------------------------------------------------------- /.bruno/kofi.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: kofi 3 | type: http 4 | seq: 5 5 | } 6 | 7 | post { 8 | url: {{api}}/kofi 9 | body: formUrlEncoded 10 | auth: inherit 11 | } 12 | 13 | body:form-urlencoded { 14 | data: ''' 15 | { 16 | "verification_token": "boobies", 17 | "message_id": "aaaa", 18 | "timestamp": "2025-06-21T18:19:37Z", 19 | "type": "Donation", 20 | "is_public": true, 21 | "from_name": "Jo Example", 22 | "message": "Good luck with the integration!", 23 | "amount": "3.00", 24 | "url": "aaaa", 25 | "email": "jo.example@example.com", 26 | "currency": "USD", 27 | "is_subscription_payment": false, 28 | "is_first_subscription_payment": false, 29 | "kofi_transaction_id": "00000000-1111-2222-3333-444444444444", 30 | "shop_items": null, 31 | "tier_name": null, 32 | "shipping": null 33 | } 34 | ''' 35 | } 36 | -------------------------------------------------------------------------------- /data/bakery_upgrades.json: -------------------------------------------------------------------------------- 1 | { 2 | "grandma": { 3 | "id": "grandma", 4 | "name": "grandma", 5 | "emoji": "👵🏻", 6 | "upgrades": "hourly", 7 | "value": 0.5 8 | }, 9 | "cursor": { 10 | "id": "cursor", 11 | "name": "cursor", 12 | "emoji": "<:nypsi_cursor:1054771565557981244>", 13 | "upgrades": "bake", 14 | "value": 1 15 | }, 16 | "super_cursor": { 17 | "id": "super_cursor", 18 | "name": "super cursor", 19 | "emoji": "<:super_cursor:1090263183278739517>", 20 | "upgrades": "bake", 21 | "value": 1.5 22 | }, 23 | "grandpa": { 24 | "id": "grandpa", 25 | "name": "grandpa", 26 | "emoji": "👴🏻", 27 | "upgrades": "cake", 28 | "value": 0.5, 29 | "max": 50 30 | }, 31 | "oven": { 32 | "id": "oven", 33 | "name": "oven", 34 | "emoji": "<:nypsi_oven:1099724311138414743>", 35 | "upgrades": "maxafk", 36 | "value": 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /prisma/migrations/20250707074050_transactions/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "TransactionType" AS ENUM ('money', 'item'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Transaction" ( 6 | "id" SERIAL NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "sourceId" TEXT NOT NULL, 9 | "targetId" TEXT NOT NULL, 10 | "type" "TransactionType" NOT NULL, 11 | "itemId" TEXT, 12 | "amount" BIGINT NOT NULL, 13 | "notes" TEXT, 14 | 15 | CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- AddForeignKey 19 | ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /src/commands/maxbet.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | import { calcMaxBet } from "../utils/functions/economy/balance"; 5 | import { createUser, userExists } from "../utils/functions/economy/utils"; 6 | 7 | const cmd = new Command("maxbet", "calculate your maximum bet", "money"); 8 | 9 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 10 | if (!(await userExists(message.member))) await createUser(message.member); 11 | 12 | const maxBet = await calcMaxBet(message.member); 13 | 14 | return message.channel.send({ 15 | embeds: [ 16 | new CustomEmbed(message.member, `your maximum bet is $**${maxBet.toLocaleString()}**`), 17 | ], 18 | }); 19 | } 20 | 21 | cmd.setRun(run); 22 | 23 | module.exports = cmd; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20230614202711_website_data/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "EconomyGuild" ALTER COLUMN "xp" SET DATA TYPE BIGINT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "badges" TEXT[]; 6 | 7 | -- CreateTable 8 | CREATE TABLE "Leaderboards" ( 9 | "userId" TEXT NOT NULL, 10 | "leaderboard" TEXT NOT NULL, 11 | "position" INTEGER NOT NULL 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE INDEX "Leaderboards_userId_idx" ON "Leaderboards"("userId"); 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "Leaderboards_leaderboard_position_key" ON "Leaderboards"("leaderboard", "position"); 19 | 20 | -- CreateIndex 21 | CREATE UNIQUE INDEX "Leaderboards_userId_leaderboard_key" ON "Leaderboards"("userId", "leaderboard"); 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "Leaderboards" ADD CONSTRAINT "Leaderboards_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /src/commands/ecohistory.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders"; 4 | import { isPremium } from "../utils/functions/premium/premium"; 5 | 6 | const cmd = new Command( 7 | "ecohistory", 8 | "view your metric data history in a graph", 9 | "money", 10 | ).setAliases(["graph"]); 11 | 12 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 13 | if (!(await isPremium(message.member))) { 14 | return message.channel.send({ 15 | embeds: [new ErrorEmbed("this command requires premium membership. /premium")], 16 | }); 17 | } 18 | 19 | return message.channel.send({ 20 | embeds: [new CustomEmbed(message.member, "moved to https://nypsi.xyz/me?ref=bot-ecohistory")], 21 | }); 22 | } 23 | 24 | cmd.setRun(run); 25 | 26 | module.exports = cmd; 27 | -------------------------------------------------------------------------------- /prisma/migrations/20231208142222_profile_views/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "ProfileViewSource" AS ENUM ('WEB', 'BOT'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "ProfileView" ( 6 | "id" TEXT NOT NULL, 7 | "userId" TEXT NOT NULL, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "source" "ProfileViewSource" NOT NULL, 10 | "viewerId" TEXT, 11 | "viewerIp" TEXT, 12 | "referrer" TEXT, 13 | 14 | CONSTRAINT "ProfileView_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateIndex 18 | CREATE INDEX "ProfileView_userId_idx" ON "ProfileView"("userId"); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "ProfileView" ADD CONSTRAINT "ProfileView_viewerId_fkey" FOREIGN KEY ("viewerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "ProfileView" ADD CONSTRAINT "ProfileView_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: "dependency review" 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: "checkout repository" 18 | uses: actions/checkout@v6 19 | - name: "dependency review" 20 | uses: actions/dependency-review-action@v4 21 | -------------------------------------------------------------------------------- /src/types/Chart.ts: -------------------------------------------------------------------------------- 1 | export interface ChartData { 2 | type: string; 3 | data: { 4 | labels: string[]; 5 | datasets: { 6 | yAxisID?: string; 7 | label: string; 8 | data: number[]; 9 | fill?: boolean; 10 | lineTension?: number; 11 | }[]; 12 | }; 13 | options?: { 14 | title?: { 15 | display: boolean; 16 | text: string; 17 | }; 18 | scales?: { 19 | yAxes: { 20 | id: string; 21 | display: true; 22 | position: "left" | "right"; 23 | gridLines?: { display?: boolean }; 24 | ticks: { min?: number; max?: number; callback?: (val: number) => string }; 25 | }[]; 26 | }; 27 | elements?: { 28 | point?: { 29 | pointStyle?: string; 30 | radius: number; 31 | }; 32 | }; 33 | plugins: { 34 | tickFormat: { 35 | style: "currency"; 36 | currency: "USD"; 37 | minimumFractionDigits: 0; 38 | }; 39 | }; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/interactions/bake.ts: -------------------------------------------------------------------------------- 1 | import { NypsiCommandInteraction } from "../models/Command"; 2 | import { InteractionHandler } from "../types/InteractionHandler"; 3 | import { isEcoBanned } from "../utils/functions/economy/utils"; 4 | import { runCommand } from "../utils/handlers/commandhandler"; 5 | 6 | export default { 7 | name: "bake", 8 | type: "interaction", 9 | async run(interaction) { 10 | if (!interaction.isButton()) return; 11 | if ((await isEcoBanned(interaction.user.id)).banned) return; 12 | if (!interaction.channel.permissionsFor(interaction.user.id).has("SendMessages")) return; 13 | 14 | const int = interaction as unknown as NypsiCommandInteraction; 15 | 16 | int.author = interaction.user; 17 | int.commandName = "bake"; 18 | 19 | setTimeout(() => { 20 | if (interaction.isRepliable()) { 21 | interaction.deferReply().catch(() => {}); 22 | } 23 | }, 2500); 24 | 25 | return runCommand("bake", int, []); 26 | }, 27 | } as InteractionHandler; 28 | -------------------------------------------------------------------------------- /src/interactions/fish.ts: -------------------------------------------------------------------------------- 1 | import { NypsiCommandInteraction } from "../models/Command"; 2 | import { InteractionHandler } from "../types/InteractionHandler"; 3 | import { isEcoBanned } from "../utils/functions/economy/utils"; 4 | import { runCommand } from "../utils/handlers/commandhandler"; 5 | 6 | export default { 7 | name: "fish", 8 | type: "interaction", 9 | async run(interaction) { 10 | if (!interaction.isButton()) return; 11 | if ((await isEcoBanned(interaction.user.id)).banned) return; 12 | if (!interaction.channel.permissionsFor(interaction.user.id).has("SendMessages")) return; 13 | 14 | const int = interaction as unknown as NypsiCommandInteraction; 15 | 16 | int.author = interaction.user; 17 | int.commandName = "fish"; 18 | 19 | setTimeout(() => { 20 | if (interaction.isRepliable()) { 21 | interaction.deferReply().catch(() => {}); 22 | } 23 | }, 2500); 24 | 25 | return runCommand("fish", int, []); 26 | }, 27 | } as InteractionHandler; 28 | -------------------------------------------------------------------------------- /src/interactions/hunt.ts: -------------------------------------------------------------------------------- 1 | import { NypsiCommandInteraction } from "../models/Command"; 2 | import { InteractionHandler } from "../types/InteractionHandler"; 3 | import { isEcoBanned } from "../utils/functions/economy/utils"; 4 | import { runCommand } from "../utils/handlers/commandhandler"; 5 | 6 | export default { 7 | name: "hunt", 8 | type: "interaction", 9 | async run(interaction) { 10 | if (!interaction.isButton()) return; 11 | if ((await isEcoBanned(interaction.user.id)).banned) return; 12 | if (!interaction.channel.permissionsFor(interaction.user.id).has("SendMessages")) return; 13 | 14 | const int = interaction as unknown as NypsiCommandInteraction; 15 | 16 | int.author = interaction.user; 17 | int.commandName = "hunt"; 18 | 19 | setTimeout(() => { 20 | if (interaction.isRepliable()) { 21 | interaction.deferReply().catch(() => {}); 22 | } 23 | }, 2500); 24 | 25 | return runCommand("hunt", int, []); 26 | }, 27 | } as InteractionHandler; 28 | -------------------------------------------------------------------------------- /src/interactions/mine.ts: -------------------------------------------------------------------------------- 1 | import { NypsiCommandInteraction } from "../models/Command"; 2 | import { InteractionHandler } from "../types/InteractionHandler"; 3 | import { isEcoBanned } from "../utils/functions/economy/utils"; 4 | import { runCommand } from "../utils/handlers/commandhandler"; 5 | 6 | export default { 7 | name: "mine", 8 | type: "interaction", 9 | async run(interaction) { 10 | if (!interaction.isButton()) return; 11 | if ((await isEcoBanned(interaction.user.id)).banned) return; 12 | if (!interaction.channel.permissionsFor(interaction.user.id).has("SendMessages")) return; 13 | 14 | const int = interaction as unknown as NypsiCommandInteraction; 15 | 16 | int.author = interaction.user; 17 | int.commandName = "mine"; 18 | 19 | setTimeout(() => { 20 | if (interaction.isRepliable()) { 21 | interaction.deferReply().catch(() => {}); 22 | } 23 | }, 2500); 24 | 25 | return runCommand("mine", int, []); 26 | }, 27 | } as InteractionHandler; 28 | -------------------------------------------------------------------------------- /src/commands/github.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders.js"; 4 | 5 | const cmd = new Command("github", "view code for the bot on github", "info").setAliases(["git"]); 6 | 7 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 8 | const embed = new CustomEmbed( 9 | message.member, 10 | "nypsi is open source!!\n" + 11 | "click [here](https://github.com/mxz7/nypsi) to view the source code on github", 12 | ) 13 | .setTitle("github") 14 | .setURL("https://github.com/mxz7/nypsi") 15 | .addField( 16 | "what does this mean?", 17 | "if you know how to code, you could fix bugs, add features, create your own commands.. the list goes on.", 18 | ); 19 | 20 | return message.channel.send({ embeds: [embed] }); 21 | } 22 | 23 | cmd.setRun(run); 24 | 25 | module.exports = cmd; 26 | -------------------------------------------------------------------------------- /prisma/migrations/20230218201725_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `Auction` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - The `id` column on the `Auction` table would be dropped and recreated. This will lead to data loss if there is data in the column. 6 | - Made the column `earned` on table `Game` required. This step will fail if there are existing NULL values in that column. 7 | - Made the column `xpEarned` on table `Game` required. This step will fail if there are existing NULL values in that column. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "Auction" DROP CONSTRAINT "Auction_pkey", 12 | DROP COLUMN "id", 13 | ADD COLUMN "id" SERIAL NOT NULL, 14 | ADD CONSTRAINT "Auction_pkey" PRIMARY KEY ("id"); 15 | 16 | -- AlterTable 17 | ALTER TABLE "Game" ALTER COLUMN "earned" SET NOT NULL, 18 | ALTER COLUMN "earned" SET DEFAULT 0, 19 | ALTER COLUMN "xpEarned" SET NOT NULL, 20 | ALTER COLUMN "xpEarned" SET DEFAULT 0; 21 | -------------------------------------------------------------------------------- /src/interactions/vote_reminders.ts: -------------------------------------------------------------------------------- 1 | import { MessageFlags } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { InteractionHandler } from "../types/InteractionHandler"; 4 | import Constants from "../utils/Constants"; 5 | import { getDmSettings, updateDmSettings } from "../utils/functions/users/notifications"; 6 | 7 | export default { 8 | name: "enable-vote-reminders", 9 | type: "interaction", 10 | async run(interaction) { 11 | const settings = await getDmSettings(interaction.user.id); 12 | settings.voteReminder = true; 13 | await updateDmSettings(interaction.user.id, settings); 14 | 15 | return interaction.reply({ 16 | embeds: [ 17 | new CustomEmbed( 18 | null, 19 | "✅ vote reminders have been enabled, you now have an extra **2%** gamble multi and **5%** sell multi", 20 | ).setColor(Constants.EMBED_SUCCESS_COLOR), 21 | ], 22 | flags: MessageFlags.Ephemeral, 23 | }); 24 | }, 25 | } as InteractionHandler; 26 | -------------------------------------------------------------------------------- /src/utils/functions/economy/items/bitch.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiCommandInteraction, NypsiMessage } from "../../../../models/Command"; 3 | import { CustomEmbed } from "../../../../models/EmbedBuilders"; 4 | import { ItemUse } from "../../../../models/ItemUse"; 5 | import { pluralize } from "../../string"; 6 | import { getInventory } from "../inventory"; 7 | import { addStat } from "../stats"; 8 | import { getItems } from "../utils"; 9 | 10 | module.exports = new ItemUse( 11 | "bitch", 12 | async (message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) => { 13 | await addStat(message.member, "bitch"); 14 | 15 | const inventory = await getInventory(message.member); 16 | 17 | return ItemUse.send(message, { 18 | embeds: [ 19 | new CustomEmbed( 20 | message.member, 21 | `you had fun with your ${pluralize(getItems()["bitch"], inventory.count("bitch"))}`, 22 | ), 23 | ], 24 | }); 25 | }, 26 | ); 27 | -------------------------------------------------------------------------------- /prisma/migrations/20240619222431_remove_useless_index/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `ModerationCase` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "Achievements_userId_idx"; 9 | 10 | -- DropIndex 11 | DROP INDEX "CommandUse_userId_idx"; 12 | 13 | -- DropIndex 14 | DROP INDEX "Inventory_userId_idx"; 15 | 16 | -- DropIndex 17 | DROP INDEX "ModerationCase_guildId_idx"; 18 | 19 | -- DropForeignKey 20 | ALTER TABLE "ModerationEvidence" DROP CONSTRAINT "ModerationEvidence_caseId_guildId_fkey"; 21 | 22 | -- AlterTable 23 | ALTER TABLE "ModerationCase" DROP CONSTRAINT "ModerationCase_pkey", 24 | ADD CONSTRAINT "ModerationCase_pkey" PRIMARY KEY ("guildId", "caseId"); 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "ModerationEvidence" ADD CONSTRAINT "ModerationEvidence_caseId_guildId_fkey" FOREIGN KEY ("caseId", "guildId") REFERENCES "ModerationCase"("caseId", "guildId") ON DELETE RESTRICT ON UPDATE CASCADE; -------------------------------------------------------------------------------- /src/interactions/aliases.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getPrefix } from "../utils/functions/guilds/utils"; 3 | import { getUserAliases } from "../utils/functions/premium/aliases"; 4 | import { logger } from "../utils/logger"; 5 | 6 | export default { 7 | name: "alias", 8 | type: "autocomplete", 9 | async run(interaction) { 10 | const aliases = await getUserAliases(interaction.user.id); 11 | 12 | if (aliases.length == 0) return await interaction.respond([]); 13 | 14 | const prefix = (await getPrefix(interaction.guild))[0]; 15 | 16 | const formatted = aliases.map((i) => ({ 17 | name: `${prefix}${i.alias} (${prefix}${i.command})`, 18 | value: i.alias, 19 | })); 20 | 21 | return await interaction.respond(formatted).catch(() => { 22 | logger.warn(`failed to respond to autocomplete in time`, { 23 | userId: interaction.user.id, 24 | command: interaction.commandName, 25 | }); 26 | }); 27 | }, 28 | } as AutocompleteHandler; 29 | -------------------------------------------------------------------------------- /prisma/migrations/20230727133856_tags/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `badges` on the `User` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "ActiveChannels" DROP CONSTRAINT "ActiveChannels_userId_fkey"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "User" DROP COLUMN "badges"; 12 | 13 | -- CreateTable 14 | CREATE TABLE "Tags" ( 15 | "userId" TEXT NOT NULL, 16 | "tagId" TEXT NOT NULL, 17 | "selected" BOOLEAN NOT NULL DEFAULT false, 18 | "created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP 19 | ); 20 | 21 | -- CreateIndex 22 | CREATE UNIQUE INDEX "Tags_userId_tagId_key" ON "Tags"("userId", "tagId"); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "Tags" ADD CONSTRAINT "Tags_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "ActiveChannels" ADD CONSTRAINT "ActiveChannels_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 29 | -------------------------------------------------------------------------------- /src/commands/wiki.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | 5 | const cmd = new Command( 6 | "wiki", 7 | "get the link to the nypsi wiki / documentation", 8 | "info", 9 | ).setAliases(["docs"]); 10 | 11 | cmd.slashEnabled = true; 12 | 13 | async function run( 14 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 15 | send: SendMessage, 16 | ) { 17 | const embed = new CustomEmbed( 18 | message.member, 19 | "https://nypsi.xyz/docs?ref=bot-wiki\n\n" + 20 | "nypsi documentation / wiki is fully open source, meaning you can contribute and add to it! it may not be in the best shape right now and have all of the information, but it's always being improved and kept up to date", 21 | ).setHeader("nypsi documentation", message.author.avatarURL()); 22 | 23 | return send({ embeds: [embed] }); 24 | } 25 | 26 | cmd.setRun(run); 27 | 28 | module.exports = cmd; 29 | -------------------------------------------------------------------------------- /src/interactions/crash_out.ts: -------------------------------------------------------------------------------- 1 | import redis from "../init/redis"; 2 | import { NypsiClient } from "../models/Client"; 3 | import { ErrorEmbed } from "../models/EmbedBuilders"; 4 | import { InteractionHandler } from "../types/InteractionHandler"; 5 | import Constants from "../utils/Constants"; 6 | import { crashOut } from "../utils/functions/economy/crash"; 7 | import { isEcoBanned, userExists } from "../utils/functions/economy/utils"; 8 | 9 | export default { 10 | name: "crash-out", 11 | type: "interaction", 12 | async run(interaction) { 13 | if (!interaction.isButton()) return; 14 | if ((await isEcoBanned(interaction.user.id)).banned) return; 15 | if (!(await userExists(interaction.user.id))) return; 16 | 17 | if ( 18 | (await redis.get( 19 | `${Constants.redis.nypsi.RESTART}:${(interaction.client as NypsiClient).cluster.id}`, 20 | )) == "t" 21 | ) { 22 | return interaction.reply({ embeds: [new ErrorEmbed("nypsi is rebooting")] }); 23 | } 24 | 25 | await crashOut(interaction); 26 | }, 27 | } as InteractionHandler; 28 | -------------------------------------------------------------------------------- /src/scheduled/jobs/premiumexpire.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../init/database"; 2 | import { Job } from "../../types/Jobs"; 3 | import { expireUser } from "../../utils/functions/premium/premium"; 4 | 5 | export default { 6 | name: "premiumexpire", 7 | cron: "45 23 * * *", 8 | async run(log, manager) { 9 | const query = await prisma.premium.findMany({ 10 | where: { 11 | AND: [{ credit: { lt: 1 } }, { expireDate: { lt: new Date() } }, { level: { gt: 0 } }], 12 | }, 13 | select: { 14 | userId: true, 15 | }, 16 | }); 17 | 18 | for (const user of query) { 19 | await expireUser(user.userId, manager); 20 | } 21 | 22 | log(`${query.length} members expired`); 23 | 24 | const reduceCredits = await prisma.premium.updateMany({ 25 | where: { 26 | AND: [{ credit: { gt: 0 } }, { expireDate: { lt: new Date() } }], 27 | }, 28 | data: { 29 | credit: { decrement: 1 }, 30 | }, 31 | }); 32 | 33 | log(`${reduceCredits.count} credits removed`); 34 | }, 35 | } satisfies Job; 36 | -------------------------------------------------------------------------------- /src/interactions/workers_claim.ts: -------------------------------------------------------------------------------- 1 | import { NypsiClient } from "../models/Client"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { InteractionHandler } from "../types/InteractionHandler"; 4 | import Constants from "../utils/Constants"; 5 | import { isEcoBanned } from "../utils/functions/economy/utils"; 6 | import { claimFromWorkers } from "../utils/functions/economy/workers"; 7 | 8 | export default { 9 | name: "w-claim", 10 | type: "interaction", 11 | async run(interaction) { 12 | if (!interaction.isButton()) return; 13 | if ((await isEcoBanned(interaction.user.id)).banned) return; 14 | await interaction.deferReply(); 15 | const desc = await claimFromWorkers(interaction.user.id, interaction.client as NypsiClient); 16 | 17 | const embed = new CustomEmbed() 18 | .setDescription(desc) 19 | .setColor(Constants.EMBED_SUCCESS_COLOR) 20 | .setHeader("workers", interaction.user.avatarURL()) 21 | .disableFooter(); 22 | 23 | return interaction.editReply({ embeds: [embed] }); 24 | }, 25 | } as InteractionHandler; 26 | -------------------------------------------------------------------------------- /src/commands/roll.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders.js"; 4 | 5 | const cmd = new Command("roll", "roll a dice", "utility"); 6 | 7 | async function run( 8 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 9 | send: SendMessage, 10 | args: string[], 11 | ) { 12 | let range = 6; 13 | 14 | if (args.length != 0) { 15 | if (parseInt(args[0])) { 16 | if (parseInt(args[0]) < 2 || parseInt(args[0]) > 1000000000) { 17 | return send({ embeds: [new ErrorEmbed("invalid range")] }); 18 | } else { 19 | range = parseInt(args[0]); 20 | } 21 | } 22 | } 23 | 24 | return send({ 25 | embeds: [ 26 | new CustomEmbed( 27 | message.member, 28 | "🎲 you rolled `" + (Math.floor(Math.random() * range) + 1).toLocaleString() + "`", 29 | ), 30 | ], 31 | }); 32 | } 33 | 34 | cmd.setRun(run); 35 | 36 | module.exports = cmd; 37 | -------------------------------------------------------------------------------- /prisma/migrations/20240210132835_tasks/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `vote` on the `DMSettings` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- CreateEnum 8 | CREATE TYPE "TaskType" AS ENUM ('daily', 'weekly'); 9 | 10 | -- AlterTable 11 | ALTER TABLE "DMSettings" DROP COLUMN "vote"; 12 | 13 | -- AlterTable 14 | ALTER TABLE "Economy" ADD COLUMN "dailyTaskStreak" INTEGER NOT NULL DEFAULT 0, 15 | ADD COLUMN "weeklyTaskStreak" INTEGER NOT NULL DEFAULT 0; 16 | 17 | -- CreateTable 18 | CREATE TABLE "Task" ( 19 | "user_id" TEXT NOT NULL, 20 | "task_id" TEXT NOT NULL, 21 | "progress" BIGINT NOT NULL DEFAULT 0, 22 | "target" BIGINT NOT NULL, 23 | "completed" BOOLEAN NOT NULL DEFAULT false, 24 | "prize" TEXT NOT NULL, 25 | "type" "TaskType" NOT NULL, 26 | 27 | CONSTRAINT "Task_pkey" PRIMARY KEY ("user_id","task_id") 28 | ); 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "Task" ADD CONSTRAINT "Task_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "Economy"("userId") ON DELETE RESTRICT ON UPDATE CASCADE; 32 | -------------------------------------------------------------------------------- /src/interactions/transfer.ts: -------------------------------------------------------------------------------- 1 | import redis from "../init/redis"; 2 | import { ErrorEmbed } from "../models/EmbedBuilders"; 3 | import { InteractionHandler } from "../types/InteractionHandler"; 4 | import Constants from "../utils/Constants"; 5 | import { doProfileTransfer } from "../utils/functions/users/utils"; 6 | 7 | export default { 8 | name: "t-f-p-boobies", 9 | type: "interaction", 10 | async run(interaction) { 11 | if (!interaction.isButton()) return; 12 | 13 | const from = await redis.get( 14 | `${Constants.redis.nypsi.PROFILE_TRANSFER}:${interaction.user.id}`, 15 | ); 16 | 17 | if (!from) return interaction.reply({ embeds: [new ErrorEmbed("expired")] }); 18 | if (!(await redis.exists(`${Constants.redis.nypsi.PROFILE_TRANSFER}:${from}`))) 19 | return interaction.reply({ embeds: [new ErrorEmbed("expired")] }); 20 | 21 | await interaction.reply({ content: "transferring data..." }); 22 | 23 | await doProfileTransfer(from, interaction.user.id); 24 | 25 | await interaction.editReply({ content: "done" }); 26 | }, 27 | } as InteractionHandler; 28 | -------------------------------------------------------------------------------- /prisma/migrations/20240411143533_custom_cars/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "CarUpgradeType" AS ENUM ('engine', 'turbo', 'wheel'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "CustomCar" ( 6 | "id" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "name" TEXT NOT NULL, 9 | "userId" TEXT NOT NULL, 10 | 11 | CONSTRAINT "CustomCar_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateTable 15 | CREATE TABLE "CarUpgrade" ( 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "type" "CarUpgradeType" NOT NULL, 18 | "amount" INTEGER NOT NULL DEFAULT 1, 19 | "carId" TEXT NOT NULL, 20 | 21 | CONSTRAINT "CarUpgrade_pkey" PRIMARY KEY ("carId","type") 22 | ); 23 | 24 | -- AddForeignKey 25 | ALTER TABLE "CustomCar" ADD CONSTRAINT "CustomCar_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "CarUpgrade" ADD CONSTRAINT "CarUpgrade_carId_fkey" FOREIGN KEY ("carId") REFERENCES "CustomCar"("id") ON DELETE CASCADE ON UPDATE CASCADE; 29 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # required for bot to work 2 | BOT_TOKEN="discord bot token" 3 | DATABASE_URL="postgres connection string" 4 | DATABASE_DIRECT_URL="direction connection without pooling" 5 | ENCRYPT_KEY="encryption key for data" 6 | LOG_FILE="./out/nypsi.log" 7 | 8 | # optional but may break things and will definitely lack functionality 9 | API_AUTH="meow" 10 | TOPGG_TOKEN="for vote" 11 | LASTFM_TOKEN="for music stats" 12 | TMDB_KEY="for movie / tv show stats" 13 | OPENAI_KEY="for gpt responses" 14 | NYPSI_LLMS_VECTOR_STORE="for gpt responses (nypsi.xyz/llms.txt)" 15 | S3_ENDPOINT="endpoint for s3 storage" 16 | S3_REGION="region for s3 storage" 17 | S3_KEY_ID="key id for s3 storage" 18 | S3_KEY="key for s3 storage" 19 | S3_BUCKET="bucket name for s3 storage" 20 | KOFI_VERIFICATION="for purchases" 21 | BOTLOGS_HOOK="discord webhook url" 22 | LOTTERY_HOOK="discord webhook url" 23 | TOPGLOBAL_HOOK="discord webhook url" 24 | TOPCOMMANDS_HOOK="discord webhook url" 25 | ANTICHEAT_HOOK="discord webhook url" 26 | PAYMENTS_HOOK="discord webhook url" 27 | GAMBLE_HOOK="discord webhook url" 28 | ACHIEVEMENTS_HOOK="discord webhook url" 29 | -------------------------------------------------------------------------------- /src/utils/functions/guilds/disabledcommands.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from "discord.js"; 2 | import prisma from "../../../init/database"; 3 | 4 | const disableCache = new Map(); 5 | 6 | export async function getDisabledCommands(guild: Guild): Promise { 7 | if (disableCache.has(guild.id)) { 8 | return disableCache.get(guild.id); 9 | } 10 | 11 | const query = await prisma.guild.findUnique({ 12 | where: { 13 | id: guild.id, 14 | }, 15 | select: { 16 | disabledCommands: true, 17 | }, 18 | }); 19 | 20 | disableCache.set(guild.id, query.disabledCommands); 21 | 22 | setTimeout(() => { 23 | if (disableCache.has(guild.id)) disableCache.delete(guild.id); 24 | }, 43200000); 25 | 26 | return query.disabledCommands; 27 | } 28 | 29 | export async function updateDisabledCommands(guild: Guild, array: string[]) { 30 | await prisma.guild.update({ 31 | where: { 32 | id: guild.id, 33 | }, 34 | data: { 35 | disabledCommands: array, 36 | }, 37 | }); 38 | 39 | if (disableCache.has(guild.id)) disableCache.delete(guild.id); 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/functions/workers/sort.ts: -------------------------------------------------------------------------------- 1 | import { inPlaceSort } from "fast-sort"; 2 | import { isMainThread, parentPort, Worker, workerData } from "worker_threads"; 3 | 4 | export default function workerSort( 5 | data: T[], 6 | sortFunction: keyof T, 7 | direction: "asc" | "desc", 8 | ): Promise { 9 | return new Promise((resolve, reject) => { 10 | const worker = new Worker(__filename, { 11 | workerData: [data, sortFunction, direction], 12 | }); 13 | worker.on("message", resolve); 14 | worker.on("error", reject); 15 | worker.on("exit", (code) => { 16 | if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); 17 | }); 18 | }); 19 | } 20 | 21 | if (!isMainThread) { 22 | process.title = "nypsi: sort worker"; 23 | const data: any[] = workerData[0]; 24 | const key: string = workerData[1]; 25 | const direction: "asc" | "desc" = workerData[2]; 26 | 27 | if (direction === "asc") { 28 | inPlaceSort(data).asc((i) => i[key]); 29 | } else { 30 | inPlaceSort(data).desc((i) => i[key]); 31 | } 32 | 33 | parentPort.postMessage(data); 34 | process.exit(0); 35 | } 36 | -------------------------------------------------------------------------------- /src/types/Workers.ts: -------------------------------------------------------------------------------- 1 | export interface Worker { 2 | name: string; 3 | id: string; 4 | item_emoji: string; 5 | prestige_requirement: number; 6 | cost: number; 7 | base: { 8 | per_item: number; 9 | max_storage: number; 10 | per_interval: number; 11 | byproducts?: { 12 | [item: string]: { 13 | chance: number; 14 | rolls: number; 15 | multiply_chance: boolean; 16 | multiply_rolls: boolean; 17 | }; 18 | }; 19 | }; 20 | } 21 | 22 | export interface WorkerUpgrades { 23 | name: string; 24 | plural?: string; 25 | id: string; 26 | upgrades: PossibleUpgrade; 27 | effect: number; // decimal 28 | stack_limit: number; 29 | base_cost?: number; 30 | for?: string; 31 | byproduct?: string; 32 | } 33 | 34 | export interface WorkerByproducts { 35 | [item: string]: number; 36 | } 37 | 38 | export type PossibleUpgrade = 39 | | "per_item" 40 | | "per_interval" 41 | | "max_storage" 42 | | "byproduct_chance" 43 | | "byproduct_rolls"; 44 | 45 | export type SteveData = { 46 | money: number; 47 | byproducts: { 48 | [index: string]: number; 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils/functions/economy/items/football.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiCommandInteraction, NypsiMessage } from "../../../../models/Command"; 3 | import { CustomEmbed } from "../../../../models/EmbedBuilders"; 4 | import { ItemUse } from "../../../../models/ItemUse"; 5 | import { addStat } from "../stats"; 6 | 7 | const responses = [ 8 | "img:https://c.tenor.com/xycFj0Sr_2IAAAAd/tenor.gif", 9 | "img:https://c.tenor.com/rmOSRULjBj4AAAAC/tenor.gif", 10 | "img:https://c.tenor.com/ePP-BHO96PQAAAAC/tenor.gif", 11 | ]; 12 | 13 | module.exports = new ItemUse( 14 | "football", 15 | async (message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) => { 16 | const chosen = responses[Math.floor(Math.random() * responses.length)]; 17 | 18 | const embed = new CustomEmbed(message.member); 19 | 20 | if (chosen.startsWith("img:")) embed.setImage(chosen.substring(4, chosen.length)); 21 | else embed.setDescription(chosen); 22 | 23 | await addStat(message.member, "football"); 24 | 25 | return ItemUse.send(message, { 26 | embeds: [embed], 27 | }); 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /data/guild_upgrades.json: -------------------------------------------------------------------------------- 1 | { 2 | "member": { 3 | "id": "member", 4 | "name": "member slot", 5 | "description": "+1 member slot", 6 | "cost": 1, 7 | "increment_per_level": 1 8 | }, 9 | "maxbet": { 10 | "id": "maxbet", 11 | "name": "max bet increase", 12 | "description": "+75k maxbet", 13 | "cost": 1, 14 | "increment_per_level": 0.5 15 | }, 16 | "multi": { 17 | "id": "multi", 18 | "name": "gamble multiplier", 19 | "description": "+1% gamble multiplier", 20 | "cost": 2, 21 | "increment_per_level": 2 22 | }, 23 | "sellmulti": { 24 | "id": "sellmulti", 25 | "name": "sell multiplier", 26 | "description": "+5% sell multiplier", 27 | "cost": 2, 28 | "increment_per_level": 2 29 | }, 30 | "cooldown": { 31 | "id": "cooldown", 32 | "name": "shorter cooldowns", 33 | "description": "+5% shorter cooldowns", 34 | "cost": 1, 35 | "increment_per_level": 2.5 36 | }, 37 | "bakery": { 38 | "id": "bakery", 39 | "name": "double cookies", 40 | "description": "+2% chance to double cookies produced", 41 | "cost": 1, 42 | "increment_per_level": 0.25 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/functions/economy/items/padlock.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiCommandInteraction, NypsiMessage } from "../../../../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../../../../models/EmbedBuilders"; 4 | import { ItemUse } from "../../../../models/ItemUse"; 5 | import { hasPadlock, setPadlock } from "../balance"; 6 | import { removeInventoryItem } from "../inventory"; 7 | import { addStat } from "../stats"; 8 | 9 | module.exports = new ItemUse( 10 | "padlock", 11 | async (message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) => { 12 | if (await hasPadlock(message.member)) { 13 | return ItemUse.send(message, { 14 | embeds: [new ErrorEmbed("you already have a padlock on your balance")], 15 | }); 16 | } 17 | 18 | await Promise.all([ 19 | removeInventoryItem(message.member, "padlock", 1), 20 | addStat(message.member, "padlock"), 21 | setPadlock(message.member, true), 22 | ]); 23 | 24 | return ItemUse.send(message, { 25 | embeds: [new CustomEmbed(message.member, "✅ your padlock has been applied")], 26 | }); 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /src/utils/functions/users/wordle.ts: -------------------------------------------------------------------------------- 1 | import { WordleGame } from "#generated/prisma"; 2 | import prisma from "../../../init/database"; 3 | import { addProgress } from "../economy/achievements"; 4 | import { addTaskProgress } from "../economy/tasks"; 5 | import { getUserId, MemberResolvable } from "../member"; 6 | 7 | export async function addWordleGame( 8 | member: MemberResolvable, 9 | win: boolean, 10 | guesses: string[], 11 | ms: number, 12 | word: string, 13 | ) { 14 | if (win) { 15 | addProgress(member, "wordle", 1); 16 | addTaskProgress(member, "wordles_daily"); 17 | addTaskProgress(member, "wordles_weekly"); 18 | } 19 | 20 | const id = await prisma.wordleGame.create({ 21 | data: { 22 | time: Math.floor(ms), 23 | won: win, 24 | word, 25 | guesses, 26 | userId: getUserId(member), 27 | }, 28 | select: { 29 | id: true, 30 | }, 31 | }); 32 | 33 | return id.id.toString(36); 34 | } 35 | 36 | export async function getWordleGame(id: string) { 37 | return prisma.wordleGame 38 | .findUnique({ 39 | where: { id: parseInt(id, 36) }, 40 | }) 41 | .catch(() => undefined as WordleGame); 42 | } 43 | -------------------------------------------------------------------------------- /prisma/migrations/20230327175012_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Auction" ALTER COLUMN "itemAmount" SET DATA TYPE BIGINT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "DMSettings" ADD COLUMN "autosellStatus" BOOLEAN NOT NULL DEFAULT true; 6 | 7 | -- AlterTable 8 | ALTER TABLE "Economy" ADD COLUMN "autosell" TEXT[], 9 | ADD COLUMN "offersBlock" TEXT[]; 10 | 11 | -- AlterTable 12 | ALTER TABLE "Preferences" ADD COLUMN "offers" INTEGER NOT NULL DEFAULT 3; 13 | 14 | -- CreateTable 15 | CREATE TABLE "Offer" ( 16 | "ownerId" TEXT NOT NULL, 17 | "itemId" TEXT NOT NULL, 18 | "itemAmount" BIGINT NOT NULL DEFAULT 1, 19 | "money" BIGINT NOT NULL, 20 | "messageId" TEXT NOT NULL, 21 | "targetId" TEXT NOT NULL 22 | ); 23 | 24 | -- CreateIndex 25 | CREATE UNIQUE INDEX "Offer_messageId_key" ON "Offer"("messageId"); 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "Offer" ADD CONSTRAINT "Offer_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "Offer" ADD CONSTRAINT "Offer_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "Economy"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 32 | -------------------------------------------------------------------------------- /src/commands/deleteslash.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction, Message } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import Constants from "../utils/Constants"; 4 | import { 5 | deleteSlashCommands, 6 | deleteSlashCommandsFromGuild, 7 | } from "../utils/handlers/commandhandler"; 8 | 9 | const cmd = new Command("deleteslash", "delete slash commands", "none").setPermissions([ 10 | "bot owner", 11 | ]); 12 | 13 | async function run( 14 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 15 | send: SendMessage, 16 | args: string[], 17 | ) { 18 | if (message.author.id != Constants.OWNER_ID) return; 19 | 20 | if (args.length == 0) { 21 | await deleteSlashCommandsFromGuild(message.guild.id, message.client.user.id); 22 | 23 | if (!(message instanceof Message)) return; 24 | 25 | return await message.react("✅"); 26 | } else if (args[0].toLowerCase() == "global") { 27 | await deleteSlashCommands(message.client.user.id); 28 | 29 | if (!(message instanceof Message)) return; 30 | 31 | return await message.react("✅"); 32 | } 33 | } 34 | 35 | cmd.setRun(run); 36 | 37 | module.exports = cmd; 38 | -------------------------------------------------------------------------------- /src/scheduled/jobs/netupdate-3.ts: -------------------------------------------------------------------------------- 1 | import dayjs = require("dayjs"); 2 | import prisma from "../../init/database"; 3 | import redis from "../../init/redis"; 4 | import { Job } from "../../types/Jobs"; 5 | import Constants from "../../utils/Constants"; 6 | import { MStoTime } from "../../utils/functions/date"; 7 | import { calcNetWorth } from "../../utils/functions/economy/balance"; 8 | 9 | export default { 10 | name: "netupdate-3", 11 | cron: "0 0 * * *", 12 | async run(log, manager) { 13 | const start = Date.now(); 14 | const query = await prisma.economy.findMany({ 15 | select: { 16 | userId: true, 17 | }, 18 | where: { 19 | user: { 20 | lastCommand: { gt: dayjs().subtract(1, "day").toDate() }, 21 | }, 22 | }, 23 | }); 24 | 25 | let count = 0; 26 | 27 | for (const user of query) { 28 | if (await redis.exists(`${Constants.redis.cache.economy.NETWORTH}:${user.userId}`)) continue; 29 | 30 | await calcNetWorth("job", user.userId, manager); 31 | count++; 32 | } 33 | 34 | log( 35 | `net worth updated for ${count.toLocaleString()} members in ${MStoTime(Date.now() - start)}`, 36 | ); 37 | }, 38 | } satisfies Job; 39 | -------------------------------------------------------------------------------- /src/utils/functions/guilds/channels.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from "discord.js"; 2 | import prisma from "../../../init/database"; 3 | 4 | const disabledChannels = new Map(); 5 | 6 | export async function getDisabledChannels(guild: Guild) { 7 | if (disabledChannels.has(guild.id)) { 8 | return disabledChannels.get(guild.id); 9 | } 10 | 11 | const query = await prisma.guild.findUnique({ 12 | where: { 13 | id: guild.id, 14 | }, 15 | select: { 16 | disabledChannels: true, 17 | }, 18 | }); 19 | 20 | setTimeout(() => { 21 | if (disabledChannels.has(guild.id)) disabledChannels.delete(guild.id); 22 | }, 43200000); 23 | 24 | disabledChannels.set(guild.id, query.disabledChannels); 25 | 26 | return query.disabledChannels; 27 | } 28 | 29 | export async function setDisabledChannels(guild: Guild, channels: string[]) { 30 | await prisma.guild.update({ 31 | where: { 32 | id: guild.id, 33 | }, 34 | data: { 35 | disabledChannels: channels, 36 | }, 37 | }); 38 | 39 | setTimeout(() => { 40 | if (disabledChannels.has(guild.id)) disabledChannels.delete(guild.id); 41 | }, 43200000); 42 | 43 | disabledChannels.set(guild.id, channels); 44 | } 45 | -------------------------------------------------------------------------------- /src/models/StatsProfile.ts: -------------------------------------------------------------------------------- 1 | type StatsData = { 2 | economyUserId: string; 3 | type: string; 4 | win: number | bigint; 5 | lose: number | bigint; 6 | gamble: boolean; 7 | }; 8 | 9 | export class StatsProfile { 10 | public gamble: { [key: string]: { wins: number; lose: number } }; 11 | public items: { [key: string]: number }; 12 | public rob: { wins: number; lose: number }; 13 | constructor(data: StatsData[]) { 14 | this.gamble = {}; 15 | this.items = {}; 16 | this.rob = { 17 | wins: 0, 18 | lose: 0, 19 | }; 20 | 21 | if (data) { 22 | this.setData(data); 23 | } 24 | 25 | return this; 26 | } 27 | 28 | setData(data: StatsData[]) { 29 | for (const d of data) { 30 | if (d.gamble) { 31 | this.gamble[d.type] = { 32 | wins: 0, 33 | lose: 0, 34 | }; 35 | this.gamble[d.type].wins = Number(d.win); 36 | this.gamble[d.type].lose = Number(d.lose); 37 | } else { 38 | if (d.type == "rob") { 39 | this.rob.wins = Number(d.win); 40 | this.rob.lose = Number(d.lose); 41 | } else { 42 | this.items[d.type] = Number(d.win); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/scheduled/jobs/netupdate-1.ts: -------------------------------------------------------------------------------- 1 | import dayjs = require("dayjs"); 2 | import prisma from "../../init/database"; 3 | import redis from "../../init/redis"; 4 | import { Job } from "../../types/Jobs"; 5 | import Constants from "../../utils/Constants"; 6 | import { MStoTime } from "../../utils/functions/date"; 7 | import { calcNetWorth } from "../../utils/functions/economy/balance"; 8 | 9 | export default { 10 | name: "netupdate-1", 11 | cron: "0 0 */7 * *", 12 | async run(log, manager) { 13 | const start = Date.now(); 14 | const query = await prisma.economy.findMany({ 15 | select: { 16 | userId: true, 17 | }, 18 | where: { 19 | user: { 20 | lastCommand: { lt: dayjs().subtract(7, "day").toDate() }, 21 | }, 22 | }, 23 | }); 24 | 25 | let count = 0; 26 | 27 | for (const user of query) { 28 | if (await redis.exists(`${Constants.redis.cache.economy.NETWORTH}:${user.userId}`)) continue; 29 | 30 | await calcNetWorth("job", user.userId, manager); 31 | count++; 32 | } 33 | 34 | log( 35 | `net worth updated for ${count.toLocaleString()} members in ${MStoTime(Date.now() - start)}`, 36 | ); 37 | }, 38 | } satisfies Job; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # DON'T BE A DICK PUBLIC LICENSE 2 | 3 | > Version 1.1, December 2016 4 | 5 | > Copyright (C) [2025] [maxzdev] 6 | 7 | Everyone is permitted to copy and distribute verbatim or modified 8 | copies of this license document. 9 | 10 | > DON'T BE A DICK PUBLIC LICENSE 11 | > TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 1. Do whatever you like with the original work, just don't be a dick. 14 | 15 | Being a dick includes - but is not limited to - the following instances: 16 | 17 | 1a. Outright copyright infringement - Don't just copy this and change the name. 18 | 1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick. 19 | 1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick. 20 | 21 | 2. If you become rich through modifications, related works/services, or supporting the original work, 22 | share the love. Only a dick would make loads off this work and not buy the original work's 23 | creator(s) a pint. 24 | 25 | 3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes 26 | you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back. 27 | -------------------------------------------------------------------------------- /src/commands/reloadslash.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction, Message } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { hasAdminPermission } from "../utils/functions/users/admin"; 4 | import { uploadSlashCommands, uploadSlashCommandsToGuild } from "../utils/handlers/commandhandler"; 5 | 6 | const cmd = new Command("reloadslash", "reload data for slash commands", "none").setPermissions([ 7 | "bot owner", 8 | ]); 9 | 10 | async function run( 11 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 12 | send: SendMessage, 13 | args: string[], 14 | ) { 15 | if (!(await hasAdminPermission(message.member, "reload"))) return; 16 | 17 | if (args.length == 0) { 18 | await uploadSlashCommandsToGuild(message.guild.id, message.client.user.id); 19 | 20 | if (!(message instanceof Message)) return; 21 | 22 | return await message.react("✅"); 23 | } else if (args[0].toLowerCase() == "global") { 24 | await uploadSlashCommands(message.client.user.id); 25 | 26 | if (!(message instanceof Message)) return; 27 | 28 | return await message.react("✅"); 29 | } 30 | } 31 | 32 | cmd.setRun(run); 33 | 34 | module.exports = cmd; 35 | -------------------------------------------------------------------------------- /src/utils/functions/mutex.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../logger"; 2 | 3 | export class Mutex { 4 | private locks = new Map void)[] }>(); 5 | 6 | async acquire(key: string): Promise { 7 | logger.debug(`mutex: requested ${key}`); 8 | if (!this.locks.has(key)) { 9 | this.locks.set(key, { locked: false, queue: [] }); 10 | } 11 | 12 | const lock = this.locks.get(key)!; 13 | 14 | if (!lock.locked) { 15 | logger.debug(`mutex: acquired instantly ${key}`); 16 | lock.locked = true; 17 | return; 18 | } 19 | 20 | return new Promise((resolve) => { 21 | lock.queue.push(() => { 22 | logger.debug(`mutex: acquired ${key}`); 23 | resolve(); 24 | }); 25 | }); 26 | } 27 | 28 | release(key: string): void { 29 | logger.debug(`mutex: release ${key}`); 30 | const lock = this.locks.get(key); 31 | if (!lock) return; 32 | 33 | const next = lock.queue.shift(); 34 | if (next) { 35 | next(); 36 | } else { 37 | lock.locked = false; 38 | // Clean up empty locks to prevent memory leaks 39 | if (lock.queue.length === 0) { 40 | this.locks.delete(key); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/events/channelUpdate.ts: -------------------------------------------------------------------------------- 1 | import { GuildChannel } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { addLog, isLogsEnabled } from "../utils/functions/moderation/logs"; 4 | 5 | export default async function channelUpdate(oldChannel: GuildChannel, newChannel: GuildChannel) { 6 | if (oldChannel.name != newChannel.name && (await isLogsEnabled(newChannel.guild))) { 7 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 8 | 9 | embed.setTitle("channel renamed"); 10 | embed.setDescription( 11 | `${newChannel.toString()} \`${newChannel.id}\`\n` + 12 | `${oldChannel.name} -> ${newChannel.name}`, 13 | ); 14 | 15 | await addLog(newChannel.guild, "channel", embed); 16 | } else if ( 17 | oldChannel.parentId != newChannel.parentId && 18 | (await isLogsEnabled(newChannel.guild)) 19 | ) { 20 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 21 | 22 | embed.setTitle("channel moved category"); 23 | embed.setDescription( 24 | `${newChannel.toString()} \`${newChannel.id}\`\n` + 25 | `${oldChannel.parent.name} -> ${newChannel.parent.name}`, 26 | ); 27 | 28 | await addLog(newChannel.guild, "channel", embed); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/init/s3.ts: -------------------------------------------------------------------------------- 1 | import { S3Client } from "@aws-sdk/client-s3"; 2 | 3 | const s3 = new S3Client({ 4 | endpoint: process.env.S3_ENDPOINT, 5 | region: process.env.S3_REGION, 6 | requestChecksumCalculation: "WHEN_REQUIRED", 7 | responseChecksumValidation: "WHEN_REQUIRED", 8 | credentials: { 9 | accessKeyId: process.env.S3_KEY_ID, 10 | secretAccessKey: process.env.S3_KEY, 11 | }, 12 | }); 13 | 14 | // backblaze doesn't support these headers 15 | s3.middlewareStack.add( 16 | (next) => async (args) => { 17 | // @ts-expect-error stupid aws sdk I FUCKING HATE AWS 18 | const headers = args.request.headers; 19 | 20 | // List of unsupported checksum headers 21 | const checksumHeaders = [ 22 | "x-amz-checksum-crc32", 23 | "x-amz-checksum-crc32c", 24 | "x-amz-checksum-crc64nvme", 25 | "x-amz-checksum-sha1", 26 | "x-amz-checksum-sha256", 27 | "x-amz-checksum-algorithm", 28 | "x-amz-checksum-mode", 29 | ]; 30 | 31 | for (const header of checksumHeaders) { 32 | delete headers[header]; 33 | } 34 | 35 | return next(args); 36 | }, 37 | { 38 | step: "build", 39 | name: "removeChecksumHeaders", 40 | priority: "high", 41 | }, 42 | ); 43 | 44 | export default s3; 45 | -------------------------------------------------------------------------------- /src/utils/functions/economy/lottery.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../../init/database"; 2 | import { getUserId, MemberResolvable } from "../member"; 3 | import { getItems } from "./utils"; 4 | 5 | export async function getApproximatePrizePool() { 6 | const tickets = await prisma.inventory.aggregate({ 7 | where: { 8 | item: "lottery_ticket", 9 | }, 10 | _sum: { 11 | amount: true, 12 | }, 13 | }); 14 | 15 | const value = Number(tickets._sum.amount) * getItems()["lottery_ticket"].buy; 16 | 17 | return { 18 | min: Math.floor(value / 100_000_000) * 100_000_000, 19 | max: Math.ceil(value / 100_000_000) * 100_000_000, 20 | }; 21 | } 22 | 23 | export async function getDailyLottoTickets(member: MemberResolvable) { 24 | const query = await prisma.economy.findUnique({ 25 | where: { 26 | userId: getUserId(member), 27 | }, 28 | select: { 29 | dailyLottery: true, 30 | }, 31 | }); 32 | 33 | return query.dailyLottery; 34 | } 35 | 36 | export async function setDailyLotteryTickets(member: MemberResolvable, amount: number) { 37 | await prisma.economy.update({ 38 | where: { 39 | userId: getUserId(member), 40 | }, 41 | data: { 42 | dailyLottery: amount, 43 | }, 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /prisma/migrations/20231218174018_images/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `WholesomeImage` table. If the table is not empty, all the data it contains will be lost. 5 | - You are about to drop the `WholesomeSuggestion` table. If the table is not empty, all the data it contains will be lost. 6 | 7 | */ 8 | -- CreateEnum 9 | CREATE TYPE "ImageType" AS ENUM ('cat', 'dog', 'capybara', 'wholesome'); 10 | 11 | -- DropTable 12 | DROP TABLE "WholesomeImage"; 13 | 14 | -- DropTable 15 | DROP TABLE "WholesomeSuggestion"; 16 | 17 | -- CreateTable 18 | CREATE TABLE "Image" ( 19 | "id" SERIAL NOT NULL, 20 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 21 | "url" TEXT NOT NULL, 22 | "type" "ImageType" NOT NULL, 23 | "uploaderId" TEXT, 24 | "accepterId" TEXT, 25 | "views" INTEGER NOT NULL DEFAULT 0, 26 | 27 | CONSTRAINT "Image_pkey" PRIMARY KEY ("id") 28 | ); 29 | 30 | -- CreateTable 31 | CREATE TABLE "ImageSuggestion" ( 32 | "id" SERIAL NOT NULL, 33 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 34 | "url" TEXT NOT NULL, 35 | "type" "ImageType" NOT NULL, 36 | "uploaderId" TEXT NOT NULL, 37 | 38 | CONSTRAINT "ImageSuggestion_pkey" PRIMARY KEY ("id") 39 | ); 40 | -------------------------------------------------------------------------------- /src/commands/karmahelp.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders.js"; 4 | import { getPrefix } from "../utils/functions/guilds/utils"; 5 | 6 | const cmd = new Command("karmahelp", "help about the karma system", "info"); 7 | 8 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 9 | const embed = new CustomEmbed(message.member).setTitle("karma help"); 10 | 11 | embed.setDescription( 12 | "karma is an xp-like system that rewards you for simply using nypsi\n\ninteraction with nypsi in different ways rewards you with different amounts of karma\n\nif you stop using nypsi for a period of time, your karma will deteriorate over time\n\n**what is karma used for?**\noccasionally, the karma shop will be opened, allowing you to buy things with your karma, such as premium membership, xp, crates and rare items\n\nhttps://nypsi.xyz/docs/economy/karma?ref=bot-karmahelp", 13 | ); 14 | 15 | embed.setFooter({ text: `${(await getPrefix(message.guild))[0]}karmashop` }); 16 | 17 | return message.channel.send({ embeds: [embed] }); 18 | } 19 | 20 | cmd.setRun(run); 21 | 22 | module.exports = cmd; 23 | -------------------------------------------------------------------------------- /src/utils/functions/random.ts: -------------------------------------------------------------------------------- 1 | import { randomInt } from "crypto"; 2 | 3 | export function shuffle(array: T[]): T[] { 4 | const arr = [...array]; 5 | 6 | for (let i = arr.length - 1; i > 0; i--) { 7 | const j = randomInt(i + 1); 8 | const temp = arr[i]; 9 | arr[i] = arr[j]; 10 | arr[j] = temp; 11 | } 12 | return arr; 13 | } 14 | 15 | export function percentChance(percent: number) { 16 | if (percent <= 0) return false; 17 | let max = 100; 18 | 19 | while (percent < 1 || Boolean(percent % 1)) { 20 | max *= 10; 21 | percent *= 10; 22 | } 23 | 24 | if (percent >= Math.floor(Math.random() * max) + 1) return true; 25 | return false; 26 | } 27 | 28 | // proves the above function is accurate at producing percent chances 29 | // function test() { 30 | // let yes = 0; 31 | 32 | // console.time(); 33 | // for (let i = 0; i < 100_000_000; i++) { 34 | // if (percentChance(1.3)) yes++; 35 | // } 36 | // console.timeEnd(); 37 | 38 | // console.log(`${(yes / 100_000_000) * 100}%`); 39 | // } 40 | // test(); 41 | 42 | // rounds a number up or down randomly, 43 | // fraction part is chance of rounding up 44 | export function randomRound(n: number): number { 45 | const lower = Math.floor(n); 46 | return lower + +(Math.random() < n - lower); 47 | } 48 | -------------------------------------------------------------------------------- /src/utils/functions/openai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | import redis from "../../init/redis"; 3 | import Constants from "../Constants"; 4 | import { logger } from "../logger"; 5 | import ms = require("ms"); 6 | 7 | const openai = new OpenAI({ apiKey: process.env.OPENAI_KEY }); 8 | 9 | export default openai; 10 | 11 | export async function prompt(instructions: string, text: string) { 12 | try { 13 | const response = await openai.responses.create({ 14 | model: "gpt-5-nano", 15 | instructions, 16 | input: text, 17 | }); 18 | 19 | return response.output_text; 20 | } catch (e) { 21 | console.error(e); 22 | logger.error("openai prompt error", e); 23 | return "failed to prompt gpt"; 24 | } 25 | } 26 | 27 | export async function getDocsRaw() { 28 | try { 29 | const cache = await redis.get(Constants.redis.nypsi.DOCS_CONTENT); 30 | 31 | if (cache) { 32 | return cache; 33 | } 34 | 35 | const docs = await fetch("https://nypsi.xyz/llms.txt").then((res) => res.text()); 36 | 37 | await redis.set(Constants.redis.nypsi.DOCS_CONTENT, docs, "EX", ms("1 day") / 1000); 38 | 39 | return docs; 40 | } catch (e) { 41 | console.error(e); 42 | logger.error(`openai: failed to get llms docs`); 43 | return ""; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /prisma/migrations/20221223210813_counters_update/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `GuildCounter` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - You are about to drop the column `enabled` on the `GuildCounter` table. All the data in the column will be lost. 6 | - You are about to drop the column `filterBots` on the `GuildCounter` table. All the data in the column will be lost. 7 | - A unique constraint covering the columns `[channel]` on the table `GuildCounter` will be added. If there are existing duplicate values, this will fail. 8 | 9 | */ 10 | -- CreateEnum 11 | CREATE TYPE "TrackingType" AS ENUM ('MEMBERS', 'HUMANS', 'BOOSTS', 'RICHEST_MEMBER', 'TOTAL_ITEM', 'TOTAL_BALANCE'); 12 | 13 | -- DropIndex 14 | DROP INDEX "GuildCounter_guildId_key"; 15 | 16 | -- AlterTable 17 | ALTER TABLE "GuildCounter" DROP CONSTRAINT "GuildCounter_pkey", 18 | DROP COLUMN "enabled", 19 | DROP COLUMN "filterBots", 20 | ADD COLUMN "totalItem" TEXT, 21 | ADD COLUMN "tracks" "TrackingType" NOT NULL DEFAULT 'HUMANS', 22 | ALTER COLUMN "format" SET DEFAULT 'members: %value%', 23 | ALTER COLUMN "channel" DROP DEFAULT; 24 | 25 | -- CreateIndex 26 | CREATE UNIQUE INDEX "GuildCounter_channel_key" ON "GuildCounter"("channel"); 27 | -------------------------------------------------------------------------------- /src/commands/reload.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction, Message } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { hasAdminPermission } from "../utils/functions/users/admin"; 4 | import { loadCommands, reloadCommand } from "../utils/handlers/commandhandler"; 5 | import { reloadInteractions } from "../utils/handlers/interactions"; 6 | import { logger } from "../utils/logger"; 7 | 8 | const cmd = new Command("reload", "reload commands", "none").setPermissions(["bot owner"]); 9 | 10 | async function run( 11 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 12 | send: SendMessage, 13 | args: string[], 14 | ) { 15 | if (!(await hasAdminPermission(message.member, "reload"))) return; 16 | 17 | if (args.length == 0) { 18 | loadCommands(); 19 | if (message instanceof Message) { 20 | message.react("✅"); 21 | } 22 | logger.info("commands reloaded"); 23 | } else if (args[0] === "interactions") { 24 | reloadInteractions(); 25 | if (message instanceof Message) { 26 | message.react("✅"); 27 | } 28 | } else { 29 | const res = reloadCommand(args); 30 | if (message instanceof Message) message.react(res ? "✅" : "❌"); 31 | } 32 | } 33 | 34 | cmd.setRun(run); 35 | 36 | module.exports = cmd; 37 | -------------------------------------------------------------------------------- /src/commands/inspiration.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders"; 4 | import { isImageUrl } from "../utils/functions/image"; 5 | import { addCooldown, getResponse, onCooldown } from "../utils/handlers/cooldownhandler"; 6 | 7 | const cmd = new Command( 8 | "inspiration", 9 | "generate an inspirational quote (inspirobot.me)", 10 | "fun", 11 | ).setAliases(["quote", "inspire"]); 12 | 13 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 14 | if (await onCooldown(cmd.name, message.member)) { 15 | const res = await getResponse(cmd.name, message.member); 16 | 17 | if (res.respond) message.channel.send({ embeds: [res.embed] }); 18 | return; 19 | } 20 | 21 | await addCooldown(cmd.name, message.member, 10); 22 | 23 | const res = await fetch("https://inspirobot.me/api?generate=true").then((res) => res.text()); 24 | 25 | if (!isImageUrl(res)) { 26 | return message.channel.send({ embeds: [new ErrorEmbed("error fetching image")] }); 27 | } 28 | 29 | return message.channel.send({ embeds: [new CustomEmbed(message.member).setImage(res)] }); 30 | } 31 | 32 | cmd.setRun(run); 33 | 34 | module.exports = cmd; 35 | -------------------------------------------------------------------------------- /test/lootPools.test.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require("fs"); 2 | 3 | const items = JSON.parse(readFileSync("data/items.json")); 4 | const lootPools = JSON.parse(readFileSync("data/loot_pools.json")); 5 | 6 | for (const poolId in lootPools) { 7 | test(poolId, () => { 8 | const pool = lootPools[poolId]; 9 | if (pool.nothing) { 10 | expect(Number(pool.nothing)).toBeGreaterThan(0); 11 | } 12 | for (const amount in pool.money) { 13 | expect(Number(amount)).toBeGreaterThan(0); 14 | expect(Number(pool.money[amount])).toBeGreaterThan(0); 15 | } 16 | for (const amount in pool.xp) { 17 | expect(Number(amount)).toBeGreaterThan(0); 18 | expect(Number(pool.xp[amount])).toBeGreaterThan(0); 19 | } 20 | for (const amount in pool.karma) { 21 | expect(Number(amount)).toBeGreaterThan(0); 22 | expect(Number(pool.karma[amount])).toBeGreaterThan(0); 23 | } 24 | for (const itemKey in pool.items) { 25 | const itemValue = pool.items[itemKey]; 26 | expect(items[itemKey]).toBeDefined(); 27 | if (typeof itemValue === "number") { 28 | expect(Number(itemValue)).toBeGreaterThan(0); 29 | } 30 | if (typeof itemValue === "object" && itemValue.weight) { 31 | expect(Number(itemValue.weight)).toBeGreaterThan(0); 32 | } 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/garden.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { sort } from "fast-sort"; 3 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 4 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders.js"; 5 | import { getInventory } from "../utils/functions/economy/inventory"; 6 | import { getItems } from "../utils/functions/economy/utils"; 7 | 8 | const cmd = new Command("garden", "look at all your pretty flowers", "money"); 9 | 10 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 11 | const inventory = await getInventory(message.member); 12 | 13 | const flowers = inventory.entries.filter((i) => getItems()[i.item].role === "flower"); 14 | 15 | if (flowers.length === 0) 16 | return message.channel.send({ embeds: [new ErrorEmbed("you have no flowers ):")] }); 17 | 18 | const embed = new CustomEmbed( 19 | message.member, 20 | sort(flowers) 21 | .asc((i) => getItems()[i.item].name) 22 | .map((i) => `\`${i.amount}x\` ${getItems()[i.item].emoji} **${getItems()[i.item].name}**`) 23 | .join("\n"), 24 | ).setHeader(`${message.author.username}'s garden`, message.author.avatarURL()); 25 | 26 | return message.channel.send({ embeds: [embed] }); 27 | } 28 | 29 | cmd.setRun(run); 30 | 31 | module.exports = cmd; 32 | -------------------------------------------------------------------------------- /src/commands/reloaditems.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction, Message } from "discord.js"; 2 | import { exec } from "node:child_process"; 3 | import prisma from "../init/database"; 4 | import { NypsiClient } from "../models/Client"; 5 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 6 | import { getTasksData, loadItems } from "../utils/functions/economy/utils"; 7 | import { hasAdminPermission } from "../utils/functions/users/admin"; 8 | import { logger } from "../utils/logger"; 9 | 10 | const cmd = new Command("reloaditems", "reload items", "none").setPermissions(["bot owner"]); 11 | 12 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 13 | if (!(await hasAdminPermission(message.member, "reload"))) return; 14 | 15 | loadItems(); 16 | (message.client as NypsiClient).cluster.send("reload_items"); 17 | 18 | prisma.task 19 | .deleteMany({ 20 | where: { 21 | task_id: { notIn: Object.values(getTasksData()).map((i) => i.id) }, 22 | }, 23 | }) 24 | .then((count) => { 25 | if (count.count > 0) logger.info(`${count.count} invalid tasks deleted`); 26 | }); 27 | 28 | exec(`redis-cli KEYS "*economy:task*" | xargs redis-cli DEL`); 29 | 30 | return (message as Message).react("✅"); 31 | } 32 | 33 | cmd.setRun(run); 34 | 35 | module.exports = cmd; 36 | -------------------------------------------------------------------------------- /src/utils/functions/premium/color.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../../init/database"; 2 | import redis from "../../../init/redis"; 3 | import Constants from "../../Constants"; 4 | import { getUserId, MemberResolvable } from "../member"; 5 | 6 | export async function setEmbedColor(member: MemberResolvable, color: string) { 7 | const userId = getUserId(member); 8 | 9 | await prisma.premium.update({ 10 | where: { 11 | userId, 12 | }, 13 | data: { 14 | embedColor: color, 15 | }, 16 | }); 17 | 18 | await redis.del(`${Constants.redis.cache.premium.COLOR}:${userId}`); 19 | } 20 | 21 | export async function getEmbedColor(member: MemberResolvable): Promise<`#${string}` | "default"> { 22 | const userId = getUserId(member); 23 | 24 | const cache = await redis.get(`${Constants.redis.cache.premium.COLOR}:${userId}`); 25 | 26 | if (cache) return cache as `#${string}` | "default"; 27 | 28 | const query = await prisma.premium.findFirst({ 29 | where: { 30 | AND: [{ userId }, { level: { gt: 0 } }], 31 | }, 32 | select: { 33 | embedColor: true, 34 | }, 35 | }); 36 | 37 | await redis.set( 38 | `${Constants.redis.cache.premium.COLOR}:${userId}`, 39 | query?.embedColor || "default", 40 | "EX", 41 | 3600, 42 | ); 43 | 44 | return (query?.embedColor as `#${string}` | "default") || "default"; 45 | } 46 | -------------------------------------------------------------------------------- /src/interactions/reaction_role.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getReactionRolesByGuild } from "../utils/functions/guilds/reactionroles"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default { 6 | name: "reaction-role", 7 | type: "autocomplete", 8 | async run(interaction) { 9 | const focused = interaction.options.getFocused(true); 10 | focused.value = focused.value.toLowerCase(); 11 | 12 | const reactionRoles = await getReactionRolesByGuild(interaction.guild); 13 | 14 | const filtered = reactionRoles.filter( 15 | (rr) => 16 | rr.messageId.includes(focused.value) || 17 | rr.description?.includes(focused.value) || 18 | rr.title?.includes(focused.value), 19 | ); 20 | 21 | return interaction 22 | .respond( 23 | filtered.map((rr) => { 24 | let title = rr.title; 25 | 26 | if (title?.length > 20) title = title.substring(0, 20) + "..."; 27 | 28 | return { name: title ? `${title} (${rr.messageId})` : rr.messageId, value: rr.messageId }; 29 | }), 30 | ) 31 | .catch(() => { 32 | logger.warn(`failed to respond to autocomplete in time`, { 33 | userId: interaction.user.id, 34 | command: interaction.commandName, 35 | }); 36 | }); 37 | }, 38 | } as AutocompleteHandler; 39 | -------------------------------------------------------------------------------- /src/utils/functions/economy/passive.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../../init/database"; 2 | import redis from "../../../init/redis"; 3 | import Constants from "../../Constants"; 4 | import { getUserId, MemberResolvable } from "../member"; 5 | import { userExists } from "./utils"; 6 | import ms = require("ms"); 7 | 8 | export async function isPassive(member: MemberResolvable) { 9 | if (!(await userExists(member))) return false; 10 | 11 | const userId = getUserId(member); 12 | 13 | const cache = await redis.get(`${Constants.redis.cache.economy.PASSIVE}:${userId}`); 14 | 15 | if (cache) { 16 | return cache == "t"; 17 | } 18 | 19 | const query = await prisma.economy.findUnique({ 20 | where: { 21 | userId, 22 | }, 23 | select: { 24 | passive: true, 25 | }, 26 | }); 27 | 28 | await redis.set( 29 | `${Constants.redis.cache.economy.PASSIVE}:${userId}`, 30 | query.passive ? "t" : "f", 31 | "EX", 32 | ms("24 hours") / 1000, 33 | ); 34 | 35 | return query.passive; 36 | } 37 | 38 | export async function setPassive(member: MemberResolvable, value: boolean) { 39 | const userId = getUserId(member); 40 | 41 | await prisma.economy.update({ 42 | where: { 43 | userId, 44 | }, 45 | data: { 46 | passive: value, 47 | }, 48 | }); 49 | 50 | await redis.del(`${Constants.redis.cache.economy.PASSIVE}:${userId}`); 51 | } 52 | -------------------------------------------------------------------------------- /src/scheduled/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { CronJob } from "cron"; 2 | import { readdir } from "fs/promises"; 3 | import { Job } from "../types/Jobs"; 4 | import { logger } from "../utils/logger"; 5 | import { ClusterManager } from "discord-hybrid-sharding"; 6 | 7 | const jobs = new Map void }>(); 8 | 9 | export async function loadJobs(manager: ClusterManager) { 10 | for (const { job } of jobs.values()) { 11 | job.stop(); 12 | } 13 | jobs.clear(); 14 | const files = await readdir("dist/scheduled/jobs"); 15 | 16 | for (const file of files) { 17 | delete require.cache[require.resolve(`./jobs/${file}`)]; 18 | const imported = await import(`./jobs/${file}`).then((i) => i.default as Job); 19 | 20 | if (!imported) continue; 21 | 22 | const run = async () => { 23 | logger.info(`[${imported.name}] job started`); 24 | await imported.run( 25 | (message: string) => logger.info(`[${imported.name}] ${message}`), 26 | manager, 27 | ); 28 | logger.info(`[${imported.name}] job finished`); 29 | }; 30 | 31 | jobs.set(imported.name, { 32 | name: imported.name, 33 | job: new CronJob(imported.cron, run, null, true), 34 | run, 35 | }); 36 | } 37 | 38 | logger.info(`${jobs.size} jobs loaded`); 39 | } 40 | 41 | export function runJob(job: string) { 42 | jobs.get(job)?.run(); 43 | } 44 | -------------------------------------------------------------------------------- /prisma/migrations/20250711150239_events/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Event" ( 3 | "id" SERIAL NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "ownerId" TEXT NOT NULL, 6 | "completed" BOOLEAN NOT NULL DEFAULT false, 7 | "completedAt" TIMESTAMP(3), 8 | "type" TEXT NOT NULL, 9 | "target" BIGINT NOT NULL, 10 | "expiresAt" TIMESTAMP(3) NOT NULL, 11 | 12 | CONSTRAINT "Event_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateTable 16 | CREATE TABLE "EventContribution" ( 17 | "userId" TEXT NOT NULL, 18 | "eventId" INTEGER NOT NULL, 19 | "contribution" BIGINT NOT NULL, 20 | 21 | CONSTRAINT "EventContribution_pkey" PRIMARY KEY ("userId","eventId") 22 | ); 23 | 24 | -- CreateIndex 25 | CREATE INDEX "EventContribution_eventId_idx" ON "EventContribution"("eventId"); 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "Event" ADD CONSTRAINT "Event_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "EventContribution" ADD CONSTRAINT "EventContribution_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "EventContribution" ADD CONSTRAINT "EventContribution_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE; 35 | -------------------------------------------------------------------------------- /src/commands/bitcoin.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | import { getInventory } from "../utils/functions/economy/inventory"; 5 | import { createUser, getItems, userExists } from "../utils/functions/economy/utils"; 6 | 7 | const cmd = new Command( 8 | "bitcoin", 9 | "view the current bitcoin value (reflects real life USD)", 10 | "money", 11 | ).setAliases(["btc"]); 12 | 13 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 14 | if (!(await userExists(message.member))) await createUser(message.member); 15 | const bitcoin = getItems()["bitcoin"]; 16 | const inventory = await getInventory(message.member); 17 | 18 | const bitcoinAmount = inventory.count("bitcoin"); 19 | 20 | const embed = new CustomEmbed( 21 | message.member, 22 | `**worth** $${bitcoin.sell.toLocaleString()}\n**owned** ${bitcoinAmount.toLocaleString()} ($${( 23 | bitcoinAmount * bitcoin.sell 24 | ).toLocaleString()})`, 25 | ) 26 | .setFooter({ text: "not real bitcoin, although it reflects current worth in USD" }) 27 | .setHeader("your bitcoin", message.author.avatarURL()); 28 | 29 | return message.channel.send({ embeds: [embed] }); 30 | } 31 | 32 | cmd.setRun(run); 33 | 34 | module.exports = cmd; 35 | -------------------------------------------------------------------------------- /src/commands/ethereum.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | import { getInventory } from "../utils/functions/economy/inventory"; 5 | import { createUser, getItems, userExists } from "../utils/functions/economy/utils"; 6 | 7 | const cmd = new Command( 8 | "ethereum", 9 | "view the current ethereum value (reflects real life USD)", 10 | "money", 11 | ).setAliases(["eth"]); 12 | 13 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 14 | if (!(await userExists(message.member))) await createUser(message.member); 15 | const ethereum = getItems()["ethereum"]; 16 | const inventory = await getInventory(message.member); 17 | 18 | const ethereumAmount = inventory.count("ethereum"); 19 | 20 | const embed = new CustomEmbed( 21 | message.member, 22 | `**worth** $${ethereum.sell.toLocaleString()}\n**owned** ${ethereumAmount.toLocaleString()} ($${( 23 | ethereumAmount * ethereum.sell 24 | ).toLocaleString()})`, 25 | ) 26 | .setFooter({ text: "not real ethereum, although it reflects current worth in USD" }) 27 | .setHeader("your ethereum", message.author.avatarURL()); 28 | 29 | return message.channel.send({ embeds: [embed] }); 30 | } 31 | 32 | cmd.setRun(run); 33 | 34 | module.exports = cmd; 35 | -------------------------------------------------------------------------------- /data/upgrades.json: -------------------------------------------------------------------------------- 1 | { 2 | "multi": { 3 | "id": "multi", 4 | "name": "gamble multiplier", 5 | "effect": 1, 6 | "description": "+{x}% gamble multiplier", 7 | "max": 5, 8 | "chance": 5 9 | }, 10 | "sell_multi": { 11 | "id": "sell_multi", 12 | "name": "sell multiplier", 13 | "effect": 3, 14 | "description": "+{x}% sell multiplier", 15 | "max": 15, 16 | "chance": 15 17 | }, 18 | "cursor": { 19 | "id": "cursor", 20 | "name": "cursor booster", 21 | "effect": 0.1, 22 | "description": "+{x} cookies per cursor", 23 | "max": 20, 24 | "chance": 40 25 | }, 26 | "grandma": { 27 | "id": "grandma", 28 | "name": "grandma booster", 29 | "effect": 0.1, 30 | "description": "+{x} cookies per grandma", 31 | "max": 20, 32 | "chance": 40 33 | }, 34 | "xp": { 35 | "id": "xp", 36 | "name": "xp booster", 37 | "effect": 0.05, 38 | "description": "+{x}% more xp", 39 | "max": 5, 40 | "chance": 5 41 | }, 42 | "guild_xp": { 43 | "id": "guild_xp", 44 | "name": "guild xp booster", 45 | "effect": 0.03, 46 | "description": "+{x}% xp sent to guild", 47 | "max": 5, 48 | "chance": 20 49 | }, 50 | "farm_output": { 51 | "id": "farm_output", 52 | "name": "farm output booster", 53 | "effect": 0.02, 54 | "description": "+{x}% farm output", 55 | "max": 10, 56 | "chance": 15 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /prisma/migrations/20241214122729_z/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "z" ( 3 | "userId" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "hasInvite" BOOLEAN NOT NULL DEFAULT false, 6 | "removed" BOOLEAN NOT NULL DEFAULT false, 7 | "rating" SMALLINT NOT NULL DEFAULT 0, 8 | "voteKickId" INTEGER[] DEFAULT ARRAY[]::INTEGER[], 9 | "invitedById" TEXT, 10 | 11 | CONSTRAINT "z_pkey" PRIMARY KEY ("userId") 12 | ); 13 | 14 | -- CreateTable 15 | CREATE TABLE "zKicks" ( 16 | "userId" TEXT NOT NULL, 17 | "targetId" TEXT NOT NULL, 18 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | 20 | CONSTRAINT "zKicks_pkey" PRIMARY KEY ("userId","targetId") 21 | ); 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "z" ADD CONSTRAINT "z_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "z" ADD CONSTRAINT "z_invitedById_fkey" FOREIGN KEY ("invitedById") REFERENCES "z"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 28 | 29 | -- AddForeignKey 30 | ALTER TABLE "zKicks" ADD CONSTRAINT "zKicks_userId_fkey" FOREIGN KEY ("userId") REFERENCES "z"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "zKicks" ADD CONSTRAINT "zKicks_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "z"("userId") ON DELETE CASCADE ON UPDATE CASCADE; 34 | -------------------------------------------------------------------------------- /src/scheduled/jobs/netupdate-2.ts: -------------------------------------------------------------------------------- 1 | import dayjs = require("dayjs"); 2 | import prisma from "../../init/database"; 3 | import redis from "../../init/redis"; 4 | import { Job } from "../../types/Jobs"; 5 | import Constants from "../../utils/Constants"; 6 | import { MStoTime } from "../../utils/functions/date"; 7 | import { calcNetWorth } from "../../utils/functions/economy/balance"; 8 | 9 | export default { 10 | name: "netupdate-2", 11 | cron: "0 4 * * *", 12 | async run(log, manager) { 13 | const start = Date.now(); 14 | const query = await prisma.economy.findMany({ 15 | select: { 16 | userId: true, 17 | }, 18 | where: { 19 | AND: [ 20 | { 21 | user: { 22 | lastCommand: { gt: dayjs().subtract(7, "day").toDate() }, 23 | }, 24 | }, 25 | { 26 | user: { 27 | lastCommand: { lt: dayjs().subtract(1, "day").toDate() }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | }); 33 | 34 | let count = 0; 35 | 36 | for (const user of query) { 37 | if (await redis.exists(`${Constants.redis.cache.economy.NETWORTH}:${user.userId}`)) continue; 38 | 39 | await calcNetWorth("job", user.userId, manager); 40 | count++; 41 | } 42 | 43 | log( 44 | `net worth updated for ${count.toLocaleString()} members in ${MStoTime(Date.now() - start)}`, 45 | ); 46 | }, 47 | } satisfies Job; 48 | -------------------------------------------------------------------------------- /src/utils/functions/guilds/altpunish.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from "discord.js"; 2 | import prisma from "../../../init/database"; 3 | import redis from "../../../init/redis"; 4 | import Constants from "../../Constants"; 5 | import ms = require("ms"); 6 | 7 | export async function isAltPunish(guild: Guild) { 8 | if (await redis.exists(`${Constants.redis.cache.guild.ALT_PUNISH}:${guild.id}`)) { 9 | return (await redis.get(`${Constants.redis.cache.guild.ALT_PUNISH}:${guild.id}`)) === "t"; 10 | } 11 | 12 | const res = await prisma.guild 13 | .findUnique({ 14 | where: { 15 | id: guild.id, 16 | }, 17 | select: { 18 | alt_punish: true, 19 | }, 20 | }) 21 | .then((q) => q.alt_punish); 22 | 23 | if (res) { 24 | await redis.set( 25 | `${Constants.redis.cache.guild.ALT_PUNISH}:${guild.id}`, 26 | "t", 27 | "EX", 28 | ms("24 hour") / 1000, 29 | ); 30 | } else { 31 | await redis.set( 32 | `${Constants.redis.cache.guild.ALT_PUNISH}:${guild.id}`, 33 | "f", 34 | "EX", 35 | ms("24 hour") / 1000, 36 | ); 37 | } 38 | 39 | return res; 40 | } 41 | 42 | export async function setAltPunish(guild: Guild, bool: boolean) { 43 | await prisma.guild.update({ 44 | where: { 45 | id: guild.id, 46 | }, 47 | data: { 48 | alt_punish: bool, 49 | }, 50 | }); 51 | 52 | await redis.del(`${Constants.redis.cache.guild.ALT_PUNISH}:${guild.id}`); 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/functions/guilds/slash.ts: -------------------------------------------------------------------------------- 1 | import { Guild } from "discord.js"; 2 | import prisma from "../../../init/database"; 3 | import redis from "../../../init/redis"; 4 | import Constants from "../../Constants"; 5 | import ms = require("ms"); 6 | 7 | export async function isSlashOnly(guild: Guild) { 8 | if (await redis.exists(`${Constants.redis.cache.guild.SLASH_ONLY}:${guild.id}`)) { 9 | return (await redis.get(`${Constants.redis.cache.guild.SLASH_ONLY}:${guild.id}`)) === "t"; 10 | } 11 | 12 | const res = await prisma.guild 13 | .findUnique({ 14 | where: { 15 | id: guild.id, 16 | }, 17 | select: { 18 | slash_only: true, 19 | }, 20 | }) 21 | .then((q) => q.slash_only); 22 | 23 | if (res) { 24 | await redis.set( 25 | `${Constants.redis.cache.guild.SLASH_ONLY}:${guild.id}`, 26 | "t", 27 | "EX", 28 | ms("24 hour") / 1000, 29 | ); 30 | } else { 31 | await redis.set( 32 | `${Constants.redis.cache.guild.SLASH_ONLY}:${guild.id}`, 33 | "f", 34 | "EX", 35 | ms("24 hour") / 1000, 36 | ); 37 | } 38 | 39 | return res; 40 | } 41 | 42 | export async function setSlashOnly(guild: Guild, bool: boolean) { 43 | await prisma.guild.update({ 44 | where: { 45 | id: guild.id, 46 | }, 47 | data: { 48 | slash_only: bool, 49 | }, 50 | }); 51 | 52 | await redis.del(`${Constants.redis.cache.guild.SLASH_ONLY}:${guild.id}`); 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/functions/users/mentions.ts: -------------------------------------------------------------------------------- 1 | import { Mention } from "#generated/prisma"; 2 | import prisma from "../../../init/database"; 3 | import { getUserId, MemberResolvable } from "../member"; 4 | 5 | export interface MentionQueueItem { 6 | members: string[]; 7 | channelMembers: string[]; 8 | content: string; 9 | url: string; 10 | username: string; 11 | date: number; 12 | guildId: string; 13 | } 14 | 15 | export async function fetchUserMentions( 16 | member: MemberResolvable, 17 | global: true | string, 18 | amount = 100, 19 | ) { 20 | const userId = getUserId(member); 21 | 22 | let mentions: Mention[]; 23 | 24 | if (typeof global === "boolean" && global) { 25 | mentions = await prisma.mention.findMany({ 26 | where: { 27 | targetId: userId, 28 | }, 29 | orderBy: { 30 | id: "desc", 31 | }, 32 | take: amount, 33 | }); 34 | } else if (typeof global === "string") { 35 | mentions = await prisma.mention.findMany({ 36 | where: { 37 | AND: [{ guildId: global }, { targetId: userId }], 38 | }, 39 | orderBy: { 40 | id: "desc", 41 | }, 42 | take: amount, 43 | }); 44 | } 45 | 46 | return mentions; 47 | } 48 | 49 | export async function deleteUserMentions(member: MemberResolvable, guildId?: string) { 50 | await prisma.mention.deleteMany({ 51 | where: { 52 | AND: [{ guildId: guildId }, { targetId: getUserId(member) }], 53 | }, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/interactions/item_global.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getItems } from "../utils/functions/economy/utils"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default { 6 | name: "item-global", 7 | type: "autocomplete", 8 | async run(interaction) { 9 | const focused = interaction.options.getFocused(true); 10 | focused.value = focused.value.toLowerCase(); 11 | 12 | const items = getItems(); 13 | 14 | let options = Object.keys(items).filter( 15 | (item) => 16 | item.includes(focused.value) || 17 | items[item].name.includes(focused.value) || 18 | items[item].aliases?.includes(focused.value), 19 | ); 20 | 21 | if (options.length > 25) options = options.splice(0, 24); 22 | 23 | if (options.length == 0) return interaction.respond([]); 24 | 25 | const formatted = options.map((i) => ({ 26 | name: `${ 27 | items[i].emoji.startsWith("<:") || 28 | items[i].emoji.startsWith(" { 37 | logger.warn(`failed to respond to autocomplete in time`, { 38 | userId: interaction.user.id, 39 | command: interaction.commandName, 40 | }); 41 | }); 42 | }, 43 | } as AutocompleteHandler; 44 | -------------------------------------------------------------------------------- /src/events/messageDeleteBulk.ts: -------------------------------------------------------------------------------- 1 | import { Collection, Message, Snowflake, TextBasedChannel } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { addLog, isLogsEnabled } from "../utils/functions/moderation/logs"; 4 | import { logger } from "../utils/logger"; 5 | import dayjs = require("dayjs"); 6 | 7 | export default async function messageDeleteBulk( 8 | messages: Collection, 9 | channel: TextBasedChannel, 10 | ) { 11 | if (channel.isDMBased()) return; 12 | 13 | if (await isLogsEnabled(channel.guild)) { 14 | logger.debug("logs enabled"); 15 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 16 | 17 | embed.setTitle(`${messages.size} messages deleted in #${channel.name} [bulk delete]`); 18 | 19 | const desc: string[] = []; 20 | 21 | messages.each((message) => { 22 | if (message.content) { 23 | desc.push( 24 | `[${dayjs(message.createdTimestamp).format("YYYY-MM-DD HH:mm:ss")}] ${ 25 | message.author.username 26 | }: ${message.content}`, 27 | ); 28 | } 29 | 30 | if (desc.join("\n").length > 1500) { 31 | desc.push("..."); 32 | return false; 33 | } 34 | return true; 35 | }); 36 | 37 | desc.reverse(); 38 | 39 | embed.setDescription(`\`\`\`${desc.join("\n")}\`\`\``); 40 | 41 | logger.debug(`formatted`); 42 | 43 | await addLog(channel.guild, "message", embed); 44 | logger.debug("added logs"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/interactions/item_buy.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getItems } from "../utils/functions/economy/utils"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default { 6 | name: "item-buy", 7 | type: "autocomplete", 8 | async run(interaction) { 9 | const focused = interaction.options.getFocused(true); 10 | focused.value = focused.value.toLowerCase(); 11 | 12 | const items = getItems(); 13 | 14 | let options = Object.keys(items).filter( 15 | (item) => 16 | (item.includes(focused.value) || 17 | items[item].name.includes(focused.value) || 18 | items[item].aliases?.includes(focused.value)) && 19 | items[item].buy, 20 | ); 21 | 22 | if (options.length > 25) options = options.splice(0, 24); 23 | 24 | if (options.length == 0) return interaction.respond([]); 25 | 26 | const formatted = options.map((i) => ({ 27 | name: `${ 28 | items[i].emoji.startsWith("<:") || 29 | items[i].emoji.startsWith(" { 38 | logger.warn(`failed to respond to autocomplete in time`, { 39 | userId: interaction.user.id, 40 | command: interaction.commandName, 41 | }); 42 | }); 43 | }, 44 | } as AutocompleteHandler; 45 | -------------------------------------------------------------------------------- /src/commands/news.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders"; 4 | import Constants from "../utils/Constants"; 5 | import { formatDate } from "../utils/functions/date"; 6 | import { getNews, setNews } from "../utils/functions/news"; 7 | 8 | const cmd = new Command("news", "set the news for the help command", "info"); 9 | 10 | async function run( 11 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 12 | send: SendMessage, 13 | args: string[], 14 | ) { 15 | if (args.length == 0 || message.author.id != Constants.OWNER_ID) { 16 | const news = await getNews(); 17 | 18 | if (news.text == "") { 19 | return send({ embeds: [new ErrorEmbed("no news has been set")] }); 20 | } 21 | 22 | const lastSet = formatDate(news.date); 23 | 24 | const embed = new CustomEmbed(message.member, `${news.text}\n\nset on: ${lastSet}`); 25 | 26 | return send({ embeds: [embed] }); 27 | } else { 28 | if (message.author.id != Constants.OWNER_ID) return; 29 | await setNews(args.join(" ")); 30 | 31 | const news = await getNews(); 32 | 33 | const lastSet = formatDate(news.date); 34 | 35 | const embed = new CustomEmbed(message.member, `${news.text}\n\nset on: ${lastSet}`); 36 | 37 | return send({ embeds: [embed] }); 38 | } 39 | } 40 | 41 | cmd.setRun(run); 42 | 43 | module.exports = cmd; 44 | -------------------------------------------------------------------------------- /src/interactions/craft_item.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getItems } from "../utils/functions/economy/utils"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default { 6 | name: "craft-item", 7 | type: "autocomplete", 8 | async run(interaction) { 9 | const focused = interaction.options.getFocused(true); 10 | focused.value = focused.value.toLowerCase(); 11 | 12 | const items = getItems(); 13 | 14 | let options = Object.keys(items).filter( 15 | (item) => 16 | items[item].craft && 17 | (item.includes(focused.value) || 18 | items[item].name.includes(focused.value) || 19 | items[item].aliases?.includes(focused.value)), 20 | ); 21 | 22 | if (options.length > 25) options = options.splice(0, 24); 23 | 24 | if (options.length == 0) return interaction.respond([]); 25 | 26 | const formatted = options.map((i) => ({ 27 | name: `${ 28 | items[i].emoji.startsWith("<:") || 29 | items[i].emoji.startsWith(" { 38 | logger.warn(`failed to respond to autocomplete in time`, { 39 | userId: interaction.user.id, 40 | command: interaction.commandName, 41 | }); 42 | }); 43 | }, 44 | } as AutocompleteHandler; 45 | -------------------------------------------------------------------------------- /src/events/channelCreate.ts: -------------------------------------------------------------------------------- 1 | import { GuildChannel } from "discord.js"; 2 | import { CustomEmbed } from "../models/EmbedBuilders"; 3 | import { addLog, isLogsEnabled } from "../utils/functions/moderation/logs"; 4 | import { getMuteRole } from "../utils/functions/moderation/mute"; 5 | 6 | export default async function channelCreate(channel: GuildChannel) { 7 | if (!channel.guild) return; 8 | 9 | if (await isLogsEnabled(channel.guild)) { 10 | const embed = new CustomEmbed().disableFooter().setTimestamp(); 11 | 12 | embed.setTitle("channel created"); 13 | embed.setDescription( 14 | `${channel.toString()} \`${channel.id}\`\n\n**name** ${channel.name}\n${ 15 | channel.parent ? `**category** ${channel.parent.name}\n` : "" 16 | }**type** ${channel.type}`, 17 | ); 18 | 19 | await addLog(channel.guild, "channel", embed); 20 | } 21 | 22 | if ((await getMuteRole(channel.guild)) == "timeout") return; 23 | 24 | let muteRole = await channel.guild.roles.cache.get(await getMuteRole(channel.guild)); 25 | 26 | if (!(await getMuteRole(channel.guild))) { 27 | muteRole = channel.guild.roles.cache.find((r) => r.name.toLowerCase() == "muted"); 28 | } 29 | 30 | if (!muteRole) return; 31 | 32 | channel.permissionOverwrites 33 | .edit(muteRole, { 34 | SendMessages: false, 35 | Speak: false, 36 | AddReactions: false, 37 | SendMessagesInThreads: false, 38 | CreatePublicThreads: false, 39 | CreatePrivateThreads: false, 40 | }) 41 | .catch(() => {}); 42 | } 43 | -------------------------------------------------------------------------------- /src/commands/enlarge.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders"; 4 | import { getPrefix } from "../utils/functions/guilds/utils"; 5 | 6 | const cmd = new Command("enlarge", "enlarge a custom emoji to its full size", "utility").setAliases( 7 | ["emoji", "makebig"], 8 | ); 9 | 10 | async function run( 11 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 12 | send: SendMessage, 13 | args: string[], 14 | ) { 15 | const prefix = (await getPrefix(message.guild))[0]; 16 | 17 | if (args.length == 0) { 18 | return send({ 19 | embeds: [new ErrorEmbed(`${prefix}enlarge `).setTitle("`❌` usage")], 20 | }); 21 | } 22 | 23 | let emoji: string | string[] = args[0]; 24 | 25 | emoji = emoji.split(":"); 26 | 27 | if (!emoji[2]) { 28 | return send({ 29 | embeds: [new ErrorEmbed("invalid emoji - please use a custom emoji")], 30 | }); 31 | } 32 | 33 | const emojiID = emoji[2].slice(0, emoji[2].length - 1); 34 | 35 | let url = `https://cdn.discordapp.com/emojis/${emojiID}`; 36 | 37 | if (emoji[0].includes("a")) { 38 | url = url + ".gif"; 39 | } else { 40 | url = url + ".png"; 41 | } 42 | 43 | return send({ 44 | embeds: [new CustomEmbed(message.member).setImage(url).setFooter({ text: `id: ${emojiID}` })], 45 | }); 46 | } 47 | 48 | cmd.setRun(run); 49 | 50 | module.exports = cmd; 51 | -------------------------------------------------------------------------------- /src/interactions/achievements.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getAchievements } from "../utils/functions/economy/utils"; 3 | import { logger } from "../utils/logger"; 4 | 5 | export default { 6 | name: "achievement", 7 | type: "autocomplete", 8 | async run(interaction) { 9 | const focused = interaction.options.getFocused(true); 10 | focused.value = focused.value.toLowerCase(); 11 | 12 | const achievements = getAchievements(); 13 | 14 | let options = Object.keys(achievements).filter( 15 | (i) => 16 | i.includes(focused.value) || 17 | achievements[i].name.replaceAll("*", "").toLowerCase().includes(focused.value), 18 | ); 19 | 20 | if (options.length > 25) options = options.splice(0, 24); 21 | 22 | if (options.length == 0) return interaction.respond([]); 23 | 24 | const formatted = options.map((i) => ({ 25 | name: `${ 26 | achievements[i].emoji.startsWith("<:") || 27 | achievements[i].emoji.startsWith(" { 36 | logger.warn(`failed to respond to autocomplete in time`, { 37 | userId: interaction.user.id, 38 | command: interaction.commandName, 39 | }); 40 | }); 41 | }, 42 | } as AutocompleteHandler; 43 | -------------------------------------------------------------------------------- /prisma/migrations/20240618124410_moderation_restructure_2/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Moderation` table. If the table is not empty, all the data it contains will be lost. 5 | - Added the required column `caseId_new` to the `ModerationCase` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "Moderation" DROP CONSTRAINT "Moderation_guildId_fkey"; 10 | 11 | -- DropForeignKey 12 | ALTER TABLE "ModerationBan" DROP CONSTRAINT "ModerationBan_guildId_fkey"; 13 | 14 | -- DropForeignKey 15 | ALTER TABLE "ModerationCase" DROP CONSTRAINT "ModerationCase_guildId_fkey"; 16 | 17 | -- DropForeignKey 18 | ALTER TABLE "ModerationMute" DROP CONSTRAINT "ModerationMute_guildId_fkey"; 19 | 20 | -- AlterTable 21 | ALTER TABLE "ModerationCase" ADD COLUMN "caseId_new" INTEGER NOT NULL, 22 | ALTER COLUMN "time" SET DEFAULT CURRENT_TIMESTAMP; 23 | 24 | -- DropTable 25 | DROP TABLE "Moderation"; 26 | 27 | -- AddForeignKey 28 | ALTER TABLE "ModerationBan" ADD CONSTRAINT "ModerationBan_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE; 29 | 30 | -- AddForeignKey 31 | ALTER TABLE "ModerationMute" ADD CONSTRAINT "ModerationMute_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE; 32 | 33 | -- AddForeignKey 34 | ALTER TABLE "ModerationCase" ADD CONSTRAINT "ModerationCase_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE CASCADE ON UPDATE CASCADE; 35 | -------------------------------------------------------------------------------- /src/commands/banner.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction, GuildMember } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders.js"; 4 | import { getMember } from "../utils/functions/member"; 5 | import { escapeFormattingCharacters } from "../utils/functions/string"; 6 | 7 | const cmd = new Command("banner", "get a person's banner", "info"); 8 | 9 | async function run( 10 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 11 | send: SendMessage, 12 | args: string[], 13 | ) { 14 | let member: GuildMember; 15 | 16 | if (args.length == 0) { 17 | member = message.member; 18 | } else { 19 | member = await getMember(message.guild, args.join(" ")); 20 | } 21 | 22 | if (!member) { 23 | return send({ embeds: [new ErrorEmbed("invalid user")] }); 24 | } 25 | 26 | const user = await message.client.users.fetch(member.user.id, { force: true }); 27 | 28 | if (!user.banner) { 29 | return send({ 30 | embeds: [ 31 | new ErrorEmbed( 32 | `${member == message.member ? "you do" : `${escapeFormattingCharacters(user.username)} does`} not have a banner`, 33 | ), 34 | ], 35 | }); 36 | } 37 | 38 | const banner = user.bannerURL({ size: 512 }); 39 | 40 | const embed = new CustomEmbed(member).setHeader(member.user.username).setImage(banner); 41 | 42 | return send({ embeds: [embed] }); 43 | } 44 | 45 | cmd.setRun(run); 46 | 47 | module.exports = cmd; 48 | -------------------------------------------------------------------------------- /src/scheduled/jobs/topbalance.ts: -------------------------------------------------------------------------------- 1 | import { flavors } from "@catppuccin/palette"; 2 | import { ColorResolvable, WebhookClient } from "discord.js"; 3 | import { CustomEmbed } from "../../models/EmbedBuilders"; 4 | import { Job } from "../../types/Jobs"; 5 | import { topBalanceGlobal, topGuilds } from "../../utils/functions/economy/top"; 6 | 7 | export default { 8 | name: "top balance", 9 | cron: "0 0 * * *", 10 | async run(log) { 11 | const baltop = await topBalanceGlobal(10); 12 | const guilds = await topGuilds(); 13 | 14 | const balance = new CustomEmbed(); 15 | const guild = new CustomEmbed(); 16 | 17 | balance.setHeader( 18 | "top balance", 19 | "https://cdn.discordapp.com/avatars/678711738845102087/cb2dcd61010f2b89ceb1cd5ff15816cf.png?size=256", 20 | "https://nypsi.xyz/leaderboards/balance?ref=bot-lb", 21 | ); 22 | guild.setHeader( 23 | "top guilds", 24 | "https://cdn.discordapp.com/avatars/678711738845102087/cb2dcd61010f2b89ceb1cd5ff15816cf.png?size=256", 25 | "https://nypsi.xyz/leaderboards/guilds?ref=bot-lb", 26 | ); 27 | 28 | balance.setColor(flavors.latte.colors.base.hex as ColorResolvable); 29 | guild.setColor(flavors.latte.colors.base.hex as ColorResolvable); 30 | 31 | balance.setDescription(baltop.join("\n")); 32 | guild.setDescription(guilds.pages.get(1).join("\n")); 33 | 34 | const hook = new WebhookClient({ url: process.env.TOPGLOBAL_HOOK }); 35 | 36 | await hook.send({ embeds: [balance, guild] }); 37 | 38 | log("sent global baltop"); 39 | 40 | hook.destroy(); 41 | }, 42 | } satisfies Job; 43 | -------------------------------------------------------------------------------- /src/commands/clearsnipe.ts: -------------------------------------------------------------------------------- 1 | import { Channel, CommandInteraction, PermissionFlagsBits } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders.js"; 4 | import { eSnipe, snipe } from "../utils/functions/guilds/utils"; 5 | 6 | const cmd = new Command("clearsnipe", "delete the current sniped thing", "moderation") 7 | .setAliases(["cs"]) 8 | .setPermissions(["MANAGE_MESSAGES"]); 9 | 10 | async function run( 11 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 12 | send: SendMessage, 13 | args: string[], 14 | ) { 15 | if (!message.member.permissions.has(PermissionFlagsBits.ManageMessages)) return; 16 | let channel: Channel = message.channel; 17 | 18 | if (args.length == 1) { 19 | if (!message.mentions.channels.first()) { 20 | return send({ embeds: [new ErrorEmbed("invalid channel")] }); 21 | } 22 | channel = message.mentions.channels.first(); 23 | if (!channel) { 24 | return send({ embeds: [new ErrorEmbed("invalid channel")] }); 25 | } 26 | } 27 | 28 | if (!snipe || (!snipe.get(channel.id) && (!eSnipe || !eSnipe.get(channel.id)))) { 29 | return send({ 30 | embeds: [new ErrorEmbed("nothing has been sniped in " + channel.toString())], 31 | }); 32 | } 33 | 34 | snipe.delete(channel.id); 35 | eSnipe.delete(channel.id); 36 | 37 | return send({ 38 | embeds: [new CustomEmbed(message.member, "✅ snipe cleared in " + channel.toString())], 39 | }); 40 | } 41 | 42 | cmd.setRun(run); 43 | 44 | module.exports = cmd; 45 | -------------------------------------------------------------------------------- /src/utils/functions/users/admin.ts: -------------------------------------------------------------------------------- 1 | import ms = require("ms"); 2 | import prisma from "../../../init/database"; 3 | import redis from "../../../init/redis"; 4 | import Constants, { AdminPermission } from "../../Constants"; 5 | import { getUserId, MemberResolvable } from "../member"; 6 | 7 | export async function hasAdminPermission(member: MemberResolvable, permission: AdminPermission) { 8 | return (await getAdminLevel(member)) >= Constants.ADMIN_PERMISSIONS.get(permission); 9 | } 10 | 11 | export async function getAdminLevel(member: MemberResolvable) { 12 | const userId = getUserId(member); 13 | 14 | if (await redis.exists(`${Constants.redis.cache.user.ADMIN_LEVEL}:${userId}`)) { 15 | return parseInt(await redis.get(`${Constants.redis.cache.user.ADMIN_LEVEL}:${userId}`)); 16 | } 17 | 18 | let query = await prisma.user.findUnique({ 19 | where: { 20 | id: userId, 21 | }, 22 | select: { 23 | adminLevel: true, 24 | }, 25 | }); 26 | 27 | if (!query) { 28 | query = { 29 | adminLevel: 0, 30 | }; 31 | } 32 | 33 | await redis.set( 34 | `${Constants.redis.cache.user.ADMIN_LEVEL}:${userId}`, 35 | query.adminLevel, 36 | "EX", 37 | Math.floor(ms("3 hours") / 1000), 38 | ); 39 | 40 | return query.adminLevel; 41 | } 42 | 43 | export async function setAdminLevel(member: MemberResolvable, level: number) { 44 | const userId = getUserId(member); 45 | 46 | await redis.del(`${Constants.redis.cache.user.ADMIN_LEVEL}:${userId}`); 47 | await prisma.user.update({ 48 | where: { 49 | id: userId, 50 | }, 51 | data: { 52 | adminLevel: level, 53 | }, 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/interactions/crash_join.ts: -------------------------------------------------------------------------------- 1 | import { MessageFlags } from "discord.js"; 2 | import redis from "../init/redis"; 3 | import { NypsiClient } from "../models/Client"; 4 | import { CustomEmbed, ErrorEmbed } from "../models/EmbedBuilders"; 5 | import { InteractionHandler } from "../types/InteractionHandler"; 6 | import Constants from "../utils/Constants"; 7 | import { addCrashPlayer } from "../utils/functions/economy/crash"; 8 | import { isEcoBanned, userExists } from "../utils/functions/economy/utils"; 9 | 10 | export default { 11 | name: "crash-join", 12 | type: "interaction", 13 | async run(interaction) { 14 | if (!interaction.isButton()) return; 15 | if ((await isEcoBanned(interaction.user.id)).banned) return; 16 | if (!(await userExists(interaction.user.id))) return; 17 | 18 | if ( 19 | (await redis.get( 20 | `${Constants.redis.nypsi.RESTART}:${(interaction.client as NypsiClient).cluster.id}`, 21 | )) == "t" 22 | ) { 23 | return interaction.reply({ 24 | embeds: [new ErrorEmbed("nypsi is rebooting")], 25 | flags: MessageFlags.Ephemeral, 26 | }); 27 | } 28 | 29 | if (await redis.get("nypsi:maintenance")) { 30 | return interaction.reply({ 31 | embeds: [ 32 | new CustomEmbed( 33 | this.member, 34 | "fun & moderation commands are still available to you. maintenance mode only prevents certain commands to prevent loss of progress", 35 | ).setTitle("⚠️ nypsi is under maintenance"), 36 | ], 37 | flags: MessageFlags.Ephemeral, 38 | }); 39 | } 40 | 41 | await addCrashPlayer(interaction); 42 | }, 43 | } as InteractionHandler; 44 | -------------------------------------------------------------------------------- /prisma/migrations/20240619184548_mode_ration_evidence/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "ModerationEvidence" ( 3 | "id" TEXT NOT NULL, 4 | "bytes" BIGINT NOT NULL, 5 | "caseId" INTEGER NOT NULL, 6 | "guildId" TEXT NOT NULL, 7 | "userId" TEXT, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | 10 | CONSTRAINT "ModerationEvidence_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "GuildEvidenceCredit" ( 15 | "id" SERIAL NOT NULL, 16 | "guildId" TEXT NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "bytes" BIGINT NOT NULL, 19 | 20 | CONSTRAINT "GuildEvidenceCredit_pkey" PRIMARY KEY ("id") 21 | ); 22 | 23 | -- CreateIndex 24 | CREATE UNIQUE INDEX "ModerationEvidence_caseId_guildId_key" ON "ModerationEvidence"("caseId", "guildId"); 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "ModerationEvidence" ADD CONSTRAINT "ModerationEvidence_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 28 | 29 | -- AddForeignKey 30 | ALTER TABLE "ModerationEvidence" ADD CONSTRAINT "ModerationEvidence_caseId_guildId_fkey" FOREIGN KEY ("caseId", "guildId") REFERENCES "ModerationCase"("caseId", "guildId") ON DELETE RESTRICT ON UPDATE CASCADE; 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "ModerationEvidence" ADD CONSTRAINT "ModerationEvidence_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "GuildEvidenceCredit" ADD CONSTRAINT "GuildEvidenceCredit_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 37 | -------------------------------------------------------------------------------- /src/utils/functions/clusters.ts: -------------------------------------------------------------------------------- 1 | import { ClusterManager } from "discord-hybrid-sharding"; 2 | import { NypsiClient } from "../../models/Client"; 3 | 4 | export async function findChannelCluster(client: NypsiClient | ClusterManager, channelId: string) { 5 | const clusterHas = await ( 6 | client instanceof ClusterManager ? client : client.cluster 7 | ).broadcastEval( 8 | async (c, { channelId }) => { 9 | const client = c as unknown as NypsiClient; 10 | 11 | const channel = client.channels.cache.get(channelId); 12 | 13 | if (channel && !channel.isDMBased()) { 14 | return { cluster: client.cluster.id, guildId: channel.guildId }; 15 | } else { 16 | return "not-found"; 17 | } 18 | }, 19 | { 20 | context: { channelId }, 21 | }, 22 | ); 23 | 24 | for (const i of clusterHas) { 25 | if (i != "not-found") { 26 | return i; 27 | } 28 | } 29 | 30 | return null; 31 | } 32 | 33 | export async function findGuildCluster(client: NypsiClient | ClusterManager, guildId: string) { 34 | const clusterHas = await ( 35 | client instanceof ClusterManager ? client : client.cluster 36 | ).broadcastEval( 37 | async (c, { guildId }) => { 38 | const client = c as unknown as NypsiClient; 39 | 40 | const guild = client.guilds.cache.get(guildId); 41 | 42 | if (guild) { 43 | return client.cluster.id; 44 | } else { 45 | return "not-found"; 46 | } 47 | }, 48 | { 49 | context: { guildId }, 50 | }, 51 | ); 52 | 53 | for (const i of clusterHas) { 54 | if (i != "not-found") { 55 | return i; 56 | } 57 | } 58 | 59 | return null; 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/multi.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiClient } from "../models/Client"; 3 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 4 | import { CustomEmbed } from "../models/EmbedBuilders.js"; 5 | import { getGambleMulti, getSellMulti } from "../utils/functions/economy/balance"; 6 | import { createUser, userExists } from "../utils/functions/economy/utils.js"; 7 | import { addCooldown, getResponse, onCooldown } from "../utils/handlers/cooldownhandler"; 8 | 9 | const cmd = new Command("multi", "check your multipliers", "money").setAliases([ 10 | "multis", 11 | "multiplier", 12 | "multipliers", 13 | ]); 14 | 15 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 16 | if (await onCooldown(cmd.name, message.member)) { 17 | const res = await getResponse(cmd.name, message.member); 18 | 19 | if (res.respond) message.channel.send({ embeds: [res.embed] }); 20 | return; 21 | } 22 | 23 | await addCooldown(cmd.name, message.member, 5); 24 | 25 | if (!(await userExists(message.member))) await createUser(message.member); 26 | 27 | const embed = new CustomEmbed(message.member).setDescription( 28 | `gamble multiplier: **${Math.floor((await getGambleMulti(message.member, message.client as NypsiClient)).multi * 100)}**%\n` + 29 | `sell multiplier: **${Math.floor((await getSellMulti(message.member, message.client as NypsiClient)).multi * 100)}**%`, 30 | ); 31 | 32 | embed.setHeader(`${message.author.username}`, message.author.avatarURL()); 33 | 34 | return message.channel.send({ embeds: [embed] }); 35 | } 36 | 37 | cmd.setRun(run); 38 | 39 | module.exports = cmd; 40 | -------------------------------------------------------------------------------- /src/init/database.ts: -------------------------------------------------------------------------------- 1 | import { PrismaPg } from "@prisma/adapter-pg"; 2 | import { parentPort } from "worker_threads"; 3 | import { PrismaClient } from "../generated/prisma/client"; 4 | import Constants from "../utils/Constants"; 5 | import { logger } from "../utils/logger"; 6 | import redis from "./redis"; 7 | 8 | const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL }); 9 | 10 | const prisma = new PrismaClient({ adapter }).$extends({ 11 | query: { 12 | $allModels: { 13 | async $allOperations({ query, args, model, operation }) { 14 | const start = performance.now(); 15 | const result = await query(args); 16 | const end = performance.now(); 17 | 18 | const timeTaken = end - start; 19 | 20 | if (["Mention", "GraphMetrics"].includes(model)) return result; 21 | 22 | redis.lpush(Constants.redis.nypsi.HOURLY_DB_REPORT, timeTaken); 23 | if (timeTaken > 1000 && !parentPort) { 24 | let loggerArgs: typeof args; 25 | if ( 26 | JSON.stringify( 27 | args, 28 | (key, value) => (typeof value === "bigint" ? value.toString() : value), 29 | "\n", 30 | ).split("\n").length <= 250 31 | ) { 32 | loggerArgs = args; 33 | } 34 | 35 | if (timeTaken > 15000) { 36 | logger.warn(`query ${model}.${operation} took ${timeTaken.toFixed(2)}ms`, loggerArgs); 37 | } else { 38 | logger.debug(`query ${model}.${operation} took ${timeTaken.toFixed(2)}ms`, loggerArgs); 39 | } 40 | } 41 | 42 | return result; 43 | }, 44 | }, 45 | }, 46 | }); 47 | 48 | export default prisma; 49 | -------------------------------------------------------------------------------- /src/utils/functions/heartbeat.ts: -------------------------------------------------------------------------------- 1 | import { Cluster } from "discord-hybrid-sharding"; 2 | import redis from "../../init/redis"; 3 | import Constants from "../Constants"; 4 | import { logger } from "../logger"; 5 | import { dmQueueWorker } from "../queues/dms"; 6 | 7 | const failedHeartbeats = new Map(); 8 | 9 | export async function sendHeartbeat(cluster: Cluster) { 10 | if ((await redis.get(`${Constants.redis.nypsi.RESTART}:${cluster.id}`)) == "t") { 11 | return true; 12 | } 13 | 14 | return new Promise((resolve, reject) => { 15 | setImmediate(async () => { 16 | const res: any = await cluster.request({ alive: true }); 17 | 18 | if (res.alive) { 19 | resolve(true); 20 | } 21 | }); 22 | setTimeout(() => { 23 | reject("no response from cluster"); 24 | }, 5000); 25 | }); 26 | } 27 | 28 | export async function addFailedHeartbeat(cluster: Cluster) { 29 | if (failedHeartbeats.has(cluster.id)) { 30 | if (failedHeartbeats.get(cluster.id) >= 5) { 31 | logger.info(`respawning cluster ${cluster.id} due to missing heartbeats`); 32 | await dmQueueWorker.pause(); 33 | await cluster.respawn().then((c) => { 34 | logger.debug(`${cluster.id} respawn promise resolved, sending ready event`); 35 | c.emit("ready"); 36 | logger.debug(`${cluster.id} ready event sent`); 37 | }); 38 | dmQueueWorker.resume(); 39 | failedHeartbeats.delete(cluster.id); 40 | } else { 41 | failedHeartbeats.set(cluster.id, failedHeartbeats.get(cluster.id) + 1); 42 | } 43 | } else { 44 | failedHeartbeats.set(cluster.id, 1); 45 | } 46 | } 47 | 48 | setInterval(() => { 49 | failedHeartbeats.clear(); 50 | }, 600000); 51 | -------------------------------------------------------------------------------- /src/utils/functions/economy/items/dave.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { NypsiCommandInteraction, NypsiMessage } from "../../../../models/Command"; 3 | import { CustomEmbed } from "../../../../models/EmbedBuilders"; 4 | import { ItemUse } from "../../../../models/ItemUse"; 5 | import { addStat } from "../stats"; 6 | 7 | const responses = [ 8 | "AHHHH DONT EAT ME PLSSS", 9 | "meow", 10 | "meow meow", 11 | "meow meow meow", 12 | "meow meow meow meow", 13 | "meow meow meow meow meow", 14 | "meow meow meow meow meow meow", 15 | "meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow", 16 | "img:https://file.maxz.dev/XT6lUbETxJ.jpg", 17 | "img:https://file.maxz.dev/ZsuEygTbNQ.jpg", 18 | "img:https://file.maxz.dev/J297W2CFaw.webp", 19 | "img:https://file.maxz.dev/hnSsiUxbhW.jpg", 20 | "img:https://file.maxz.dev/ObdqjCUXgT.jpg", 21 | "img:https://file.maxz.dev/t63l3wjm40.jpg", 22 | "img:https://file.maxz.dev/SCSBCMxXPK.jpg", 23 | "img:https://file.maxz.dev/lyWI8uBDJq.jpg", 24 | "img:https://file.maxz.dev/JfUeBPZPBC.jpg", 25 | "img:https://file.maxz.dev/FIFtW0Ow9I.jpg", 26 | ]; 27 | 28 | module.exports = new ItemUse( 29 | "dave", 30 | async (message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) => { 31 | const chosen = responses[Math.floor(Math.random() * responses.length)]; 32 | 33 | const embed = new CustomEmbed(message.member); 34 | 35 | if (chosen.startsWith("img:")) embed.setImage(chosen.substring(4, chosen.length)); 36 | else embed.setDescription(chosen); 37 | 38 | await addStat(message.member, "dave"); 39 | 40 | return ItemUse.send(message, { 41 | embeds: [embed], 42 | }); 43 | }, 44 | ); 45 | -------------------------------------------------------------------------------- /src/commands/toggletracking.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage } from "../models/Command"; 3 | import { CustomEmbed } from "../models/EmbedBuilders"; 4 | import { getPrefix } from "../utils/functions/guilds/utils"; 5 | import { disableTracking, enableTracking, isTracking } from "../utils/functions/users/history"; 6 | import { addCooldown, getResponse, onCooldown } from "../utils/handlers/cooldownhandler"; 7 | 8 | const cmd = new Command( 9 | "toggletracking", 10 | "toggle tracking your username and avatar changes", 11 | "info", 12 | ); 13 | 14 | async function run(message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction)) { 15 | if (await onCooldown(cmd.name, message.member)) { 16 | const res = await getResponse(cmd.name, message.member); 17 | 18 | if (res.respond) message.channel.send({ embeds: [res.embed] }); 19 | return; 20 | } 21 | 22 | await addCooldown(cmd.name, message.member, 90); 23 | 24 | if (await isTracking(message.member)) { 25 | await disableTracking(message.member); 26 | return message.channel.send({ 27 | embeds: [ 28 | new CustomEmbed( 29 | message.member, 30 | "✅ username and avatar tracking has been disabled", 31 | ).setFooter({ 32 | text: `use ${(await getPrefix(message.guild))[0]}(un/avh) clear to clear your history`, 33 | }), 34 | ], 35 | }); 36 | } else { 37 | await enableTracking(message.member); 38 | return message.channel.send({ 39 | embeds: [new CustomEmbed(message.member, "✅ username and avatar tracking has been enabled")], 40 | }); 41 | } 42 | } 43 | 44 | cmd.setRun(run); 45 | 46 | module.exports = cmd; 47 | -------------------------------------------------------------------------------- /src/interactions/item.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getInventory } from "../utils/functions/economy/inventory"; 3 | import { getItems } from "../utils/functions/economy/utils"; 4 | import { logger } from "../utils/logger"; 5 | 6 | export default { 7 | name: "item", 8 | type: "autocomplete", 9 | async run(interaction) { 10 | const focused = interaction.options.getFocused(true); 11 | focused.value = focused.value.toLowerCase(); 12 | 13 | const inventory = await getInventory(interaction.user.id); 14 | 15 | if (!inventory) return; 16 | 17 | const items = getItems(); 18 | 19 | let options = inventory.entries 20 | .map((i) => i.item) 21 | .filter( 22 | (item) => 23 | item.includes(focused.value) || 24 | items[item].name.includes(focused.value) || 25 | items[item].aliases?.includes(focused.value), 26 | ); 27 | 28 | if (options.length > 25) options = options.splice(0, 24); 29 | 30 | if (options.length == 0) return interaction.respond([]); 31 | 32 | const formatted = options.map((i) => ({ 33 | name: `${ 34 | items[i].emoji.startsWith("<:") || 35 | items[i].emoji.startsWith(" { 44 | logger.warn(`failed to respond to autocomplete in time`, { 45 | userId: interaction.user.id, 46 | command: interaction.commandName, 47 | }); 48 | }); 49 | }, 50 | } as AutocompleteHandler; 51 | -------------------------------------------------------------------------------- /src/interactions/car.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteHandler } from "../types/InteractionHandler"; 2 | import { getInventory } from "../utils/functions/economy/inventory"; 3 | import { getItems } from "../utils/functions/economy/utils"; 4 | import { logger } from "../utils/logger"; 5 | 6 | export default { 7 | name: "car", 8 | type: "autocomplete", 9 | async run(interaction) { 10 | const focused = interaction.options.getFocused(true); 11 | focused.value = focused.value.toLowerCase(); 12 | 13 | const inventory = await getInventory(interaction.user.id); 14 | 15 | const items = getItems(); 16 | 17 | let options = inventory.entries 18 | .map((i) => i.item) 19 | .filter( 20 | (item) => 21 | (item.includes(focused.value) || 22 | items[item].name.includes(focused.value) || 23 | items[item].aliases?.includes(focused.value)) && 24 | items[item].role == "car", 25 | ); 26 | 27 | options.push("cycle"); 28 | 29 | if (options.length > 25) options = options.splice(0, 24); 30 | 31 | if (options.length == 0) return interaction.respond([]); 32 | 33 | const formatted = options.map((i) => ({ 34 | name: `${ 35 | items[i].emoji.startsWith("<:") || 36 | items[i].emoji.startsWith(" { 45 | logger.warn(`failed to respond to autocomplete in time`, { 46 | userId: interaction.user.id, 47 | command: interaction.commandName, 48 | }); 49 | }); 50 | }, 51 | } as AutocompleteHandler; 52 | -------------------------------------------------------------------------------- /src/scheduled/jobs/crafted.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../init/database"; 2 | import { CustomEmbed } from "../../models/EmbedBuilders"; 3 | import { Job } from "../../types/Jobs"; 4 | import { addProgress } from "../../utils/functions/economy/achievements"; 5 | import { addInventoryItem, isGem } from "../../utils/functions/economy/inventory"; 6 | import { getItems } from "../../utils/functions/economy/utils"; 7 | import { addNotificationToQueue, getDmSettings } from "../../utils/functions/users/notifications"; 8 | 9 | export default { 10 | name: "crafted", 11 | cron: "0 * * * *", 12 | async run() { 13 | const query = await prisma.crafting.findMany({ 14 | where: { 15 | finished: { lt: new Date() }, 16 | }, 17 | }); 18 | 19 | for (const item of query) { 20 | await prisma.crafting.delete({ 21 | where: { 22 | id: item.id, 23 | }, 24 | }); 25 | 26 | await addInventoryItem(item.userId, item.itemId, item.amount); 27 | 28 | await addProgress(item.userId, "crafter", item.amount); 29 | if (isGem(item.itemId)) await addProgress(item.userId, "gem_hunter", item.amount); 30 | 31 | if ((await getDmSettings(item.userId)).other) { 32 | addNotificationToQueue({ 33 | memberId: item.userId, 34 | payload: { 35 | content: `you have finished crafting ${item.amount} ${getItems()[item.itemId].emoji} ${ 36 | getItems()[item.itemId].name 37 | }`, 38 | embed: new CustomEmbed(item.userId).setDescription( 39 | `\`${item.amount}x\` ${getItems()[item.itemId].emoji} ${getItems()[item.itemId].name}`, 40 | ), 41 | }, 42 | }); 43 | } 44 | } 45 | }, 46 | } satisfies Job; 47 | -------------------------------------------------------------------------------- /src/utils/functions/workers/wordlesort.ts: -------------------------------------------------------------------------------- 1 | import { inPlaceSort } from "fast-sort"; 2 | import { isMainThread, parentPort, Worker, workerData } from "worker_threads"; 3 | 4 | export default function wordleSortWorker( 5 | query: { 6 | user: { 7 | lastKnownUsername: string; 8 | blacklisted: boolean; 9 | id: string; 10 | }; 11 | win6: number; 12 | win5: number; 13 | win4: number; 14 | win3: number; 15 | win2: number; 16 | win1: number; 17 | }[], 18 | ): Promise< 19 | { 20 | wins: number; 21 | user: { 22 | lastKnownUsername: string; 23 | blacklisted: boolean; 24 | id: string; 25 | }; 26 | }[] 27 | > { 28 | return new Promise((resolve, reject) => { 29 | const worker = new Worker(__filename, { 30 | workerData: [query], 31 | }); 32 | worker.on("message", resolve); 33 | worker.on("error", reject); 34 | worker.on("exit", (code) => { 35 | if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`)); 36 | }); 37 | }); 38 | } 39 | 40 | if (!isMainThread) { 41 | process.title = "nypsi: sort worker"; 42 | const query: { 43 | user: { 44 | lastKnownTag: string; 45 | blacklisted: boolean; 46 | id: string; 47 | }; 48 | win6: number; 49 | win5: number; 50 | win4: number; 51 | win3: number; 52 | win2: number; 53 | win1: number; 54 | }[] = workerData[0]; 55 | 56 | const data = query 57 | .filter((i) => !i.user.blacklisted) 58 | .map((i) => { 59 | return { wins: i.win1 + i.win2 + i.win3 + i.win4 + i.win5 + i.win6, user: i.user }; 60 | }); 61 | 62 | inPlaceSort(data).desc((i) => i.wins); 63 | 64 | parentPort.postMessage(data.slice(0, 100)); 65 | process.exit(0); 66 | } 67 | -------------------------------------------------------------------------------- /src/commands/requestdm.ts: -------------------------------------------------------------------------------- 1 | import { CommandInteraction, Message } from "discord.js"; 2 | import { Command, NypsiCommandInteraction, NypsiMessage, SendMessage } from "../models/Command"; 3 | import { ErrorEmbed } from "../models/EmbedBuilders"; 4 | import { hasAdminPermission } from "../utils/functions/users/admin"; 5 | import { addNotificationToQueue } from "../utils/functions/users/notifications"; 6 | 7 | const cmd = new Command( 8 | "requestdm", 9 | "attempt to send a DM to a given user (this is my way of having fun leave me alone)", 10 | "none", 11 | ); 12 | 13 | async function run( 14 | message: NypsiMessage | (NypsiCommandInteraction & CommandInteraction), 15 | send: SendMessage, 16 | args: string[], 17 | ) { 18 | if (!(await hasAdminPermission(message.member, "requestdm"))) return; 19 | 20 | if (args.length < 2) { 21 | return send({ embeds: [new ErrorEmbed("$requestdm ")] }); 22 | } 23 | 24 | const user = args[0]; 25 | 26 | args.shift(); 27 | 28 | const job = await addNotificationToQueue({ 29 | memberId: user, 30 | payload: { 31 | content: args.join(" "), 32 | }, 33 | }).then((r) => r[0]); 34 | 35 | if (!(message instanceof Message)) return; 36 | 37 | const interval = setInterval(async () => { 38 | const state = await job.getState(); 39 | 40 | switch (state) { 41 | case "completed": 42 | clearInterval(interval); 43 | message.react("✅"); 44 | break; 45 | case "failed": 46 | clearInterval(interval); 47 | message.react("❌"); 48 | break; 49 | case "unknown": 50 | clearInterval(interval); 51 | message.react("❓"); 52 | break; 53 | } 54 | }, 500); 55 | } 56 | 57 | cmd.setRun(run); 58 | 59 | module.exports = cmd; 60 | -------------------------------------------------------------------------------- /src/utils/functions/users/aura.ts: -------------------------------------------------------------------------------- 1 | import prisma from "../../../init/database"; 2 | import redis from "../../../init/redis"; 3 | import Constants from "../../Constants"; 4 | import { getUserId, MemberResolvable } from "../member"; 5 | 6 | const base = 1000; 7 | 8 | export async function getAura(member: MemberResolvable) { 9 | const userId = getUserId(member); 10 | 11 | const cache = await redis.get(`${Constants.redis.cache.user.aura}:${userId}`); 12 | 13 | if (cache) return parseInt(cache); 14 | 15 | const received = await prisma.aura.aggregate({ 16 | where: { recipientId: userId }, 17 | _sum: { amount: true }, 18 | }); 19 | const sent = await prisma.aura.aggregate({ 20 | where: { senderId: userId }, 21 | _sum: { amount: true }, 22 | }); 23 | 24 | const total = base + (received._sum.amount - sent._sum.amount); 25 | 26 | await redis.set(`${Constants.redis.cache.user.aura}:${userId}`, total); 27 | 28 | return total; 29 | } 30 | 31 | export async function createAuraTransaction( 32 | recipient: MemberResolvable, 33 | sender: MemberResolvable, 34 | amount: number, 35 | ) { 36 | const senderId = getUserId(sender); 37 | const recipientId = getUserId(recipient); 38 | 39 | await prisma.aura.create({ 40 | data: { senderId, recipientId, amount }, 41 | }); 42 | await redis.del( 43 | `${Constants.redis.cache.user.aura}:${recipientId}`, 44 | `${Constants.redis.cache.user.aura}:${senderId}`, 45 | ); 46 | } 47 | 48 | export async function getAuraTransactions(member: MemberResolvable) { 49 | const userId = getUserId(member); 50 | 51 | const query = await prisma.aura.findMany({ 52 | where: { OR: [{ recipientId: userId }, { senderId: userId }] }, 53 | orderBy: { id: "asc" }, 54 | }); 55 | 56 | return query; 57 | } 58 | -------------------------------------------------------------------------------- /src/scheduled/jobs/offerexpire.ts: -------------------------------------------------------------------------------- 1 | import dayjs = require("dayjs"); 2 | import prisma from "../../init/database"; 3 | import { CustomEmbed } from "../../models/EmbedBuilders"; 4 | import { Job } from "../../types/Jobs"; 5 | import { addBalance } from "../../utils/functions/economy/balance"; 6 | import { getItems } from "../../utils/functions/economy/utils"; 7 | import { addNotificationToQueue } from "../../utils/functions/users/notifications"; 8 | import { getLastKnownUsername } from "../../utils/functions/users/username"; 9 | 10 | export default { 11 | name: "offerexpire", 12 | cron: "0 */2 * * *", 13 | async run(log) { 14 | const query = await prisma.offer.findMany({ 15 | where: { 16 | AND: [ 17 | { 18 | createdAt: { lt: dayjs().subtract(7, "day").toDate() }, 19 | }, 20 | { 21 | sold: false, 22 | }, 23 | ], 24 | }, 25 | select: { 26 | itemId: true, 27 | itemAmount: true, 28 | money: true, 29 | targetId: true, 30 | ownerId: true, 31 | messageId: true, 32 | }, 33 | }); 34 | 35 | for (const offer of query) { 36 | await prisma.offer.delete({ where: { messageId: offer.messageId } }); 37 | await addBalance(offer.ownerId, Number(offer.money)); 38 | 39 | const embed = new CustomEmbed( 40 | offer.ownerId, 41 | `your offer to **${await getLastKnownUsername(offer.targetId)}** for ${offer.itemAmount}x ${getItems()[offer.itemId].name} has expired`, 42 | ).setFooter({ text: `+$${offer.money.toLocaleString()}` }); 43 | 44 | addNotificationToQueue({ memberId: offer.ownerId, payload: { embed } }); 45 | } 46 | 47 | log(`${query.length} offers expired`); 48 | }, 49 | } satisfies Job; 50 | --------------------------------------------------------------------------------