Add initial Azure Functions support, clean up gitignore

This commit is contained in:
Essem 2022-04-17 10:40:56 -05:00
parent dd7bd6b4cc
commit 6bf0537c29
No known key found for this signature in database
GPG Key ID: 7D497397CC3A2A8C
29 changed files with 267 additions and 65 deletions

View File

@ -43,5 +43,12 @@ TMP_DOMAIN=
# Port for serving metrics. Metrics served are compatible with Prometheus.
METRICS=
# Set this to true if you want to use the external image API script, located in api/index.js
API=false
# The image API type to be used
# Set this to `none` to process all images locally
# Set this to `ws` if you want to use the external image API script, located in api/index.js
# Set this to `azure` to use the Azure Functions API
API_TYPE=none
# If API_TYPE is `azure`, set this to your Azure webhook URL
AZURE_URL=
# If API_TYPE is `azure`, set an optional password for webhook responses
AZURE_PASS=

136
.gitignore vendored
View File

@ -1,42 +1,130 @@
cache/
build/
data/
appold.js
migrate.js
config.json
.env
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
lib-cov
coverage
.nyc_output
.grunt
bower_components
# node-waf configuration
.lock-wscript
build/Release
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/
# Dependency directories
node_modules/
jspm_packages/
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
.next
.ftpconfig
coverage.lcov
bannedusers.json
*.code-workspace
todo.txt
.vscode/
migratedb.js
processed.txt
data.sqlite
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress v2.x temp and cache directory
.temp
.cache
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Debugging
*.heap
*.out.*
# vscode stuff
.vscode
*.code-workspace
# Databases
data/
*.sqlite
# Azure Functions artifacts
bin
obj
appsettings.json
local.settings.json
# Azurite artifacts
__blobstorage__
__queuestorage__
__azurite_db*__.json

View File

@ -25,7 +25,7 @@ class InfoCommand extends Command {
},
{
name: "📝 Credits:",
value: "Bot by **[Essem](https://essem.space)** and **[various contributors](https://github.com/esmBot/esmBot/graphs/contributors)**\nIcon by **[MintBurrow](https://twitter.com/MintBurrow)**"
value: "Bot by **[Essem](https://essem.space)** and **[various contributors](https://github.com/esmBot/esmBot/graphs/contributors)**\nLogo by **[MintBurrow](https://twitter.com/MintBurrow)**"
},
{
name: "💬 Total Servers:",

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class NineGagCommand extends ImageCommand {
params = {
water: "./assets/images/9gag.png",
water: "assets/images/9gag.png",
gravity: 6
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class AVSCommand extends ImageCommand {
params = {
water: "./assets/images/avs4you.png",
water: "assets/images/avs4you.png",
gravity: 5,
resize: true
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class BandicamCommand extends ImageCommand {
params = {
water: "./assets/images/bandicam.png",
water: "assets/images/bandicam.png",
gravity: 2,
resize: true
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class DeviantArtCommand extends ImageCommand {
params = {
water: "./assets/images/deviantart.png",
water: "assets/images/deviantart.png",
gravity: 5,
resize: true
};

View File

@ -10,14 +10,14 @@ class FlagCommand extends ImageCommand {
const text = this.type === "classic" ? this.args[0] : this.options.text;
if (!text.match(emojiRegex)) return false;
const flag = emoji.unemojify(text).replaceAll(":", "").replace("flag-", "");
let path = `./assets/images/region-flags/png/${flag.toUpperCase()}.png`;
if (flag === "pirate_flag") path = "./assets/images/pirateflag.png";
if (flag === "rainbow-flag") path = "./assets/images/rainbowflag.png";
if (flag === "checkered_flag") path = "./assets/images/checkeredflag.png";
if (flag === "transgender_flag") path = "./assets/images/transflag.png";
if (text === "🏴󠁧󠁢󠁳󠁣󠁴󠁿") path = "./assets/images/region-flags/png/GB-SCT.png";
if (text === "🏴󠁧󠁢󠁷󠁬󠁳󠁿") path = "./assets/images/region-flags/png/GB-WLS.png";
if (text === "🏴󠁧󠁢󠁥󠁮󠁧󠁿") path = "./assets/images/region-flags/png/GB-ENG.png";
let path = `assets/images/region-flags/png/${flag.toUpperCase()}.png`;
if (flag === "pirate_flag") path = "assets/images/pirateflag.png";
if (flag === "rainbow-flag") path = "assets/images/rainbowflag.png";
if (flag === "checkered_flag") path = "assets/images/checkeredflag.png";
if (flag === "transgender_flag") path = "assets/images/transflag.png";
if (text === "🏴󠁧󠁢󠁳󠁣󠁴󠁿") path = "assets/images/region-flags/png/GB-SCT.png";
if (text === "🏴󠁧󠁢󠁷󠁬󠁳󠁿") path = "assets/images/region-flags/png/GB-WLS.png";
if (text === "🏴󠁧󠁢󠁥󠁮󠁧󠁿") path = "assets/images/region-flags/png/GB-ENG.png";
try {
await fs.promises.access(path);
this.flagPath = path;

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class FunkyCommand extends ImageCommand {
params = {
water: "./assets/images/funky.png",
water: "assets/images/funky.png",
gravity: 3,
resize: true
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class HypercamCommand extends ImageCommand {
params = {
water: "./assets/images/hypercam.png",
water: "assets/images/hypercam.png",
gravity: 1,
resize: true
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class iFunnyCommand extends ImageCommand {
params = {
water: "./assets/images/ifunny.png",
water: "assets/images/ifunny.png",
gravity: 8,
resize: true,
append: true

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class KineMasterCommand extends ImageCommand {
params = {
water: "./assets/images/kinemaster.png",
water: "assets/images/kinemaster.png",
gravity: 3,
resize: true
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class MemeCenterCommand extends ImageCommand {
params = {
water: "./assets/images/memecenter.png",
water: "assets/images/memecenter.png",
gravity: 9,
mc: true
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class ShutterstockCommand extends ImageCommand {
params = {
water: "./assets/images/shutterstock.png",
water: "assets/images/shutterstock.png",
gravity: 5,
resize: true
};

View File

@ -2,7 +2,7 @@ import ImageCommand from "../../classes/imageCommand.js";
class SpeechBubbleCommand extends ImageCommand {
params = {
water: "./assets/images/speechbubble.png",
water: "assets/images/speechbubble.png",
gravity: "north",
resize: true,
yscale: 0.2,

View File

@ -15,6 +15,7 @@ Napi::Value Flag(const Napi::CallbackInfo &info) {
Napi::Buffer<char> data = obj.Get("data").As<Napi::Buffer<char>>();
string overlay = obj.Get("overlay").As<Napi::String>().Utf8Value();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -31,7 +32,8 @@ Napi::Value Flag(const Napi::CallbackInfo &info) {
} catch (Magick::Warning &warning) {
cerr << "Warning: " << warning.what() << endl;
}
watermark.read(overlay);
string assetPath = basePath + overlay;
watermark.read(assetPath);
watermark.alphaChannel(Magick::SetAlphaChannel);
watermark.evaluate(Magick::AlphaChannel, Magick::MultiplyEvaluateOperator,
0.5);

View File

@ -14,6 +14,7 @@ Napi::Value Gamexplain(const Napi::CallbackInfo &info) {
Napi::Object obj = info[0].As<Napi::Object>();
Napi::Buffer<char> data = obj.Get("data").As<Napi::Buffer<char>>();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -30,7 +31,8 @@ Napi::Value Gamexplain(const Napi::CallbackInfo &info) {
} catch (Magick::Warning &warning) {
cerr << "Warning: " << warning.what() << endl;
}
watermark.read("./assets/images/gamexplain.png");
string assetPath = basePath + "assets/images/gamexplain.png";
watermark.read(assetPath);
coalesceImages(&coalesced, frames.begin(), frames.end());
for (Image &image : coalesced) {

View File

@ -14,6 +14,7 @@ Napi::Value Globe(const Napi::CallbackInfo &info) {
Napi::Object obj = info[0].As<Napi::Object>();
Napi::Buffer<char> data = obj.Get("data").As<Napi::Buffer<char>>();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -31,8 +32,8 @@ Napi::Value Globe(const Napi::CallbackInfo &info) {
} catch (Magick::Warning &warning) {
cerr << "Warning: " << warning.what() << endl;
}
distort.read("./assets/images/spheremap.png");
overlay.read("./assets/images/sphere_overlay.png");
distort.read(basePath + "assets/images/spheremap.png");
overlay.read(basePath + "assets/images/sphere_overlay.png");
coalesceImages(&coalesced, frames.begin(), frames.end());
if (type != "gif") {

View File

@ -12,11 +12,13 @@ Napi::Value Homebrew(const Napi::CallbackInfo &info) {
try {
Napi::Object obj = info[0].As<Napi::Object>();
string caption = obj.Get("caption").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
Blob blob;
Image image;
image.read("./assets/images/hbc.png");
string assetPath = basePath + "assets/images/hbc.png";
image.read(assetPath);
image.textGravity(Magick::CenterGravity);
image.font("./assets/hbc.ttf");
image.textKerning(-5);

View File

@ -14,6 +14,7 @@ Napi::Value Reddit(const Napi::CallbackInfo &info) {
Napi::Buffer<char> data = obj.Get("data").As<Napi::Buffer<char>>();
string text = obj.Get("caption").As<Napi::String>().Utf8Value();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -32,7 +33,7 @@ Napi::Value Reddit(const Napi::CallbackInfo &info) {
cerr << "Warning: " << warning.what() << endl;
}
watermark.read("./assets/images/reddit.png");
watermark.read(basePath + "assets/images/reddit.png");
text_image.textGravity(Magick::WestGravity);
text_image.font("Roboto");
text_image.fontPointsize(47);

View File

@ -14,6 +14,7 @@ Napi::Value Retro(const Napi::CallbackInfo &info) {
string line1 = obj.Get("line1").As<Napi::String>().Utf8Value();
string line2 = obj.Get("line2").As<Napi::String>().Utf8Value();
string line3 = obj.Get("line3").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
Blob blob;
@ -22,7 +23,7 @@ Napi::Value Retro(const Napi::CallbackInfo &info) {
Image line2_text;
Image line3_text;
image.read("./assets/images/retro.png");
image.read(basePath + "assets/images/retro.png");
line2_text.backgroundColor("none");
line2_text.fontPointsize(128);

View File

@ -14,6 +14,7 @@ Napi::Value Scott(const Napi::CallbackInfo &info) {
Napi::Object obj = info[0].As<Napi::Object>();
Napi::Buffer<char> data = obj.Get("data").As<Napi::Buffer<char>>();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -30,7 +31,7 @@ Napi::Value Scott(const Napi::CallbackInfo &info) {
} catch (Magick::Warning &warning) {
cerr << "Warning: " << warning.what() << endl;
}
watermark.read("./assets/images/scott.png");
watermark.read(basePath + "assets/images/scott.png");
coalesceImages(&coalesced, frames.begin(), frames.end());
for (Image &image : coalesced) {

View File

@ -12,6 +12,7 @@ Napi::Value Sonic(const Napi::CallbackInfo &info) {
try {
Napi::Object obj = info[0].As<Napi::Object>();
string text = obj.Get("text").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
Blob blob;
@ -24,7 +25,7 @@ Napi::Value Sonic(const Napi::CallbackInfo &info) {
text_image.read("pango:<span foreground='white'>" + text + "</span>");
text_image.resize(Geometry(474, 332));
text_image.extent(Geometry("1024x538-435-145"), Magick::CenterGravity);
image.read("./assets/images/sonic.jpg");
image.read(basePath + "assets/images/sonic.jpg");
image.composite(text_image, Geometry("+160+10"), Magick::OverCompositeOp);
image.magick("PNG");
image.write(&blob);

View File

@ -26,6 +26,7 @@ Napi::Value Watermark(const Napi::CallbackInfo &info) {
? obj.Get("append").As<Napi::Boolean>().Value()
: false;
bool mc = obj.Has("mc") ? obj.Get("mc").As<Napi::Boolean>().Value() : false;
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -43,7 +44,8 @@ Napi::Value Watermark(const Napi::CallbackInfo &info) {
} catch (Magick::Warning &warning) {
cerr << "Warning: " << warning.what() << endl;
}
watermark.read(water);
string merged = basePath + water;
watermark.read(merged);
if (resize && append) {
string query(to_string(frames.front().baseColumns()) + "x");
watermark.scale(Geometry(query));

View File

@ -14,6 +14,7 @@ Napi::Value Wdt(const Napi::CallbackInfo &info) {
Napi::Object obj = info[0].As<Napi::Object>();
Napi::Buffer<char> data = obj.Get("data").As<Napi::Buffer<char>>();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -30,7 +31,7 @@ Napi::Value Wdt(const Napi::CallbackInfo &info) {
} catch (Magick::Warning &warning) {
cerr << "Warning: " << warning.what() << endl;
}
watermark.read("./assets/images/whodidthis.png");
watermark.read(basePath + "assets/images/whodidthis.png");
coalesceImages(&coalesced, frames.begin(), frames.end());
for (Image &image : coalesced) {

View File

@ -13,6 +13,7 @@ Napi::Value Zamn(const Napi::CallbackInfo &info) {
Napi::Object obj = info[0].As<Napi::Object>();
Napi::Buffer<char> data = obj.Get("data").As<Napi::Buffer<char>>();
string type = obj.Get("type").As<Napi::String>().Utf8Value();
string basePath = obj.Get("basePath").As<Napi::String>().Utf8Value();
int delay =
obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
@ -23,7 +24,7 @@ Napi::Value Zamn(const Napi::CallbackInfo &info) {
list<Image> mid;
Image watermark;
readImages(&frames, Blob(data.Data(), data.Length()));
watermark.read("./assets/images/zamn.png");
watermark.read(basePath + "assets/images/zamn.png");
coalesceImages(&coalesced, frames.begin(), frames.end());
for (Image &image : coalesced) {

View File

@ -1,10 +1,13 @@
import { createRequire } from "module";
import { isMainThread, parentPort, workerData } from "worker_threads";
import fetch from "node-fetch";
import path from "path";
import { fileURLToPath } from "url";
const nodeRequire = createRequire(import.meta.url);
const magick = nodeRequire(`../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`);
const relPath = `../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`;
const magick = nodeRequire(relPath);
const enumMap = {
"forget": 0,
@ -42,6 +45,7 @@ export default function run(object) {
objectWithFixedType.gravity = enumMap[objectWithFixedType.gravity];
}
}
objectWithFixedType.basePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../");
try {
const result = magick[object.cmd](objectWithFixedType);
const returnObject = {

View File

@ -5,6 +5,9 @@ import path from "path";
import { fileURLToPath } from "url";
import { Worker } from "worker_threads";
import { createRequire } from "module";
import { createServer } from "http";
import fetch from "node-fetch";
import EventEmitter from "events";
// only requiring this to work around an issue regarding worker threads
const nodeRequire = createRequire(import.meta.url);
@ -18,11 +21,14 @@ class ImageWorker extends BaseServiceWorker {
console.info = (str) => this.ipc.sendToAdmiral("info", str);
if (process.env.API === "true") {
this.jobs = {};
if (process.env.API_TYPE === "ws") {
this.connections = new Map();
this.servers = JSON.parse(fs.readFileSync(new URL("../../servers.json", import.meta.url), { encoding: "utf8" })).image;
this.nextID = 0;
} else if (process.env.API_TYPE === "azure") {
this.jobs = new Map();
this.webhook = createServer();
this.port = parseInt(process.env.WEBHOOK_PORT) || 3763;
}
this.begin().then(() => this.serviceReady());
@ -30,7 +36,7 @@ class ImageWorker extends BaseServiceWorker {
async begin() {
// connect to image api if enabled
if (process.env.API === "true") {
if (process.env.API_TYPE === "ws") {
for (const server of this.servers) {
try {
await this.connect(server.server, server.auth);
@ -38,6 +44,73 @@ class ImageWorker extends BaseServiceWorker {
logger.error(e);
}
}
} else if (process.env.API_TYPE === "azure") {
this.webhook.on("request", async (req, res) => {
if (req.method !== "POST") {
res.statusCode = 405;
return res.end("405 Method Not Allowed");
}
if (process.env.AZURE_PASS && req.headers.authorization !== process.env.AZURE_PASS) {
res.statusCode = 401;
return res.end("401 Unauthorized");
}
const reqUrl = new URL(req.url, `http://${req.headers.host}`);
if (reqUrl.pathname === "/callback") {
try {
const chunks = [];
req.on("data", (data) => {
chunks.push(data);
});
req.once("end", () => {
if (this.jobs.has(req.headers["x-azure-id"])) {
try {
const error = JSON.parse(Buffer.concat(chunks).toString());
if (error.error) this.jobs.get(req.headers["x-azure-id"]).emit("error", new Error(error.message));
} catch {
// no-op
}
const contentType = req.headers["content-type"];
let type;
switch (contentType) {
case "image/gif":
type = "gif";
break;
case "image/png":
type = "png";
break;
case "image/jpeg":
type = "jpg";
break;
case "image/webp":
type = "webp";
break;
default:
type = contentType;
break;
}
this.jobs.get(req.headers["x-azure-id"]).emit("image", { buffer: Buffer.concat(chunks), type });
return res.end("OK");
} else {
res.statusCode = 409;
return res.end("409 Conflict");
}
});
} catch (e) {
logger.error("An error occurred while processing a webhook request: ", e);
res.statusCode = 500;
return res.end("500 Internal Server Error");
}
} else {
res.statusCode = 404;
return res.end("404 Not Found");
}
});
this.webhook.on("error", (e) => {
logger.error("An error occurred on the Azure webhook: ", e);
});
this.webhook.listen(this.port, () => {
logger.log(`Azure HTTP webhook listening on port ${this.port}`);
});
}
}
@ -49,7 +122,7 @@ class ImageWorker extends BaseServiceWorker {
async getRunning() {
const statuses = [];
if (process.env.API === "true") {
if (process.env.API_TYPE === "ws") {
for (const [address, connection] of this.connections) {
if (connection.conn.readyState !== 0 && connection.conn.readyState !== 1) {
continue;
@ -115,8 +188,17 @@ class ImageWorker extends BaseServiceWorker {
});
}
waitForAzure(event) {
return new Promise((resolve, reject) => {
event.once("image", (data) => {
resolve(data);
});
event.once("error", reject);
});
}
async run(object) {
if (process.env.API === "true") {
if (process.env.API_TYPE === "ws") {
let num = this.nextID++;
if (num > 4294967295) num = this.nextID = 0;
for (let i = 0; i < 3; i++) {
@ -135,6 +217,12 @@ class ImageWorker extends BaseServiceWorker {
}
}
}
} else if (process.env.API_TYPE === "azure") {
object.callback = `${process.env.AZURE_CALLBACK_URL}:${this.port}/callback`;
const response = await fetch(`${process.env.AZURE_URL}/api/orchestrators/ImageOrchestrator`, { method: "POST", body: JSON.stringify(object) }).then(r => r.json());
const event = new EventEmitter();
this.jobs.set(response.id, event);
return await this.waitForAzure(event);
} else {
// Called from command (not using image API)
const worker = new Worker(path.join(path.dirname(fileURLToPath(import.meta.url)), "../image-runner.js"), {

View File

@ -22,7 +22,7 @@ class PrometheusWorker extends BaseServiceWorker {
# HELP esmbot_shard_count Number of shards the bot has
# TYPE esmbot_shard_count gauge
`);
if (process.env.API === "true") {
if (process.env.API_TYPE === "ws") {
const servers = await this.ipc.serviceCommand("image", { type: "stats" }, true);
res.write(`# HELP esmbot_connected_workers Number of workers connected
# TYPE esmbot_connected_workers gauge