Merge branch 'main' into i18n

This commit is contained in:
dangered wolf 2024-05-01 00:30:42 -04:00 committed by GitHub
commit 5a151ecace
Signed by: DevComp
GPG key ID: B5690EEEBB952194
14 changed files with 59 additions and 56 deletions

View file

@ -4,6 +4,7 @@
![][icons] ![][icons]
[![Crowdin][crowdinbadge]][crowdin]
[![esbuild][buildbadge]][build] [![esbuild][buildbadge]][build]
[![Tests][testsbadge]][tests] [![Tests][testsbadge]][tests]
[![Status][statusbadge]][status] [![Status][statusbadge]][status]
@ -20,6 +21,8 @@
[licensebadge]: https://img.shields.io/github/license/FixTweet/FxTwitter [licensebadge]: https://img.shields.io/github/license/FixTweet/FxTwitter
[status]: https://status.fxtwitter.com [status]: https://status.fxtwitter.com
[statusbadge]: https://status.fxtwitter.com/api/badge/8/uptime/720?label=Uptime%2030d [statusbadge]: https://status.fxtwitter.com/api/badge/8/uptime/720?label=Uptime%2030d
[crowdinbadge]: https://badges.crowdin.net/fxtwitter/localized.svg
[crowdin]: https://crowdin.com/project/fxtwitter
## Written in TypeScript as a Cloudflare Worker to scale, packed with more features and [best-in-class user privacy 🔒](#built-with-privacy-in-mind). ## Written in TypeScript as a Cloudflare Worker to scale, packed with more features and [best-in-class user privacy 🔒](#built-with-privacy-in-mind).

38
package-lock.json generated
View file

@ -10,9 +10,9 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@hono/sentry": "^1.0.1", "@hono/sentry": "^1.0.1",
"hono": "^3.12.12",
"i18next": "^23.8.2", "i18next": "^23.8.2",
"i18next-icu": "^2.3.0" "i18next-icu": "^2.3.0",
"hono": "^4.2.9"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20240423.0", "@cloudflare/workers-types": "^4.20240423.0",
@ -34,7 +34,7 @@
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"wrangler": "^3.52.0" "wrangler": "^3.53.0"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@ -1752,7 +1752,7 @@
"@miniflare/core": "2.14.2", "@miniflare/core": "2.14.2",
"@miniflare/shared": "2.14.2", "@miniflare/shared": "2.14.2",
"http-cache-semantics": "^4.1.0", "http-cache-semantics": "^4.1.0",
"undici": "5.28.2" "undici": "5.28.4"
}, },
"engines": { "engines": {
"node": ">=16.13" "node": ">=16.13"
@ -1772,7 +1772,7 @@
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"kleur": "^4.1.4", "kleur": "^4.1.4",
"set-cookie-parser": "^2.4.8", "set-cookie-parser": "^2.4.8",
"undici": "5.28.2", "undici": "5.28.4",
"urlpattern-polyfill": "^4.0.3" "urlpattern-polyfill": "^4.0.3"
}, },
"engines": { "engines": {
@ -1810,7 +1810,7 @@
"@miniflare/core": "2.14.2", "@miniflare/core": "2.14.2",
"@miniflare/shared": "2.14.2", "@miniflare/shared": "2.14.2",
"@miniflare/storage-memory": "2.14.2", "@miniflare/storage-memory": "2.14.2",
"undici": "5.28.2" "undici": "5.28.4"
}, },
"engines": { "engines": {
"node": ">=16.13" "node": ">=16.13"
@ -1825,7 +1825,7 @@
"@miniflare/core": "2.14.2", "@miniflare/core": "2.14.2",
"@miniflare/shared": "2.14.2", "@miniflare/shared": "2.14.2",
"html-rewriter-wasm": "^0.4.1", "html-rewriter-wasm": "^0.4.1",
"undici": "5.28.2" "undici": "5.28.4"
}, },
"engines": { "engines": {
"node": ">=16.13" "node": ">=16.13"
@ -1863,7 +1863,7 @@
"dependencies": { "dependencies": {
"@miniflare/core": "2.14.2", "@miniflare/core": "2.14.2",
"@miniflare/shared": "2.14.2", "@miniflare/shared": "2.14.2",
"undici": "5.28.2" "undici": "5.28.4"
}, },
"engines": { "engines": {
"node": ">=16.13" "node": ">=16.13"
@ -1979,7 +1979,7 @@
"dependencies": { "dependencies": {
"@miniflare/core": "2.14.2", "@miniflare/core": "2.14.2",
"@miniflare/shared": "2.14.2", "@miniflare/shared": "2.14.2",
"undici": "5.28.2", "undici": "5.28.4",
"ws": "^8.2.2" "ws": "^8.2.2"
}, },
"engines": { "engines": {
@ -4549,9 +4549,9 @@
} }
}, },
"node_modules/hono": { "node_modules/hono": {
"version": "3.12.12", "version": "4.2.9",
"resolved": "https://registry.npmjs.org/hono/-/hono-3.12.12.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.2.9.tgz",
"integrity": "sha512-5IAMJOXfpA5nT+K0MNjClchzz0IhBHs2Szl7WFAhrFOsbtQsYmNynFyJRg/a3IPsmCfxcrf8txUGiNShXpK5Rg==", "integrity": "sha512-59FAv52UxDWUt/NlC0NzrRCjeVCThUnVlqlrKYm+k80XujBu6uJwBIa5gACKKZWobjA0MJ6Vds0I3URKf383Cw==",
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
} }
@ -6970,7 +6970,7 @@
"exit-hook": "^2.2.1", "exit-hook": "^2.2.1",
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"undici": "^5.28.2", "undici": "^5.28.4",
"workerd": "1.20240419.0", "workerd": "1.20240419.0",
"ws": "^8.11.0", "ws": "^8.11.0",
"youch": "^3.2.2", "youch": "^3.2.2",
@ -8766,9 +8766,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.28.2", "version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@fastify/busboy": "^2.0.0" "@fastify/busboy": "^2.0.0"
@ -9047,9 +9047,9 @@
} }
}, },
"node_modules/wrangler": { "node_modules/wrangler": {
"version": "3.52.0", "version": "3.53.0",
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.52.0.tgz", "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.53.0.tgz",
"integrity": "sha512-HR06jTym+yr7+CI3Ggld3nfp1OM9vSC7h4B8vwWHwhi5K0sYg8p44rxV514Gmsv9dkFHegkRP70SM3sjuuxxpQ==", "integrity": "sha512-JxkvCQekL9j8Mu4CEKM/HEVyDnymWzKQuMUuJH0yum1AilutD5HAP9kVVYmvu7BvwlRyRUAj8TI5OUxXnLCEpQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@cloudflare/kv-asset-handler": "0.3.2", "@cloudflare/kv-asset-handler": "0.3.2",

View file

@ -35,12 +35,12 @@
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"wrangler": "^3.52.0" "wrangler": "^3.53.0"
}, },
"dependencies": { "dependencies": {
"@hono/sentry": "^1.0.1", "@hono/sentry": "^1.0.1",
"hono": "^3.12.12",
"i18next": "^23.8.2", "i18next": "^23.8.2",
"i18next-icu": "^2.3.0" "i18next-icu": "^2.3.0",
"hono": "^4.2.9"
} }
} }

View file

@ -90,11 +90,11 @@ export const cacheMiddleware = (): MiddlewareHandler => async (c, next) => {
/* We properly state our OPTIONS when asked */ /* We properly state our OPTIONS when asked */
case 'OPTIONS': case 'OPTIONS':
c.header('allow', Constants.RESPONSE_HEADERS.allow); c.header('allow', Constants.RESPONSE_HEADERS.allow);
c.body(null);
c.status(204); c.status(204);
return; return;
default: default:
c.status(405);
if (returnAsJson) return c.json(''); if (returnAsJson) return c.json('');
return c.html(''); return c.html('', 405);
} }
}; };

View file

@ -1,4 +1,7 @@
import { Context } from 'hono'; import { Context } from 'hono';
import { StatusCode } from 'hono/utils/http-status';
import i18next from 'i18next';
import icu from "i18next-icu";
import { Constants } from '../constants'; import { Constants } from '../constants';
import { handleQuote } from '../helpers/quote'; import { handleQuote } from '../helpers/quote';
import { formatNumber, sanitizeText, truncateWithEllipsis } from '../helpers/utils'; import { formatNumber, sanitizeText, truncateWithEllipsis } from '../helpers/utils';
@ -9,8 +12,6 @@ import { renderVideo } from '../render/video';
import { renderInstantView } from '../render/instantview'; import { renderInstantView } from '../render/instantview';
import { constructTwitterThread } from '../providers/twitter/conversation'; import { constructTwitterThread } from '../providers/twitter/conversation';
import { Experiment, experimentCheck } from '../experiments'; import { Experiment, experimentCheck } from '../experiments';
import i18next from 'i18next';
import icu from "i18next-icu";
import translationResources from '../../i18n/resources.json'; import translationResources from '../../i18n/resources.json';
export const returnError = (c: Context, error: string): Response => { export const returnError = (c: Context, error: string): Response => {
@ -81,7 +82,7 @@ export const handleStatus = async (
/* Catch this request if it's an API response */ /* Catch this request if it's an API response */
if (flags?.api) { if (flags?.api) {
c.status(api.code); c.status(api.code as StatusCode);
// Add every header from Constants.API_RESPONSE_HEADERS // Add every header from Constants.API_RESPONSE_HEADERS
for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) { for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) {
c.header(header, value); c.header(header, value);

View file

@ -4,6 +4,7 @@ import { buildAPITwitterStatus } from './processor';
import { Experiment, experimentCheck } from '../../experiments'; import { Experiment, experimentCheck } from '../../experiments';
import { isGraphQLTwitterStatus } from '../../helpers/graphql'; import { isGraphQLTwitterStatus } from '../../helpers/graphql';
import { Context } from 'hono'; import { Context } from 'hono';
import { StatusCode } from 'hono/utils/http-status';
const writeDataPoint = ( const writeDataPoint = (
c: Context, c: Context,
@ -579,10 +580,9 @@ export const threadAPIProvider = async (c: Context) => {
const processedResponse = await constructTwitterThread(id, true, c, undefined); const processedResponse = await constructTwitterThread(id, true, c, undefined);
c.status(processedResponse.code);
// Add every header from Constants.API_RESPONSE_HEADERS // Add every header from Constants.API_RESPONSE_HEADERS
for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) { for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) {
c.header(header, value); c.header(header, value);
} }
return c.json(processedResponse); return c.json(processedResponse, processedResponse.code as StatusCode);
}; };

View file

@ -5,7 +5,7 @@ export const linkHitRequest = async (c: Context) => {
const userAgent = c.req.header('User-Agent') || ''; const userAgent = c.req.header('User-Agent') || '';
if (userAgent.includes('Telegram')) { if (userAgent.includes('Telegram')) {
c.status(403); return c.text('', 403);
} }
// If param `url` exists, 302 redirect to it // If param `url` exists, 302 redirect to it
if (typeof c.req.query('url') === 'string') { if (typeof c.req.query('url') === 'string') {

View file

@ -9,11 +9,13 @@ export const api = new Hono();
api.use('*', async (c, next) => { api.use('*', async (c, next) => {
if (!c.req.header('user-agent')) { if (!c.req.header('user-agent')) {
c.status(401); return c.json(
return c.json({ {
error: error:
"You must identify yourself with a User-Agent header in order to use the FixTweet API. We recommend using a descriptive User-Agent header to identify your app, such as 'MyAwesomeBot/1.0 (+http://example.com/myawesomebot)'. We don't track or save what kinds of data you are pulling, but you may be blocked if you send too many requests from an unidentifiable user agent." "You must identify yourself with a User-Agent header in order to use the FixTweet API. We recommend using a descriptive User-Agent header to identify your app, such as 'MyAwesomeBot/1.0 (+http://example.com/myawesomebot)'. We don't track or save what kinds of data you are pulling, but you may be blocked if you send too many requests from an unidentifiable user agent."
}); },
401
);
} }
await next(); await next();
}); });

View file

@ -28,7 +28,6 @@ export const oembed = async (c: Context) => {
}; };
c.header('content-type', 'application/json'); c.header('content-type', 'application/json');
c.status(200);
/* Stringify and send it on its way! */ /* Stringify and send it on its way! */
return c.text(JSON.stringify(data)); return c.text(JSON.stringify(data), 200);
}; };

View file

@ -25,12 +25,11 @@ export const setRedirectRequest = async (c: Context) => {
/* Check that origin either does not exist or is in our domain list */ /* Check that origin either does not exist or is in our domain list */
const origin = c.req.header('origin'); const origin = c.req.header('origin');
if (origin && !Constants.STANDARD_DOMAIN_LIST.includes(new URL(origin).hostname)) { if (origin && !Constants.STANDARD_DOMAIN_LIST.includes(new URL(origin).hostname)) {
c.status(403);
return c.html( return c.html(
Strings.MESSAGE_HTML.format({ Strings.MESSAGE_HTML.format({
message: `Failed to set base redirect: Your request seems to be originating from another domain, please open this up in a new tab if you are trying to set your base redirect.` message: `Failed to set base redirect: Your request seems to be originating from another domain, please open this up in a new tab if you are trying to set your base redirect.`
}) }),
403
); );
} }
@ -46,11 +45,11 @@ export const setRedirectRequest = async (c: Context) => {
'content-security-policy', 'content-security-policy',
`frame-ancestors ${Constants.STANDARD_DOMAIN_LIST.join(' ')};` `frame-ancestors ${Constants.STANDARD_DOMAIN_LIST.join(' ')};`
); );
c.status(200);
return c.html( return c.html(
Strings.MESSAGE_HTML.format({ Strings.MESSAGE_HTML.format({
message: `Your base redirect has been cleared. To set one, please pass along the <code>url</code> parameter.` message: `Your base redirect has been cleared. To set one, please pass along the <code>url</code> parameter.`
}) }),
200
); );
} }
@ -71,11 +70,11 @@ export const setRedirectRequest = async (c: Context) => {
'content-security-policy', 'content-security-policy',
`frame-ancestors ${Constants.STANDARD_DOMAIN_LIST.join(' ')};` `frame-ancestors ${Constants.STANDARD_DOMAIN_LIST.join(' ')};`
); );
c.status(200);
return c.html( return c.html(
Strings.MESSAGE_HTML.format({ Strings.MESSAGE_HTML.format({
message: `Your URL does not appear to be well-formed. Example: ?url=https://nitter.net` message: `Your URL does not appear to be well-formed. Example: ?url=https://nitter.net`
}) }),
200
); );
} }

View file

@ -131,8 +131,7 @@ export const statusRequest = async (c: Context) => {
return statusResponse; return statusResponse;
} else { } else {
/* Somehow handleStatus sent us nothing. This should *never* happen, but we have a case for it. */ /* Somehow handleStatus sent us nothing. This should *never* happen, but we have a case for it. */
c.status(500); return c.text(Strings.ERROR_UNKNOWN, 500);
return c.text(Strings.ERROR_UNKNOWN);
} }
} else { } else {
/* A human has clicked a fxtwitter.com/:screen_name/status/:id link! /* A human has clicked a fxtwitter.com/:screen_name/status/:id link!

View file

@ -322,8 +322,8 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc
let previousThreadPieceAuthor: string | null = null; let previousThreadPieceAuthor: string | null = null;
let originalAuthor: string | null = null; let originalAuthor: string | null = null;
const useThread = thread?.thread ?? [ thread?.status ] const useThread = thread?.thread ?? [thread?.status];
if (!status) { if (!status) {
throw new Error('Status is undefined'); throw new Error('Status is undefined');
@ -359,7 +359,8 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc
<sub><a href="${status.url}">${i18next.t('ivViewOriginal')}</a></sub> <sub><a href="${status.url}">${i18next.t('ivViewOriginal')}</a></sub>
<h1>${status.author.name} (@${status.author.screen_name})</h1> <h1>${status.author.name} (@${status.author.screen_name})</h1>
${useThread.map(status => { ${useThread
.map(status => {
console.log('previousThreadPieceAuthor', previousThreadPieceAuthor); console.log('previousThreadPieceAuthor', previousThreadPieceAuthor);
if (!status) { if (!status) {
return ''; return '';

View file

@ -2,6 +2,7 @@ import { Context } from 'hono';
import { Constants } from './constants'; import { Constants } from './constants';
import { Strings } from './strings'; import { Strings } from './strings';
import { userAPI } from './providers/twitter/profile'; import { userAPI } from './providers/twitter/profile';
import { StatusCode } from 'hono/utils/http-status';
export const returnError = (c: Context, error: string): Response => { export const returnError = (c: Context, error: string): Response => {
return c.html( return c.html(
@ -29,7 +30,7 @@ export const handleProfile = async (
/* Catch this request if it's an API response */ /* Catch this request if it's an API response */
// For now we just always return the API response while testing // For now we just always return the API response while testing
if (flags?.api) { if (flags?.api) {
c.status(api.code); c.status(api.code as StatusCode);
// Add every header from Constants.API_RESPONSE_HEADERS // Add every header from Constants.API_RESPONSE_HEADERS
for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) { for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) {
c.header(header, value); c.header(header, value);

View file

@ -8,6 +8,7 @@ import { Constants } from './constants';
import { api } from './realms/api/router'; import { api } from './realms/api/router';
import { twitter } from './realms/twitter/router'; import { twitter } from './realms/twitter/router';
import { cacheMiddleware } from './caches'; import { cacheMiddleware } from './caches';
import { StatusCode } from 'hono/utils/http-status';
const noCache = 'max-age=0, no-cache, no-store, must-revalidate'; const noCache = 'max-age=0, no-cache, no-store, must-revalidate';
const embeddingClientRegex = const embeddingClientRegex =
@ -89,10 +90,9 @@ app.onError((err, c) => {
if (c.req.header('User-Agent')?.match(embeddingClientRegex)) { if (c.req.header('User-Agent')?.match(embeddingClientRegex)) {
errorCode = 200; errorCode = 200;
} }
c.status(errorCode);
c.header('cache-control', noCache); c.header('cache-control', noCache);
return c.html(Strings.ERROR_HTML); return c.html(Strings.ERROR_HTML, errorCode as StatusCode);
}); });
const customLogger = (message: string, ...rest: string[]) => { const customLogger = (message: string, ...rest: string[]) => {
@ -138,12 +138,10 @@ app.all('/error', async c => {
c.header('cache-control', noCache); c.header('cache-control', noCache);
if (c.req.header('User-Agent')?.match(embeddingClientRegex)) { if (c.req.header('User-Agent')?.match(embeddingClientRegex)) {
c.status(200); return c.html(Strings.ERROR_HTML, 200);
return c.html(Strings.ERROR_HTML);
} }
c.status(400);
/* We return it as a 200 so embedded applications can display the error */ /* We return it as a 200 so embedded applications can display the error */
return c.body(''); return c.body('', 400);
}); });
export default { export default {