plugins-evert/url-atproto/plugin.ts
2024-11-23 11:35:50 +02:00

176 lines
3.9 KiB
TypeScript

import {
Plugin,
Configurable,
EventListener,
DependencyLoad,
} from '@squeebot/core/lib/plugin';
import { IMessage } from '@squeebot/core/lib/types';
import { httpGET, sanitizeEscapedText } from '@squeebot/core/lib/common';
interface ATRecordEmbed {
$type: string;
images: [
{
alt: string;
aspectRatio: {
height: number;
width: number;
};
image: {
$type: string;
ref: {
$link: string;
};
mimeType: string;
size: number;
};
}
];
}
interface ATRecord {
$type: string;
createdAt: string;
langs: string[];
text: string;
embed?: ATRecordEmbed;
facets?: unknown[];
}
interface ATEmbed {
$type: string;
images?: [
{
thumb: string;
fullsize: string;
alt: string;
aspectRatio: {
height: number;
width: number;
};
}
];
record?: ATRecord;
}
interface ATThread {
thread: {
$type: 'app.bsky.feed.defs#threadViewPost';
post: {
uri: string;
cid: string;
author: {
did: string;
handle: string;
displayName: string;
avatar: string;
labels: unknown[];
createdAt: string;
};
record: ATRecord;
embed?: ATEmbed;
replyCount: number;
repostCount: number;
likeCount: number;
quoteCount: number;
indexedAt: string;
labels: unknown[];
};
replies: ATThread['thread'][];
};
}
@Configurable({})
class TwitterURLPlugin extends Plugin {
@EventListener('pluginUnload')
public unloadEventHandler(plugin: string | Plugin): void {
if (plugin === this.name || plugin === this) {
this.emit('pluginUnloaded', this);
}
}
async handlePost(
data: ATThread['thread']['post'],
msg: IMessage
): Promise<boolean> {
const keys = [];
const end = sanitizeEscapedText(data.record.text);
keys.push(['field', 'Bluesky', { color: 'cyan', type: 'title' }]);
keys.push([
'field',
data.likeCount.toString(),
{ color: 'red', label: ['♥', 'Likes'], type: 'metric' },
]);
keys.push([
'field',
data.repostCount.toString(),
{ color: 'green', label: ['↱↲', 'Reposts'], type: 'metric' },
]);
keys.push(['bold', '@' + data.author.handle + ':']);
keys.push(['field', end, { type: 'content' }]);
if (data.embed?.images?.length) {
const amount = data.embed.images.length;
keys.push([
'field',
`[${amount} attachment${amount !== 1 ? 's' : ''}]`,
{ color: 'brown' },
]);
}
if (data.embed?.record) {
keys.push(['field', '[quoted post]', { color: 'brown' }]);
}
msg.resolve(keys);
return true;
}
async atprotoGetThread(url: URL, msg: IMessage): Promise<boolean> {
const urlString = url.toString();
const det = urlString.match(
/^(https?):\/\/((?:[\w\d-]+\.)*[\w\d-]+\.\w{2,16})\/(?:profile\/)?@?([\d\w-_.]+)\/(?:post\/)?([\w\d]+[^&#?\s/])/i
);
if (!det) {
return false;
}
const [_, proto, webviewHost, atHost, postId] = det;
if (proto !== 'https' || webviewHost !== 'bsky.app') {
// no other webviews atm
return false;
}
try {
const postInfo = await httpGET(
`https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?depth=1&parentHeight=1&uri=at://${atHost}/app.bsky.feed.post/${postId}`
);
const data = JSON.parse(postInfo) as ATThread;
if (data?.thread) {
return this.handlePost(data.thread.post, msg);
}
} catch (e) {
msg.resolve('Could not fetch Bluesky post, it may be private.');
}
return false;
}
@DependencyLoad('urlreply')
addURLHandler(urlreply: any): void {
urlreply.registerHandler(
this.name,
'bsky.app/',
(url: URL, msg: IMessage) => {
return this.atprotoGetThread(url, msg);
}
);
}
}
module.exports = TwitterURLPlugin;