omgebouwd naar adonisjs, werkt nog niet echt goed

This commit is contained in:
Daan Meijer
2026-06-03 23:41:09 +02:00
parent dfd3336447
commit 62f1767df3
89 changed files with 9892 additions and 1174 deletions
@@ -0,0 +1,29 @@
import User from '#models/user'
import { loginValidator } from '#validators/user'
import type { HttpContext } from '@adonisjs/core/http'
import UserTransformer from '#transformers/user_transformer'
export default class AccessTokensController {
async store({ request, serialize }: HttpContext) {
const { email, password } = await request.validateUsing(loginValidator)
const user = await User.verifyCredentials(email, password)
const token = await User.accessTokens.create(user)
return serialize({
user: UserTransformer.transform(user),
token: token.value!.release(),
})
}
async destroy({ auth }: HttpContext) {
const user = auth.getUserOrFail()
if (user.currentAccessToken) {
await User.accessTokens.delete(user, user.currentAccessToken.identifier)
}
return {
message: 'Logged out successfully',
}
}
}
+18
View File
@@ -0,0 +1,18 @@
import User from '#models/user'
import { signupValidator } from '#validators/user'
import type { HttpContext } from '@adonisjs/core/http'
import UserTransformer from '#transformers/user_transformer'
export default class NewAccountController {
async store({ request, serialize }: HttpContext) {
const { fullName, email, password } = await request.validateUsing(signupValidator)
const user = await User.create({ fullName, email, password })
const token = await User.accessTokens.create(user)
return serialize({
user: UserTransformer.transform(user),
token: token.value!.release(),
})
}
}
+8
View File
@@ -0,0 +1,8 @@
import UserTransformer from '#transformers/user_transformer'
import type { HttpContext } from '@adonisjs/core/http'
export default class ProfileController {
async show({ auth, serialize }: HttpContext) {
return serialize(UserTransformer.transform(auth.getUserOrFail()))
}
}
+28
View File
@@ -0,0 +1,28 @@
import app from '@adonisjs/core/services/app'
import { type HttpContext, ExceptionHandler } from '@adonisjs/core/http'
export default class HttpExceptionHandler extends ExceptionHandler {
/**
* In debug mode, the exception handler will display verbose errors
* with pretty printed stack traces.
*/
protected debug = !app.inProduction
/**
* The method is used for handling errors and returning
* response to the client
*/
async handle(error: unknown, ctx: HttpContext) {
return super.handle(error, ctx)
}
/**
* The method is used to report error to the logging service or
* the a third party error monitoring service.
*
* @note You should not attempt to send a response from this method.
*/
async report(error: unknown, ctx: HttpContext) {
return super.report(error, ctx)
}
}
+20
View File
@@ -0,0 +1,20 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import type { Authenticators } from '@adonisjs/auth/types'
/**
* Auth middleware is used authenticate HTTP requests and deny
* access to unauthenticated users.
*/
export default class AuthMiddleware {
async handle(
ctx: HttpContext,
next: NextFn,
options: {
guards?: (keyof Authenticators)[]
} = {}
) {
await ctx.auth.authenticateUsing(options.guards)
return next()
}
}
@@ -0,0 +1,19 @@
import { Logger } from '@adonisjs/core/logger'
import { HttpContext } from '@adonisjs/core/http'
import { type NextFn } from '@adonisjs/core/types/http'
/**
* The container bindings middleware binds classes to their request
* specific value using the container resolver.
*
* - We bind "HttpContext" class to the "ctx" object
* - And bind "Logger" class to the "ctx.logger" object
*/
export default class ContainerBindingsMiddleware {
handle(ctx: HttpContext, next: NextFn) {
ctx.containerResolver.bindValue(HttpContext, ctx)
ctx.containerResolver.bindValue(Logger, ctx.logger)
return next()
}
}
@@ -0,0 +1,9 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
export default class ForceJsonResponseMiddleware {
handle(ctx: HttpContext, next: NextFn) {
ctx.request.request.headers.accept = 'application/json'
return next()
}
}
+16
View File
@@ -0,0 +1,16 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
/**
* Silent auth middleware can be used as a global middleware to silent check
* if the user is logged-in or not.
*
* The request continues as usual, even when the user is not logged-in.
*/
export default class SilentAuthMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
await ctx.auth.check()
return next()
}
}
+4
View File
@@ -0,0 +1,4 @@
import { BaseModel as AdonisBaseModel } from '@adonisjs/lucid/orm'
export default class BaseModel extends AdonisBaseModel {
}
+32
View File
@@ -0,0 +1,32 @@
import { DateTime } from 'luxon'
import { column } from '@adonisjs/lucid/orm'
import BaseModel from '#models/base_model'
export default class File extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare filename: string
@column()
declare poster: string
@column()
declare date: number
@column()
declare parts: number
@column({ columnName: 'message_ids' })
declare messageIds: Record<string, any>
@column()
declare groups: string[]
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
+23
View File
@@ -0,0 +1,23 @@
import { DateTime } from 'luxon'
import { column } from '@adonisjs/lucid/orm'
import BaseModel from '#models/base_model'
export default class Group extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare name: string
@column()
declare active: boolean
@column({ columnName: 'last_indexed_id' })
declare lastIndexedId: bigint
@column.dateTime({ autoCreate: true })
declare createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}
+18
View File
@@ -0,0 +1,18 @@
import { UserSchema } from '#database/schema'
import hash from '@adonisjs/core/services/hash'
import { compose } from '@adonisjs/core/helpers'
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'
import { type AccessToken, DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
export default class User extends compose(UserSchema, withAuthFinder(hash)) {
static accessTokens = DbAccessTokensProvider.forModel(User)
declare currentAccessToken?: AccessToken
get initials() {
const [first, last] = this.fullName ? this.fullName.split(' ') : this.email.split('@')
if (first && last) {
return `${first.charAt(0)}${last.charAt(0)}`.toUpperCase()
}
return `${first.slice(0, 2)}`.toUpperCase()
}
}
+72
View File
@@ -0,0 +1,72 @@
import nntpConfig from '#config/nntp'
import { NNTP } from "nntp-js";
class NntpService {
private readonly poolSize: number;
private allConnections: Set<any>;
private idleConnections: any[];
private waiters: ((conn: any) => void)[];
private createdCount: number;
constructor(poolSize = 10) {
this.poolSize = poolSize;
this.allConnections = new Set();
this.idleConnections = [];
this.waiters = [];
this.createdCount = 0;
console.log(`NNTP Pool initialized with size ${this.poolSize}`)
}
private async _createConnection() {
// This connection logic is based on the older, working pool implementation.
const conn = new NNTP(nntpConfig.host, nntpConfig.port);
await conn.connect();
if (nntpConfig.user) {
await conn.login(nntpConfig.user, nntpConfig.password?.release());
}
this.allConnections.add(conn);
return conn;
}
public async acquire() {
if (this.idleConnections.length > 0) {
console.log('Reusing existing connection from pool.');
return this.idleConnections.pop();
}
if (this.createdCount < this.poolSize) {
this.createdCount++;
console.log(`Creating new connection (${this.createdCount}/${this.poolSize}).`);
return this._createConnection();
}
console.log(`Pool maxed out at ${this.poolSize}. Waiting for a connection to become available.`);
return new Promise(resolve => this.waiters.push(resolve));
}
public release(conn: any) {
if (this.waiters.length > 0) {
console.log('Releasing connection directly to a waiting task.');
const resolve = this.waiters.shift();
if(resolve) resolve(conn);
} else {
console.log('Returning connection to the idle pool.');
this.idleConnections.push(conn);
}
}
public async shutdown() {
console.log('Shutting down all connections in the pool.');
const shutdownPromises: Promise<any>[] = [];
for (const conn of this.allConnections) {
shutdownPromises.push(conn.quit());
}
await Promise.all(shutdownPromises);
this.allConnections.clear();
this.idleConnections.length = 0;
this.waiters.length = 0;
this.createdCount = 0;
}
}
export default new NntpService();
+30
View File
@@ -0,0 +1,30 @@
import { Queue } from 'bullmq'
import queueConfig from '#config/queue'
class QueueService {
public readonly nntpFetchQueue: Queue
public readonly headerQueue: Queue
public readonly fileQueue: Queue
public readonly bodyQueue: Queue
public readonly collectionQueue: Queue
constructor() {
this.nntpFetchQueue = new Queue('nntp-fetch-queue', { connection: queueConfig.connection })
this.headerQueue = new Queue('header-queue', { connection: queueConfig.connection })
this.fileQueue = new Queue('file-queue', { connection: queueConfig.connection })
this.bodyQueue = new Queue('body-queue', { connection: queueConfig.connection })
this.collectionQueue = new Queue('collection-queue', { connection: queueConfig.connection })
}
async closeAll() {
await Promise.all([
this.nntpFetchQueue.close(),
this.headerQueue.close(),
this.fileQueue.close(),
this.bodyQueue.close(),
this.collectionQueue.close(),
])
}
}
export default new QueueService()
+15
View File
@@ -0,0 +1,15 @@
import { createRequire } from 'module'
import redisConfig from '#config/redis'
const require = createRequire(import.meta.url)
const IORedis = require('ioredis')
class RedisService {
public readonly client: any
constructor() {
this.client = new IORedis(redisConfig.connections.main)
}
}
export default new RedisService()
+19
View File
@@ -0,0 +1,19 @@
import { decode } from 'simple-yenc'
export class YencFile {
private buffer: Buffer | null = null
public processPart(partBuffer: Buffer) {
// This is a simplified implementation.
// simple-yenc's decode function is synchronous and works on a full buffer.
// A more complex implementation would handle multi-part decoding.
this.buffer = decode(partBuffer)
}
public getBuffer(): Buffer {
if (!this.buffer) {
throw new Error('No data has been processed yet.')
}
return this.buffer
}
}
+36
View File
@@ -0,0 +1,36 @@
export function parseYencMeta(buffer: Buffer): { header: Record<string, any>; crc32?: string } {
const text = buffer.toString('latin1')
const lines = text.split(/\\r?\\n/)
const header: Record<string, any> = {}
let crc32: string | undefined
for (const line of lines) {
if (line.startsWith('=ybegin')) {
const parts = line.split(' ')
parts.forEach((part) => {
if (part.includes('=')) {
const [key, value] = part.split('=')
if (key === 'name') {
header[key] = value.trim()
} else {
header[key] = parseInt(value, 10)
}
}
})
} else if (line.startsWith('=ypart')) {
const match = /begin=(\d+)/.exec(line)
if (match) {
header.partBegin = parseInt(match[1], 10)
}
} else if (line.startsWith('=yend')) {
const match = /crc32=([a-fA-F0-9]+)/.exec(line)
if (match) {
crc32 = match[1]
}
break // End of yenc data
}
}
return { header, crc32 }
}
+15
View File
@@ -0,0 +1,15 @@
import type User from '#models/user'
import { BaseTransformer } from '@adonisjs/core/transformers'
export default class UserTransformer extends BaseTransformer<User> {
toObject() {
return this.pick(this.resource, [
'id',
'fullName',
'email',
'createdAt',
'updatedAt',
'initials',
])
}
}
+26
View File
@@ -0,0 +1,26 @@
import vine from '@vinejs/vine'
/**
* Shared rules for email and password.
*/
const email = () => vine.string().email().maxLength(254)
const password = () => vine.string().minLength(8).maxLength(32)
/**
* Validator to use when performing self-signup
*/
export const signupValidator = vine.create({
fullName: vine.string().nullable(),
email: email().unique({ table: 'users', column: 'email' }),
password: password(),
passwordConfirmation: password().sameAs('password'),
})
/**
* Validator to use before validating user credentials
* during login
*/
export const loginValidator = vine.create({
email: email(),
password: vine.string(),
})