0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Initial custom embed provider for Twitter

refs https://github.com/TryGhost/Team/issues/1001

We fall back to existing behaviour if no API key is present, or if there
is an error communicating with the Twitter API. We're also currently
requesting all the data, which will be thinned down once we understand
what we need.

This also includes a custom renderer for embeds of type "twitter" which
will be used to output the custom HTML for emails
This commit is contained in:
Fabien egg O'Carroll 2021-11-30 10:34:14 +02:00 committed by Fabien 'egg' O'Carroll
parent c4021b6eb4
commit e6856f6ac6
5 changed files with 99 additions and 5 deletions

View file

@ -3,13 +3,24 @@ const externalRequest = require('../../lib/request-external');
const OEmbed = require('../../services/oembed');
const oembed = new OEmbed({config, externalRequest});
const NFT = require('../../services/nft-oembed');
const nft = new NFT({
config: {
apiKey: config.get('opensea').privateReadOnlyApiKey
}
});
const Twitter = require('../../services/twitter-embed');
const twitter = new Twitter({
config: {
bearerToken: config.get('twitter').privateReadOnlyToken
},
logging: require('@tryghost/logging')
});
oembed.registerProvider(nft);
oembed.registerProvider(twitter);
module.exports = {
docName: 'oembed',

View file

@ -0,0 +1,80 @@
const {extract} = require('oembed-parser');
/**
* @typedef {import('./oembed').ICustomProvider} ICustomProvider
* @typedef {import('./oembed').IExternalRequest} IExternalRequest
*/
const TWITTER_PATH_REGEX = /\/status\/(\d+)/;
/**
* @implements ICustomProvider
*/
class TwitterOEmbedProvider {
/**
* @param {object} dependencies
*/
constructor(dependencies) {
this.dependencies = dependencies;
}
/**
* @param {URL} url
* @returns {Promise<boolean>}
*/
async canSupportRequest(url) {
return url.host === 'twitter.com' && TWITTER_PATH_REGEX.test(url.pathname);
}
/**
* @param {URL} url
* @param {IExternalRequest} externalRequest
*
* @returns {Promise<object>}
*/
async getOEmbedData(url, externalRequest) {
const [match, tweetId] = url.pathname.match(TWITTER_PATH_REGEX);
if (!match) {
return null;
}
/** @type {object} */
const oembedData = await extract(url.href);
if (this.dependencies.config.bearerToken) {
const query = {
expansions: ['attachments.poll_ids', 'attachments.media_keys', 'author_id', 'entities.mentions.username', 'geo.place_id', 'in_reply_to_user_id', 'referenced_tweets.id', 'referenced_tweets.id.author_id'],
'media.fields': ['duration_ms', 'height', 'media_key', 'preview_image_url', 'type', 'url', 'width', 'public_metrics', 'alt_text'],
'place.fields': ['contained_within', 'country', 'country_code', 'full_name', 'geo', 'id', 'name', 'place_type'],
'poll.fields': ['duration_minutes', 'end_datetime', 'id', 'options', 'voting_status'],
'tweet.fields': ['attachments', 'author_id', 'context_annotations', 'conversation_id', 'created_at', 'entities', 'geo', 'id', 'in_reply_to_user_id', 'lang', 'public_metrics', 'possibly_sensitive', 'referenced_tweets', 'reply_settings', 'source', 'text', 'withheld'],
'user.fields': ['created_at', 'description', 'entities', 'id', 'location', 'name', 'pinned_tweet_id', 'profile_image_url', 'protected', 'public_metrics', 'url', 'username', 'verified', 'withheld']
};
const queryString = Object.keys(query).map((key) => {
return `${key}=${query[key].join(',')}`;
}).join('&');
try {
const result = await externalRequest(`https://api.twitter.com/2/tweets/${tweetId}?${queryString}`, {
responseType: 'json',
headers: {
Authorization: `Bearer ${this.dependencies.config.bearerToken}`
}
});
const body = JSON.parse(result.body);
oembedData.tweet_data = body.data;
} catch (err) {
this.dependencies.logging.error(err);
}
}
oembedData.type = 'twitter';
return oembedData;
}
}
module.exports = TwitterOEmbedProvider;

View file

@ -136,5 +136,8 @@
},
"opensea": {
"privateReadOnlyApiKey": null
},
"twitter": {
"privateReadOnlyToken": null
}
}

View file

@ -73,7 +73,7 @@
"@tryghost/job-manager": "0.8.13",
"@tryghost/kg-card-factory": "3.1.0",
"@tryghost/kg-default-atoms": "3.1.0",
"@tryghost/kg-default-cards": "5.8.5",
"@tryghost/kg-default-cards": "5.9.0",
"@tryghost/kg-markdown-html-renderer": "5.1.0",
"@tryghost/kg-mobiledoc-html-renderer": "5.3.0",
"@tryghost/limit-service": "1.0.0",

View file

@ -1422,10 +1422,10 @@
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-atoms/-/kg-default-atoms-3.1.0.tgz#4daff7104c1f261b1b816dd75ae4104009f9c1e6"
integrity sha512-FfROzVgqJWqJ7cVdS9dcALz7rnzNfV8zcrymAJrDTHDsqzAdMfSLe1tNQRm8zas9pyZLsD8zBGmLxG9dr4WFSA==
"@tryghost/kg-default-cards@5.8.5":
version "5.8.5"
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-cards/-/kg-default-cards-5.8.5.tgz#e203ea584ac6d82b1481cd87f916968e0d7ce1ee"
integrity sha512-j+dqm9CgRux87aakWw9TngRoOVtj88WaD6GC4BcfImdTwc0T2tF8IymiuMGkTA+5ZnV6B5qB4MBg/HIke1C0Nw==
"@tryghost/kg-default-cards@5.9.0":
version "5.9.0"
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-cards/-/kg-default-cards-5.9.0.tgz#2dbd1bf0545376e505fde69fc4fd525241662283"
integrity sha512-w2zfY/tYmf/uTfgzGajLBavoTCQ1XEN+GaLY7v6GeGPkbLIMsAIW4DG9WdR73P5Sq7sDrTQ5fKoUcI195cEE1g==
dependencies:
"@tryghost/kg-markdown-html-renderer" "^5.1.0"
"@tryghost/url-utils" "^2.0.0"