main.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. // We need some kind of module resolver for @shared/db. We use module-alias.
  2. require("module-alias/register");
  3. import dotenv from "dotenv";
  4. if (process.env.NODE_ENV == "dev") {
  5. dotenv.config({
  6. path: "../.env"
  7. });
  8. dotenv.config({
  9. path: "../db.env"
  10. });
  11. process.env.TYPEORM_HOST = "localhost";
  12. process.env.FACEDETECT_URL = "localhost:8081";
  13. process.env.TYPEORM_USERNAME = process.env.DB_USERNAME;
  14. process.env.TYPEORM_PASSWORD = process.env.DB_PASSWORD;
  15. process.env.TYPEORM_DATABASE = process.env.DB_NAME;
  16. }
  17. import * as fs from "fs";
  18. import * as path from "path";
  19. import { client } from "./client";
  20. import * as mCmd from "./model/command";
  21. import "reflect-metadata";
  22. import { createConnection, getConnectionOptions } from "typeorm";
  23. import { getNumberEnums } from "./util";
  24. import { DB_ENTITIES } from "@shared/db/entities";
  25. import { logger } from "./logging";
  26. const REACT_PROBABILITY = 0.3;
  27. async function trigger(type: mCmd.ActionType, ...params: unknown[]) {
  28. let actionDone = false;
  29. const actions = botEvents[type];
  30. for (let i = 0; i < actions.length; i++) {
  31. const action = actions[i] as (...args: unknown[]) => boolean | Promise<boolean>;
  32. const actionResult = action(actionDone, ...params);
  33. if (actionResult instanceof Promise)
  34. actionDone = (await actionResult) || actionDone;
  35. else
  36. actionDone = actionResult || actionDone;
  37. }
  38. return actionDone;
  39. }
  40. type BotEventCollection = { [event in mCmd.ActionType]: mCmd.BotAction[] };
  41. const commandSets: mCmd.ICommand[] = [];
  42. const botCommands: mCmd.IBotCommand[] = [];
  43. const botEvents: BotEventCollection = getNumberEnums(mCmd.ActionType).reduce((p, c) => { p[c as mCmd.ActionType] = []; return p; }, {} as BotEventCollection);
  44. const startActions: Array<() => void | Promise<void>> = [];
  45. client.bot.on("ready", async () => {
  46. logger.info("Starting up NoctBot");
  47. client.botUser.setActivity(process.env.NODE_ENV == "dev" ? "Maintenance" : "@NoctBot help", {
  48. type: "PLAYING"
  49. });
  50. for (let i = 0; i < startActions.length; i++) {
  51. const action = startActions[i];
  52. const val = action();
  53. if (val instanceof Promise)
  54. await val;
  55. }
  56. logger.info("NoctBot is ready");
  57. });
  58. client.bot.on("message", async m => {
  59. if (m.author.id == client.botUser.id)
  60. return;
  61. if(process.env.FOOLS == "TRUE" && (m.channel.id == "297109482905796608" || m.channel.id == "429295461099110402") && Math.random() <= 0.01) {
  62. const neighs = ["*NEIGH*", "neeeeeigh!", "Gimme carrots!", "NEEEEIIIIGH", "**N E I G H**"];
  63. await m.channel.send(neighs[Math.floor(Math.random() * neighs.length)]);
  64. return;
  65. }
  66. let content = m.cleanContent.trim();
  67. if (await trigger(mCmd.ActionType.MESSAGE, m, content))
  68. return;
  69. if (m.mentions.users.size > 0 && m.mentions.users.has(client.botUser.id)) {
  70. const trimmedContent = m.content.trim();
  71. if (trimmedContent.startsWith(client.nameMention) || trimmedContent.startsWith(client.usernameMention)) {
  72. content = content.substring(`@${client.botUser.username}`.length).trim();
  73. const lowerCaseContent = content.toLowerCase();
  74. for (const c of botCommands) {
  75. if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) {
  76. c.action(m, content);
  77. return;
  78. }
  79. else if (c.pattern instanceof RegExp) {
  80. const result = c.pattern.exec(content);
  81. if (result != null) {
  82. c.action(m, content, result);
  83. return;
  84. }
  85. }
  86. }
  87. if (await trigger(mCmd.ActionType.DIRECT_MENTION, m, lowerCaseContent))
  88. return;
  89. }
  90. if (await trigger(mCmd.ActionType.INDIRECT_MENTION, m))
  91. return;
  92. }
  93. await trigger(mCmd.ActionType.POST_MESSAGE);
  94. });
  95. client.bot.on("messageReactionAdd", (r, u) => {
  96. if (Math.random() <= REACT_PROBABILITY && !u.bot) {
  97. logger.verbose(`Reacting to message ${r.message.id} because user ${u.tag} reacted to it`);
  98. r.message.react(r.emoji);
  99. }
  100. });
  101. function loadCommand(mod: Record<string, unknown>) {
  102. for (const i in mod) {
  103. if (!Object.prototype.hasOwnProperty.call(mod, i))
  104. continue;
  105. const commandClass = mod[i] as unknown;
  106. // Ensure this is indeed a command class
  107. if (!mCmd.isCommandSet(commandClass))
  108. continue;
  109. const cmd = new commandClass();
  110. commandSets.push(cmd);
  111. if (cmd._botCommands)
  112. botCommands.push(...cmd._botCommands.map(c => ({ ...c, action: c.action.bind(cmd) })));
  113. if (cmd._botEvents)
  114. for (const [i, event] of Object.entries(cmd._botEvents)) {
  115. botEvents[+i as mCmd.ActionType].push((event as mCmd.BotAction).bind(cmd));
  116. }
  117. if(cmd.onStart)
  118. startActions.push(cmd.onStart.bind(cmd));
  119. }
  120. }
  121. interface IDocumentationData {
  122. name: string;
  123. doc?: string;
  124. example?: string;
  125. auth: boolean;
  126. }
  127. export function getDocumentation() : IDocumentationData[] {
  128. return botCommands.filter(m => m.documentation !== undefined).map(m => ({
  129. name: m.pattern.toString(),
  130. doc: m.documentation?.description,
  131. example: m.documentation?.example,
  132. auth: m.auth || false
  133. }));
  134. }
  135. async function main() {
  136. await createConnection({
  137. ...await getConnectionOptions(),
  138. entities: DB_ENTITIES
  139. });
  140. const commandsPath = path.resolve(path.dirname(module.filename), "commands");
  141. const files = fs.readdirSync(commandsPath);
  142. for (const file of files) {
  143. const ext = path.extname(file);
  144. if (ext != ".js")
  145. continue;
  146. // eslint-disable-next-line @typescript-eslint/no-var-requires
  147. loadCommand(require(path.resolve(commandsPath, file)));
  148. }
  149. client.bot.login(process.env.BOT_TOKEN);
  150. }
  151. main();