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
36 changes: 32 additions & 4 deletions apps/app/src/components/DistributorBadges.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
import { Badge } from "./ui/badge";
import {
getDistributorUrl,
getDistributorDisplayName,
type DistributorConfig,
} from "../lib/distributor-urls";

export function DistributorBadges({
distribute,
}: {
distribute: { plugin: string }[];
distribute: DistributorConfig[];
}) {
return (
<div className="flex flex-wrap gap-1 max-w-full overflow-hidden">
{distribute.map((distributor, index) => {
const pluginName = distributor.plugin.replace("@curatedotfun/", "");
const displayName = getDistributorDisplayName(distributor);
const url = getDistributorUrl(distributor);

const badgeContent = (
<Badge className="text-black border border-stone-500 rounded-md bg-stone-50 shadow-none px-2 py-0.5 text-xs whitespace-nowrap flex-shrink-0 cursor-pointer hover:bg-stone-100 transition-colors">
{displayName}
</Badge>
);

// If we have a URL, wrap in a link, otherwise just render the badge
if (url) {
return (
<a
key={`${distributor.plugin}-${index}`}
href={url}
target="_blank"
rel="noopener noreferrer"
className="inline-block"
>
{badgeContent}
</a>
);
}

return (
<Badge
key={`${distributor.plugin}-${index}`}
className="text-black border border-stone-500 rounded-md bg-stone-50 shadow-none capitalize px-2 py-0.5 text-xs whitespace-nowrap flex-shrink-0"
className="text-black border border-stone-500 rounded-md bg-stone-50 shadow-none px-2 py-0.5 text-xs whitespace-nowrap flex-shrink-0"
>
{pluginName}
{displayName}
</Badge>
);
})}
Expand Down
261 changes: 261 additions & 0 deletions apps/app/src/lib/distributor-urls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/**
* Distributor URL utility functions
*
* This module provides functions to generate appropriate URLs for different
* distributor types based on their configuration.
*/

export interface DistributorConfig {
plugin: string;
config?: Record<string, unknown>;
}

/**
* Checks if a value is a template variable (contains {{ }})
*/
function isTemplateVariable(value: unknown): boolean {
return typeof value === "string" && value.includes("{{");
}

/**
* Safely gets a string value from config, returning null if it's a template
*/
function getConfigString(
config: Record<string, unknown> | undefined,
key: string,
): string | null {
const value = config?.[key];
if (typeof value === "string" && !isTemplateVariable(value)) {
return value;
}
return null;
}

/**
* Generate URLs for RSS distributor
*/
function getRssUrl(config: Record<string, unknown> | undefined): string | null {
// RSS distributors have a serviceUrl in their config that points to the RSS feed
const serviceUrl = getConfigString(config, "serviceUrl");
return serviceUrl;
}

/**
* Generate URLs for Twitter distributor
*/
function getTwitterUrl(config: Record<string, unknown> | undefined): string {
// Twitter links to the specific profile if available
const username =
getConfigString(config, "username") || getConfigString(config, "handle");
if (username) {
return `https://twitter.com/${username.replace("@", "")}`;
}
return "https://twitter.com";
}

/**
* Generate URLs for Telegram distributor
*/
function getTelegramUrl(config: Record<string, unknown> | undefined): string {
// Telegram links to the channel if chatId is available
const chatId =
getConfigString(config, "chatId") || getConfigString(config, "channelId");
if (chatId) {
const cleanChatId = chatId.replace("@", "");
return `https://t.me/${cleanChatId}`;
}
return "https://telegram.org";
}

/**
* Generate URLs for Notion distributor
*/
function getNotionUrl(config: Record<string, unknown> | undefined): string {
// Notion links to the table/database if available
const databaseId = getConfigString(config, "databaseId");
if (databaseId) {
return `https://notion.so/${databaseId}`;
}
return "https://notion.so";
}

/**
* Generate URLs for Crosspost distributor
*/
function getCrosspostUrl(config: Record<string, unknown> | undefined): string {
// Crosspost links to the Open Crosspost platform
// Could potentially link to specific account if signerId is available
const signerId = getConfigString(config, "signerId");
if (signerId) {
return `https://near.social/mob.near/widget/MainPage.N.Profile.Page?accountId=${signerId}`;
}
return "https://opencrosspost.com";
}

/**
* Generate URLs for Discord distributor
*/
function getDiscordUrl(config: Record<string, unknown> | undefined): string {
// Discord links to the specific channel if available
const channelId = getConfigString(config, "channelId");
if (channelId) {
// Discord channel URLs format: https://discord.com/channels/serverId/channelId
// Since we don't have serverId, we use @me which works for direct channel links
return `https://discord.com/channels/@me/${channelId}`;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify Discord channel URL format compatibility.

The Discord URL format using @me may not work for all channel types. Server channels typically require the actual server ID.

Please verify that Discord channel URLs with @me work correctly for your use case:


🌐 Web query:

Does Discord support channel URLs in the format https://discord.com/channels/@me/channelId for all channel types?

💡 Result:

Yes, Discord supports channel URLs in the format https://discord.com/channels/@me/channelId for direct message (DM) channels. This URL structure allows users to access their private conversations directly. For other channel types, such as text or voice channels within a server (guild), the URL format is https://discord.com/channels/guildId/channelId. This distinction ensures that users can navigate to both server-based and direct message channels using appropriate URLs.


Ensure correct Discord channel URL generation for DMs and guild channels

We verified that https://discord.com/channels/@me/<channelId> only works for direct‐message channels, whereas server (guild) channels require the guild ID in the URL. If this utility may be used for guild channels, update it to accept an optional guildId and generate the URL accordingly.

• apps/app/src/lib/distributor-urls.ts: adjust return value
• Introduce an optional guildId parameter (or pull it from context)

Suggested diff:

-   return `https://discord.com/channels/@me/${channelId}`;
+   if (guildId) {
+     return `https://discord.com/channels/${guildId}/${channelId}`;
+   }
+   return `https://discord.com/channels/@me/${channelId}`;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/app/src/lib/distributor-urls.ts at line 104, the function currently
returns a URL formatted only for direct-message channels using
`https://discord.com/channels/@me/${channelId}`. To support both DM and guild
channels, modify the function to accept an optional `guildId` parameter. If
`guildId` is provided, return the URL as
`https://discord.com/channels/${guildId}/${channelId}`, otherwise keep the
existing DM URL format. Update the function signature and the return statement
accordingly to handle both cases.

}
return "https://discord.com";
}

/**
* Generate URLs for NEAR Social distributor
*/
function getNearSocialUrl(config: Record<string, unknown> | undefined): string {
// NEAR Social links to the specific account if available
const accountId = getConfigString(config, "accountId");
if (accountId) {
return `https://near.social/mob.near/widget/MainPage.N.Profile.Page?accountId=${accountId}`;
}
return "https://near.social";
}

/**
* Generate URLs for Supabase distributor
*/
function getSupabaseUrl(config: Record<string, unknown> | undefined): string {
// Supabase links to the project dashboard if URL is available
const url = getConfigString(config, "url");
if (url) {
// Extract project reference from Supabase URL
const match = url.match(/https:\/\/([^.]+)\.supabase\.co/);
if (match) {
const projectRef = match[1];
return `https://supabase.com/dashboard/project/${projectRef}`;
}
Comment on lines +129 to +133
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Strengthen URL validation for Supabase project extraction.

The regex pattern for extracting Supabase project references could be more robust and should handle edge cases.

-    const match = url.match(/https:\/\/([^.]+)\.supabase\.co/);
+    const match = url.match(/^https:\/\/([a-zA-Z0-9-]+)\.supabase\.co(?:\/|$)/);

This ensures:

  • URL starts with https://
  • Project ref contains only valid characters
  • Properly handles trailing slashes
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const match = url.match(/https:\/\/([^.]+)\.supabase\.co/);
if (match) {
const projectRef = match[1];
return `https://supabase.com/dashboard/project/${projectRef}`;
}
const match = url.match(/^https:\/\/([a-zA-Z0-9-]+)\.supabase\.co(?:\/|$)/);
if (match) {
const projectRef = match[1];
return `https://supabase.com/dashboard/project/${projectRef}`;
}
🤖 Prompt for AI Agents
In apps/app/src/lib/distributor-urls.ts around lines 129 to 133, the current
regex for extracting the Supabase project reference is too loose and may match
invalid URLs. Update the regex to ensure the URL starts with https://, the
project reference contains only valid characters (e.g., alphanumeric and
hyphens), and it properly handles optional trailing slashes. This will make the
URL validation more robust and prevent incorrect matches.

}
return "https://supabase.com";
}

/**
* Get the clean plugin name without the @curatedotfun/ prefix
*/
function getPluginName(plugin: string): string {
return plugin.replace("@curatedotfun/", "");
}

/**
* Main function to get distributor URL based on plugin type and configuration
*/
export function getDistributorUrl(
distributor: DistributorConfig,
): string | null {
const pluginName = getPluginName(distributor.plugin);

switch (pluginName) {
case "rss":
return getRssUrl(distributor.config);

case "twitter":
case "twitter-distributor":
return getTwitterUrl(distributor.config);

case "telegram":
case "telegram-distributor":
return getTelegramUrl(distributor.config);

case "notion":
case "notion-distributor":
return getNotionUrl(distributor.config);

case "crosspost":
case "crosspost-distributor":
return getCrosspostUrl(distributor.config);

case "discord":
case "discord-distributor":
return getDiscordUrl(distributor.config);

case "near-social":
case "near-social-distributor":
case "nearsocial":
return getNearSocialUrl(distributor.config);

case "supabase":
case "supabase-distributor":
return getSupabaseUrl(distributor.config);

default:
// For unknown distributors, return null to show badge without link
return null;
}
}

/**
* Get a display-friendly name for the distributor
*/
export function getDistributorDisplayName(
distributor: DistributorConfig,
): string {
const pluginName = getPluginName(distributor.plugin);

// Convert plugin names to display names
switch (pluginName) {
case "near-social":
case "nearsocial":
return "NEAR Social";
case "rss":
return "RSS";
default:
// Capitalize first letter for other distributors
return pluginName.charAt(0).toUpperCase() + pluginName.slice(1);
}
}

/**
* Check if a distributor has a functional URL (not just a fallback)
*/
export function hasSpecificUrl(distributor: DistributorConfig): boolean {
const pluginName = getPluginName(distributor.plugin);
const config = distributor.config;

switch (pluginName) {
case "rss":
return !!getConfigString(config, "serviceUrl");

case "twitter":
case "twitter-distributor":
return !!(
getConfigString(config, "username") || getConfigString(config, "handle")
);

case "telegram":
case "telegram-distributor":
return !!(
getConfigString(config, "chatId") ||
getConfigString(config, "channelId")
);

case "notion":
case "notion-distributor":
return !!getConfigString(config, "databaseId");

case "crosspost":
case "crosspost-distributor":
return !!getConfigString(config, "signerId");

case "discord":
case "discord-distributor":
return !!getConfigString(config, "channelId");

case "near-social":
case "near-social-distributor":
case "nearsocial":
return !!getConfigString(config, "accountId");

case "supabase":
case "supabase-distributor":
return !!getConfigString(config, "url");

default:
return false;
}
}
Loading