diff --git a/README.md b/README.md index aa11bb8..41a68a9 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ ![][icons] +[![Crowdin][crowdinbadge]][crowdin] [![esbuild][buildbadge]][build] [![Tests][testsbadge]][tests] [![Status][statusbadge]][status] @@ -20,6 +21,8 @@ [licensebadge]: https://img.shields.io/github/license/FixTweet/FxTwitter [status]: https://status.fxtwitter.com [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). diff --git a/package-lock.json b/package-lock.json index 404b1c1..50c7fe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,9 @@ "license": "MIT", "dependencies": { "@hono/sentry": "^1.0.1", - "hono": "^3.12.12", "i18next": "^23.8.2", - "i18next-icu": "^2.3.0" + "i18next-icu": "^2.3.0", + "hono": "^4.2.9" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240423.0", @@ -34,7 +34,7 @@ "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", "typescript": "^5.4.5", - "wrangler": "^3.52.0" + "wrangler": "^3.53.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1752,7 +1752,7 @@ "@miniflare/core": "2.14.2", "@miniflare/shared": "2.14.2", "http-cache-semantics": "^4.1.0", - "undici": "5.28.2" + "undici": "5.28.4" }, "engines": { "node": ">=16.13" @@ -1772,7 +1772,7 @@ "dotenv": "^10.0.0", "kleur": "^4.1.4", "set-cookie-parser": "^2.4.8", - "undici": "5.28.2", + "undici": "5.28.4", "urlpattern-polyfill": "^4.0.3" }, "engines": { @@ -1810,7 +1810,7 @@ "@miniflare/core": "2.14.2", "@miniflare/shared": "2.14.2", "@miniflare/storage-memory": "2.14.2", - "undici": "5.28.2" + "undici": "5.28.4" }, "engines": { "node": ">=16.13" @@ -1825,7 +1825,7 @@ "@miniflare/core": "2.14.2", "@miniflare/shared": "2.14.2", "html-rewriter-wasm": "^0.4.1", - "undici": "5.28.2" + "undici": "5.28.4" }, "engines": { "node": ">=16.13" @@ -1863,7 +1863,7 @@ "dependencies": { "@miniflare/core": "2.14.2", "@miniflare/shared": "2.14.2", - "undici": "5.28.2" + "undici": "5.28.4" }, "engines": { "node": ">=16.13" @@ -1979,7 +1979,7 @@ "dependencies": { "@miniflare/core": "2.14.2", "@miniflare/shared": "2.14.2", - "undici": "5.28.2", + "undici": "5.28.4", "ws": "^8.2.2" }, "engines": { @@ -4549,9 +4549,9 @@ } }, "node_modules/hono": { - "version": "3.12.12", - "resolved": "https://registry.npmjs.org/hono/-/hono-3.12.12.tgz", - "integrity": "sha512-5IAMJOXfpA5nT+K0MNjClchzz0IhBHs2Szl7WFAhrFOsbtQsYmNynFyJRg/a3IPsmCfxcrf8txUGiNShXpK5Rg==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.2.9.tgz", + "integrity": "sha512-59FAv52UxDWUt/NlC0NzrRCjeVCThUnVlqlrKYm+k80XujBu6uJwBIa5gACKKZWobjA0MJ6Vds0I3URKf383Cw==", "engines": { "node": ">=16.0.0" } @@ -6970,7 +6970,7 @@ "exit-hook": "^2.2.1", "glob-to-regexp": "^0.4.1", "stoppable": "^1.1.0", - "undici": "^5.28.2", + "undici": "^5.28.4", "workerd": "1.20240419.0", "ws": "^8.11.0", "youch": "^3.2.2", @@ -8766,9 +8766,9 @@ } }, "node_modules/undici": { - "version": "5.28.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", - "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" @@ -9047,9 +9047,9 @@ } }, "node_modules/wrangler": { - "version": "3.52.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.52.0.tgz", - "integrity": "sha512-HR06jTym+yr7+CI3Ggld3nfp1OM9vSC7h4B8vwWHwhi5K0sYg8p44rxV514Gmsv9dkFHegkRP70SM3sjuuxxpQ==", + "version": "3.53.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.53.0.tgz", + "integrity": "sha512-JxkvCQekL9j8Mu4CEKM/HEVyDnymWzKQuMUuJH0yum1AilutD5HAP9kVVYmvu7BvwlRyRUAj8TI5OUxXnLCEpQ==", "dev": true, "dependencies": { "@cloudflare/kv-asset-handler": "0.3.2", diff --git a/package.json b/package.json index f987eb8..0c58bd2 100644 --- a/package.json +++ b/package.json @@ -35,12 +35,12 @@ "ts-jest": "^29.1.2", "ts-loader": "^9.5.1", "typescript": "^5.4.5", - "wrangler": "^3.52.0" + "wrangler": "^3.53.0" }, "dependencies": { "@hono/sentry": "^1.0.1", - "hono": "^3.12.12", "i18next": "^23.8.2", - "i18next-icu": "^2.3.0" + "i18next-icu": "^2.3.0", + "hono": "^4.2.9" } } diff --git a/src/caches.ts b/src/caches.ts index a9c1229..2321e9a 100644 --- a/src/caches.ts +++ b/src/caches.ts @@ -90,11 +90,11 @@ export const cacheMiddleware = (): MiddlewareHandler => async (c, next) => { /* We properly state our OPTIONS when asked */ case 'OPTIONS': c.header('allow', Constants.RESPONSE_HEADERS.allow); + c.body(null); c.status(204); return; default: - c.status(405); if (returnAsJson) return c.json(''); - return c.html(''); + return c.html('', 405); } }; diff --git a/src/embed/status.ts b/src/embed/status.ts index 5746927..7d6fece 100644 --- a/src/embed/status.ts +++ b/src/embed/status.ts @@ -1,4 +1,7 @@ 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 { handleQuote } from '../helpers/quote'; import { formatNumber, sanitizeText, truncateWithEllipsis } from '../helpers/utils'; @@ -9,8 +12,6 @@ import { renderVideo } from '../render/video'; import { renderInstantView } from '../render/instantview'; import { constructTwitterThread } from '../providers/twitter/conversation'; import { Experiment, experimentCheck } from '../experiments'; -import i18next from 'i18next'; -import icu from 'i18next-icu'; import translationResources from '../../i18n/resources.json'; 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 */ if (flags?.api) { - c.status(api.code); + c.status(api.code as StatusCode); // Add every header from Constants.API_RESPONSE_HEADERS for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) { c.header(header, value); diff --git a/src/providers/twitter/conversation.ts b/src/providers/twitter/conversation.ts index c61cc60..5aff2b6 100644 --- a/src/providers/twitter/conversation.ts +++ b/src/providers/twitter/conversation.ts @@ -4,6 +4,7 @@ import { buildAPITwitterStatus } from './processor'; import { Experiment, experimentCheck } from '../../experiments'; import { isGraphQLTwitterStatus } from '../../helpers/graphql'; import { Context } from 'hono'; +import { StatusCode } from 'hono/utils/http-status'; const writeDataPoint = ( c: Context, @@ -579,10 +580,9 @@ export const threadAPIProvider = async (c: Context) => { const processedResponse = await constructTwitterThread(id, true, c, undefined); - c.status(processedResponse.code); // Add every header from Constants.API_RESPONSE_HEADERS for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) { c.header(header, value); } - return c.json(processedResponse); + return c.json(processedResponse, processedResponse.code as StatusCode); }; diff --git a/src/realms/api/hit.ts b/src/realms/api/hit.ts index 4d8db8e..99822c2 100644 --- a/src/realms/api/hit.ts +++ b/src/realms/api/hit.ts @@ -5,7 +5,7 @@ export const linkHitRequest = async (c: Context) => { const userAgent = c.req.header('User-Agent') || ''; if (userAgent.includes('Telegram')) { - c.status(403); + return c.text('', 403); } // If param `url` exists, 302 redirect to it if (typeof c.req.query('url') === 'string') { diff --git a/src/realms/api/router.ts b/src/realms/api/router.ts index da08d56..d0d067e 100644 --- a/src/realms/api/router.ts +++ b/src/realms/api/router.ts @@ -9,11 +9,13 @@ export const api = new Hono(); api.use('*', async (c, next) => { if (!c.req.header('user-agent')) { - c.status(401); - return c.json({ - 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." - }); + return c.json( + { + 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." + }, + 401 + ); } await next(); }); diff --git a/src/realms/twitter/routes/oembed.ts b/src/realms/twitter/routes/oembed.ts index c822b81..7cdb7af 100644 --- a/src/realms/twitter/routes/oembed.ts +++ b/src/realms/twitter/routes/oembed.ts @@ -28,7 +28,6 @@ export const oembed = async (c: Context) => { }; c.header('content-type', 'application/json'); - c.status(200); /* Stringify and send it on its way! */ - return c.text(JSON.stringify(data)); + return c.text(JSON.stringify(data), 200); }; diff --git a/src/realms/twitter/routes/redirects.ts b/src/realms/twitter/routes/redirects.ts index cfff1f2..fc711b8 100644 --- a/src/realms/twitter/routes/redirects.ts +++ b/src/realms/twitter/routes/redirects.ts @@ -25,12 +25,11 @@ export const setRedirectRequest = async (c: Context) => { /* Check that origin either does not exist or is in our domain list */ const origin = c.req.header('origin'); if (origin && !Constants.STANDARD_DOMAIN_LIST.includes(new URL(origin).hostname)) { - c.status(403); - return c.html( 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.` - }) + }), + 403 ); } @@ -46,11 +45,11 @@ export const setRedirectRequest = async (c: Context) => { 'content-security-policy', `frame-ancestors ${Constants.STANDARD_DOMAIN_LIST.join(' ')};` ); - c.status(200); return c.html( Strings.MESSAGE_HTML.format({ message: `Your base redirect has been cleared. To set one, please pass along the url parameter.` - }) + }), + 200 ); } @@ -71,11 +70,11 @@ export const setRedirectRequest = async (c: Context) => { 'content-security-policy', `frame-ancestors ${Constants.STANDARD_DOMAIN_LIST.join(' ')};` ); - c.status(200); return c.html( Strings.MESSAGE_HTML.format({ message: `Your URL does not appear to be well-formed. Example: ?url=https://nitter.net` - }) + }), + 200 ); } diff --git a/src/realms/twitter/routes/status.ts b/src/realms/twitter/routes/status.ts index dea9c21..1337e0c 100644 --- a/src/realms/twitter/routes/status.ts +++ b/src/realms/twitter/routes/status.ts @@ -131,8 +131,7 @@ export const statusRequest = async (c: Context) => { return statusResponse; } else { /* 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); + return c.text(Strings.ERROR_UNKNOWN, 500); } } else { /* A human has clicked a fxtwitter.com/:screen_name/status/:id link! diff --git a/src/user.ts b/src/user.ts index 71d6503..9781345 100644 --- a/src/user.ts +++ b/src/user.ts @@ -2,6 +2,7 @@ import { Context } from 'hono'; import { Constants } from './constants'; import { Strings } from './strings'; import { userAPI } from './providers/twitter/profile'; +import { StatusCode } from 'hono/utils/http-status'; export const returnError = (c: Context, error: string): Response => { return c.html( @@ -29,7 +30,7 @@ export const handleProfile = async ( /* Catch this request if it's an API response */ // For now we just always return the API response while testing if (flags?.api) { - c.status(api.code); + c.status(api.code as StatusCode); // Add every header from Constants.API_RESPONSE_HEADERS for (const [header, value] of Object.entries(Constants.API_RESPONSE_HEADERS)) { c.header(header, value); diff --git a/src/worker.ts b/src/worker.ts index 3d5ab74..f162405 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -8,6 +8,7 @@ import { Constants } from './constants'; import { api } from './realms/api/router'; import { twitter } from './realms/twitter/router'; import { cacheMiddleware } from './caches'; +import { StatusCode } from 'hono/utils/http-status'; const noCache = 'max-age=0, no-cache, no-store, must-revalidate'; const embeddingClientRegex = @@ -89,10 +90,9 @@ app.onError((err, c) => { if (c.req.header('User-Agent')?.match(embeddingClientRegex)) { errorCode = 200; } - c.status(errorCode); 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[]) => { @@ -138,12 +138,10 @@ app.all('/error', async c => { c.header('cache-control', noCache); if (c.req.header('User-Agent')?.match(embeddingClientRegex)) { - c.status(200); - return c.html(Strings.ERROR_HTML); + return c.html(Strings.ERROR_HTML, 200); } - c.status(400); /* We return it as a 200 so embedded applications can display the error */ - return c.body(''); + return c.body('', 400); }); export default {