Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replies for Mastodon and Bluesky #249

Merged
merged 2 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
54 changes: 53 additions & 1 deletion electron-app/src/server/websites/bluesky/bluesky.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ 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';

// Start of Polyfill

Expand Down Expand Up @@ -205,6 +206,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 Down Expand Up @@ -242,6 +245,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 @@ -270,6 +274,7 @@ export class Bluesky extends Website {
password: accountData.password,
});

const reply = await this.getReplyRef(agent, data.options.replyToUrl);
const status = this.appendRichTextTags(data.tags, data.description);

let labelsRecord: ComAtprotoLabelDefs.SelfLabels | undefined;
Expand All @@ -286,7 +291,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 Down Expand Up @@ -367,6 +373,8 @@ export class Bluesky extends Website {
);
}

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

return { problems, warnings };
}

Expand All @@ -388,6 +396,50 @@ export class Bluesky extends Website {
);
}

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 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;
}
}
29 changes: 28 additions & 1 deletion electron-app/src/server/websites/mastodon/mastodon.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export class Mastodon extends Website {
}`.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);
Expand All @@ -233,7 +234,10 @@ export class Mastodon extends Website {

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;
}
Expand Down Expand Up @@ -282,6 +286,11 @@ export class Mastodon extends Website {
}
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;
Expand Down Expand Up @@ -384,6 +393,8 @@ export class Mastodon extends Website {
);
}

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

return { problems, warnings };
}

Expand All @@ -392,6 +403,7 @@ export class Mastodon extends Website {
submissionPart: SubmissionPart<MastodonNotificationOptions>,
defaultPart: SubmissionPart<DefaultOptions>,
): ValidationParts {
const problems = [];
const warnings = [];
const description = this.defaultDescriptionParser(
FormContent.getDescription(defaultPart.data.description, submissionPart.data.description),
Expand All @@ -409,6 +421,21 @@ export class Mastodon extends Website {
);
}

return { problems: [], warnings };
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 {
// 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);
return match ? match[1] : null;
}
}
10 changes: 8 additions & 2 deletions ui/src/websites/bluesky/Bluesky.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ BlueskyNotificationOptions
<Select.Option value={'nudity'}>Adult: Nudity</Select.Option>
<Select.Option value={'porn'}>Adult: Porn</Select.Option>
</Select>
</Form.Item>,
</Form.Item>,
<Form.Item label="Reply To Post URL">
<Input value={data.replyToUrl} onChange={this.handleValueChange.bind(this, 'replyToUrl')} />
</Form.Item>,
);
return elements;
}
Expand All @@ -84,13 +87,16 @@ export class BlueskyFileSubmissionForm extends GenericFileSubmissionSection<Blue
<Select.Option value={'nudity'}>Adult: Nudity</Select.Option>
<Select.Option value={'porn'}>Adult: Porn</Select.Option>
</Select>
</Form.Item>,
</Form.Item>,
<Form.Item label="Alt Text">
<Input
value={data.altText}
onChange={this.handleValueChange.bind(this, 'altText')}
/>
</Form.Item>,
<Form.Item label="Reply To Post URL">
<Input value={data.replyToUrl} onChange={this.handleValueChange.bind(this, 'replyToUrl')} />
</Form.Item>,
);
return elements;
}
Expand Down
10 changes: 8 additions & 2 deletions ui/src/websites/mastodon/Mastodon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ class MastodonNotificationSubmissionForm extends GenericSubmissionSection<
<Select.Option value="private">Followers Only</Select.Option>
<Select.Option value="direct">Mentioned Users Only</Select.Option>
</Select>
</Form.Item>
</Form.Item>,
<Form.Item label="Reply To Post URL">
<Input value={data.replyToUrl} onChange={this.handleValueChange.bind(this, 'replyToUrl')} />
</Form.Item>,
);
return elements;
}
Expand Down Expand Up @@ -132,7 +135,10 @@ export class MastodonFileSubmissionForm extends GenericFileSubmissionSection<Mas
<Select.Option value="private">Followers Only</Select.Option>
<Select.Option value="direct">Mentioned Users Only</Select.Option>
</Select>
</Form.Item>
</Form.Item>,
<Form.Item label="Reply To Post URL">
<Input value={data.replyToUrl} onChange={this.handleValueChange.bind(this, 'replyToUrl')} />
</Form.Item>,
);
return elements;
}
Expand Down