diff --git a/lib/routes/pinterest/namespace.ts b/lib/routes/pinterest/namespace.ts
new file mode 100644
index 00000000000000..b115b7fac4be39
--- /dev/null
+++ b/lib/routes/pinterest/namespace.ts
@@ -0,0 +1,7 @@
+import type { Namespace } from '@/types';
+
+export const namespace: Namespace = {
+ name: 'Pinterest',
+ url: 'www.pinterest.com',
+ lang: 'en',
+};
diff --git a/lib/routes/pinterest/types.ts b/lib/routes/pinterest/types.ts
new file mode 100644
index 00000000000000..ca3b8c8885a5c7
--- /dev/null
+++ b/lib/routes/pinterest/types.ts
@@ -0,0 +1,330 @@
+export interface BoardsFeedResource {
+ node_id: string;
+ name: string;
+ created_at: string;
+ section_count: number;
+ images: BoardImages;
+ image_cover_hd_url: string;
+ archived_by_me_at: null;
+ access: any[];
+ image_cover_url: string;
+ follower_count: number;
+ event_date: null;
+ should_show_shop_feed: boolean;
+ should_show_board_collaborators: boolean;
+ collaborator_requests_enabled: boolean;
+ is_temporarily_disabled: boolean;
+ collaborating_users: any[];
+ followed_by_me: boolean;
+ viewer_collaborator_join_requested: boolean;
+ cover_pin: CoverPin;
+ place_saves_count: number;
+ board_order_modified_at: string;
+ owner: Owner;
+ allow_homefeed_recommendations: boolean;
+ pin_count: number;
+ collaborator_count: number;
+ has_custom_cover: boolean;
+ privacy: string;
+ should_show_more_ideas: boolean;
+ event_start_date: null;
+ url: string;
+ collaborated_by_me: boolean;
+ is_collaborative: boolean;
+ cover_images: CoverImages;
+ is_ads_only: boolean;
+ type: string;
+ description: string;
+}
+
+interface BoardImages {
+ '170x': ImageItem[];
+}
+
+interface Images {
+ orig: ImageItem;
+ [x: string]: ImageItem;
+}
+
+interface ImageItem {
+ url: string;
+ width: number;
+ height: number;
+ dominant_color: string;
+}
+
+interface CoverPin {
+ pin_id: string;
+ timestamp: number;
+ image_signature: string;
+ crop?: number[];
+ size?: number[];
+ scale?: number;
+ image_url?: string;
+ custom_cover?: boolean;
+ image_size?: number[] | null;
+}
+
+interface Owner {
+ node_id: string;
+ explicitly_followed_by_me: boolean;
+ is_partner: boolean;
+ is_ads_only_profile: boolean;
+ is_private_profile: boolean;
+ ads_only_profile_site: null;
+ domain_verified: boolean;
+ type: string;
+ image_medium_url: string;
+ is_default_image: boolean;
+ id: string;
+ full_name: string;
+ username: string;
+ is_verified_merchant: boolean;
+ verified_identity: object;
+}
+
+interface Pinner extends Owner {
+ image_large_url: string;
+ image_small_url: string;
+}
+
+interface CoverImages {
+ '200x150': ImageItem;
+ '222x': ImageItem;
+}
+
+export interface UserActivityPinsResource {
+ node_id: string;
+ is_stale_product: boolean;
+ attribution: null;
+ access: any[];
+ images: Images;
+ comment_count: number;
+ digital_media_source_type: null;
+ promoted_is_removable: boolean;
+ is_eligible_for_pdp: boolean;
+ sponsorship: null;
+ story_pin_data_id: string;
+ description_html: string;
+ shopping_flags: any[];
+ is_uploaded: boolean;
+ campaign_id: null;
+ is_playable: boolean;
+ manual_interest_tags: null;
+ video_status: null;
+ seo_url: string;
+ image_signature: string;
+ is_eligible_for_web_closeup: boolean;
+ ad_match_reason: number;
+ is_oos_product: boolean;
+ dominant_color: string;
+ aggregated_pin_data: AggregatedPinData;
+ creator_analytics: null;
+ is_repin: boolean;
+ done_by_me: boolean;
+ board: Board;
+ view_tags: any[];
+ video_status_message: null;
+ description: string;
+ domain: string;
+ is_downstream_promotion: boolean;
+ is_video: boolean;
+ promoter: null;
+ embed: null;
+ comments: Comments;
+ collection_pin: null;
+ is_promoted: boolean;
+ grid_title: string;
+ is_whitelisted_for_tried_it: boolean;
+ promoted_is_lead_ad: boolean;
+ debug_info_html: null;
+ link: null;
+ is_native: boolean;
+ id: string;
+ promoted_lead_form: null;
+ type: string;
+ rich_summary: null;
+ title: string;
+ alt_text: null;
+ created_at: string;
+ is_quick_promotable: boolean;
+ pinner: Pinner;
+ image_crop: ImageCrop;
+ reaction_counts: object;
+ tracking_params: string;
+ is_eligible_for_related_products: boolean;
+ product_pin_data: null;
+ privacy: string;
+ price_currency: string;
+ has_required_attribution_provider: boolean;
+ seo_title: string;
+ price_value: number;
+ insertion_id: null;
+ seo_noindex_reason: null;
+ carousel_data: null;
+ videos: null;
+ story_pin_data: StoryPinData;
+ grid_description: string;
+ repin_count: number;
+ native_creator: Owner;
+ should_open_in_stream: boolean;
+ additional_hide_reasons: any[];
+ method: string;
+}
+
+interface AggregatedPinData {
+ node_id: string;
+ is_shop_the_look: boolean;
+ aggregated_stats: AggregatedStats;
+ did_it_data: DidItData;
+ creator_analytics: null;
+ has_xy_tags: boolean;
+ id: string;
+}
+
+interface AggregatedStats {
+ saves: number;
+ done: number;
+}
+
+interface DidItData {
+ type: string;
+ details_count: number;
+ recommend_scores: RecommendScore[];
+ rating: number;
+ tags: any[];
+ user_count: number;
+ videos_count: number;
+ images_count: number;
+ recommended_count: number;
+ responses_count: number;
+}
+
+interface RecommendScore {
+ score: number;
+ count: number;
+}
+
+interface Board {
+ node_id: string;
+ layout: string;
+ type: string;
+ privacy: string;
+ followed_by_me: boolean;
+ image_thumbnail_url: string;
+ name: string;
+ collaborated_by_me: boolean;
+ owner: Owner;
+ is_collaborative: boolean;
+ url: string;
+ id: string;
+}
+
+interface Comments {
+ uri: string;
+ data: any[];
+ bookmark: null;
+}
+
+interface ImageCrop {
+ min_y: number;
+ max_y: number;
+}
+
+interface StoryPinData {
+ node_id: string;
+ page_count: number;
+ type: string;
+ has_affiliate_products: boolean;
+ static_page_count: number;
+ pages: Page[];
+ metadata: Metadata;
+ is_deleted: boolean;
+ total_video_duration: number;
+ pages_preview: PagePreview[];
+ has_product_pins: boolean;
+ id: string;
+ last_edited: null;
+}
+
+interface Page {
+ blocks: Block[];
+}
+
+interface Block {
+ type: string;
+ block_type: number;
+ text: string;
+ block_style: BlockStyle;
+ image_signature: string;
+ image: null;
+ tracking_id: string;
+}
+
+interface BlockStyle {
+ x_coord: number;
+ corner_radius: number;
+ rotation: number;
+ height: number;
+ width: number;
+ y_coord: number;
+}
+
+interface Metadata {
+ basics: null;
+ is_compatible: boolean;
+ root_user_id: string;
+ canvas_aspect_ratio: number;
+ diy_data: null;
+ compatible_version: string;
+ is_editable: boolean;
+ root_pin_id: string;
+ pin_title: string | null;
+ template_type: null;
+ showreel_data: null;
+ recipe_data: null;
+ is_promotable: boolean;
+ version: string;
+ pin_image_signature: string;
+}
+
+interface PagePreview {
+ blocks: Block[];
+}
+
+export interface UserProfile extends Owner {
+ image_xlarge_url: string;
+ impressum_url: null;
+ seo_canonical_domain: string;
+ following_count: number;
+ last_pin_save_time: string;
+ first_name: string;
+ eligible_profile_tabs: EligibleProfileTab[];
+ seo_noindex_reason: null;
+ board_count: number;
+ instagram_data: null;
+ profile_cover: ProfileCover;
+ seo_description: string;
+ follower_count: number;
+ interest_following_count: null;
+ is_inspirational_merchant: boolean;
+ about: string;
+ partner: null;
+ website_url: null;
+ domain_url: null;
+ seo_title: string;
+ indexed: boolean;
+ is_primary_website_verified: boolean;
+}
+
+interface EligibleProfileTab {
+ id: string;
+ type: string;
+ tab_type: number;
+ name: string;
+ is_default: boolean;
+}
+
+interface ProfileCover {
+ id: string;
+}
diff --git a/lib/routes/pinterest/user.ts b/lib/routes/pinterest/user.ts
new file mode 100644
index 00000000000000..731133336ee59b
--- /dev/null
+++ b/lib/routes/pinterest/user.ts
@@ -0,0 +1,104 @@
+import { Route } from '@/types';
+import cache from '@/utils/cache';
+import ofetch from '@/utils/ofetch';
+import { parseDate } from '@/utils/parse-date';
+import { BoardsFeedResource, UserActivityPinsResource, UserProfile } from './types';
+
+export const route: Route = {
+ path: '/user/:username/:type?',
+ categories: ['picture'],
+ example: '/pinterest/user/howieserious',
+ parameters: {
+ username: 'Username',
+ type: {
+ description: 'Type, default to `_created`',
+ default: '_created',
+ options: [
+ { value: '_created', label: 'Created' },
+ { value: '_saved', label: 'Saved' },
+ ],
+ },
+ },
+ radar: [
+ {
+ source: ['www.pinterest.com/:id/:type?', 'www.pinterest.com/:id'],
+ target: '/user/:id/:type?',
+ },
+ ],
+ name: 'User',
+ maintainers: ['TonyRL'],
+ handler,
+};
+
+const baseUrl = 'https://www.pinterest.com';
+
+async function handler(ctx) {
+ const { username, type = '_created' } = ctx.req.param();
+
+ const profile = await getUserResource(username);
+ const response = type === '_created' ? await getUserActivityPinsResource(username, profile.id) : await getBoardsFeedResource(username);
+
+ const items =
+ type === '_created'
+ ? (response as UserActivityPinsResource[]).map((item) => ({
+ title: item.title || item.seo_title,
+ description: `${item.grid_description}
`,
+ link: `${baseUrl}${item.seo_url}`,
+ author: item.pinner.full_name,
+ pubDate: parseDate(item.created_at),
+ image: item.images.orig.url,
+ }))
+ : (response as BoardsFeedResource[]).map((item) => ({
+ title: item.name,
+ description: item.description + (item.images?.['170x'] ? '
' + item.images['170x'].map((img) => ``).join('') : ''),
+ link: `${baseUrl}${item.url}`,
+ author: item.owner.full_name,
+ pubDate: parseDate(item.created_at),
+ image: item.image_cover_hd_url,
+ }));
+
+ return {
+ title: profile.seo_title,
+ description: profile.seo_description,
+ image: profile.image_xlarge_url ?? profile.image_medium_url,
+ link: `${baseUrl}/${username}/`,
+ item: items,
+ };
+}
+
+const getUserResource = (username: string) =>
+ cache.tryGet(`pinterest:user:${username}`, async () => {
+ const response = await ofetch(`${baseUrl}/resource/UserResource/get/`, {
+ query: {
+ source_url: `/${username}/_created`,
+ data: JSON.stringify({ options: { username, field_set_key: 'unauth_profile' }, context: {} }),
+ _: Date.now(),
+ },
+ });
+
+ return response.resource_response.data;
+ }) as Promise;
+
+const getUserActivityPinsResource = async (username: string, userId: string) => {
+ const response = await ofetch(`${baseUrl}/resource/UserActivityPinsResource/get/`, {
+ query: {
+ source_url: `/${username}/_created`,
+ data: JSON.stringify({ options: { exclude_add_pin_rep: true, field_set_key: 'grid_item', is_own_profile_pins: false, user_id: userId, username }, context: {} }),
+ _: Date.now(),
+ },
+ });
+
+ return response.resource_response.data as UserActivityPinsResource[];
+};
+
+const getBoardsFeedResource = async (username: string) => {
+ const response = await ofetch(`${baseUrl}/resource/BoardsFeedResource/get/`, {
+ query: {
+ source_url: `/${username}/_saved`,
+ data: JSON.stringify({ options: { field_set_key: 'profile_grid_item', filter_stories: false, sort: 'last_pinned_to', username }, context: {} }),
+ _: Date.now(),
+ },
+ });
+
+ return (response.resource_response.data as BoardsFeedResource[]).filter((item) => item.node_id);
+};