From 3bff7f3b2eb20139b1e407535defa351673bf1e4 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 13 Dec 2025 14:06:59 -0500 Subject: [PATCH 1/2] MCP - update_artist_socials --- lib/artist/updateArtistSocials.ts | 68 +++++++++++++++++++ lib/mcp/tools/artistSocials/index.ts | 13 ++++ .../registerGetArtistSocialsTool.ts | 1 + .../registerUpdateArtistSocialsTool.ts | 66 ++++++++++++++++++ lib/mcp/tools/index.ts | 4 +- lib/socials/getUsernameFromProfileUrl.ts | 24 ++++--- .../account_socials/deleteAccountSocial.ts | 23 +++++++ .../account_socials/insertAccountSocial.ts | 27 ++++++++ lib/supabase/socials/insertSocials.ts | 21 ++++++ lib/supabase/socials/selectSocials.ts | 5 ++ 10 files changed, 239 insertions(+), 13 deletions(-) create mode 100644 lib/artist/updateArtistSocials.ts create mode 100644 lib/mcp/tools/artistSocials/index.ts rename lib/mcp/tools/{ => artistSocials}/registerGetArtistSocialsTool.ts (99%) create mode 100644 lib/mcp/tools/artistSocials/registerUpdateArtistSocialsTool.ts create mode 100644 lib/supabase/account_socials/deleteAccountSocial.ts create mode 100644 lib/supabase/account_socials/insertAccountSocial.ts create mode 100644 lib/supabase/socials/insertSocials.ts diff --git a/lib/artist/updateArtistSocials.ts b/lib/artist/updateArtistSocials.ts new file mode 100644 index 000000000..12fc25c71 --- /dev/null +++ b/lib/artist/updateArtistSocials.ts @@ -0,0 +1,68 @@ +import { getSocialPlatformByLink } from "@/lib/artists/getSocialPlatformByLink"; +import { getUsernameFromProfileUrl } from "@/lib/socials/getUsernameFromProfileUrl"; +import { selectAccountSocials } from "@/lib/supabase/account_socials/selectAccountSocials"; +import { deleteAccountSocial } from "@/lib/supabase/account_socials/deleteAccountSocial"; +import { insertAccountSocial } from "@/lib/supabase/account_socials/insertAccountSocial"; +import { selectSocials } from "@/lib/supabase/socials/selectSocials"; +import { insertSocials } from "@/lib/supabase/socials/insertSocials"; +import type { AccountSocialWithSocial } from "@/lib/supabase/account_socials/selectAccountSocials"; + +/** + * Updates artist socials by replacing existing socials for each platform type. + * + * @param artistId - The artist account ID + * @param profileUrls - Record mapping platform types to profile URLs + * @returns Array of updated account socials with joined social data + */ +export async function updateArtistSocials( + artistId: string, + profileUrls: Record, +): Promise { + // Get current account socials (with no limit to get all) + const accountSocials = await selectAccountSocials(artistId, 0, 10000); + + // Process each platform type + const profilePromises = Object.entries(profileUrls).map(async ([type, value]) => { + const socials = value ? await selectSocials({ profile_url: value }) : null; + const social = socials && socials.length > 0 ? socials[0] : null; + const existingSocial = (accountSocials || []).find( + (account_social: AccountSocialWithSocial) => + getSocialPlatformByLink(account_social.social?.profile_url || "") === type, + ); + + // Delete existing social for this platform type if it exists + if (existingSocial && existingSocial.social?.id) { + await deleteAccountSocial(artistId, existingSocial.social.id); + } + + // Insert new social if URL provided + if (value) { + if (social) { + // Social already exists, check if account_social relationship exists + const existing = (accountSocials || []).find( + (as: AccountSocialWithSocial) => as.social_id === social.id, + ); + if (!existing) { + await insertAccountSocial(artistId, social.id); + } + } else { + // Create new social record + const newSocials = await insertSocials([ + { + username: getUsernameFromProfileUrl(value), + profile_url: value, + }, + ]); + if (newSocials.length > 0) { + await insertAccountSocial(artistId, newSocials[0].id); + } + } + } + }); + + await Promise.all(profilePromises); + + // Return the updated account socials + const updated = await selectAccountSocials(artistId, 0, 10000); + return updated || []; +} diff --git a/lib/mcp/tools/artistSocials/index.ts b/lib/mcp/tools/artistSocials/index.ts new file mode 100644 index 000000000..6e31ff254 --- /dev/null +++ b/lib/mcp/tools/artistSocials/index.ts @@ -0,0 +1,13 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerGetArtistSocialsTool } from "./registerGetArtistSocialsTool"; +import { registerUpdateArtistSocialsTool } from "./registerUpdateArtistSocialsTool"; + +/** + * Registers all artist socials-related MCP tools on the server. + * + * @param server - The MCP server instance to register tools on. + */ +export const registerAllArtistSocialsTools = (server: McpServer): void => { + registerGetArtistSocialsTool(server); + registerUpdateArtistSocialsTool(server); +}; diff --git a/lib/mcp/tools/registerGetArtistSocialsTool.ts b/lib/mcp/tools/artistSocials/registerGetArtistSocialsTool.ts similarity index 99% rename from lib/mcp/tools/registerGetArtistSocialsTool.ts rename to lib/mcp/tools/artistSocials/registerGetArtistSocialsTool.ts index 4fe6b10c1..54d216ea2 100644 --- a/lib/mcp/tools/registerGetArtistSocialsTool.ts +++ b/lib/mcp/tools/artistSocials/registerGetArtistSocialsTool.ts @@ -26,3 +26,4 @@ export function registerGetArtistSocialsTool(server: McpServer): void { }, ); } + diff --git a/lib/mcp/tools/artistSocials/registerUpdateArtistSocialsTool.ts b/lib/mcp/tools/artistSocials/registerUpdateArtistSocialsTool.ts new file mode 100644 index 000000000..898e1dc59 --- /dev/null +++ b/lib/mcp/tools/artistSocials/registerUpdateArtistSocialsTool.ts @@ -0,0 +1,66 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { updateArtistSocials } from "@/lib/artist/updateArtistSocials"; +import { getSocialPlatformByLink } from "@/lib/artists/getSocialPlatformByLink"; +import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess"; +import { getToolResultError } from "@/lib/mcp/getToolResultError"; + +const updateArtistSocialsSchema = z.object({ + artistId: z + .string() + .min(1, "Artist account ID is required") + .describe("The artist's account ID to update socials for."), + urls: z + .array(z.string()) + .describe( + "An array of social profile URLs to associate with the artist. The platform will be inferred automatically.", + ), +}); + +type UpdateArtistSocialsArgs = z.infer; + +/** + * Registers the "update_artist_socials" tool on the MCP server. + * Updates the artist_socials records for an artist. + * + * @param server - The MCP server instance to register the tool on. + */ +export function registerUpdateArtistSocialsTool(server: McpServer): void { + server.registerTool( + "update_artist_socials", + { + description: `Update the artist_socials records for an artist. Provide the artistId and an array of social profile URLs. The tool will infer the platform for each URL and update the artist's socials accordingly.`, + inputSchema: updateArtistSocialsSchema, + }, + async (args: UpdateArtistSocialsArgs) => { + try { + // Map each URL to its platform type + const profileUrls: Record = {}; + for (const url of args.urls) { + const platform = getSocialPlatformByLink(url); + if (platform && platform !== "NONE") { + profileUrls[platform] = url; + } + } + + const socials = await updateArtistSocials(args.artistId, profileUrls); + + const response = { + success: true, + message: "Artist socials updated successfully.", + socials, + }; + + return getToolResultSuccess(response); + } catch (error) { + console.error("Error updating artist socials:", error); + const errorMessage = + error instanceof Error + ? error.message + : "Failed to update artist socials."; + return getToolResultError(errorMessage); + } + }, + ); +} + diff --git a/lib/mcp/tools/index.ts b/lib/mcp/tools/index.ts index 219bfba56..81bef56b4 100644 --- a/lib/mcp/tools/index.ts +++ b/lib/mcp/tools/index.ts @@ -1,5 +1,4 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { registerGetArtistSocialsTool } from "./registerGetArtistSocialsTool"; import { registerGetLocalTimeTool } from "./registerGetLocalTimeTool"; import { registerAllTaskTools } from "./tasks"; import { registerAllImageTools } from "./images"; @@ -8,6 +7,7 @@ import { registerAllSora2Tools } from "./sora2"; import { registerAllSpotifyTools } from "./spotify"; import { registerContactTeamTool } from "./registerContactTeamTool"; import { registerUpdateAccountInfoTool } from "./registerUpdateAccountInfoTool"; +import { registerAllArtistSocialsTools } from "./artistSocials"; /** * Registers all MCP tools on the server. @@ -16,7 +16,7 @@ import { registerUpdateAccountInfoTool } from "./registerUpdateAccountInfoTool"; * @param server - The MCP server instance to register tools on. */ export const registerAllTools = (server: McpServer): void => { - registerGetArtistSocialsTool(server); + registerAllArtistSocialsTools(server); registerGetLocalTimeTool(server); registerAllTaskTools(server); registerAllImageTools(server); diff --git a/lib/socials/getUsernameFromProfileUrl.ts b/lib/socials/getUsernameFromProfileUrl.ts index 385b8d280..54219c5aa 100644 --- a/lib/socials/getUsernameFromProfileUrl.ts +++ b/lib/socials/getUsernameFromProfileUrl.ts @@ -1,20 +1,22 @@ /** - * Extracts username from a profile URL by pulling text after .com/ or .net/ until ? or / + * Extracts username from a profile URL. + * Uses URL parsing for robustness, with fallback to string splitting. * - * @param profileUrl - The profile URL to extract username from - * @returns The username or empty string if unable to extract + * @param profileUrl - The profile URL to extract username from (can be null or undefined) + * @returns The extracted username, or empty string if not found */ -export const getUsernameFromProfileUrl = (profileUrl: string | null | undefined): string => { +export function getUsernameFromProfileUrl(profileUrl: string | null | undefined): string { if (!profileUrl) { return ""; } try { - const normalizedUrl = profileUrl.toLowerCase().trim(); - const match = normalizedUrl.match(/(?:\.com|\.net)\/([^/?]+)/); - return match ? match[1] : ""; - } catch (error) { - console.error("[ERROR] Error extracting username from profile URL:", error); - return ""; + const url = new URL(profileUrl); + const pathParts = url.pathname.split("/").filter(Boolean); + return pathParts[pathParts.length - 1] || ""; + } catch { + // If URL parsing fails, try to extract from the end of the string + const parts = profileUrl.split("/").filter(Boolean); + return parts[parts.length - 1] || ""; } -}; +} diff --git a/lib/supabase/account_socials/deleteAccountSocial.ts b/lib/supabase/account_socials/deleteAccountSocial.ts new file mode 100644 index 000000000..50544c340 --- /dev/null +++ b/lib/supabase/account_socials/deleteAccountSocial.ts @@ -0,0 +1,23 @@ +import supabase from "../serverClient"; + +/** + * Deletes an account_social record by account ID and social ID. + * + * @param accountId - The account ID + * @param socialId - The social ID + * @returns True if successful, false otherwise + */ +export async function deleteAccountSocial(accountId: string, socialId: string): Promise { + const { error } = await supabase + .from("account_socials") + .delete() + .eq("account_id", accountId) + .eq("social_id", socialId); + + if (error) { + console.error("[ERROR] deleteAccountSocial:", error); + return false; + } + + return true; +} diff --git a/lib/supabase/account_socials/insertAccountSocial.ts b/lib/supabase/account_socials/insertAccountSocial.ts new file mode 100644 index 000000000..bf1f53b93 --- /dev/null +++ b/lib/supabase/account_socials/insertAccountSocial.ts @@ -0,0 +1,27 @@ +import supabase from "../serverClient"; +import type { Tables } from "@/types/database.types"; + +/** + * Inserts a new account_social record. + * + * @param accountId - The account ID + * @param socialId - The social ID + * @returns The inserted account_social record, or null if failed + */ +export async function insertAccountSocial( + accountId: string, + socialId: string, +): Promise | null> { + const { data, error } = await supabase + .from("account_socials") + .insert({ account_id: accountId, social_id: socialId }) + .select("*") + .single(); + + if (error) { + console.error("[ERROR] insertAccountSocial:", error); + return null; + } + + return data || null; +} diff --git a/lib/supabase/socials/insertSocials.ts b/lib/supabase/socials/insertSocials.ts new file mode 100644 index 000000000..f149a0a3c --- /dev/null +++ b/lib/supabase/socials/insertSocials.ts @@ -0,0 +1,21 @@ +import supabase from "../serverClient"; +import type { Tables, TablesInsert } from "@/types/database.types"; + +/** + * Inserts one or more social records. + * + * @param socials - Array of social data to insert + * @returns Array of inserted social records + */ +export async function insertSocials( + socials: TablesInsert<"socials">[], +): Promise[]> { + const { data, error } = await supabase.from("socials").insert(socials).select("*"); + + if (error) { + console.error("[ERROR] insertSocials:", error); + return []; + } + + return data || []; +} diff --git a/lib/supabase/socials/selectSocials.ts b/lib/supabase/socials/selectSocials.ts index da0b91ca7..ee04075b0 100644 --- a/lib/supabase/socials/selectSocials.ts +++ b/lib/supabase/socials/selectSocials.ts @@ -3,6 +3,7 @@ import supabase from "../serverClient"; type SelectSocialsParams = { id?: string; + profile_url?: string; }; /** @@ -21,6 +22,10 @@ export async function selectSocials( query = query.eq("id", params.id); } + if (params.profile_url) { + query = query.eq("profile_url", params.profile_url); + } + const { data, error } = await query; if (error) { From b71b130abb870a69e19d0b19988b46feb7ab8672 Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Sat, 13 Dec 2025 14:16:37 -0500 Subject: [PATCH 2/2] rollback lib/socials/getUsernameFromProfileUrl.ts --- lib/socials/getUsernameFromProfileUrl.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/socials/getUsernameFromProfileUrl.ts b/lib/socials/getUsernameFromProfileUrl.ts index 54219c5aa..385b8d280 100644 --- a/lib/socials/getUsernameFromProfileUrl.ts +++ b/lib/socials/getUsernameFromProfileUrl.ts @@ -1,22 +1,20 @@ /** - * Extracts username from a profile URL. - * Uses URL parsing for robustness, with fallback to string splitting. + * Extracts username from a profile URL by pulling text after .com/ or .net/ until ? or / * - * @param profileUrl - The profile URL to extract username from (can be null or undefined) - * @returns The extracted username, or empty string if not found + * @param profileUrl - The profile URL to extract username from + * @returns The username or empty string if unable to extract */ -export function getUsernameFromProfileUrl(profileUrl: string | null | undefined): string { +export const getUsernameFromProfileUrl = (profileUrl: string | null | undefined): string => { if (!profileUrl) { return ""; } try { - const url = new URL(profileUrl); - const pathParts = url.pathname.split("/").filter(Boolean); - return pathParts[pathParts.length - 1] || ""; - } catch { - // If URL parsing fails, try to extract from the end of the string - const parts = profileUrl.split("/").filter(Boolean); - return parts[parts.length - 1] || ""; + const normalizedUrl = profileUrl.toLowerCase().trim(); + const match = normalizedUrl.match(/(?:\.com|\.net)\/([^/?]+)/); + return match ? match[1] : ""; + } catch (error) { + console.error("[ERROR] Error extracting username from profile URL:", error); + return ""; } -} +};