From 51901959a36d37a91b362c818b63b6854e905b7d Mon Sep 17 00:00:00 2001 From: Paul Robert Lloyd Date: Sat, 13 Feb 2021 19:58:59 +0000 Subject: [PATCH] feat(syndicator-twitter): always use absolute urls for uploading media --- packages/syndicator-twitter/index.js | 4 +- packages/syndicator-twitter/lib/twitter.js | 12 ++++-- packages/syndicator-twitter/lib/utils.js | 16 ++++++++ .../syndicator-twitter/tests/unit/twitter.js | 40 +++++++++++-------- .../syndicator-twitter/tests/unit/utils.js | 14 +++++++ 5 files changed, 63 insertions(+), 23 deletions(-) diff --git a/packages/syndicator-twitter/index.js b/packages/syndicator-twitter/index.js index b7b11f115..82b329aee 100644 --- a/packages/syndicator-twitter/index.js +++ b/packages/syndicator-twitter/index.js @@ -42,7 +42,7 @@ export const TwitterSyndicator = class { return this.info.uid; } - async syndicate(properties) { - return twitter(this.options).post(properties); + async syndicate(properties, publication) { + return twitter(this.options).post(properties, publication); } }; diff --git a/packages/syndicator-twitter/lib/twitter.js b/packages/syndicator-twitter/lib/twitter.js index e882fbef0..9074c9774 100644 --- a/packages/syndicator-twitter/lib/twitter.js +++ b/packages/syndicator-twitter/lib/twitter.js @@ -3,6 +3,7 @@ import got from 'got'; import Twitter from 'twitter-lite'; import { createStatus, + getAbsoluteUrl, getStatusIdFromUrl, isTweetUrl } from './utils.js'; @@ -73,9 +74,10 @@ export const twitter = options => ({ * Upload media and return Twitter media id * * @param {string} media JF2 media object + * @param {string} me Publication URL * @returns {string} Twitter media id */ - async uploadMedia(media) { + async uploadMedia(media, me) { const {alt, url} = media; if (typeof url !== 'string') { @@ -83,7 +85,8 @@ export const twitter = options => ({ } try { - const response = await got(url, {responseType: 'buffer'}); + const mediaUrl = getAbsoluteUrl(url, me); + const response = await got(mediaUrl, {responseType: 'buffer'}); const buffer = Buffer.from(response.body).toString('base64'); const {media_id_string} = await this.client('upload').post('media/upload', {media_data: buffer}); @@ -105,9 +108,10 @@ export const twitter = options => ({ * Post to Twitter * * @param {object} properties JF2 properties object + * @param {object} publication Publication configuration * @returns {string} URL of syndicated tweet */ - async post(properties) { + async post(properties, publication) { let mediaIds = []; // Upload photos @@ -117,7 +121,7 @@ export const twitter = options => ({ // Trim to 4 photos as Twitter doesn’t support more const photos = properties.photo.slice(0, 4); for await (const photo of photos) { - uploads.push(this.uploadMedia(photo)); + uploads.push(this.uploadMedia(photo, publication.me)); } mediaIds = await Promise.all(uploads); diff --git a/packages/syndicator-twitter/lib/utils.js b/packages/syndicator-twitter/lib/utils.js index 8a3130216..76bdcd904 100644 --- a/packages/syndicator-twitter/lib/utils.js +++ b/packages/syndicator-twitter/lib/utils.js @@ -67,6 +67,22 @@ export const createStatus = (properties, mediaIds = false) => { return parameters; }; +/** + * Get absolute URL + * + * @param {string} string URL or path + * @param {string} me Publication URL + * @returns {URL} Absolute URL + */ +export const getAbsoluteUrl = (string, me) => { + try { + return new URL(string).toString(); + } catch { + const absoluteUrl = path.posix.join(me, string); + return new URL(absoluteUrl).toString(); + } +}; + /** * Get status ID from Twitter status URL * diff --git a/packages/syndicator-twitter/tests/unit/twitter.js b/packages/syndicator-twitter/tests/unit/twitter.js index 98dabe882..3d3ae8b05 100644 --- a/packages/syndicator-twitter/tests/unit/twitter.js +++ b/packages/syndicator-twitter/tests/unit/twitter.js @@ -1,4 +1,5 @@ /* eslint-disable camelcase */ +import 'dotenv/config.js'; // eslint-disable-line import/no-unassigned-import import test from 'ava'; import nock from 'nock'; import {getFixture} from '@indiekit-test/get-fixture'; @@ -22,6 +23,9 @@ test.beforeEach(t => { accessTokenKey: 'ABCDEFGHIJKLMNabcdefghijklmnopqrstuvwxyz0123456789', accessTokenSecret: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN', user: 'username' + }, + publication: { + me: process.env.TEST_PUBLICATION_URL } }; }); @@ -133,7 +137,7 @@ test('Throws error fetching media to upload', async t => { .get('/image.jpg') .replyWithError('Not found'); - await t.throwsAsync(twitter(t.context.options).uploadMedia(t.context.media), { + await t.throwsAsync(twitter(t.context.options).uploadMedia(t.context.media, t.context.publication), { message: /Not found/ }); }); @@ -151,7 +155,7 @@ test('Uploads media and returns a media id', async t => { .post('/1.1/media/metadata/create.json') .reply(200, {}); - const result = await twitter(t.context.options).uploadMedia(t.context.media); + const result = await twitter(t.context.options).uploadMedia(t.context.media, t.context.publication); t.is(result, '1234567890987654321'); }); @@ -168,13 +172,13 @@ test('Throws error uploading media', async t => { }] }); - await t.throwsAsync(twitter(t.context.options).uploadMedia(t.context.media), { + await t.throwsAsync(twitter(t.context.options).uploadMedia(t.context.media, t.context.publication), { message: /Not found/ }); }); test('Returns false passing an object to media upload function', async t => { - const result = await twitter(t.context.options).uploadMedia({foo: 'bar'}); + const result = await twitter(t.context.options).uploadMedia({foo: 'bar'}, t.context.publication); t.falsy(result); }); @@ -186,7 +190,7 @@ test('Posts a like of a tweet to Twitter', async t => { const result = await twitter(t.context.options).post({ 'like-of': t.context.tweetUrl - }); + }, t.context.publication); t.is(result, 'https://twitter.com/username/status/1234567890987654321'); }); @@ -194,7 +198,7 @@ test('Posts a like of a tweet to Twitter', async t => { test('Doesn’t post a like of a URL to Twitter', async t => { const result = await twitter(t.context.options).post({ 'like-of': 'https://foo.bar/lunchtime' - }); + }, t.context.publication); t.falsy(result); }); @@ -206,7 +210,7 @@ test('Posts a repost of a tweet to Twitter', async t => { const result = await twitter(t.context.options).post({ 'repost-of': t.context.tweetUrl - }); + }, t.context.publication); t.is(result, 'https://twitter.com/username/status/1234567890987654321'); }); @@ -214,7 +218,7 @@ test('Posts a repost of a tweet to Twitter', async t => { test('Doesn’t post a repost of a URL to Twitter', async t => { const result = await twitter(t.context.options).post({ 'repost-of': 'https://foo.bar/lunchtime' - }); + }, t.context.publication); t.falsy(result); }); @@ -228,7 +232,7 @@ test('Posts a quote status to Twitter', async t => { content: 'Someone else who likes cheese sandwiches.', 'repost-of': t.context.tweetUrl, 'post-type': 'repost' - }); + }, t.context.publication); t.is(result, 'https://twitter.com/username/status/1234567890987654321'); }); @@ -244,19 +248,19 @@ test('Posts a status to Twitter', async t => { text: 'I ate a cheese sandwich, which was nice.' }, url: 'https://foo.bar/lunchtime' - }); + }, t.context.publication); t.is(result, 'https://twitter.com/username/status/1234567890987654321'); }); test('Posts a status to Twitter with 4 out of 5 photos', async t => { - nock('https://website.example') + nock(t.context.publication.me) .get('/image1.jpg') .reply(200, {body: getFixture('file-types/photo.jpg', false)}); - nock('https://website.example') + nock(t.context.publication.me) .get('/image2.jpg') .reply(200, {body: getFixture('file-types/photo.jpg', false)}); - nock('https://website.example') + nock(t.context.publication.me) .get('/image3.jpg') .reply(200, {body: getFixture('file-types/photo.jpg', false)}); nock('https://website.example') @@ -281,13 +285,15 @@ test('Posts a status to Twitter with 4 out of 5 photos', async t => { const result = await twitter(t.context.options).post({ content: 'Here’s the cheese sandwiches I ate.', photo: [ - {url: 'https://website.example/image1.jpg'}, - {url: 'https://website.example/image2.jpg'}, - {url: 'https://website.example/image3.jpg'}, + {url: `${t.context.publication.me}image1.jpg`}, + {url: `${t.context.publication.me}image2.jpg`}, + {url: 'image3.jpg'}, {url: 'https://website.example/image4.jpg'}, {url: 'https://website.example/image5.jpg'} ] - }); + }, t.context.publication); + + t.log(t.context.publication.me); t.is(result, 'https://twitter.com/username/status/1234567890987654321'); }); diff --git a/packages/syndicator-twitter/tests/unit/utils.js b/packages/syndicator-twitter/tests/unit/utils.js index 2acd75ae2..dd36da67d 100644 --- a/packages/syndicator-twitter/tests/unit/utils.js +++ b/packages/syndicator-twitter/tests/unit/utils.js @@ -1,7 +1,9 @@ +import 'dotenv/config.js'; // eslint-disable-line import/no-unassigned-import import test from 'ava'; import {getFixture} from '@indiekit-test/get-fixture'; import { createStatus, + getAbsoluteUrl, getStatusIdFromUrl, htmlToStatusText, isTweetUrl @@ -76,6 +78,18 @@ test('Tests if string is a tweet permalink', t => { t.false(isTweetUrl('https://getindiekit.com')); }); +test('Gets absolute URL', t => { + const result = getAbsoluteUrl(`${process.env.TEST_PUBLICATION_URL}media/photo.jpg`, process.env.TEST_PUBLICATION_URL); + + t.is(result, `${process.env.TEST_PUBLICATION_URL}media/photo.jpg`); +}); + +test('Gets absolute URL by prepending publication URL', t => { + const result = getAbsoluteUrl('/media/photo.jpg', process.env.TEST_PUBLICATION_URL); + + t.is(result, `${process.env.TEST_PUBLICATION_URL}media/photo.jpg`); +}); + test('Gets status ID from Twitter permalink', t => { const result = getStatusIdFromUrl('https://twitter.com/paulrobertlloyd/status/1341502435760680961');