diff --git a/ui/hocs/withResolvedClaimRender/view.jsx b/ui/hocs/withResolvedClaimRender/view.jsx
index 2a6ff2cde7..f51db8d18a 100644
--- a/ui/hocs/withResolvedClaimRender/view.jsx
+++ b/ui/hocs/withResolvedClaimRender/view.jsx
@@ -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';
@@ -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;
+ }
+}
+
const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam) => {
const ResolvedClaimRender = (props: Props) => {
const {
@@ -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 =
@@ -111,6 +158,11 @@ const withResolvedClaimRender = (ClaimRenderComponent: FunctionalComponentParam)
}
}, [resolveRequired, resolveClaim]);
+ // If URL needs correction, show loading while redirect happens
+ if (correctedPath) {
+ return ;
+ }
+
if (!hasClaim) {
if (hasClaim === undefined) {
return ;
diff --git a/web/src/html.js b/web/src/html.js
index ef0a64f59b..54ba0805bc 100644
--- a/web/src/html.js
+++ b/web/src/html.js
@@ -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');
@@ -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;
diff --git a/web/src/lbryURI.js b/web/src/lbryURI.js
index e0400f483e..aed680141c 100644
--- a/web/src/lbryURI.js
+++ b/web/src/lbryURI.js
@@ -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,
@@ -370,4 +459,6 @@ module.exports = {
isURIClaimable,
splitBySeparator,
convertToShareLink,
+ getCorrectedChannelUri,
+ getCorrectedChannelWebPath,
};
diff --git a/web/src/oEmbed.js b/web/src/oEmbed.js
index 4ac42ee005..10ef61e023 100644
--- a/web/src/oEmbed.js
+++ b/web/src/oEmbed.js
@@ -9,6 +9,7 @@ const {
escapeHtmlProperty,
} = require('../../ui/util/web');
const { lbryProxy: Lbry } = require('../lbry');
+const { getCorrectedChannelWebPath } = require('./lbryURI');
Lbry.setDaemonConnectionString(PROXY_URL);
@@ -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 {
diff --git a/web/src/routes.js b/web/src/routes.js
index 2d5731257b..367f5c8d25 100644
--- a/web/src/routes.js
+++ b/web/src/routes.js
@@ -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;