diff --git a/commons/package-lock.json b/commons/package-lock.json index 9477c924..8fa51282 100644 --- a/commons/package-lock.json +++ b/commons/package-lock.json @@ -12,7 +12,7 @@ "@types/node": "^14.11.1", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", - "megalodon": "^6.2.0" + "megalodon": "^7.0.0" } }, "node_modules/@types/node": { @@ -21,9 +21,9 @@ "integrity": "sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw==" }, "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==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.2.tgz", + "integrity": "sha512-Nu3/abQ6yR9VlsCdX3aiGsWFkj6OJvJqDvg/36t8Gwf2mFXdBZXPDN3K+2yfeA6Lo2m1Q12F8Qil9TZ48nWhOQ==", "dependencies": { "@types/node": "*" } @@ -58,9 +58,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -124,9 +124,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", "funding": [ { "type": "individual", @@ -181,23 +181,23 @@ "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" }, "node_modules/megalodon": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-6.2.0.tgz", - "integrity": "sha512-OTnZ8zcg9fhYMTbGDTDEOunSA0/zIqzEu03Ne925aTaAqL7i4gnxiwBHrRusPcA7JS4wnuicAYuwuOXZVrDK0w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-7.0.1.tgz", + "integrity": "sha512-zJhYXw+FNaNkjpY8uJo+C4u6p2yxdPYRmnY4tMY1/kQQca9ZdqvP9a9sT4Yiv7uvQPxwXLjVeWNqWQefkBwT1A==", "dependencies": { - "@types/oauth": "^0.9.0", + "@types/oauth": "^0.9.2", "@types/ws": "^8.5.5", - "axios": "1.4.0", + "axios": "1.5.0", "dayjs": "^1.11.9", "form-data": "^4.0.0", - "https-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.2", "oauth": "^0.10.0", "object-assign-deep": "^0.4.0", "parse-link-header": "^2.0.0", - "socks-proxy-agent": "^8.0.1", + "socks-proxy-agent": "^8.0.2", "typescript": "5.1.6", - "uuid": "^9.0.0", - "ws": "8.13.0" + "uuid": "^9.0.1", + "ws": "8.14.2" }, "engines": { "node": ">=15.0.0" @@ -276,11 +276,11 @@ } }, "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==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", "dependencies": { - "agent-base": "^7.0.1", + "agent-base": "^7.0.2", "debug": "^4.3.4", "socks": "^2.7.1" }, @@ -306,9 +306,13 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -322,9 +326,9 @@ } }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "engines": { "node": ">=10.0.0" }, @@ -357,9 +361,9 @@ "integrity": "sha512-oTQgnd0hblfLsJ6BvJzzSL+Inogp3lq9fGgqRkMB/ziKMgEUaFl801OncOzUmalfzt14N0oPHMK47ipl+wbTIw==" }, "@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==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.2.tgz", + "integrity": "sha512-Nu3/abQ6yR9VlsCdX3aiGsWFkj6OJvJqDvg/36t8Gwf2mFXdBZXPDN3K+2yfeA6Lo2m1Q12F8Qil9TZ48nWhOQ==", "requires": { "@types/node": "*" } @@ -391,9 +395,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -443,9 +447,9 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" }, "form-data": { "version": "4.0.0", @@ -477,23 +481,23 @@ "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==" }, "megalodon": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-6.2.0.tgz", - "integrity": "sha512-OTnZ8zcg9fhYMTbGDTDEOunSA0/zIqzEu03Ne925aTaAqL7i4gnxiwBHrRusPcA7JS4wnuicAYuwuOXZVrDK0w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-7.0.1.tgz", + "integrity": "sha512-zJhYXw+FNaNkjpY8uJo+C4u6p2yxdPYRmnY4tMY1/kQQca9ZdqvP9a9sT4Yiv7uvQPxwXLjVeWNqWQefkBwT1A==", "requires": { - "@types/oauth": "^0.9.0", + "@types/oauth": "^0.9.2", "@types/ws": "^8.5.5", - "axios": "1.4.0", + "axios": "1.5.0", "dayjs": "^1.11.9", "form-data": "^4.0.0", - "https-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.2", "oauth": "^0.10.0", "object-assign-deep": "^0.4.0", "parse-link-header": "^2.0.0", - "socks-proxy-agent": "^8.0.1", + "socks-proxy-agent": "^8.0.2", "typescript": "5.1.6", - "uuid": "^9.0.0", - "ws": "8.13.0" + "uuid": "^9.0.1", + "ws": "8.14.2" } }, "mime-db": { @@ -552,11 +556,11 @@ } }, "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==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", + "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", "requires": { - "agent-base": "^7.0.1", + "agent-base": "^7.0.2", "debug": "^4.3.4", "socks": "^2.7.1" } @@ -572,9 +576,9 @@ "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "validator": { "version": "13.0.0", @@ -582,9 +586,9 @@ "integrity": "sha512-anYx5fURbgF04lQV18nEQWZ/3wHGnxiKdG4aL8J+jEDsm98n/sU/bey+tYk6tnGJzm7ioh5FoqrAiQ6m03IgaA==" }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "requires": {} }, "xtend": { diff --git a/commons/package.json b/commons/package.json index c8817ed3..295602e8 100644 --- a/commons/package.json +++ b/commons/package.json @@ -14,6 +14,6 @@ "@types/node": "^14.11.1", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", - "megalodon": "^6.2.0" + "megalodon": "^7.0.0" } } diff --git a/commons/src/index.ts b/commons/src/index.ts index 62857b28..f977f505 100644 --- a/commons/src/index.ts +++ b/commons/src/index.ts @@ -60,12 +60,14 @@ export * from './interfaces/websites/hentai-foundry/hentai-foundry.file.options. export * from './interfaces/websites/inkbunny/inkbunny.file.options.interface'; export * from './interfaces/websites/ko-fi/ko-fi.file.options.interface'; export * from './interfaces/websites/manebooru/manebooru.file.options.interface'; -export * from './interfaces/websites/mastodon/mastodon.account.interface'; +export * from './interfaces/websites/megalodon/megalodon.account.interface'; export * from './interfaces/websites/mastodon/mastodon.file.options.interface'; export * from './interfaces/websites/mastodon/mastodon.notification.options.interface'; export * from './interfaces/websites/misskey/misskey.account.interface'; export * from './interfaces/websites/misskey/misskey.file.options.interface'; export * from './interfaces/websites/misskey/misskey.notification.options.interface'; +export * from './interfaces/websites/pleroma/pleroma.file.options.interface'; +export * from './interfaces/websites/pleroma/pleroma.notification.options.interface'; export * from './interfaces/websites/newgrounds/newgrounds.file.options.interface'; export * from './interfaces/websites/patreon/patreon.file.options.interface'; export * from './interfaces/websites/patreon/patreon.notification.options.interface'; @@ -88,7 +90,6 @@ export * from './interfaces/websites/itaku/itaku.notification.options.interface' export * from './interfaces/websites/telegram/telegram.account.interface'; export * from './interfaces/websites/telegram/telegram.file.options.interface'; export * from './interfaces/websites/telegram/telegram.notification.options.interface'; -export * from './interfaces/websites/pixelfed/pixelfed.account.interface'; export * from './interfaces/websites/pixelfed/pixelfed.file.options.interface'; export * from './interfaces/websites/bluesky/bluesky.account.interface'; export * from './interfaces/websites/bluesky/bluesky.file.options.interface'; diff --git a/commons/src/interfaces/websites/mastodon/mastodon.account.interface.ts b/commons/src/interfaces/websites/megalodon/megalodon.account.interface.ts similarity index 59% rename from commons/src/interfaces/websites/mastodon/mastodon.account.interface.ts rename to commons/src/interfaces/websites/megalodon/megalodon.account.interface.ts index 6a6c07fb..56a4dff1 100644 --- a/commons/src/interfaces/websites/mastodon/mastodon.account.interface.ts +++ b/commons/src/interfaces/websites/megalodon/megalodon.account.interface.ts @@ -1,4 +1,4 @@ -export interface MastodonAccountData { +export interface MegalodonAccountData { token: string; website: string; username: string; diff --git a/commons/src/interfaces/websites/pixelfed/pixelfed.account.interface.ts b/commons/src/interfaces/websites/pixelfed/pixelfed.account.interface.ts deleted file mode 100644 index 8a552bd3..00000000 --- a/commons/src/interfaces/websites/pixelfed/pixelfed.account.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface PixelfedAccountData { - token: string; - website: string; - username: string; -} diff --git a/commons/src/interfaces/websites/pleroma/pleroma.file.options.interface.ts b/commons/src/interfaces/websites/pleroma/pleroma.file.options.interface.ts new file mode 100644 index 00000000..dd4fe0ca --- /dev/null +++ b/commons/src/interfaces/websites/pleroma/pleroma.file.options.interface.ts @@ -0,0 +1,9 @@ +import { DefaultFileOptions } from '../../submission/default-options.interface'; + +export interface PleromaFileOptions extends DefaultFileOptions { + useTitle: boolean; + spoilerText?: string; + visibility: string; + altText?: string; + replyToUrl?: string; +} diff --git a/commons/src/interfaces/websites/pleroma/pleroma.notification.options.interface.ts b/commons/src/interfaces/websites/pleroma/pleroma.notification.options.interface.ts new file mode 100644 index 00000000..a674afb0 --- /dev/null +++ b/commons/src/interfaces/websites/pleroma/pleroma.notification.options.interface.ts @@ -0,0 +1,8 @@ +import { DefaultOptions } from '../../submission/default-options.interface'; + +export interface PleromaNotificationOptions extends DefaultOptions { + useTitle: boolean; + spoilerText?: string; + visibility: string; + replyToUrl?: string; +} diff --git a/commons/src/websites/pleroma/pleroma.file.options.ts b/commons/src/websites/pleroma/pleroma.file.options.ts new file mode 100644 index 00000000..94d559db --- /dev/null +++ b/commons/src/websites/pleroma/pleroma.file.options.ts @@ -0,0 +1,38 @@ +import { Expose } from 'class-transformer'; +import { IsBoolean, IsOptional, IsString } from 'class-validator'; +import { DefaultFileOptions } from '../../interfaces/submission/default-options.interface'; +import { PleromaFileOptions } from '../../interfaces/websites/pleroma/pleroma.file.options.interface'; +import { DefaultValue } from '../../models/decorators/default-value.decorator'; +import { DefaultFileOptionsEntity } from '../../models/default-file-options.entity'; + +export class PleromaFileOptionsEntity extends DefaultFileOptionsEntity + implements PleromaFileOptions { + @Expose() + @IsBoolean() + @DefaultValue(false) + useTitle!: boolean; + + @Expose() + @IsOptional() + @IsString() + spoilerText?: string; + + @Expose() + @IsString() + @DefaultValue('public') + visibility!: string; + + @Expose() + @IsOptional() + @IsString() + altText?: string; + + @Expose() + @IsOptional() + @IsString() + replyToUrl?: string; + + constructor(entity?: Partial) { + super(entity as DefaultFileOptions); + } +} diff --git a/commons/src/websites/pleroma/pleroma.notification.options.ts b/commons/src/websites/pleroma/pleroma.notification.options.ts new file mode 100644 index 00000000..b39ca0ab --- /dev/null +++ b/commons/src/websites/pleroma/pleroma.notification.options.ts @@ -0,0 +1,33 @@ +import { Expose } from 'class-transformer'; +import { IsBoolean, IsOptional, IsString } from 'class-validator'; +import { DefaultOptions } from '../../interfaces/submission/default-options.interface'; +import { PleromaNotificationOptions } from '../../interfaces/websites/pleroma/pleroma.notification.options.interface'; +import { DefaultValue } from '../../models/decorators/default-value.decorator'; +import { DefaultOptionsEntity } from '../../models/default-options.entity'; + +export class PleromaNotificationOptionsEntity extends DefaultOptionsEntity + implements PleromaNotificationOptions { + @Expose() + @IsBoolean() + @DefaultValue(false) + useTitle!: boolean; + + @Expose() + @IsOptional() + @IsString() + spoilerText?: string; + + @Expose() + @IsString() + @DefaultValue('public') + visibility!: string; + + @Expose() + @IsOptional() + @IsString() + replyToUrl?: string; + + constructor(entity?: Partial) { + super(entity as DefaultOptions); + } +} diff --git a/commons/src/websites/pleroma/pleroma.options.ts b/commons/src/websites/pleroma/pleroma.options.ts new file mode 100644 index 00000000..e7c70970 --- /dev/null +++ b/commons/src/websites/pleroma/pleroma.options.ts @@ -0,0 +1,7 @@ +import { PleromaFileOptionsEntity } from './pleroma.file.options'; +import { PleromaNotificationOptionsEntity } from './pleroma.notification.options'; + +export class Pleroma { + static readonly FileOptions = PleromaFileOptionsEntity; + static readonly NotificationOptions = PleromaNotificationOptionsEntity; +} diff --git a/commons/src/websites/websites.ts b/commons/src/websites/websites.ts index bca5143f..73263191 100644 --- a/commons/src/websites/websites.ts +++ b/commons/src/websites/websites.ts @@ -14,6 +14,7 @@ export * from './ko-fi/ko-fi.options'; export * from './manebooru/manebooru.options'; export * from './mastodon/mastodon.options'; export * from './misskey/misskey.options'; +export * from './pleroma/pleroma.options'; export * from './newgrounds/newgrounds.options'; export * from './patreon/patreon.options'; export * from './picarto/picarto.options'; diff --git a/electron-app/package-lock.json b/electron-app/package-lock.json index 02403f65..ee212ae4 100644 --- a/electron-app/package-lock.json +++ b/electron-app/package-lock.json @@ -1,12 +1,12 @@ { "name": "postybirb-plus", - "version": "3.1.30", + "version": "3.1.31", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "postybirb-plus", - "version": "3.1.30", + "version": "3.1.31", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -35,7 +35,7 @@ "jimp": "^0.16.1", "lodash": "^4.17.20", "lowdb": "^1.0.0", - "megalodon": "^6.2.0", + "megalodon": "^7.0.0", "nanoid": "^2.1.8", "nedb": "^1.8.0", "node-fetch": "^2.6.12", @@ -89,7 +89,7 @@ "@types/node": "^14.11.1", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", - "megalodon": "^6.2.0" + "megalodon": "^7.0.0" } }, "../commons/node_modules/@types/node": { @@ -20150,23 +20150,23 @@ } }, "node_modules/megalodon": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-6.2.0.tgz", - "integrity": "sha512-OTnZ8zcg9fhYMTbGDTDEOunSA0/zIqzEu03Ne925aTaAqL7i4gnxiwBHrRusPcA7JS4wnuicAYuwuOXZVrDK0w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-7.0.1.tgz", + "integrity": "sha512-zJhYXw+FNaNkjpY8uJo+C4u6p2yxdPYRmnY4tMY1/kQQca9ZdqvP9a9sT4Yiv7uvQPxwXLjVeWNqWQefkBwT1A==", "dependencies": { - "@types/oauth": "^0.9.0", + "@types/oauth": "^0.9.2", "@types/ws": "^8.5.5", - "axios": "1.4.0", + "axios": "1.5.0", "dayjs": "^1.11.9", "form-data": "^4.0.0", - "https-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.2", "oauth": "^0.10.0", "object-assign-deep": "^0.4.0", "parse-link-header": "^2.0.0", - "socks-proxy-agent": "^8.0.1", + "socks-proxy-agent": "^8.0.2", "typescript": "5.1.6", - "uuid": "^9.0.0", - "ws": "8.13.0" + "uuid": "^9.0.1", + "ws": "8.14.2" }, "engines": { "node": ">=15.0.0" @@ -20184,9 +20184,9 @@ } }, "node_modules/megalodon/node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -20256,9 +20256,9 @@ } }, "node_modules/megalodon/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "engines": { "node": ">=10.0.0" }, @@ -43023,23 +43023,23 @@ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "megalodon": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-6.2.0.tgz", - "integrity": "sha512-OTnZ8zcg9fhYMTbGDTDEOunSA0/zIqzEu03Ne925aTaAqL7i4gnxiwBHrRusPcA7JS4wnuicAYuwuOXZVrDK0w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-7.0.1.tgz", + "integrity": "sha512-zJhYXw+FNaNkjpY8uJo+C4u6p2yxdPYRmnY4tMY1/kQQca9ZdqvP9a9sT4Yiv7uvQPxwXLjVeWNqWQefkBwT1A==", "requires": { - "@types/oauth": "^0.9.0", + "@types/oauth": "^0.9.2", "@types/ws": "^8.5.5", - "axios": "1.4.0", + "axios": "1.5.0", "dayjs": "^1.11.9", "form-data": "^4.0.0", - "https-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.2", "oauth": "^0.10.0", "object-assign-deep": "^0.4.0", "parse-link-header": "^2.0.0", - "socks-proxy-agent": "^8.0.1", + "socks-proxy-agent": "^8.0.2", "typescript": "5.1.6", - "uuid": "^9.0.0", - "ws": "8.13.0" + "uuid": "^9.0.1", + "ws": "8.14.2" }, "dependencies": { "agent-base": { @@ -43051,9 +43051,9 @@ } }, "axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "requires": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -43098,9 +43098,9 @@ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "requires": {} } } @@ -44886,7 +44886,7 @@ "@types/node": "^14.11.1", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", - "megalodon": "^6.2.0" + "megalodon": "^7.0.0" }, "dependencies": { "@types/node": { diff --git a/electron-app/package.json b/electron-app/package.json index c2e74501..34e8a391 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -62,7 +62,7 @@ "jimp": "^0.16.1", "lodash": "^4.17.20", "lowdb": "^1.0.0", - "megalodon": "^6.2.0", + "megalodon": "^7.0.0", "nanoid": "^2.1.8", "nedb": "^1.8.0", "node-fetch": "^2.6.12", diff --git a/electron-app/src/server/websites/mastodon/mastodon.service.ts b/electron-app/src/server/websites/mastodon/mastodon.service.ts index 9327dd5b..38f81018 100644 --- a/electron-app/src/server/websites/mastodon/mastodon.service.ts +++ b/electron-app/src/server/websites/mastodon/mastodon.service.ts @@ -1,38 +1,11 @@ import { Injectable } from '@nestjs/common'; -import generator, { Entity, Response } from 'megalodon'; import { - DefaultOptions, FileRecord, - FileSubmission, FileSubmissionType, - MastodonAccountData, - MastodonFileOptions, - MastodonNotificationOptions, - PostResponse, - Submission, - SubmissionPart, - SubmissionRating, } from 'postybirb-commons'; import { ScalingOptions } from '../interfaces/scaling-options.interface'; -import UserAccountEntity from 'src/server//account/models/user-account.entity'; -import { PlaintextParser } from 'src/server/description-parsing/plaintext/plaintext.parser'; -import ImageManipulator from 'src/server/file-manipulation/manipulators/image.manipulator'; -import Http from 'src/server/http/http.util'; -import { CancellationToken } from 'src/server/submission/post/cancellation/cancellation-token'; -import { - FilePostData, - PostFile, -} from 'src/server/submission/post/interfaces/file-post-data.interface'; -import { PostData } from 'src/server/submission/post/interfaces/post-data.interface'; -import { ValidationParts } from 'src/server/submission/validator/interfaces/validation-parts.interface'; -import FileSize from 'src/server/utils/filesize.util'; -import FormContent from 'src/server/utils/form-content.util'; -import WebsiteValidator from 'src/server/utils/website-validator.util'; -import { LoginResponse } from '../interfaces/login-response.interface'; -import { Website } from '../website.base'; import _ from 'lodash'; -import WaitUtil from 'src/server/utils/wait.util'; -import { FileManagerService } from 'src/server/file-manager/file-manager.service'; +import { Megalodon } from '../megalodon/megalodon.service'; const INFO_KEY = 'INSTANCE INFO'; @@ -50,20 +23,13 @@ type MastodonInstanceInfo = { video_matrix_limit: number; }; }; - upload_limit?: number; // Pleroma, Akkoma - max_toot_chars?: number; // Pleroma, Akkoma - max_media_attachments?: number; //Pleroma -}; +} @Injectable() -export class Mastodon extends Website { - constructor(private readonly fileRepository: FileManagerService) { - super(); - } - readonly BASE_URL: string; - readonly enableAdvertisement = false; - readonly acceptsAdditionalFiles = true; - readonly defaultDescriptionParser = PlaintextParser.parse; +export class Mastodon extends Megalodon { + + readonly megalodonService = 'mastodon'; + readonly acceptsFiles = [ 'png', 'jpeg', @@ -91,23 +57,11 @@ export class Mastodon extends Website { 'wma', ]; - async checkLoginStatus(data: UserAccountEntity): Promise { - const status: LoginResponse = { loggedIn: false, username: null }; - const accountData: MastodonAccountData = data.data; - if (accountData && accountData.token) { - await this.getAndStoreInstanceInfo(data._id, accountData); - - status.loggedIn = true; - status.username = accountData.username; - } - return status; - } - - private async getAndStoreInstanceInfo(profileId: string, data: MastodonAccountData) { - const client = generator('mastodon', data.website, data.token); - const instance = await client.getInstance(); + getInstanceSettings(accountId: string) { + const instanceInfo: MastodonInstanceInfo = this.getAccountInfo(accountId, INFO_KEY); - this.storeAccountInformation(profileId, INFO_KEY, instance.data); + this.maxCharLength = instanceInfo?.configuration?.statuses?.max_characters ?? 500; + this.maxMediaCount = instanceInfo ? instanceInfo?.configuration?.statuses?.max_media_attachments : 4; } getScalingOptions(file: FileRecord, accountId: string): ScalingOptions { @@ -126,313 +80,12 @@ export class Mastodon extends Website { ? instanceInfo.configuration.media_attachments.image_size_limit : instanceInfo.configuration.media_attachments.video_size_limit, }; - } else if (instanceInfo?.upload_limit) { - return { - maxSize: instanceInfo?.upload_limit, - }; } else { return undefined; } } - // Megaladon api has uploadMedia method, hovewer, it does not work with mastodon - private async uploadMedia( - data: MastodonAccountData, - file: PostFile, - altText: string, - ): Promise { - const upload = await Http.post<{ id: string; errors: any; url: string }>( - `${data.website}/api/v2/media`, - undefined, - { - type: 'multipart', - data: { - file, - description: altText, - }, - requestOptions: { json: true }, - headers: { - Accept: '*/*', - 'User-Agent': 'node-mastodon-client/PostyBirb', - Authorization: `Bearer ${data.token}`, - }, - }, - ); - - this.verifyResponse(upload, 'Verify upload'); - - // Processing - if (upload.response.statusCode === 202 || !upload.body.url) { - for (let i = 0; i < 10; i++) { - await WaitUtil.wait(4000); - const checkUpload = await Http.get<{ id: string; errors: any; url: string }>( - `${data.website}/api/v1/media/${upload.body.id}`, - undefined, - { - requestOptions: { json: true }, - headers: { - Accept: '*/*', - 'User-Agent': 'node-mastodon-client/PostyBirb', - Authorization: `Bearer ${data.token}`, - }, - }, - ); - - if (checkUpload.body.url) { - break; - } - } - } - - if (upload.body.errors) { - return Promise.reject( - this.createPostResponse({ additionalInfo: upload.body, message: upload.body.errors }), - ); - } - - return upload.body.id; - } - - async postFileSubmission( - cancellationToken: CancellationToken, - data: FilePostData, - accountData: MastodonAccountData, - ): Promise { - const M = generator('mastodon', accountData.website, accountData.token); - - const files = [data.primary, ...data.additional]; - const uploadedMedias: string[] = []; - for (const file of files) { - this.checkCancelled(cancellationToken); - uploadedMedias.push(await this.uploadMedia(accountData, file.file, data.options.altText)); - } - - const instanceInfo: MastodonInstanceInfo = this.getAccountInfo(data.part.accountId, INFO_KEY); - const chunkCount = - instanceInfo?.configuration?.statuses?.max_media_attachments ?? - instanceInfo?.max_media_attachments ?? - (instanceInfo?.upload_limit ? 1000 : 4); - const maxChars = - instanceInfo?.configuration?.statuses?.max_characters ?? instanceInfo?.max_toot_chars ?? 500; - - const isSensitive = data.rating !== SubmissionRating.GENERAL; - const chunks = _.chunk(uploadedMedias, chunkCount); - let status = `${data.options.useTitle && data.title ? `${data.title}\n` : ''}${ - data.description - }`.substring(0, maxChars); - let lastId = ''; - let source = ''; - const replyToId = this.getPostIdFromUrl(data.options.replyToUrl); - - for (let i = 0; i < chunks.length; i++) { - this.checkCancelled(cancellationToken); - const statusOptions: any = { - sensitive: isSensitive, - visibility: data.options.visibility || 'public', - media_ids: chunks[i], - }; - - if (i !== 0) { - statusOptions.in_reply_to_id = lastId; - } else if (replyToId) { - statusOptions.in_reply_to_id = replyToId; - } - - if (data.options.spoilerText) { - statusOptions.spoiler_text = data.options.spoilerText; - } - - status = this.appendTags(this.formatTags(data.tags), status, maxChars); - - try { - const result = (await M.postStatus(status, statusOptions)).data as Entity.Status; - if (!source) source = result.url; - lastId = result.id; - } catch (err) { - return Promise.reject( - this.createPostResponse({ - message: err.message, - stack: err.stack, - additionalInfo: { chunkNumber: i }, - }), - ); - } - } - - this.checkCancelled(cancellationToken); - - return this.createPostResponse({ source }); - } - - async postNotificationSubmission( - cancellationToken: CancellationToken, - data: PostData, - accountData: MastodonAccountData, - ): Promise { - const M = generator('mastodon', accountData.website, accountData.token); - const instanceInfo: MastodonInstanceInfo = this.getAccountInfo(data.part.accountId, INFO_KEY); - const maxChars = instanceInfo?.configuration?.statuses?.max_characters ?? 500; - - const isSensitive = data.rating !== SubmissionRating.GENERAL; - const statusOptions: any = { - sensitive: isSensitive, - visibility: data.options.visibility || 'public', - }; - let status = `${data.options.useTitle && data.title ? `${data.title}\n` : ''}${ - data.description - }`; - if (data.options.spoilerText) { - statusOptions.spoiler_text = data.options.spoilerText; - } - status = this.appendTags(this.formatTags(data.tags), status, maxChars); - - const replyToId = this.getPostIdFromUrl(data.options.replyToUrl); - if (replyToId) { - statusOptions.in_reply_to_id = replyToId; - } - - this.checkCancelled(cancellationToken); - try { - const result = (await M.postStatus(status, statusOptions)).data as Entity.Status; - return this.createPostResponse({ source: result.url }); - } catch (error) { - return Promise.reject(this.createPostResponse(error)); - } - } - - formatTags(tags: string[]) { - return this.parseTags( - tags - .map(tag => tag.replace(/[^a-z0-9]/gi, ' ')) - .map(tag => - tag - .split(' ') - // .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(''), - ), - { spaceReplacer: '_' }, - ).map(tag => `#${tag}`); - } - - validateFileSubmission( - submission: FileSubmission, - submissionPart: SubmissionPart, - defaultPart: SubmissionPart, - ): ValidationParts { - const problems: string[] = []; - const warnings: string[] = []; - const isAutoscaling: boolean = submissionPart.data.autoScale; - - const description = this.defaultDescriptionParser( - FormContent.getDescription(defaultPart.data.description, submissionPart.data.description), - ); - - const instanceInfo: MastodonInstanceInfo = this.getAccountInfo( - submissionPart.accountId, - INFO_KEY, - ); - const maxChars = instanceInfo?.configuration?.statuses?.max_characters ?? 500; - - if (description.length > maxChars) { - warnings.push( - `Max description length allowed is ${maxChars} characters (for this instance).`, - ); - } - - const files = [ - submission.primary, - ...(submission.additional || []).filter( - f => !f.ignoredAccounts!.includes(submissionPart.accountId), - ), - ]; - - files.forEach(file => { - const { type, size, name, mimetype } = file; - if (!WebsiteValidator.supportsFileType(file, this.acceptsFiles)) { - problems.push(`Does not support file format: (${name}) ${mimetype}.`); - } - - const scalingOptions = this.getScalingOptions(file, submissionPart.accountId); - - if (scalingOptions && scalingOptions.maxSize < size) { - if ( - isAutoscaling && - type === FileSubmissionType.IMAGE && - ImageManipulator.isMimeType(mimetype) - ) { - warnings.push( - `${name} will be scaled down to ${FileSize.BytesToMB(scalingOptions.maxSize)}MB`, - ); - } else { - problems.push( - `This instance limits ${mimetype} to ${FileSize.BytesToMB(scalingOptions.maxSize)}MB`, - ); - } - } - - if ( - scalingOptions && - isAutoscaling && - type === FileSubmissionType.IMAGE && - scalingOptions.maxWidth && - scalingOptions.maxHeight && - (file.height > scalingOptions.maxHeight || file.width > scalingOptions.maxWidth) - ) { - warnings.push( - `${name} will be scaled down to a maximum size of ${scalingOptions.maxWidth}x${scalingOptions.maxHeight}, while maintaining aspect ratio`, - ); - } - }); - - if ( - (submissionPart.data.tags.value.length > 1 || defaultPart.data.tags.value.length > 1) && - submissionPart.data.visibility != 'public' - ) { - warnings.push( - `This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.`, - ); - } - - this.validateReplyToUrl(problems, submissionPart.data.replyToUrl); - - return { problems, warnings }; - } - - validateNotificationSubmission( - submission: Submission, - submissionPart: SubmissionPart, - defaultPart: SubmissionPart, - ): ValidationParts { - const problems = []; - const warnings = []; - const description = this.defaultDescriptionParser( - FormContent.getDescription(defaultPart.data.description, submissionPart.data.description), - ); - - const instanceInfo: MastodonInstanceInfo = this.getAccountInfo( - submissionPart.accountId, - INFO_KEY, - ); - const maxChars = - instanceInfo?.configuration?.statuses?.max_characters ?? instanceInfo?.max_toot_chars ?? 500; - if (description.length > maxChars) { - warnings.push( - `Max description length allowed is ${maxChars} characters (for this instance).`, - ); - } - - this.validateReplyToUrl(problems, submissionPart.data.replyToUrl); - - return { problems, warnings }; - } - - private validateReplyToUrl(problems: string[], url?: string): void { - if(url?.trim() && !this.getPostIdFromUrl(url)) { - problems.push("Invalid post URL to reply to."); - } - } - - private getPostIdFromUrl(url: string): string | null { + getPostIdFromUrl(url: string): string | null { // We expect this to a post URL like https://{instance}/@{user}/{id} or // https://:instance/deck/@{user}/{id}. We grab the id after the @ part. const match = /\/@[^\/]+\/([0-9]+)/.exec(url); diff --git a/electron-app/src/server/websites/megalodon/megalodon.service.ts b/electron-app/src/server/websites/megalodon/megalodon.service.ts new file mode 100644 index 00000000..335483b7 --- /dev/null +++ b/electron-app/src/server/websites/megalodon/megalodon.service.ts @@ -0,0 +1,349 @@ +import generator, { Entity } from 'megalodon'; +import { + DefaultOptions, + FileRecord, + FileSubmission, + FileSubmissionType, + MegalodonAccountData, + MastodonFileOptions, + MastodonNotificationOptions, + PostResponse, + Submission, + SubmissionPart, + SubmissionRating, +} from 'postybirb-commons'; +import { ScalingOptions } from '../interfaces/scaling-options.interface'; +import UserAccountEntity from 'src/server//account/models/user-account.entity'; +import { PlaintextParser } from 'src/server/description-parsing/plaintext/plaintext.parser'; +import ImageManipulator from 'src/server/file-manipulation/manipulators/image.manipulator'; +import { CancellationToken } from 'src/server/submission/post/cancellation/cancellation-token'; +import { + FilePostData, + PostFile, +} from 'src/server/submission/post/interfaces/file-post-data.interface'; +import { PostData } from 'src/server/submission/post/interfaces/post-data.interface'; +import { ValidationParts } from 'src/server/submission/validator/interfaces/validation-parts.interface'; +import FileSize from 'src/server/utils/filesize.util'; +import FormContent from 'src/server/utils/form-content.util'; +import WebsiteValidator from 'src/server/utils/website-validator.util'; +import { LoginResponse } from '../interfaces/login-response.interface'; +import { Website } from '../website.base'; +import _ from 'lodash'; +import { FileManagerService } from 'src/server/file-manager/file-manager.service'; +import * as fs from 'fs'; +import { tmpdir } from 'os'; +import * as path from 'path'; + +const INFO_KEY = 'INSTANCE INFO'; + +export abstract class Megalodon extends Website { + constructor(private readonly fileRepository: FileManagerService) { + super(); + } + + megalodonService: 'mastodon' | 'pleroma' | 'misskey' | 'friendica' = 'mastodon'; // Set this as appropriate in your constructor + maxCharLength = 500; // Set this off the instance information! + maxMediaCount = 4; + + readonly BASE_URL: string; + readonly enableAdvertisement = false; + readonly acceptsAdditionalFiles = true; + readonly defaultDescriptionParser = PlaintextParser.parse; + readonly acceptsFiles = [ // Override, or extend this list in your inherited classes! + 'png', + 'jpeg', + 'jpg', + 'gif', + 'webp', + 'm4v', + 'mov' + ]; + + // Boiler plate login check code across all versions of services using the megalodon library + async checkLoginStatus(data: UserAccountEntity): Promise { + const status: LoginResponse = { loggedIn: false, username: null }; + const accountData: MegalodonAccountData = data.data; + this.logger.debug(`Login check: ${data._id} = ${accountData.website}`); + if (accountData && accountData.token) { + await this.getAndStoreInstanceInfo(data._id, accountData); + + status.loggedIn = true; + status.username = accountData.username; + } + return status; + } + + private async getAndStoreInstanceInfo(profileId: string, data: MegalodonAccountData) { + const client = generator(this.megalodonService, data.website, data.token); + const instance = await client.getInstance(); + + this.storeAccountInformation(profileId, INFO_KEY, instance.data); + } + + abstract getScalingOptions(file: FileRecord, accountId: string): ScalingOptions; + + abstract getInstanceSettings(accountId: string); + + async postFileSubmission( + cancellationToken: CancellationToken, + data: FilePostData, + accountData: MegalodonAccountData, + ): Promise { + this.logger.log("Posting a file") + this.getInstanceSettings(data.part.accountId); + + const M = generator(this.megalodonService, accountData.website, accountData.token); + + const files = [data.primary, ...data.additional]; + const uploadedMedias: string[] = []; + for (const file of files) { + this.checkCancelled(cancellationToken); + uploadedMedias.push(await this.uploadMedia(accountData, file.file, data.options.altText)); + } + + const isSensitive = data.rating !== SubmissionRating.GENERAL; + const chunks = _.chunk(uploadedMedias, this.maxMediaCount); + let status = `${data.options.useTitle && data.title ? `${data.title}\n` : ''}${ + data.description + }`.substring(0, this.maxCharLength); + let lastId = ''; + let source = ''; + const replyToId = this.getPostIdFromUrl(data.options.replyToUrl); + + for (let i = 0; i < chunks.length; i++) { + this.checkCancelled(cancellationToken); + const statusOptions: any = { + sensitive: isSensitive, + visibility: data.options.visibility || 'public', + media_ids: chunks[i], + }; + + if (i !== 0) { + statusOptions.in_reply_to_id = lastId; + } else if (replyToId) { + statusOptions.in_reply_to_id = replyToId; + } + + if (data.options.spoilerText) { + statusOptions.spoiler_text = data.options.spoilerText; + } + + status = this.appendTags(this.formatTags(data.tags), status, this.maxCharLength); + + try { + const result = (await M.postStatus(status, statusOptions)).data as Entity.Status; + if (!source) source = result.url; + lastId = result.id; + } catch (err) { + return Promise.reject( + this.createPostResponse({ + message: err.message, + stack: err.stack, + additionalInfo: { chunkNumber: i }, + }), + ); + } + } + + this.checkCancelled(cancellationToken); + + return this.createPostResponse({ source }); + } + + async postNotificationSubmission( + cancellationToken: CancellationToken, + data: PostData, + accountData: MegalodonAccountData, + ): Promise { + this.logger.log("Posting a notification") + this.getInstanceSettings(data.part.accountId); + + const M = generator(this.megalodonService, accountData.website, accountData.token); + + const isSensitive = data.rating !== SubmissionRating.GENERAL; + const statusOptions: any = { + sensitive: isSensitive, + visibility: data.options.visibility || 'public', + }; + let status = `${data.options.useTitle && data.title ? `${data.title}\n` : ''}${ + data.description + }`; + if (data.options.spoilerText) { + statusOptions.spoiler_text = data.options.spoilerText; + } + status = this.appendTags(this.formatTags(data.tags), status, this.maxCharLength); + + const replyToId = this.getPostIdFromUrl(data.options.replyToUrl); + if (replyToId) { + statusOptions.in_reply_to_id = replyToId; + } + + this.checkCancelled(cancellationToken); + try { + const result = (await M.postStatus(status, statusOptions)).data as Entity.Status; + return this.createPostResponse({ source: result.url }); + } catch (error) { + return Promise.reject(this.createPostResponse(error)); + } + } + + formatTags(tags: string[]) { + return this.parseTags( + tags.map(tag => tag.replace(/[^a-z0-9]/gi, ' ')).map(tag => tag.split(' ').join('')), + { spaceReplacer: '_' }, + ).map(tag => `#${tag}`); + } + + validateFileSubmission( + submission: FileSubmission, + submissionPart: SubmissionPart, + defaultPart: SubmissionPart, + ): ValidationParts { + this.getInstanceSettings(defaultPart.accountId); + + const problems: string[] = []; + const warnings: string[] = []; + const isAutoscaling: boolean = submissionPart.data.autoScale; + + const description = this.defaultDescriptionParser( + FormContent.getDescription(defaultPart.data.description, submissionPart.data.description), + ); + + if (description.length > this.maxCharLength) { + warnings.push( + `Max description length allowed is ${this.maxCharLength} characters.`, + ); + } + + const files = [ + submission.primary, + ...(submission.additional || []).filter( + f => !f.ignoredAccounts!.includes(submissionPart.accountId), + ), + ]; + + files.forEach(file => { + const { type, size, name, mimetype } = file; + if (!WebsiteValidator.supportsFileType(file, this.acceptsFiles)) { + problems.push(`Does not support file format: (${name}) ${mimetype}.`); + } + + const scalingOptions = this.getScalingOptions(file, submissionPart.accountId); + + if (scalingOptions && scalingOptions.maxSize < size) { + if ( + isAutoscaling && + type === FileSubmissionType.IMAGE && + ImageManipulator.isMimeType(mimetype) + ) { + warnings.push( + `${name} will be scaled down to ${FileSize.BytesToMB(scalingOptions.maxSize)}MB`, + ); + } else { + problems.push( + `This instance limits ${mimetype} to ${FileSize.BytesToMB(scalingOptions.maxSize)}MB`, + ); + } + } + + if ( + scalingOptions && + isAutoscaling && + type === FileSubmissionType.IMAGE && + scalingOptions.maxWidth && + scalingOptions.maxHeight && + (file.height > scalingOptions.maxHeight || file.width > scalingOptions.maxWidth) + ) { + warnings.push( + `${name} will be scaled down to a maximum size of ${scalingOptions.maxWidth}x${scalingOptions.maxHeight}, while maintaining aspect ratio`, + ); + } + }); + + if ( + (submissionPart.data.tags.value.length > 1 || defaultPart.data.tags.value.length > 1) && + submissionPart.data.visibility != 'public' + ) { + warnings.push( + `This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.`, + ); + } + + this.validateReplyToUrl(problems, submissionPart.data.replyToUrl); + + return { problems, warnings }; + } + + validateNotificationSubmission( + submission: Submission, + submissionPart: SubmissionPart, + defaultPart: SubmissionPart, + ): ValidationParts { + this.getInstanceSettings(defaultPart.accountId); + + const problems = []; + const warnings = []; + + const description = this.defaultDescriptionParser( + FormContent.getDescription(defaultPart.data.description, submissionPart.data.description), + ); + + if (description.length > this.maxCharLength) { + warnings.push( + `Max description length allowed is ${this.maxCharLength} characters.`, + ); + } + + this.validateReplyToUrl(problems, submissionPart.data.replyToUrl); + + return { problems, warnings }; + } + + validateReplyToUrl(problems: string[], url?: string): void { + if(url?.trim() && !this.getPostIdFromUrl(url)) { + problems.push("Invalid post URL to reply to."); + } + } + + abstract getPostIdFromUrl(url: string): string | null; + + async uploadMedia( + data: MegalodonAccountData, + file: PostFile, + altText: string, + ): Promise { + //): Promise<{ id: string }> {\ + this.logger.log("Uploading media") + const M = generator(this.megalodonService, data.website, data.token); + + // megalodon is odd, and doesnt seem to like the buffer being passed to the upload media call + // So lets write the file out to a temp file, then pass that to createReadStream, then that to uploadMedia. + // That works .... \o/ + const tempDir = tmpdir(); + + fs.writeFileSync(path.join(tempDir, file.options.filename), file.value); + + const upload = await M.uploadMedia(fs.createReadStream(path.join(tempDir, file.options.filename)), { description: altText }); + + this.logger.log(upload) + + fs.unlink(path.join(tempDir, file.options.filename), (err) => { + if (err) { + this.logger.error("Unable to remove the temp file", err.stack, err.message); + } + }); + + if (upload.status > 300) { + this.logger.log(upload); + return Promise.reject( + this.createPostResponse({ additionalInfo: upload.status, message: upload.statusText }), + ); + } + + this.logger.log("Image uploaded"); + +// return { id: upload.data.id }; + return upload.data.id; + } + +} diff --git a/electron-app/src/server/websites/pixelfed/pixelfed.service.ts b/electron-app/src/server/websites/pixelfed/pixelfed.service.ts index 5493ec9d..e0968637 100644 --- a/electron-app/src/server/websites/pixelfed/pixelfed.service.ts +++ b/electron-app/src/server/websites/pixelfed/pixelfed.service.ts @@ -1,37 +1,12 @@ import { Injectable } from '@nestjs/common'; -import generator, { Entity, Response } from 'megalodon'; import { - DefaultOptions, FileRecord, - FileSubmission, FileSubmissionType, - PixelfedAccountData, - PixelfedFileOptions, - PostResponse, - Submission, - SubmissionPart, - SubmissionRating, } from 'postybirb-commons'; import { ScalingOptions } from '../interfaces/scaling-options.interface'; -import UserAccountEntity from 'src/server//account/models/user-account.entity'; -import { PlaintextParser } from 'src/server/description-parsing/plaintext/plaintext.parser'; -import ImageManipulator from 'src/server/file-manipulation/manipulators/image.manipulator'; -import Http from 'src/server/http/http.util'; -import { CancellationToken } from 'src/server/submission/post/cancellation/cancellation-token'; -import { - FilePostData, - PostFile, -} from 'src/server/submission/post/interfaces/file-post-data.interface'; -import { PostData } from 'src/server/submission/post/interfaces/post-data.interface'; -import { ValidationParts } from 'src/server/submission/validator/interfaces/validation-parts.interface'; import FileSize from 'src/server/utils/filesize.util'; -import FormContent from 'src/server/utils/form-content.util'; -import WebsiteValidator from 'src/server/utils/website-validator.util'; -import { LoginResponse } from '../interfaces/login-response.interface'; -import { Website } from '../website.base'; import _ from 'lodash'; -import WaitUtil from 'src/server/utils/wait.util'; -import { FileManagerService } from 'src/server/file-manager/file-manager.service'; +import { Megalodon } from '../megalodon/megalodon.service'; const INFO_KEY = 'INSTANCE INFO'; @@ -50,49 +25,18 @@ type PixelfedInstanceInfo = { }; @Injectable() -export class Pixelfed extends Website { - constructor(private readonly fileRepository: FileManagerService) { - super(); - } - readonly BASE_URL: string; +export class Pixelfed extends Megalodon { + readonly enableAdvertisement = false; - readonly acceptsAdditionalFiles = true; - readonly defaultDescriptionParser = PlaintextParser.parse; readonly acceptsFiles = ['png', 'jpeg', 'jpg', 'gif', 'swf', 'flv', 'mp4']; - async checkLoginStatus(data: UserAccountEntity): Promise { - const status: LoginResponse = { loggedIn: false, username: null }; - const accountData: PixelfedAccountData = data.data; - if (accountData && accountData.token) { - const refresh = await this.refreshToken(accountData); - if (refresh) { - status.loggedIn = true; - status.username = accountData.username; - this.getInstanceInfo(data._id, accountData); - } - } - return status; - } - - private async getInstanceInfo(profileId: string, data: PixelfedAccountData) { - const client = generator('mastodon', data.website, data.token); - const instance = await client.getInstance(); + readonly megalodonService = 'mastodon'; // At some point will change this when they get Pixelfed support natively - this.storeAccountInformation(profileId, INFO_KEY, instance.data); - } - - // TBH not entirely sure why this is a thing, but its in the old code so... :shrug: - private async refreshToken(data: PixelfedAccountData): Promise { - const M = this.getPixelfedInstance(data); - return true; - } + getInstanceSettings(accountId: string) { + const instanceInfo: PixelfedInstanceInfo = this.getAccountInfo(accountId, INFO_KEY); - private getPixelfedInstance(data: PixelfedAccountData): Entity.Instance { - const client = generator('mastodon', data.website, data.token); - client.getInstance().then(res => { - return res.data; - }); - return null; + this.maxCharLength = instanceInfo?.configuration?.statuses?.max_characters ?? 500; + this.maxMediaCount = instanceInfo ? instanceInfo?.configuration?.statuses?.max_media_attachments : 4; } getScalingOptions(file: FileRecord, accountId: string): ScalingOptions { @@ -113,233 +57,12 @@ export class Pixelfed extends Website { }; } - private async uploadMedia( - data: PixelfedAccountData, - file: PostFile, - altText: string, - ): Promise<{ id: string }> { - const upload = await Http.post<{ id: string; errors: any; url: string }>( - `${data.website}/api/v2/media`, - undefined, - { - type: 'multipart', - data: { - file, - description: altText, - }, - requestOptions: { json: true }, - headers: { - Accept: '*/*', - 'User-Agent': 'node-mastodon-client/PostyBirb', - Authorization: `Bearer ${data.token}`, - }, - }, - ); - - this.verifyResponse(upload, 'Verify upload'); - - // Processing - if (upload.response.statusCode === 202 || !upload.body.url) { - for (let i = 0; i < 10; i++) { - await WaitUtil.wait(4000); - const checkUpload = await Http.get<{ id: string; errors: any; url: string }>( - `${data.website}/api/v1/media/${upload.body.id}`, - undefined, - { - requestOptions: { json: true }, - headers: { - Accept: '*/*', - 'User-Agent': 'node-mastodon-client/PostyBirb', - Authorization: `Bearer ${data.token}`, - }, - }, - ); - - if (checkUpload.body.url) { - break; - } - } - } - - if (upload.body.errors) { - return Promise.reject( - this.createPostResponse({ additionalInfo: upload.body, message: upload.body.errors }), - ); - } - - return { id: upload.body.id }; - } - - async postFileSubmission( - cancellationToken: CancellationToken, - data: FilePostData, - accountData: PixelfedAccountData, - ): Promise { - const M = generator('mastodon', accountData.website, accountData.token); - - const files = [data.primary, ...data.additional]; - this.checkCancelled(cancellationToken); - const uploadedMedias: { - id: string; - }[] = []; - for (const file of files) { - uploadedMedias.push(await this.uploadMedia(accountData, file.file, data.options.altText)); - } - - const instanceInfo: PixelfedInstanceInfo = this.getAccountInfo(data.part.accountId, INFO_KEY); - const chunkCount = instanceInfo - ? instanceInfo?.configuration?.statuses?.max_media_attachments - : 4; - const maxChars = instanceInfo ? instanceInfo?.configuration?.statuses?.max_characters : 500; - - const isSensitive = data.rating !== SubmissionRating.GENERAL; - const { options } = data; - const chunks = _.chunk(uploadedMedias, chunkCount); - let lastId = undefined; - let statusOptions: any = { - sensitive: isSensitive, - visibility: options.visibility || 'public', - in_reply_to_id: lastId, - spoiler_text: '', - }; - let status = ''; - - for (let i = 0; i < chunks.length; i++) { - if (i === 0) { - status = `${options.useTitle && data.title ? `${data.title}\n` : ''}${ - data.description - }`.substring(0, maxChars); - statusOptions.media_ids = chunks[i].map(media => media.id); - } - - const tags = this.formatTags(data.tags); - - this.logger.debug(`Number of tags set ${tags.length}`); - - // Update the post content with the Tags if any are specified - for Pixelfed, we need to append - // these onto the post, *IF* there is character count available. - if (tags.length > 0) { - status += '\n\n'; - } - - tags.forEach(tag => { - let remain = maxChars - status.length; - let tagToInsert = tag; - if (remain > tagToInsert.length) { - status += ` ${tagToInsert}`; - } - // We don't exit the loop, so we can cram in every possible tag, even if there are short ones! - }); - - if (options.spoilerText) { - statusOptions.spoiler_text = options.spoilerText; - } - - this.checkCancelled(cancellationToken); - - await M.postStatus(status, statusOptions) - .then(result => { - lastId = result.data.id; - let res = result.data as Entity.Status; - return this.createPostResponse({ source: res.url }); - }) - .catch((err: Error) => { - return Promise.reject(this.createPostResponse({ message: err.message })); - }); - } - - return this.createPostResponse({}); - } - - formatTags(tags: string[]) { - return this.parseTags( - tags - .map(tag => tag.replace(/[^a-z0-9]/gi, ' ')) - .map(tag => - tag - .split(' ') - // .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(''), - ), - { spaceReplacer: '_' }, - ).map(tag => `#${tag}`); - } - - validateFileSubmission( - submission: FileSubmission, - submissionPart: SubmissionPart, - defaultPart: SubmissionPart, - ): ValidationParts { - const problems: string[] = []; - const warnings: string[] = []; - const isAutoscaling: boolean = submissionPart.data.autoScale; - - const description = this.defaultDescriptionParser( - FormContent.getDescription(defaultPart.data.description, submissionPart.data.description), - ); - - const instanceInfo: PixelfedInstanceInfo = this.getAccountInfo( - submissionPart.accountId, - INFO_KEY, - ); - const maxChars = instanceInfo ? instanceInfo?.configuration?.statuses?.max_characters : 500; - - if (description.length > maxChars) { - warnings.push( - `Max description length allowed is ${maxChars} characters (for this Pixelfed client).`, - ); + // https://{instance}/i/web/post/{id} + getPostIdFromUrl(url: string): string | null { + if (url && url.lastIndexOf('/') > -1) { + return url.slice(url.lastIndexOf('/') + 1); + } else { + return null; } - - const files = [ - submission.primary, - ...(submission.additional || []).filter( - f => !f.ignoredAccounts!.includes(submissionPart.accountId), - ), - ]; - - const maxImageSize = instanceInfo - ? instanceInfo?.configuration?.media_attachments?.image_size_limit - : FileSize.MBtoBytes(50); - - files.forEach(file => { - const { type, size, name, mimetype } = file; - if (!WebsiteValidator.supportsFileType(file, this.acceptsFiles)) { - problems.push(`Does not support file format: (${name}) ${mimetype}.`); - } - - if (maxImageSize < size) { - if ( - isAutoscaling && - type === FileSubmissionType.IMAGE && - ImageManipulator.isMimeType(mimetype) - ) { - warnings.push(`${name} will be scaled down to ${FileSize.BytesToMB(maxImageSize)}MB`); - } else { - problems.push(`Pixelfed limits ${mimetype} to ${FileSize.BytesToMB(maxImageSize)}MB`); - } - } - - // Check the image dimensions are not over 4000 x 4000 - this is the Pixelfed server max - if ( - isAutoscaling && - type === FileSubmissionType.IMAGE && - (file.height > 4000 || file.width > 4000) - ) { - warnings.push(`${name} will be scaled down to a maximum size of 4000x4000, while maintaining - aspect ratio`); - } - }); - - if ( - (submissionPart.data.tags.value.length > 1 || defaultPart.data.tags.value.length > 1) && - submissionPart.data.visibility != 'public' - ) { - warnings.push( - `This post won't be listed under any hashtag as it is not public. Only public posts - can be searched by hashtag.`, - ); - } - - return { problems, warnings }; } } diff --git a/electron-app/src/server/websites/pleroma/pleroma.controller.ts b/electron-app/src/server/websites/pleroma/pleroma.controller.ts new file mode 100644 index 00000000..50c3e97e --- /dev/null +++ b/electron-app/src/server/websites/pleroma/pleroma.controller.ts @@ -0,0 +1,21 @@ +import { Body, Controller, Get, Query} from '@nestjs/common'; + +@Controller('pleroma') +export class PleromaController { + + @Get('display/:auth') + async display(@Query('token') token : string, @Query('code') code : string) { + if (token === undefined) { + token = "" + } + if (code === undefined) { + code = "" + } + + return ` +

Token: ${token}
+ Code: ${code}

+ ` + } + +} diff --git a/electron-app/src/server/websites/pleroma/pleroma.module.ts b/electron-app/src/server/websites/pleroma/pleroma.module.ts new file mode 100644 index 00000000..7b845bdb --- /dev/null +++ b/electron-app/src/server/websites/pleroma/pleroma.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { Pleroma } from './pleroma.service'; +import { FileManagerModule } from 'src/server/file-manager/file-manager.module'; +import { PleromaController } from './pleroma.controller'; + +@Module({ + controllers: [PleromaController], + providers: [Pleroma], + exports: [Pleroma], + imports: [FileManagerModule], +}) +export class PleromaModule {} diff --git a/electron-app/src/server/websites/pleroma/pleroma.service.ts b/electron-app/src/server/websites/pleroma/pleroma.service.ts new file mode 100644 index 00000000..7e1106f0 --- /dev/null +++ b/electron-app/src/server/websites/pleroma/pleroma.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; +import { + FileRecord, + FileSubmissionType, +} from 'postybirb-commons'; +import { ScalingOptions } from '../interfaces/scaling-options.interface'; +import FileSize from 'src/server/utils/filesize.util'; +import _ from 'lodash'; +import { Megalodon } from '../megalodon/megalodon.service'; + +const INFO_KEY = 'INSTANCE INFO'; + +type PleromaInstanceInfo = { + upload_limit?: number; // Pleroma, Akkoma + max_toot_chars?: number; // Pleroma, Akkoma + max_media_attachments?: number; //Pleroma + configuration: { + media_attachments: { + image_size_limit: number; + video_size_limit: number; + }; + } +}; + +@Injectable() +export class Pleroma extends Megalodon { + + readonly acceptsAdditionalFiles = true; + megalodonService: 'mastodon' | 'pleroma' | 'misskey' | 'friendica' = 'pleroma'; + readonly acceptsFiles = [ + 'png', + 'jpeg', + 'jpg', + 'gif', + 'swf', + 'flv', + 'mp4', + 'doc', + 'rtf', + 'txt', + 'mp3', + ]; + + getInstanceSettings(accountId: string) { + console.log(this.getAccountInfo(accountId, INFO_KEY)); + const instanceInfo: PleromaInstanceInfo = this.getAccountInfo(accountId, INFO_KEY); + + this.maxCharLength = instanceInfo?.max_toot_chars ?? 500; + this.maxMediaCount = instanceInfo?.max_media_attachments ?? 4; + } + + getScalingOptions(file: FileRecord, accountId: string): ScalingOptions { + const instanceInfo: PleromaInstanceInfo = this.getAccountInfo(accountId, INFO_KEY); + return instanceInfo?.configuration?.media_attachments + ? { + maxHeight: 4000, + maxWidth: 4000, + maxSize: + file.type === FileSubmissionType.IMAGE + ? instanceInfo.configuration.media_attachments.image_size_limit + : instanceInfo.configuration.media_attachments.video_size_limit, + } + : { + maxHeight: 4000, + maxWidth: 4000, + maxSize: FileSize.MBtoBytes(16) + }; + } + + getPostIdFromUrl(url: string): string | null { + if (url) { + const match = url.slice(url.lastIndexOf('/')+1) + return match ? match[1] : null; + } else { + return null; + } + } +} diff --git a/electron-app/src/server/websites/website-provider.service.ts b/electron-app/src/server/websites/website-provider.service.ts index 20819ae3..aa05b1f4 100644 --- a/electron-app/src/server/websites/website-provider.service.ts +++ b/electron-app/src/server/websites/website-provider.service.ts @@ -32,6 +32,7 @@ import { SubscribeStarAdult } from './subscribe-star-adult/subscribe-star-adult. import { Pixelfed } from './pixelfed/pixelfed.service'; import { MissKey } from './misskey/misskey.service'; import { Bluesky } from './bluesky/bluesky.service'; +import { Pleroma } from './pleroma/pleroma.service'; @Injectable() export class WebsiteProvider { @@ -71,6 +72,7 @@ export class WebsiteProvider { readonly itaku: Itaku, readonly picarto: Picarto, readonly pixelfed: Pixelfed, + readonly pleroma: Pleroma, ) { // eslint-disable-next-line this.websiteModules = [...arguments].filter(arg => arg instanceof Website); diff --git a/electron-app/src/server/websites/websites.module.ts b/electron-app/src/server/websites/websites.module.ts index eca07fa4..1b61c581 100644 --- a/electron-app/src/server/websites/websites.module.ts +++ b/electron-app/src/server/websites/websites.module.ts @@ -26,6 +26,7 @@ import { DeviantArtModule } from './deviant-art/deviant-art.module'; import { ManebooruModule } from './manebooru/manebooru.module'; import { MastodonModule } from './mastodon/mastodon.module'; import { MissKeyModule } from './misskey/misskey.module'; +import { PleromaModule } from './pleroma/pleroma.module'; import { PillowfortModule } from './pillowfort/pillowfort.module'; import { TelegramModule } from './telegram/telegram.module'; import { FurbooruModule } from './furbooru/furbooru.module'; @@ -58,6 +59,7 @@ import { BlueskyModule } from './bluesky/bluesky.module'; ManebooruModule, MastodonModule, MissKeyModule, + PleromaModule, NewgroundsModule, PatreonModule, PiczelModule, diff --git a/package-lock.json b/package-lock.json index 9bd3bc63..a5100ba6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "postybirb-plus", - "version": "3.1.30", + "version": "3.1.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "postybirb-plus", - "version": "3.1.30", + "version": "3.1.31", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/ui/package-lock.json b/ui/package-lock.json index 3ef14ed7..5bedab72 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "postybirb-plus-ui", - "version": "3.1.30", + "version": "3.1.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "postybirb-plus-ui", - "version": "3.1.30", + "version": "3.1.31", "license": "BSD-3-Clause", "dependencies": { "@atproto/api": "^0.6.4", @@ -19,7 +19,7 @@ "axios": "^0.21.4", "file-saver": "^2.0.5", "lodash": "^4.17.21", - "megalodon": "^6.2.0", + "megalodon": "^7.0.0", "mobx": "^5.15.7", "mobx-react": "^6.3.1", "postybirb-commons": "file:../commons", @@ -52,7 +52,7 @@ "@types/node": "^14.11.1", "class-transformer": "^0.3.1", "class-validator": "^0.12.2", - "megalodon": "^6.2.0" + "megalodon": "^7.0.0" } }, "../commons/node_modules/@types/node": { @@ -12459,32 +12459,32 @@ } }, "node_modules/megalodon": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-6.2.0.tgz", - "integrity": "sha512-OTnZ8zcg9fhYMTbGDTDEOunSA0/zIqzEu03Ne925aTaAqL7i4gnxiwBHrRusPcA7JS4wnuicAYuwuOXZVrDK0w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-7.0.1.tgz", + "integrity": "sha512-zJhYXw+FNaNkjpY8uJo+C4u6p2yxdPYRmnY4tMY1/kQQca9ZdqvP9a9sT4Yiv7uvQPxwXLjVeWNqWQefkBwT1A==", "dependencies": { - "@types/oauth": "^0.9.0", + "@types/oauth": "^0.9.2", "@types/ws": "^8.5.5", - "axios": "1.4.0", + "axios": "1.5.0", "dayjs": "^1.11.9", "form-data": "^4.0.0", - "https-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.2", "oauth": "^0.10.0", "object-assign-deep": "^0.4.0", "parse-link-header": "^2.0.0", - "socks-proxy-agent": "^8.0.1", + "socks-proxy-agent": "^8.0.2", "typescript": "5.1.6", - "uuid": "^9.0.0", - "ws": "8.13.0" + "uuid": "^9.0.1", + "ws": "8.14.2" }, "engines": { "node": ">=15.0.0" } }, "node_modules/megalodon/node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -12529,9 +12529,9 @@ } }, "node_modules/megalodon/node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "engines": { "node": ">=10.0.0" }, diff --git a/ui/package.json b/ui/package.json index 8610a506..a730a68c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -16,7 +16,7 @@ "axios": "^0.21.4", "file-saver": "^2.0.5", "lodash": "^4.17.21", - "megalodon": "^6.2.0", + "megalodon": "^7.0.0", "mobx": "^5.15.7", "mobx-react": "^6.3.1", "postybirb-commons": "file:../commons", diff --git a/ui/src/websites/mastodon/MastodonLogin.tsx b/ui/src/websites/mastodon/MastodonLogin.tsx index 7ee76e8a..5b60fc66 100644 --- a/ui/src/websites/mastodon/MastodonLogin.tsx +++ b/ui/src/websites/mastodon/MastodonLogin.tsx @@ -2,11 +2,11 @@ import { Button, Form, Input, message, Spin } from 'antd'; import Axios from 'axios'; import React from 'react'; import ReactDOM from 'react-dom'; -import { MastodonAccountData } from 'postybirb-commons'; +import { MegalodonAccountData } from 'postybirb-commons'; import LoginService from '../../services/login.service'; import { LoginDialogProps } from '../interfaces/website.interface'; -interface State extends MastodonAccountData { +interface State extends MegalodonAccountData { code: string; loading: boolean; } diff --git a/ui/src/websites/pixelfed/PixelfedLogin.tsx b/ui/src/websites/pixelfed/PixelfedLogin.tsx index 6b8215e9..396e7cb1 100644 --- a/ui/src/websites/pixelfed/PixelfedLogin.tsx +++ b/ui/src/websites/pixelfed/PixelfedLogin.tsx @@ -2,12 +2,12 @@ import { Button, Form, Input, message, Spin } from 'antd'; import Axios from 'axios'; import React from 'react'; import ReactDOM from 'react-dom'; -import { PixelfedAccountData } from 'postybirb-commons'; +import { MegalodonAccountData } from 'postybirb-commons'; import LoginService from '../../services/login.service'; import { LoginDialogProps } from '../interfaces/website.interface'; import { stringify } from 'querystring'; -interface State extends PixelfedAccountData { +interface State extends MegalodonAccountData { code: string; loading: boolean; } diff --git a/ui/src/websites/pleroma/Pleroma.tsx b/ui/src/websites/pleroma/Pleroma.tsx new file mode 100644 index 00000000..2c4c492e --- /dev/null +++ b/ui/src/websites/pleroma/Pleroma.tsx @@ -0,0 +1,144 @@ +import { Checkbox, Form, Input, Select } from 'antd'; +import { + FileSubmission, + PleromaFileOptions, + PleromaNotificationOptions, + Submission, + SubmissionRating +} from 'postybirb-commons'; +import React from 'react'; +import { WebsiteSectionProps } from '../form-sections/website-form-section.interface'; +import GenericFileSubmissionSection from '../generic/GenericFileSubmissionSection'; +import { GenericSelectProps } from '../generic/GenericSelectProps'; +import GenericSubmissionSection from '../generic/GenericSubmissionSection'; +import { LoginDialogProps } from '../interfaces/website.interface'; +import { WebsiteImpl } from '../website.base'; +import PleromaLogin from './PleromaLogin'; + +export class Pleroma extends WebsiteImpl { + internalName: string = 'Pleroma'; + name: string = 'Pleroma Instance'; + supportsAdditionalFiles: boolean = true; + supportsTags: boolean = true; + loginUrl: string = ''; + + LoginDialog = (props: LoginDialogProps) => ; + + FileSubmissionForm = (props: WebsiteSectionProps) => ( + + ); + + NotificationSubmissionForm = ( + props: WebsiteSectionProps + ) => ( + + ); +} + +class PleromaNotificationSubmissionForm extends GenericSubmissionSection< + PleromaNotificationOptions> { + renderLeftForm(data: PleromaNotificationOptions) { + const elements = super.renderLeftForm(data); + elements.push( +
+ + Use title + +
, + + + , + + + , + + + , + ); + return elements; + } +} + +export class PleromaFileSubmissionForm extends GenericFileSubmissionSection { + renderLeftForm(data: PleromaFileOptions) { + const elements = super.renderLeftForm(data); + elements.push( +
+ + Use title + +
, + + + , + + + , + + + , + + + , + ); + return elements; + } +} diff --git a/ui/src/websites/pleroma/PleromaLogin.tsx b/ui/src/websites/pleroma/PleromaLogin.tsx new file mode 100644 index 00000000..ee923c3a --- /dev/null +++ b/ui/src/websites/pleroma/PleromaLogin.tsx @@ -0,0 +1,134 @@ +import { Button, Form, Input, message, Spin } from 'antd'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import { MegalodonAccountData } from 'postybirb-commons'; +import LoginService from '../../services/login.service'; +import { LoginDialogProps } from '../interfaces/website.interface'; +import generator, { OAuth } from 'megalodon' + +interface State extends MegalodonAccountData { + code: string; + client_id: string; + client_secret: string; + loading: boolean; +} + +export default class PleromaLogin extends React.Component { + state: State = { + website: 'pleroma.io', + code: '', + loading: true, + client_id: '', + client_secret: '', + username: '', + token: '' + }; + + private view: any; + + constructor(props: LoginDialogProps) { + super(props); + this.state = { + ...this.state, + ...(props.data as State) + }; + } + + componentDidMount() { + const node = ReactDOM.findDOMNode(this); + if (node instanceof HTMLElement) { + const view: any = node.querySelector('.webview'); + this.view = view; + view.addEventListener('did-stop-loading', () => { + if (this.state.loading) this.setState({ loading: false }); + }); + view.allowpopups = true; + view.partition = `persist:${this.props.account._id}`; + this.getAuthURL(this.state.website); + } + } + + private getWebsiteURL(website?: string) { + return `https://${website || this.state.website}`; + } + + private getAuthURL(website: string) { + let auth_url : string = ""; + // Get the Auth URL ... Display it. + const client = generator('pleroma', this.getWebsiteURL(website)); + this.state.website = website; + let opts: any = { + redirect_uris: `https://localhost:${window['PORT']}/misskey/display/${window.AUTH_ID}` + } + client.registerApp('PostyBirb', opts ) + .then(appData => { + this.state.client_id = appData.clientId; + this.state.client_secret = appData.clientSecret; + this.state.username = appData.name; + auth_url = appData.url || "Error - no auth url"; + this.view.src = auth_url; + }); + } + + submit() { + const website = this.getWebsiteURL(); + const client = generator('pleroma', website); + client.fetchAccessToken(this.state.client_id, this.state.client_secret, this.state.code).then((value: OAuth.TokenData) => { + // Get the username so we have complete data. + const usernameClient = generator('pleroma', website, value.accessToken); + usernameClient.verifyAccountCredentials().then((res)=>{ + this.state.username = res.data.username; + this.state.token = value.access_token; + + LoginService.setAccountData(this.props.account._id, { ...this.state, website } ).then( + () => { + message.success(`${website} authenticated.`); + }); + }); + }) + .catch((err: Error) => { + message.error(`Failed to authenticate ${website}.`); + }) + } + + isValid(): boolean { + return !!this.state.website && !!this.state.code; + } + + render() { + return ( +
+
+
+ + { + const website = target.value.replace(/(https:\/\/|http:\/\/)/, ''); + this.view.loadURL(this.getAuthURL(website)); + this.setState({ website: website }); + }} + /> + + + this.setState({ code: target.value })} + addonAfter={ + + } + /> + +
+
+ + + +
+ ); + } +} diff --git a/ui/src/websites/website-registry.ts b/ui/src/websites/website-registry.ts index 29a636d1..22c37b42 100644 --- a/ui/src/websites/website-registry.ts +++ b/ui/src/websites/website-registry.ts @@ -24,6 +24,7 @@ import { Manebooru } from './manebooru/Manebooru'; import { Mastodon } from './mastodon/Mastodon'; import { MissKey } from './misskey/MissKey'; import { Pillowfort } from './pillowfort/Pillowfort'; +import { Pleroma } from './pleroma/Pleroma'; import { Telegram } from './telegram/Telegram'; import { Furbooru } from './furbooru/Furbooru'; import { Itaku } from './itaku/Itaku'; @@ -59,6 +60,7 @@ export class WebsiteRegistry { Pillowfort: new Pillowfort(), Pixelfed: new Pixelfed(), Pixiv: new Pixiv(), + Pleroma: new Pleroma(), SoFurry: new SoFurry(), SubscribeStar: new SubscribeStar(), SubscribeStarAdult: new SubscribeStarAdult(),