Compare commits

...

10 Commits

Author SHA1 Message Date
Louis Lam
7bcef8edf9 WIP 2023-01-09 00:35:52 +08:00
Louis Lam
42f1a9ff34 Merge remote-tracking branch 'origin/master' into plugins 2023-01-08 14:13:13 +08:00
Louis Lam
fae45ee3b4 WIP 2022-12-30 13:33:03 +08:00
Louis Lam
c1b94fda13 WIP 2022-12-27 22:59:53 +08:00
Louis Lam
7e3c752409 Merge remote-tracking branch 'origin/master' into plugins
# Conflicts:
#	server/server.js
#	server/uptime-kuma-server.js
#	src/languages/en.js
#	src/router.js
2022-12-27 21:47:36 +08:00
Louis Lam
4265c6f66a WIP: Plugin List 2022-09-17 01:17:04 +08:00
Louis Lam
53c06c2b06 WIP: Plugin List 2022-09-16 17:45:46 +08:00
Louis Lam
6d12fb2c2c Merge remote-tracking branch 'origin/master' into plugins
# Conflicts:
#	server/uptime-kuma-server.js
2022-09-16 16:38:23 +08:00
Louis Lam
b712748867 Testing 2022-06-17 16:05:00 +08:00
Louis Lam
aa872bf782 working 2022-06-17 15:26:49 +08:00
15 changed files with 389 additions and 4 deletions

View File

@ -4,6 +4,7 @@ const { setSetting, setting } = require("./util-server");
const { log, sleep } = require("../src/util");
const dayjs = require("dayjs");
const knex = require("knex");
const { PluginsManager } = require("./plugins-manager");
/**
* Database & App Data Folder
@ -83,6 +84,13 @@ class Database {
static init(args) {
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
// Plugin feature is working only if the dataDir = "./data";
if (Database.dataDir !== "./data/") {
log.warn("PLUGIN", "Warning: In order to enable plugin feature, you need to use the default data directory: ./data/");
PluginsManager.disable = true;
}
Database.path = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });

View File

@ -617,6 +617,15 @@ class Monitor extends BeanModel {
}
}
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type in UptimeKumaServer.monitorTypeList) {
let startTime = dayjs().valueOf();
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
await monitorType.check(this, bean);
if (!bean.ping) {
bean.ping = dayjs().valueOf() - startTime;
}
} else {
bean.msg = "Unknown Monitor Type";
bean.status = PENDING;

View File

@ -0,0 +1,19 @@
class MonitorType {
name = undefined;
/**
*
* @param {Monitor} monitor
* @param {Heartbeat} heartbeat
* @returns {Promise<void>}
*/
async check(monitor, heartbeat) {
throw new Error("You need to override check()");
}
}
module.exports = {
MonitorType,
};

13
server/plugin.js Normal file
View File

@ -0,0 +1,13 @@
class Plugin {
async install() {
}
async uninstall() {
}
}
module.exports = {
Plugin,
};

141
server/plugins-manager.js Normal file
View File

@ -0,0 +1,141 @@
const fs = require("fs");
const { log } = require("../src/util");
const path = require("path");
class PluginsManager {
static disable = false;
/**
* Plugin List
* @type {PluginWrapper[]}
*/
pluginList = [];
/**
* Plugins Dir
*/
pluginsDir;
/**
*
* @param {UptimeKumaServer} server
* @param {string} dir
*/
constructor(server) {
if (!PluginsManager.disable) {
this.pluginsDir = "./data/plugins/";
if (! fs.existsSync(this.pluginsDir)) {
fs.mkdirSync(this.pluginsDir, { recursive: true });
}
log.debug("plugin", "Scanning plugin directory");
let list = fs.readdirSync(this.pluginsDir);
this.pluginList = [];
for (let item of list) {
let plugin = new PluginWrapper(server, this.pluginsDir + item);
try {
plugin.load();
this.pluginList.push(plugin);
} catch (e) {
log.error("plugin", "Failed to load plugin: " + this.pluginsDir + item);
log.error("plugin", "Reason: " + e.message);
}
}
} else {
log.warn("PLUGIN", "Skip scanning plugin directory");
}
}
/**
* Install a Plugin
* @param {string} tarGzFileURL The url of tar.gz file
* @param {number} userID User ID - Used for streaming installing progress
*/
installPlugin(tarGzFileURL, userID = undefined) {
}
/**
* Remove a plugin
* @param pluginID
*/
removePlugin(pluginID) {
}
/**
* Update a plugin
* Only available for plugins which were downloaded from the official list
* @param pluginID
*/
updatePlugin(pluginID) {
}
}
class PluginWrapper {
server = undefined;
pluginDir = undefined;
/**
* Must be an `new-able` class.
* @type {function}
*/
pluginClass = undefined;
/**
*
* @type {*}
*/
object = undefined;
info = {};
/**
*
* @param {UptimeKumaServer} server
* @param {string} pluginDir
*/
constructor(server, pluginDir) {
this.server = server;
this.pluginDir = pluginDir;
}
load() {
let indexFile = this.pluginDir + "/index.js";
let packageJSON = this.pluginDir + "/package.json";
if (fs.existsSync(indexFile)) {
this.pluginClass = require(path.join(process.cwd(), indexFile));
let pluginClassType = typeof this.pluginClass;
if (pluginClassType === "function") {
this.object = new this.pluginClass(this.server);
} else {
throw new Error("Invalid plugin, it does not export a class");
}
if (fs.existsSync(packageJSON)) {
this.info = require(path.join(process.cwd(), packageJSON));
} else {
this.info.fullName = this.pluginDir;
this.info.name = "[unknown]";
this.info.version = "[unknown-version]";
}
log.info("plugin", `${this.info.fullName} v${this.info.version} loaded`);
}
}
}
module.exports = {
PluginsManager,
PluginWrapper
};

View File

@ -138,6 +138,7 @@ const { maintenanceSocketHandler } = require("./socket-handlers/maintenance-sock
const { generalSocketHandler } = require("./socket-handlers/general-socket-handler");
const { Settings } = require("./settings");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { pluginsHandler } = require("./socket-handlers/plugins-handler");
app.use(express.json());
@ -166,7 +167,7 @@ let needSetup = false;
Database.init(args);
await initDatabase(testMode);
await server.initAfterDatabaseReady();
server.loadPlugins();
server.entryPage = await Settings.get("entryPage");
await StatusPage.loadDomainMappingList();
@ -1491,6 +1492,7 @@ let needSetup = false;
dockerSocketHandler(socket);
maintenanceSocketHandler(socket);
generalSocketHandler(socket, server);
pluginsHandler(socket);
log.debug("server", "added all socket handlers");

View File

@ -0,0 +1,25 @@
const { checkLogin } = require("../util-server");
const axios = require("axios");
/**
* Handlers for plugins
* @param {Socket} socket Socket.io instance
*/
module.exports.pluginsHandler = (socket) => {
// Get Plugin List
socket.on("getPluginList", async (callback) => {
try {
checkLogin(socket);
const res = await axios.get("https://uptime.kuma.pet/c/plugins.json");
callback(res.data);
} catch (error) {
console.log(error);
callback({
ok: false,
msg: error.message,
});
}
});
};

View File

@ -10,6 +10,7 @@ const util = require("util");
const { CacheableDnsHttpAgent } = require("./cacheable-dns-http-agent");
const { Settings } = require("./settings");
const dayjs = require("dayjs");
const { PluginsManager } = require("./plugins-manager");
// DO NOT IMPORT HERE IF THE MODULES USED `UptimeKumaServer.getInstance()`
/**
@ -48,6 +49,20 @@ class UptimeKumaServer {
generateMaintenanceTimeslotsInterval = undefined;
/**
* Plugins Manager
* @type {PluginsManager}
*/
pluginsManager = null;
/**
*
* @type {{}}
*/
static monitorTypeList = {
};
static getInstance(args) {
if (UptimeKumaServer.instance == null) {
UptimeKumaServer.instance = new UptimeKumaServer(args);
@ -240,6 +255,38 @@ class UptimeKumaServer {
async stop() {
clearTimeout(this.generateMaintenanceTimeslotsInterval);
}
loadPlugins(dir) {
this.pluginsManager = new PluginsManager(this);
}
/**
*
* @param {MonitorType} monitorType
*/
addMonitorType(monitorType) {
if (monitorType instanceof MonitorType && monitorType.name) {
if (monitorType.name in UptimeKumaServer.monitorTypeList) {
log.error("", "Conflict Monitor Type name");
}
UptimeKumaServer.monitorTypeList[monitorType.name] = monitorType;
} else {
log.error("", "Invalid Monitor Type: " + monitorType.name);
}
}
/**
*
* @param {MonitorType} monitorType
*/
removeMonitorType(monitorType) {
if (UptimeKumaServer.monitorTypeList[monitorType.name] === monitorType) {
delete UptimeKumaServer.monitorTypeList[monitorType.name];
} else {
log.error("", "Remove MonitorType failed: " + monitorType.name);
}
}
}
module.exports = {
@ -248,3 +295,4 @@ module.exports = {
// Must be at the end
const MaintenanceTimeslot = require("./model/maintenance_timeslot");
const { MonitorType } = require("./monitor-types/monitor-type");

View File

@ -0,0 +1,45 @@
<template>
<div class="plugin-item pt-4 pb-2">
<div>
<h6>{{ plugin.name }}</h6>
<p class="description">
{{ plugin.description }}
</p>
<span class="version">{{ $t("Version") }}: {{ plugin.version }}</span>
</div>
<div class="buttons">
<button class="btn btn-primary">Install</button>
</div>
</div>
</template>
<script>
export default {
props: {
plugin: {
type: Object,
required: true,
},
}
};
</script>
<style lang="scss" scoped>
@import "../assets/vars.scss";
.plugin-item {
display: flex;
justify-content: space-between;
align-content: center;
align-items: center;
.description {
font-size: 13px;
margin-bottom: 0;
}
.version {
font-size: 13px;
}
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div>
<div class="mt-3">{{ remotePluginListMsg }}</div>
<PluginItem v-for="plugin in remotePluginList" :key="plugin.id" :plugin="plugin" />
</div>
</template>
<script>
import PluginItem from "../PluginItem.vue";
export default {
components: {
PluginItem
},
data() {
return {
remotePluginList: [],
remotePluginListMsg: "",
};
},
computed: {
pluginList() {
return this.$parent.$parent.$parent.pluginList;
},
settings() {
return this.$parent.$parent.$parent.settings;
},
saveSettings() {
return this.$parent.$parent.$parent.saveSettings;
},
settingsLoaded() {
return this.$parent.$parent.$parent.settingsLoaded;
},
},
async mounted() {
this.remotePluginListMsg = this.$t("Loading") + "...";
this.$root.getSocket().emit("getPluginList", (res) => {
if (res.ok) {
this.remotePluginList = res.pluginList;
this.remotePluginListMsg = "";
} else {
this.remotePluginListMsg = this.$t("loadingError") + " " + res.message;
}
});
},
methods: {
},
};
</script>
<style lang="scss" scoped>
</style>

View File

@ -675,4 +675,5 @@ export default {
"General Monitor Type": "General Monitor Type",
"Passive Monitor Type": "Passive Monitor Type",
"Specific Monitor Type": "Specific Monitor Type",
loadingError: "Cannot fetch the data, please try again later.",
};

View File

@ -61,6 +61,12 @@
Radius
</option>
</optgroup>
<optgroup :label="$t('Custom Monitor Type')">
<option value="browser">
(Early Access/WIP) HTTP(s) (Browser Engine)
</option>
</optgroup>
</select>
</div>
@ -71,7 +77,7 @@
</div>
<!-- URL -->
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' " class="my-3">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'browser' " class="my-3">
<label for="url" class="form-label">{{ $t("URL") }}</label>
<input id="url" v-model="monitor.url" type="url" class="form-control" pattern="https?://.+" required>
</div>
@ -93,10 +99,10 @@
</div>
<!-- Keyword -->
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword' " class="my-3">
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="my-3">
<label for="keyword" class="form-label">{{ $t("Keyword") }}</label>
<input id="keyword" v-model="monitor.keyword" type="text" class="form-control" required>
<div class="form-text">
<div v-if="monitor.type === 'keyword' || monitor.type === 'grpc-keyword'" class="form-text">
{{ $t("keywordDescription") }}
</div>
</div>

View File

@ -110,6 +110,9 @@ export default {
backup: {
title: this.$t("Backup"),
},
plugins: {
title: this.$t("Plugins"),
},
about: {
title: this.$t("About"),
},

View File

@ -18,6 +18,7 @@ import NotFound from "./pages/NotFound.vue";
import DockerHosts from "./components/settings/Docker.vue";
import MaintenanceDetails from "./pages/MaintenanceDetails.vue";
import ManageMaintenance from "./pages/ManageMaintenance.vue";
import Plugins from "./components/settings/Plugins.vue";
// Settings - Sub Pages
import Appearance from "./components/settings/Appearance.vue";
@ -30,6 +31,7 @@ import Proxies from "./components/settings/Proxies.vue";
import Backup from "./components/settings/Backup.vue";
import About from "./components/settings/About.vue";
const routes = [
{
path: "/",
@ -115,6 +117,10 @@ const routes = [
path: "backup",
component: Backup,
},
{
path: "plugins",
component: Plugins,
},
{
path: "about",
component: About,

Binary file not shown.