feat(pinterest): add pinterest (#17747)

* feat(pinterest): add Pinterest

* Update lib/routes/pinterest/user.ts

* Update lib/routes/pinterest/types.ts

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Tony 2024-11-28 11:51:17 -08:00 committed by GitHub
parent 71af316730
commit 52c8c8f955
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 441 additions and 0 deletions

View File

@ -0,0 +1,7 @@
import type { Namespace } from '@/types';
export const namespace: Namespace = {
name: 'Pinterest',
url: 'www.pinterest.com',
lang: 'en',
};

View File

@ -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;
}

View File

@ -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}<br><img src="${item.images.orig.url}">`,
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'] ? '<br>' + item.images['170x'].map((img) => `<img src="${img.url}">`).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<UserProfile>;
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);
};