Skip to content

Commit

Permalink
Merge pull request #252 from mvdicarlo/develop
Browse files Browse the repository at this point in the history
v3.1.30
  • Loading branch information
mvdicarlo authored Oct 7, 2023
2 parents 0ac6f5f + 402c62d commit b176e9f
Show file tree
Hide file tree
Showing 17 changed files with 244 additions and 141 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { DefaultFileOptions } from '../../submission/default-options.interface';
export interface BlueskyFileOptions extends DefaultFileOptions {
altText?: string;
label_rating: string;
replyToUrl?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { DefaultOptions } from '../../submission/default-options.interface';

export interface BlueskyNotificationOptions extends DefaultOptions {
label_rating: string;
replyToUrl?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface MastodonFileOptions extends DefaultFileOptions {
spoilerText?: string;
visibility: string;
altText?: string;
replyToUrl?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface MastodonNotificationOptions extends DefaultOptions {
useTitle: boolean;
spoilerText?: string;
visibility: string;
replyToUrl?: string;
}
5 changes: 5 additions & 0 deletions commons/src/websites/bluesky/bluesky.file.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export class BlueskyFileOptionsEntity
@DefaultValue('')
label_rating: string = '';

@Expose()
@IsOptional()
@IsString()
replyToUrl?: string;

constructor(entity?: Partial<BlueskyFileOptions>) {
super(entity as DefaultFileOptions);
}
Expand Down
5 changes: 5 additions & 0 deletions commons/src/websites/bluesky/bluesky.notification.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export class BlueskyNotificationOptionsEntity
@DefaultValue('')
label_rating: string = '';

@Expose()
@IsOptional()
@IsString()
replyToUrl?: string;

constructor(entity?: Partial<BlueskyNotificationOptions>) {
super(entity as DefaultOptions);
}
Expand Down
5 changes: 5 additions & 0 deletions commons/src/websites/mastodon/mastodon.file.options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export class MastodonFileOptionsEntity
@IsString()
altText?: string;

@Expose()
@IsOptional()
@IsString()
replyToUrl?: string;

constructor(entity?: Partial<MastodonFileOptions>) {
super(entity as DefaultFileOptions);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ export class MastodonNotificationOptionsEntity
@DefaultValue('public')
visibility!: string;

@Expose()
@IsOptional()
@IsString()
replyToUrl?: string;

constructor(entity?: Partial<MastodonNotificationOptions>) {
super(entity as DefaultOptions);
}
Expand Down
2 changes: 1 addition & 1 deletion electron-app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "postybirb-plus",
"version": "3.1.29",
"version": "3.1.30",
"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",
Expand Down
141 changes: 82 additions & 59 deletions electron-app/src/server/websites/bluesky/bluesky.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import { BskyAgent, stringifyLex, jsonToLex, AppBskyEmbedImages, AppBskyRichtex
import { PlaintextParser } from 'src/server/description-parsing/plaintext/plaintext.parser';
import fetch from "node-fetch";
import Graphemer from 'graphemer';
import { ReplyRef } from '@atproto/api/dist/client/types/app/bsky/feed/post';
import FormContent from 'src/server/utils/form-content.util';

// Start of Polyfill

Expand Down Expand Up @@ -111,6 +113,10 @@ async function fetchHandler(

// End of Polyfill

function getRichTextLength(text: string): number {
return new RichText({text}).graphemeLength;
}

@Injectable()
export class Bluesky extends Website {
readonly BASE_URL = '';
Expand Down Expand Up @@ -165,6 +171,10 @@ export class Bluesky extends Website {
).map(tag => `#${tag}`);
}

private appendRichTextTags(tags: string[], description: string): string {
return this.appendTags(this.formatTags(tags), description, this.MAX_CHARS, getRichTextLength);
}

private async uploadMedia(
agent: BskyAgent,
data: BlueskyAccountData,
Expand Down Expand Up @@ -197,6 +207,8 @@ export class Bluesky extends Website {
password: accountData.password,
});

const reply = await this.getReplyRef(agent, data.options.replyToUrl);

const files = [data.primary, ...data.additional];
let uploadedMedias: AppBskyEmbedImages.Image[] = [];
let fileCount = 0;
Expand All @@ -215,29 +227,7 @@ export class Bluesky extends Website {
$type: 'app.bsky.embed.images',
};

let status = data.description;
let r = new RichText({text: status});

const tags = this.formatTags(data.tags);

// Update the post content with the Tags if any are specified - for BlueSky (There is no tagging engine yet), 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 = this.MAX_CHARS - status.length;
let tagToInsert = tag;
if (!tag.startsWith('#')) {
tagToInsert = `#${tagToInsert}`;
}
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!
r = new RichText({text: status});
});
const status = this.appendRichTextTags(data.tags, data.description);

let labelsRecord: ComAtprotoLabelDefs.SelfLabels | undefined;
if (data.options.label_rating) {
Expand All @@ -256,6 +246,7 @@ export class Bluesky extends Website {
facets: rt.facets,
embed: embeds,
labels: labelsRecord,
...(reply ? { reply } : {}),
})
.catch(err => {
return Promise.reject(this.createPostResponse({ message: err }));
Expand Down Expand Up @@ -284,29 +275,8 @@ export class Bluesky extends Website {
password: accountData.password,
});

let status = data.description;
let r = new RichText({text: status});

const tags = this.formatTags(data.tags);

// Update the post content with the Tags if any are specified - for BlueSky (There is no tagging engine yet), 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 = this.MAX_CHARS - r.graphemeLength;
let tagToInsert = tag;
if (!tag.startsWith('#')) {
tagToInsert = `#${tagToInsert}`;
}
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!
r = new RichText({text: status});
});
const reply = await this.getReplyRef(agent, data.options.replyToUrl);
const status = this.appendRichTextTags(data.tags, data.description);

let labelsRecord: ComAtprotoLabelDefs.SelfLabels | undefined;
if (data.options.label_rating) {
Expand All @@ -322,7 +292,8 @@ export class Bluesky extends Website {
let postResult = await agent.post({
text: rt.text,
facets: rt.facets,
labels: labelsRecord
labels: labelsRecord,
...(reply ? { reply } : {}),
}).catch(err => {
return Promise.reject(
this.createPostResponse({ message: err }),
Expand All @@ -348,19 +319,14 @@ export class Bluesky extends Website {
const isAutoscaling: boolean = submissionPart.data.autoScale;

if (!submissionPart.data.altText) {
warnings.push(`Bluesky recommends alt text to be provided`);
}

const rt = new RichText({ text: submissionPart.data.description.value });
const agent = new BskyAgent({ service: 'https://bsky.social' })
rt.detectFacets(agent);

if (rt.graphemeLength > this.MAX_CHARS) {
problems.push(
`Max description length allowed is ${this.MAX_CHARS} graphemes.`,
'Bluesky currently always requires alt text to be provided, ' +
'even if your settings say otherwise. This is a bug on their side.',
);
}

this.validateDescriptionLength(problems, submissionPart, defaultPart);

const files = [
submission.primary,
...(submission.additional || []).filter(
Expand Down Expand Up @@ -403,6 +369,8 @@ export class Bluesky extends Website {
);
}

this.validateReplyToUrl(problems, submissionPart.data.replyToUrl);

return { problems, warnings };
}

Expand All @@ -414,16 +382,71 @@ export class Bluesky extends Website {
const problems: string[] = [];
const warnings: string[] = [];

const rt = new RichText({ text: submissionPart.data.description.value });
this.validateDescriptionLength(problems, submissionPart, defaultPart);
this.validateReplyToUrl(problems, submissionPart.data.replyToUrl);

return { problems, warnings };
}

private validateDescriptionLength(
problems: string[],
submissionPart: SubmissionPart<BlueskyNotificationOptions>,
defaultPart: SubmissionPart<DefaultOptions>,
): void {
const description = this.defaultDescriptionParser(
FormContent.getDescription(defaultPart.data.description, submissionPart.data.description),
);

const rt = new RichText({ text: description });
const agent = new BskyAgent({ service: 'https://bsky.social' })
rt.detectFacets(agent);

if (rt.graphemeLength > this.MAX_CHARS) {
problems.push(
`Max description length allowed is ${this.MAX_CHARS} graphemes.`,
`Max description length allowed is ${this.MAX_CHARS} characters.`,
);
}
}

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 async getReplyRef(agent: BskyAgent, url?: string): Promise<ReplyRef | null> {
if (!url?.trim()) {
return null;
}

const postId = this.getPostIdFromUrl(url);
if (!postId) {
throw new Error(`Invalid reply to url '${url}'`);
}

// cf. https://atproto.com/blog/create-post#replies
const parent = await agent.getPost(postId);
const reply = parent.value.reply;
const root = reply ? reply.root : parent;
return {
root: { uri: root.uri, cid: root.cid },
parent: { uri: parent.uri, cid: parent.cid },
};
}

private getPostIdFromUrl(url: string): { repo: string; rkey: string } | null {
// A regular web link like https://bsky.app/profile/{repo}/post/{id}
const link = /\/profile\/([^\/]+)\/post\/([a-zA-Z0-9\.\-_~]+)/.exec(url);
if (link) {
return { repo: link[1], rkey: link[2] };
}

// Protocol link like at://did:plc:{repo}/app.bsky.feed.post/{id}
const at = /(did:plc:[a-zA-Z0-9\.\-_~]+)\/.+\.post\/([a-zA-Z0-9\.\-_~]+)/.exec(url);
if (at) {
return { repo: at[1], rkey: at[2] };
}

return null;
}
}
34 changes: 2 additions & 32 deletions electron-app/src/server/websites/furtastic/furtastic.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import {
SubmissionRating,
} from 'postybirb-commons';
import UserAccountEntity from 'src/server//account/models/user-account.entity';
import { UsernameParser } from 'src/server/description-parsing/miscellaneous/username.parser';
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';
Expand All @@ -25,13 +23,13 @@ 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';
import { MarkdownParser } from 'src/server/description-parsing/markdown/markdown.parser';

@Injectable()
export class Furtastic extends Website {
readonly BASE_URL: string = 'https://api.furtastic.art';
readonly acceptsFiles: string[] = ['jpeg', 'jpg', 'png', 'gif', 'webm'];
readonly acceptsSourceUrls: boolean = true;
readonly defaultDescriptionParser = PlaintextParser.parse;
readonly defaultDescriptionParser = MarkdownParser.parse;
readonly enableAdvertisement: boolean = true;
readonly acceptsAdditionalFiles: boolean = true;

Expand All @@ -55,34 +53,6 @@ export class Furtastic extends Website {
getScalingOptions(file: FileRecord): ScalingOptions {
return { maxSize: FileSize.MBtoBytes(100) };
}

preparseDescription(text: string) {
return UsernameParser.replaceText(text, 'ft', '@$1').replace(
/<a(.*?)href="(.*?)"(.*?)>(.*?)<\/a>/gi,
'"$4":$2',
);
}

parseDescription(text: string) {
text = text.replace(/<b>/gi, '**');
text = text.replace(/<i>/gi, '*');
text = text.replace(/<u>/gi, '');
text = text.replace(/<s>/gi, '~~');
text = text.replace(/<\/b>/gi, '**');
text = text.replace(/<\/i>/gi, '*');
text = text.replace(/<\/u>/gi, '');
text = text.replace(/<\/s>/gi, '~~');
text = text.replace(/<em>/gi, '*');
text = text.replace(/<\/em>/gi, '*');
text = text.replace(/<strong>/gi, '**');
text = text.replace(/<\/strong>/gi, '**');
text = text.replace(
/<span style="color:\s*(.*?);*">((.|\n)*?)<\/span>/gim,
'$2',
);
text = text.replace(/<a(.*?)href="(.*?)"(.*?)>(.*?)<\/a>/gi, '[$4]($2)');
return super.parseDescription(text);
}

async postFileSubmission(
cancellationToken: CancellationToken,
Expand Down
Loading

0 comments on commit b176e9f

Please sign in to comment.