mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post form): publish twimg query-format links with path extension
Normalize pbs.twimg.com media URLs that use ?format= to their file-extension form at publish time without rewriting the input field.
This commit is contained in:
@@ -313,37 +313,57 @@ vi.mock('../../loading-ellipsis', () => ({
|
||||
default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string),
|
||||
}));
|
||||
|
||||
vi.mock('../../../lib/utils/media-utils', () => ({
|
||||
getDisplayMediaInfoType: (type: string, t: (key: string) => string) => t(type),
|
||||
getLinkMediaInfo: (link: string) => {
|
||||
if (link.endsWith('.gif')) {
|
||||
return { type: 'gif', url: link };
|
||||
}
|
||||
if (link.endsWith('.jpg') || link.endsWith('.jpeg') || link.endsWith('.png')) {
|
||||
return { type: 'image', url: link };
|
||||
}
|
||||
if (link.endsWith('.mp4')) {
|
||||
return { type: 'video', url: link };
|
||||
}
|
||||
if (link.endsWith('.mp3')) {
|
||||
return { type: 'audio', url: link };
|
||||
}
|
||||
if (link.includes('youtube.com')) {
|
||||
return { patternThumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg', type: 'iframe', url: link };
|
||||
}
|
||||
return { type: 'webpage', url: link };
|
||||
},
|
||||
getYouTubeThumbnailUrlFromLink: (link: string) => {
|
||||
vi.mock('../../../lib/utils/media-utils', () => {
|
||||
const getTwimgMediaFilePublishUrl = (link: string) => {
|
||||
try {
|
||||
const url = new URL(link);
|
||||
if (!url.hostname.includes('youtube.com')) return undefined;
|
||||
const videoId = url.searchParams.get('v');
|
||||
return videoId ? `https://img.youtube.com/vi/${videoId}/0.jpg` : undefined;
|
||||
const url = new URL(link.trim());
|
||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||
const [mediaPath, mediaId] = pathParts;
|
||||
const format = url.searchParams.get('format')?.toLowerCase();
|
||||
if (url.hostname !== 'pbs.twimg.com' || pathParts.length !== 2 || mediaPath !== 'media' || !mediaId || mediaId.includes('.')) return undefined;
|
||||
if (!format || !['jpg', 'jpeg', 'png', 'webp', 'gif'].includes(format)) return undefined;
|
||||
return `https://pbs.twimg.com/media/${mediaId}.${format}`;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
getDisplayMediaInfoType: (type: string, t: (key: string) => string) => t(type),
|
||||
getLinkMediaInfo: (link: string) => {
|
||||
if (getTwimgMediaFilePublishUrl(link)) {
|
||||
return { type: 'image', url: link };
|
||||
}
|
||||
if (link.endsWith('.gif')) {
|
||||
return { type: 'gif', url: link };
|
||||
}
|
||||
if (link.endsWith('.jpg') || link.endsWith('.jpeg') || link.endsWith('.png')) {
|
||||
return { type: 'image', url: link };
|
||||
}
|
||||
if (link.endsWith('.mp4')) {
|
||||
return { type: 'video', url: link };
|
||||
}
|
||||
if (link.endsWith('.mp3')) {
|
||||
return { type: 'audio', url: link };
|
||||
}
|
||||
if (link.includes('youtube.com')) {
|
||||
return { patternThumbnailUrl: 'https://img.youtube.com/vi/abc123/0.jpg', type: 'iframe', url: link };
|
||||
}
|
||||
return { type: 'webpage', url: link };
|
||||
},
|
||||
getTwimgMediaFilePublishUrl,
|
||||
getYouTubeThumbnailUrlFromLink: (link: string) => {
|
||||
try {
|
||||
const url = new URL(link);
|
||||
if (!url.hostname.includes('youtube.com')) return undefined;
|
||||
const videoId = url.searchParams.get('v');
|
||||
return videoId ? `https://img.youtube.com/vi/${videoId}/0.jpg` : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../lib/media-hosting/show-upload-controls', () => ({
|
||||
getMediaHostingRuntime: () => testState.mediaHostingRuntime,
|
||||
@@ -718,6 +738,32 @@ describe('PostForm', () => {
|
||||
expect(testState.publishedPostOptions?.content).toBe(youtubeLink);
|
||||
});
|
||||
|
||||
it('publishes known twimg query-format post links with a path extension without editing the field', async () => {
|
||||
const twimgLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU?format=jpg&name=medium';
|
||||
const publishLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU.jpg';
|
||||
|
||||
await renderPostForm('/all');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table') as HTMLTableElement;
|
||||
const select = table.querySelector('select') as HTMLSelectElement;
|
||||
const textarea = table.querySelector('textarea') as HTMLTextAreaElement;
|
||||
const linkInput = table.querySelectorAll<HTMLInputElement>('input[type="text"]')[3];
|
||||
|
||||
await dispatchChange(select, 'music-posting.eth');
|
||||
await dispatchInput(textarea, 'Twimg thread');
|
||||
await dispatchInput(linkInput, twimgLink);
|
||||
await clickByText(table, 'post');
|
||||
|
||||
expect(linkInput.value).toBe(twimgLink);
|
||||
expect(testState.publishPostMock).toHaveBeenCalledTimes(1);
|
||||
expect(testState.publishPostMock).toHaveBeenCalledWith({
|
||||
content: 'Twimg thread',
|
||||
link: publishLink,
|
||||
});
|
||||
expect(testState.publishedPostOptions?.link).toBe(publishLink);
|
||||
});
|
||||
|
||||
it('shows Oekaki draw controls only on the /i/ board form', async () => {
|
||||
testState.directories.push({
|
||||
address: 'oekaki-posting.bso',
|
||||
@@ -1430,6 +1476,36 @@ describe('PostForm', () => {
|
||||
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('publishes known twimg query-format reply links with a path extension without editing the field', async () => {
|
||||
const twimgLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU?format=jpg&name=medium';
|
||||
const publishLink = 'https://pbs.twimg.com/media/HJxnhNKWMAAhqFU.jpg';
|
||||
|
||||
testState.comments = {
|
||||
'thread-cid': {
|
||||
postCid: 'thread-cid',
|
||||
},
|
||||
};
|
||||
testState.resolvedCommunityAddress = 'music-posting.eth';
|
||||
|
||||
await renderPostForm('/mu/thread/thread-cid');
|
||||
await clickByText(container, 'post_a_reply');
|
||||
|
||||
const table = container.querySelector('table') as HTMLTableElement;
|
||||
const textarea = table.querySelector('textarea') as HTMLTextAreaElement;
|
||||
const linkInput = table.querySelectorAll<HTMLInputElement>('input[type="text"]')[2];
|
||||
|
||||
await dispatchInput(textarea, 'Twimg reply');
|
||||
await dispatchInput(linkInput, twimgLink);
|
||||
await clickByText(table, 'post');
|
||||
|
||||
expect(linkInput.value).toBe(twimgLink);
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledTimes(1);
|
||||
expect(testState.publishReplyMock).toHaveBeenCalledWith({
|
||||
content: 'Twimg reply',
|
||||
link: publishLink,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LinkTypePreviewer', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Comment, setAccount, useAccount, useEditedComment } from '@bitsocial/bitsocial-react-hooks';
|
||||
import getShortAddress from '../../lib/get-short-address';
|
||||
import useCommunitiesPagesStore from '@bitsocial/bitsocial-react-hooks/dist/stores/communities-pages';
|
||||
import { getDisplayMediaInfoType, getLinkMediaInfo } from '../../lib/utils/media-utils';
|
||||
import { getDisplayMediaInfoType, getLinkMediaInfo, getTwimgMediaFilePublishUrl } from '../../lib/utils/media-utils';
|
||||
import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation-utils';
|
||||
import {
|
||||
type DiceRoll,
|
||||
@@ -72,6 +72,15 @@ const isPostFormFileMediaType = (type: string | undefined): boolean => Boolean(t
|
||||
|
||||
const isPostFormFileMediaLink = (link: string): boolean => isPostFormFileMediaType(getLinkMediaInfo(link)?.type);
|
||||
|
||||
const getPublishLinkOptions = (link: string, includeCurrentLink: boolean): Partial<Pick<Comment, 'link'>> => {
|
||||
const twimgPublishUrl = getTwimgMediaFilePublishUrl(link);
|
||||
if (twimgPublishUrl) {
|
||||
return { link: twimgPublishUrl };
|
||||
}
|
||||
|
||||
return includeCurrentLink && link ? { link } : {};
|
||||
};
|
||||
|
||||
export const LinkTypePreviewer = ({ link, requireFile = false }: { link: string; requireFile?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const mediaInfo = getLinkMediaInfo(link);
|
||||
@@ -673,7 +682,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
};
|
||||
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? getBoardIndexPath() : null;
|
||||
publishPost({ content: publishContent, ...(appliedYouTubeConversion ? { link: currentUrl } : {}), ...publishOptions });
|
||||
publishPost({ content: publishContent, ...getPublishLinkOptions(currentUrl, appliedYouTubeConversion), ...publishOptions });
|
||||
};
|
||||
|
||||
// redirect to pending page when pending comment is created
|
||||
@@ -790,7 +799,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const flagPublishOptions = getCommentFlagPublishOptionsForDirectory(directoryEntry, flagRef.current?.value);
|
||||
|
||||
nonokoRedirectPathRef.current = hasNonokoOption(currentOptions) ? getBoardIndexPath() : null;
|
||||
publishReply({ content: publishContent, ...(appliedYouTubeConversion ? { link: currentUrl } : {}), ...flagPublishOptions });
|
||||
publishReply({ content: publishContent, ...getPublishLinkOptions(currentUrl, appliedYouTubeConversion), ...flagPublishOptions });
|
||||
};
|
||||
|
||||
const setLinkValue = (nextUrl: string) => {
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
getLinkMediaInfo,
|
||||
getMediaDimensions,
|
||||
getPostMediaTypeLabel,
|
||||
getTwimgMediaFilePublishUrl,
|
||||
getYouTubeEmbedPostMediaFileLink,
|
||||
getYouTubeThumbnailUrlFromLink,
|
||||
} from '../media-utils';
|
||||
@@ -192,6 +193,14 @@ describe('media-utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes known twimg query-format media links for publishing', () => {
|
||||
expect(getTwimgMediaFilePublishUrl('https://pbs.twimg.com/media/HJxnhNKWMAAhqFU?format=jpg&name=medium')).toBe('https://pbs.twimg.com/media/HJxnhNKWMAAhqFU.jpg');
|
||||
expect(getTwimgMediaFilePublishUrl('http://pbs.twimg.com/media/HJxnhNKWMAAhqFU?format=PNG&name=small')).toBe('https://pbs.twimg.com/media/HJxnhNKWMAAhqFU.png');
|
||||
expect(getTwimgMediaFilePublishUrl('https://pbs.twimg.com/media/HJxnhNKWMAAhqFU.jpg?format=png&name=medium')).toBeUndefined();
|
||||
expect(getTwimgMediaFilePublishUrl('https://example.com/media/HJxnhNKWMAAhqFU?format=jpg&name=medium')).toBeUndefined();
|
||||
expect(getTwimgMediaFilePublishUrl('https://pbs.twimg.com/media/HJxnhNKWMAAhqFU?format=txt&name=medium')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('builds comment media info and strips thumbnails for blacklisted domains', () => {
|
||||
testState.canEmbedHosts = new Set(['www.youtube.com']);
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ const KNOWN_VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'flv', 'wmv'
|
||||
const KNOWN_AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'];
|
||||
const KNOWN_SWF_EXTENSIONS = ['swf'];
|
||||
const KNOWN_MEDIA_EXTENSIONS = new Set([...KNOWN_IMAGE_EXTENSIONS, ...KNOWN_VIDEO_EXTENSIONS, ...KNOWN_AUDIO_EXTENSIONS, ...KNOWN_SWF_EXTENSIONS]);
|
||||
const TWIMG_MEDIA_FORMAT_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'gif']);
|
||||
|
||||
// some sites don't show thumbnails, so the backend-side thumbnail fetching needs to be disabled, or it might fetch non-thumbnails such as emojis
|
||||
const THUMBNAIL_BLACKLISTED_DOMAINS = ['twitter.com', 'x.com'];
|
||||
@@ -157,6 +158,27 @@ const getDirectMediaExtension = (url: URL): string => {
|
||||
return pathParts.length > 1 ? pathParts[pathParts.length - 1] : '';
|
||||
};
|
||||
|
||||
export const getTwimgMediaFilePublishUrl = (link: string): string | undefined => {
|
||||
const url = parseHttpUrl(link.trim());
|
||||
if (!url || url.hostname.toLowerCase() !== 'pbs.twimg.com') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const pathParts = url.pathname.split('/').filter(Boolean);
|
||||
const [mediaPath, mediaId] = pathParts;
|
||||
if (pathParts.length !== 2 || mediaPath !== 'media' || !mediaId || mediaId.includes('.')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const format = url.searchParams.get('format')?.toLowerCase();
|
||||
if (!format || !TWIMG_MEDIA_FORMAT_EXTENSIONS.has(format)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
url.protocol = 'https:';
|
||||
return `${url.origin}/media/${mediaId}.${format}`;
|
||||
};
|
||||
|
||||
export const getLinkMediaInfo = memoize(
|
||||
(link: string): CommentMediaInfo | undefined => {
|
||||
if (!isValidURL(link)) {
|
||||
|
||||
Reference in New Issue
Block a user