plugin_manager.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import path from "path";
  2. import fs from "fs";
  3. import { Message, Client, ClientEvents } from "discord.js";
  4. import { EventType, BotEvent, ICommand, isPlugin, BotEventData, isCustomEvent, IPlugin, CommandType } from "./model/plugin";
  5. import { isAuthorisedAsync } from "./util";
  6. import { tryDo } from "../../shared/lib/src/common/async_utils";
  7. interface IDocumentationData {
  8. type: CommandType;
  9. name: string;
  10. doc?: string;
  11. example?: string;
  12. auth: boolean;
  13. }
  14. type BotEventCollection = { [event in EventType]?: BotEvent[] };
  15. export class PluginManager {
  16. private plugins: IPlugin[] = [];
  17. private commands: ICommand[] = [];
  18. private botEvents: BotEventCollection = {};
  19. constructor(private cmdPath: string) {
  20. this.init();
  21. }
  22. private init(): void {
  23. const files = fs.readdirSync(this.cmdPath);
  24. for (const file of files) {
  25. const ext = path.extname(file);
  26. if (ext != ".js")
  27. continue;
  28. // eslint-disable-next-line @typescript-eslint/no-var-requires
  29. this.loadCommand(require(path.resolve(this.cmdPath, file)));
  30. }
  31. }
  32. private loadCommand(mod: Record<string, unknown>) {
  33. const getBotEventArray = (evtType: EventType) => {
  34. if (!this.botEvents[evtType])
  35. this.botEvents[evtType] = [];
  36. return this.botEvents[evtType];
  37. };
  38. for (const i in mod) {
  39. if (!Object.prototype.hasOwnProperty.call(mod, i))
  40. continue;
  41. const commandClass = mod[i] as unknown;
  42. // Ensure this is indeed a command class
  43. if (!isPlugin(commandClass))
  44. continue;
  45. const cmd = new commandClass();
  46. this.plugins.push(cmd);
  47. if (cmd.botCommands)
  48. this.commands.push(...cmd.botCommands.map(c => ({ ...c, action: c.action.bind(cmd) })));
  49. if (cmd.botEvents)
  50. for (const [i, event] of Object.entries(cmd.botEvents)) {
  51. getBotEventArray(i as EventType)?.push((event as BotEvent).bind(cmd));
  52. }
  53. }
  54. }
  55. get documentation(): IDocumentationData[] {
  56. return this.commands.filter(m => m.documentation !== undefined).map(m => ({
  57. type: m.type,
  58. name: m.pattern.toString(),
  59. doc: m.documentation?.description,
  60. example: m.documentation?.example,
  61. auth: m.auth || false
  62. }));
  63. }
  64. async start(client: Client): Promise<void> {
  65. for (const evtName of Object.keys(this.botEvents)) {
  66. if (!Object.prototype.hasOwnProperty.call(this.botEvents, evtName) || isCustomEvent(evtName))
  67. continue;
  68. client.on(evtName as keyof ClientEvents, async (...args: unknown[]) =>
  69. await this.trigger(evtName as EventType, ...args)
  70. );
  71. }
  72. for (const plugin of this.plugins) {
  73. if (plugin.start)
  74. await plugin.start();
  75. }
  76. }
  77. async trigger(event: EventType, ...params: unknown[]): Promise<boolean> {
  78. const eventData: BotEventData = {
  79. actionsDone: false
  80. };
  81. const eventHandlers = this.botEvents[event];
  82. if (!eventHandlers)
  83. return eventData.actionsDone;
  84. for (const handler of eventHandlers) {
  85. const actionResult = handler(eventData, ...params);
  86. if (actionResult instanceof Promise)
  87. await actionResult;
  88. }
  89. return eventData.actionsDone;
  90. }
  91. async runCommand(type: CommandType, m: Message, content: string): Promise<boolean> {
  92. const lowerCaseContent = content.toLowerCase();
  93. for (const c of this.commands) {
  94. if (c.type != type)
  95. continue;
  96. let match = false;
  97. let matchData: string | RegExpMatchArray = "";
  98. if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) {
  99. match = true;
  100. matchData = content;
  101. }
  102. else if (c.pattern instanceof RegExp) {
  103. const result = c.pattern.exec(content);
  104. if (result != null) {
  105. match = true;
  106. matchData = result;
  107. }
  108. }
  109. if (match) {
  110. if (!c.allowDM && m.channel.type == "dm") {
  111. await tryDo(m.reply("Sorry, this command is not available in DMs for now."));
  112. return false;
  113. }
  114. if (c.auth && !(await isAuthorisedAsync(m.member)))
  115. return false;
  116. const eventResult = c.action({ message: m, contents: matchData });
  117. if (eventResult instanceof Promise)
  118. await eventResult;
  119. return true;
  120. }
  121. }
  122. return false;
  123. }
  124. }