alpha release

update v0.8.0
This commit is contained in:
2026-03-17 22:49:38 -04:00
commit 63a91931da
157 changed files with 10951 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import type { Request, Response } from 'express';
import type { Twttr } from 'twtxt-lib';
import {
generateEtag,
getQueryParameterArray,
getValueOrFirstEntry,
} from '../../lib/utils.js';
import { QueryParameters } from '../../types.js';
import NodeCache from '@cacheable/node-cache';
/**
*
* @param req
* @param res
* @param cache
* @param followingParameter
*/
export default function followingHandler(
req: Request,
res: Response,
cache: NodeCache<unknown>,
followingParameter: QueryParameters['following']
) {
const followingsToMatch = getQueryParameterArray(
req.query[followingParameter]
);
const nicksToMatch = getQueryParameterArray(req.query.nick);
const urlsToMatch = getQueryParameterArray(req.query.url);
const searchTermsToMatch = [
...getQueryParameterArray(req.query.search),
...getQueryParameterArray(req.query.s),
];
const wantsJson =
req.is('json') ||
getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json';
if (wantsJson) res.set('content-type', 'application/json');
else res.set('content-type', 'text/plain');
const matchedFollowing = (cache.get('following') as Twttr[]).filter(
({ nick, url }) =>
(!followingsToMatch.length ||
(followingsToMatch.length === 1 && followingsToMatch[0] === '') ||
followingsToMatch.includes(nick) ||
followingsToMatch.includes(`@${nick}`) ||
followingsToMatch.includes(url)) &&
(!nicksToMatch.length ||
nicksToMatch.includes(nick) ||
nicksToMatch.includes(`@${nick}`)) &&
(!urlsToMatch.length || urlsToMatch.includes(url)) &&
(!searchTermsToMatch.length ||
searchTermsToMatch.some(
(term) => nick.includes(term) || url.includes(term)
))
);
const result = wantsJson
? JSON.stringify(matchedFollowing)
: matchedFollowing.map(({ nick, url }) => `@${nick} ${url}`).join('\n');
res.set('etag', generateEtag(result)).send(result);
}

View File

@@ -0,0 +1 @@
export { default } from "./queryHandler.js";

View File

@@ -0,0 +1,92 @@
import type { Request, Response } from 'express';
import type { Metadata } from 'twtxt-lib';
import { env } from '../../lib/env.js';
import twtxtCache from '../../lib/twtxtCache.js';
import {
generateEtag,
getQueryParameterArray,
getValueOrFirstEntry,
} from '../../lib/utils.js';
import NodeCache from '@cacheable/node-cache';
import { QueryParameters } from '../../types.js';
export interface MetadataHandler {
cache: NodeCache<unknown>;
metadataParameter: QueryParameters['metadata'];
req: Request;
res: Response;
}
/**
*
* @param req
* @param res
* @param cache
* @param metadataParameter
*/
export default function metadataHandler(
req: Request,
res: Response,
cache: NodeCache<unknown>,
metadataParameter: QueryParameters['metadata']
) {
const metadataToMatch = getQueryParameterArray(req.query[metadataParameter]);
const searchTermsToMatch = [
...getQueryParameterArray(req.query.search),
...getQueryParameterArray(req.query.s),
];
const metadata = (cache.get('metadata') as Metadata) ?? {};
const wantsJson =
req.is('json') ||
getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json';
if (wantsJson) res.set('content-type', 'application/json');
else res.set('content-type', 'text/plain');
const matchedMetadata = Object.keys(metadata)
.filter(
(key) =>
(!metadataToMatch.length ||
(metadataToMatch.length === 1 && metadataToMatch[0] === '') ||
metadataToMatch.includes(key)) &&
(!searchTermsToMatch.length ||
searchTermsToMatch.some((term) =>
key.includes(term) || Array.isArray(metadata[key])
? (metadata[key] as string[]).some((val) => val.includes(term))
: metadata[key].includes(term)
))
)
.reduce(
(acc, key) => {
const value = metadata[key as keyof typeof metadata];
acc[key] = Array.isArray(value)
? value.filter(
(value) =>
!searchTermsToMatch.length ||
searchTermsToMatch.some(
(term) => key.includes(term) || value.includes(term)
)
)
: value;
return acc;
},
{} as Record<string, string | string[]>
);
const result = wantsJson
? JSON.stringify(matchedMetadata)
: Object.keys(matchedMetadata)
.map((key) => {
const value = matchedMetadata[key as keyof typeof matchedMetadata];
return Array.isArray(value)
? value.map((rowVal) => `${key}: ${rowVal}`).join('\n')
: `${key}: ${value}`;
})
.join('\n');
res.set('etag', generateEtag(result)).send(result);
}

View File

@@ -0,0 +1,88 @@
import path from 'node:path';
import NodeCache from '@cacheable/node-cache';
import Debug from 'debug';
import type { NextFunction, Request, Response } from 'express';
import { __dirname } from '../../lib/constants.js';
import { generateEtag } from '../../lib/utils.js';
import { TwtKprConfiguration } from '../../types.js';
import renderApp from '../renderApp/index.js';
import followingHandler from './followingHandler.js';
import metadataHandler from './metadataHandler.js';
import twtHandler from './twtHandler.js';
import { Twt } from 'twtxt-lib';
const debug = Debug('twtkpr:queryHandler');
/**
*
* @param config
* @param cache
* @param verifyAuthRequest
* @returns
*/
export default function queryHandler(
config: TwtKprConfiguration,
cache: NodeCache<unknown>,
verifyAuthRequest: (r: Request) => Promise<boolean>
) {
const { mainRoute, queryParameters, uploadConfiguration } = config;
return async (req: Request, res: Response, next: NextFunction) => {
debug({ query: JSON.stringify(req.query) });
if (!Object.keys(req.query).length) {
next();
return;
}
if (req.query[queryParameters.app] !== undefined) {
const appContent = renderApp({ mainRoute, uploadConfiguration });
res.set('etag', generateEtag(appContent)).send(appContent);
return;
}
if (req.query[queryParameters.css] !== undefined) {
res.sendFile('styles.css', {
root: path.resolve(__dirname, 'client'),
});
return;
}
if (req.query[queryParameters.js] !== undefined) {
res.sendFile('script.js', {
root: path.resolve(__dirname, 'client'),
});
return;
}
if (
req.query[queryParameters.following] !== undefined &&
cache.get('following')
) {
return followingHandler(req, res, cache, queryParameters.following);
}
if (
req.query[queryParameters.metadata] !== undefined &&
cache.get('metadata')
) {
return metadataHandler(req, res, cache, queryParameters.metadata);
}
if (
(req.query[queryParameters.twt] !== undefined ||
req.query[queryParameters.twts] !== undefined) &&
cache.get('twts')
) {
return twtHandler(
req,
res,
cache.get('twts') as Twt[],
queryParameters.twt,
queryParameters.twts
);
}
next();
};
}

View File

@@ -0,0 +1,101 @@
import type { Request, Response } from 'express';
import type { Metadata, Twt } from 'twtxt-lib';
import {
generateEtag,
getQueryParameterArray,
getValueOrFirstEntry,
} from '../../lib/utils.js';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import NodeCache from '@cacheable/node-cache';
import { QueryParameters } from '../../types.js';
dayjs.extend(utc);
/**
*
* @param req
* @param res
* @param twts
* @param twtParameter
* @param twtsParameter
* @returns
*/
export default function twtHandler(
req: Request,
res: Response,
twts: Twt[] = [],
twtParameter: QueryParameters['twt'],
twtsParameter: QueryParameters['twts']
) {
const twtsToMatch = getQueryParameterArray(req.query[twtsParameter]);
const showLastTwt = getQueryParameterArray(req.query[twtParameter]);
const hashesToMatch = getQueryParameterArray(req.query.hash);
const searchTermsToMatch = [
...getQueryParameterArray(req.query.search),
...getQueryParameterArray(req.query.s),
];
const createdDatesToMatch = getQueryParameterArray(req.query.created_date);
const createdUTCStartDatesToMatch = getQueryParameterArray(
req.query.created_date_start
).map((val) => dayjs.utc(val));
const createdUTCEndDatesToMatch = getQueryParameterArray(
req.query.created_date_end
).map((val) => dayjs.utc(val));
const wantsJson =
req.is('json') ||
getValueOrFirstEntry(getQueryParameterArray(req.query.format)) === 'json';
if (wantsJson) res.set('content-type', 'application/json');
else res.set('content-type', 'text/plain');
if (showLastTwt.length === 1 && showLastTwt[0] === '') {
const lastTwt = twts.reduce((matched, curr) =>
matched?.createdUTC > curr.createdUTC ? matched : curr
);
let result = 'No results';
if (lastTwt) {
result = wantsJson
? JSON.stringify(lastTwt)
: `${lastTwt?.created || ''}\t${lastTwt?.content || ''}\n`;
}
res.set('etag', generateEtag(result)).send(result);
return;
}
const matchedTwts = twts.filter(({ content, created, createdUTC, hash }) => {
return (
(!twtsToMatch.length ||
(twtsToMatch.length === 1 && twtsToMatch[0] === '') ||
twtsToMatch.includes(created) ||
(hash &&
(twtsToMatch.includes(hash) || twtsToMatch.includes(`#${hash}`)))) &&
(!hashesToMatch.length ||
(hash &&
(hashesToMatch.includes(hash) ||
hashesToMatch.includes(`#${hash}`)))) &&
(!createdDatesToMatch.length ||
createdDatesToMatch.some((date) => created.includes(date))) &&
(!createdUTCStartDatesToMatch.length ||
createdUTCStartDatesToMatch.some(
(date) => date.diff(createdUTC) < 0
)) &&
(!createdUTCEndDatesToMatch.length ||
createdUTCEndDatesToMatch.some((date) => date.diff(createdUTC) > 0)) &&
(!searchTermsToMatch.length ||
searchTermsToMatch.some((term) => content.includes(term)))
);
});
const result = wantsJson
? JSON.stringify(matchedTwts)
: matchedTwts
.map(({ content, created }) => `${created}\t${content}`)
.join('\n');
res.set('etag', generateEtag(result)).send(result);
}