[Chat] Add OpenGraph card for chat invite links (#10629)
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import assert from 'node:assert'
|
||||
|
||||
import {type ChatBskyGroupDefs} from '@atproto/api'
|
||||
import resvg from '@resvg/resvg-js'
|
||||
import {type Express} from 'express'
|
||||
import satori from 'satori'
|
||||
|
||||
import {
|
||||
CHAT_INVITE_HEIGHT,
|
||||
CHAT_INVITE_WIDTH,
|
||||
ChatInvite,
|
||||
} from '../components/ChatInvite.js'
|
||||
import {type AppContext} from '../context.js'
|
||||
import {httpLogger} from '../logger.js'
|
||||
import {loadEmojiAsSvg} from '../util.js'
|
||||
import {
|
||||
getImage,
|
||||
handler,
|
||||
hideAvatarLabels,
|
||||
originVerifyMiddleware,
|
||||
} from './util.js'
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
return app.get(
|
||||
'/chat-invite/:code',
|
||||
originVerifyMiddleware(ctx),
|
||||
handler(async (req, res) => {
|
||||
const {code} = req.params
|
||||
let preview: ChatBskyGroupDefs.JoinLinkPreviewView
|
||||
try {
|
||||
const result = await ctx.chatAgent.chat.bsky.group.getJoinLinkPreviews({
|
||||
codes: [code],
|
||||
})
|
||||
const found = result.data.joinLinkPreviews[0]
|
||||
if (!found) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
preview = found
|
||||
} catch (err) {
|
||||
httpLogger.warn({err, code}, 'could not fetch chat invite preview')
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
|
||||
// Load the owner's avatar (if any, and not labeled).
|
||||
let ownerImage: Buffer | null = null
|
||||
const owner = preview.owner
|
||||
const ownerHasSafeAvatar =
|
||||
!!owner.avatar && !owner.labels?.some(l => hideAvatarLabels.has(l.val))
|
||||
if (ownerHasSafeAvatar) {
|
||||
try {
|
||||
assert(owner.avatar)
|
||||
ownerImage = await getImage(owner.avatar)
|
||||
} catch (err) {
|
||||
httpLogger.warn(
|
||||
{err, code, did: owner.did},
|
||||
'could not fetch owner image',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const svg = await satori(
|
||||
<ChatInvite preview={preview} ownerImage={ownerImage} />,
|
||||
{
|
||||
fonts: ctx.fonts,
|
||||
height: CHAT_INVITE_HEIGHT,
|
||||
width: CHAT_INVITE_WIDTH,
|
||||
loadAdditionalAsset: async (lang, text) => {
|
||||
if (lang === 'emoji') {
|
||||
return (await loadEmojiAsSvg(text)) ?? ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
},
|
||||
)
|
||||
const output = await resvg.renderAsync(svg)
|
||||
res.statusCode = 200
|
||||
res.setHeader('content-type', 'image/png')
|
||||
res.setHeader('cdn-tag', owner.did)
|
||||
return res.end(output.asPng())
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import {type Express} from 'express'
|
||||
|
||||
import {type AppContext} from '../context.js'
|
||||
import {default as avatarBubbles} from './avatar-bubbles.js'
|
||||
import {default as chatInvite} from './chat-invite.js'
|
||||
import {default as health} from './health.js'
|
||||
import {default as starterPack} from './starter-pack.js'
|
||||
|
||||
@@ -10,6 +11,7 @@ export * from './util.js'
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
app = health(ctx, app) // GET /_health
|
||||
app = starterPack(ctx, app) // GET /start/:actor/:rkey
|
||||
app = chatInvite(ctx, app) // GET /chat-invite/:code
|
||||
app = avatarBubbles(ctx, app) // GET /avatar-bubbles?dids=...
|
||||
return app
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
hideAvatarLabels,
|
||||
originVerifyMiddleware,
|
||||
} from './util.js'
|
||||
import * as bsky from '../types/bsky/index.js'
|
||||
|
||||
export default function (ctx: AppContext, app: Express) {
|
||||
return app.get(
|
||||
@@ -29,9 +30,9 @@ export default function (ctx: AppContext, app: Express) {
|
||||
const uri = AtUri.make(actor, 'app.bsky.graph.starterpack', rkey)
|
||||
let starterPack: AppBskyGraphDefs.StarterPackView
|
||||
try {
|
||||
const result = await ctx.appviewAgent.api.app.bsky.graph.getStarterPack(
|
||||
{starterPack: uri.toString()},
|
||||
)
|
||||
const result = await ctx.appviewAgent.app.bsky.graph.getStarterPack({
|
||||
starterPack: uri.toString(),
|
||||
})
|
||||
starterPack = result.data.starterPack
|
||||
} catch (err) {
|
||||
httpLogger.warn(
|
||||
@@ -41,7 +42,7 @@ export default function (ctx: AppContext, app: Express) {
|
||||
return res.status(404).end('not found')
|
||||
}
|
||||
const imageEntries = await Promise.all(
|
||||
[starterPack.creator]
|
||||
([starterPack.creator] as Array<bsky.profile.AnyProfileView>)
|
||||
.concat((starterPack.listItemsSample ?? []).map(li => li.subject))
|
||||
// has avatar
|
||||
.filter(p => p.avatar)
|
||||
|
||||
Reference in New Issue
Block a user