Skip to content

Commit f7c76ae

Browse files
committed
Adds support for malformed channel URLs (thx grok)
Addresses an issue where channel URLs from platforms like Grok/Twitter may omit the "@" prefix.
1 parent 2db0627 commit f7c76ae

File tree

5 files changed

+173
-8
lines changed

5 files changed

+173
-8
lines changed

ui/hocs/withResolvedClaimRender/view.jsx

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// @flow
22
import React from 'react';
3+
import { useHistory, useLocation } from 'react-router-dom';
34

45
import Spinner from 'component/spinner';
56
import Button from 'component/button';
@@ -34,11 +35,46 @@ type Props = {
3435
};
3536

3637
/**
37-
* HigherOrderComponent to resolve a claim and return the correct states for render, such as loading, failed, restricted, etc
38-
*
39-
* @param Component: FunctionalComponentParam
40-
* @returns {FunctionalComponent}
38+
* Checks if a web URL path might be missing the @ prefix for the channel name.
39+
* Returns a corrected path with @ if applicable, or null if not applicable.
40+
* This handles cases like "/channel:id/content:id" which should be "/@channel:id/content:id"
41+
* (e.g., malformed URLs from Grok/Twitter that omit the @)
4142
*/
43+
function getCorrectedChannelPath(pathname: string): ?string {
44+
try {
45+
// Remove leading slash
46+
const webPath = pathname.startsWith('/') ? pathname.slice(1) : pathname;
47+
48+
// Must have a slash with content after it (channel/content pattern)
49+
const slashIndex = webPath.indexOf('/');
50+
if (slashIndex === -1 || slashIndex === webPath.length - 1) {
51+
return null;
52+
}
53+
54+
const firstPart = webPath.substring(0, slashIndex);
55+
const secondPart = webPath.substring(slashIndex + 1);
56+
57+
// First part should not already start with @
58+
if (firstPart.startsWith('@')) {
59+
return null;
60+
}
61+
62+
// Both parts should have content (non-empty name before any modifiers)
63+
// Modifiers in web URLs use : instead of #
64+
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
65+
const secondNameMatch = secondPart.match(/^[^#:$*]+/);
66+
67+
if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
68+
return null;
69+
}
70+
71+
// Return the corrected path with @ prefix
72+
return `/@${webPath}`;
73+
} catch (e) {
74+
return null;
75+
}
76+
}
77+
4278
const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam) => {
4379
const ResolvedClaimRender = (props: Props) => {
4480
const {
@@ -66,8 +102,19 @@ const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam)
66102
...otherProps
67103
} = props;
68104

105+
const { replace: historyReplace } = useHistory();
106+
const location = useLocation();
69107
const { streamName, /* channelName, */ isChannel } = parseURI(uri);
70108

109+
// Check if the browser URL is missing @ prefix and redirect if needed
110+
// This handles malformed URLs from Grok/Twitter like "/channel:id/content:id"
111+
const correctedPath = getCorrectedChannelPath(location.pathname);
112+
React.useEffect(() => {
113+
if (correctedPath) {
114+
historyReplace(correctedPath + location.search + location.hash);
115+
}
116+
}, [correctedPath, historyReplace, location.search, location.hash]);
117+
71118
const claimIsRestricted = !claimIsMine && (geoRestriction !== null || isClaimBlackListed || isClaimFiltered);
72119

73120
const resolveRequired =
@@ -111,6 +158,11 @@ const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam)
111158
}
112159
}, [resolveRequired, resolveClaim]);
113160

161+
// If URL needs correction, show loading while redirect happens
162+
if (correctedPath) {
163+
return <LoadingSpinner text={__('Resolving...')} />;
164+
}
165+
114166
if (!hasClaim) {
115167
if (hasClaim === undefined) {
116168
return <LoadingSpinner text={__('Resolving...')} />;

web/src/html.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const {
2121
const { fetchStreamUrl } = require('./fetchStreamUrl');
2222
const { lbryProxy: Lbry } = require('../lbry');
2323
const { getHomepageJsonV1 } = require('./getHomepageJSON');
24-
const { buildURI, parseURI, normalizeClaimUrl } = require('./lbryURI');
24+
const { buildURI, parseURI, normalizeClaimUrl, getCorrectedChannelWebPath } = require('./lbryURI');
2525
const fs = require('fs');
2626
const PAGES = require('../../ui/constants/pages');
2727
const path = require('path');
@@ -663,9 +663,20 @@ async function getHtml(ctx) {
663663

664664
if (!requestPath.includes('$')) {
665665
let parsedUri, claimUri;
666+
let pathToUse = requestPath.slice(1);
667+
668+
// Check if URL is missing @ prefix (e.g., malformed URLs from Grok/Twitter)
669+
const correctedPath = getCorrectedChannelWebPath(pathToUse);
670+
if (correctedPath) {
671+
// Redirect to the corrected URL with @ prefix, preserving query string
672+
// Use encodeURI to handle special characters like parentheses
673+
const queryString = ctx.querystring ? `?${ctx.querystring}` : '';
674+
ctx.redirect(encodeURI(`/${correctedPath}`) + queryString);
675+
return;
676+
}
666677

667678
try {
668-
parsedUri = parseURI(normalizeClaimUrl(requestPath.slice(1)));
679+
parsedUri = parseURI(normalizeClaimUrl(pathToUse));
669680
claimUri = buildURI({ ...parsedUri, startTime: undefined }, true);
670681
} catch (err) {
671682
ctx.status = 404;

web/src/lbryURI.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,95 @@ function normalizeClaimUrl(url) {
359359
return normalizeURI(url.replace(/:/g, '#'));
360360
}
361361

362+
/**
363+
* Checks if a URI might be missing the @ prefix for the channel name.
364+
* Returns a corrected URI with @ if applicable, or null if not applicable.
365+
* This handles cases like "channel:id/content:id" which should be "@channel:id/content:id"
366+
* (e.g., malformed URLs from Grok/Twitter that omit the @)
367+
*/
368+
function getCorrectedChannelUri(uri) {
369+
try {
370+
// Check if the URI has a path separator (indicating channel/content pattern)
371+
// but doesn't have a channel name (meaning no @ was detected)
372+
const parsed = parseURI(uri);
373+
374+
// If it already has a channelName, the @ is present - no correction needed
375+
if (parsed.channelName) {
376+
return null;
377+
}
378+
379+
// Check if the raw URI contains a "/" followed by more content (channel/content pattern)
380+
// Remove lbry:// protocol for checking
381+
const uriWithoutProtocol = uri.replace(/^lbry:\/\//, '');
382+
const slashIndex = uriWithoutProtocol.indexOf('/');
383+
384+
// Must have a slash with content after it (channel/content pattern)
385+
if (slashIndex === -1 || slashIndex === uriWithoutProtocol.length - 1) {
386+
return null;
387+
}
388+
389+
const firstPart = uriWithoutProtocol.substring(0, slashIndex);
390+
const secondPart = uriWithoutProtocol.substring(slashIndex + 1);
391+
392+
// First part should not already start with @
393+
if (firstPart.startsWith('@')) {
394+
return null;
395+
}
396+
397+
// Both parts should have content (non-empty after any modifiers are stripped)
398+
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
399+
const secondNameMatch = secondPart.match(/^[^#:$*]+/);
400+
401+
if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
402+
return null;
403+
}
404+
405+
// Build the corrected URI with @ prefix
406+
return `lbry://@${uriWithoutProtocol}`;
407+
} catch (e) {
408+
return null;
409+
}
410+
}
411+
412+
/**
413+
* Checks if a web URL path might be missing the @ prefix for the channel name.
414+
* Returns a corrected path with @ if applicable, or null if not applicable.
415+
* This works on raw web paths BEFORE they are parsed into lbry:// URIs.
416+
*
417+
* Example: "JustMe:05/content:0" -> "@JustMe:05/content:0"
418+
*/
419+
function getCorrectedChannelWebPath(webPath) {
420+
try {
421+
// Must have a slash with content after it (channel/content pattern)
422+
const slashIndex = webPath.indexOf('/');
423+
if (slashIndex === -1 || slashIndex === webPath.length - 1) {
424+
return null;
425+
}
426+
427+
const firstPart = webPath.substring(0, slashIndex);
428+
const secondPart = webPath.substring(slashIndex + 1);
429+
430+
// First part should not already start with @
431+
if (firstPart.startsWith('@')) {
432+
return null;
433+
}
434+
435+
// Both parts should have content (non-empty name before any modifiers)
436+
// Modifiers in web URLs use : instead of #
437+
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
438+
const secondNameMatch = secondPart.match(/^[^#:$*]+/);
439+
440+
if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
441+
return null;
442+
}
443+
444+
// Return the corrected path with @ prefix
445+
return `@${webPath}`;
446+
} catch (e) {
447+
return null;
448+
}
449+
}
450+
362451
module.exports = {
363452
parseURI,
364453
buildURI,
@@ -370,4 +459,6 @@ module.exports = {
370459
isURIClaimable,
371460
splitBySeparator,
372461
convertToShareLink,
462+
getCorrectedChannelUri,
463+
getCorrectedChannelWebPath,
373464
};

web/src/oEmbed.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
escapeHtmlProperty,
1010
} = require('../../ui/util/web');
1111
const { lbryProxy: Lbry } = require('../lbry');
12+
const { getCorrectedChannelWebPath } = require('./lbryURI');
1213

1314
Lbry.setDaemonConnectionString(PROXY_URL);
1415

@@ -17,7 +18,14 @@ Lbry.setDaemonConnectionString(PROXY_URL);
1718
// ****************************************************************************
1819

1920
async function getClaim(requestUrl) {
20-
const uri = requestUrl.replace(`${URL}/`, 'lbry://');
21+
// Extract the path from the URL and check for missing @ prefix
22+
let webPath = requestUrl.replace(`${URL}/`, '');
23+
const correctedPath = getCorrectedChannelWebPath(webPath);
24+
if (correctedPath) {
25+
webPath = correctedPath;
26+
}
27+
28+
const uri = `lbry://${webPath}`;
2129

2230
let claim, error;
2331
try {

web/src/routes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,10 @@ router.get('*', async (ctx, next) => {
125125
}
126126

127127
const html = await getHtml(ctx);
128-
ctx.body = html;
128+
// Only set body if not already redirecting (3xx status)
129+
if (ctx.status < 300 || ctx.status >= 400) {
130+
ctx.body = html;
131+
}
129132
});
130133

131134
module.exports = router;

0 commit comments

Comments
 (0)