Merge branch 'main' into fix/telegram-photo-size

This commit is contained in:
dangered wolf
2026-09-27 09:57:05 -07:00
committed by GitHub
7 changed files with 123 additions and 7 deletions
@@ -17,6 +17,12 @@ https://d.fxtwitter.com/user/status/123456789/photo/3
https://d.fixupx.com/user/status/123456789/video/2
```
For Bluesky image posts, use the same `/photo/N` suffix:
```
https://fxbsky.app/profile/user.bsky.social/post/abc123/photo/2
```
<img
src="/guide/readme/selectphoto.png"
alt="Discord embed showing a specific photo or video from a post"
+20 -4
View File
@@ -31,7 +31,7 @@
"@sentry/esbuild-plugin": "^5.4.0",
"@typescript-eslint/eslint-plugin": "^8.70.0",
"@typescript-eslint/parser": "^8.70.0",
"dotenv": "^17.4.2",
"dotenv": "^18.0.1",
"eslint": "^10.10.0",
"eslint-config-prettier": "^10.1.8",
"eslint-config-typescript": "^3.0.0",
@@ -2962,6 +2962,19 @@
}
}
},
"node_modules/@sentry/bundler-plugins/node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/@sentry/cli": {
"version": "2.58.6",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.6.tgz",
@@ -4499,11 +4512,14 @@
}
},
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"version": "18.0.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-18.0.1.tgz",
"integrity": "sha512-0eR4m4D/jH5eaI3evo2ZqqMii5mrTCR12v12VChdie6O1gPHs7XAjzTep0CWc1Bd+oNx3vVfbi734dK4zghigw==",
"dev": true,
"license": "BSD-2-Clause",
"bin": {
"dotenv": "dist/index.cjs"
},
"engines": {
"node": ">=12"
},
+1 -1
View File
@@ -42,7 +42,7 @@
"@sentry/esbuild-plugin": "^5.4.0",
"@typescript-eslint/eslint-plugin": "^8.70.0",
"@typescript-eslint/parser": "^8.70.0",
"dotenv": "^17.4.2",
"dotenv": "^18.0.1",
"eslint": "^10.10.0",
"eslint-config-prettier": "^10.1.8",
"eslint-config-typescript": "^3.0.0",
+7
View File
@@ -260,6 +260,13 @@ export const handleStatus = async (
console.log(api);
return returnError(c, Strings.ERROR_API_FAIL);
}
if (
provider === DataProvider.Bluesky &&
mediaNumber &&
!status.media?.photos?.[mediaNumber - 1]
) {
return c.text('Photo not found', 404);
}
/* Should sensitive statuses be allowed Instant View? */
let useIV = false;
+14
View File
@@ -19,7 +19,21 @@ bluesky.use(trimTrailingSlash());
bluesky.get('/owoembed', oembed);
bluesky.get('/api/v1/statuses/:snowcode', activityRequest);
bluesky.get('/:prefix/:handle/post/:id', blueskyStatusRequest);
bluesky.get('/:prefix/:handle/post/:id/photo/:mediaNumber{[1-9][0-9]*}', blueskyStatusRequest);
bluesky.get(
'/:prefix/:handle/post/:id/photo/:mediaNumber{[1-9][0-9]*}/:language',
blueskyStatusRequest
);
bluesky.get('/profile/:handle/post/:id', blueskyStatusRequest);
bluesky.get('/profile/:handle/post/:id/photo/:mediaNumber{[1-9][0-9]*}', blueskyStatusRequest);
bluesky.get(
'/profile/:handle/post/:id/photo/:mediaNumber{[1-9][0-9]*}/:language',
blueskyStatusRequest
);
bluesky.get(
'/:prefix/profile/:handle/post/:id/photo/:mediaNumber{[1-9][0-9]*}/:language',
blueskyStatusRequest
);
bluesky.get('/:prefix/profile/:handle/post/:id/:language', blueskyStatusRequest);
bluesky.get('/profile/:handle/post/:id/:language', blueskyStatusRequest);
bluesky.get('/profile/:handle/feed.xml', profileFeedRssBluesky);
+2 -2
View File
@@ -9,7 +9,7 @@ import { isHorizonEmbedParam } from '../../twitter/router';
export const blueskyStatusRequest = async (c: Context) => {
console.log('bluesky status request!!!');
const { handle, id, language } = c.req.param();
const { handle, id, mediaNumber, language } = c.req.param();
const actualId = id.match(/\w+/g)?.[0] ?? '';
const userAgent = c.req.header('User-Agent') || '';
@@ -73,7 +73,7 @@ export const blueskyStatusRequest = async (c: Context) => {
c,
actualId,
handle,
undefined, //mediaNumber ? parseInt(mediaNumber) : undefined,
mediaNumber ? parseInt(mediaNumber, 10) : undefined,
userAgent,
flags,
language,
+73
View File
@@ -0,0 +1,73 @@
import { afterEach, expect, test, vi } from 'vitest';
import { app } from '../src/worker';
import threadMultiImage from './fixtures/bluesky/thread-multi-image.json';
afterEach(() => vi.restoreAllMocks());
function mockMultiImageThread() {
vi.spyOn(globalThis, 'fetch').mockImplementation(async input => {
const url = typeof input === 'string' ? input : input.url;
if (url.includes('app.bsky.feed.getPostThread')) return Response.json(threadMultiImage);
if (url.includes('app.bsky.actor.getProfiles')) return Response.json({ profiles: [] });
if (url.includes('mosaic.fxbsky.app')) return Response.json({ ok: true });
throw new Error(`Unexpected fetch: ${url}`);
});
}
test.each([
['photo/1', 'https://cdn.bsky.app/full1'],
['photo/2', 'https://cdn.bsky.app/full2'],
['photo/2/en', 'https://cdn.bsky.app/full2']
])('Bluesky /%s embeds only the selected image', async (path, expected) => {
mockMultiImageThread();
const response = await app.request(`https://fxbsky.app/profile/pics.test/post/rkeypics/${path}`, {
headers: { 'User-Agent': 'Twitterbot' }
});
expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain(`<meta property="og:image" content="${expected}"`);
});
test('Bluesky prefixed localized photo URL embeds the selected image', async () => {
mockMultiImageThread();
const response = await app.request(
'https://fxbsky.app/bsky/profile/pics.test/post/rkeypics/photo/2/en',
{ headers: { 'User-Agent': 'Twitterbot' } }
);
expect(response.status).toBe(200);
const html = await response.text();
expect(html).toContain('<meta property="og:image" content="https://cdn.bsky.app/full2"');
expect(html).not.toContain('<meta property="og:image" content="https://cdn.bsky.app/full1"');
});
test('Bluesky /photo/N rejects an unavailable image', async () => {
mockMultiImageThread();
const response = await app.request(
'https://fxbsky.app/profile/pics.test/post/rkeypics/photo/99',
{
headers: { 'User-Agent': 'Twitterbot' }
}
);
expect(response.status).toBe(404);
});
test('Bluesky photo selection reaches the Discord activity response', async () => {
mockMultiImageThread();
const response = await app.request('https://fxbsky.app/profile/pics.test/post/rkeypics/photo/2', {
headers: { 'User-Agent': 'Discordbot/2.0' }
});
expect(response.status).toBe(200);
const html = await response.text();
const activityHref = html.match(
/<link href='([^']+)' rel='alternate' type='application\/activity\+json'/
)?.[1];
expect(activityHref).toBeDefined();
const snowcode = activityHref?.split('/').at(-1);
const activity = await app.request(`https://fxbsky.app/api/v1/statuses/${snowcode}`, {
headers: { 'User-Agent': 'Discordbot/2.0' }
});
expect(activity.status).toBe(200);
const body = await activity.text();
expect(body).toContain('https://cdn.bsky.app/full2');
expect(body).not.toContain('https://cdn.bsky.app/full1');
});