15 Commits

Author SHA1 Message Date
f767a9483b v4.1.6 2025-11-05 17:54:32 -05:00
b9a95f1c61 bump dependencies 2025-11-05 17:53:37 -05:00
bea25616a0 4.1.5 2025-10-09 23:30:54 -05:00
ace2c3c180 update dependencies 2025-08-18 14:25:38 -05:00
3019f9132f omit dev 2025-06-17 15:51:28 -05:00
a04ffade6f install python 2025-06-17 15:46:54 -05:00
a243892fd4 bump dependencies 2025-06-17 15:38:46 -05:00
1db899b5c8 4.1.2: dependencies 2025-05-04 12:55:07 -05:00
fc71bc08db Merge pull request #2 from CorySanin/dependabot/npm_and_yarn/tar-fs-2.1.2
Bump tar-fs from 2.1.1 to 2.1.2
2025-05-04 12:46:38 -05:00
dependabot[bot]
c560100a8b Bump tar-fs from 2.1.1 to 2.1.2
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.1 to 2.1.2.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.1...v2.1.2)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-28 23:57:01 +00:00
3c34a98e67 4.1.1: use build script package 2025-03-18 21:28:13 -05:00
cb28872652 Merge pull request #1 from CorySanin/ts-modules
TS + modules
2025-01-28 17:54:53 -05:00
a8c057245a github action 2025-01-27 23:22:17 -05:00
d328414186 ts + modules 2025-01-27 20:11:53 -05:00
5b44841cbf 4.0.8: fun with curl 2025-01-07 01:28:35 -05:00
20 changed files with 2029 additions and 1302 deletions

View File

@@ -1,3 +1,4 @@
volume/ volume/
config/ config/
distribution/
node_modules node_modules

90
.github/workflows/build-app-image.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: App Image CI
on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
branches:
- master
jobs:
build_app_image:
name: Build app image
runs-on: ubuntu-latest
env:
GH_REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
REPOSITORY: ${{ github.event.repository.name }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
install: true
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.GH_REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for release Docker image
if: startsWith(github.ref, 'refs/tags/v')
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.GH_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Extract metadata for develop Docker image
if: "!startsWith(github.ref, 'refs/tags/v')"
id: meta-develop
uses: docker/metadata-action@v5
with:
images: |
${{ env.GH_REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
- name: Build and push release Docker image
if: startsWith(github.ref, 'refs/tags/v')
uses: docker/build-push-action@v6
with:
target: deploy
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64
cache-from: type=gha,scope=${{ github.workflow }}
cache-to: type=gha,mode=max,scope=${{ github.workflow }}
- name: Build and push develop Docker image
if: "!startsWith(github.ref, 'refs/tags/v')"
uses: docker/build-push-action@v6
with:
target: deploy
push: true
tags: ${{ steps.meta-develop.outputs.tags }}
labels: ${{ steps.meta-develop.outputs.labels }}
platforms: linux/amd64
cache-from: type=gha,scope=${{ github.workflow }}
cache-to: type=gha,mode=max,scope=${{ github.workflow }}

3
.gitignore vendored
View File

@@ -108,4 +108,5 @@ docker-cache/
assets/css/ assets/css/
assets/js/ assets/js/
assets/webp/ assets/webp/
config/ config/
distribution/

View File

@@ -1,23 +0,0 @@
image: docker:latest
services:
- docker:dind
variables:
DOCKER_DRIVER: overlay
stages:
- build
- deploy
artix-packy-pusher:
stage: build
script:
- docker build --pull --no-cache -t "$CI_REGISTRY_IMAGE" .
deploy:
stage: deploy
only:
- master
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push "$CI_REGISTRY_IMAGE"

View File

@@ -6,7 +6,7 @@ FROM baseimg AS build-env
WORKDIR /usr/notifier WORKDIR /usr/notifier
RUN pacman -Sy --noconfirm nodejs npm RUN pacman -Sy --noconfirm nodejs npm typescript python
COPY package*.json ./ COPY package*.json ./
@@ -14,8 +14,9 @@ RUN npm install
COPY . . COPY . .
RUN npm run-script build && \ RUN tsc && \
npm ci --only=production npm run-script build && \
npm ci --omit=dev
FROM baseimg AS deploy FROM baseimg AS deploy
@@ -46,6 +47,5 @@ ENV ARTIX_MIRROR="https://mirror.sanin.dev/artix-linux/%s/os/x86_64/"
ENV ARCH_MIRROR="https://mirror.sanin.dev/arch-linux/%s/os/x86_64/" ENV ARCH_MIRROR="https://mirror.sanin.dev/arch-linux/%s/os/x86_64/"
ENV ARTIX_REPOS="system-goblins,world-goblins,galaxy-goblins,lib32-goblins,system-gremlins,world-gremlins,galaxy-gremlins,lib32-gremlins,system,world,galaxy,lib32" ENV ARTIX_REPOS="system-goblins,world-goblins,galaxy-goblins,lib32-goblins,system-gremlins,world-gremlins,galaxy-gremlins,lib32-gremlins,system,world,galaxy,lib32"
ENV ARCH_REPOS="core-staging,extra-staging,multilib-staging,core-testing,extra-testing,multilib-testing,core,extra,multilib" ENV ARCH_REPOS="core-staging,extra-staging,multilib-staging,core-testing,extra-testing,multilib-testing,core,extra,multilib"
ENV GITEA_TOKEN="CHANGEME"
CMD [ "node", "index.js"] CMD [ "node", "distribution/index.mjs"]

View File

@@ -1,4 +1,4 @@
# artix-packy-notifier # artix-checkupdates-web
Notification system and web frontend for Artix packages with pending operations. Notifications can be sent via Notification system and web frontend for Artix packages with pending operations. Notifications can be sent via
[Apprise](https://github.com/caronc/apprise/wiki#notification-services) or IRC. Web interface shows all packages with pending operations [Apprise](https://github.com/caronc/apprise/wiki#notification-services) or IRC. Web interface shows all packages with pending operations
@@ -33,12 +33,13 @@ If the channel is intended only for the bot to broadcast, consider setting the c
``` ```
npm install npm install
node index.js npm exec tsc
node distribution/index.mjs
``` ```
## Docker Setup ## Docker Setup
Image : `registry.gitlab.com/sanin.dev/artix-packy-notifier` Image : `ghcr.io/corysanin/artix-checkupdates-web:latest`
mount a folder to `/usr/notifier/config`. mount a folder to `/usr/notifier/config`.

View File

@@ -1,31 +1,29 @@
/** import fs from 'fs';
* Writing this myself because gulp has a billion garbage dependencies import path from 'path';
* and webpack sucks poop through a straw. Everyone is stupid. import child_process from 'child_process';
*/ import uglifyjs from "uglify-js";
const fsp = require('fs').promises; import * as sass from 'sass';
const path = require('path'); import * as csso from 'csso';
const spawn = require('child_process').spawn;
const sass = require('sass');
const csso = require('csso');
const uglifyjs = require("uglify-js");
const spawn = child_process.spawn;
const fsp = fs.promises;
const STYLESDIR = 'styles'; const STYLESDIR = 'styles';
const SCRIPTSDIR = 'scripts'; const SCRIPTSDIR = 'scripts';
const IMAGESDIR = path.join('assets', 'images'); const IMAGESDIR = path.join('assets', 'images');
const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join(__dirname, 'assets', 'css'); const STYLEOUTDIR = process.env.STYLEOUTDIR || path.join('assets', 'css');
const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join(__dirname, 'assets', 'js'); const SCRIPTSOUTDIR = process.env.SCRIPTSOUTDIR || path.join('assets', 'js');
const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join(__dirname, 'assets', 'webp'); const IMAGESOUTDIR = process.env.IMAGESOUTDIR || path.join('assets', 'webp');
const STYLEOUTFILE = process.env.STYLEOUTFILE || 'styles.css'; const STYLEOUTFILE = process.env.STYLEOUTFILE || 'styles.css';
const SQUASH = new RegExp('^[0-9]+-'); const SQUASH = new RegExp('^[0-9]+-');
async function emptyDir(dir) { async function emptyDir(dir: string) {
await Promise.all((await fsp.readdir(dir, { withFileTypes: true })).map(f => path.join(dir, f.name)).map(p => fsp.rm(p, { await Promise.all((await fsp.readdir(dir, { withFileTypes: true })).map(f => path.join(dir, f.name)).map(p => fsp.rm(p, {
recursive: true, recursive: true,
force: true force: true
}))); })));
} }
async function mkdir(dir) { async function mkdir(dir: string | string[]) {
if (typeof dir === 'string') { if (typeof dir === 'string') {
await fsp.mkdir(dir, { recursive: true }); await fsp.mkdir(dir, { recursive: true });
} }
@@ -37,8 +35,8 @@ async function mkdir(dir) {
// Process styles // Process styles
async function styles() { async function styles() {
await mkdir([STYLEOUTDIR, STYLESDIR]); await mkdir([STYLEOUTDIR, STYLESDIR]);
await await emptyDir(STYLEOUTDIR); await emptyDir(STYLEOUTDIR);
let styles = []; let styles: string[] = [];
let files = await fsp.readdir(STYLESDIR); let files = await fsp.readdir(STYLESDIR);
await Promise.all(files.map(f => new Promise(async (res, reject) => { await Promise.all(files.map(f => new Promise(async (res, reject) => {
let p = path.join(STYLESDIR, f); let p = path.join(STYLESDIR, f);
@@ -110,7 +108,7 @@ async function images(dir = '') {
clearTimeout(timeout); clearTimeout(timeout);
if (code === 0) { if (code === 0) {
console.log(`Wrote ${outfile}`); console.log(`Wrote ${outfile}`);
res(); res(null);
} }
else { else {
reject(code); reject(code);
@@ -124,6 +122,10 @@ async function images(dir = '') {
} }
} }
function isAbortError(err: unknown): boolean {
return typeof err === 'object' && err !== null && 'name' in err && err.name === 'AbortError';
}
(async function () { (async function () {
await Promise.all([styles(), scripts(), images()]); await Promise.all([styles(), scripts(), images()]);
if (process.argv.indexOf('--watch') >= 0) { if (process.argv.indexOf('--watch') >= 0) {
@@ -134,7 +136,7 @@ async function images(dir = '') {
for await (const _ of watcher) for await (const _ of watcher)
await styles(); await styles();
} catch (err) { } catch (err) {
if (err.name === 'AbortError') if (isAbortError(err))
return; return;
throw err; throw err;
} }
@@ -146,7 +148,7 @@ async function images(dir = '') {
for await (const _ of watcher) for await (const _ of watcher)
await scripts(); await scripts();
} catch (err) { } catch (err) {
if (err.name === 'AbortError') if (isAbortError(err))
return; return;
throw err; throw err;
} }
@@ -160,7 +162,7 @@ async function images(dir = '') {
for await (const _ of watcher) for await (const _ of watcher)
await images(); await images();
} catch (err) { } catch (err) {
if (err.name === 'AbortError') if (isAbortError(err))
return; return;
throw err; throw err;
} }

View File

@@ -1,33 +0,0 @@
const path = require('path');
const fs = require('fs');
const json5 = require('json5');
fs.readFile(process.env.CONFIG || path.join(__dirname, 'config', 'config.json'), (err, data) => {
if (err) {
console.error(err);
}
else {
let config = json5.parse(data);
let arg = process.env.COMPONENT || (process.execArgv && process.execArgv[0]);
if (arg === 'daemon') {
const Daemon = require('./daemon');
const daemon = new Daemon(config);
process.on('SIGTERM', daemon.close);
}
else if (arg === 'ircbot') {
const IRCBot = require('./ircbot');
const bot = new IRCBot(config);
bot.connect();
process.on('SIGTERM', bot.close);
}
else if (arg === 'web') {
const Web = require('./web');
const web = new Web(config);
process.on('SIGTERM', web.close);
}
else {
console.error('Please pass the component you wish to run.');
}
}
});

2536
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,55 @@
{ {
"name": "artix-packy-notifier", "name": "artix-checkupdates-web",
"version": "4.0.7", "version": "4.1.6",
"description": "Determine packages that need attention", "description": "Determine packages that need attention",
"main": "index.js", "main": "./distribution/index.js",
"type": "module",
"scripts": { "scripts": {
"build": "node build.js", "build": "npx build-shit",
"watch": "node build.js --watch" "watch": "npx build-shit --watch"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+ssh://git@gitlab.com/sanin.dev/artix-packy-notifier.git" "url": "git+ssh://git@github.com:CorySanin/artix-checkupdates-web.git"
}, },
"keywords": [ "keywords": [
"artix", "artix",
"linux", "linux",
"packages" "packages"
], ],
"files": [
"distribution",
"assets",
"views",
"userbar"
],
"author": "Cory Sanin", "author": "Cory Sanin",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://gitlab.com/sanin.dev/artix-packy-notifier/issues" "url": "https://github.com/CorySanin/artix-checkupdates-web/issues"
}, },
"homepage": "https://gitlab.com/sanin.dev/artix-packy-notifier#readme", "homepage": "https://github.com/CorySanin/artix-checkupdates-web#readme",
"dependencies": { "dependencies": {
"better-sqlite3": "11.7.2", "artix-checkupdates": "1.1.0",
"dayjs": "1.11.13", "better-sqlite3": "12.4.1",
"dayjs": "1.11.19",
"ejs": "3.1.10", "ejs": "3.1.10",
"express": "4.21.2", "express": "5.1.0",
"express-useragent": "2.0.1",
"irc-framework": "4.14.0", "irc-framework": "4.14.0",
"json5": "2.2.3", "json5": "2.2.3",
"node-cron": "3.0.3", "ky": "1.14.0",
"phin": "3.7.1", "node-cron": "4.2.1",
"prom-client": "15.1.3", "prom-client": "15.1.3",
"sharp": "0.33.5" "sharp": "0.34.4"
}, },
"devDependencies": { "devDependencies": {
"csso": "5.0.5", "@sindresorhus/tsconfig": "8.1.0",
"sass": "1.83.1", "@types/better-sqlite3": "7.6.13",
"uglify-js": "3.19.3" "@types/express": "5.0.5",
"@types/express-useragent": "1.0.5",
"@types/node": "^24.10.0",
"forking-build-shit":"1.0.5",
"typescript": "5.9.3"
} }
} }

35
src/config.d.ts vendored Normal file
View File

@@ -0,0 +1,35 @@
import { } from "node:os";
export type IRCFrameworkConfig = {
host: string;
port: number;
nick: string;
}
export type AuxiliaryIRCConfig = {
channel?: string;
channel_key: string;
}
export type Maintainer = {
name: string;
channels?: string[];
ircName?: string;
}
export type MaintainerArrayElement = (string | Maintainer);
export type Config = {
webhostname: string;
apprise: string;
maintainers: MaintainerArrayElement[];
cron?: string;
syncfreq: number;
db?: string;
port?: number;
privateport?: number;
savePath?: string;
irchostname?: string;
'irc-framework'?: IRCFrameworkConfig;
ircClient?: AuxiliaryIRCConfig;
}

View File

@@ -1,42 +1,60 @@
const path = require('path'); import { Checkupdates } from 'artix-checkupdates';
const fs = require('fs'); import { DB } from './db.mjs';
const os = require('os'); import { spawn } from 'child_process';
const spawn = require('child_process').spawn; import ky from 'ky';
const cron = require('node-cron'); import * as path from 'path';
const dayjs = require('dayjs'); import * as fsp from 'node:fs/promises';
const express = require('express'); import * as cron from 'node-cron';
const phin = require('phin'); import dayjs from 'dayjs';
const DB = require('./db'); import express from 'express';
const fsp = fs.promises; import type http from "http";
import type { Express } from "express";
import type { Config, MaintainerArrayElement } from './config.js';
import type { Category, PackageDBEntry } from './db.mjs';
const TIMEOUT = 180000; const TIMEOUT = 180000;
const ORPHAN = { const ORPHAN = {
"name": "orphan", "name": "orphan",
"ircName": "orphaned" "ircName": "orphaned"
}; };
const EXTRASPACE = new RegExp('\\s+', 'g');
const CHECKUPDATESCACHE = path.join(os.homedir(), '.cache', 'artix-checkupdates');
const NICETYPES = { const NICETYPES = {
move: 'move', move: 'move',
udate: 'update' udate: 'update'
};
type SaveData = {
'last-sync': string | null;
move: string[];
update: string[];
}
type AppriseConf = {
api: string;
urls: string[];
}
function notStupidParseInt(v: string | undefined): number {
return v === undefined ? NaN : parseInt(v);
} }
const NAMECOMPLIANCE = [
p => p.replace(/([a-zA-Z0-9]+)\+([a-zA-Z]+)/g, '$1-$2'),
p => p.replace(/\+/g, "plus"),
p => p.replace(/[^a-zA-Z0-9_\-\.]/g, "-"),
p => p.replace(/[_\-]{2,}/g, "-")
]
class Daemon { class Daemon {
private _config: Config;
private _savePath: string;
private _locked: boolean;
private _saveData: SaveData;
private _db: DB;
private _cronjob: cron.ScheduledTask;
private _webserver: http.Server;
constructor(config) { constructor(config: Config) {
const app = express(); const PROJECT_ROOT = path.resolve(import.meta.dirname, '..');
const port = process.env.PRIVATEPORT || config.privateport || 8081; const app: Express = express();
const port = process.env['PRIVATEPORT'] || config.privateport || 8081;
this._config = config; this._config = config;
this._savePath = process.env.SAVEPATH || config.savePath || path.join(__dirname, 'config', 'data.json'); this._savePath = process.env['SAVEPATH'] || config.savePath || path.join(PROJECT_ROOT, 'config', 'data.json');
console.log('Written by Cory Sanin for Artix Linux'); console.log('Written by Cory Sanin for Artix Linux');
this._locked = false; this._locked = false;
const db = this._db = new DB(process.env.DBPATH || config.db || path.join(__dirname, 'config', 'packages.db')); const db = this._db = new DB(process.env['DBPATH'] || config.db || path.join(PROJECT_ROOT, 'config', 'packages.db'));
this._saveData = { this._saveData = {
'last-sync': null, 'last-sync': null,
move: [], move: [],
@@ -45,7 +63,7 @@ class Daemon {
app.set('trust proxy', 1); app.set('trust proxy', 1);
app.get('/healthcheck', (req, res) => { app.get('/healthcheck', (_, res) => {
res.send('Healthy'); res.send('Healthy');
}); });
@@ -54,22 +72,21 @@ class Daemon {
// resetting flags in case of improper shutdown // resetting flags in case of improper shutdown
db.restoreFlags(); db.restoreFlags();
this._cronjob = cron.schedule(process.env.CRON || config.cron || '*/30 * * * *', () => { this._cronjob = cron.schedule(process.env['CRON'] || config.cron || '*/30 * * * *', () => {
this.main(this._config); this.main(this._config);
}); });
this._webserver = app.listen(port); this._webserver = app.listen(port);
} }
main = async (config) => { main = async (config: Config) => {
const db = this._db;
if (this._locked) { if (this._locked) {
return return
} }
this._locked = true; this._locked = true;
console.log('Starting scheduled task'); console.log('Starting scheduled task');
let now = dayjs(); let now = dayjs();
if (!('last-sync' in this._saveData) || !this._saveData['last-sync'] || dayjs(this._saveData['last-sync']).isBefore(now.subtract(process.env.SYNCFREQ || config.syncfreq || 2, 'days'))) { if (!('last-sync' in this._saveData) || !this._saveData['last-sync'] || dayjs(this._saveData['last-sync']).isBefore(now.subtract(notStupidParseInt(process.env['SYNCFREQ']) || config.syncfreq || 2, 'days'))) {
await this.updateMaintainers(config); await this.updateMaintainers(config);
this._saveData['last-sync'] = now.toJSON(); this._saveData['last-sync'] = now.toJSON();
await this.writeSaveData(); await this.writeSaveData();
@@ -80,30 +97,21 @@ class Daemon {
console.log('Task complete.'); console.log('Task complete.');
} }
cleanUpLockfiles = async () => { updateMaintainers = async (config: Config) => {
try {
await fsp.rm(CHECKUPDATESCACHE, { recursive: true, force: true });
}
catch (ex) {
console.error('Failed to remove the artix-checkupdates cache directory:', ex);
}
}
updateMaintainers = async (config) => {
const db = this._db; const db = this._db;
console.log('Syncing packages...'); console.log('Syncing packages...');
const lastseen = (new Date()).getTime(); const lastseen = (new Date()).getTime();
const maintainers = [...(config.maintainers || []), ORPHAN]; const maintainers = [...(config.maintainers || []), ORPHAN];
for (let i = 0; i < maintainers.length; i++) { for (let i = 0; i < maintainers.length; i++) {
let maintainer = maintainers[i]; let maintainer = maintainers[i] as MaintainerArrayElement;
if (typeof maintainer === 'object') { if (typeof maintainer === 'object') {
maintainer = maintainer.name; maintainer = maintainer.name;
} }
console.log(`Syncing ${maintainer}...`); console.log(`Syncing ${maintainer}...`);
try { try {
const packages = await this.getMaintainersPackages(maintainer); const packages: string[] = await this.getMaintainersPackages(maintainer);
for (let j = 0; j < packages.length; j++) { for (let j = 0; j < packages.length; j++) {
db.updatePackage(packages[j], maintainer, lastseen); db.updatePackage(packages[j] as string, maintainer, lastseen);
} }
} }
catch (err) { catch (err) {
@@ -116,56 +124,25 @@ class Daemon {
console.log(`Package sync complete`); console.log(`Package sync complete`);
} }
checkupdates = async (config) => { checkupdates = async (config: Config) => {
const db = this._db; const check = new Checkupdates();
try { try {
await this.handleUpdates(config, this._saveData.move = await this.execCheckUpdates(['-m']), 'move'); await this.handleUpdates(config, this._saveData.move = (await check.fetchMovable()).map(p => p.basename), 'move');
await this.handleUpdates(config, this._saveData.update = await this.execCheckUpdates(['-u']), 'udate'); await this.handleUpdates(config, this._saveData.update = (await check.fetchUpgradable()).map(p => p.basename), 'udate');
} }
catch (ex) { catch (ex) {
console.error('Failed to check for updates:', ex); console.error('Failed to check for updates:', ex);
} }
} }
execCheckUpdates = (flags) => { getMaintainersPackages = (maintainer: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
let process = spawn('artix-checkupdates', flags);
let timeout = setTimeout(async () => {
process.kill() && await this.cleanUpLockfiles();
reject('Timed out');
}, TIMEOUT);
let outputstr = '';
let errorOutput = '';
process.stdout.on('data', data => {
outputstr += data.toString();
});
process.stderr.on('data', err => {
const errstr = err.toString();
errorOutput += `${errstr}, `;
console.error(errstr);
});
process.on('exit', async (code) => {
if (code === 0 && errorOutput.length === 0) {
clearTimeout(timeout);
resolve(this.parseCheckUpdatesOutput(outputstr));
}
else {
errorOutput.includes('unable to lock database') && this.cleanUpLockfiles();
reject((code && `exited with ${code}`) || errorOutput);
}
});
});
}
getMaintainersPackages = (maintainer) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let process = spawn('artixpkg', ['admin', 'query', maintainer === ORPHAN.name ? '-t' : '-m', maintainer]); let process = spawn('artixpkg', ['admin', 'query', maintainer === ORPHAN.name ? '-t' : '-m', maintainer]);
let timeout = setTimeout(() => { let timeout = setTimeout(() => {
reject('Timed out'); reject('Timed out');
process.kill(); process.kill();
}, TIMEOUT); }, TIMEOUT);
let packagelist = []; let packagelist: string[] = [];
process.stdout.on('data', data => { process.stdout.on('data', data => {
packagelist = packagelist.concat(data.toString().trim().split('\n')); packagelist = packagelist.concat(data.toString().trim().split('\n'));
}); });
@@ -184,29 +161,16 @@ class Daemon {
}); });
} }
parseCheckUpdatesOutput = (output) => { handleUpdates = async (config: Config, packs: string[], type: Category) => {
let packages = [];
const lines = output.split('\n');
lines.forEach(l => {
// "package" is "reserved"
const reservethis = l.trim().replace(EXTRASPACE, ' ');
if (reservethis.length > 0 && reservethis.indexOf('Package basename') < 0) {
packages.push(NAMECOMPLIANCE.reduce((s, fn) => fn(s), reservethis.split(' ', 2)[0]));
}
});
return packages;
}
handleUpdates = async (config, packs, type) => {
const db = this._db; const db = this._db;
packs.forEach(v => { packs.forEach(v => {
let p = db.getPackage(v); let p = db.getPackage(v);
p && db.updateFlag(v, type, p[type] > 0 ? 2 : 4); p && db.updateFlag(v, type, p[type] > 0 ? 2 : 4);
}); });
const maintainers = [...config.maintainers, ORPHAN]; const maintainers: MaintainerArrayElement[] = [...config.maintainers, ORPHAN];
for (let i = 0; i < maintainers.length; i++) { for (let i = 0; i < maintainers.length; i++) {
const m = maintainers[i]; const m = maintainers[i] as MaintainerArrayElement;
const mname = typeof m === 'object' ? m.name : m; const mname: string = typeof m === 'object' ? m.name : m;
const ircName = typeof m === 'object' ? (m.ircName || mname) : m; const ircName = typeof m === 'object' ? (m.ircName || mname) : m;
const packages = db.getNewByMaintainer(mname, type); const packages = db.getNewByMaintainer(mname, type);
if (typeof m === 'object' && m.channels) { if (typeof m === 'object' && m.channels) {
@@ -222,18 +186,16 @@ class Daemon {
db.restoreFlags(type); db.restoreFlags(type);
} }
notify = async (apprise, packarr, type) => { notify = async (apprise: AppriseConf, packarr: PackageDBEntry[], type: string) => {
if (!(packarr && packarr.length && apprise && apprise.api && apprise.urls)) { if (!(packarr && packarr.length && apprise && apprise.api && apprise.urls)) {
return; return;
} }
const packagesStr = packarr.map(p => p.package).join('\n'); const packagesStr = packarr.map(p => p.package).join('\n');
for (let i = 0; i < 25; i++) { for (let i = 0; i < 25; i++) {
try { try {
return await phin({ return await ky.post(`${apprise.api}/notify/`, {
url: `${apprise.api}/notify/`, json: {
method: 'POST', title: `${packarr[0]?.maintainer}: packages ready to ${type}`,
data: {
title: `${packarr[0].maintainer}: packages ready to ${type}`,
body: packagesStr, body: packagesStr,
urls: apprise.urls.join(',') urls: apprise.urls.join(',')
} }
@@ -247,19 +209,17 @@ class Daemon {
return null; return null;
} }
ircNotify = async (packarr, maintainer, type) => { ircNotify = async (packarr: PackageDBEntry[], maintainer: string, type: string) => {
const config = this._config; const config = this._config;
if (!(packarr && packarr.length && config['irc-framework'])) { if (!(packarr && packarr.length && config['irc-framework'])) {
return; return;
} }
const hostname = process.env.IRCHOSTNAME || config.irchostname || 'http://artix-notifier-irc:8081'; const hostname = process.env['IRCHOSTNAME'] || config.irchostname || 'http://artix-notifier-irc:8081';
const packagesStr = packarr.map(p => p.package).join('\n'); const packagesStr = packarr.map(p => p.package).join('\n');
for (let i = 0; i < 25; i++) { for (let i = 0; i < 25; i++) {
try { try {
return await phin({ return await ky.post(`${hostname}/api/1.0/notifications`, {
url: `${hostname}/api/1.0/notifications`, json: {
method: 'POST',
data: {
message: `${maintainer}: packages ready to ${type}\n${packagesStr}\n-------- EOF --------` message: `${maintainer}: packages ready to ${type}\n${packagesStr}\n-------- EOF --------`
} }
}); });
@@ -274,7 +234,7 @@ class Daemon {
readSaveData = async () => { readSaveData = async () => {
try { try {
this._saveData = JSON.parse(await fsp.readFile(this._savePath)); this._saveData = JSON.parse((await fsp.readFile(this._savePath)).toString());
} }
catch { catch {
console.error(`Failed to read existing save data at ${this._savePath}`); console.error(`Failed to read existing save data at ${this._savePath}`);
@@ -283,13 +243,10 @@ class Daemon {
writeSaveData = async () => { writeSaveData = async () => {
const config = this._config; const config = this._config;
const hostname = process.env.WEBHOSTNAME || config.webhostname || 'http://artix-notifier-web:8081'; const hostname = process.env['WEBHOSTNAME'] || config.webhostname || 'http://artix-notifier-web:8081';
try { try {
await fsp.writeFile(this._savePath, JSON.stringify(this._saveData)); await fsp.writeFile(this._savePath, JSON.stringify(this._saveData));
phin({ ky.put(`${hostname}/api/1.0/data`);
url: `${hostname}/api/1.0/data`,
method: 'PUT'
});
} }
catch { catch {
console.error(`Failed to write save data to ${this._savePath}`); console.error(`Failed to write save data to ${this._savePath}`);
@@ -306,4 +263,6 @@ class Daemon {
} }
} }
module.exports = Daemon; export default Daemon;
export { Daemon };
export type { SaveData };

View File

@@ -1,9 +1,50 @@
const sqlite = require('better-sqlite3'); import Database from 'better-sqlite3';
const TABLE = 'packages'; const TABLE = 'packages';
type Category = 'move' | 'udate';
interface CommonOperations {
GET: Database.Statement;
GETNEWBYMAINTAINER: Database.Statement;
UPDATE: Database.Statement;
INCREMENT: Database.Statement;
DECREMENT: Database.Statement;
FIXFLAG: Database.Statement;
GETPACKAGESBYMAINTAINER: Database.Statement;
GETPACKAGECOUNTBYMAINTAINER: Database.Statement;
}
interface DatabaseOperations {
GETPACKAGE: Database.Statement;
ADDPACKAGE: Database.Statement;
GETPACKAGES: Database.Statement;
GETPACKAGESSTARTWITH: Database.Statement;
UPDATEMAINTAINER: Database.Statement;
GETMAINTAINERPACKAGECOUNT: Database.Statement;
REMOVEOLDPACKAGE: Database.Statement;
move: CommonOperations;
udate: CommonOperations;
}
interface PackageDBEntry {
package: string;
maintainer: string;
move: number;
udate: number;
lastseen: Date;
}
interface CountResult {
count: number;
}
class DB { class DB {
constructor(file) { private _db: Database.Database;
this._db = new sqlite(file); private _queries: DatabaseOperations;
constructor(file: string | Buffer) {
this._db = new Database(file);
const db = this._db; const db = this._db;
db.pragma('journal_mode = WAL'); db.pragma('journal_mode = WAL');
@@ -42,23 +83,27 @@ class DB {
}; };
} }
restoreFlags() { restoreFlags(type: Category | null = null) {
this._queries.move.FIXFLAG.run(); if (type !== 'udate') {
this._queries.udate.FIXFLAG.run(); this._queries.move.FIXFLAG.run();
}
if (type !== 'move') {
this._queries.udate.FIXFLAG.run();
}
} }
getPackage(pack) { getPackage(pack: string): PackageDBEntry {
return this._queries.GETPACKAGE.get({ return this._queries.GETPACKAGE.get({
package: pack package: pack
}); }) as PackageDBEntry;
} }
getPackages(startsWith = null) { getPackages(startsWith: string | null = null): string[] {
return ((!!startsWith) ? this._queries.GETPACKAGESSTARTWITH.all({ startsWith }) : return ((!!startsWith) ? this._queries.GETPACKAGESSTARTWITH.all({ startsWith }) :
this._queries.GETPACKAGES.all()).map(p => p.package); this._queries.GETPACKAGES.all()).map(p => (p as PackageDBEntry).package);
} }
updatePackage(pack, maintainer, lastseen) { updatePackage(pack: string, maintainer: string, lastseen: number): Database.RunResult {
return this._queries[this.getPackage(pack) ? 'UPDATEMAINTAINER' : 'ADDPACKAGE'].run({ return this._queries[this.getPackage(pack) ? 'UPDATEMAINTAINER' : 'ADDPACKAGE'].run({
package: pack, package: pack,
maintainer, maintainer,
@@ -66,19 +111,19 @@ class DB {
}); });
} }
incrementFlag(pack, type) { incrementFlag(pack: string, type: Category): boolean {
this._queries[type].INCREMENT.run({ this._queries[type].INCREMENT.run({
package: pack package: pack
}); });
return true; return true;
} }
decrementFlags(type) { decrementFlags(type: Category): boolean {
this._queries[type].DECREMENT.run(); this._queries[type].DECREMENT.run();
return true; return true;
} }
updateFlag(pack, type, bool) { updateFlag(pack: string, type: Category, bool: number): boolean {
this._queries[type].UPDATE.run({ this._queries[type].UPDATE.run({
package: pack, package: pack,
bool bool
@@ -86,41 +131,43 @@ class DB {
return true; return true;
} }
getFlag(type, bool = true) { getFlag(type: Category, bool: boolean = true): PackageDBEntry[] {
return this._queries[type].GET.all({ return this._queries[type].GET.all({
bool bool
}); }) as PackageDBEntry[];
} }
getNewByMaintainer(maintainer, type) { getNewByMaintainer(maintainer: string, type: Category): PackageDBEntry[] {
return this._queries[type].GETNEWBYMAINTAINER.all({ return this._queries[type].GETNEWBYMAINTAINER.all({
maintainer maintainer
}); }) as PackageDBEntry[];
} }
getPackagesByMaintainer(maintainer, type) { getPackagesByMaintainer(maintainer: string, type: Category): PackageDBEntry[] {
return this._queries[type].GETPACKAGESBYMAINTAINER.all({ return this._queries[type].GETPACKAGESBYMAINTAINER.all({
maintainer maintainer
}); }) as PackageDBEntry[];
} }
getPackageCountByMaintainer(maintainer, type) { getPackageCountByMaintainer(maintainer: string, type: Category): number {
return this._queries[type].GETPACKAGECOUNTBYMAINTAINER.get({ return (this._queries[type].GETPACKAGECOUNTBYMAINTAINER.get({
maintainer maintainer
}).count; }) as CountResult).count;
} }
cleanOldPackages(lastseen) { cleanOldPackages(lastseen: number): Database.RunResult {
return this._queries.REMOVEOLDPACKAGE.run({ return this._queries.REMOVEOLDPACKAGE.run({
lastseen lastseen
}); });
} }
getMaintainerPackageCount(maintainer) { getMaintainerPackageCount(maintainer: string): number {
return this._queries.GETMAINTAINERPACKAGECOUNT.get({ return (this._queries.GETMAINTAINERPACKAGECOUNT.get({
maintainer maintainer
}).count; }) as CountResult).count;
} }
} }
module.exports = DB; export default DB;
export { DB };
export type { PackageDBEntry, Category };

29
src/index.mts Normal file
View File

@@ -0,0 +1,29 @@
import * as path from 'path';
import * as fsp from 'node:fs/promises';
import JSON5 from 'json5';
import { IRCBot } from './ircBot.mjs';
import { Daemon } from './daemon.mjs';
import { Web } from './web.mjs';
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..');
const data = await fsp.readFile(process.env['CONFIG'] || path.join(PROJECT_ROOT, 'config', 'config.json'));
let config = JSON5.parse(data.toString());
let arg = process.env['COMPONENT'] || (process.execArgv && process.execArgv[0]);
if (arg === 'daemon') {
const daemon = new Daemon(config);
process.on('SIGTERM', daemon.close);
}
else if (arg === 'ircbot') {
const bot = new IRCBot(config);
bot.connect();
process.on('SIGTERM', bot.close);
}
else if (arg === 'web') {
const web = new Web(config);
process.on('SIGTERM', web.close);
}
else {
console.error('Please pass the component you wish to run.');
}

View File

@@ -1,21 +1,31 @@
const IRC = require('irc-framework'); import express from 'express';
const express = require('express'); import type { Config, AuxiliaryIRCConfig } from './config.js';
import type http from "http";
// @ts-ignore
import IRC from 'irc-framework';
class IRCBot { class IRCBot {
private _aux: AuxiliaryIRCConfig | undefined;
private _channel: string | undefined;
private _bot: any;
private _enabled: boolean;
private _messageQueue: string[];
private _webserver: http.Server | undefined;
constructor(config) {
constructor(config: Config) {
const options = config['irc-framework']; const options = config['irc-framework'];
const aux = this._aux = config.ircClient || {}; const aux = this._aux = config.ircClient || undefined;
const app = express(); const app = express();
const port = process.env.PRIVATEPORT || config.privateport || 8081; const port = process.env['PRIVATEPORT'] || config.privateport || 8081;
this._channel = aux.channel; this._channel = aux?.channel;
this._messageQueue = []; this._messageQueue = [];
this._enabled = !!options; this._enabled = !!options;
app.set('trust proxy', 1); app.set('trust proxy', 1);
app.use(express.json()); app.use(express.json());
app.get('/healthcheck', (req, res) => { app.get('/healthcheck', (_, res) => {
if (this._bot && this._bot.connected) { if (this._bot && this._bot.connected) {
res.send('healthy'); res.send('healthy');
} }
@@ -44,22 +54,22 @@ class IRCBot {
if (options) { if (options) {
const bot = this._bot = new IRC.Client(options); const bot = this._bot = new IRC.Client(options);
bot.on('sasl failed', d => console.error(d)); bot.on('sasl failed', (d: Error) => console.error(d));
bot.on('notice', d => console.log(`irc:notice: ${d.message}`)); bot.on('notice', (d: Error) => console.log(`irc:notice: ${d.message}`));
bot.on('action', d => console.log(`irc:action: ${d.message}`)); bot.on('action', (d: Error) => console.log(`irc:action: ${d.message}`));
setInterval(() => this.processMessageQueue(), 2000); setInterval(() => this.processMessageQueue(), 2000);
this._webserver = app.listen(port, () => console.log(`artix-packy-notifier-irc running on port ${port}`)); this._webserver = app.listen(port, () => console.log(`artix-checkupdates-notifier-irc running on port ${port}`));
} }
else { else {
console.log('"ircClient" not provided in config. IRC notifications will not be delivered.'); console.log('"ircClient" not provided in config. IRC notifications will not be delivered.');
} }
} }
connect() { connect(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this._enabled) { if (this._enabled) {
const bot = this._bot; const bot = this._bot;
@@ -67,7 +77,7 @@ class IRCBot {
const callback = () => { const callback = () => {
clearTimeout(timeout); clearTimeout(timeout);
console.log(`IRC bot ${bot.user.nick} connected.`); console.log(`IRC bot ${bot.user.nick} connected.`);
bot.join(this._aux.channel, this._aux.channel_key); bot.join(this._aux?.channel, this._aux?.channel_key);
bot.removeListener('registered', callback); bot.removeListener('registered', callback);
resolve(); resolve();
}; };
@@ -83,7 +93,7 @@ class IRCBot {
}); });
} }
sendMessage(str) { sendMessage(str: string) {
(this._enabled ? str.split('\n') : []).forEach(line => { (this._enabled ? str.split('\n') : []).forEach(line => {
this._messageQueue.push(line); this._messageQueue.push(line);
}); });
@@ -105,4 +115,5 @@ class IRCBot {
} }
} }
module.exports = IRCBot; export default IRCBot;
export { IRCBot };

View File

@@ -1,40 +1,59 @@
const express = require('express'); import fs from 'fs';
const prom = require('prom-client'); import * as fsp from 'node:fs/promises';
const sharp = require('sharp'); import { DB } from './db.mjs';
const fs = require('fs'); import express from 'express';
const path = require('path'); import * as useragent from 'express-useragent';
const DB = require('./db'); import prom from 'prom-client';
const fsp = fs.promises; import sharp from 'sharp';
import * as path from 'path';
import type http from "http";
import type { Request, Response } from "express";
import type { Config } from './config.js';
import type { PackageDBEntry } from './db.mjs';
import type { SaveData } from './daemon.mjs';
const PROJECT_ROOT = __dirname; const PROJECT_ROOT = path.resolve(import.meta.dirname, '..');
const VIEWOPTIONS = { const VIEWOPTIONS = {
outputFunctionName: 'echo' outputFunctionName: 'echo'
}; };
function inliner(file) { type Action = "Move" | "Update";
type WebPackageObject = {
package: string;
action: Action;
url: string;
}
type ParseablePackage = string | PackageDBEntry;
function inliner(file: string) {
return fs.readFileSync(path.join(PROJECT_ROOT, file)); return fs.readFileSync(path.join(PROJECT_ROOT, file));
} }
function packageUrl(p) { function parsePackage(p: ParseablePackage): string {
const packagename = typeof p === 'string' ? p : p.package; return typeof p === 'string' ? p : p.package;
return `https://gitea.artixlinux.org/packages/${packagename}`;
} }
function prepPackages(arr, action) { function packageUrl(p: ParseablePackage) {
return `https://gitea.artixlinux.org/packages/${parsePackage(p)}`;
}
function prepPackages(arr: ParseablePackage[], action: Action): WebPackageObject[] {
return arr.map(m => { return arr.map(m => {
return { return {
package: m, package: parsePackage(m),
action, action,
url: packageUrl(m) url: packageUrl(m)
} }
}); });
} }
async function createOutlinedText(string, meta, gravity = 'west') { async function createOutlinedText(string: string, meta: sharp.Metadata, gravity: sharp.Gravity = 'west') {
const txt = sharp({ const txt = sharp({
create: { create: {
width: meta.width, width: meta.width || 0,
height: meta.height, height: meta.height || 0,
channels: 4, channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 } background: { r: 0, g: 0, b: 0, alpha: 0 }
} }
@@ -45,7 +64,7 @@ async function createOutlinedText(string, meta, gravity = 'west') {
text: string, text: string,
font: 'Visitor TT2 BRK', font: 'Visitor TT2 BRK',
fontfile: path.join(PROJECT_ROOT, 'userbar', 'visitor', 'visitor2.ttf'), fontfile: path.join(PROJECT_ROOT, 'userbar', 'visitor', 'visitor2.ttf'),
width: meta.width, width: meta.width || 0,
dpi: 109, dpi: 109,
rgba: true rgba: true
} }
@@ -90,16 +109,20 @@ async function createOutlinedText(string, meta, gravity = 'west') {
} }
class Web { class Web {
constructor(options) { private _webserver: http.Server;
const db = new DB(process.env.DBPATH || options.db || path.join(__dirname, 'config', 'packages.db')); private _privateserver: http.Server;
constructor(options: Config) {
const db = new DB(process.env['DBPATH'] || options.db || path.join(PROJECT_ROOT, 'config', 'packages.db'));
const app = express(); const app = express();
const privateapp = express(); const privateapp = express();
const port = process.env.PORT || options.port || 8080; const port = process.env['PORT'] || options.port || 8080;
const privateport = process.env.PRIVATEPORT || options.privateport || 8081; const privateport = process.env['PRIVATEPORT'] || options.privateport || 8081;
const METRICPREFIX = process.env.METRICPREFIX || 'artixpackages_'; const METRICPREFIX = process.env['METRICPREFIX'] || 'artixpackages_';
const maintainers = this._maintainers = (options.maintainers || []).map(m => typeof m === 'object' ? m.name : m).sort(); const maintainers = (options.maintainers || []).map(m => typeof m === 'object' ? m.name : m).sort();
const savePath = process.env.SAVEPATH || options.savePath || path.join(__dirname, 'config', 'data.json'); const savePath = process.env['SAVEPATH'] || options.savePath || path.join(PROJECT_ROOT, 'config', 'data.json');
let saveData = { let saveData: SaveData = {
'last-sync': null,
move: [], move: [],
update: [] update: []
}; };
@@ -108,8 +131,14 @@ class Web {
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
app.set('view options', VIEWOPTIONS); app.set('view options', VIEWOPTIONS);
function sendError(req, res, status, description) { app.use(useragent.express());
function sendError(req: Request, res: Response, status: number, description: string) {
console.log(`${status} (${description}): ${req.url} requested by ${req.ip} "${req.headers['user-agent']}"`); console.log(`${status} (${description}): ${req.url} requested by ${req.ip} "${req.headers['user-agent']}"`);
if (req.useragent?.browser === 'curl') {
res.send('404: not found\n');
return;
}
res.render('error', res.render('error',
{ {
inliner, inliner,
@@ -133,16 +162,26 @@ class Web {
} }
async function readSave() { async function readSave() {
saveData = JSON.parse(await fsp.readFile(savePath)); saveData = JSON.parse((await fsp.readFile(savePath)).toString());
}
function renderForCurl(packages: WebPackageObject[]) {
const colHeader = 'Package basename';
const tabSize = packages.reduce((acc, cur) => Math.max(acc, cur.package?.length || 0), colHeader.length) + 4;
return `${colHeader.padEnd(tabSize, ' ')}Action\n${packages.map(p => `${p.package?.padEnd(tabSize, ' ')}${p.action}`).join('\n')}\n`;
} }
app.get('/healthcheck', async (_, res) => { app.get('/healthcheck', async (_, res) => {
res.send('Healthy'); res.send('Healthy');
}); });
app.get('/', async (_, res) => { app.get('/', async (req, res) => {
let packages = prepPackages(saveData.move, 'Move'); let packages = prepPackages(saveData.move, 'Move');
packages = packages.concat(prepPackages(saveData.update, 'Update')); packages = packages.concat(prepPackages(saveData.update, 'Update'));
if (req.useragent?.browser === 'curl') {
res.send(renderForCurl(packages));
return;
}
res.render('index', res.render('index',
{ {
inliner, inliner,
@@ -171,6 +210,10 @@ class Web {
let packages = prepPackages(db.getPackagesByMaintainer(maintainer, 'move'), 'Move'); let packages = prepPackages(db.getPackagesByMaintainer(maintainer, 'move'), 'Move');
packages = packages.concat(prepPackages(db.getPackagesByMaintainer(maintainer, 'udate'), 'Update')); packages = packages.concat(prepPackages(db.getPackagesByMaintainer(maintainer, 'udate'), 'Update'));
if (packagesOwned > 0) { if (packagesOwned > 0) {
if (req.useragent?.browser === 'curl') {
res.send(`${maintainer}'s pending actions\n\n${renderForCurl(packages)}`);
return;
}
res.render('maintainer', res.render('maintainer',
{ {
inliner, inliner,
@@ -203,7 +246,7 @@ class Web {
const packagesOwned = db.getMaintainerPackageCount(maintainer); const packagesOwned = db.getMaintainerPackageCount(maintainer);
if (packagesOwned > 0) { if (packagesOwned > 0) {
const img = sharp(path.join(PROJECT_ROOT, 'userbar', 'userbar.png')); const img = sharp(path.join(PROJECT_ROOT, 'userbar', 'userbar.png'));
const meta = await img.metadata(); const meta: sharp.Metadata = await img.metadata();
const layers = [ const layers = [
{ {
@@ -249,7 +292,7 @@ class Web {
app.get('/api/1.0/packages', (req, res) => { app.get('/api/1.0/packages', (req, res) => {
const acceptHeader = req.headers.accept; const acceptHeader = req.headers.accept;
const startsWith = req.query.startswith; const startsWith = req.query['startswith'] as string;
const packages = db.getPackages(startsWith); const packages = db.getPackages(startsWith);
res.set('Cache-Control', 'public, max-age=360'); res.set('Cache-Control', 'public, max-age=360');
if (acceptHeader && acceptHeader.includes('application/json')) { if (acceptHeader && acceptHeader.includes('application/json')) {
@@ -262,14 +305,14 @@ class Web {
} }
}); });
privateapp.put('/api/1.0/data', (req, res) => { privateapp.put('/api/1.0/data', (_, res) => {
try { try {
readSave(); readSave();
res.json({ res.json({
success: true success: true
}); });
} }
catch(ex) { catch (ex) {
console.error(ex); console.error(ex);
res.status(500).json({ res.status(500).json({
success: false, success: false,
@@ -325,7 +368,7 @@ class Web {
res.end(await register.metrics()); res.end(await register.metrics());
} }
catch (ex) { catch (ex) {
console.error(err); console.error(ex);
res.status(500).send('something went wrong.'); res.status(500).send('something went wrong.');
} }
}); });
@@ -340,7 +383,7 @@ class Web {
readSave(); readSave();
this._webserver = app.listen(port, () => console.log(`artix-packy-notifier-web running on port ${port}`)); this._webserver = app.listen(port, () => console.log(`artix-checkupdates-web running on port ${port}`));
this._privateserver = privateapp.listen(privateport); this._privateserver = privateapp.listen(privateport);
} }
@@ -350,4 +393,5 @@ class Web {
} }
} }
module.exports = Web; export default Web;
export { Web };

12
tsconfig.dist.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"sourceMap": true,
"inlineSources": true,
"rootDir": "./src",
"outDir": "./distribution"
},
"include": [
"src"
]
}

10
tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "@sindresorhus/tsconfig",
"compilerOptions": {
"exactOptionalPropertyTypes": true,
"esModuleInterop": true
},
"include": [
"src"
]
}

View File

@@ -3,7 +3,7 @@
<ul> <ul>
<li>© <%= (new Date()).getFullYear() %> Artix Linux</li> <li>© <%= (new Date()).getFullYear() %> Artix Linux</li>
<li>Developed by Cory Sanin</li> <li>Developed by Cory Sanin</li>
<li><a href="https://gitea.artixlinux.org/corysanin/artix-packy-notifier">Source</a></li> <li><a href="https://github.com/CorySanin/artix-checkupdates-web">Source</a></li>
</ul> </ul>
</footer> </footer>
</div> </div>

View File

@@ -24,7 +24,7 @@
</tr> </tr>
<% packages && packages.forEach(p => { %> <% packages && packages.forEach(p => { %>
<tr> <tr>
<td><a href="<%= p.url %>"><%= p.package.package %></a></td> <td><a href="<%= p.url %>"><%= p.package %></a></td>
<td><%= p.action %></td> <td><%= p.action %></td>
</tr> </tr>
<% }); %> <% }); %>