From cd8b6f72eed1e9f6e4a90ed923683ad2e8682e0a Mon Sep 17 00:00:00 2001 From: Michael DiCarlo Date: Tue, 2 Mar 2021 07:17:46 -0500 Subject: [PATCH 1/7] Start v3.0.26 --- electron-app/package.json | 2 +- package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/electron-app/package.json b/electron-app/package.json index f882b629..e8fbc033 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -1,6 +1,6 @@ { "name": "postybirb-plus", - "version": "3.0.25", + "version": "3.0.26", "description": "(ClientServer) PostyBirb is an application that helps artists post art and other multimedia to multiple websites more quickly.", "main": "dist/main.js", "author": "Michael DiCarlo", diff --git a/package.json b/package.json index 6bc426cb..1cbf7839 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "postybirb-plus", - "version": "3.0.25", + "version": "3.0.26", "description": "PostyBirb is an application that helps artists post art and other multimedia to multiple websites more quickly..", "main": "index.js", "scripts": { diff --git a/ui/package.json b/ui/package.json index 75b52b40..02c79684 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "postybirb-plus-ui", - "version": "3.0.25", + "version": "3.0.26", "license": "BSD-3-Clause", "private": true, "Author": "Michael DiCarlo", From 97b11ef1a5103f61608353c0766153b1b7c3b396 Mon Sep 17 00:00:00 2001 From: Michael DiCarlo Date: Tue, 2 Mar 2021 07:19:35 -0500 Subject: [PATCH 2/7] Logging: Log http errors --- electron-app/src/server/http/http.util.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/electron-app/src/server/http/http.util.ts b/electron-app/src/server/http/http.util.ts index da1cff8d..5f4e3aca 100644 --- a/electron-app/src/server/http/http.util.ts +++ b/electron-app/src/server/http/http.util.ts @@ -4,6 +4,7 @@ import 'url'; import * as _ from 'lodash'; import CookieConverter from 'src/server/utils/cookie-converter.util'; import * as setCookie from 'set-cookie-parser'; +import { Logger } from '@nestjs/common'; interface GetOptions { headers?: any; updateCookies?: boolean; @@ -24,6 +25,7 @@ export interface HttpResponse { } export default class Http { + private static logger: Logger = new Logger(Http.name); static Request = request.defaults({ headers: { 'User-Agent': session.defaultSession.getUserAgent(), @@ -31,7 +33,7 @@ export default class Http { }); static parseCookies(cookies: Electron.Cookie[]) { - return cookies.map(c => `${c.name}=${c.value}`).join('; '); + return cookies.map((c) => `${c.name}=${c.value}`).join('; '); } static async getWebsiteCookies(partitionId: string, url: string): Promise { @@ -66,8 +68,8 @@ export default class Http { expirationDate = new Date(expirationDate.setMonth(expirationDate.getMonth() + 2)); await Promise.all( cookies - .filter(c => c.session) - .map(c => { + .filter((c) => c.session) + .map((c) => { const cookie: Electron.CookiesSetDetails = { ...CookieConverter.convertCookie(c), expirationDate: expirationDate.valueOf() / 1000, @@ -109,7 +111,7 @@ export default class Http { } const opts = Http.getCommonOptions(headers, options.requestOptions); - return new Promise(resolve => { + return new Promise((resolve) => { Http.Request.get(uri, opts, async (error, response, body) => { const res: HttpResponse = { response, @@ -118,6 +120,10 @@ export default class Http { returnUrl: _.get(response, 'request.uri.href'), }; + if (error) { + Http.logger.error(error, null, uri); + } + if (options.updateCookies && response.headers['set-cookie']) { const cookies = setCookie.parse(response); const ses = session.fromPartition(`persist:${partitionId}`); @@ -182,7 +188,7 @@ export default class Http { opts.body = options.data; } - return new Promise(resolve => { + return new Promise((resolve) => { const request = Http.Request[type](uri, opts, (error, response, body) => { const res: HttpResponse = { error, @@ -190,6 +196,9 @@ export default class Http { body, returnUrl: _.get(response, 'request.uri.href'), }; + if (error) { + Http.logger.error(error, null, uri); + } resolve(res); }); From c4136dac1ee8047f8c27a08897d9a664d216c94c Mon Sep 17 00:00:00 2001 From: Michael DiCarlo Date: Tue, 2 Mar 2021 07:28:41 -0500 Subject: [PATCH 3/7] Posting: Websites capable of taking source urls will now wait for all other websites to complete instead of posting when the first source is provided --- .../src/server/submission/post/post.service.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/electron-app/src/server/submission/post/post.service.ts b/electron-app/src/server/submission/post/post.service.ts index 780692d5..395f68df 100644 --- a/electron-app/src/server/submission/post/post.service.ts +++ b/electron-app/src/server/submission/post/post.service.ts @@ -226,10 +226,6 @@ export class PostService { this.accountPostTimeMap[`${data.part.accountId}-${data.part.website}`] = Date.now(); } - // if (this.settings.getValue('emptyQueueOnFailedPost')) { - // this.emptyQueue(submission.type); - // } - // Save part and then check/notify this.partService .createOrUpdateSubmissionPart( @@ -262,9 +258,9 @@ export class PostService { this.postingParts[type].forEach(poster => { poster.addSource(source); // Start poster that was waiting for a source - if (poster.waitForExternalStart) { - poster.doPost(); - } + // if (poster.waitForExternalStart) { + // poster.doPost(); + // } }); } @@ -276,7 +272,7 @@ export class PostService { const waitingForExternal = incomplete.filter(poster => poster.waitForExternalStart); if (waitingForExternal.length === incomplete.length) { - // Force post start as no source was found + // Start posts that were awaiting external sources, even if they haven't gotten a source. waitingForExternal.forEach(poster => poster.doPost()); } } else { From 2ea9e5f75e50a3b0d492f57fc6d2ad678aad9f36 Mon Sep 17 00:00:00 2001 From: Michael DiCarlo Date: Tue, 2 Mar 2021 08:26:38 -0500 Subject: [PATCH 4/7] Patreon: Teaser text option + Only attachment option + tier gating --- .../patreon/patreon.file.options.interface.ts | 2 + .../patreon.notification.options.interface.ts | 1 + .../websites/patreon/patreon.file.options.ts | 12 ++- .../patreon/patreon.notification.options.ts | 7 +- .../websites/patreon/patreon.service.ts | 7 +- ui/src/websites/patreon/Patreon.tsx | 79 ++++++++++++++++++- 6 files changed, 100 insertions(+), 8 deletions(-) diff --git a/commons/src/interfaces/websites/patreon/patreon.file.options.interface.ts b/commons/src/interfaces/websites/patreon/patreon.file.options.interface.ts index 3703ad1f..df6f244e 100644 --- a/commons/src/interfaces/websites/patreon/patreon.file.options.interface.ts +++ b/commons/src/interfaces/websites/patreon/patreon.file.options.interface.ts @@ -4,4 +4,6 @@ export interface PatreonFileOptions extends DefaultFileOptions { tiers: string[]; charge: boolean; schedule?: string; // as date string + teaser?: string; + allAsAttachment: boolean; } diff --git a/commons/src/interfaces/websites/patreon/patreon.notification.options.interface.ts b/commons/src/interfaces/websites/patreon/patreon.notification.options.interface.ts index dad4df30..c1d3f07a 100644 --- a/commons/src/interfaces/websites/patreon/patreon.notification.options.interface.ts +++ b/commons/src/interfaces/websites/patreon/patreon.notification.options.interface.ts @@ -4,4 +4,5 @@ export interface PatreonNotificationOptions extends DefaultOptions { tiers: string[]; charge: boolean; schedule?: string; // as date string + teaser?: string; } diff --git a/commons/src/websites/patreon/patreon.file.options.ts b/commons/src/websites/patreon/patreon.file.options.ts index 9e2cca90..ddc4f97f 100644 --- a/commons/src/websites/patreon/patreon.file.options.ts +++ b/commons/src/websites/patreon/patreon.file.options.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { IsArray, IsBoolean, IsDateString, IsOptional } from 'class-validator'; +import { IsArray, IsBoolean, IsDateString, IsOptional, IsString } from 'class-validator'; import { DefaultFileOptions } from '../../interfaces/submission/default-options.interface'; import { PatreonFileOptions } from '../../interfaces/websites/patreon/patreon.file.options.interface'; import { DefaultValue } from '../../models/decorators/default-value.decorator'; @@ -22,6 +22,16 @@ export class PatreonFileOptionsEntity extends DefaultFileOptionsEntity @IsDateString() schedule?: string; + @Expose() + @IsOptional() + @IsString() + teaser?: string; + + @Expose() + @IsBoolean() + @DefaultValue(false) + allAsAttachment!: boolean; + constructor(entity?: Partial) { super(entity as DefaultFileOptions); } diff --git a/commons/src/websites/patreon/patreon.notification.options.ts b/commons/src/websites/patreon/patreon.notification.options.ts index 215bcf9e..8ca8f2ea 100644 --- a/commons/src/websites/patreon/patreon.notification.options.ts +++ b/commons/src/websites/patreon/patreon.notification.options.ts @@ -1,5 +1,5 @@ import { Expose } from 'class-transformer'; -import { IsArray, IsBoolean, IsDateString, IsOptional } from 'class-validator'; +import { IsArray, IsBoolean, IsDateString, IsOptional, IsString } from 'class-validator'; import { DefaultOptions } from '../../interfaces/submission/default-options.interface'; import { PatreonNotificationOptions } from '../../interfaces/websites/patreon/patreon.notification.options.interface'; import { DefaultValue } from '../../models/decorators/default-value.decorator'; @@ -22,6 +22,11 @@ export class PatreonNotificationOptionsEntity extends DefaultOptionsEntity @IsDateString() schedule?: string; + @Expose() + @IsOptional() + @IsString() + teaser?: string; + constructor(entity?: Partial) { super(entity as DefaultOptions); } diff --git a/electron-app/src/server/websites/patreon/patreon.service.ts b/electron-app/src/server/websites/patreon/patreon.service.ts index 97c8939e..1866a046 100644 --- a/electron-app/src/server/websites/patreon/patreon.service.ts +++ b/electron-app/src/server/websites/patreon/patreon.service.ts @@ -305,7 +305,7 @@ export class Patreon extends Website { post_type: createData.data.type, is_paid: options.charge ? 'true' : 'false', title: data.title, - teaser_text: '', + teaser_text: options.teaser || '', post_metadata: {}, tags: { publish: true }, }; @@ -417,7 +417,8 @@ export class Patreon extends Website { for (const file of data.additional) { if ( data.primary.type === FileSubmissionType.IMAGE && - file.type === FileSubmissionType.IMAGE + file.type === FileSubmissionType.IMAGE && + !data.options.allAsAttachment ) { const upload = await this.uploadFile( link, @@ -455,7 +456,7 @@ export class Patreon extends Website { post_type: createData.data.type, is_paid: options.charge ? 'true' : 'false', title: data.title, - teaser_text: '', + teaser_text: options.teaser || '', post_metadata: {}, tags: { publish: true }, }; diff --git a/ui/src/websites/patreon/Patreon.tsx b/ui/src/websites/patreon/Patreon.tsx index 37c319a7..e2cdeb52 100644 --- a/ui/src/websites/patreon/Patreon.tsx +++ b/ui/src/websites/patreon/Patreon.tsx @@ -1,4 +1,4 @@ -import { Checkbox, DatePicker, Form, Select } from 'antd'; +import { Checkbox, DatePicker, Form, Input, Select } from 'antd'; import _ from 'lodash'; import moment from 'moment'; import { @@ -71,6 +71,9 @@ export class PatreonNotificationSubmissionForm extends GenericSubmissionSection< folders: [] }; + patronsOnlyId: string = ''; + publicId: string = ''; + constructor(props: WebsiteSectionProps) { super(props); this.state = { @@ -81,6 +84,15 @@ export class PatreonNotificationSubmissionForm extends GenericSubmissionSection< ({ data }) => { if (data) { if (!_.isEqual(this.state.folders, data)) { + data.forEach(tier => { + if (tier.label === 'Patrons Only') { + this.patronsOnlyId = tier.value!; + } + + if (tier.label === 'Public') { + this.publicId = tier.value!; + } + }); this.setState({ folders: data }); } } @@ -112,7 +124,19 @@ export class PatreonNotificationSubmissionForm extends GenericSubmissionSection< className="w-full" value={data.tiers} mode="multiple" - onChange={this.setValue.bind(this, 'tiers')} + onChange={(value: string[] | undefined) => { + if (value) { + let val = [...value]; + if (val.includes(this.patronsOnlyId)) { + val = [this.patronsOnlyId]; + } else if (val.includes(this.publicId)) { + val = [this.publicId]; + } + this.setValue('tiers', val); + } else { + this.setState('tiers', value); + } + }} allowClear > {this.state.folders.map(f => @@ -138,6 +162,13 @@ export class PatreonNotificationSubmissionForm extends GenericSubmissionSection< this.setValue('schedule', value ? value.toDate().toString() : undefined) } /> + , + + ); return elements; @@ -149,6 +180,9 @@ export class PatreonFileSubmissionForm extends GenericFileSubmissionSection) { super(props); this.state = { @@ -159,6 +193,15 @@ export class PatreonFileSubmissionForm extends GenericFileSubmissionSection { if (data) { if (!_.isEqual(this.state.folders, data)) { + data.forEach(tier => { + if (tier.label === 'Patrons Only') { + this.patronsOnlyId = tier.value!; + } + + if (tier.label === 'Public') { + this.publicId = tier.value!; + } + }); this.setState({ folders: data }); } } @@ -176,6 +219,17 @@ export class PatreonFileSubmissionForm extends GenericFileSubmissionSection + , +
+ + Additional images as attachments{' '} + + (Additional images will be posted as file attachments.) + +
); return elements; @@ -190,7 +244,19 @@ export class PatreonFileSubmissionForm extends GenericFileSubmissionSection { + if (value) { + let val = [...value]; + if (val.includes(this.patronsOnlyId)) { + val = [this.patronsOnlyId]; + } else if (val.includes(this.publicId)) { + val = [this.publicId]; + } + this.setValue('tiers', val); + } else { + this.setState('tiers', value); + } + }} allowClear > {this.state.folders.map(f => @@ -216,6 +282,13 @@ export class PatreonFileSubmissionForm extends GenericFileSubmissionSection + , + + ); return elements; From 40c2b4c2f621ec06df6f673e86066dd73d3a7185 Mon Sep 17 00:00:00 2001 From: Michael DiCarlo Date: Wed, 3 Mar 2021 07:10:22 -0500 Subject: [PATCH 5/7] Furbooru: Added Furbooru (by duplicating Derpibooru) --- commons/src/index.ts | 1 + .../furbooru.file.options.interface.ts | 5 + .../furbooru/furbooru.file.options.ts | 17 ++ .../src/websites/furbooru/furbooru.options.ts | 7 + .../websites/furbooru/furbooru.module.ts | 8 + .../websites/furbooru/furbooru.service.ts | 199 ++++++++++++++++++ .../websites/website-provider.service.ts | 2 + .../src/server/websites/websites.module.ts | 38 ++-- ui/src/websites/furbooru/Furbooru.tsx | 47 +++++ ui/src/websites/website-registry.ts | 2 + 10 files changed, 308 insertions(+), 18 deletions(-) create mode 100644 commons/src/interfaces/websites/furbooru/furbooru.file.options.interface.ts create mode 100644 commons/src/websites/furbooru/furbooru.file.options.ts create mode 100644 commons/src/websites/furbooru/furbooru.options.ts create mode 100644 electron-app/src/server/websites/furbooru/furbooru.module.ts create mode 100644 electron-app/src/server/websites/furbooru/furbooru.service.ts create mode 100644 ui/src/websites/furbooru/Furbooru.tsx diff --git a/commons/src/index.ts b/commons/src/index.ts index b6924230..307b8962 100644 --- a/commons/src/index.ts +++ b/commons/src/index.ts @@ -41,6 +41,7 @@ export * from './interfaces/websites/artconomy/artconomy.file.options.interface' export * from './interfaces/websites/aryion/aryion.file.options.interface'; export * from './interfaces/websites/custom/custom.account.interface'; export * from './interfaces/websites/derpibooru/derpibooru.file.options.interface'; +export * from './interfaces/websites/furbooru/furbooru.file.options.interface'; export * from './interfaces/websites/deviant-art/deviant-art.account.interface'; export * from './interfaces/websites/deviant-art/deviant-art.file.options.interface'; export * from './interfaces/websites/discord/discord.file.options.interface'; diff --git a/commons/src/interfaces/websites/furbooru/furbooru.file.options.interface.ts b/commons/src/interfaces/websites/furbooru/furbooru.file.options.interface.ts new file mode 100644 index 00000000..46f09d57 --- /dev/null +++ b/commons/src/interfaces/websites/furbooru/furbooru.file.options.interface.ts @@ -0,0 +1,5 @@ +import { DefaultFileOptions } from '../../submission/default-options.interface'; + +export interface FurbooruFileOptions extends DefaultFileOptions { + source?: string; +} diff --git a/commons/src/websites/furbooru/furbooru.file.options.ts b/commons/src/websites/furbooru/furbooru.file.options.ts new file mode 100644 index 00000000..48142303 --- /dev/null +++ b/commons/src/websites/furbooru/furbooru.file.options.ts @@ -0,0 +1,17 @@ +import { Expose } from 'class-transformer'; +import { IsOptional, IsString } from 'class-validator'; +import { DefaultFileOptions } from '../../interfaces/submission/default-options.interface'; +import { FurbooruFileOptions } from '../../interfaces/websites/furbooru/furbooru.file.options.interface'; +import { DefaultFileOptionsEntity } from '../../models/default-file-options.entity'; + +export class FurbooruFileOptionsEntity extends DefaultFileOptionsEntity + implements FurbooruFileOptions { + @Expose() + @IsOptional() + @IsString() + source?: string; + + constructor(entity?: Partial) { + super(entity as DefaultFileOptions); + } +} diff --git a/commons/src/websites/furbooru/furbooru.options.ts b/commons/src/websites/furbooru/furbooru.options.ts new file mode 100644 index 00000000..8abaad29 --- /dev/null +++ b/commons/src/websites/furbooru/furbooru.options.ts @@ -0,0 +1,7 @@ +import { DefaultOptionsEntity } from '../../models/default-options.entity'; +import { FurbooruFileOptionsEntity } from './furbooru.file.options'; + +export class Furbooru { + static readonly FileOptions = FurbooruFileOptionsEntity; + static readonly NotificationOptions = DefaultOptionsEntity; +} diff --git a/electron-app/src/server/websites/furbooru/furbooru.module.ts b/electron-app/src/server/websites/furbooru/furbooru.module.ts new file mode 100644 index 00000000..ddc5f970 --- /dev/null +++ b/electron-app/src/server/websites/furbooru/furbooru.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { Furbooru } from './furbooru.service'; + +@Module({ + providers: [Furbooru], + exports: [Furbooru], +}) +export class FurbooruModule {} diff --git a/electron-app/src/server/websites/furbooru/furbooru.service.ts b/electron-app/src/server/websites/furbooru/furbooru.service.ts new file mode 100644 index 00000000..11ee8a72 --- /dev/null +++ b/electron-app/src/server/websites/furbooru/furbooru.service.ts @@ -0,0 +1,199 @@ +import { Injectable } from '@nestjs/common'; +import { + DefaultOptions, + FileRecord, + FileSubmission, + FileSubmissionType, + FurbooruFileOptions, + PostResponse, + SubmissionPart, + SubmissionRating, + UsernameShortcut, +} from 'postybirb-commons'; +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 } from 'src/server/submission/post/interfaces/file-post-data.interface'; +import { ValidationParts } from 'src/server/submission/validator/interfaces/validation-parts.interface'; +import BrowserWindowUtil from 'src/server/utils/browser-window.util'; +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 { ScalingOptions } from '../interfaces/scaling-options.interface'; +import { Website } from '../website.base'; + +@Injectable() +export class Furbooru extends Website { + BASE_URL: string = 'https://furbooru.org'; + acceptsFiles: string[] = ['jpeg', 'jpg', 'png', 'svg', 'gif', 'webm']; + acceptsSourceUrls: boolean = true; + enableAdvertisement: boolean = false; + defaultDescriptionParser = PlaintextParser.parse; + usernameShortcuts: UsernameShortcut[] = [ + { + key: 'furb', + url: 'https://furbooru.org/profiles/$1', + }, + ]; + + async checkLoginStatus(data: UserAccountEntity): Promise { + const status: LoginResponse = { loggedIn: false, username: null }; + const res = await Http.get(`${this.BASE_URL}`, data._id); + if (res.body.includes('Logout')) { + status.loggedIn = true; + status.username = res.body.match(/data-user-name="(.*?)"/)[1]; + Http.saveSessionCookies(this.BASE_URL, data._id); + } + return status; + } + + getScalingOptions(file: FileRecord): ScalingOptions { + return { maxSize: FileSize.MBtoBytes(100) }; + } + + preparseDescription(text: string) { + // NOTE: Has a weird format issue when inlines are nested e.g. italic within a bold + return text + .replace(//gi, '*') + .replace(//gi, '_') + .replace(//gi, '+') + .replace(//gi, '-') + .replace(/<\/b>/gi, '*') + .replace(/<\/i>/gi, '_') + .replace(/<\/u>/gi, '+') + .replace(/<\/s>/gi, '-') + .replace(//gi, '_') + .replace(/<\/em>/gi, '_') + .replace(//gi, '*') + .replace(/<\/strong>/gi, '*') + .replace(/(.*?)<\/a>/gi, '"$4":$2'); + } + + parseDescription(text: string) { + return super.parseDescription(text.replace(/(.*?)<\/a>/gi, '"$4":$2')); + } + + async postFileSubmission( + cancellationToken: CancellationToken, + data: FilePostData, + ): Promise { + try { + return await this.attemptFilePost(cancellationToken, data); + } catch (err) { + // Users have reported it working on a second post :shrug: + this.logger.warn(err, 'Furbooru Post Retry'); + return await this.attemptFilePost(cancellationToken, data); + } + } + + private getRating(rating: SubmissionRating) { + switch (rating) { + case SubmissionRating.MATURE: + return 'questionable'; + case SubmissionRating.ADULT: + case SubmissionRating.EXTREME: + return 'explicit'; + case SubmissionRating.GENERAL: + default: + return 'safe'; + } + } + + private async attemptFilePost( + cancellationToken: CancellationToken, + data: FilePostData, + ): Promise { + const tags: string[] = this.parseTags(data.tags, { + spaceReplacer: ' ', + minLength: 1, + maxLength: 100, + }); + const ratingTag: string = this.getRating(data.rating); + const knownRatings: string[] = [ + 'safe', + 'suggestive', + 'questionable', + 'explicit', + 'semi-grimdark', + 'grimdark', + 'grotesque', + ]; + const lowerCaseTags = tags.map((t) => t.toLowerCase()); + if (!lowerCaseTags.includes(ratingTag)) { + let add = true; + + for (const r of knownRatings) { + if (lowerCaseTags.includes(r)) { + add = false; + break; + } + } + + if (add) { + tags.push(ratingTag); + } + } + + const form: any = { + ...(await BrowserWindowUtil.getFormData(data.part.accountId, `${this.BASE_URL}/images/new`, { + custom: 'document.body.querySelectorAll("form")[3]', + })), + _method: 'post', + 'image[tag_input]': this.formatTags(tags), + 'image[image]': data.primary.file, + 'image[description]': data.description, + 'image[source_url]': data.options.source || data.sources[0] || '', + }; + + this.checkCancelled(cancellationToken); + const postResponse = await Http.post(`${this.BASE_URL}/images`, data.part.accountId, { + type: 'multipart', + data: form, + }); + + this.verifyResponse(postResponse); + return this.createPostResponse({}); + } + + formatTags(tags: string[]) { + return super.formatTags(tags).join(', ').trim(); + } + + validateFileSubmission( + submission: FileSubmission, + submissionPart: SubmissionPart, + defaultPart: SubmissionPart, + ): ValidationParts { + const problems: string[] = []; + const warnings: string[] = []; + const isAutoscaling: boolean = submissionPart.data.autoScale; + + if (FormContent.getTags(defaultPart.data.tags, submissionPart.data.tags).length < 5) { + problems.push('Requires at least 5 tags.'); + } + + if (!WebsiteValidator.supportsFileType(submission.primary, this.acceptsFiles)) { + problems.push(`Currently supported file formats: ${this.acceptsFiles.join(', ')}`); + } + + const { type, size, name } = submission.primary; + let maxMB: number = 100; + + if (FileSize.MBtoBytes(maxMB) < size) { + if ( + isAutoscaling && + type === FileSubmissionType.IMAGE && + ImageManipulator.isMimeType(submission.primary.mimetype) + ) { + warnings.push(`${name} will be scaled down to ${maxMB}MB`); + } else { + problems.push(`Furbooru limits ${submission.primary.mimetype} to ${maxMB}MB`); + } + } + + return { problems, warnings }; + } +} diff --git a/electron-app/src/server/websites/website-provider.service.ts b/electron-app/src/server/websites/website-provider.service.ts index 1ecc53e0..b3b365ff 100644 --- a/electron-app/src/server/websites/website-provider.service.ts +++ b/electron-app/src/server/websites/website-provider.service.ts @@ -27,6 +27,7 @@ import { Mastodon } from './mastodon/mastodon.service'; import { Twitter } from './twitter/twitter.service'; import { Pillowfort } from './pillowfort/pillowfort.service'; import { Telegram } from './telegram/telegram.service'; +import { Furbooru } from './furbooru/furbooru.service'; @Injectable() export class WebsiteProvider { @@ -61,6 +62,7 @@ export class WebsiteProvider { readonly twitter: Twitter, readonly pillowfort: Pillowfort, readonly telegram: Telegram, + readonly furbooru: Furbooru ) { this.websiteModules = [...arguments].filter(arg => arg instanceof Website); this.websiteModules.forEach( diff --git a/electron-app/src/server/websites/websites.module.ts b/electron-app/src/server/websites/websites.module.ts index 6fe0b1df..13bcc345 100644 --- a/electron-app/src/server/websites/websites.module.ts +++ b/electron-app/src/server/websites/websites.module.ts @@ -29,6 +29,7 @@ import { MastodonModule } from './mastodon/mastodon.module'; import { TwitterModule } from './twitter/twitter.module'; import { PillowfortModule } from './pillowfort/pillowfort.module'; import { TelegramModule } from './telegram/telegram.module'; +import { FurbooruModule } from './furbooru/furbooru.module'; @Module({ controllers: [WebsitesController], @@ -36,32 +37,33 @@ import { TelegramModule } from './telegram/telegram.module'; exports: [WebsiteProvider, WebsitesService], imports: [ ArtconomyModule, - PiczelModule, - WeasylModule, - FurifficModule, - DiscordModule, + AryionModule, + CustomModule, DerpibooruModule, - KoFiModule, - InkbunnyModule, - SoFurryModule, + DeviantArtModule, + DiscordModule, E621Module, FurAffinityModule, - SubscribeStarModule, - HentaiFoundryModule, - AryionModule, - CustomModule, - NewgroundsModule, - PixivModule, - NewTumblModule, + FurbooruModule, + FurifficModule, FurryLifeModule, FurryNetworkModule, - PatreonModule, - TumblrModule, - DeviantArtModule, + HentaiFoundryModule, + InkbunnyModule, + KoFiModule, MastodonModule, - TwitterModule, + NewTumblModule, + NewgroundsModule, + PatreonModule, + PiczelModule, PillowfortModule, + PixivModule, + SoFurryModule, + SubscribeStarModule, TelegramModule, + TumblrModule, + TwitterModule, + WeasylModule, ], }) export class WebsitesModule {} diff --git a/ui/src/websites/furbooru/Furbooru.tsx b/ui/src/websites/furbooru/Furbooru.tsx new file mode 100644 index 00000000..5e4c2bd8 --- /dev/null +++ b/ui/src/websites/furbooru/Furbooru.tsx @@ -0,0 +1,47 @@ +import { Form, Input } from 'antd'; +import { FurbooruFileOptions, FileSubmission } from 'postybirb-commons'; +import React from 'react'; +import { WebsiteSectionProps } from '../form-sections/website-form-section.interface'; +import GenericFileSubmissionSection from '../generic/GenericFileSubmissionSection'; +import { WebsiteImpl } from '../website.base'; + +export class Furbooru extends WebsiteImpl { + internalName: string = 'Furbooru'; + name: string = 'Furbooru'; + supportsAdditionalFiles: boolean = false; + supportsTags: boolean = true; + loginUrl: string = 'https://furbooru.org/session/new'; + + FileSubmissionForm = (props: WebsiteSectionProps) => ( + + ); +} + +export class FurbooruFileSubmissionForm extends GenericFileSubmissionSection { + constructor(props: WebsiteSectionProps) { + super(props); + this.state = { + folders: [] + }; + } + + renderRightForm(data: FurbooruFileOptions) { + const elements = super.renderRightForm(data); + elements.push( + + + + ); + return elements; + } +} diff --git a/ui/src/websites/website-registry.ts b/ui/src/websites/website-registry.ts index 8597d682..85f54094 100644 --- a/ui/src/websites/website-registry.ts +++ b/ui/src/websites/website-registry.ts @@ -26,6 +26,7 @@ import { Mastodon } from './mastodon/Mastodon'; import { Twitter } from './twitter/Twitter'; import { Pillowfort } from './pillowfort/Pillowfort'; import { Telegram } from './telegram/Telegram'; +import { Furbooru } from './furbooru/Furbooru'; export class WebsiteRegistry { static readonly websites: Record = { @@ -36,6 +37,7 @@ export class WebsiteRegistry { DeviantArt: new DeviantArt(), Discord: new Discord(), FurAffinity: new FurAffinity(), + Furbooru: new Furbooru(), Furiffic: new Furiffic(), FurryLife: new FurryLife(), FurryNetwork: new FurryNetwork(), From 056596e131a32556268cd8f5d5792a46df50d33b Mon Sep 17 00:00:00 2001 From: Michael DiCarlo Date: Wed, 3 Mar 2021 07:15:40 -0500 Subject: [PATCH 6/7] UI: Made a specific update field for v3 lookup --- ui/src/services/update.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/services/update.service.ts b/ui/src/services/update.service.ts index 75deea9b..7c755fdd 100644 --- a/ui/src/services/update.service.ts +++ b/ui/src/services/update.service.ts @@ -20,7 +20,7 @@ export default class UpdateService { .get('http://postybirb.com/version.html') .then(res => res.data) .then(html => { - const value = html.match(/name="updates" value="(.*?)"/)[1]; + const value = html.match(/name="updates-v3" value="(.*?)"/)[1]; if (value) { notification.info({ message: 'Notice', From b74ae9e5cdea35aafec1e91dd9196f92237b7a77 Mon Sep 17 00:00:00 2001 From: Michael DiCarlo Date: Wed, 3 Mar 2021 08:06:38 -0500 Subject: [PATCH 7/7] UI: Reverse login display ordering (logged out first) --- ui/src/views/login/Login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/views/login/Login.tsx b/ui/src/views/login/Login.tsx index 57fc49ff..b0bc7145 100644 --- a/ui/src/views/login/Login.tsx +++ b/ui/src/views/login/Login.tsx @@ -30,7 +30,7 @@ export class Login extends React.Component { if (aLoggedIn === bLoggedIn) { return a[1].name.localeCompare(b[1].name); } else { - if (aLoggedIn && !bLoggedIn) return -1; + if (!aLoggedIn && bLoggedIn) return -1; else return 1; } });