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 ccc188b..7be2c24 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/render/instantview.ts b/src/render/instantview.ts
index cd1c787..a4e82d1 100644
--- a/src/render/instantview.ts
+++ b/src/render/instantview.ts
@@ -322,8 +322,8 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc
let previousThreadPieceAuthor: string | null = null;
let originalAuthor: string | null = null;
-
- const useThread = thread?.thread ?? [ thread?.status ]
+
+ const useThread = thread?.thread ?? [thread?.status];
if (!status) {
throw new Error('Status is undefined');
@@ -359,7 +359,8 @@ export const renderInstantView = (properties: RenderProperties): ResponseInstruc
${i18next.t('ivViewOriginal')}