Matthew Macdonald-Wallace
3 years ago
committed by
GitHub
208 changed files with 38779 additions and 9777 deletions
@ -0,0 +1,12 @@ |
|||
# These are supported funding model platforms |
|||
|
|||
github: louislam # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] |
|||
#patreon: # Replace with a single Patreon username |
|||
open_collective: uptime-kuma # Replace with a single Open Collective username |
|||
#ko_fi: # Replace with a single Ko-fi username |
|||
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel |
|||
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry |
|||
#liberapay: # Replace with a single Liberapay username |
|||
#issuehunt: # Replace with a single IssueHunt username |
|||
#otechie: # Replace with a single Otechie username |
|||
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] |
@ -0,0 +1,35 @@ |
|||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node |
|||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions |
|||
|
|||
name: Auto Test |
|||
|
|||
on: |
|||
push: |
|||
branches: [ master ] |
|||
pull_request: |
|||
branches: [ master ] |
|||
|
|||
jobs: |
|||
auto-test: |
|||
runs-on: ${{ matrix.os }} |
|||
|
|||
strategy: |
|||
matrix: |
|||
os: [macos-latest, ubuntu-latest, windows-latest] |
|||
node-version: [14.x, 16.x] |
|||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ |
|||
|
|||
steps: |
|||
- uses: actions/checkout@v2 |
|||
|
|||
- name: Use Node.js ${{ matrix.node-version }} |
|||
uses: actions/setup-node@v2 |
|||
with: |
|||
node-version: ${{ matrix.node-version }} |
|||
cache: 'npm' |
|||
- run: npm run install-legacy |
|||
- run: npm run build |
|||
- run: npm test |
|||
env: |
|||
HEADLESS_TEST: 1 |
|||
JUST_FOR_TEST: ${{ secrets.JUST_FOR_TEST }} |
@ -1,3 +1,9 @@ |
|||
{ |
|||
"extends": "stylelint-config-recommended", |
|||
"extends": "stylelint-config-standard", |
|||
"rules": { |
|||
"indentation": 4, |
|||
"no-descending-specificity": null, |
|||
"selector-list-comma-newline-after": null, |
|||
"declaration-empty-line-before": null |
|||
} |
|||
} |
|||
|
@ -0,0 +1 @@ |
|||
git.kuma.pet |
@ -0,0 +1,31 @@ |
|||
# Security Policy |
|||
|
|||
## Supported Versions |
|||
|
|||
Use this section to tell people about which versions of your project are |
|||
currently being supported with security updates. |
|||
|
|||
### Uptime Kuma Versions |
|||
|
|||
| Version | Supported | |
|||
| ------- | ------------------ | |
|||
| 1.7.X | :white_check_mark: | |
|||
| < 1.7 | ❌ | |
|||
|
|||
### Upgradable Docker Tags |
|||
|
|||
| Tag | Supported | |
|||
| ------- | ------------------ | |
|||
| 1 | :white_check_mark: | |
|||
| 1-debian | :white_check_mark: | |
|||
| 1-alpine | :white_check_mark: | |
|||
| latest | :white_check_mark: | |
|||
| debian | :white_check_mark: | |
|||
| alpine | :white_check_mark: | |
|||
| All other tags | ❌ | |
|||
|
|||
## Reporting a Vulnerability |
|||
|
|||
Please report security issues to uptime@kuma.pet. |
|||
|
|||
Do not use the issue tracker or discuss it in the public as it will cause more damage. |
@ -0,0 +1,11 @@ |
|||
const config = {}; |
|||
|
|||
if (process.env.TEST_FRONTEND) { |
|||
config.presets = ["@babel/preset-env"]; |
|||
} |
|||
|
|||
if (process.env.TEST_BACKEND) { |
|||
config.plugins = ["babel-plugin-rewire"]; |
|||
} |
|||
|
|||
module.exports = config; |
@ -0,0 +1,5 @@ |
|||
module.exports = { |
|||
"rootDir": "..", |
|||
"testRegex": "./test/backend.spec.js", |
|||
}; |
|||
|
@ -0,0 +1,5 @@ |
|||
module.exports = { |
|||
"rootDir": "..", |
|||
"testRegex": "./test/frontend.spec.js", |
|||
}; |
|||
|
@ -0,0 +1,6 @@ |
|||
module.exports = { |
|||
"launch": { |
|||
"headless": process.env.HEADLESS_TEST || false, |
|||
"userDataDir": "./data/test-chrome-profile", |
|||
} |
|||
}; |
@ -0,0 +1,11 @@ |
|||
module.exports = { |
|||
"verbose": true, |
|||
"preset": "jest-puppeteer", |
|||
"globals": { |
|||
"__DEV__": true |
|||
}, |
|||
"testRegex": "./test/e2e.spec.js", |
|||
"rootDir": "..", |
|||
"testTimeout": 30000, |
|||
}; |
|||
|
@ -0,0 +1,24 @@ |
|||
import legacy from "@vitejs/plugin-legacy"; |
|||
import vue from "@vitejs/plugin-vue"; |
|||
import { defineConfig } from "vite"; |
|||
|
|||
const postCssScss = require("postcss-scss"); |
|||
const postcssRTLCSS = require("postcss-rtlcss"); |
|||
|
|||
// https://vitejs.dev/config/
|
|||
export default defineConfig({ |
|||
plugins: [ |
|||
vue(), |
|||
legacy({ |
|||
targets: ["ie > 11"], |
|||
additionalLegacyPolyfills: ["regenerator-runtime/runtime"] |
|||
}) |
|||
], |
|||
css: { |
|||
postcss: { |
|||
"parser": postCssScss, |
|||
"map": false, |
|||
"plugins": [postcssRTLCSS] |
|||
} |
|||
}, |
|||
}); |
@ -0,0 +1,10 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE user |
|||
ADD twofa_secret VARCHAR(64); |
|||
|
|||
ALTER TABLE user |
|||
ADD twofa_status BOOLEAN default 0 NOT NULL; |
|||
|
|||
COMMIT; |
@ -0,0 +1,7 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE monitor |
|||
ADD retry_interval INTEGER default 0 not null; |
|||
|
|||
COMMIT; |
@ -0,0 +1,30 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
create table `group` |
|||
( |
|||
id INTEGER not null |
|||
constraint group_pk |
|||
primary key autoincrement, |
|||
name VARCHAR(255) not null, |
|||
created_date DATETIME default (DATETIME('now')) not null, |
|||
public BOOLEAN default 0 not null, |
|||
active BOOLEAN default 1 not null, |
|||
weight BOOLEAN NOT NULL DEFAULT 1000 |
|||
); |
|||
|
|||
CREATE TABLE [monitor_group] |
|||
( |
|||
[id] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, |
|||
[monitor_id] INTEGER NOT NULL REFERENCES [monitor] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, |
|||
[group_id] INTEGER NOT NULL REFERENCES [group] ([id]) ON DELETE CASCADE ON UPDATE CASCADE, |
|||
weight BOOLEAN NOT NULL DEFAULT 1000 |
|||
); |
|||
|
|||
CREATE INDEX [fk] |
|||
ON [monitor_group] ( |
|||
[monitor_id], |
|||
[group_id]); |
|||
|
|||
|
|||
COMMIT; |
@ -0,0 +1,10 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
-- For sendHeartbeatList |
|||
CREATE INDEX monitor_time_index ON heartbeat (monitor_id, time); |
|||
|
|||
-- For sendImportantHeartbeatList |
|||
CREATE INDEX monitor_important_time_index ON heartbeat (monitor_id, important,time); |
|||
|
|||
COMMIT; |
@ -0,0 +1,18 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
create table incident |
|||
( |
|||
id INTEGER not null |
|||
constraint incident_pk |
|||
primary key autoincrement, |
|||
title VARCHAR(255) not null, |
|||
content TEXT not null, |
|||
style VARCHAR(30) default 'warning' not null, |
|||
created_date DATETIME default (DATETIME('now')) not null, |
|||
last_updated_date DATETIME, |
|||
pin BOOLEAN default 1 not null, |
|||
active BOOLEAN default 1 not null |
|||
); |
|||
|
|||
COMMIT; |
@ -0,0 +1,7 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE monitor |
|||
ADD push_token VARCHAR(20) DEFAULT NULL; |
|||
|
|||
COMMIT; |
@ -0,0 +1,22 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
-- Generated by Intellij IDEA |
|||
create table setting_dg_tmp |
|||
( |
|||
id INTEGER |
|||
primary key autoincrement, |
|||
key VARCHAR(200) not null |
|||
unique, |
|||
value TEXT, |
|||
type VARCHAR(20) |
|||
); |
|||
|
|||
insert into setting_dg_tmp(id, key, value, type) select id, key, value, type from setting; |
|||
|
|||
drop table setting; |
|||
|
|||
alter table setting_dg_tmp rename to setting; |
|||
|
|||
|
|||
COMMIT; |
@ -0,0 +1,19 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
CREATE TABLE tag ( |
|||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
|||
name VARCHAR(255) NOT NULL, |
|||
color VARCHAR(255) NOT NULL, |
|||
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL |
|||
); |
|||
|
|||
CREATE TABLE monitor_tag ( |
|||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
|||
monitor_id INTEGER NOT NULL, |
|||
tag_id INTEGER NOT NULL, |
|||
value TEXT, |
|||
CONSTRAINT FK_tag FOREIGN KEY (tag_id) REFERENCES tag(id) ON DELETE CASCADE ON UPDATE CASCADE, |
|||
CONSTRAINT FK_monitor FOREIGN KEY (monitor_id) REFERENCES monitor(id) ON DELETE CASCADE ON UPDATE CASCADE |
|||
); |
|||
|
|||
CREATE INDEX monitor_tag_monitor_id_index ON monitor_tag (monitor_id); |
|||
CREATE INDEX monitor_tag_tag_id_index ON monitor_tag (tag_id); |
@ -0,0 +1,10 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE monitor |
|||
ADD dns_resolve_type VARCHAR(5); |
|||
|
|||
ALTER TABLE monitor |
|||
ADD dns_resolve_server VARCHAR(255); |
|||
|
|||
COMMIT; |
@ -0,0 +1,7 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE monitor |
|||
ADD dns_last_result VARCHAR(255); |
|||
|
|||
COMMIT; |
@ -0,0 +1,7 @@ |
|||
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db. |
|||
BEGIN TRANSACTION; |
|||
|
|||
ALTER TABLE notification |
|||
ADD is_default BOOLEAN default 0 NOT NULL; |
|||
|
|||
COMMIT; |
@ -0,0 +1,8 @@ |
|||
# DON'T UPDATE TO alpine3.13, 1.14, see #41. |
|||
FROM node:14-alpine3.12 |
|||
WORKDIR /app |
|||
|
|||
# Install apprise, iputils for non-root ping, setpriv |
|||
RUN apk add --no-cache iputils setpriv dumb-init python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib && \ |
|||
pip3 --no-cache-dir install apprise && \ |
|||
rm -rf /root/.cache |
@ -0,0 +1,12 @@ |
|||
# DON'T UPDATE TO node:14-bullseye-slim, see #372. |
|||
# If the image changed, the second stage image should be changed too |
|||
FROM node:14-buster-slim |
|||
WORKDIR /app |
|||
|
|||
# Install Apprise, add sqlite3 cli for debugging in the future, iputils-ping for ping, util-linux for setpriv |
|||
# Stupid python3 and python3-pip actually install a lot of useless things into Debian, specific --no-install-recommends to skip them, make the base even smaller than alpine! |
|||
RUN apt update && \ |
|||
apt --yes --no-install-recommends install python3 python3-pip python3-cryptography python3-six python3-yaml python3-click python3-markdown python3-requests python3-requests-oauthlib \ |
|||
sqlite3 iputils-ping util-linux dumb-init && \ |
|||
pip3 --no-cache-dir install apprise && \ |
|||
rm -rf /var/lib/apt/lists/* |
@ -0,0 +1,51 @@ |
|||
FROM louislam/uptime-kuma:base-debian AS build |
|||
WORKDIR /app |
|||
|
|||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 |
|||
|
|||
COPY . . |
|||
RUN npm ci && \ |
|||
npm run build && \ |
|||
npm ci --production && \ |
|||
chmod +x /app/extra/entrypoint.sh |
|||
|
|||
|
|||
FROM louislam/uptime-kuma:base-debian AS release |
|||
WORKDIR /app |
|||
|
|||
# Copy app files from build layer |
|||
COPY --from=build /app /app |
|||
|
|||
EXPOSE 3001 |
|||
VOLUME ["/app/data"] |
|||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js |
|||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"] |
|||
CMD ["node", "server/server.js"] |
|||
|
|||
FROM release AS nightly |
|||
RUN npm run mark-as-nightly |
|||
|
|||
# Upload the artifact to Github |
|||
FROM louislam/uptime-kuma:base-debian AS upload-artifact |
|||
WORKDIR / |
|||
RUN apt update && \ |
|||
apt --yes install curl file |
|||
|
|||
ARG GITHUB_TOKEN |
|||
ARG TARGETARCH |
|||
ARG PLATFORM=debian |
|||
ARG VERSION |
|||
ARG FILE=$PLATFORM-$TARGETARCH-$VERSION.tar.gz |
|||
ARG DIST=dist.tar.gz |
|||
|
|||
COPY --from=build /app /app |
|||
RUN chmod +x /app/extra/upload-github-release-asset.sh |
|||
|
|||
# Full Build |
|||
# RUN tar -zcvf $FILE app |
|||
# RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=$FILE |
|||
|
|||
# Dist only |
|||
RUN cd /app && tar -zcvf $DIST dist |
|||
RUN /app/extra/upload-github-release-asset.sh github_api_token=$GITHUB_TOKEN owner=louislam repo=uptime-kuma tag=$VERSION filename=/app/$DIST |
|||
|
@ -0,0 +1,26 @@ |
|||
FROM louislam/uptime-kuma:base-alpine AS build |
|||
WORKDIR /app |
|||
|
|||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 |
|||
|
|||
COPY . . |
|||
RUN npm ci && \ |
|||
npm run build && \ |
|||
npm ci --production && \ |
|||
chmod +x /app/extra/entrypoint.sh |
|||
|
|||
|
|||
FROM louislam/uptime-kuma:base-alpine AS release |
|||
WORKDIR /app |
|||
|
|||
# Copy app files from build layer |
|||
COPY --from=build /app /app |
|||
|
|||
EXPOSE 3001 |
|||
VOLUME ["/app/data"] |
|||
HEALTHCHECK --interval=60s --timeout=30s --start-period=180s --retries=5 CMD node extra/healthcheck.js |
|||
ENTRYPOINT ["/usr/bin/dumb-init", "--", "extra/entrypoint.sh"] |
|||
CMD ["node", "server/server.js"] |
|||
|
|||
FROM release AS nightly |
|||
RUN npm run mark-as-nightly |
@ -1,28 +0,0 @@ |
|||
# DON'T UPDATE TO alpine3.13, 1.14, see #41. |
|||
FROM node:14-alpine3.12 AS release |
|||
WORKDIR /app |
|||
|
|||
# split the sqlite install here, so that it can caches the arm prebuilt |
|||
RUN apk add --no-cache --virtual .build-deps make g++ python3 python3-dev && \ |
|||
ln -s /usr/bin/python3 /usr/bin/python && \ |
|||
npm install @louislam/sqlite3@5.0.3 bcrypt@5.0.1 && \ |
|||
apk del .build-deps && \ |
|||
rm -f /usr/bin/python |
|||
|
|||
# Touching above code may causes sqlite3 re-compile again, painful slow. |
|||
|
|||
# Install apprise |
|||
RUN apk add --no-cache python3 py3-cryptography py3-pip py3-six py3-yaml py3-click py3-markdown py3-requests py3-requests-oauthlib |
|||
RUN pip3 --no-cache-dir install apprise && \ |
|||
rm -rf /root/.cache |
|||
|
|||
COPY . . |
|||
RUN npm install && npm run build && npm prune |
|||
|
|||
EXPOSE 3001 |
|||
VOLUME ["/app/data"] |
|||
HEALTHCHECK --interval=60s --timeout=30s --start-period=300s CMD node extra/healthcheck.js |
|||
CMD ["npm", "run", "start-server"] |
|||
|
|||
FROM release AS nightly |
|||
RUN npm run mark-as-nightly |
@ -0,0 +1,6 @@ |
|||
module.exports = { |
|||
apps: [{ |
|||
name: "uptime-kuma", |
|||
script: "./server/server.js", |
|||
}] |
|||
} |
@ -1 +1,2 @@ |
|||
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh |
|||
# Must enable File Sharing in Docker Desktop |
|||
docker run -it --rm -v ${pwd}:/app louislam/batsh /usr/bin/batsh bash --output ./install.sh ./extra/install.batsh |
|||
|
@ -0,0 +1,57 @@ |
|||
console.log("Downloading dist"); |
|||
const https = require("https"); |
|||
const tar = require("tar"); |
|||
|
|||
const packageJSON = require("../package.json"); |
|||
const fs = require("fs"); |
|||
const version = packageJSON.version; |
|||
|
|||
const filename = "dist.tar.gz"; |
|||
|
|||
const url = `https://github.com/louislam/uptime-kuma/releases/download/${version}/${filename}`; |
|||
download(url); |
|||
|
|||
function download(url) { |
|||
console.log(url); |
|||
|
|||
https.get(url, (response) => { |
|||
if (response.statusCode === 200) { |
|||
console.log("Extracting dist..."); |
|||
|
|||
if (fs.existsSync("./dist")) { |
|||
|
|||
if (fs.existsSync("./dist-backup")) { |
|||
fs.rmdirSync("./dist-backup", { |
|||
recursive: true |
|||
}); |
|||
} |
|||
|
|||
fs.renameSync("./dist", "./dist-backup"); |
|||
} |
|||
|
|||
const tarStream = tar.x({ |
|||
cwd: "./", |
|||
}); |
|||
|
|||
tarStream.on("close", () => { |
|||
fs.rmdirSync("./dist-backup", { |
|||
recursive: true |
|||
}); |
|||
console.log("Done"); |
|||
}); |
|||
|
|||
tarStream.on("error", () => { |
|||
if (fs.existsSync("./dist-backup")) { |
|||
fs.renameSync("./dist-backup", "./dist"); |
|||
} |
|||
console.log("Done"); |
|||
}); |
|||
|
|||
response.pipe(tarStream); |
|||
} else if (response.statusCode === 302) { |
|||
download(response.headers.location); |
|||
} else { |
|||
console.log("dist not found"); |
|||
} |
|||
}); |
|||
} |
@ -0,0 +1,21 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# set -e Exit the script if an error happens |
|||
set -e |
|||
PUID=${PUID=0} |
|||
PGID=${PGID=0} |
|||
|
|||
files_ownership () { |
|||
# -h Changes the ownership of an encountered symbolic link and not that of the file or directory pointed to by the symbolic link. |
|||
# -R Recursively descends the specified directories |
|||
# -c Like verbose but report only when a change is made |
|||
chown -hRc "$PUID":"$PGID" /app/data |
|||
} |
|||
|
|||
echo "==> Performing startup jobs and maintenance tasks" |
|||
files_ownership |
|||
|
|||
echo "==> Starting application with user $PUID group $PGID" |
|||
|
|||
# --clear-groups Clear supplementary groups. |
|||
exec setpriv --reuid "$PUID" --regid "$PGID" --clear-groups "$@" |
@ -1,19 +1,34 @@ |
|||
let http = require("http"); |
|||
/* |
|||
* This script should be run after a period of time (180s), because the server may need some time to prepare. |
|||
*/ |
|||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; |
|||
|
|||
let client; |
|||
|
|||
if (process.env.SSL_KEY && process.env.SSL_CERT) { |
|||
client = require("https"); |
|||
} else { |
|||
client = require("http"); |
|||
} |
|||
|
|||
let options = { |
|||
host: "localhost", |
|||
port: "3001", |
|||
timeout: 2000, |
|||
host: process.env.HOST || "127.0.0.1", |
|||
port: parseInt(process.env.PORT) || 3001, |
|||
timeout: 28 * 1000, |
|||
}; |
|||
let request = http.request(options, (res) => { |
|||
console.log(`STATUS: ${res.statusCode}`); |
|||
if (res.statusCode == 200) { |
|||
|
|||
let request = client.request(options, (res) => { |
|||
console.log(`Health Check OK [Res Code: ${res.statusCode}]`); |
|||
if (res.statusCode === 200) { |
|||
process.exit(0); |
|||
} else { |
|||
process.exit(1); |
|||
} |
|||
}); |
|||
|
|||
request.on("error", function (err) { |
|||
console.log("ERROR"); |
|||
console.error("Health Check ERROR"); |
|||
process.exit(1); |
|||
}); |
|||
|
|||
request.end(); |
|||
|
@ -0,0 +1,245 @@ |
|||
// install.sh is generated by ./extra/install.batsh, do not modify it directly. |
|||
// "npm run compile-install-script" to compile install.sh |
|||
// The command is working on Windows PowerShell and Docker for Windows only. |
|||
|
|||
|
|||
// curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh |
|||
println("====================="); |
|||
println("Uptime Kuma Installer"); |
|||
println("====================="); |
|||
println("Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian"); |
|||
println("---------------------------------------"); |
|||
println("This script is designed for Linux and basic usage."); |
|||
println("For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation"); |
|||
println("---------------------------------------"); |
|||
println(""); |
|||
println("Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2"); |
|||
println("Docker - Install Uptime Kuma Docker container"); |
|||
println(""); |
|||
|
|||
if ("$1" != "") { |
|||
type = "$1"; |
|||
} else { |
|||
call("read", "-p", "Which installation method do you prefer? [DOCKER/local]: ", "type"); |
|||
} |
|||
|
|||
defaultPort = "3001"; |
|||
|
|||
function checkNode() { |
|||
bash("nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])')"); |
|||
println("Node Version: " ++ nodeVersion); |
|||
|
|||
if (nodeVersion < "12") { |
|||
println("Error: Required Node.js 14"); |
|||
call("exit", "1"); |
|||
} |
|||
|
|||
if (nodeVersion == "12") { |
|||
println("Warning: NodeJS " ++ nodeVersion ++ " is not tested."); |
|||
} |
|||
} |
|||
|
|||
function deb() { |
|||
bash("nodeCheck=$(node -v)"); |
|||
bash("apt --yes update"); |
|||
|
|||
if (nodeCheck != "") { |
|||
checkNode(); |
|||
} else { |
|||
|
|||
// Old nodejs binary name is "nodejs" |
|||
bash("check=$(nodejs --version)"); |
|||
if (check != "") { |
|||
println("Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old."); |
|||
bash("exit 1"); |
|||
} |
|||
|
|||
bash("curlCheck=$(curl --version)"); |
|||
if (curlCheck == "") { |
|||
println("Installing Curl"); |
|||
bash("apt --yes install curl"); |
|||
} |
|||
|
|||
println("Installing Node.js 14"); |
|||
bash("curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt"); |
|||
bash("apt --yes install nodejs"); |
|||
bash("node -v"); |
|||
|
|||
bash("nodeCheckAgain=$(node -v)"); |
|||
|
|||
if (nodeCheckAgain == "") { |
|||
println("Error during Node.js installation"); |
|||
bash("exit 1"); |
|||
} |
|||
} |
|||
|
|||
bash("check=$(git --version)"); |
|||
if (check == "") { |
|||
println("Installing Git"); |
|||
bash("apt --yes install git"); |
|||
} |
|||
} |
|||
|
|||
if (type == "local") { |
|||
defaultInstallPath = "/opt/uptime-kuma"; |
|||
|
|||
if (exists("/etc/redhat-release")) { |
|||
os = call("cat", "/etc/redhat-release"); |
|||
distribution = "rhel"; |
|||
|
|||
} else if (exists("/etc/issue")) { |
|||
bash("os=$(head -n1 /etc/issue | cut -f 1 -d ' ')"); |
|||
if (os == "Ubuntu") { |
|||
distribution = "ubuntu"; |
|||
} |
|||
if (os == "Debian") { |
|||
distribution = "debian"; |
|||
} |
|||
} |
|||
|
|||
bash("arch=$(uname -i)"); |
|||
|
|||
println("Your OS: " ++ os); |
|||
println("Distribution: " ++ distribution); |
|||
println("Arch: " ++ arch); |
|||
|
|||
if ("$3" != "") { |
|||
port = "$3"; |
|||
} else { |
|||
call("read", "-p", "Listening Port [$defaultPort]: ", "port"); |
|||
|
|||
if (port == "") { |
|||
port = defaultPort; |
|||
} |
|||
} |
|||
|
|||
if ("$2" != "") { |
|||
installPath = "$2"; |
|||
} else { |
|||
call("read", "-p", "Installation Path [$defaultInstallPath]: ", "installPath"); |
|||
|
|||
if (installPath == "") { |
|||
installPath = defaultInstallPath; |
|||
} |
|||
} |
|||
|
|||
// CentOS |
|||
if (distribution == "rhel") { |
|||
bash("nodeCheck=$(node -v)"); |
|||
|
|||
if (nodeCheck != "") { |
|||
checkNode(); |
|||
} else { |
|||
|
|||
bash("curlCheck=$(curl --version)"); |
|||
if (curlCheck == "") { |
|||
println("Installing Curl"); |
|||
bash("yum -y -q install curl"); |
|||
} |
|||
|
|||
println("Installing Node.js 14"); |
|||
bash("curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt"); |
|||
bash("yum install -y -q nodejs"); |
|||
bash("node -v"); |
|||
|
|||
bash("nodeCheckAgain=$(node -v)"); |
|||
|
|||
if (nodeCheckAgain == "") { |
|||
println("Error during Node.js installation"); |
|||
bash("exit 1"); |
|||
} |
|||
} |
|||
|
|||
bash("check=$(git --version)"); |
|||
if (check == "") { |
|||
println("Installing Git"); |
|||
bash("yum -y -q install git"); |
|||
} |
|||
|
|||
// Ubuntu |
|||
} else if (distribution == "ubuntu") { |
|||
deb(); |
|||
|
|||
// Debian |
|||
} else if (distribution == "debian") { |
|||
deb(); |
|||
|
|||
} else { |
|||
// Unknown distribution |
|||
error = 0; |
|||
|
|||
bash("check=$(git --version)"); |
|||
if (check == "") { |
|||
error = 1; |
|||
println("Error: git is missing"); |
|||
} |
|||
|
|||
bash("check=$(node -v)"); |
|||
if (check == "") { |
|||
error = 1; |
|||
println("Error: node is missing"); |
|||
} |
|||
|
|||
if (error > 0) { |
|||
println("Please install above missing software"); |
|||
bash("exit 1"); |
|||
} |
|||
} |
|||
|
|||
bash("check=$(pm2 --version)"); |
|||
if (check == "") { |
|||
println("Installing PM2"); |
|||
bash("npm install pm2 -g"); |
|||
bash("pm2 startup"); |
|||
} |
|||
|
|||
bash("mkdir -p $installPath"); |
|||
bash("cd $installPath"); |
|||
bash("git clone https://github.com/louislam/uptime-kuma.git ."); |
|||
bash("npm run setup"); |
|||
|
|||
bash("pm2 start server/server.js --name uptime-kuma -- --port=$port"); |
|||
|
|||
} else { |
|||
defaultVolume = "uptime-kuma"; |
|||
|
|||
bash("check=$(docker -v)"); |
|||
if (check == "") { |
|||
println("Error: docker is not found!"); |
|||
bash("exit 1"); |
|||
} |
|||
|
|||
bash("check=$(docker info)"); |
|||
|
|||
bash("if [[ \"$check\" == *\"Is the docker daemon running\"* ]]; then |
|||
\"echo\" \"Error: docker is not running\" |
|||
\"exit\" \"1\" |
|||
fi"); |
|||
|
|||
if ("$3" != "") { |
|||
port = "$3"; |
|||
} else { |
|||
call("read", "-p", "Expose Port [$defaultPort]: ", "port"); |
|||
|
|||
if (port == "") { |
|||
port = defaultPort; |
|||
} |
|||
} |
|||
|
|||
if ("$2" != "") { |
|||
volume = "$2"; |
|||
} else { |
|||
call("read", "-p", "Volume Name [$defaultVolume]: ", "volume"); |
|||
|
|||
if (volume == "") { |
|||
volume = defaultVolume; |
|||
} |
|||
} |
|||
|
|||
println("Port: $port"); |
|||
println("Volume: $volume"); |
|||
bash("docker volume create $volume"); |
|||
bash("docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1"); |
|||
} |
|||
|
|||
println("http://localhost:$port"); |
@ -0,0 +1,144 @@ |
|||
/* |
|||
* Simple DNS Server |
|||
* For testing DNS monitoring type, dev only |
|||
*/ |
|||
const dns2 = require("dns2"); |
|||
|
|||
const { Packet } = dns2; |
|||
|
|||
const server = dns2.createServer({ |
|||
udp: true |
|||
}); |
|||
|
|||
server.on("request", (request, send, rinfo) => { |
|||
for (let question of request.questions) { |
|||
console.log(question.name, type(question.type), question.class); |
|||
|
|||
const response = Packet.createResponseFromRequest(request); |
|||
|
|||
if (question.name === "existing.com") { |
|||
|
|||
if (question.type === Packet.TYPE.A) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
address: "1.2.3.4" |
|||
}); |
|||
} if (question.type === Packet.TYPE.AAAA) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
address: "fe80::::1234:5678:abcd:ef00", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.CNAME) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
domain: "cname1.existing.com", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.MX) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
exchange: "mx1.existing.com", |
|||
priority: 5 |
|||
}); |
|||
} else if (question.type === Packet.TYPE.NS) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
ns: "ns1.existing.com", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.SOA) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
primary: "existing.com", |
|||
admin: "admin@existing.com", |
|||
serial: 2021082701, |
|||
refresh: 300, |
|||
retry: 3, |
|||
expiration: 10, |
|||
minimum: 10, |
|||
}); |
|||
} else if (question.type === Packet.TYPE.SRV) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
priority: 5, |
|||
weight: 5, |
|||
port: 8080, |
|||
target: "srv1.existing.com", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.TXT) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
data: "#v=spf1 include:_spf.existing.com ~all", |
|||
}); |
|||
} else if (question.type === Packet.TYPE.CAA) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
flags: 0, |
|||
tag: "issue", |
|||
value: "ca.existing.com", |
|||
}); |
|||
} |
|||
|
|||
} |
|||
|
|||
if (question.name === "4.3.2.1.in-addr.arpa") { |
|||
if (question.type === Packet.TYPE.PTR) { |
|||
response.answers.push({ |
|||
name: question.name, |
|||
type: question.type, |
|||
class: question.class, |
|||
ttl: 300, |
|||
domain: "ptr1.existing.com", |
|||
}); |
|||
} |
|||
} |
|||
|
|||
send(response); |
|||
} |
|||
}); |
|||
|
|||
server.on("listening", () => { |
|||
console.log("Listening"); |
|||
console.log(server.addresses()); |
|||
}); |
|||
|
|||
server.on("close", () => { |
|||
console.log("server closed"); |
|||
}); |
|||
|
|||
server.listen({ |
|||
udp: 5300 |
|||
}); |
|||
|
|||
function type(code) { |
|||
for (let name in Packet.TYPE) { |
|||
if (Packet.TYPE[name] === code) { |
|||
return name; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,3 @@ |
|||
package-lock.json |
|||
test.js |
|||
languages/ |
@ -0,0 +1,84 @@ |
|||
// Need to use ES6 to read language files
|
|||
|
|||
import fs from "fs"; |
|||
import path from "path"; |
|||
import util from "util"; |
|||
|
|||
// https://stackoverflow.com/questions/13786160/copy-folder-recursively-in-node-js
|
|||
/** |
|||
* Look ma, it's cp -R. |
|||
* @param {string} src The path to the thing to copy. |
|||
* @param {string} dest The path to the new copy. |
|||
*/ |
|||
const copyRecursiveSync = function (src, dest) { |
|||
let exists = fs.existsSync(src); |
|||
let stats = exists && fs.statSync(src); |
|||
let isDirectory = exists && stats.isDirectory(); |
|||
|
|||
if (isDirectory) { |
|||
fs.mkdirSync(dest); |
|||
fs.readdirSync(src).forEach(function (childItemName) { |
|||
copyRecursiveSync(path.join(src, childItemName), |
|||
path.join(dest, childItemName)); |
|||
}); |
|||
} else { |
|||
fs.copyFileSync(src, dest); |
|||
} |
|||
}; |
|||
|
|||
console.log("Arguments:", process.argv) |
|||
const baseLangCode = process.argv[2] || "en"; |
|||
console.log("Base Lang: " + baseLangCode); |
|||
fs.rmdirSync("./languages", { recursive: true }); |
|||
copyRecursiveSync("../../src/languages", "./languages"); |
|||
|
|||
const en = (await import("./languages/en.js")).default; |
|||
const baseLang = (await import(`./languages/${baseLangCode}.js`)).default; |
|||
const files = fs.readdirSync("./languages"); |
|||
console.log("Files:", files); |
|||
|
|||
for (const file of files) { |
|||
if (!file.endsWith(".js")) { |
|||
console.log("Skipping " + file) |
|||
continue; |
|||
} |
|||
|
|||
console.log("Processing " + file); |
|||
const lang = await import("./languages/" + file); |
|||
|
|||
let obj; |
|||
|
|||
if (lang.default) { |
|||
obj = lang.default; |
|||
} else { |
|||
console.log("Empty file"); |
|||
obj = { |
|||
languageName: "<Your Language name in your language (not in English)>" |
|||
}; |
|||
} |
|||
|
|||
// En first
|
|||
for (const key in en) { |
|||
if (! obj[key]) { |
|||
obj[key] = en[key]; |
|||
} |
|||
} |
|||
|
|||
if (baseLang !== en) { |
|||
// Base second
|
|||
for (const key in baseLang) { |
|||
if (! obj[key]) { |
|||
obj[key] = key; |
|||
} |
|||
} |
|||
} |
|||
|
|||
const code = "export default " + util.inspect(obj, { |
|||
depth: null, |
|||
}); |
|||
|
|||
fs.writeFileSync(`../../src/languages/${file}`, code); |
|||
} |
|||
|
|||
fs.rmdirSync("./languages", { recursive: true }); |
|||
console.log("Done. Fixing formatting by ESLint..."); |
@ -0,0 +1,12 @@ |
|||
{ |
|||
"name": "update-language-files", |
|||
"type": "module", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -0,0 +1,64 @@ |
|||
#!/usr/bin/env bash |
|||
# |
|||
# Author: Stefan Buck |
|||
# License: MIT |
|||
# https://gist.github.com/stefanbuck/ce788fee19ab6eb0b4447a85fc99f447 |
|||
# |
|||
# |
|||
# This script accepts the following parameters: |
|||
# |
|||
# * owner |
|||
# * repo |
|||
# * tag |
|||
# * filename |
|||
# * github_api_token |
|||
# |
|||
# Script to upload a release asset using the GitHub API v3. |
|||
# |
|||
# Example: |
|||
# |
|||
# upload-github-release-asset.sh github_api_token=TOKEN owner=stefanbuck repo=playground tag=v0.1.0 filename=./build.zip |
|||
# |
|||
|
|||
# Check dependencies. |
|||
set -e |
|||
xargs=$(which gxargs || which xargs) |
|||
|
|||
# Validate settings. |
|||
[ "$TRACE" ] && set -x |
|||
|
|||
CONFIG=$@ |
|||
|
|||
for line in $CONFIG; do |
|||
eval "$line" |
|||
done |
|||
|
|||
# Define variables. |
|||
GH_API="https://api.github.com" |
|||
GH_REPO="$GH_API/repos/$owner/$repo" |
|||
GH_TAGS="$GH_REPO/releases/tags/$tag" |
|||
AUTH="Authorization: token $github_api_token" |
|||
WGET_ARGS="--content-disposition --auth-no-challenge --no-cookie" |
|||
CURL_ARGS="-LJO#" |
|||
|
|||
if [[ "$tag" == 'LATEST' ]]; then |
|||
GH_TAGS="$GH_REPO/releases/latest" |
|||
fi |
|||
|
|||
# Validate token. |
|||
curl -o /dev/null -sH "$AUTH" $GH_REPO || { echo "Error: Invalid repo, token or network issue!"; exit 1; } |
|||
|
|||
# Read asset tags. |
|||
response=$(curl -sH "$AUTH" $GH_TAGS) |
|||
|
|||
# Get ID of the asset based on given filename. |
|||
eval $(echo "$response" | grep -m 1 "id.:" | grep -w id | tr : = | tr -cd '[[:alnum:]]=') |
|||
[ "$id" ] || { echo "Error: Failed to get release id for tag: $tag"; echo "$response" | awk 'length($0)<100' >&2; exit 1; } |
|||
|
|||
# Upload asset |
|||
echo "Uploading asset... " |
|||
|
|||
# Construct url |
|||
GH_ASSET="https://uploads.github.com/repos/$owner/$repo/releases/$id/assets?name=$(basename $filename)" |
|||
|
|||
curl "$GITHUB_OAUTH_BASIC" --data-binary @"$filename" -H "Authorization: token $github_api_token" -H "Content-Type: application/octet-stream" $GH_ASSET |
@ -0,0 +1,203 @@ |
|||
# install.sh is generated by ./extra/install.batsh, do not modify it directly. |
|||
# "npm run compile-install-script" to compile install.sh |
|||
# The command is working on Windows PowerShell and Docker for Windows only. |
|||
# curl -o kuma_install.sh https://raw.githubusercontent.com/louislam/uptime-kuma/master/install.sh && sudo bash kuma_install.sh |
|||
"echo" "-e" "=====================" |
|||
"echo" "-e" "Uptime Kuma Installer" |
|||
"echo" "-e" "=====================" |
|||
"echo" "-e" "Supported OS: CentOS 7/8, Ubuntu >= 16.04 and Debian" |
|||
"echo" "-e" "---------------------------------------" |
|||
"echo" "-e" "This script is designed for Linux and basic usage." |
|||
"echo" "-e" "For advanced usage, please go to https://github.com/louislam/uptime-kuma/wiki/Installation" |
|||
"echo" "-e" "---------------------------------------" |
|||
"echo" "-e" "" |
|||
"echo" "-e" "Local - Install Uptime Kuma in your current machine with git, Node.js 14 and pm2" |
|||
"echo" "-e" "Docker - Install Uptime Kuma Docker container" |
|||
"echo" "-e" "" |
|||
if [ "$1" != "" ]; then |
|||
type="$1" |
|||
else |
|||
"read" "-p" "Which installation method do you prefer? [DOCKER/local]: " "type" |
|||
fi |
|||
defaultPort="3001" |
|||
function checkNode { |
|||
local _0 |
|||
nodeVersion=$(node -e 'console.log(process.versions.node.split(`.`)[0])') |
|||
"echo" "-e" "Node Version: ""$nodeVersion" |
|||
_0="12" |
|||
if [ $(($nodeVersion < $_0)) == 1 ]; then |
|||
"echo" "-e" "Error: Required Node.js 14" |
|||
"exit" "1" |
|||
fi |
|||
if [ "$nodeVersion" == "12" ]; then |
|||
"echo" "-e" "Warning: NodeJS ""$nodeVersion"" is not tested." |
|||
fi |
|||
} |
|||
function deb { |
|||
nodeCheck=$(node -v) |
|||
apt --yes update |
|||
if [ "$nodeCheck" != "" ]; then |
|||
"checkNode" |
|||
else |
|||
# Old nodejs binary name is "nodejs" |
|||
check=$(nodejs --version) |
|||
if [ "$check" != "" ]; then |
|||
"echo" "-e" "Error: 'node' command is not found, but 'nodejs' command is found. Your NodeJS should be too old." |
|||
exit 1 |
|||
fi |
|||
curlCheck=$(curl --version) |
|||
if [ "$curlCheck" == "" ]; then |
|||
"echo" "-e" "Installing Curl" |
|||
apt --yes install curl |
|||
fi |
|||
"echo" "-e" "Installing Node.js 14" |
|||
curl -sL https://deb.nodesource.com/setup_14.x | bash - > log.txt |
|||
apt --yes install nodejs |
|||
node -v |
|||
nodeCheckAgain=$(node -v) |
|||
if [ "$nodeCheckAgain" == "" ]; then |
|||
"echo" "-e" "Error during Node.js installation" |
|||
exit 1 |
|||
fi |
|||
fi |
|||
check=$(git --version) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Installing Git" |
|||
apt --yes install git |
|||
fi |
|||
} |
|||
if [ "$type" == "local" ]; then |
|||
defaultInstallPath="/opt/uptime-kuma" |
|||
if [ -e "/etc/redhat-release" ]; then |
|||
os=$("cat" "/etc/redhat-release") |
|||
distribution="rhel" |
|||
else |
|||
if [ -e "/etc/issue" ]; then |
|||
os=$(head -n1 /etc/issue | cut -f 1 -d ' ') |
|||
if [ "$os" == "Ubuntu" ]; then |
|||
distribution="ubuntu" |
|||
fi |
|||
if [ "$os" == "Debian" ]; then |
|||
distribution="debian" |
|||
fi |
|||
fi |
|||
fi |
|||
arch=$(uname -i) |
|||
"echo" "-e" "Your OS: ""$os" |
|||
"echo" "-e" "Distribution: ""$distribution" |
|||
"echo" "-e" "Arch: ""$arch" |
|||
if [ "$3" != "" ]; then |
|||
port="$3" |
|||
else |
|||
"read" "-p" "Listening Port [$defaultPort]: " "port" |
|||
if [ "$port" == "" ]; then |
|||
port="$defaultPort" |
|||
fi |
|||
fi |
|||
if [ "$2" != "" ]; then |
|||
installPath="$2" |
|||
else |
|||
"read" "-p" "Installation Path [$defaultInstallPath]: " "installPath" |
|||
if [ "$installPath" == "" ]; then |
|||
installPath="$defaultInstallPath" |
|||
fi |
|||
fi |
|||
# CentOS |
|||
if [ "$distribution" == "rhel" ]; then |
|||
nodeCheck=$(node -v) |
|||
if [ "$nodeCheck" != "" ]; then |
|||
"checkNode" |
|||
else |
|||
curlCheck=$(curl --version) |
|||
if [ "$curlCheck" == "" ]; then |
|||
"echo" "-e" "Installing Curl" |
|||
yum -y -q install curl |
|||
fi |
|||
"echo" "-e" "Installing Node.js 14" |
|||
curl -sL https://rpm.nodesource.com/setup_14.x | bash - > log.txt |
|||
yum install -y -q nodejs |
|||
node -v |
|||
nodeCheckAgain=$(node -v) |
|||
if [ "$nodeCheckAgain" == "" ]; then |
|||
"echo" "-e" "Error during Node.js installation" |
|||
exit 1 |
|||
fi |
|||
fi |
|||
check=$(git --version) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Installing Git" |
|||
yum -y -q install git |
|||
fi |
|||
# Ubuntu |
|||
else |
|||
if [ "$distribution" == "ubuntu" ]; then |
|||
"deb" |
|||
# Debian |
|||
else |
|||
if [ "$distribution" == "debian" ]; then |
|||
"deb" |
|||
else |
|||
# Unknown distribution |
|||
error=$((0)) |
|||
check=$(git --version) |
|||
if [ "$check" == "" ]; then |
|||
error=$((1)) |
|||
"echo" "-e" "Error: git is missing" |
|||
fi |
|||
check=$(node -v) |
|||
if [ "$check" == "" ]; then |
|||
error=$((1)) |
|||
"echo" "-e" "Error: node is missing" |
|||
fi |
|||
if [ $(($error > 0)) == 1 ]; then |
|||
"echo" "-e" "Please install above missing software" |
|||
exit 1 |
|||
fi |
|||
fi |
|||
fi |
|||
fi |
|||
check=$(pm2 --version) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Installing PM2" |
|||
npm install pm2 -g |
|||
pm2 startup |
|||
fi |
|||
mkdir -p $installPath |
|||
cd $installPath |
|||
git clone https://github.com/louislam/uptime-kuma.git . |
|||
npm run setup |
|||
pm2 start server/server.js --name uptime-kuma -- --port=$port |
|||
else |
|||
defaultVolume="uptime-kuma" |
|||
check=$(docker -v) |
|||
if [ "$check" == "" ]; then |
|||
"echo" "-e" "Error: docker is not found!" |
|||
exit 1 |
|||
fi |
|||
check=$(docker info) |
|||
if [[ "$check" == *"Is the docker daemon running"* ]]; then |
|||
"echo" "Error: docker is not running" |
|||
"exit" "1" |
|||
fi |
|||
if [ "$3" != "" ]; then |
|||
port="$3" |
|||
else |
|||
"read" "-p" "Expose Port [$defaultPort]: " "port" |
|||
if [ "$port" == "" ]; then |
|||
port="$defaultPort" |
|||
fi |
|||
fi |
|||
if [ "$2" != "" ]; then |
|||
volume="$2" |
|||
else |
|||
"read" "-p" "Volume Name [$defaultVolume]: " "volume" |
|||
if [ "$volume" == "" ]; then |
|||
volume="$defaultVolume" |
|||
fi |
|||
fi |
|||
"echo" "-e" "Port: $port" |
|||
"echo" "-e" "Volume: $volume" |
|||
docker volume create $volume |
|||
docker run -d --restart=always -p $port:3001 -v $volume:/app/data --name uptime-kuma louislam/uptime-kuma:1 |
|||
fi |
|||
"echo" "-e" "http://localhost:$port" |
@ -0,0 +1,32 @@ |
|||
# Uptime-Kuma K8s Deployment |
|||
|
|||
⚠ Warning: K8s deployment is provided by contributors. I have no experience with K8s and I can't fix error in the future. I only test Docker and Node.js. Use at your own risk. |
|||
|
|||
## How does it work? |
|||
|
|||
Kustomize is a tool which builds a complete deployment file for all config elements. |
|||
You can edit the files in the ```uptime-kuma``` folder except the ```kustomization.yml``` until you know what you're doing. |
|||
If you want to choose another namespace you can edit the ```kustomization.yml``` in the ```kubernetes```-Folder and change the ```namespace: uptime-kuma``` to something you like. |
|||
|
|||
It creates a certificate with the specified Issuer and creates the Ingress for the Uptime-Kuma ClusterIP-Service. |
|||
|
|||
## What do I have to edit? |
|||
|
|||
You have to edit the ```ingressroute.yml``` to your needs. |
|||
This ingressroute.yml is for the [nginx-ingress-controller](https://kubernetes.github.io/ingress-nginx/) in combination with the [cert-manager](https://cert-manager.io/). |
|||
|
|||
- Host |
|||
- Secrets and secret names |
|||
- (Cluster)Issuer (optional) |
|||
- The Version in the Deployment-File |
|||
- Update: |
|||
- Change to newer version and run the above commands, it will update the pods one after another |
|||
|
|||
## How To use |
|||
|
|||
- Install [kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) |
|||
- Edit files mentioned above to your needs |
|||
- Run ```kustomize build > apply.yml``` |
|||
- Run ```kubectl apply -f apply.yml``` |
|||
|
|||
Now you should see some k8s magic and Uptime-Kuma should be available at the specified address. |
@ -0,0 +1,10 @@ |
|||
namespace: uptime-kuma |
|||
namePrefix: uptime-kuma- |
|||
|
|||
commonLabels: |
|||
app: uptime-kuma |
|||
|
|||
bases: |
|||
- uptime-kuma |
|||
|
|||
|
@ -0,0 +1,45 @@ |
|||
apiVersion: apps/v1 |
|||
kind: Deployment |
|||
metadata: |
|||
labels: |
|||
component: uptime-kuma |
|||
name: deployment |
|||
spec: |
|||
selector: |
|||
matchLabels: |
|||
component: uptime-kuma |
|||
replicas: 1 |
|||
strategy: |
|||
type: Recreate |
|||
|
|||
template: |
|||
metadata: |
|||
labels: |
|||
component: uptime-kuma |
|||
spec: |
|||
containers: |
|||
- name: app |
|||
image: louislam/uptime-kuma:1 |
|||
ports: |
|||
- containerPort: 3001 |
|||
volumeMounts: |
|||
- mountPath: /app/data |
|||
name: storage |
|||
livenessProbe: |
|||
exec: |
|||
command: |
|||
- node |
|||
- extra/healthcheck.js |
|||
initialDelaySeconds: 180 |
|||
periodSeconds: 60 |
|||
timeoutSeconds: 30 |
|||
readinessProbe: |
|||
httpGet: |
|||
path: / |
|||
port: 3001 |
|||
scheme: HTTP |
|||
|
|||
volumes: |
|||
- name: storage |
|||
persistentVolumeClaim: |
|||
claimName: pvc |
@ -0,0 +1,39 @@ |
|||
apiVersion: networking.k8s.io/v1 |
|||
kind: Ingress |
|||
metadata: |
|||
annotations: |
|||
kubernetes.io/ingress.class: nginx |
|||
cert-manager.io/cluster-issuer: letsencrypt-prod |
|||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" |
|||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" |
|||
nginx.ingress.kubernetes.io/server-snippets: | |
|||
location / { |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_http_version 1.1; |
|||
proxy_set_header X-Forwarded-Host $http_host; |
|||
proxy_set_header X-Forwarded-Proto $scheme; |
|||
proxy_set_header X-Forwarded-For $remote_addr; |
|||
proxy_set_header Host $host; |
|||
proxy_set_header Connection "upgrade"; |
|||
proxy_set_header X-Real-IP $remote_addr; |
|||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
|||
proxy_set_header Upgrade $http_upgrade; |
|||
proxy_cache_bypass $http_upgrade; |
|||
} |
|||
name: ingress |
|||
spec: |
|||
tls: |
|||
- hosts: |
|||
- example.com |
|||
secretName: example-com-tls |
|||
rules: |
|||
- host: example.com |
|||
http: |
|||
paths: |
|||
- path: / |
|||
pathType: Prefix |
|||
backend: |
|||
service: |
|||
name: service |
|||
port: |
|||
number: 3001 |
@ -0,0 +1,5 @@ |
|||
resources: |
|||
- deployment.yml |
|||
- service.yml |
|||
- ingressroute.yml |
|||
- pvc.yml |
@ -0,0 +1,10 @@ |
|||
apiVersion: v1 |
|||
kind: PersistentVolumeClaim |
|||
metadata: |
|||
name: pvc |
|||
spec: |
|||
accessModes: |
|||
- ReadWriteOnce |
|||
resources: |
|||
requests: |
|||
storage: 4Gi |
@ -0,0 +1,13 @@ |
|||
apiVersion: v1 |
|||
kind: Service |
|||
metadata: |
|||
name: service |
|||
spec: |
|||
selector: |
|||
component: uptime-kuma |
|||
type: ClusterIP |
|||
ports: |
|||
- name: http |
|||
port: 3001 |
|||
targetPort: 3001 |
|||
protocol: TCP |
File diff suppressed because it is too large
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 9.5 KiB |
@ -0,0 +1,19 @@ |
|||
{ |
|||
"name": "Uptime Kuma", |
|||
"short_name": "Uptime Kuma", |
|||
"start_url": "/", |
|||
"background_color": "#fff", |
|||
"display": "standalone", |
|||
"icons": [ |
|||
{ |
|||
"src": "icon-192x192.png", |
|||
"sizes": "192x192", |
|||
"type": "image/png" |
|||
}, |
|||
{ |
|||
"src": "icon-512x512.png", |
|||
"sizes": "512x512", |
|||
"type": "image/png" |
|||
} |
|||
] |
|||
} |
@ -0,0 +1,42 @@ |
|||
const { setSetting } = require("./util-server"); |
|||
const axios = require("axios"); |
|||
|
|||
exports.version = require("../package.json").version; |
|||
exports.latestVersion = null; |
|||
|
|||
let interval; |
|||
|
|||
exports.startInterval = () => { |
|||
let check = async () => { |
|||
try { |
|||
const res = await axios.get("https://raw.githubusercontent.com/louislam/uptime-kuma/master/package.json"); |
|||
|
|||
if (typeof res.data === "string") { |
|||
res.data = JSON.parse(res.data); |
|||
} |
|||
|
|||
// For debug
|
|||
if (process.env.TEST_CHECK_VERSION === "1") { |
|||
res.data.version = "1000.0.0"; |
|||
} |
|||
|
|||
exports.latestVersion = res.data.version; |
|||
} catch (_) { } |
|||
|
|||
}; |
|||
|
|||
check(); |
|||
interval = setInterval(check, 3600 * 1000 * 48); |
|||
}; |
|||
|
|||
exports.enableCheckUpdate = async (value) => { |
|||
await setSetting("checkUpdate", value); |
|||
|
|||
clearInterval(interval); |
|||
|
|||
if (value) { |
|||
exports.startInterval(); |
|||
} |
|||
}; |
|||
|
|||
exports.socket = null; |
@ -0,0 +1,100 @@ |
|||
/* |
|||
* For Client Socket |
|||
*/ |
|||
const { TimeLogger } = require("../src/util"); |
|||
const { R } = require("redbean-node"); |
|||
const { io } = require("./server"); |
|||
const { setting } = require("./util-server"); |
|||
const checkVersion = require("./check-version"); |
|||
|
|||
async function sendNotificationList(socket) { |
|||
const timeLogger = new TimeLogger(); |
|||
|
|||
let result = []; |
|||
let list = await R.find("notification", " user_id = ? ", [ |
|||
socket.userID, |
|||
]); |
|||
|
|||
for (let bean of list) { |
|||
result.push(bean.export()); |
|||
} |
|||
|
|||
io.to(socket.userID).emit("notificationList", result); |
|||
|
|||
timeLogger.print("Send Notification List"); |
|||
|
|||
return list; |
|||
} |
|||
|
|||
/** |
|||
* Send Heartbeat History list to socket |
|||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only |
|||
* @param overwrite Overwrite client-side's heartbeat list |
|||
*/ |
|||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) { |
|||
const timeLogger = new TimeLogger(); |
|||
|
|||
let list = await R.getAll(` |
|||
SELECT * FROM heartbeat |
|||
WHERE monitor_id = ? |
|||
ORDER BY time DESC |
|||
LIMIT 100 |
|||
`, [
|
|||
monitorID, |
|||
]); |
|||
|
|||
let result = list.reverse(); |
|||
|
|||
if (toUser) { |
|||
io.to(socket.userID).emit("heartbeatList", monitorID, result, overwrite); |
|||
} else { |
|||
socket.emit("heartbeatList", monitorID, result, overwrite); |
|||
} |
|||
|
|||
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`); |
|||
} |
|||
|
|||
/** |
|||
* Important Heart beat list (aka event list) |
|||
* @param socket |
|||
* @param monitorID |
|||
* @param toUser True = send to all browsers with the same user id, False = send to the current browser only |
|||
* @param overwrite Overwrite client-side's heartbeat list |
|||
*/ |
|||
async function sendImportantHeartbeatList(socket, monitorID, toUser = false, overwrite = false) { |
|||
const timeLogger = new TimeLogger(); |
|||
|
|||
let list = await R.find("heartbeat", ` |
|||
monitor_id = ? |
|||
AND important = 1 |
|||
ORDER BY time DESC |
|||
LIMIT 500 |
|||
`, [
|
|||
monitorID, |
|||
]); |
|||
|
|||
timeLogger.print(`[Monitor: ${monitorID}] sendImportantHeartbeatList`); |
|||
|
|||
if (toUser) { |
|||
io.to(socket.userID).emit("importantHeartbeatList", monitorID, list, overwrite); |
|||
} else { |
|||
socket.emit("importantHeartbeatList", monitorID, list, overwrite); |
|||
} |
|||
|
|||
} |
|||
|
|||
async function sendInfo(socket) { |
|||
socket.emit("info", { |
|||
version: checkVersion.version, |
|||
latestVersion: checkVersion.latestVersion, |
|||
primaryBaseURL: await setting("primaryBaseURL") |
|||
}); |
|||
} |
|||
|
|||
module.exports = { |
|||
sendNotificationList, |
|||
sendImportantHeartbeatList, |
|||
sendHeartbeatList, |
|||
sendInfo |
|||
}; |
|||
|
@ -0,0 +1,57 @@ |
|||
/* |
|||
From https://github.com/DiegoZoracKy/image-data-uri/blob/master/lib/image-data-uri.js
|
|||
Modified with 0 dependencies |
|||
*/ |
|||
let fs = require("fs"); |
|||
|
|||
let ImageDataURI = (() => { |
|||
|
|||
function decode(dataURI) { |
|||
if (!/data:image\//.test(dataURI)) { |
|||
console.log("ImageDataURI :: Error :: It seems that it is not an Image Data URI. Couldn't match \"data:image/\""); |
|||
return null; |
|||
} |
|||
|
|||
let regExMatches = dataURI.match("data:(image/.*);base64,(.*)"); |
|||
return { |
|||
imageType: regExMatches[1], |
|||
dataBase64: regExMatches[2], |
|||
dataBuffer: new Buffer(regExMatches[2], "base64") |
|||
}; |
|||
} |
|||
|
|||
function encode(data, mediaType) { |
|||
if (!data || !mediaType) { |
|||
console.log("ImageDataURI :: Error :: Missing some of the required params: data, mediaType "); |
|||
return null; |
|||
} |
|||
|
|||
mediaType = (/\//.test(mediaType)) ? mediaType : "image/" + mediaType; |
|||
let dataBase64 = (Buffer.isBuffer(data)) ? data.toString("base64") : new Buffer(data).toString("base64"); |
|||
let dataImgBase64 = "data:" + mediaType + ";base64," + dataBase64; |
|||
|
|||
return dataImgBase64; |
|||
} |
|||
|
|||
function outputFile(dataURI, filePath) { |
|||
filePath = filePath || "./"; |
|||
return new Promise((resolve, reject) => { |
|||
let imageDecoded = decode(dataURI); |
|||
|
|||
fs.writeFile(filePath, imageDecoded.dataBuffer, err => { |
|||
if (err) { |
|||
return reject("ImageDataURI :: Error :: " + JSON.stringify(err, null, 4)); |
|||
} |
|||
resolve(filePath); |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
decode: decode, |
|||
encode: encode, |
|||
outputFile: outputFile, |
|||
}; |
|||
})(); |
|||
|
|||
module.exports = ImageDataURI; |
@ -0,0 +1,34 @@ |
|||
const { BeanModel } = require("redbean-node/dist/bean-model"); |
|||
const { R } = require("redbean-node"); |
|||
|
|||
class Group extends BeanModel { |
|||
|
|||
async toPublicJSON() { |
|||
let monitorBeanList = await this.getMonitorList(); |
|||
let monitorList = []; |
|||
|
|||
for (let bean of monitorBeanList) { |
|||
monitorList.push(await bean.toPublicJSON()); |
|||
} |
|||
|
|||
return { |
|||
id: this.id, |
|||
name: this.name, |
|||
weight: this.weight, |
|||
monitorList, |
|||
}; |
|||
} |
|||
|
|||
async getMonitorList() { |
|||
return R.convertToBeans("monitor", await R.getAll(` |
|||
SELECT monitor.* FROM monitor, monitor_group |
|||
WHERE monitor.id = monitor_group.monitor_id |
|||
AND group_id = ? |
|||
ORDER BY monitor_group.weight |
|||
`, [
|
|||
this.id, |
|||
])); |
|||
} |
|||
} |
|||
|
|||
module.exports = Group; |
@ -0,0 +1,18 @@ |
|||
const { BeanModel } = require("redbean-node/dist/bean-model"); |
|||
|
|||
class Incident extends BeanModel { |
|||
|
|||
toPublicJSON() { |
|||
return { |
|||
id: this.id, |
|||
style: this.style, |
|||
title: this.title, |
|||
content: this.content, |
|||
pin: this.pin, |
|||
createdDate: this.createdDate, |
|||
lastUpdatedDate: this.lastUpdatedDate, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
module.exports = Incident; |
@ -0,0 +1,13 @@ |
|||
const { BeanModel } = require("redbean-node/dist/bean-model"); |
|||
|
|||
class Tag extends BeanModel { |
|||
toJSON() { |
|||
return { |
|||
id: this._id, |
|||
name: this._name, |
|||
color: this._color, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
module.exports = Tag; |
@ -0,0 +1,749 @@ |
|||
let url = require("url"); |
|||
let MemoryCache = require("./memory-cache"); |
|||
|
|||
let t = { |
|||
ms: 1, |
|||
second: 1000, |
|||
minute: 60000, |
|||
hour: 3600000, |
|||
day: 3600000 * 24, |
|||
week: 3600000 * 24 * 7, |
|||
month: 3600000 * 24 * 30, |
|||
}; |
|||
|
|||
let instances = []; |
|||
|
|||
let matches = function (a) { |
|||
return function (b) { |
|||
return a === b; |
|||
}; |
|||
}; |
|||
|
|||
let doesntMatch = function (a) { |
|||
return function (b) { |
|||
return !matches(a)(b); |
|||
}; |
|||
}; |
|||
|
|||
let logDuration = function (d, prefix) { |
|||
let str = d > 1000 ? (d / 1000).toFixed(2) + "sec" : d + "ms"; |
|||
return "\x1b[33m- " + (prefix ? prefix + " " : "") + str + "\x1b[0m"; |
|||
}; |
|||
|
|||
function getSafeHeaders(res) { |
|||
return res.getHeaders ? res.getHeaders() : res._headers; |
|||
} |
|||
|
|||
function ApiCache() { |
|||
let memCache = new MemoryCache(); |
|||
|
|||
let globalOptions = { |
|||
debug: false, |
|||
defaultDuration: 3600000, |
|||
enabled: true, |
|||
appendKey: [], |
|||
jsonp: false, |
|||
redisClient: false, |
|||
headerBlacklist: [], |
|||
statusCodes: { |
|||
include: [], |
|||
exclude: [], |
|||
}, |
|||
events: { |
|||
expire: undefined, |
|||
}, |
|||
headers: { |
|||
// 'cache-control': 'no-cache' // example of header overwrite
|
|||
}, |
|||
trackPerformance: false, |
|||
respectCacheControl: false, |
|||
}; |
|||
|
|||
let middlewareOptions = []; |
|||
let instance = this; |
|||
let index = null; |
|||
let timers = {}; |
|||
let performanceArray = []; // for tracking cache hit rate
|
|||
|
|||
instances.push(this); |
|||
this.id = instances.length; |
|||
|
|||
function debug(a, b, c, d) { |
|||
let arr = ["\x1b[36m[apicache]\x1b[0m", a, b, c, d].filter(function (arg) { |
|||
return arg !== undefined; |
|||
}); |
|||
let debugEnv = process.env.DEBUG && process.env.DEBUG.split(",").indexOf("apicache") !== -1; |
|||
|
|||
return (globalOptions.debug || debugEnv) && console.log.apply(null, arr); |
|||
} |
|||
|
|||
function shouldCacheResponse(request, response, toggle) { |
|||
let opt = globalOptions; |
|||
let codes = opt.statusCodes; |
|||
|
|||
if (!response) { |
|||
return false; |
|||
} |
|||
|
|||
if (toggle && !toggle(request, response)) { |
|||
return false; |
|||
} |
|||
|
|||
if (codes.exclude && codes.exclude.length && codes.exclude.indexOf(response.statusCode) !== -1) { |
|||
return false; |
|||
} |
|||
if (codes.include && codes.include.length && codes.include.indexOf(response.statusCode) === -1) { |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
function addIndexEntries(key, req) { |
|||
let groupName = req.apicacheGroup; |
|||
|
|||
if (groupName) { |
|||
debug("group detected \"" + groupName + "\""); |
|||
let group = (index.groups[groupName] = index.groups[groupName] || []); |
|||
group.unshift(key); |
|||
} |
|||
|
|||
index.all.unshift(key); |
|||
} |
|||
|
|||
function filterBlacklistedHeaders(headers) { |
|||
return Object.keys(headers) |
|||
.filter(function (key) { |
|||
return globalOptions.headerBlacklist.indexOf(key) === -1; |
|||
}) |
|||
.reduce(function (acc, header) { |
|||
acc[header] = headers[header]; |
|||
return acc; |
|||
}, {}); |
|||
} |
|||
|
|||
function createCacheObject(status, headers, data, encoding) { |
|||
return { |
|||
status: status, |
|||
headers: filterBlacklistedHeaders(headers), |
|||
data: data, |
|||
encoding: encoding, |
|||
timestamp: new Date().getTime() / 1000, // seconds since epoch. This is used to properly decrement max-age headers in cached responses.
|
|||
}; |
|||
} |
|||
|
|||
function cacheResponse(key, value, duration) { |
|||
let redis = globalOptions.redisClient; |
|||
let expireCallback = globalOptions.events.expire; |
|||
|
|||
if (redis && redis.connected) { |
|||
try { |
|||
redis.hset(key, "response", JSON.stringify(value)); |
|||
redis.hset(key, "duration", duration); |
|||
redis.expire(key, duration / 1000, expireCallback || function () {}); |
|||
} catch (err) { |
|||
debug("[apicache] error in redis.hset()"); |
|||
} |
|||
} else { |
|||
memCache.add(key, value, duration, expireCallback); |
|||
} |
|||
|
|||
// add automatic cache clearing from duration, includes max limit on setTimeout
|
|||
timers[key] = setTimeout(function () { |
|||
instance.clear(key, true); |
|||
}, Math.min(duration, 2147483647)); |
|||
} |
|||
|
|||
function accumulateContent(res, content) { |
|||
if (content) { |
|||
if (typeof content == "string") { |
|||
res._apicache.content = (res._apicache.content || "") + content; |
|||
} else if (Buffer.isBuffer(content)) { |
|||
let oldContent = res._apicache.content; |
|||
|
|||
if (typeof oldContent === "string") { |
|||
oldContent = !Buffer.from ? new Buffer(oldContent) : Buffer.from(oldContent); |
|||
} |
|||
|
|||
if (!oldContent) { |
|||
oldContent = !Buffer.alloc ? new Buffer(0) : Buffer.alloc(0); |
|||
} |
|||
|
|||
res._apicache.content = Buffer.concat( |
|||
[oldContent, content], |
|||
oldContent.length + content.length |
|||
); |
|||
} else { |
|||
res._apicache.content = content; |
|||
} |
|||
} |
|||
} |
|||
|
|||
function makeResponseCacheable(req, res, next, key, duration, strDuration, toggle) { |
|||
// monkeypatch res.end to create cache object
|
|||
res._apicache = { |
|||
write: res.write, |
|||
writeHead: res.writeHead, |
|||
end: res.end, |
|||
cacheable: true, |
|||
content: undefined, |
|||
}; |
|||
|
|||
// append header overwrites if applicable
|
|||
Object.keys(globalOptions.headers).forEach(function (name) { |
|||
res.setHeader(name, globalOptions.headers[name]); |
|||
}); |
|||
|
|||
res.writeHead = function () { |
|||
// add cache control headers
|
|||
if (!globalOptions.headers["cache-control"]) { |
|||
if (shouldCacheResponse(req, res, toggle)) { |
|||
res.setHeader("cache-control", "max-age=" + (duration / 1000).toFixed(0)); |
|||
} else { |
|||
res.setHeader("cache-control", "no-cache, no-store, must-revalidate"); |
|||
} |
|||
} |
|||
|
|||
res._apicache.headers = Object.assign({}, getSafeHeaders(res)); |
|||
return res._apicache.writeHead.apply(this, arguments); |
|||
}; |
|||
|
|||
// patch res.write
|
|||
res.write = function (content) { |
|||
accumulateContent(res, content); |
|||
return res._apicache.write.apply(this, arguments); |
|||
}; |
|||
|
|||
// patch res.end
|
|||
res.end = function (content, encoding) { |
|||
if (shouldCacheResponse(req, res, toggle)) { |
|||
accumulateContent(res, content); |
|||
|
|||
if (res._apicache.cacheable && res._apicache.content) { |
|||
addIndexEntries(key, req); |
|||
let headers = res._apicache.headers || getSafeHeaders(res); |
|||
let cacheObject = createCacheObject( |
|||
res.statusCode, |
|||
headers, |
|||
res._apicache.content, |
|||
encoding |
|||
); |
|||
cacheResponse(key, cacheObject, duration); |
|||
|
|||
// display log entry
|
|||
let elapsed = new Date() - req.apicacheTimer; |
|||
debug("adding cache entry for \"" + key + "\" @ " + strDuration, logDuration(elapsed)); |
|||
debug("_apicache.headers: ", res._apicache.headers); |
|||
debug("res.getHeaders(): ", getSafeHeaders(res)); |
|||
debug("cacheObject: ", cacheObject); |
|||
} |
|||
} |
|||
|
|||
return res._apicache.end.apply(this, arguments); |
|||
}; |
|||
|
|||
next(); |
|||
} |
|||
|
|||
function sendCachedResponse(request, response, cacheObject, toggle, next, duration) { |
|||
if (toggle && !toggle(request, response)) { |
|||
return next(); |
|||
} |
|||
|
|||
let headers = getSafeHeaders(response); |
|||
|
|||
// Modified by @louislam, removed Cache-control, since I don't need client side cache!
|
|||
// Original Source: https://github.com/kwhitley/apicache/blob/0d5686cc21fad353c6dddee646288c2fca3e4f50/src/apicache.js#L254
|
|||
Object.assign(headers, filterBlacklistedHeaders(cacheObject.headers || {})); |
|||
|
|||
// only embed apicache headers when not in production environment
|
|||
if (process.env.NODE_ENV !== "production") { |
|||
Object.assign(headers, { |
|||
"apicache-store": globalOptions.redisClient ? "redis" : "memory", |
|||
"apicache-version": "1.6.2-modified", |
|||
}); |
|||
} |
|||
|
|||
// unstringify buffers
|
|||
let data = cacheObject.data; |
|||
if (data && data.type === "Buffer") { |
|||
data = |
|||
typeof data.data === "number" ? new Buffer.alloc(data.data) : new Buffer.from(data.data); |
|||
} |
|||
|
|||
// test Etag against If-None-Match for 304
|
|||
let cachedEtag = cacheObject.headers.etag; |
|||
let requestEtag = request.headers["if-none-match"]; |
|||
|
|||
if (requestEtag && cachedEtag === requestEtag) { |
|||
response.writeHead(304, headers); |
|||
return response.end(); |
|||
} |
|||
|
|||
response.writeHead(cacheObject.status || 200, headers); |
|||
|
|||
return response.end(data, cacheObject.encoding); |
|||
} |
|||
|
|||
function syncOptions() { |
|||
for (let i in middlewareOptions) { |
|||
Object.assign(middlewareOptions[i].options, globalOptions, middlewareOptions[i].localOptions); |
|||
} |
|||
} |
|||
|
|||
this.clear = function (target, isAutomatic) { |
|||
let group = index.groups[target]; |
|||
let redis = globalOptions.redisClient; |
|||
|
|||
if (group) { |
|||
debug("clearing group \"" + target + "\""); |
|||
|
|||
group.forEach(function (key) { |
|||
debug("clearing cached entry for \"" + key + "\""); |
|||
clearTimeout(timers[key]); |
|||
delete timers[key]; |
|||
if (!globalOptions.redisClient) { |
|||
memCache.delete(key); |
|||
} else { |
|||
try { |
|||
redis.del(key); |
|||
} catch (err) { |
|||
console.log("[apicache] error in redis.del(\"" + key + "\")"); |
|||
} |
|||
} |
|||
index.all = index.all.filter(doesntMatch(key)); |
|||
}); |
|||
|
|||
delete index.groups[target]; |
|||
} else if (target) { |
|||
debug("clearing " + (isAutomatic ? "expired" : "cached") + " entry for \"" + target + "\""); |
|||
clearTimeout(timers[target]); |
|||
delete timers[target]; |
|||
// clear actual cached entry
|
|||
if (!redis) { |
|||
memCache.delete(target); |
|||
} else { |
|||
try { |
|||
redis.del(target); |
|||
} catch (err) { |
|||
console.log("[apicache] error in redis.del(\"" + target + "\")"); |
|||
} |
|||
} |
|||
|
|||
// remove from global index
|
|||
index.all = index.all.filter(doesntMatch(target)); |
|||
|
|||
// remove target from each group that it may exist in
|
|||
Object.keys(index.groups).forEach(function (groupName) { |
|||
index.groups[groupName] = index.groups[groupName].filter(doesntMatch(target)); |
|||
|
|||
// delete group if now empty
|
|||
if (!index.groups[groupName].length) { |
|||
delete index.groups[groupName]; |
|||
} |
|||
}); |
|||
} else { |
|||
debug("clearing entire index"); |
|||
|
|||
if (!redis) { |
|||
memCache.clear(); |
|||
} else { |
|||
// clear redis keys one by one from internal index to prevent clearing non-apicache entries
|
|||
index.all.forEach(function (key) { |
|||
clearTimeout(timers[key]); |
|||
delete timers[key]; |
|||
try { |
|||
redis.del(key); |
|||
} catch (err) { |
|||
console.log("[apicache] error in redis.del(\"" + key + "\")"); |
|||
} |
|||
}); |
|||
} |
|||
this.resetIndex(); |
|||
} |
|||
|
|||
return this.getIndex(); |
|||
}; |
|||
|
|||
function parseDuration(duration, defaultDuration) { |
|||
if (typeof duration === "number") { |
|||
return duration; |
|||
} |
|||
|
|||
if (typeof duration === "string") { |
|||
let split = duration.match(/^([\d\.,]+)\s?(\w+)$/); |
|||
|
|||
if (split.length === 3) { |
|||
let len = parseFloat(split[1]); |
|||
let unit = split[2].replace(/s$/i, "").toLowerCase(); |
|||
if (unit === "m") { |
|||
unit = "ms"; |
|||
} |
|||
|
|||
return (len || 1) * (t[unit] || 0); |
|||
} |
|||
} |
|||
|
|||
return defaultDuration; |
|||
} |
|||
|
|||
this.getDuration = function (duration) { |
|||
return parseDuration(duration, globalOptions.defaultDuration); |
|||
}; |
|||
|
|||
/** |
|||
* Return cache performance statistics (hit rate). Suitable for putting into a route: |
|||
* <code> |
|||
* app.get('/api/cache/performance', (req, res) => { |
|||
* res.json(apicache.getPerformance()) |
|||
* }) |
|||
* </code> |
|||
*/ |
|||
this.getPerformance = function () { |
|||
return performanceArray.map(function (p) { |
|||
return p.report(); |
|||
}); |
|||
}; |
|||
|
|||
this.getIndex = function (group) { |
|||
if (group) { |
|||
return index.groups[group]; |
|||
} else { |
|||
return index; |
|||
} |
|||
}; |
|||
|
|||
this.middleware = function cache(strDuration, middlewareToggle, localOptions) { |
|||
let duration = instance.getDuration(strDuration); |
|||
let opt = {}; |
|||
|
|||
middlewareOptions.push({ |
|||
options: opt, |
|||
}); |
|||
|
|||
let options = function (localOptions) { |
|||
if (localOptions) { |
|||
middlewareOptions.find(function (middleware) { |
|||
return middleware.options === opt; |
|||
}).localOptions = localOptions; |
|||
} |
|||
|
|||
syncOptions(); |
|||
|
|||
return opt; |
|||
}; |
|||
|
|||
options(localOptions); |
|||
|
|||
/** |
|||
* A Function for non tracking performance |
|||
*/ |
|||
function NOOPCachePerformance() { |
|||
this.report = this.hit = this.miss = function () {}; // noop;
|
|||
} |
|||
|
|||
/** |
|||
* A function for tracking and reporting hit rate. These statistics are returned by the getPerformance() call above. |
|||
*/ |
|||
function CachePerformance() { |
|||
/** |
|||
* Tracks the hit rate for the last 100 requests. |
|||
* If there have been fewer than 100 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast100 = new Uint8Array(100 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* Tracks the hit rate for the last 1000 requests. |
|||
* If there have been fewer than 1000 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast1000 = new Uint8Array(1000 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* Tracks the hit rate for the last 10000 requests. |
|||
* If there have been fewer than 10000 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast10000 = new Uint8Array(10000 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* Tracks the hit rate for the last 100000 requests. |
|||
* If there have been fewer than 100000 requests, the hit rate just considers the requests that have happened. |
|||
*/ |
|||
this.hitsLast100000 = new Uint8Array(100000 / 4); // each hit is 2 bits
|
|||
|
|||
/** |
|||
* The number of calls that have passed through the middleware since the server started. |
|||
*/ |
|||
this.callCount = 0; |
|||
|
|||
/** |
|||
* The total number of hits since the server started |
|||
*/ |
|||
this.hitCount = 0; |
|||
|
|||
/** |
|||
* The key from the last cache hit. This is useful in identifying which route these statistics apply to. |
|||
*/ |
|||
this.lastCacheHit = null; |
|||
|
|||
/** |
|||
* The key from the last cache miss. This is useful in identifying which route these statistics apply to. |
|||
*/ |
|||
this.lastCacheMiss = null; |
|||
|
|||
/** |
|||
* Return performance statistics |
|||
*/ |
|||
this.report = function () { |
|||
return { |
|||
lastCacheHit: this.lastCacheHit, |
|||
lastCacheMiss: this.lastCacheMiss, |
|||
callCount: this.callCount, |
|||
hitCount: this.hitCount, |
|||
missCount: this.callCount - this.hitCount, |
|||
hitRate: this.callCount == 0 ? null : this.hitCount / this.callCount, |
|||
hitRateLast100: this.hitRate(this.hitsLast100), |
|||
hitRateLast1000: this.hitRate(this.hitsLast1000), |
|||
hitRateLast10000: this.hitRate(this.hitsLast10000), |
|||
hitRateLast100000: this.hitRate(this.hitsLast100000), |
|||
}; |
|||
}; |
|||
|
|||
/** |
|||
* Computes a cache hit rate from an array of hits and misses. |
|||
* @param {Uint8Array} array An array representing hits and misses. |
|||
* @returns a number between 0 and 1, or null if the array has no hits or misses |
|||
*/ |
|||
this.hitRate = function (array) { |
|||
let hits = 0; |
|||
let misses = 0; |
|||
for (let i = 0; i < array.length; i++) { |
|||
let n8 = array[i]; |
|||
for (let j = 0; j < 4; j++) { |
|||
switch (n8 & 3) { |
|||
case 1: |
|||
hits++; |
|||
break; |
|||
case 2: |
|||
misses++; |
|||
break; |
|||
} |
|||
n8 >>= 2; |
|||
} |
|||
} |
|||
let total = hits + misses; |
|||
if (total == 0) { |
|||
return null; |
|||
} |
|||
return hits / total; |
|||
}; |
|||
|
|||
/** |
|||
* Record a hit or miss in the given array. It will be recorded at a position determined |
|||
* by the current value of the callCount variable. |
|||
* @param {Uint8Array} array An array representing hits and misses. |
|||
* @param {boolean} hit true for a hit, false for a miss |
|||
* Each element in the array is 8 bits, and encodes 4 hit/miss records. |
|||
* Each hit or miss is encoded as to bits as follows: |
|||
* 00 means no hit or miss has been recorded in these bits |
|||
* 01 encodes a hit |
|||
* 10 encodes a miss |
|||
*/ |
|||
this.recordHitInArray = function (array, hit) { |
|||
let arrayIndex = ~~(this.callCount / 4) % array.length; |
|||
let bitOffset = (this.callCount % 4) * 2; // 2 bits per record, 4 records per uint8 array element
|
|||
let clearMask = ~(3 << bitOffset); |
|||
let record = (hit ? 1 : 2) << bitOffset; |
|||
array[arrayIndex] = (array[arrayIndex] & clearMask) | record; |
|||
}; |
|||
|
|||
/** |
|||
* Records the hit or miss in the tracking arrays and increments the call count. |
|||
* @param {boolean} hit true records a hit, false records a miss |
|||
*/ |
|||
this.recordHit = function (hit) { |
|||
this.recordHitInArray(this.hitsLast100, hit); |
|||
this.recordHitInArray(this.hitsLast1000, hit); |
|||
this.recordHitInArray(this.hitsLast10000, hit); |
|||
this.recordHitInArray(this.hitsLast100000, hit); |
|||
if (hit) { |
|||
this.hitCount++; |
|||
} |
|||
this.callCount++; |
|||
}; |
|||
|
|||
/** |
|||
* Records a hit event, setting lastCacheMiss to the given key |
|||
* @param {string} key The key that had the cache hit |
|||
*/ |
|||
this.hit = function (key) { |
|||
this.recordHit(true); |
|||
this.lastCacheHit = key; |
|||
}; |
|||
|
|||
/** |
|||
* Records a miss event, setting lastCacheMiss to the given key |
|||
* @param {string} key The key that had the cache miss |
|||
*/ |
|||
this.miss = function (key) { |
|||
this.recordHit(false); |
|||
this.lastCacheMiss = key; |
|||
}; |
|||
} |
|||
|
|||
let perf = globalOptions.trackPerformance ? new CachePerformance() : new NOOPCachePerformance(); |
|||
|
|||
performanceArray.push(perf); |
|||
|
|||
let cache = function (req, res, next) { |
|||
function bypass() { |
|||
debug("bypass detected, skipping cache."); |
|||
return next(); |
|||
} |
|||
|
|||
// initial bypass chances
|
|||
if (!opt.enabled) { |
|||
return bypass(); |
|||
} |
|||
if ( |
|||
req.headers["x-apicache-bypass"] || |
|||
req.headers["x-apicache-force-fetch"] || |
|||
(opt.respectCacheControl && req.headers["cache-control"] == "no-cache") |
|||
) { |
|||
return bypass(); |
|||
} |
|||
|
|||
// REMOVED IN 0.11.1 TO CORRECT MIDDLEWARE TOGGLE EXECUTE ORDER
|
|||
// if (typeof middlewareToggle === 'function') {
|
|||
// if (!middlewareToggle(req, res)) return bypass()
|
|||
// } else if (middlewareToggle !== undefined && !middlewareToggle) {
|
|||
// return bypass()
|
|||
// }
|
|||
|
|||
// embed timer
|
|||
req.apicacheTimer = new Date(); |
|||
|
|||
// In Express 4.x the url is ambigious based on where a router is mounted. originalUrl will give the full Url
|
|||
let key = req.originalUrl || req.url; |
|||
|
|||
// Remove querystring from key if jsonp option is enabled
|
|||
if (opt.jsonp) { |
|||
key = url.parse(key).pathname; |
|||
} |
|||
|
|||
// add appendKey (either custom function or response path)
|
|||
if (typeof opt.appendKey === "function") { |
|||
key += "$$appendKey=" + opt.appendKey(req, res); |
|||
} else if (opt.appendKey.length > 0) { |
|||
let appendKey = req; |
|||
|
|||
for (let i = 0; i < opt.appendKey.length; i++) { |
|||
appendKey = appendKey[opt.appendKey[i]]; |
|||
} |
|||
key += "$$appendKey=" + appendKey; |
|||
} |
|||
|
|||
// attempt cache hit
|
|||
let redis = opt.redisClient; |
|||
let cached = !redis ? memCache.getValue(key) : null; |
|||
|
|||
// send if cache hit from memory-cache
|
|||
if (cached) { |
|||
let elapsed = new Date() - req.apicacheTimer; |
|||
debug("sending cached (memory-cache) version of", key, logDuration(elapsed)); |
|||
|
|||
perf.hit(key); |
|||
return sendCachedResponse(req, res, cached, middlewareToggle, next, duration); |
|||
} |
|||
|
|||
// send if cache hit from redis
|
|||
if (redis && redis.connected) { |
|||
try { |
|||
redis.hgetall(key, function (err, obj) { |
|||
if (!err && obj && obj.response) { |
|||
let elapsed = new Date() - req.apicacheTimer; |
|||
debug("sending cached (redis) version of", key, logDuration(elapsed)); |
|||
|
|||
perf.hit(key); |
|||
return sendCachedResponse( |
|||
req, |
|||
res, |
|||
JSON.parse(obj.response), |
|||
middlewareToggle, |
|||
next, |
|||
duration |
|||
); |
|||
} else { |
|||
perf.miss(key); |
|||
return makeResponseCacheable( |
|||
req, |
|||
res, |
|||
next, |
|||
key, |
|||
duration, |
|||
strDuration, |
|||
middlewareToggle |
|||
); |
|||
} |
|||
}); |
|||
} catch (err) { |
|||
// bypass redis on error
|
|||
perf.miss(key); |
|||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); |
|||
} |
|||
} else { |
|||
perf.miss(key); |
|||
return makeResponseCacheable(req, res, next, key, duration, strDuration, middlewareToggle); |
|||
} |
|||
}; |
|||
|
|||
cache.options = options; |
|||
|
|||
return cache; |
|||
}; |
|||
|
|||
this.options = function (options) { |
|||
if (options) { |
|||
Object.assign(globalOptions, options); |
|||
syncOptions(); |
|||
|
|||
if ("defaultDuration" in options) { |
|||
// Convert the default duration to a number in milliseconds (if needed)
|
|||
globalOptions.defaultDuration = parseDuration(globalOptions.defaultDuration, 3600000); |
|||
} |
|||
|
|||
if (globalOptions.trackPerformance) { |
|||
debug("WARNING: using trackPerformance flag can cause high memory usage!"); |
|||
} |
|||
|
|||
return this; |
|||
} else { |
|||
return globalOptions; |
|||
} |
|||
}; |
|||
|
|||
this.resetIndex = function () { |
|||
index = { |
|||
all: [], |
|||
groups: {}, |
|||
}; |
|||
}; |
|||
|
|||
this.newInstance = function (config) { |
|||
let instance = new ApiCache(); |
|||
|
|||
if (config) { |
|||
instance.options(config); |
|||
} |
|||
|
|||
return instance; |
|||
}; |
|||
|
|||
this.clone = function () { |
|||
return this.newInstance(this.options()); |
|||
}; |
|||
|
|||
// initialize index
|
|||
this.resetIndex(); |
|||
} |
|||
|
|||
module.exports = new ApiCache(); |
@ -0,0 +1,14 @@ |
|||
const apicache = require("./apicache"); |
|||
|
|||
apicache.options({ |
|||
headerBlacklist: [ |
|||
"cache-control" |
|||
], |
|||
headers: { |
|||
// Disable client side cache, only server side cache.
|
|||
// BUG! Not working for the second request
|
|||
"cache-control": "no-cache", |
|||
}, |
|||
}); |
|||
|
|||
module.exports = apicache; |
@ -0,0 +1,59 @@ |
|||
function MemoryCache() { |
|||
this.cache = {}; |
|||
this.size = 0; |
|||
} |
|||
|
|||
MemoryCache.prototype.add = function (key, value, time, timeoutCallback) { |
|||
let old = this.cache[key]; |
|||
let instance = this; |
|||
|
|||
let entry = { |
|||
value: value, |
|||
expire: time + Date.now(), |
|||
timeout: setTimeout(function () { |
|||
instance.delete(key); |
|||
return timeoutCallback && typeof timeoutCallback === "function" && timeoutCallback(value, key); |
|||
}, time) |
|||
}; |
|||
|
|||
this.cache[key] = entry; |
|||
this.size = Object.keys(this.cache).length; |
|||
|
|||
return entry; |
|||
}; |
|||
|
|||
MemoryCache.prototype.delete = function (key) { |
|||
let entry = this.cache[key]; |
|||
|
|||
if (entry) { |
|||
clearTimeout(entry.timeout); |
|||
} |
|||
|
|||
delete this.cache[key]; |
|||
|
|||
this.size = Object.keys(this.cache).length; |
|||
|
|||
return null; |
|||
}; |
|||
|
|||
MemoryCache.prototype.get = function (key) { |
|||
let entry = this.cache[key]; |
|||
|
|||
return entry; |
|||
}; |
|||
|
|||
MemoryCache.prototype.getValue = function (key) { |
|||
let entry = this.get(key); |
|||
|
|||
return entry && entry.value; |
|||
}; |
|||
|
|||
MemoryCache.prototype.clear = function () { |
|||
Object.keys(this.cache).forEach(function (key) { |
|||
this.delete(key); |
|||
}, this); |
|||
|
|||
return true; |
|||
}; |
|||
|
|||
module.exports = MemoryCache; |
@ -0,0 +1,26 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const child_process = require("child_process"); |
|||
|
|||
class Apprise extends NotificationProvider { |
|||
|
|||
name = "apprise"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let s = child_process.spawnSync("apprise", [ "-vv", "-b", msg, notification.appriseURL]) |
|||
|
|||
let output = (s.stdout) ? s.stdout.toString() : "ERROR: maybe apprise not found"; |
|||
|
|||
if (output) { |
|||
|
|||
if (! output.includes("ERROR")) { |
|||
return "Sent Successfully"; |
|||
} |
|||
|
|||
throw new Error(output) |
|||
} else { |
|||
return "No output from apprise"; |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Apprise; |
@ -0,0 +1,115 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const { DOWN, UP } = require("../../src/util"); |
|||
|
|||
class Discord extends NotificationProvider { |
|||
|
|||
name = "discord"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
const discordDisplayName = notification.discordUsername || "Uptime Kuma"; |
|||
|
|||
// If heartbeatJSON is null, assume we're testing.
|
|||
if (heartbeatJSON == null) { |
|||
let discordtestdata = { |
|||
username: discordDisplayName, |
|||
content: msg, |
|||
} |
|||
await axios.post(notification.discordWebhookUrl, discordtestdata) |
|||
return okMsg; |
|||
} |
|||
|
|||
let url; |
|||
|
|||
if (monitorJSON["type"] === "port") { |
|||
url = monitorJSON["hostname"]; |
|||
if (monitorJSON["port"]) { |
|||
url += ":" + monitorJSON["port"]; |
|||
} |
|||
|
|||
} else { |
|||
url = monitorJSON["url"]; |
|||
} |
|||
|
|||
// If heartbeatJSON is not null, we go into the normal alerting loop.
|
|||
if (heartbeatJSON["status"] == DOWN) { |
|||
let discorddowndata = { |
|||
username: discordDisplayName, |
|||
embeds: [{ |
|||
title: "❌ Your service " + monitorJSON["name"] + " went down. ❌", |
|||
color: 16711680, |
|||
timestamp: heartbeatJSON["time"], |
|||
fields: [ |
|||
{ |
|||
name: "Service Name", |
|||
value: monitorJSON["name"], |
|||
}, |
|||
{ |
|||
name: "Service URL", |
|||
value: url, |
|||
}, |
|||
{ |
|||
name: "Time (UTC)", |
|||
value: heartbeatJSON["time"], |
|||
}, |
|||
{ |
|||
name: "Error", |
|||
value: heartbeatJSON["msg"], |
|||
}, |
|||
], |
|||
}], |
|||
} |
|||
|
|||
if (notification.discordPrefixMessage) { |
|||
discorddowndata.content = notification.discordPrefixMessage; |
|||
} |
|||
|
|||
await axios.post(notification.discordWebhookUrl, discorddowndata) |
|||
return okMsg; |
|||
|
|||
} else if (heartbeatJSON["status"] == UP) { |
|||
let discordupdata = { |
|||
username: discordDisplayName, |
|||
embeds: [{ |
|||
title: "✅ Your service " + monitorJSON["name"] + " is up! ✅", |
|||
color: 65280, |
|||
timestamp: heartbeatJSON["time"], |
|||
fields: [ |
|||
{ |
|||
name: "Service Name", |
|||
value: monitorJSON["name"], |
|||
}, |
|||
{ |
|||
name: "Service URL", |
|||
value: url.startsWith("http") ? "[Visit Service](" + url + ")" : url, |
|||
}, |
|||
{ |
|||
name: "Time (UTC)", |
|||
value: heartbeatJSON["time"], |
|||
}, |
|||
{ |
|||
name: "Ping", |
|||
value: heartbeatJSON["ping"] + "ms", |
|||
}, |
|||
], |
|||
}], |
|||
} |
|||
|
|||
if (notification.discordPrefixMessage) { |
|||
discordupdata.content = notification.discordPrefixMessage; |
|||
} |
|||
|
|||
await axios.post(notification.discordWebhookUrl, discordupdata) |
|||
return okMsg; |
|||
} |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
module.exports = Discord; |
@ -0,0 +1,83 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const { DOWN, UP } = require("../../src/util"); |
|||
|
|||
class Feishu extends NotificationProvider { |
|||
name = "Feishu"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
let feishuWebHookUrl = notification.feishuWebHookUrl; |
|||
|
|||
try { |
|||
if (heartbeatJSON == null) { |
|||
let testdata = { |
|||
msg_type: "text", |
|||
content: { |
|||
text: msg, |
|||
}, |
|||
}; |
|||
await axios.post(feishuWebHookUrl, testdata); |
|||
return okMsg; |
|||
} |
|||
|
|||
if (heartbeatJSON["status"] == DOWN) { |
|||
let downdata = { |
|||
msg_type: "post", |
|||
content: { |
|||
post: { |
|||
zh_cn: { |
|||
title: "UptimeKuma Alert: " + monitorJSON["name"], |
|||
content: [ |
|||
[ |
|||
{ |
|||
tag: "text", |
|||
text: |
|||
"[Down] " + |
|||
heartbeatJSON["msg"] + |
|||
"\nTime (UTC): " + |
|||
heartbeatJSON["time"], |
|||
}, |
|||
], |
|||
], |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
await axios.post(feishuWebHookUrl, downdata); |
|||
return okMsg; |
|||
} |
|||
|
|||
if (heartbeatJSON["status"] == UP) { |
|||
let updata = { |
|||
msg_type: "post", |
|||
content: { |
|||
post: { |
|||
zh_cn: { |
|||
title: "UptimeKuma Alert: " + monitorJSON["name"], |
|||
content: [ |
|||
[ |
|||
{ |
|||
tag: "text", |
|||
text: |
|||
"[Up] " + |
|||
heartbeatJSON["msg"] + |
|||
"\nTime (UTC): " + |
|||
heartbeatJSON["time"], |
|||
}, |
|||
], |
|||
], |
|||
}, |
|||
}, |
|||
}, |
|||
}; |
|||
await axios.post(feishuWebHookUrl, updata); |
|||
return okMsg; |
|||
} |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Feishu; |
@ -0,0 +1,28 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
class Gotify extends NotificationProvider { |
|||
|
|||
name = "gotify"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
try { |
|||
if (notification.gotifyserverurl && notification.gotifyserverurl.endsWith("/")) { |
|||
notification.gotifyserverurl = notification.gotifyserverurl.slice(0, -1); |
|||
} |
|||
await axios.post(`${notification.gotifyserverurl}/message?token=${notification.gotifyapplicationToken}`, { |
|||
"message": msg, |
|||
"priority": notification.gotifyPriority || 8, |
|||
"title": "Uptime-Kuma", |
|||
}) |
|||
|
|||
return okMsg; |
|||
|
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Gotify; |
@ -0,0 +1,60 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const { DOWN, UP } = require("../../src/util"); |
|||
|
|||
class Line extends NotificationProvider { |
|||
|
|||
name = "line"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
try { |
|||
let lineAPIUrl = "https://api.line.me/v2/bot/message/push"; |
|||
let config = { |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
"Authorization": "Bearer " + notification.lineChannelAccessToken |
|||
} |
|||
}; |
|||
if (heartbeatJSON == null) { |
|||
let testMessage = { |
|||
"to": notification.lineUserID, |
|||
"messages": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "Test Successful!" |
|||
} |
|||
] |
|||
} |
|||
await axios.post(lineAPIUrl, testMessage, config) |
|||
} else if (heartbeatJSON["status"] == DOWN) { |
|||
let downMessage = { |
|||
"to": notification.lineUserID, |
|||
"messages": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "UptimeKuma Alert: [🔴 Down]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"] |
|||
} |
|||
] |
|||
} |
|||
await axios.post(lineAPIUrl, downMessage, config) |
|||
} else if (heartbeatJSON["status"] == UP) { |
|||
let upMessage = { |
|||
"to": notification.lineUserID, |
|||
"messages": [ |
|||
{ |
|||
"type": "text", |
|||
"text": "UptimeKuma Alert: [✅ Up]\n" + "Name: " + monitorJSON["name"] + " \n" + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"] |
|||
} |
|||
] |
|||
} |
|||
await axios.post(lineAPIUrl, upMessage, config) |
|||
} |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Line; |
@ -0,0 +1,48 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const { DOWN, UP } = require("../../src/util"); |
|||
|
|||
class LunaSea extends NotificationProvider { |
|||
|
|||
name = "lunasea"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
let lunaseadevice = "https://notify.lunasea.app/v1/custom/device/" + notification.lunaseaDevice |
|||
|
|||
try { |
|||
if (heartbeatJSON == null) { |
|||
let testdata = { |
|||
"title": "Uptime Kuma Alert", |
|||
"body": "Testing Successful.", |
|||
} |
|||
await axios.post(lunaseadevice, testdata) |
|||
return okMsg; |
|||
} |
|||
|
|||
if (heartbeatJSON["status"] == DOWN) { |
|||
let downdata = { |
|||
"title": "UptimeKuma Alert: " + monitorJSON["name"], |
|||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], |
|||
} |
|||
await axios.post(lunaseadevice, downdata) |
|||
return okMsg; |
|||
} |
|||
|
|||
if (heartbeatJSON["status"] == UP) { |
|||
let updata = { |
|||
"title": "UptimeKuma Alert: " + monitorJSON["name"], |
|||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], |
|||
} |
|||
await axios.post(lunaseadevice, updata) |
|||
return okMsg; |
|||
} |
|||
|
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
module.exports = LunaSea; |
@ -0,0 +1,45 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const Crypto = require("crypto"); |
|||
const { debug } = require("../../src/util"); |
|||
|
|||
class Matrix extends NotificationProvider { |
|||
name = "matrix"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
const size = 20; |
|||
const randomString = encodeURIComponent( |
|||
Crypto |
|||
.randomBytes(size) |
|||
.toString("base64") |
|||
.slice(0, size) |
|||
); |
|||
|
|||
debug("Random String: " + randomString); |
|||
|
|||
const roomId = encodeURIComponent(notification.internalRoomId); |
|||
|
|||
debug("Matrix Room ID: " + roomId); |
|||
|
|||
try { |
|||
let config = { |
|||
headers: { |
|||
"Authorization": `Bearer ${notification.accessToken}`, |
|||
} |
|||
}; |
|||
let data = { |
|||
"msgtype": "m.text", |
|||
"body": msg |
|||
}; |
|||
|
|||
await axios.put(`${notification.homeserverUrl}/_matrix/client/r0/rooms/${roomId}/send/m.room.message/${randomString}`, data, config); |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Matrix; |
@ -0,0 +1,123 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const { DOWN, UP } = require("../../src/util"); |
|||
|
|||
class Mattermost extends NotificationProvider { |
|||
|
|||
name = "mattermost"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
try { |
|||
const mattermostUserName = notification.mattermostusername || "Uptime Kuma"; |
|||
// If heartbeatJSON is null, assume we're testing.
|
|||
if (heartbeatJSON == null) { |
|||
let mattermostTestData = { |
|||
username: mattermostUserName, |
|||
text: msg, |
|||
} |
|||
await axios.post(notification.mattermostWebhookUrl, mattermostTestData) |
|||
return okMsg; |
|||
} |
|||
|
|||
const mattermostChannel = notification.mattermostchannel; |
|||
const mattermostIconEmoji = notification.mattermosticonemo; |
|||
const mattermostIconUrl = notification.mattermosticonurl; |
|||
|
|||
if (heartbeatJSON["status"] == DOWN) { |
|||
let mattermostdowndata = { |
|||
username: mattermostUserName, |
|||
text: "Uptime Kuma Alert", |
|||
channel: mattermostChannel, |
|||
icon_emoji: mattermostIconEmoji, |
|||
icon_url: mattermostIconUrl, |
|||
attachments: [ |
|||
{ |
|||
fallback: |
|||
"Your " + |
|||
monitorJSON["name"] + |
|||
" service went down.", |
|||
color: "#FF0000", |
|||
title: |
|||
"❌ " + |
|||
monitorJSON["name"] + |
|||
" service went down. ❌", |
|||
title_link: monitorJSON["url"], |
|||
fields: [ |
|||
{ |
|||
short: true, |
|||
title: "Service Name", |
|||
value: monitorJSON["name"], |
|||
}, |
|||
{ |
|||
short: true, |
|||
title: "Time (UTC)", |
|||
value: heartbeatJSON["time"], |
|||
}, |
|||
{ |
|||
short: false, |
|||
title: "Error", |
|||
value: heartbeatJSON["msg"], |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}; |
|||
await axios.post( |
|||
notification.mattermostWebhookUrl, |
|||
mattermostdowndata |
|||
); |
|||
return okMsg; |
|||
} else if (heartbeatJSON["status"] == UP) { |
|||
let mattermostupdata = { |
|||
username: mattermostUserName, |
|||
text: "Uptime Kuma Alert", |
|||
channel: mattermostChannel, |
|||
icon_emoji: mattermostIconEmoji, |
|||
icon_url: mattermostIconUrl, |
|||
attachments: [ |
|||
{ |
|||
fallback: |
|||
"Your " + |
|||
monitorJSON["name"] + |
|||
" service went up!", |
|||
color: "#32CD32", |
|||
title: |
|||
"✅ " + |
|||
monitorJSON["name"] + |
|||
" service went up! ✅", |
|||
title_link: monitorJSON["url"], |
|||
fields: [ |
|||
{ |
|||
short: true, |
|||
title: "Service Name", |
|||
value: monitorJSON["name"], |
|||
}, |
|||
{ |
|||
short: true, |
|||
title: "Time (UTC)", |
|||
value: heartbeatJSON["time"], |
|||
}, |
|||
{ |
|||
short: false, |
|||
title: "Ping", |
|||
value: heartbeatJSON["ping"] + "ms", |
|||
}, |
|||
], |
|||
}, |
|||
], |
|||
}; |
|||
await axios.post( |
|||
notification.mattermostWebhookUrl, |
|||
mattermostupdata |
|||
); |
|||
return okMsg; |
|||
} |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
module.exports = Mattermost; |
@ -0,0 +1,36 @@ |
|||
class NotificationProvider { |
|||
|
|||
/** |
|||
* Notification Provider Name |
|||
* @type string |
|||
*/ |
|||
name = undefined; |
|||
|
|||
/** |
|||
* @param notification : BeanModel |
|||
* @param msg : string General Message |
|||
* @param monitorJSON : object Monitor details (For Up/Down only) |
|||
* @param heartbeatJSON : object Heartbeat details (For Up/Down only) |
|||
* @returns {Promise<string>} Return Successful Message |
|||
* Throw Error with fail msg |
|||
*/ |
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
throw new Error("Have to override Notification.send(...)"); |
|||
} |
|||
|
|||
throwGeneralAxiosError(error) { |
|||
let msg = "Error: " + error + " "; |
|||
|
|||
if (error.response && error.response.data) { |
|||
if (typeof error.response.data === "string") { |
|||
msg += error.response.data; |
|||
} else { |
|||
msg += JSON.stringify(error.response.data) |
|||
} |
|||
} |
|||
|
|||
throw new Error(msg) |
|||
} |
|||
} |
|||
|
|||
module.exports = NotificationProvider; |
@ -0,0 +1,64 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
class Octopush extends NotificationProvider { |
|||
|
|||
name = "octopush"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
// Default - V2
|
|||
if (notification.octopushVersion == 2 || !notification.octopushVersion) { |
|||
let config = { |
|||
headers: { |
|||
"api-key": notification.octopushAPIKey, |
|||
"api-login": notification.octopushLogin, |
|||
"cache-control": "no-cache" |
|||
} |
|||
}; |
|||
let data = { |
|||
"recipients": [ |
|||
{ |
|||
"phone_number": notification.octopushPhoneNumber |
|||
} |
|||
], |
|||
//octopush not supporting non ascii char
|
|||
"text": msg.replace(/[^\x00-\x7F]/g, ""), |
|||
"type": notification.octopushSMSType, |
|||
"purpose": "alert", |
|||
"sender": notification.octopushSenderName |
|||
}; |
|||
await axios.post("https://api.octopush.com/v1/public/sms-campaign/send", data, config) |
|||
} else if (notification.octopushVersion == 1) { |
|||
let data = { |
|||
"user_login": notification.octopushDMLogin, |
|||
"api_key": notification.octopushDMAPIKey, |
|||
"sms_recipients": notification.octopushDMPhoneNumber, |
|||
"sms_sender": notification.octopushDMSenderName, |
|||
"sms_type": (notification.octopushDMSMSType == "sms_premium") ? "FR" : "XXX", |
|||
"transactional": "1", |
|||
//octopush not supporting non ascii char
|
|||
"sms_text": msg.replace(/[^\x00-\x7F]/g, ""), |
|||
}; |
|||
|
|||
let config = { |
|||
headers: { |
|||
"cache-control": "no-cache" |
|||
}, |
|||
params: data |
|||
}; |
|||
await axios.post("https://www.octopush-dm.com/api/sms/json", {}, config) |
|||
} else { |
|||
throw new Error("Unknown Octopush version!"); |
|||
} |
|||
|
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Octopush; |
@ -0,0 +1,41 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
class PromoSMS extends NotificationProvider { |
|||
|
|||
name = "promosms"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
let config = { |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
"Authorization": "Basic " + Buffer.from(notification.promosmsLogin + ":" + notification.promosmsPassword).toString('base64'), |
|||
"Accept": "text/json", |
|||
} |
|||
}; |
|||
let data = { |
|||
"recipients": [ notification.promosmsPhoneNumber ], |
|||
//Lets remove non ascii char
|
|||
"text": msg.replace(/[^\x00-\x7F]/g, ""), |
|||
"type": Number(notification.promosmsSMSType), |
|||
"sender": notification.promosmsSenderName |
|||
}; |
|||
|
|||
let resp = await axios.post("https://promosms.com/api/rest/v3_2/sms", data, config); |
|||
|
|||
if (resp.data.response.status !== 0) { |
|||
let error = "Something gone wrong. Api returned " + resp.data.response.status + "."; |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
|
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = PromoSMS; |
@ -0,0 +1,50 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
const { DOWN, UP } = require("../../src/util"); |
|||
|
|||
class Pushbullet extends NotificationProvider { |
|||
|
|||
name = "pushbullet"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
let pushbulletUrl = "https://api.pushbullet.com/v2/pushes"; |
|||
let config = { |
|||
headers: { |
|||
"Access-Token": notification.pushbulletAccessToken, |
|||
"Content-Type": "application/json" |
|||
} |
|||
}; |
|||
if (heartbeatJSON == null) { |
|||
let testdata = { |
|||
"type": "note", |
|||
"title": "Uptime Kuma Alert", |
|||
"body": "Testing Successful.", |
|||
} |
|||
await axios.post(pushbulletUrl, testdata, config) |
|||
} else if (heartbeatJSON["status"] == DOWN) { |
|||
let downdata = { |
|||
"type": "note", |
|||
"title": "UptimeKuma Alert: " + monitorJSON["name"], |
|||
"body": "[🔴 Down] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], |
|||
} |
|||
await axios.post(pushbulletUrl, downdata, config) |
|||
} else if (heartbeatJSON["status"] == UP) { |
|||
let updata = { |
|||
"type": "note", |
|||
"title": "UptimeKuma Alert: " + monitorJSON["name"], |
|||
"body": "[✅ Up] " + heartbeatJSON["msg"] + "\nTime (UTC): " + heartbeatJSON["time"], |
|||
} |
|||
await axios.post(pushbulletUrl, updata, config) |
|||
} |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Pushbullet; |
@ -0,0 +1,49 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
class Pushover extends NotificationProvider { |
|||
|
|||
name = "pushover"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
let pushoverlink = "https://api.pushover.net/1/messages.json" |
|||
|
|||
try { |
|||
if (heartbeatJSON == null) { |
|||
let data = { |
|||
"message": "<b>Uptime Kuma Pushover testing successful.</b>", |
|||
"user": notification.pushoveruserkey, |
|||
"token": notification.pushoverapptoken, |
|||
"sound": notification.pushoversounds, |
|||
"priority": notification.pushoverpriority, |
|||
"title": notification.pushovertitle, |
|||
"retry": "30", |
|||
"expire": "3600", |
|||
"html": 1, |
|||
} |
|||
await axios.post(pushoverlink, data) |
|||
return okMsg; |
|||
} |
|||
|
|||
let data = { |
|||
"message": "<b>Uptime Kuma Alert</b>\n\n<b>Message</b>:" + msg + "\n<b>Time (UTC)</b>:" + heartbeatJSON["time"], |
|||
"user": notification.pushoveruserkey, |
|||
"token": notification.pushoverapptoken, |
|||
"sound": notification.pushoversounds, |
|||
"priority": notification.pushoverpriority, |
|||
"title": notification.pushovertitle, |
|||
"retry": "30", |
|||
"expire": "3600", |
|||
"html": 1, |
|||
} |
|||
await axios.post(pushoverlink, data) |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
module.exports = Pushover; |
@ -0,0 +1,30 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
class Pushy extends NotificationProvider { |
|||
|
|||
name = "pushy"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
await axios.post(`https://api.pushy.me/push?api_key=${notification.pushyAPIKey}`, { |
|||
"to": notification.pushyToken, |
|||
"data": { |
|||
"message": "Uptime-Kuma" |
|||
}, |
|||
"notification": { |
|||
"body": msg, |
|||
"badge": 1, |
|||
"sound": "ping.aiff" |
|||
} |
|||
}) |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Pushy; |
@ -0,0 +1,66 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const Slack = require("./slack"); |
|||
const { setting } = require("../util-server"); |
|||
const { getMonitorRelativeURL, UP, DOWN } = require("../../src/util"); |
|||
|
|||
class RocketChat extends NotificationProvider { |
|||
|
|||
name = "rocket.chat"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
try { |
|||
if (heartbeatJSON == null) { |
|||
let data = { |
|||
"text": msg, |
|||
"channel": notification.rocketchannel, |
|||
"username": notification.rocketusername, |
|||
"icon_emoji": notification.rocketiconemo, |
|||
}; |
|||
await axios.post(notification.rocketwebhookURL, data); |
|||
return okMsg; |
|||
} |
|||
|
|||
const time = heartbeatJSON["time"]; |
|||
|
|||
let data = { |
|||
"text": "Uptime Kuma Alert", |
|||
"channel": notification.rocketchannel, |
|||
"username": notification.rocketusername, |
|||
"icon_emoji": notification.rocketiconemo, |
|||
"attachments": [ |
|||
{ |
|||
"title": "Uptime Kuma Alert *Time (UTC)*\n" + time, |
|||
"text": "*Message*\n" + msg, |
|||
} |
|||
] |
|||
}; |
|||
|
|||
// Color
|
|||
if (heartbeatJSON.status === DOWN) { |
|||
data.attachments[0].color = "#ff0000"; |
|||
} else { |
|||
data.attachments[0].color = "#32cd32"; |
|||
} |
|||
|
|||
if (notification.rocketbutton) { |
|||
await Slack.deprecateURL(notification.rocketbutton); |
|||
} |
|||
|
|||
const baseURL = await setting("primaryBaseURL"); |
|||
|
|||
if (baseURL) { |
|||
data.attachments[0].title_link = baseURL + getMonitorRelativeURL(monitorJSON.id); |
|||
} |
|||
|
|||
await axios.post(notification.rocketwebhookURL, data); |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
module.exports = RocketChat; |
@ -0,0 +1,27 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
class Signal extends NotificationProvider { |
|||
|
|||
name = "signal"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
let data = { |
|||
"message": msg, |
|||
"number": notification.signalNumber, |
|||
"recipients": notification.signalRecipients.replace(/\s/g, "").split(","), |
|||
}; |
|||
let config = {}; |
|||
|
|||
await axios.post(notification.signalURL, data, config) |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Signal; |
@ -0,0 +1,98 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const { setSettings, setting } = require("../util-server"); |
|||
const { getMonitorRelativeURL } = require("../../src/util"); |
|||
|
|||
class Slack extends NotificationProvider { |
|||
|
|||
name = "slack"; |
|||
|
|||
/** |
|||
* Deprecated property notification.slackbutton |
|||
* Set it as primary base url if this is not yet set. |
|||
*/ |
|||
static async deprecateURL(url) { |
|||
let currentPrimaryBaseURL = await setting("primaryBaseURL"); |
|||
|
|||
if (!currentPrimaryBaseURL) { |
|||
console.log("Move the url to be the primary base URL"); |
|||
await setSettings("general", { |
|||
primaryBaseURL: url, |
|||
}); |
|||
} else { |
|||
console.log("Already there, no need to move the primary base URL"); |
|||
} |
|||
} |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
try { |
|||
if (heartbeatJSON == null) { |
|||
let data = { |
|||
"text": msg, |
|||
"channel": notification.slackchannel, |
|||
"username": notification.slackusername, |
|||
"icon_emoji": notification.slackiconemo, |
|||
}; |
|||
await axios.post(notification.slackwebhookURL, data); |
|||
return okMsg; |
|||
} |
|||
|
|||
const time = heartbeatJSON["time"]; |
|||
let data = { |
|||
"text": "Uptime Kuma Alert", |
|||
"channel": notification.slackchannel, |
|||
"username": notification.slackusername, |
|||
"icon_emoji": notification.slackiconemo, |
|||
"blocks": [{ |
|||
"type": "header", |
|||
"text": { |
|||
"type": "plain_text", |
|||
"text": "Uptime Kuma Alert", |
|||
}, |
|||
}, |
|||
{ |
|||
"type": "section", |
|||
"fields": [{ |
|||
"type": "mrkdwn", |
|||
"text": "*Message*\n" + msg, |
|||
}, |
|||
{ |
|||
"type": "mrkdwn", |
|||
"text": "*Time (UTC)*\n" + time, |
|||
}], |
|||
}], |
|||
}; |
|||
|
|||
if (notification.slackbutton) { |
|||
await Slack.deprecateURL(notification.slackbutton); |
|||
} |
|||
|
|||
const baseURL = await setting("primaryBaseURL"); |
|||
|
|||
// Button
|
|||
if (baseURL) { |
|||
data.blocks.push({ |
|||
"type": "actions", |
|||
"elements": [{ |
|||
"type": "button", |
|||
"text": { |
|||
"type": "plain_text", |
|||
"text": "Visit Uptime Kuma", |
|||
}, |
|||
"value": "Uptime-Kuma", |
|||
"url": baseURL + getMonitorRelativeURL(monitorJSON.id), |
|||
}], |
|||
}); |
|||
} |
|||
|
|||
await axios.post(notification.slackwebhookURL, data); |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
module.exports = Slack; |
@ -0,0 +1,48 @@ |
|||
const nodemailer = require("nodemailer"); |
|||
const NotificationProvider = require("./notification-provider"); |
|||
|
|||
class SMTP extends NotificationProvider { |
|||
|
|||
name = "smtp"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
|
|||
const config = { |
|||
host: notification.smtpHost, |
|||
port: notification.smtpPort, |
|||
secure: notification.smtpSecure, |
|||
}; |
|||
|
|||
// Should fix the issue in https://github.com/louislam/uptime-kuma/issues/26#issuecomment-896373904
|
|||
if (notification.smtpUsername || notification.smtpPassword) { |
|||
config.auth = { |
|||
user: notification.smtpUsername, |
|||
pass: notification.smtpPassword, |
|||
}; |
|||
} |
|||
|
|||
let transporter = nodemailer.createTransport(config); |
|||
|
|||
let bodyTextContent = msg; |
|||
if (heartbeatJSON) { |
|||
bodyTextContent = `${msg}\nTime (UTC): ${heartbeatJSON["time"]}`; |
|||
} |
|||
|
|||
// send mail with defined transport object
|
|||
await transporter.sendMail({ |
|||
from: notification.smtpFrom, |
|||
cc: notification.smtpCC, |
|||
bcc: notification.smtpBCC, |
|||
to: notification.smtpTo, |
|||
subject: msg, |
|||
text: bodyTextContent, |
|||
tls: { |
|||
rejectUnauthorized: notification.smtpIgnoreTLSError || false, |
|||
}, |
|||
}); |
|||
|
|||
return "Sent Successfully."; |
|||
} |
|||
} |
|||
|
|||
module.exports = SMTP; |
@ -0,0 +1,124 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const { DOWN, UP } = require("../../src/util"); |
|||
|
|||
class Teams extends NotificationProvider { |
|||
name = "teams"; |
|||
|
|||
_statusMessageFactory = (status, monitorName) => { |
|||
if (status === DOWN) { |
|||
return `🔴 Application [${monitorName}] went down`; |
|||
} else if (status === UP) { |
|||
return `✅ Application [${monitorName}] is back online`; |
|||
} |
|||
return "Notification"; |
|||
}; |
|||
|
|||
_getThemeColor = (status) => { |
|||
if (status === DOWN) { |
|||
return "ff0000"; |
|||
} |
|||
if (status === UP) { |
|||
return "00e804"; |
|||
} |
|||
return "008cff"; |
|||
}; |
|||
|
|||
_notificationPayloadFactory = ({ |
|||
status, |
|||
monitorMessage, |
|||
monitorName, |
|||
monitorUrl, |
|||
}) => { |
|||
const notificationMessage = this._statusMessageFactory( |
|||
status, |
|||
monitorName |
|||
); |
|||
|
|||
const facts = []; |
|||
|
|||
if (monitorName) { |
|||
facts.push({ |
|||
name: "Monitor", |
|||
value: monitorName, |
|||
}); |
|||
} |
|||
|
|||
if (monitorUrl) { |
|||
facts.push({ |
|||
name: "URL", |
|||
value: monitorUrl, |
|||
}); |
|||
} |
|||
|
|||
return { |
|||
"@context": "https://schema.org/extensions", |
|||
"@type": "MessageCard", |
|||
themeColor: this._getThemeColor(status), |
|||
summary: notificationMessage, |
|||
sections: [ |
|||
{ |
|||
activityImage: |
|||
"https://raw.githubusercontent.com/louislam/uptime-kuma/master/public/icon.png", |
|||
activityTitle: "**Uptime Kuma**", |
|||
}, |
|||
{ |
|||
activityTitle: notificationMessage, |
|||
}, |
|||
{ |
|||
activityTitle: "**Description**", |
|||
text: monitorMessage, |
|||
facts, |
|||
}, |
|||
], |
|||
}; |
|||
}; |
|||
|
|||
_sendNotification = async (webhookUrl, payload) => { |
|||
await axios.post(webhookUrl, payload); |
|||
}; |
|||
|
|||
_handleGeneralNotification = (webhookUrl, msg) => { |
|||
const payload = this._notificationPayloadFactory({ |
|||
monitorMessage: msg |
|||
}); |
|||
|
|||
return this._sendNotification(webhookUrl, payload); |
|||
}; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
if (heartbeatJSON == null) { |
|||
await this._handleGeneralNotification(notification.webhookUrl, msg); |
|||
return okMsg; |
|||
} |
|||
|
|||
let url; |
|||
|
|||
if (monitorJSON["type"] === "port") { |
|||
url = monitorJSON["hostname"]; |
|||
if (monitorJSON["port"]) { |
|||
url += ":" + monitorJSON["port"]; |
|||
} |
|||
} else { |
|||
url = monitorJSON["url"]; |
|||
} |
|||
|
|||
const payload = this._notificationPayloadFactory({ |
|||
monitorMessage: heartbeatJSON.msg, |
|||
monitorName: monitorJSON.name, |
|||
monitorUrl: url, |
|||
status: heartbeatJSON.status, |
|||
}); |
|||
|
|||
await this._sendNotification(notification.webhookUrl, payload); |
|||
return okMsg; |
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Teams; |
@ -0,0 +1,27 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
|
|||
class Telegram extends NotificationProvider { |
|||
|
|||
name = "telegram"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
await axios.get(`https://api.telegram.org/bot${notification.telegramBotToken}/sendMessage`, { |
|||
params: { |
|||
chat_id: notification.telegramChatID, |
|||
text: msg, |
|||
}, |
|||
}) |
|||
return okMsg; |
|||
|
|||
} catch (error) { |
|||
let msg = (error.response.data.description) ? error.response.data.description : "Error without description" |
|||
throw new Error(msg) |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = Telegram; |
@ -0,0 +1,44 @@ |
|||
const NotificationProvider = require("./notification-provider"); |
|||
const axios = require("axios"); |
|||
const FormData = require("form-data"); |
|||
|
|||
class Webhook extends NotificationProvider { |
|||
|
|||
name = "webhook"; |
|||
|
|||
async send(notification, msg, monitorJSON = null, heartbeatJSON = null) { |
|||
let okMsg = "Sent Successfully."; |
|||
|
|||
try { |
|||
let data = { |
|||
heartbeat: heartbeatJSON, |
|||
monitor: monitorJSON, |
|||
msg, |
|||
}; |
|||
let finalData; |
|||
let config = {}; |
|||
|
|||
if (notification.webhookContentType === "form-data") { |
|||
finalData = new FormData(); |
|||
finalData.append("data", JSON.stringify(data)); |
|||
|
|||
config = { |
|||
headers: finalData.getHeaders(), |
|||
} |
|||
|
|||
} else { |
|||
finalData = data; |
|||
} |
|||
|
|||
await axios.post(notification.webhookURL, finalData, config) |
|||
return okMsg; |
|||
|
|||
} catch (error) { |
|||
this.throwGeneralAxiosError(error) |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
|||
|
|||
module.exports = Webhook; |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue