Skip to content

Commit

Permalink
feat: module installation via URL (#237)
Browse files Browse the repository at this point in the history
* chore: ensure module names match `/^[a-z]{1,10}$/`

* chore: don't touch the spec passed to `ModuleManager.file`

* chore: initialize ModuleManager.files method

* feat: implement URL installation

* chore: make \uninstall uninstall modules installed w/ URL
  • Loading branch information
rojvv authored Aug 8, 2022
1 parent 8fdd796 commit 545305e
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 36 deletions.
8 changes: 8 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,13 @@ try {
manager.installMultiple(modules, false);
manager.install(managerModule(manager), false);
manager.installMultiple(await ModuleManager.directory("externals"), true);
manager.installMultiple(
await ModuleManager.files(
Object.keys(localStorage).filter((v) => v.startsWith("module_")).map((v) =>
localStorage.getItem(v)!
),
),
true,
);
client.addEventHandler(manager.handler, new NewMessage({}));
client.start();
3 changes: 2 additions & 1 deletion module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ export function isModule(value: unknown): value is Module {
const obj = Object(value);
return (
typeof obj.name === "string" &&
/^[a-z]{1,10}$/.test(obj.name) &&
Array.isArray(obj.handlers) &&
obj.handlers.filter((handler: unknown) => handler instanceof Handler)
.length != undefined
.length != 0
);
}

Expand Down
106 changes: 71 additions & 35 deletions module_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,54 @@ export function managerModule(manager: ModuleManager): Module {
return {
name: "manager",
handlers: [
new CommandHandler("install", async ({ client, event }) => {
const reply = await event.message.getReplyMessage();
if (!reply) {
return;
}
const { media } = reply;
if (
!(media instanceof Api.MessageMediaDocument) ||
!(media.document instanceof Api.Document) ||
!(media.document.attributes[0] instanceof
Api.DocumentAttributeFilename) ||
!media.document.attributes[0].fileName.endsWith(".ts") ||
media.document.size.gt(5000)
) {
return;
}
const result = await client.downloadMedia(media, {});
if (!result) {
await updateMessage(event, "Could not download the module.");
return;
new CommandHandler("install", async ({ client, event, args }) => {
let spec = args[0] ?? "";
let path: string | undefined;
if (!spec) {
const reply = await event.message.getReplyMessage();
if (!reply) {
return;
}
const { media } = reply;
if (
!(media instanceof Api.MessageMediaDocument) ||
!(media.document instanceof Api.Document) ||
!(media.document.attributes[0] instanceof
Api.DocumentAttributeFilename) ||
!media.document.attributes[0].fileName.endsWith(".ts") ||
media.document.size.gt(5000)
) {
return;
}
const result = await client.downloadMedia(media, {});
if (!result) {
await updateMessage(event, "Could not download the module.");
return;
}
path = join(externals, `.${media.document.id}.ts`);
await Deno.writeTextFile(
path,
typeof result === "string" ? result : result.toString(),
);
spec = ModuleManager.pathToSpec(path);
}
const spec = join(externals, `.${media.document.id}.ts`);
await Deno.writeTextFile(
spec,
typeof result === "string" ? result : result.toString(),
);
let module;
try {
module = await ModuleManager.file(spec);
} catch (_err) {
await updateMessage(event, "The replied file is not a valid module.");
await updateMessage(event, "Not a module.");
return;
}
if (manager.modules.has(module.name)) {
await updateMessage(event, "Module already installed.");
return;
}
await Deno.rename(spec, join(externals, `${module.name}.ts`));
manager.install(module, true);
if (path) {
await Deno.rename(path, join(externals, `${module.name}.ts`));
} else {
localStorage.setItem(`module_${module.name}`, spec);
}
await updateMessage(event, "Module installed.");
}),
new CommandHandler("uninstall", async ({ event, args }) => {
Expand All @@ -64,6 +73,12 @@ export function managerModule(manager: ModuleManager): Module {
} catch (_err) {
//
}
const key = `module_${arg}`;
const item = localStorage.getItem(key);
if (item) {
localStorage.removeItem(key);
uninstalled++;
}
}
await updateMessage(
event,
Expand Down Expand Up @@ -254,34 +269,55 @@ export class ModuleManager {
}

static async file(spec: string) {
spec = toFileUrl(resolve(spec)).href;
const mod = (await import(spec)).default;
if (!isModule(mod)) {
throw new Error("Invalid module");
}
return mod;
}

static async files(specs: string[]) {
const modules = new Array<Module>();
let all = 0;
for (const spec of specs) {
try {
const module = await ModuleManager.file(spec);
modules.push(module);
} catch (err) {
log.warning(`failed to load ${spec}: ${err}`);
} finally {
all++;
}
}
log.info(`loaded ${modules.length}/${all} external modules`);
return modules;
}

static pathToSpec(path: string) {
return toFileUrl(resolve(path)).href;
}

static async directory(path: string) {
const modules = new Array<Module>();
let all = 0;
let loaded = 0;
for await (let { name, isFile, isDirectory } of Deno.readDir(path)) {
if ((!isFile && !isDirectory) || name.startsWith(".")) {
continue;
}
all++;
name = name.endsWith(".ts") ? name : `${name}/mod.ts`;
const spec = join(path, name);
const filePath = join(path, name);
try {
const mod = await this.file(spec);
const mod = await ModuleManager.file(
ModuleManager.pathToSpec(filePath),
);
modules.push(mod);
loaded++;
} catch (err) {
log.warning(`failed to load ${spec} from ${path}: ${err}`);
log.warning(`failed to load ${filePath} from ${path}: ${err}`);
} finally {
all++;
}
}
log.info(`loaded ${loaded}/${all} modules from ${path}`);
log.info(`loaded ${modules.length}/${all} external modules from ${path}`);
return modules;
}
}

0 comments on commit 545305e

Please sign in to comment.