Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions ui/hocs/withResolvedClaimRender/view.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @flow
import React from 'react';
import { useHistory, useLocation } from 'react-router-dom';

import Spinner from 'component/spinner';
import Button from 'component/button';
Expand Down Expand Up @@ -34,11 +35,46 @@ type Props = {
};

/**
* HigherOrderComponent to resolve a claim and return the correct states for render, such as loading, failed, restricted, etc
*
* @param Component: FunctionalComponentParam
* @returns {FunctionalComponent}
* Checks if a web URL path might be missing the @ prefix for the channel name.
* Returns a corrected path with @ if applicable, or null if not applicable.
* This handles cases like "/channel:id/content:id" which should be "/@channel:id/content:id"
* (e.g., malformed URLs from Grok/Twitter that omit the @)
*/
function getCorrectedChannelPath(pathname: string): ?string {
try {
// Remove leading slash
const webPath = pathname.startsWith('/') ? pathname.slice(1) : pathname;

// Must have a slash with content after it (channel/content pattern)
const slashIndex = webPath.indexOf('/');
if (slashIndex === -1 || slashIndex === webPath.length - 1) {
return null;
}

const firstPart = webPath.substring(0, slashIndex);
const secondPart = webPath.substring(slashIndex + 1);

// First part should not already start with @
if (firstPart.startsWith('@')) {
return null;
}

// Both parts should have content (non-empty name before any modifiers)
// Modifiers in web URLs use : instead of #
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
const secondNameMatch = secondPart.match(/^[^#:$*]+/);

if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
return null;
}

// Return the corrected path with @ prefix
return `/@${webPath}`;
} catch (e) {
return null;
}
}
Comment on lines +43 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Heuristic may false-positive on non-channel two-segment claim paths.

getCorrectedChannelPath adds @ to any "name/name" path where the first segment lacks @. If a legitimate non-channel claim ever has a nested path pattern (without @), this would incorrectly redirect it. The risk is low since this HOC is only rendered for claim pages and LBRY channel/content paths use the @channel/content convention, but it's worth documenting this assumption in the function's JSDoc.

🧰 Tools
🪛 Biome (2.3.13)

[error] 43-43: Type annotations are a TypeScript only feature. Convert your file to a TypeScript file or remove the syntax.

TypeScript only syntax

(parse)


[error] 43-43: return types can only be used in TypeScript files

(parse)


[error] 43-43: Expected a function body but instead found '?'.

Expected a function body here.

(parse)


[error] 51-51: Illegal return statement outside of a function

(parse)


[error] 59-59: Illegal return statement outside of a function

(parse)


[error] 68-68: Illegal return statement outside of a function

(parse)


[error] 72-72: Illegal return statement outside of a function

(parse)


[error] 74-74: Illegal return statement outside of a function

(parse)

🤖 Prompt for AI Agents
In `@ui/hocs/withResolvedClaimRender/view.jsx` around lines 43 - 76, The function
getCorrectedChannelPath currently treats any two-segment path like "name/name"
as a channel by prepending "@", which can false-positive for legitimate
non-channel nested claim paths; update the function by adding a clear JSDoc
comment above getCorrectedChannelPath that documents the assumption (this HOC
only runs on claim pages, and any two-segment path missing "@" will be treated
as a channel/content path and auto-prefixed), describe the expected
input/outputs and the low-risk caveat about possible non-channel nested paths,
and include an example or note that callers should avoid invoking this function
for known non-channel routes.


const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam) => {
const ResolvedClaimRender = (props: Props) => {
const {
Expand Down Expand Up @@ -66,8 +102,19 @@ const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam)
...otherProps
} = props;

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

// Check if the browser URL is missing @ prefix and redirect if needed
// This handles malformed URLs from Grok/Twitter like "/channel:id/content:id"
const correctedPath = getCorrectedChannelPath(location.pathname);
React.useEffect(() => {
if (correctedPath) {
historyReplace(correctedPath + location.search + location.hash);
}
}, [correctedPath, historyReplace, location.search, location.hash]);

const claimIsRestricted = !claimIsMine && (geoRestriction !== null || isClaimBlackListed || isClaimFiltered);

const resolveRequired =
Expand Down Expand Up @@ -111,6 +158,11 @@ const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam)
}
}, [resolveRequired, resolveClaim]);

// If URL needs correction, show loading while redirect happens
if (correctedPath) {
return <LoadingSpinner text={__('Resolving...')} />;
}

if (!hasClaim) {
if (hasClaim === undefined) {
return <LoadingSpinner text={__('Resolving...')} />;
Expand Down
15 changes: 13 additions & 2 deletions web/src/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const {
const { fetchStreamUrl } = require('./fetchStreamUrl');
const { lbryProxy: Lbry } = require('../lbry');
const { getHomepageJsonV1 } = require('./getHomepageJSON');
const { buildURI, parseURI, normalizeClaimUrl } = require('./lbryURI');
const { buildURI, parseURI, normalizeClaimUrl, getCorrectedChannelWebPath } = require('./lbryURI');
const fs = require('fs');
const PAGES = require('../../ui/constants/pages');
const path = require('path');
Expand Down Expand Up @@ -663,9 +663,20 @@ async function getHtml(ctx) {

if (!requestPath.includes('$')) {
let parsedUri, claimUri;
let pathToUse = requestPath.slice(1);

// Check if URL is missing @ prefix (e.g., malformed URLs from Grok/Twitter)
const correctedPath = getCorrectedChannelWebPath(pathToUse);
if (correctedPath) {
// Redirect to the corrected URL with @ prefix, preserving query string
// Use encodeURI to handle special characters like parentheses
const queryString = ctx.querystring ? `?${ctx.querystring}` : '';
ctx.redirect(encodeURI(`/${correctedPath}`) + queryString);
return;
}

try {
parsedUri = parseURI(normalizeClaimUrl(requestPath.slice(1)));
parsedUri = parseURI(normalizeClaimUrl(pathToUse));
claimUri = buildURI({ ...parsedUri, startTime: undefined }, true);
} catch (err) {
ctx.status = 404;
Expand Down
91 changes: 91 additions & 0 deletions web/src/lbryURI.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,95 @@ function normalizeClaimUrl(url) {
return normalizeURI(url.replace(/:/g, '#'));
}

/**
* Checks if a URI might be missing the @ prefix for the channel name.
* Returns a corrected URI with @ if applicable, or null if not applicable.
* This handles cases like "channel:id/content:id" which should be "@channel:id/content:id"
* (e.g., malformed URLs from Grok/Twitter that omit the @)
*/
function getCorrectedChannelUri(uri) {
try {
// Check if the URI has a path separator (indicating channel/content pattern)
// but doesn't have a channel name (meaning no @ was detected)
const parsed = parseURI(uri);

// If it already has a channelName, the @ is present - no correction needed
if (parsed.channelName) {
return null;
}

// Check if the raw URI contains a "/" followed by more content (channel/content pattern)
// Remove lbry:// protocol for checking
const uriWithoutProtocol = uri.replace(/^lbry:\/\//, '');
const slashIndex = uriWithoutProtocol.indexOf('/');

// Must have a slash with content after it (channel/content pattern)
if (slashIndex === -1 || slashIndex === uriWithoutProtocol.length - 1) {
return null;
}

const firstPart = uriWithoutProtocol.substring(0, slashIndex);
const secondPart = uriWithoutProtocol.substring(slashIndex + 1);

// First part should not already start with @
if (firstPart.startsWith('@')) {
return null;
}

// Both parts should have content (non-empty after any modifiers are stripped)
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
const secondNameMatch = secondPart.match(/^[^#:$*]+/);

if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
return null;
}

// Build the corrected URI with @ prefix
return `lbry://@${uriWithoutProtocol}`;
} catch (e) {
return null;
}
}

/**
* Checks if a web URL path might be missing the @ prefix for the channel name.
* Returns a corrected path with @ if applicable, or null if not applicable.
* This works on raw web paths BEFORE they are parsed into lbry:// URIs.
*
* Example: "JustMe:05/content:0" -> "@JustMe:05/content:0"
*/
function getCorrectedChannelWebPath(webPath) {
try {
// Must have a slash with content after it (channel/content pattern)
const slashIndex = webPath.indexOf('/');
if (slashIndex === -1 || slashIndex === webPath.length - 1) {
return null;
}

const firstPart = webPath.substring(0, slashIndex);
const secondPart = webPath.substring(slashIndex + 1);

// First part should not already start with @
if (firstPart.startsWith('@')) {
return null;
}

// Both parts should have content (non-empty name before any modifiers)
// Modifiers in web URLs use : instead of #
const firstNameMatch = firstPart.match(/^[^#:$*]+/);
const secondNameMatch = secondPart.match(/^[^#:$*]+/);

if (!firstNameMatch || !firstNameMatch[0] || !secondNameMatch || !secondNameMatch[0]) {
return null;
}

// Return the corrected path with @ prefix
return `@${webPath}`;
} catch (e) {
return null;
}
}

module.exports = {
parseURI,
buildURI,
Expand All @@ -370,4 +459,6 @@ module.exports = {
isURIClaimable,
splitBySeparator,
convertToShareLink,
getCorrectedChannelUri,
getCorrectedChannelWebPath,
};
10 changes: 9 additions & 1 deletion web/src/oEmbed.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
escapeHtmlProperty,
} = require('../../ui/util/web');
const { lbryProxy: Lbry } = require('../lbry');
const { getCorrectedChannelWebPath } = require('./lbryURI');

Lbry.setDaemonConnectionString(PROXY_URL);

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

async function getClaim(requestUrl) {
const uri = requestUrl.replace(`${URL}/`, 'lbry://');
// Extract the path from the URL and check for missing @ prefix
let webPath = requestUrl.replace(`${URL}/`, '');
const correctedPath = getCorrectedChannelWebPath(webPath);
if (correctedPath) {
webPath = correctedPath;
}

const uri = `lbry://${webPath}`;

let claim, error;
try {
Expand Down
5 changes: 4 additions & 1 deletion web/src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ router.get('*', async (ctx, next) => {
}

const html = await getHtml(ctx);
ctx.body = html;
// Only set body if not already redirecting (3xx status)
if (ctx.status < 300 || ctx.status >= 400) {
ctx.body = html;
}
});

module.exports = router;
Loading