38 Commits

Author SHA1 Message Date
ccc439d30a feat(commands): add uptime command with service selection and autocomplete
fix(Dockerfile): ensure entrypoint runs register command
fix(status.service): update getStatusImageBar to use serviceId
refactor(follow.command): simplify service lookup logic
refactor(deploy-commands): remove unnecessary DataSource initialization
2025-12-15 09:40:24 +01:00
1bffb9a971 fix(status): update notification sending logic to use service IDs 2025-12-13 19:14:53 +01:00
034921d00d fix(status): add log statement for notification sending process 2025-12-13 19:11:48 +01:00
b0ced298c0 fix(Dockerfile): comment out unused npm register command 2025-12-13 18:51:24 +01:00
5fda9afb83 feat(commands): implement follow command with service selection and autocomplete
fix(deploy): initialize and destroy DataSource for command deployment
fix(index): handle autocomplete interactions in command execution
refactor(types): add autocomplete support to CommandDefinition type
fix(Dockerfile): ensure npm run register is executed during build
2025-12-13 18:47:40 +01:00
CL TheDreWen
3d2a1125b3 Update Dockerfile 2025-11-28 14:13:42 +01:00
ecb12f12b7 fix(status): remove unnecessary console log for guild name in StatusService 2025-11-26 11:01:06 +01:00
55e4cf3e6c feat(database): refactor entities and relationships for service management 2025-11-26 10:57:57 +01:00
104023162a feat(service): add Service entity with properties for service management 2025-11-26 09:45:16 +01:00
58403fd32e fix(deploy): remove redundant line for building new Docker image in deployment script 2025-11-26 09:25:34 +01:00
fcef934e60 fix(status): update host addresses and remove unused attachment logic in status command 2025-11-26 09:22:36 +01:00
aebc6be99e fix(status): adjust canvas dimensions and scaling for improved uptime bar rendering 2025-11-25 10:49:20 +01:00
9c87e8c35e fix(status): conditionally display status bar components based on live status 2025-11-25 10:45:18 +01:00
fe8d3dc78f fix(status): log errors in message editing to improve debugging 2025-11-25 10:43:00 +01:00
401ae56113 feat(status): enhance status command with image attachment and update query logic 2025-11-25 10:37:00 +01:00
e41be1e1f0 fix(status): remove unused fs imports from status.service.ts 2025-11-25 10:23:50 +01:00
c658881d24 feat: add canvas dependency and implement uptime bar image generation
- Added 'canvas' dependency to package.json and package-lock.json.
- Updated getUpdatedContainer method in StatusService to be asynchronous.
- Implemented getStatusImageBar method in StatusService to generate a visual representation of uptime data.
- Modified live_status.command.ts and statut.command.ts to await the updated container.
- Created test.ts to demonstrate uptime bar generation and save it as an image.
- Updated tsconfig.json to target ES2024 and include relevant libraries.
2025-11-25 10:22:41 +01:00
5ec7187afd fix(status): remove unused router configurations for ROUTER-DE 03 and ROUTER-DE 04 2025-11-22 10:25:27 +01:00
2aa5c56ca7 fix(status): update router title to include demonstration data notice 2025-11-09 08:32:06 +01:00
7b35fcf31b fix(status): update host configuration for ROUTER-DE 04 2025-11-07 11:01:57 +01:00
a5476b26fe feat(status): add router configurations and update InfraType to include routers 2025-11-07 10:49:56 +01:00
c99c11c241 feat(status): add new game server notification for RYZEN-GAME 02 2025-11-07 10:32:17 +01:00
df048c1352 fix(status): remove unnecessary quotes from host and name properties 2025-11-04 18:13:17 +01:00
290e8b982a feat(status): enhance website status message with globe icon and separator 2025-11-03 18:32:35 +01:00
8bfeb1c43c fix(status): update status message to include website link in notifications 2025-11-03 18:27:35 +01:00
e2a8255d5a fix(docker): comment out npm run register command in Dockerfile 2025-11-03 18:17:58 +01:00
85ec27cb2b fix(docker): correct npm command to run register in Dockerfile 2025-11-03 18:09:38 +01:00
e2c896c6f1 fix(readme): update deployment workflow status to in production 2025-11-03 18:06:00 +01:00
3ff4278217 feat(deploy): add GitHub Actions workflow for deployment to VPS 2025-11-03 18:05:47 +01:00
7b80aca9e1 fix(status): update timestamp format in notifications to include full date and time 2025-11-03 14:36:24 +01:00
afd8d1f68a fix(status): integrate dayjs for formatted timestamps in notifications 2025-11-03 14:35:06 +01:00
c571e03495 fix(client): log loaded guild configurations on client ready 2025-11-03 14:24:47 +01:00
a577f99277 fix(live_status): remove ephemeral flag from error message in channel permissions 2025-11-03 13:41:09 +01:00
4b6b2c8575 fix(live_status): change reply to editReply for error handling in channel permissions 2025-11-03 13:40:39 +01:00
e375fb2631 fix(live_status): handle errors when sending messages to the channel 2025-11-03 13:38:51 +01:00
39178d1322 fix(readme): correct project title to "Protojx Manager" 2025-11-03 13:32:09 +01:00
d7b772544f fix(live_status): add default member permissions for command execution 2025-11-03 13:23:33 +01:00
27fc82d371 fix(readme): update status of filter feature to reflect production readiness 2025-11-03 13:21:34 +01:00
19 changed files with 740 additions and 178 deletions

52
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
# This is a basic workflow to help you get started with Actions
name: Deploy
# Controls when the workflow will run
on:
push:
tags:
- '*'
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- name: Setup SSH Agent
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Add VPS to known_hosts
run: |
ssh-keyscan -p ${{ secrets.VPS_PORT }} -H ${{ secrets.VPS_HOST }} >> ~/.ssh/known_hosts
- name: Deploy to VPS
run: |
ssh -p ${{ secrets.VPS_PORT }} ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }} "
cd /home/${{ secrets.VPS_USER }}/protojx/protojx-manager &&
git pull &&
# Build new image
docker buildx build -t protojx_manager . &&
# Stop and remove old container if exists
docker stop protojx_manager 2>/dev/null || true &&
docker rm protojx_manager 2>/dev/null || true &&
# Run new container
docker run -d \
--name protojx_manager \
--restart unless-stopped \
--network shared \
protojx_manager
"

1
.gitignore vendored
View File

@@ -137,3 +137,4 @@ dist
# Vite logs files # Vite logs files
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
note.txt

View File

@@ -5,6 +5,9 @@ WORKDIR /app
COPY . . COPY . .
RUN apt-get update && apt-get install -y iputils-ping RUN apt-get update && apt-get install -y iputils-ping
RUN npm i RUN npm clean-install
# RUN npm run register
ENTRYPOINT [ "npm", "run", "register" ]
CMD [ "npm", "run", "start" ] CMD [ "npm", "run", "start" ]

View File

@@ -1,4 +1,4 @@
# Protojx Manager Non Official # Protojx Manager
A status bot and other features for protojx. A status bot and other features for protojx.
- Add the bot : https://discord.com/oauth2/authorize?client_id=1432680068085190656 - Add the bot : https://discord.com/oauth2/authorize?client_id=1432680068085190656
@@ -12,8 +12,8 @@ A status bot and other features for protojx.
| Number of services down in the bot's status. | 🌐 | | Number of services down in the bot's status. | 🌐 |
| Notification system in case of downtime. | 🌐 | | Notification system in case of downtime. | 🌐 |
| Ability to create persistent status messages that update automatically. (/live_status) | 🌐 | | Ability to create persistent status messages that update automatically. (/live_status) | 🌐 |
| Deployment workflow on Oracle VPS. | | | Deployment workflow on Oracle VPS. | 🌐 |
| Filter for notifs. | | | Filter for notifs. | 🌐 |
- 🌐 -> In production - 🌐 -> In production
- ✅ -> Done - ✅ -> Done

384
package-lock.json generated
View File

@@ -10,7 +10,9 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/ping": "^0.4.4", "@types/ping": "^0.4.4",
"canvas": "^3.2.0",
"cron": "^4.3.3", "cron": "^4.3.3",
"dayjs": "^1.11.19",
"discord.js": "^14.24.2", "discord.js": "^14.24.2",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"pg": "^8.16.3", "pg": "^8.16.3",
@@ -339,6 +341,41 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -419,6 +456,26 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/canvas": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.0.tgz",
"integrity": "sha512-jk0GxrLtUEmW/TmFsk2WghvgHe8B0pxGilqCL21y8lHkPUGa6FTsnCNtHPOzT8O3y+N+m3espawV80bbBlgfTA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.3"
},
"engines": {
"node": "^18.12.0 || >= 20.9.0"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "8.0.1", "version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@@ -552,9 +609,9 @@
} }
}, },
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.18", "version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
@@ -574,6 +631,21 @@
} }
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dedent": { "node_modules/dedent": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
@@ -588,6 +660,15 @@
} }
} }
}, },
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/define-data-property": { "node_modules/define-data-property": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@@ -605,6 +686,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/discord-api-types": { "node_modules/discord-api-types": {
"version": "0.38.31", "version": "0.38.31",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.31.tgz", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.31.tgz",
@@ -679,6 +769,15 @@
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -718,6 +817,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -755,6 +863,12 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -810,6 +924,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": { "node_modules/glob": {
"version": "10.4.5", "version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -919,6 +1039,12 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/is-callable": { "node_modules/is-callable": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
@@ -1024,6 +1150,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -1039,6 +1177,15 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@@ -1048,12 +1195,51 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.85.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/package-json-from-dist": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -1231,6 +1417,71 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
@@ -1266,6 +1517,18 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -1336,6 +1599,51 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/split2": { "node_modules/split2": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -1361,6 +1669,15 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -1457,6 +1774,43 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/to-buffer": { "node_modules/to-buffer": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
@@ -1483,6 +1837,18 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/typed-array-buffer": { "node_modules/typed-array-buffer": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@@ -1640,6 +2006,12 @@
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": { "node_modules/uuid": {
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
@@ -1780,6 +2152,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/ws": { "node_modules/ws": {
"version": "8.18.3", "version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",

View File

@@ -29,7 +29,9 @@
}, },
"dependencies": { "dependencies": {
"@types/ping": "^0.4.4", "@types/ping": "^0.4.4",
"canvas": "^3.2.0",
"cron": "^4.3.3", "cron": "^4.3.3",
"dayjs": "^1.11.19",
"discord.js": "^14.24.2", "discord.js": "^14.24.2",
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"pg": "^8.16.3", "pg": "^8.16.3",

View File

@@ -4,10 +4,16 @@ import { AppDataSource } from "../../data-source";
import { Follow } from "../../entity/follow.entity"; import { Follow } from "../../entity/follow.entity";
import statusService from "../../services/status.service"; import statusService from "../../services/status.service";
const cmd : CommandDefinition = { const cmd: CommandDefinition = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('follow') .setName('follow')
.setDescription('Enables/disables the receipt of service status notifications.') .setDescription('Enables/disables the receipt of service status notifications.')
.addStringOption((o) =>
o.setName('service')
.setDescription('Select a service to follow')
.setRequired(true)
.setAutocomplete(true)
)
.setIntegrationTypes( .setIntegrationTypes(
ApplicationIntegrationType.UserInstall ApplicationIntegrationType.UserInstall
) )
@@ -15,27 +21,21 @@ const cmd : CommandDefinition = {
InteractionContextType.BotDM, InteractionContextType.BotDM,
InteractionContextType.Guild, InteractionContextType.Guild,
InteractionContextType.PrivateChannel InteractionContextType.PrivateChannel
)
.addStringOption((option) =>
option
.setRequired(true)
.addChoices(...statusService.hosts.filter((v) => v.notify).map((s) => ({name: s.name, value: s.host})))
.setName('host')
.setDescription('Host enable/disable.')
), ),
async execute(interaction : ChatInputCommandInteraction) { async execute(interaction: ChatInputCommandInteraction) {
const userRepo = AppDataSource.getRepository(Follow); const userRepo = AppDataSource.getRepository(Follow);
const hostvalue = interaction.options.getString('host'); const hostvalue = interaction.options.getString('service');
const realHost = statusService.hosts.filter((v) => v.host == hostvalue); const realHost = await statusService.serviceRepo.findOne({where: {name: hostvalue+''}});
if(!hostvalue || realHost.length == 0) {
await interaction.reply({content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral]}); if (!hostvalue || !realHost) {
}else{ await interaction.reply({ content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral] });
let follow = await userRepo.findOne({where: {user_discord: interaction.user.id, host: hostvalue}}); } else {
if(!follow) { let follow = await userRepo.findOne({ where: { user_discord: interaction.user.id, service: { id: realHost.id } } });
if (!follow) {
follow = new Follow(); follow = new Follow();
follow.user_discord = interaction.user.id; follow.user_discord = interaction.user.id;
follow.host = hostvalue; follow.service = realHost;
await userRepo.save(follow); await userRepo.save(follow);
} }
@@ -43,13 +43,24 @@ const cmd : CommandDefinition = {
await userRepo.save(follow); await userRepo.save(follow);
await interaction.reply({content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost[0]?.name}!`, flags: [MessageFlags.Ephemeral]}); await interaction.reply({ content: `✅ Notification successfully ${follow.enable ? 'enabled 🔔' : 'disabled 🔕'} for ${realHost.name}!`, flags: [MessageFlags.Ephemeral] });
if(follow.enable) { if (follow.enable) {
await interaction.user.send({content: `🔔 Notifications have been successfully enabled for ${realHost[0]?.name} ! To disable: /follow host:${realHost[0]?.name}`}) await interaction.user.send({ content: `🔔 Notifications have been successfully enabled for ${realHost.name} ! To disable: /follow host:${realHost.name}` })
} }
} }
} },
autocompletes: [
{
name: 'service',
execute: async (interaction) => {
const services = await statusService.serviceRepo.find({where: {notify: true}});
interaction.respond(services.map((v) => ({name: v.name, value: v.name})));
}
}
]
} }
export default cmd; export default cmd;

View File

@@ -1,4 +1,4 @@
import { ApplicationIntegrationType, ChannelType, ContainerBuilder, MessageFlags, SlashCommandBuilder } from "discord.js"; import { ApplicationIntegrationType, ChannelType, ContainerBuilder, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import { CommandDefinition } from "../../type"; import { CommandDefinition } from "../../type";
import statusService from "../../services/status.service"; import statusService from "../../services/status.service";
import { AppDataSource } from "../../data-source"; import { AppDataSource } from "../../data-source";
@@ -16,7 +16,8 @@ const cmd : CommandDefinition = {
) )
.setIntegrationTypes( .setIntegrationTypes(
ApplicationIntegrationType.GuildInstall ApplicationIntegrationType.GuildInstall
), )
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) { async execute(interaction) {
await interaction.deferReply({flags: [MessageFlags.Ephemeral]}); await interaction.deferReply({flags: [MessageFlags.Ephemeral]});
@@ -25,7 +26,13 @@ const cmd : CommandDefinition = {
const channel = await interaction.guild?.channels.fetch(channel_options?.id); const channel = await interaction.guild?.channels.fetch(channel_options?.id);
if(channel?.isSendable()) { if(channel?.isSendable()) {
const message = await channel.send({components: [statusService.getUpdatedContainer(true)], flags: [MessageFlags.IsComponentsV2]}); let message;
try {
message = await channel.send({components: [await statusService.getUpdatedContainer(true)], flags: [MessageFlags.IsComponentsV2]});
} catch (error) {
await interaction.editReply({content: 'An error has occurred. Please check the permissions for the channel.'});
return;
}
try { try {
const guildRepo = AppDataSource.getRepository(Guild); const guildRepo = AppDataSource.getRepository(Guild);

View File

@@ -17,7 +17,7 @@ const cmd : CommandDefinition = {
), ),
async execute(interaction : ChatInputCommandInteraction) { async execute(interaction : ChatInputCommandInteraction) {
await interaction.deferReply(); await interaction.deferReply();
await interaction.editReply({components: [statusService.getUpdatedContainer()], flags: MessageFlags.IsComponentsV2}); await interaction.editReply({components: [await statusService.getUpdatedContainer()], flags: MessageFlags.IsComponentsV2});
} }
} }

View File

@@ -0,0 +1,54 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, ContainerBuilder, InteractionContextType, MessageFlags, SlashCommandBuilder } from "discord.js";
import { CommandDefinition } from "../../type";
import statusService from "../../services/status.service";
const cmd: CommandDefinition = {
data: new SlashCommandBuilder()
.setName('uptime')
.setDescription('Get more info for a host.')
.addStringOption((o) =>
o.setName('service')
.setDescription('Select a service')
.setRequired(true)
.setAutocomplete(true)
)
.setIntegrationTypes(
ApplicationIntegrationType.UserInstall
)
.setContexts(
InteractionContextType.BotDM,
InteractionContextType.Guild,
InteractionContextType.PrivateChannel
),
async execute(interaction: ChatInputCommandInteraction) {
const hostvalue = interaction.options.getString('service');
const realHost = await statusService.serviceRepo.findOne({where: {name: hostvalue+''}});
if (!hostvalue || !realHost) {
await interaction.reply({ content: '⚠️ Host not found !', flags: [MessageFlags.Ephemeral] });
} else {
const img = await statusService.getStatusImageBar(realHost.id);
const container = new ContainerBuilder()
.setAccentColor(0x0000ed)
.addTextDisplayComponents((t) => t
.setContent(`## ${realHost.alive ? `${process.env.EMOJI_STATUS_ONLINE}` : `${process.env.EMOJI_STATUS_OFFLINE}`} ${realHost.name}\nService status over 7 days :`)
)
.addMediaGalleryComponents((m) =>
m.addItems((i) => i.setURL('attachment://uptime.png'))
);
await interaction.reply({components: [container], flags: [MessageFlags.IsComponentsV2], files: [{attachment: img, name: 'uptime.png'}]})
}
},
autocompletes: [
{
name: 'service',
execute: async (interaction) => {
const services = await statusService.serviceRepo.find();
interaction.respond(services.map((v) => ({name: v.name, value: v.name})));
}
}
]
}
export default cmd;

View File

@@ -1,6 +1,10 @@
import "reflect-metadata" import "reflect-metadata"
import { DataSource } from "typeorm" import { DataSource } from "typeorm"
import { configDotenv } from "dotenv" import { configDotenv } from "dotenv"
import { Follow } from "./entity/follow.entity"
import { Guild } from "./entity/guild.entity"
import { HostsLog } from "./entity/hostslog.entity"
import { Service } from "./entity/service.entity"
configDotenv() configDotenv()
@@ -13,7 +17,7 @@ export const AppDataSource = new DataSource({
database: process.env.DB_DATABASE, database: process.env.DB_DATABASE,
synchronize: process.env.NODE_ENV !== "production", synchronize: process.env.NODE_ENV !== "production",
logging: process.env.DB_LOGGING === "true", logging: process.env.DB_LOGGING === "true",
entities: [__dirname + '/**/*.entity.js'], entities: [Follow, Guild, HostsLog, Service],
migrations: [__dirname + "/**/*.migration.js"], migrations: [__dirname + "/**/*.migration.js"],
subscribers: [__dirname + "/**/*.subscriber.js"], subscribers: [__dirname + "/**/*.subscriber.js"],
}) })

View File

@@ -57,6 +57,8 @@ const rest = new REST().setToken(process.env.TOKEN);
(async () => { (async () => {
try { try {
console.log("Data Source initialized for command deployment!");
console.log(`Started refreshing ${commands.length} application (/) commands.`); console.log(`Started refreshing ${commands.length} application (/) commands.`);
const data = await rest.put( const data = await rest.put(
@@ -65,6 +67,7 @@ const rest = new REST().setToken(process.env.TOKEN);
) as any[]; ) as any[];
console.log(`Successfully reloaded ${data.length} application (/) commands.`); console.log(`Successfully reloaded ${data.length} application (/) commands.`);
process.exit(0);
} catch (error) { } catch (error) {
console.error('[ERROR] Failed to deploy commands:', error); console.error('[ERROR] Failed to deploy commands:', error);
process.exit(1); process.exit(1);

View File

@@ -1,4 +1,5 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Service } from "./service.entity";
@Entity({name: 'follows'}) @Entity({name: 'follows'})
export class Follow { export class Follow {
@@ -8,8 +9,12 @@ export class Follow {
@Column() @Column()
user_discord: string; user_discord: string;
@ManyToOne(() => Service, service => service.follows)
@JoinColumn({name: 'serviceId'})
service: Service;
@Column() @Column()
host: string; serviceId: number;
@Column({default: false}) @Column({default: false})
enable: boolean; enable: boolean;

View File

@@ -1,12 +1,17 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
import { Service } from "./service.entity";
@Entity({name: 'hosts_logs'}) @Entity({name: 'hosts_logs'})
export class HostsLog { export class HostsLog {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id: number;
@ManyToOne(() => Service, service => service.logs)
@JoinColumn({name: 'serviceId'})
service: Service;
@Column() @Column()
host: string; serviceId: number;
@Column() @Column()
status: boolean; status: boolean;

View File

@@ -0,0 +1,34 @@
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { HostsLog } from "./hostslog.entity";
import { Follow } from "./follow.entity";
@Entity({name: 'services'})
export class Service {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
host: string;
@Column()
alive: boolean;
@Column()
ping_type: string;
@Column()
type: string;
@Column()
notify: boolean
@OneToMany(() => HostsLog, log => log.service)
logs: HostsLog[];
@OneToMany(() => Follow, follow => follow.service)
follows: Follow[];
}

View File

@@ -60,6 +60,20 @@ client.on(Events.InteractionCreate, async interaction => {
} }
}); });
return;
}else if(interaction.isAutocomplete()){
const option = interaction.options.getFocused(true);
commands.filter((c) => c.data.name == interaction.commandName).forEach((value) => {
if(value.autocompletes) {
const auto = value.autocompletes.filter((a) => a.name == option.name);
if(auto.length >= 1){
auto.forEach((a) => {
a.execute(interaction)
})
}
}
});
return; return;
} }
@@ -86,6 +100,9 @@ client.on(Events.InteractionCreate, async interaction => {
client.once(Events.ClientReady, readyClient => { client.once(Events.ClientReady, readyClient => {
console.log(`Ready! Logged in as ${readyClient.user.tag}`); console.log(`Ready! Logged in as ${readyClient.user.tag}`);
client.guilds.cache.forEach((value) => {
console.log(`${value.name} conf loaded !`);
});
statusService.setClient(client); statusService.setClient(client);
}); });

View File

@@ -1,118 +1,32 @@
import ping from "ping"; import ping from "ping";
import * as cron from 'cron'; import * as cron from 'cron';
import { ActivityType, Client, ContainerBuilder, MessageFlags } from "discord.js"; import { ActivityType, Client, ContainerBuilder, MessageFlags } from "discord.js";
import { Host, InfraType } from "../type"; import { InfraType } from "../type";
import { AppDataSource } from "../data-source"; import { AppDataSource } from "../data-source";
import { HostsLog } from "../entity/hostslog.entity"; import { HostsLog } from "../entity/hostslog.entity";
import { Repository } from "typeorm"; import { Repository } from "typeorm";
import { Follow } from "../entity/follow.entity"; import { Follow } from "../entity/follow.entity";
import { Guild } from "../entity/guild.entity"; import { Guild } from "../entity/guild.entity";
import dayjs, { Dayjs } from "dayjs";
import { Canvas } from "canvas";
import { Service } from "../entity/service.entity";
type Nofity = {time: Date, name : string, alive : boolean, type : InfraType, host: string}; type Nofity = {time: Date, name : string, alive : boolean, type : string, host: Service};
export class StatusService { export class StatusService {
public hosts: Host[] = [
{
'host': 'https://protojx.com',
'name': 'Protojx Website',
alive: false,
ping_type: 'website',
type: 'website',
notify: false
},
{
'host': 'https://manager.protojx.com',
'name': 'Espace Client',
alive: false,
ping_type: 'website',
type: 'website',
notify: false
},
{
host: '5.178.99.4',
name: 'RYZEN 01',
alive: false,
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '5.178.99.6',
name: 'RYZEN 02',
alive: false,
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '5.178.99.5',
name: 'RYZEN 03',
alive: false,
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '154.16.254.10',
name: 'RYZEN7 04',
alive: false,
ping_type: 'ping',
type: 'ryzen',
notify: true
},
{
host: '5.178.99.177',
name: 'XEON 01',
alive: false,
ping_type: 'ping',
type: 'xeon',
notify: true
},
{
host: '154.16.254.45',
name: 'XEON 02',
alive: false,
ping_type: 'ping',
type: 'xeon',
notify: true
},
{
host: '5.178.99.232',
name: 'XEON 03',
alive: false,
ping_type: 'ping',
type: 'xeon',
notify: true
},
{
host: '5.178.99.53',
name: 'RYZEN-GAME 01',
alive: false,
ping_type: 'ping',
type: 'games',
notify: true
},
{
host: '5.178.99.63',
name: 'XEON-GAME 01',
alive: false,
ping_type: 'ping',
type: 'games',
notify: true
}
];
private client: Client | null = null; private client: Client | null = null;
private hostsLogRepo: Repository<HostsLog>; private hostsLogRepo: Repository<HostsLog>;
private followRepo: Repository<Follow>; private followRepo: Repository<Follow>;
private guildRepo: Repository<Guild>; private guildRepo: Repository<Guild>;
public serviceRepo: Repository<Service>;
constructor() { constructor() {
this.hostsLogRepo = AppDataSource.getRepository(HostsLog); this.hostsLogRepo = AppDataSource.getRepository(HostsLog);
this.followRepo = AppDataSource.getRepository(Follow); this.followRepo = AppDataSource.getRepository(Follow);
this.guildRepo = AppDataSource.getRepository(Guild); this.guildRepo = AppDataSource.getRepository(Guild);
this.serviceRepo = AppDataSource.getRepository(Service);
setTimeout(async () => { setTimeout(async () => {
await this.fetch() await this.fetch()
@@ -145,9 +59,11 @@ export class StatusService {
const channel = await guild.channels.fetch(gdb.persistent_message_channel_id); const channel = await guild.channels.fetch(gdb.persistent_message_channel_id);
if(channel?.isSendable()) { if(channel?.isSendable()) {
const message = await channel.messages.fetch(gdb.persistent_message_id); const message = await channel.messages.fetch(gdb.persistent_message_id);
await message.edit({components: [this.getUpdatedContainer(true)]}); await message.edit({components: [await this.getUpdatedContainer(true)]});
} }
} catch (error) {} } catch (error) {
console.log(error + ' GuildIdInDB : '+gdb.id);
}
} }
}); });
@@ -165,10 +81,77 @@ export class StatusService {
this.client.user?.setActivity({ name: '💭 Server load and status...' }) this.client.user?.setActivity({ name: '💭 Server load and status...' })
} }
public async getStatusImageBar(serviceId: number) {
const datas = await this.hostsLogRepo.createQueryBuilder()
.where('HostsLog.serviceId = :serviceId AND HostsLog.created_at > :date ORDER BY HostsLog.created_at ASC', {serviceId, date: dayjs().subtract(1, 'week').toDate()}).getMany();
const uptimes : { up: boolean, date: Dayjs }[] = datas.map((log) => {
return {
up: log.status,
date: dayjs(log.created_at)
}
});
const now = dayjs();
const week = now.clone().subtract(1, 'week');
const canvas = new Canvas(500, 10, "image");
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#27FF00";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const maxTime = (now.unix() - week.unix());
const ranges: { min: number, max: number }[] = [];
let minTime: number | null = null;
uptimes.map((element, index) => {
const positionForMaxTime = (element.date.unix() - week.unix());
const percent = Math.round((positionForMaxTime / maxTime) * 100);
if (ranges.length == 0 && minTime == null) {
if (element.up && minTime == null) {
ranges.push({
min: 0,
max: percent
});
} else {
minTime = percent;
}
} else {
if (!element.up) {
minTime = percent;
if(minTime != null && index == uptimes.length - 1) {
ranges.push({
min: minTime,
max: 100
});
}
} else {
if (minTime) {
ranges.push({
min: minTime,
max: percent
});
}
}
}
});
ctx.fillStyle = '#ff0000';
ranges.map((value) => {
ctx.fillRect(value.min * 5, 0, value.max * 5 - value.min * 5, canvas.height);
});
return canvas.toBuffer('image/png');
}
private async updateClientStatus() { private async updateClientStatus() {
if (this.client) { if (this.client) {
const hosts = this.hosts.length; const hosts_db = await this.serviceRepo.find();
const hostsAlive = this.hosts.filter((h) => h.alive).length; const hosts = hosts_db.length;
const hostsAlive = hosts_db.filter((h) => h.alive).length;
this.client.user?.setActivity({ this.client.user?.setActivity({
name: ( name: (
@@ -178,56 +161,59 @@ export class StatusService {
} }
} }
private async fetchAlive(host: Host, notifs : Nofity[]) { private async fetchAlive(service: Service, notifs : Nofity[]) {
const latestLog = await this.hostsLogRepo.findOne({ where: { host: host.host }, order: { created_at: 'DESC' } }); const latestLog = await this.hostsLogRepo.findOne({ where: { service }, order: { created_at: 'DESC' } });
// ? Ping and Request Hosts // ? Ping and Request Hosts
if (host.ping_type === 'ping') { if (service.ping_type === 'ping') {
let res = await ping.promise.probe(host.host, { timeout: 10 }); let res = await ping.promise.probe(service.host, { timeout: 10 });
host.alive = res.alive; service.alive = res.alive;
} else if (host.ping_type === 'website') { } else if (service.ping_type === 'website') {
try { try {
const response = await fetch(host.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) }); const response = await fetch(service.host, { method: 'HEAD', signal: AbortSignal.timeout(10000) });
host.alive = response.ok; service.alive = response.ok;
} catch (error) { } catch (error) {
host.alive = false; service.alive = false;
} }
} }
// ? Notification System : // ? Notification System :
if (!latestLog || latestLog.status != host.alive) { if (!latestLog || latestLog.status != service.alive) {
const log = new HostsLog(); const log = new HostsLog();
log.host = host.host; log.service = service;
log.status = host.alive; log.status = service.alive;
this.hostsLogRepo.save(log); this.hostsLogRepo.save(log);
if(latestLog && host.notify) { if(latestLog && service.notify) {
notifs.push({alive: host.alive, name: host.name, time: new Date(), type: host.type, host: host.host}); notifs.push({alive: service.alive, name: service.name, time: new Date(), type: service.type, host: service});
} }
} }
return host; this.serviceRepo.save(service);
return service;
} }
private async fetch(max = 1, notifs : Nofity[] = []) { private async fetch(max = 1, notifs : Nofity[] = []) {
const max_ping = 3; const max_ping = 3;
const hosts = this.hosts.filter((value, index) => index < max * max_ping && index >= (max - 1) * max_ping); const services = await this.serviceRepo.find();
const hosts = services.filter((value, index) => index < max * max_ping && index >= (max - 1) * max_ping);
const fetchPromises = hosts.map(host => this.fetchAlive(host, notifs)); const fetchPromises = hosts.map(host => this.fetchAlive(host, notifs));
const updatedHosts = await Promise.all(fetchPromises); const updatedHosts = await Promise.all(fetchPromises);
updatedHosts.forEach((updatedHost, index) => { updatedHosts.forEach((updatedHost, index) => {
const originalIndex = (max - 1) * max_ping + index; const originalIndex = (max - 1) * max_ping + index;
if (originalIndex < this.hosts.length) { if (originalIndex < services.length) {
this.hosts[originalIndex] = updatedHost; services[originalIndex] = updatedHost;
} }
}); });
if (this.hosts.length > max * max_ping) { if (services.length > max * max_ping) {
await this.fetch(max + 1, notifs); await this.fetch(max + 1, notifs);
}else if(notifs.length > 0){ }else if(notifs.length > 0){
// ? Notification System (part 2 !): // ? Notification System (part 2 !):
@@ -242,7 +228,8 @@ export class StatusService {
const users = await this.followRepo.find({where: {enable: true}}); const users = await this.followRepo.find({where: {enable: true}});
const hosts = notifs.map((n) => n.host); const hosts = notifs.map((n) => n.host);
const users_ids : string[] = []; const users_ids : string[] = [];
users.filter(v => hosts.includes(v.host)).forEach(async (user) => { console.log("Sending notifs...")
users.filter(v => hosts.map((h) => h.id).includes(v.serviceId)).forEach(async (user) => {
if(!users_ids.includes(user.user_discord)) { if(!users_ids.includes(user.user_discord)) {
users_ids.push(user.user_discord) users_ids.push(user.user_discord)
try { try {
@@ -256,8 +243,10 @@ export class StatusService {
} }
} }
public getUpdatedContainer(live : boolean = false): ContainerBuilder { public async getUpdatedContainer(live : boolean = false): Promise<ContainerBuilder> {
const hostTexts = this.hosts.map((s) => { const services = await this.serviceRepo.find({order: {id: 'ASC'}});
const hostTexts = services.map((s) => {
return { type: s.type, value: `- ${s.name} : ${s.alive ? `${process.env.EMOJI_STATUS_ONLINE} Online` : `${process.env.EMOJI_STATUS_OFFLINE} Offline`}` }; return { type: s.type, value: `- ${s.name} : ${s.alive ? `${process.env.EMOJI_STATUS_ONLINE} Online` : `${process.env.EMOJI_STATUS_OFFLINE} Offline`}` };
}); });
@@ -285,6 +274,11 @@ export class StatusService {
title: 'Games', title: 'Games',
type: 'games', type: 'games',
thumbnail: 'https://protojx.com/assets/img/hero-img.png' thumbnail: 'https://protojx.com/assets/img/hero-img.png'
},
{
title: 'Routers\n-# *The data displayed here is not real data but demonstration data. (Beta)*',
type: 'router',
thumbnail: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRMnCmtQRkLlcD1Cb6vKXz6NOxAu79vzmq2pRqpNYxpTJa5JQEsouhqnVn7cyl6ivYSyzY&usqp=CAU'
} }
] ]
@@ -296,15 +290,15 @@ export class StatusService {
(text) => (text) =>
text.setContent('## ' + sectionData.title + '\n' + hostTexts.filter((v) => v.type == sectionData.type).map((v) => v.value).join('\n')) text.setContent('## ' + sectionData.title + '\n' + hostTexts.filter((v) => v.type == sectionData.type).map((v) => v.value).join('\n'))
) )
.setThumbnailAccessory( .setThumbnailAccessory(
(acc) => (acc) =>
acc.setURL(sectionData.thumbnail) acc.setURL(sectionData.thumbnail)
) )
) )
}); });
const now = new Date(); container.addSeparatorComponents((s) => s);
container.addTextDisplayComponents((text) => text.setContent(`${live ? 'Last update : ' : ''}${now.getDate()}-${now.getMonth() + 1}-${now.getFullYear()} ${(now.getHours() + '').padStart(2, "0")}:${(now.getMinutes() + '').padStart(2, "0")} - Receive automatic notifications when there is an outage with /follow !`)); container.addTextDisplayComponents((text) => text.setContent(`:globe_with_meridians: Website Status : https://statut.protojx.com/\n${live ? 'Last update : ' : ''}<t:${dayjs().unix()}:f> - Receive automatic notifications when there is an outage with /follow !`));
return container; return container;
} }

14
src/type.d.ts vendored
View File

@@ -1,12 +1,4 @@
import { ButtonInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js"; import { AutocompleteInteraction, ButtonInteraction, ChatInputCommandInteraction, SlashCommandBuilder } from "discord.js";
export type InfraType = 'website' | 'ryzen' | 'xeon' | 'games'; export type InfraType = 'website' | 'ryzen' | 'xeon' | 'games' | 'router';
export type Host = { export type CommandDefinition = { data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[], autocompletes?: {name: string, execute: (interaction: AutocompleteInteraction) => void}[]};
host: string,
name: string,
alive: boolean,
ping_type: 'ping' | 'website',
type: InfraType,
notify: boolean;
};
export type CommandDefinition = { data: SlashCommandBuilder | SlashCommandOptionsOnlyBuilder, execute: (interaction: ChatInputCommandInteraction) => void, buttons?: { id: string, handle: (interaction: ButtonInteraction) => void}[]};

View File

@@ -2,9 +2,9 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./src",
"target": "ES2020", "target": "ES2024",
"module": "CommonJS", "module": "CommonJS",
"lib": ["ES2020"], "lib": ["ES2024"],
"moduleResolution": "node", "moduleResolution": "node",
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,