This commit is contained in:
dangered wolf 2024-05-01 00:33:20 -04:00
commit 0fc0fc3fca
No known key found for this signature in database
GPG key ID: 41E4D37680ED8B58
13 changed files with 55 additions and 53 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

@ -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 {