From 95d5c62607f24f545608f2a0ab93501a92f9ab55 Mon Sep 17 00:00:00 2001 From: Ronald Langeveld Date: Thu, 19 Dec 2024 15:51:04 +0700 Subject: [PATCH] Added minimal implementation - noticed something which may be a potential blocker for integrating this library, which is that it uses v1 of the Twitter API. While it bypasses the need for an API Key, some data points appear to be missing. --- .../services/oembed/RettiwtOEmbedProvider.js | 114 +++++++++--------- .../core/server/services/oembed/service.js | 13 +- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/ghost/core/core/server/services/oembed/RettiwtOEmbedProvider.js b/ghost/core/core/server/services/oembed/RettiwtOEmbedProvider.js index 4b98e9fc1b..b3764b50f5 100644 --- a/ghost/core/core/server/services/oembed/RettiwtOEmbedProvider.js +++ b/ghost/core/core/server/services/oembed/RettiwtOEmbedProvider.js @@ -1,6 +1,4 @@ const logging = require('@tryghost/logging'); -const {FetcherService, EResourceType, Rettiwt} = require('rettiwt-api'); -const {id} = require('../../../shared/SentryKnexTracingIntegration'); /** * @typedef {import('./oembed').ICustomProvider} ICustomProvider @@ -9,53 +7,65 @@ const {id} = require('../../../shared/SentryKnexTracingIntegration'); const TWITTER_PATH_REGEX = /\/status\/(\d+)/; -function mapTweetDetails(rettiwtTweet) { - return { - edit_history_tweet_ids: [rettiwtTweet.id], - referenced_tweets: rettiwtTweet.replyTo - ? [{type: 'replied_to', id: rettiwtTweet.replyTo}] - : undefined, - author_id: rettiwtTweet.tweetBy.id, - created_at: new Date(rettiwtTweet.createdAt).toISOString(), - in_reply_to_user_id: rettiwtTweet.replyTo ? rettiwtTweet.tweetBy.id : undefined, - text: rettiwtTweet.fullText, - reply_settings: 'everyone', // Default value as per the original structure - conversation_id: rettiwtTweet.replyTo || rettiwtTweet.id, - possibly_sensitive: false, // Default value as it's not in the rettiwt object - lang: rettiwtTweet.lang, - entities: { - mentions: rettiwtTweet.entities.mentions || [] - }, - context_annotations: [], // Placeholder, as context annotations are not provided in rettiwt - id: rettiwtTweet.id, - public_metrics: { - retweet_count: rettiwtTweet.retweetCount, - reply_count: rettiwtTweet.replyCount, - like_count: rettiwtTweet.likeCount, - quote_count: rettiwtTweet.quoteCount, - bookmark_count: rettiwtTweet.bookmarkCount, - impression_count: rettiwtTweet.viewCount - }, - includes: { - users: [ - { - id: rettiwtTweet.tweetBy.id, - username: rettiwtTweet.tweetBy.username, - name: rettiwtTweet.tweetBy.fullName, - created_at: rettiwtTweet.tweetBy.createdAt, // Assuming this field exists - description: rettiwtTweet.tweetBy.description || '', - verified: rettiwtTweet.tweetBy.isVerified || false, - profile_image_url: rettiwtTweet.tweetBy.profileImage || '' - } - ], - tweets: [] // Placeholder, as no quoted or retweeted tweets are in rettiwt object - } - }; -} - /** * @implements ICustomProvider */ + +function mapTweetEntity(tweet) { + return { + id: tweet?.id, + created_at: new Date(tweet?.createdAt), + text: tweet?.fullText, + public_metrics: { + retweet_count: tweet?.retweetCount || 0, + like_count: tweet?.likeCount || 0, + reply_count: tweet?.replyCount || 0, + view_count: tweet?.viewCount || 0 + }, + author_id: tweet?.tweetBy?.id, + entities: { + mentions: (tweet?.entities?.mentionedUsers || []).map(user => ({ + start: 0, // Update with actual start index if available + end: 0, // Update with actual end index if available + username: user?.userName + })), + hashtags: (tweet?.entities?.hashtags || []).map(hashtag => ({ + start: 0, // Update with actual start index if available + end: 0, // Update with actual end index if available + tag: hashtag?.tag || hashtag + })), + urls: (tweet?.entities?.urls || []).map(url => ({ + start: 0, // Update with actual start index if available + end: 0, // Update with actual end index if available + url: url?.url, + display_url: url?.displayUrl || url?.url, + expanded_url: url?.expandedUrl || url?.url + })) + }, + users: [ + { + id: tweet?.tweetBy?.id, + name: tweet?.tweetBy?.fullName, + username: tweet?.tweetBy?.userName, + profile_image_url: tweet?.tweetBy?.profileImage, + description: tweet?.tweetBy?.description, + verified: tweet?.tweetBy?.isVerified, + location: tweet?.tweetBy?.location + } + ], + attachments: { + media_keys: tweet?.media ? tweet?.media.map((_, index) => `media_${index + 1}`) : [] + }, + includes: { + media: (tweet?.media || []).map((media, index) => ({ + media_key: `media_${index + 1}`, + type: media?.type, + url: media?.url, + preview_image_url: media?.previewUrl || media?.url + })) + } + }; +} class RettiwtOEmbedProvider { /** * @param {object} dependencies @@ -74,11 +84,10 @@ class RettiwtOEmbedProvider { /** * @param {URL} url - * @param {IExternalRequest} externalRequest * * @returns {Promise} */ - async getOEmbedData(url, externalRequest) { + async getOEmbedData(url) { if (url.host === 'x.com') { // api is still at twitter.com... also not certain how people are getting x urls because twitter currently redirects every x host to twitter url = new URL('https://twitter.com' + url.pathname); } @@ -106,11 +115,9 @@ class RettiwtOEmbedProvider { }).join('&'); try { - console.log('fetching tweet data'); - const rettiwt = new Rettiwt(); - const tweet = await rettiwt.tweet.details(tweetId); - // flatten tweet and set as oembedData.tweet_data - oembedData.tweet_data = mapTweetDetails(tweet); + // const tweet = await .request(EResourceType.TWEET_DETAILS, {id: tweetId, query: queryString}); + const tweet = await this.dependencies.externalRequest.tweet.details(tweetId, queryString); + oembedData.tweet_data = mapTweetEntity(tweet); } catch (err) { if (err.response?.body) { try { @@ -124,7 +131,6 @@ class RettiwtOEmbedProvider { } oembedData.type = 'twitter'; - console.log('oembedData---', oembedData); return oembedData; } } diff --git a/ghost/core/core/server/services/oembed/service.js b/ghost/core/core/server/services/oembed/service.js index 9163fdf466..cd1978d409 100644 --- a/ghost/core/core/server/services/oembed/service.js +++ b/ghost/core/core/server/services/oembed/service.js @@ -1,6 +1,7 @@ const config = require('../../../shared/config'); const storage = require('../../adapters/storage'); const externalRequest = require('../../lib/request-external'); +const {Rettiwt} = require('rettiwt-api'); const OEmbed = require('@tryghost/oembed-service'); const oembed = new OEmbed({config, externalRequest, storage}); @@ -12,18 +13,10 @@ const nft = new NFT({ } }); -// const Twitter = require('./TwitterOEmbedProvider'); -// const twitter = new Twitter({ -// config: { -// bearerToken: config.get('twitter').privateReadOnlyToken -// } -// }); - const Twitter = require('./RettiwtOEmbedProvider'); +const fetcher = new Rettiwt(); const twitter = new Twitter({ - config: { - bearerToken: config.get('twitter').privateReadOnlyToken - } + externalRequest: fetcher }); oembed.registerProvider(nft);