plugin_manager.ts 4.8 KB

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