diff --git a/helpers/mock-agent/endpoint-syndicate.js b/helpers/mock-agent/endpoint-syndicate.js index ea183d606..355140f31 100644 --- a/helpers/mock-agent/endpoint-syndicate.js +++ b/helpers/mock-agent/endpoint-syndicate.js @@ -9,19 +9,55 @@ export function mockClient() { agent.disableNetConnect(); agent.enableNetConnect(/(?:127\.0\.0\.1:\d{5})/); - const origin = "https://store.example"; + const statusId = "1234567890987654321"; + const storeOrigin = "https://store.example"; + const syndicatorOrigin = "https://mastodon.example"; + const syndicatorResponseOptions = { + headers: { "Content-type": "application/json" }, + }; // Create file on content store agent - .get(origin) + .get(storeOrigin) .intercept({ path: /\/user\/.*\.(md|jpg)/, method: "PUT" }) .reply(201); // Update file on content store agent - .get(origin) + .get(storeOrigin) .intercept({ path: /\/user\/.*\.(md|jpg)/, method: "PATCH" }) .reply(201); + // Get instance information from syndication target + agent + .get(syndicatorOrigin) + .intercept({ + path: `/api/v1/instance`, + }) + .reply( + 200, + { + uri: syndicatorOrigin, + urls: { streaming_api: "https://streaming.mastodon.example" }, + version: "4.1.2", + }, + syndicatorResponseOptions + ) + .persist(); + + // Post status to syndication target + agent + .get(syndicatorOrigin) + .intercept({ path: `/api/v1/statuses`, method: "POST" }) + .reply( + 200, + { + id: statusId, + url: `https://mastodon.example/@username/${statusId}`, + }, + syndicatorResponseOptions + ) + .persist(); + return agent; } diff --git a/helpers/mock-agent/syndicator-mastodon.js b/helpers/mock-agent/syndicator-mastodon.js new file mode 100644 index 000000000..de6075d0a --- /dev/null +++ b/helpers/mock-agent/syndicator-mastodon.js @@ -0,0 +1,126 @@ +import { MockAgent } from "undici"; +import { getFixture } from "@indiekit-test/fixtures"; + +/** + * @returns {import("undici").MockAgent} Undici MockAgent + * @see {@link https://undici.nodejs.org/#/docs/api/MockAgent} + */ +export function mockClient() { + const agent = new MockAgent(); + agent.disableNetConnect(); + + const id = "1234567890987654321"; + const instanceOrigin = "https://mastodon.example"; + const instanceResponse = { + uri: instanceOrigin, + urls: { streaming_api: "https://streaming.mastodon.example" }, + version: "4.1.2", + }; + const statusResponse = { + id, + url: `https://mastodon.example/@username/${id}`, + }; + const responseOptions = { + headers: { "Content-type": "application/json" }, + }; + const websiteOrigin = "https://website.example"; + + // Instance information + agent + .get(instanceOrigin) + .intercept({ + path: `/api/v1/instance`, + headers: { authorization: "Bearer token" }, + }) + .reply(200, instanceResponse, responseOptions) + .persist(); + + // Instance information (Unauthorized, invalid access token) + agent + .get(instanceOrigin) + .intercept({ + path: `/api/v1/instance`, + headers: { + authorization: "Bearer invalid", + }, + }) + .reply(401) + .persist(); + + // Instance information (Unauthorized, no access token provided) + agent + .get(instanceOrigin) + .intercept({ + path: `/api/v1/instance`, + }) + .reply(401) + .persist(); + + // Post favourite + agent + .get(instanceOrigin) + .intercept({ path: `/api/v1/statuses/${id}/favourite`, method: "POST" }) + .reply(200, statusResponse, responseOptions) + .persist(); + + // Post reblog + agent + .get(instanceOrigin) + .intercept({ path: `/api/v1/statuses/${id}/reblog`, method: "POST" }) + .reply(200, statusResponse, responseOptions) + .persist(); + + // Post status + agent + .get(instanceOrigin) + .intercept({ path: `/api/v1/statuses`, method: "POST" }) + .reply(200, statusResponse, responseOptions) + .persist(); + + // Media information + agent + .get(instanceOrigin) + .intercept({ path: "/api/v1/media/1" }) + .reply( + 200, + { id: 1, url: `https://mastodon.example/1.jpg` }, + responseOptions + ) + .persist(); + + // Upload media + agent + .get(instanceOrigin) + .intercept({ + path: `/api/v2/media`, + method: "POST", + }) + .reply(202, { id: 1, type: "image" }, responseOptions) + .persist(); + + // Upload media (Not Found) + agent + .get(instanceOrigin) + .intercept({ + path: `/api/v2/media`, + method: "POST", + }) + .reply(404, { error: "Record not found" }, responseOptions) + .persist(); + + // Get media file + agent + .get(websiteOrigin) + .intercept({ path: "/photo1.jpg" }) + .reply(200, getFixture("file-types/photo.jpg", false)) + .persist(); + + // Get media file (Not Found) + agent + .get(websiteOrigin) + .intercept({ path: "/404.jpg" }) + .reply(404, {}) + .persist(); + + return agent; +} diff --git a/package-lock.json b/package-lock.json index 660ea25a6..81352a676 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,8 +107,7 @@ }, "helpers/server/node_modules/get-port": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.0.0.tgz", - "integrity": "sha512-mDHFgApoQd+azgMdwylJrv2DX47ywGq1i5VFJE7fZ0dttNq3iQMfsU4IvEgBHojA3KqEudyu7Vq+oN8kNaNkWw==", + "license": "MIT", "engines": { "node": ">=16" }, @@ -2376,6 +2375,23 @@ "node": ">=8" } }, + "node_modules/@mastojs/ponyfills": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@mastojs/ponyfills/-/ponyfills-1.0.4.tgz", + "integrity": "sha512-1NaIGmcU7OmyNzx0fk+cYeGTkdXlOJOSdetaC4pStVWsrhht2cdlYSAfe5NDW3FcUmcEm2vVceB9lcClN1RCxw==", + "dependencies": { + "@types/node": "^18.11.17", + "@types/node-fetch": "^2.6.2", + "abort-controller": "^3.0.0", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@mastojs/ponyfills/node_modules/@types/node": { + "version": "18.16.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.19.tgz", + "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==" + }, "node_modules/@messageformat/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.2.0.tgz", @@ -3951,19 +3967,33 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==" }, - "node_modules/@types/oauth": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.1.tgz", - "integrity": "sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -4022,14 +4052,6 @@ "@types/webidl-conversions": "*" } }, - "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@vitejs/plugin-vue": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz", @@ -4405,6 +4427,17 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -4492,33 +4525,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/aggregate-error/node_modules/clean-stack": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", - "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/aggregate-error/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -5447,6 +5453,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -5490,6 +5505,16 @@ } ] }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, "node_modules/cbor": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", @@ -5547,6 +5572,25 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -5644,14 +5688,15 @@ } }, "node_modules/clean-stack": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", - "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, "dependencies": { "escape-string-regexp": "5.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5661,6 +5706,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, "engines": { "node": ">=12" }, @@ -6030,6 +6076,16 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6624,11 +6680,6 @@ "node": "*" } }, - "node_modules/dayjs": { - "version": "1.11.9", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz", - "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -6927,6 +6978,15 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -7473,6 +7533,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -8654,6 +8722,15 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -9426,6 +9503,14 @@ "node": ">=0.10.0" } }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", @@ -11160,6 +11245,14 @@ "node": ">=8" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -11337,6 +11430,25 @@ "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==", "dev": true }, + "node_modules/masto": { + "version": "5.11.4", + "resolved": "https://registry.npmjs.org/masto/-/masto-5.11.4.tgz", + "integrity": "sha512-sLF3SJTNZDAP57Y+8vAdd1KQTuWWxmGUrBF1R2GLPL6zij/1wXxV05+h8GZhnfg+696arkt+w6ZlKvEEfH1yvg==", + "dependencies": { + "@mastojs/ponyfills": "^1.0.4", + "change-case": "^4.1.2", + "eventemitter3": "^5.0.0", + "isomorphic-ws": "^5.0.0", + "qs": "^6.11.0", + "semver": "^7.3.7", + "ws": "^8.13.0" + } + }, + "node_modules/masto/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/matcher": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz", @@ -11425,77 +11537,6 @@ "node": ">= 0.6" } }, - "node_modules/megalodon": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-6.1.2.tgz", - "integrity": "sha512-sLftVOMUvgaBGcZ/hKAyLI2PcyZIbcqrGGsNX5UzPBX9GbCmIBub1BM7MNSs25zAJhAJiA3BuHB63k4eb5ul3Q==", - "dependencies": { - "@types/oauth": "^0.9.0", - "@types/ws": "^8.5.5", - "axios": "1.4.0", - "dayjs": "^1.11.8", - "form-data": "^4.0.0", - "https-proxy-agent": "^7.0.0", - "oauth": "^0.10.0", - "object-assign-deep": "^0.4.0", - "parse-link-header": "^2.0.0", - "socks-proxy-agent": "^8.0.1", - "typescript": "5.0.4", - "uuid": "^9.0.0", - "ws": "8.13.0" - }, - "engines": { - "node": ">=15.0.0" - } - }, - "node_modules/megalodon/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/megalodon/node_modules/https-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", - "integrity": "sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/megalodon/node_modules/socks-proxy-agent": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz", - "integrity": "sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ==", - "dependencies": { - "agent-base": "^7.0.1", - "debug": "^4.3.4", - "socks": "^2.7.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/megalodon/node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=12.20" - } - }, "node_modules/mem": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/mem/-/mem-9.0.2.tgz", @@ -12215,6 +12256,15 @@ "isarray": "0.0.1" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/nock": { "version": "13.3.2", "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.2.tgz", @@ -12939,11 +12989,6 @@ "node": ">=12" } }, - "node_modules/oauth": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz", - "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==" - }, "node_modules/oauth-1.0a": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", @@ -12957,14 +13002,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-assign-deep": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-assign-deep/-/object-assign-deep-0.4.0.tgz", - "integrity": "sha512-54Uvn3s+4A/cMWx9tlRez1qtc7pN7pbQ+Yi7mjLjcBpWLlP+XbSHiHbQW6CElDiV4OvuzqnMrBdkgxI1mT8V/Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -13398,6 +13435,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13439,14 +13485,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/parse-link-header": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-link-header/-/parse-link-header-2.0.0.tgz", - "integrity": "sha512-xjU87V0VyHZybn2RrCX5TIFGxTVZE6zqqZWMPlIKiSKuWh/X5WZdt+w1Ki1nXB+8L/KtL+nZ4iq+sfI6MrhhMw==", - "dependencies": { - "xtend": "~4.0.1" - } - }, "node_modules/parse-ms": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", @@ -13519,6 +13557,24 @@ "node": ">= 0.8" } }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -15217,6 +15273,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -15534,6 +15600,15 @@ "npm": ">= 3.0.0" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -17077,6 +17152,22 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -18032,8 +18123,7 @@ }, "packages/create-indiekit/node_modules/chalk": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -18242,14 +18332,38 @@ "node": ">=18" } }, + "packages/indiekit/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/indiekit/node_modules/commander": { "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "license": "MIT", "engines": { "node": ">=16" } }, + "packages/indiekit/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/preset-hugo": { "name": "@indiekit/preset-hugo", "version": "1.0.0-beta.4", @@ -18265,8 +18379,7 @@ }, "packages/preset-hugo/node_modules/camelcase": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -18276,8 +18389,7 @@ }, "packages/preset-hugo/node_modules/camelcase-keys": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-8.0.2.tgz", - "integrity": "sha512-qMKdlOfsjlezMqxkUGGMaWWs17i2HoL15tM+wtx8ld4nLrUwU58TFdvyGOz/piNP842KeO8yXvggVQSdQ828NA==", + "license": "MIT", "dependencies": { "camelcase": "^7.0.0", "map-obj": "^4.3.0", @@ -18293,8 +18405,7 @@ }, "packages/preset-hugo/node_modules/quick-lru": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.1.tgz", - "integrity": "sha512-S27GBT+F0NTRiehtbrgaSE1idUAJ5bX8dPAQTdylEyNlrdcH5X4Lz7Edz3DYzecbsCluD5zO8ZNEe04z3D3u6Q==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -18304,8 +18415,7 @@ }, "packages/preset-hugo/node_modules/type-fest": { "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -18411,10 +18521,9 @@ "dependencies": { "@indiekit/error": "^1.0.0-beta.3", "@indiekit/util": "^1.0.0-beta.4", - "axios": "^1.1.3", "brevity": "^0.2.9", "html-to-text": "^9.0.0", - "megalodon": "^6.0.0" + "masto": "^5.11.3" }, "engines": { "node": ">=18" diff --git a/packages/endpoint-syndicate/tests/integration/200-syndicates-recent-post.js b/packages/endpoint-syndicate/tests/integration/200-syndicates-recent-post.js index 198e2df42..ed2165c1a 100644 --- a/packages/endpoint-syndicate/tests/integration/200-syndicates-recent-post.js +++ b/packages/endpoint-syndicate/tests/integration/200-syndicates-recent-post.js @@ -1,7 +1,6 @@ import { createHash } from "node:crypto"; import process from "node:process"; import test from "ava"; -import nock from "nock"; import supertest from "supertest"; import jwt from "jsonwebtoken"; import { mockAgent } from "@indiekit-test/mock-agent"; @@ -16,10 +15,6 @@ test.beforeEach(() => { }); test("Syndicates recent post (via Netlify webhook)", async (t) => { - nock("https://mastodon.example").post("/api/v1/statuses").reply(200, { - url: "https://mastodon.example/@username/1234567890987654321", - }); - const sha256 = createHash("sha256").update("foo").digest("hex"); const webhookSignature = jwt.sign( { iss: "netlify", sha256 }, diff --git a/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-multiple-targets.js b/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-multiple-targets.js index 8202d57ee..f3c93e190 100644 --- a/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-multiple-targets.js +++ b/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-multiple-targets.js @@ -1,5 +1,4 @@ import test from "ava"; -import nock from "nock"; import sinon from "sinon"; import supertest from "supertest"; import { mockAgent } from "@indiekit-test/mock-agent"; @@ -12,10 +11,6 @@ await mockAgent("endpoint-syndicate"); test("Syndicates a URL to multiple targets (one fails)", async (t) => { sinon.stub(console, "error"); - nock("https://mastodon.example").post("/api/v1/statuses").reply(200, { - url: "https://mastodon.example/@username/1234567890987654321", - }); - const server = await testServer({ plugins: [ "@indiekit/syndicator-internet-archive", diff --git a/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-target.js b/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-target.js index b39e8d879..090b2b675 100644 --- a/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-target.js +++ b/packages/endpoint-syndicate/tests/integration/200-syndicates-url-for-target.js @@ -1,5 +1,4 @@ import test from "ava"; -import nock from "nock"; import supertest from "supertest"; import { mockAgent } from "@indiekit-test/mock-agent"; import { testServer } from "@indiekit-test/server"; @@ -8,10 +7,6 @@ import { testToken } from "@indiekit-test/token"; await mockAgent("endpoint-syndicate"); test("Syndicates a URL", async (t) => { - nock("https://mastodon.example").post("/api/v1/statuses").reply(200, { - url: "https://mastodon.example/@username/1234567890987654321", - }); - const server = await testServer({ plugins: ["@indiekit/syndicator-mastodon"], }); diff --git a/packages/endpoint-syndicate/tests/integration/302-syndicates-url-with-redirect.js b/packages/endpoint-syndicate/tests/integration/302-syndicates-url-with-redirect.js index 07c575acc..5dc61aa9c 100644 --- a/packages/endpoint-syndicate/tests/integration/302-syndicates-url-with-redirect.js +++ b/packages/endpoint-syndicate/tests/integration/302-syndicates-url-with-redirect.js @@ -1,5 +1,4 @@ import test from "ava"; -import nock from "nock"; import supertest from "supertest"; import { mockAgent } from "@indiekit-test/mock-agent"; import { testServer } from "@indiekit-test/server"; @@ -8,10 +7,6 @@ import { testToken } from "@indiekit-test/token"; await mockAgent("endpoint-syndicate"); test("Syndicates a URL and redirects", async (t) => { - nock("https://mastodon.example").post("/api/v1/statuses").reply(200, { - url: "https://mastodon.example/@username/1234567890987654321", - }); - const server = await testServer({ plugins: ["@indiekit/syndicator-mastodon"], }); diff --git a/packages/endpoint-syndicate/tests/integration/403-syndicates-url-missing-update-scope.js b/packages/endpoint-syndicate/tests/integration/403-syndicates-url-missing-update-scope.js index 8e968e188..622472974 100644 --- a/packages/endpoint-syndicate/tests/integration/403-syndicates-url-missing-update-scope.js +++ b/packages/endpoint-syndicate/tests/integration/403-syndicates-url-missing-update-scope.js @@ -1,5 +1,4 @@ import test from "ava"; -import nock from "nock"; import supertest from "supertest"; import { mockAgent } from "@indiekit-test/mock-agent"; import { testServer } from "@indiekit-test/server"; @@ -8,10 +7,6 @@ import { testToken } from "@indiekit-test/token"; await mockAgent("endpoint-syndicate"); test("Syndicates a URL and redirects", async (t) => { - nock("https://mastodon.example").post("/api/v1/statuses").reply(200, { - url: "https://mastodon.example/@username/1234567890987654321", - }); - const access_token = testToken({ scope: "create" }); const server = await testServer({ plugins: ["@indiekit/syndicator-mastodon"], diff --git a/packages/syndicator-mastodon/index.js b/packages/syndicator-mastodon/index.js index 3ba624c1f..43ad1446e 100644 --- a/packages/syndicator-mastodon/index.js +++ b/packages/syndicator-mastodon/index.js @@ -91,7 +91,7 @@ export default class MastodonSyndicator { throw new IndiekitError(error.message, { cause: error, plugin: this.name, - status: error.status, + status: error.statusCode, }); } } diff --git a/packages/syndicator-mastodon/lib/mastodon.js b/packages/syndicator-mastodon/lib/mastodon.js index afe118655..07956a8aa 100644 --- a/packages/syndicator-mastodon/lib/mastodon.js +++ b/packages/syndicator-mastodon/lib/mastodon.js @@ -1,12 +1,14 @@ +import { IndiekitError } from "@indiekit/error"; import { getCanonicalUrl, isSameOrigin } from "@indiekit/util"; -import axios from "axios"; -import megalodon from "megalodon"; +import { login } from "masto"; import { createStatus, getStatusIdFromUrl } from "./utils.js"; export const mastodon = ({ accessToken, characterLimit, serverUrl }) => ({ - client() { - const generator = megalodon.default; - return generator("mastodon", serverUrl, accessToken); + async client() { + return login({ + accessToken: accessToken, + url: serverUrl, + }); }, /** @@ -15,9 +17,10 @@ export const mastodon = ({ accessToken, characterLimit, serverUrl }) => ({ * @returns {Promise} Mastodon status URL */ async postFavourite(statusUrl) { + const { v1 } = await this.client(); const statusId = getStatusIdFromUrl(statusUrl); - const { data } = await this.client().favouriteStatus(statusId); - return data.url; + const status = await v1.statuses.favourite(statusId); + return status.url; }, /** @@ -26,9 +29,10 @@ export const mastodon = ({ accessToken, characterLimit, serverUrl }) => ({ * @returns {Promise} Mastodon status URL */ async postReblog(statusUrl) { + const { v1 } = await this.client(); const statusId = getStatusIdFromUrl(statusUrl); - const { data } = await this.client().reblogStatus(statusId); - return data.url; + const status = await v1.statuses.reblog(statusId); + return status.url; }, /** @@ -37,11 +41,9 @@ export const mastodon = ({ accessToken, characterLimit, serverUrl }) => ({ * @returns {Promise} Mastodon status URL */ async postStatus(parameters) { - const { data } = await this.client().postStatus(parameters.status, { - in_reply_to_id: parameters.in_reply_to_status_id, - media_ids: parameters.media_ids, - }); - return data.url; + const { v1 } = await this.client(); + const status = await v1.statuses.create(parameters); + return status.url; }, /** @@ -59,15 +61,22 @@ export const mastodon = ({ accessToken, characterLimit, serverUrl }) => ({ try { const mediaUrl = getCanonicalUrl(url, me); - const response = await axios(mediaUrl, { - responseType: "stream", - }); - const { data } = await this.client().uploadMedia(response.data, { + const mediaResponse = await fetch(mediaUrl); + + if (!mediaResponse.ok) { + throw await IndiekitError.fromFetch(mediaResponse); + } + + const { v2 } = await this.client(); + const blob = await mediaResponse.blob(); + const attachment = await v2.mediaAttachments.create({ + file: new Blob([blob]), description: alt, }); - return data.id; + + return attachment.id; } catch (error) { - const message = error.response?.data?.error || error.message; + const message = error.message; throw new Error(message); } }, diff --git a/packages/syndicator-mastodon/lib/utils.js b/packages/syndicator-mastodon/lib/utils.js index 48a73e747..7998f79bc 100644 --- a/packages/syndicator-mastodon/lib/utils.js +++ b/packages/syndicator-mastodon/lib/utils.js @@ -9,7 +9,7 @@ import { htmlToText } from "html-to-text"; * @param {object} [options] - Options * @param {number} [options.characterLimit] - Character limit * @param {Array} [options.mediaIds] - Mastodon media IDs - * @param {string} [options.serverUrl] - Server URL, i.e. https://mastodon.social + * @param {string} [options.serverUrl] - Server URL * @returns {object} Status parameters */ export const createStatus = (properties, options = {}) => { @@ -47,7 +47,7 @@ export const createStatus = (properties, options = {}) => { // Add media IDs if (mediaIds) { - parameters.media_ids = mediaIds; + parameters.mediaIds = mediaIds; } // If post is in reply to a status, add respective parameter @@ -59,7 +59,7 @@ export const createStatus = (properties, options = {}) => { if (inReplyToHostname === serverHostname) { // Reply to status const statusId = getStatusIdFromUrl(inReplyTo); - parameters.in_reply_to_status_id = statusId; + parameters.inReplyToId = statusId; } else { throw IndiekitError.badRequest("Not a reply to a URL at this target"); } diff --git a/packages/syndicator-mastodon/package.json b/packages/syndicator-mastodon/package.json index ed23fa1cb..6f9a38ceb 100644 --- a/packages/syndicator-mastodon/package.json +++ b/packages/syndicator-mastodon/package.json @@ -36,10 +36,9 @@ "dependencies": { "@indiekit/error": "^1.0.0-beta.3", "@indiekit/util": "^1.0.0-beta.4", - "axios": "^1.1.3", "brevity": "^0.2.9", "html-to-text": "^9.0.0", - "megalodon": "^6.0.0" + "masto": "^5.11.3" }, "publishConfig": { "access": "public" diff --git a/packages/syndicator-mastodon/tests/index.js b/packages/syndicator-mastodon/tests/index.js index 90319cfb8..2f2428046 100644 --- a/packages/syndicator-mastodon/tests/index.js +++ b/packages/syndicator-mastodon/tests/index.js @@ -1,9 +1,11 @@ import test from "ava"; -import nock from "nock"; import { Indiekit } from "@indiekit/indiekit"; import { getFixture } from "@indiekit-test/fixtures"; +import { mockAgent } from "@indiekit-test/mock-agent"; import MastodonSyndicator from "../index.js"; +await mockAgent("syndicator-mastodon"); + const mastodon = new MastodonSyndicator({ accessToken: "token", url: "https://mastodon.example", @@ -12,21 +14,12 @@ const mastodon = new MastodonSyndicator({ test.beforeEach((t) => { t.context = { - apiResponse: { - emojis: [], - id: "1234567890987654321", - media_attachments: [], - mentions: [], - tags: [], - url: "https://mastodon.example/@username/1234567890987654321", - }, properties: JSON.parse( getFixture("jf2/article-content-provided-html-text.jf2") ), publication: { me: "https://website.example", }, - instanceUrl: "https://mastodon.example", }; }); @@ -53,10 +46,7 @@ test("Initiates plug-in", async (t) => { }); test("Returns syndicated URL", async (t) => { - const { apiResponse, instanceUrl, properties, publication } = t.context; - - nock(instanceUrl).post("/api/v1/statuses").reply(200, apiResponse); - + const { properties, publication } = t.context; const result = await mastodon.syndicate(properties, publication); t.is(result, "https://mastodon.example/@username/1234567890987654321"); @@ -77,7 +67,7 @@ test("Throws error getting syndicated URL if no server URL provided", async (t) test("Throws error getting username if no username provided", (t) => { const mastodonNoUser = new MastodonSyndicator({ accessToken: "token", - url: t.context.instanceUrl, + url: "https://mastodon.example", }); t.throws( @@ -89,24 +79,13 @@ test("Throws error getting username if no username provided", (t) => { }); test("Throws error getting syndicated URL if no access token provided", async (t) => { - const { instanceUrl, properties, publication } = t.context; - - nock(instanceUrl) - .post("/api/v1/statuses") - .reply(401, { - errors: [ - { - message: "Mastodon syndicator: Request failed with status code 401", - }, - ], - }); - + const { properties, publication } = t.context; const mastodonNoToken = new MastodonSyndicator({ - url: instanceUrl, + url: "https://mastodon.example", user: "username", }); await t.throwsAsync(mastodonNoToken.syndicate(properties, publication), { - message: "Mastodon syndicator: Request failed with status code 401", + message: "Mastodon syndicator: Unexpected error occurred", }); }); diff --git a/packages/syndicator-mastodon/tests/unit/mastodon.js b/packages/syndicator-mastodon/tests/unit/mastodon.js index a583601bc..4237d4630 100644 --- a/packages/syndicator-mastodon/tests/unit/mastodon.js +++ b/packages/syndicator-mastodon/tests/unit/mastodon.js @@ -1,238 +1,146 @@ import test from "ava"; -import nock from "nock"; -import { getFixture } from "@indiekit-test/fixtures"; +import { mockAgent } from "@indiekit-test/mock-agent"; import { mastodon } from "../../lib/mastodon.js"; +await mockAgent("syndicator-mastodon"); + test.beforeEach((t) => { t.context = { - apiResponse: { - emojis: [], - id: "1234567890987654321", - media_attachments: [], - mentions: [], - tags: [], - url: "https://mastodon.example/@username/1234567890987654321", - }, me: "https://website.example", - media: { - url: "https://website.example/image.jpg", - alt: "Example image", - }, options: { accessToken: "token", serverUrl: "https://mastodon.example", }, - status: "Toot", + photo: [ + { + url: "https://website.example/photo1.jpg", + alt: "Example image", + }, + ], + statusParameters: { status: "Toot" }, statusId: "1234567890987654321", statusUrl: "https://mastodon.example/@username/1234567890987654321", }; }); test("Posts a favourite", async (t) => { - nock(t.context.options.serverUrl) - .post(`/api/v1/statuses/${t.context.statusId}/favourite`) - .reply(200, t.context.apiResponse); - - const result = await mastodon(t.context.options).postFavourite( - t.context.statusUrl - ); + const { options, statusId, statusUrl } = t.context; + const result = await mastodon(options).postFavourite(statusUrl); - t.is(result, `https://mastodon.example/@username/${t.context.statusId}`); + t.is(result, `https://mastodon.example/@username/${statusId}`); }); test("Throws error posting a favourite", async (t) => { - nock(t.context.options.serverUrl) - .post(`/api/v1/statuses/${t.context.statusId}/favourite`) - .reply(404, { message: "Not found" }); + const { options, statusUrl } = t.context; + options.accessToken = "invalid"; - await t.throwsAsync( - mastodon(t.context.options).postFavourite(t.context.statusUrl), - { - message: "Request failed with status code 404", - } - ); + await t.throwsAsync(mastodon(options).postFavourite(statusUrl), { + message: "Unexpected error occurred", + }); }); test("Posts a reblog", async (t) => { - nock(t.context.options.serverUrl) - .post(`/api/v1/statuses/${t.context.statusId}/reblog`) - .reply(200, t.context.apiResponse); + const { options, statusId, statusUrl } = t.context; + const result = await mastodon(options).postReblog(statusUrl); - const result = await mastodon(t.context.options).postReblog( - t.context.statusUrl - ); - - t.is(result, `https://mastodon.example/@username/${t.context.statusId}`); + t.is(result, `https://mastodon.example/@username/${statusId}`); }); test("Throws error posting a reblog", async (t) => { - nock(t.context.options.serverUrl) - .post(`/api/v1/statuses/${t.context.statusId}/reblog`) - .reply(404, { message: "Not found" }); + const { options, statusUrl } = t.context; + options.accessToken = "invalid"; - await t.throwsAsync( - mastodon(t.context.options).postReblog(t.context.statusUrl), - { - message: "Request failed with status code 404", - } - ); + await t.throwsAsync(mastodon(options).postReblog(statusUrl), { + message: "Unexpected error occurred", + }); }); test("Posts a status", async (t) => { - nock(t.context.options.serverUrl) - .post("/api/v1/statuses") - .reply(200, t.context.apiResponse); + const { options, statusParameters, statusId } = t.context; + const result = await mastodon(options).postStatus(statusParameters); - const result = await mastodon(t.context.options).postStatus(t.context.status); - - t.is(result, "https://mastodon.example/@username/1234567890987654321"); + t.is(result, `https://mastodon.example/@username/${statusId}`); }); test("Throws error posting a status", async (t) => { - nock(t.context.options.serverUrl) - .post("/api/v1/statuses") - .reply(404, { message: "Not found" }); + const { options, statusParameters } = t.context; + options.accessToken = "invalid"; - await t.throwsAsync( - mastodon(t.context.options).postStatus(t.context.status), - { - message: "Request failed with status code 404", - } - ); -}); - -test.serial("Throws error fetching media to upload", async (t) => { - nock("https://website.example").get("/image.jpg").replyWithError("Not found"); - - await t.throwsAsync( - mastodon(t.context.options).uploadMedia(t.context.media, t.context.me), - { - message: /Not found/, - } - ); -}); - -// Fails as Nock doesn’t send _httpMessage.path value used by form-data module -test.failing("Uploads media and returns a media id", async (t) => { - nock("https://website.example") - .get("/image.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock(t.context.options.serverUrl).post("/api/v1/media").reply(200, { - id: "1234567890987654321", + await t.throwsAsync(mastodon(options).postStatus(statusParameters), { + message: "Unexpected error occurred", }); - nock(t.context.options.serverUrl).post("/api/v1/media").reply(200, {}); - - const result = await mastodon(t.context.options).uploadMedia( - t.context.media, - t.context.me - ); - - t.is(result, "1234567890987654321"); }); -// Fails as Nock doesn’t send _httpMessage.path value used by form-data module -test.failing("Throws error uploading media", async (t) => { - nock("https://website.example") - .get("/image.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock(t.context.options.serverUrl) - .post("/api/v1/media") - .reply(404, { message: "Not found" }); - +test("Throws error fetching media to upload", async (t) => { + const { me, options } = t.context; await t.throwsAsync( - mastodon(t.context.options).uploadMedia(t.context.media, t.context.me), + mastodon(options).uploadMedia({ url: `${me}/404.jpg` }, me), { - message: "Request failed with status code 404", + message: "Not Found", } ); }); test("Returns false passing an object to media upload function", async (t) => { - const result = await mastodon(t.context.options).uploadMedia( - { foo: "bar" }, - t.context.me - ); + const { me, options } = t.context; + const result = await mastodon(options).uploadMedia({ foo: "bar" }, me); t.falsy(result); }); test("Posts a favourite of a Mastodon status to Mastodon", async (t) => { - nock(t.context.options.serverUrl) - .post(`/api/v1/statuses/${t.context.statusId}/favourite`) - .reply(200, t.context.apiResponse); - - const result = await mastodon(t.context.options).post( - { - "like-of": t.context.statusUrl, - }, - t.context.me - ); + const { me, options, statusUrl } = t.context; + const result = await mastodon(options).post({ "like-of": statusUrl }, me); t.is(result, "https://mastodon.example/@username/1234567890987654321"); }); test("Doesn’t post a favourite of a URL to Mastodon", async (t) => { - const result = await mastodon(t.context.options).post( - { - "like-of": "https://foo.bar/lunchtime", - }, - t.context.me + const { me, options } = t.context; + const result = await mastodon(options).post( + { "like-of": "https://foo.bar/lunchtime" }, + me ); t.falsy(result); }); test("Posts a repost of a Mastodon status to Mastodon", async (t) => { - nock(t.context.options.serverUrl) - .post(`/api/v1/statuses/${t.context.statusId}/reblog`) - .reply(200, t.context.apiResponse); - - const result = await mastodon(t.context.options).post( - { - "repost-of": t.context.statusUrl, - }, - t.context.me - ); + const { me, options, statusUrl } = t.context; + const result = await mastodon(options).post({ "repost-of": statusUrl }, me); t.is(result, "https://mastodon.example/@username/1234567890987654321"); }); test("Doesn’t post a repost of a URL to Mastodon", async (t) => { - const result = await mastodon(t.context.options).post( - { - "repost-of": "https://foo.bar/lunchtime", - }, - t.context.me + const { me, options } = t.context; + const result = await mastodon(options).post( + { "repost-of": "https://foo.bar/lunchtime" }, + me ); t.falsy(result); }); test("Posts a quote status to Mastodon", async (t) => { - nock(t.context.options.serverUrl) - .post("/api/v1/statuses") - .reply(200, t.context.apiResponse); - - const result = await mastodon(t.context.options).post( + const { me, options, statusUrl } = t.context; + const result = await mastodon(options).post( { content: { html: "

Someone else who likes cheese sandwiches.

", }, - "repost-of": t.context.statusUrl, + "repost-of": statusUrl, "post-type": "repost", }, - t.context.me + me ); t.is(result, "https://mastodon.example/@username/1234567890987654321"); }); test("Posts a status to Mastodon", async (t) => { - nock(t.context.options.serverUrl) - .post("/api/v1/statuses") - .reply(200, t.context.apiResponse); - - const result = await mastodon(t.context.options).post( + const { me, options } = t.context; + const result = await mastodon(options).post( { content: { html: "

I ate a cheese sandwich, which was nice.

", @@ -240,85 +148,30 @@ test("Posts a status to Mastodon", async (t) => { }, url: "https://foo.bar/lunchtime", }, - t.context.me + me ); t.is(result, "https://mastodon.example/@username/1234567890987654321"); }); -// Fails as Nock doesn’t send _httpMessage.path value used by form-data module -test.failing("Posts a status to Mastodon with 4 out of 5 photos", async (t) => { - nock(t.context.me) - .get("/image1.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock(t.context.me) - .get("/image2.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock(t.context.me) - .get("/image3.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock("https://website.example") - .get("/image4.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock(t.context.options.url).post("/api/v1/media").reply(200, { id: "1" }); - nock(t.context.options.url).post("/api/v1/media").reply(200, { id: "2" }); - nock(t.context.options.url).post("/api/v1/media").reply(200, { id: "3" }); - nock(t.context.options.url).post("/api/v1/media").reply(200, { id: "4" }); - nock(t.context.options.url) - .post("/api/v1/statuses") - .reply(200, t.context.apiResponse); - - const result = await mastodon(t.context.options).post( +test("Posts a status with photo to Mastodon", async (t) => { + const { me, options, photo } = t.context; + const result = await mastodon(options).post( { content: { html: "

Here’s the cheese sandwiches I ate.

", }, - photo: [ - { url: `${t.context.me}/photo1.jpg` }, - { url: `${t.context.me}/photo2.jpg` }, - { url: `${t.context.me}/photo3.jpg` }, - { url: `${t.context.me}/photo4.jpg` }, - { url: `${t.context.me}/photo5.jpg` }, - ], + photo, }, - t.context.me + me ); t.is(result, "https://mastodon.example/@username/1234567890987654321"); }); -test("Throws an error posting a status to Mastodon with 4 out of 5 photos", async (t) => { - nock(t.context.me) - .get("/photo1.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock(t.context.me) - .get("/photo2.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock(t.context.me) - .get("/photo3.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); - nock("https://website.example") - .get("/photo4.jpg") - .reply(200, { body: getFixture("file-types/photo.jpg", false) }); +test("Uploads media and returns a media id", async (t) => { + const { me, options, photo } = t.context; + const result = await mastodon(options).uploadMedia(photo[0], me); - await t.throwsAsync( - mastodon(t.context.options).post( - { - content: { - html: "

Here’s the cheese sandwiches I ate.

", - }, - photo: [ - { url: `${t.context.me}/photo1.jpg` }, - { url: `${t.context.me}/photo2.jpg` }, - { url: `${t.context.me}/photo3.jpg` }, - { url: `${t.context.me}/photo4.jpg` }, - { url: `${t.context.me}/photo5.jpg` }, - ], - }, - t.context.me - ), - { - message: "Cannot read properties of undefined (reading 'path')", - } - ); + t.truthy(result); }); diff --git a/packages/syndicator-mastodon/tests/unit/utils.js b/packages/syndicator-mastodon/tests/unit/utils.js index 930aa30c7..31ac66c03 100644 --- a/packages/syndicator-mastodon/tests/unit/utils.js +++ b/packages/syndicator-mastodon/tests/unit/utils.js @@ -79,7 +79,7 @@ test("Adds link to status post is in reply to", (t) => { ); t.is(result.status, "I ate a cheese sandwich too!"); - t.is(result.in_reply_to_status_id, "1234567890987654321"); + t.is(result.inReplyToId, "1234567890987654321"); }); test("Throws creating a status if post is off-service reply", (t) => { @@ -110,7 +110,7 @@ test("Creates a status with a photo", (t) => { ); t.is(result.status, "Here’s the cheese sandwich I ate."); - t.deepEqual(result.media_ids, ["1", "2", "3", "4"]); + t.deepEqual(result.mediaIds, ["1", "2", "3", "4"]); }); test("Gets status ID from Mastodon permalink", (t) => {