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 @@ |
|||||
|
# 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 |
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 = { |
let options = { |
||||
host: "localhost", |
host: process.env.HOST || "127.0.0.1", |
||||
port: "3001", |
port: parseInt(process.env.PORT) || 3001, |
||||
timeout: 2000, |
timeout: 28 * 1000, |
||||
}; |
}; |
||||
let request = http.request(options, (res) => { |
|
||||
console.log(`STATUS: ${res.statusCode}`); |
let request = client.request(options, (res) => { |
||||
if (res.statusCode == 200) { |
console.log(`Health Check OK [Res Code: ${res.statusCode}]`); |
||||
|
if (res.statusCode === 200) { |
||||
process.exit(0); |
process.exit(0); |
||||
} else { |
} else { |
||||
process.exit(1); |
process.exit(1); |
||||
} |
} |
||||
}); |
}); |
||||
|
|
||||
request.on("error", function (err) { |
request.on("error", function (err) { |
||||
console.log("ERROR"); |
console.error("Health Check ERROR"); |
||||
process.exit(1); |
process.exit(1); |
||||
}); |
}); |
||||
|
|
||||
request.end(); |
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; |
@ -0,0 +1,191 @@ |
|||||
|
let express = require("express"); |
||||
|
const { allowDevAllOrigin, getSettings, setting } = require("../util-server"); |
||||
|
const { R } = require("redbean-node"); |
||||
|
const server = require("../server"); |
||||
|
const apicache = require("../modules/apicache"); |
||||
|
const Monitor = require("../model/monitor"); |
||||
|
const dayjs = require("dayjs"); |
||||
|
const { UP } = require("../../src/util"); |
||||
|
let router = express.Router(); |
||||
|
|
||||
|
let cache = apicache.middleware; |
||||
|
let io = server.io; |
||||
|
|
||||
|
router.get("/api/entry-page", async (_, response) => { |
||||
|
allowDevAllOrigin(response); |
||||
|
response.json(server.entryPage); |
||||
|
}); |
||||
|
|
||||
|
router.get("/api/push/:pushToken", async (request, response) => { |
||||
|
try { |
||||
|
let pushToken = request.params.pushToken; |
||||
|
let msg = request.query.msg || "OK"; |
||||
|
let ping = request.query.ping; |
||||
|
|
||||
|
let monitor = await R.findOne("monitor", " push_token = ? AND active = 1 ", [ |
||||
|
pushToken |
||||
|
]); |
||||
|
|
||||
|
if (! monitor) { |
||||
|
throw new Error("Monitor not found or not active."); |
||||
|
} |
||||
|
|
||||
|
let bean = R.dispense("heartbeat"); |
||||
|
bean.monitor_id = monitor.id; |
||||
|
bean.time = R.isoDateTime(dayjs.utc()); |
||||
|
bean.status = UP; |
||||
|
bean.msg = msg; |
||||
|
bean.ping = ping; |
||||
|
|
||||
|
await R.store(bean); |
||||
|
|
||||
|
io.to(monitor.user_id).emit("heartbeat", bean.toJSON()); |
||||
|
Monitor.sendStats(io, monitor.id, monitor.user_id); |
||||
|
|
||||
|
response.json({ |
||||
|
ok: true, |
||||
|
}); |
||||
|
} catch (e) { |
||||
|
response.json({ |
||||
|
ok: false, |
||||
|
msg: e.message |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Status Page Config
|
||||
|
router.get("/api/status-page/config", async (_request, response) => { |
||||
|
allowDevAllOrigin(response); |
||||
|
|
||||
|
let config = await getSettings("statusPage"); |
||||
|
|
||||
|
if (! config.statusPageTheme) { |
||||
|
config.statusPageTheme = "light"; |
||||
|
} |
||||
|
|
||||
|
if (! config.statusPagePublished) { |
||||
|
config.statusPagePublished = true; |
||||
|
} |
||||
|
|
||||
|
if (! config.title) { |
||||
|
config.title = "Uptime Kuma"; |
||||
|
} |
||||
|
|
||||
|
response.json(config); |
||||
|
}); |
||||
|
|
||||
|
// Status Page - Get the current Incident
|
||||
|
// Can fetch only if published
|
||||
|
router.get("/api/status-page/incident", async (_, response) => { |
||||
|
allowDevAllOrigin(response); |
||||
|
|
||||
|
try { |
||||
|
await checkPublished(); |
||||
|
|
||||
|
let incident = await R.findOne("incident", " pin = 1 AND active = 1"); |
||||
|
|
||||
|
if (incident) { |
||||
|
incident = incident.toPublicJSON(); |
||||
|
} |
||||
|
|
||||
|
response.json({ |
||||
|
ok: true, |
||||
|
incident, |
||||
|
}); |
||||
|
|
||||
|
} catch (error) { |
||||
|
send403(response, error.message); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Status Page - Monitor List
|
||||
|
// Can fetch only if published
|
||||
|
router.get("/api/status-page/monitor-list", cache("5 minutes"), async (_request, response) => { |
||||
|
allowDevAllOrigin(response); |
||||
|
|
||||
|
try { |
||||
|
await checkPublished(); |
||||
|
const publicGroupList = []; |
||||
|
let list = await R.find("group", " public = 1 ORDER BY weight "); |
||||
|
|
||||
|
for (let groupBean of list) { |
||||
|
publicGroupList.push(await groupBean.toPublicJSON()); |
||||
|
} |
||||
|
|
||||
|
response.json(publicGroupList); |
||||
|
|
||||
|
} catch (error) { |
||||
|
send403(response, error.message); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Status Page Polling Data
|
||||
|
// Can fetch only if published
|
||||
|
router.get("/api/status-page/heartbeat", cache("5 minutes"), async (_request, response) => { |
||||
|
allowDevAllOrigin(response); |
||||
|
|
||||
|
try { |
||||
|
await checkPublished(); |
||||
|
|
||||
|
let heartbeatList = {}; |
||||
|
let uptimeList = {}; |
||||
|
|
||||
|
let monitorIDList = await R.getCol(` |
||||
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\` |
||||
|
WHERE monitor_group.group_id = \`group\`.id
|
||||
|
AND public = 1 |
||||
|
`);
|
||||
|
|
||||
|
for (let monitorID of monitorIDList) { |
||||
|
let list = await R.getAll(` |
||||
|
SELECT * FROM heartbeat |
||||
|
WHERE monitor_id = ? |
||||
|
ORDER BY time DESC |
||||
|
LIMIT 50 |
||||
|
`, [
|
||||
|
monitorID, |
||||
|
]); |
||||
|
|
||||
|
list = R.convertToBeans("heartbeat", list); |
||||
|
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON()); |
||||
|
|
||||
|
const type = 24; |
||||
|
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID); |
||||
|
} |
||||
|
|
||||
|
response.json({ |
||||
|
heartbeatList, |
||||
|
uptimeList |
||||
|
}); |
||||
|
|
||||
|
} catch (error) { |
||||
|
send403(response, error.message); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
async function checkPublished() { |
||||
|
if (! await isPublished()) { |
||||
|
throw new Error("The status page is not published"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Default is published |
||||
|
* @returns {Promise<boolean>} |
||||
|
*/ |
||||
|
async function isPublished() { |
||||
|
const value = await setting("statusPagePublished"); |
||||
|
if (value === null) { |
||||
|
return true; |
||||
|
} |
||||
|
return value; |
||||
|
} |
||||
|
|
||||
|
function send403(res, msg = "") { |
||||
|
res.status(403).json({ |
||||
|
"status": "fail", |
||||
|
"msg": msg, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
module.exports = router; |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue