diff --git a/src/lib/image.ts b/src/lib/file.ts similarity index 73% rename from src/lib/image.ts rename to src/lib/file.ts index 80f3d5a..e814954 100644 --- a/src/lib/image.ts +++ b/src/lib/file.ts @@ -1,5 +1,5 @@ /// -import type { AppBskyEmbedImages } from "@atcute/bluesky" +import type { AppBskyEmbedImages, AppBskyEmbedVideo } from "@atcute/bluesky" import { type Did } from "@atcute/lexicons" import { isLegacyBlob } from "@atcute/lexicons/interfaces" import { @@ -66,6 +66,54 @@ export const uploadImages = async ( return imageIds } +interface UploadVideoParams { + accessToken: string + did: Did + video: AppBskyEmbedVideo.Main + targetChannelId: string +} + +export const uploadVideo = async ( + { accessToken, did, video, targetChannelId }: UploadVideoParams, +) => { + if (isLegacyBlob(video.video)) { + throw new Error("Legacy blobs are not supported") + } + + const { data: downloadRes, ok } = await client.get( + "com.atproto.sync.getBlob", + { + as: "blob", + params: { + did, + cid: video.video.ref.$link, + }, + }, + ) + + if (!ok) { + throw new Error( + `Failed to download video: ${video.video.ref.$link}`, + ) + } + + const { data: uploadedFile } = await postFile({ + headers: { + Authorization: `Bearer ${accessToken}`, + }, + body: { + channelId: targetChannelId, + file: downloadRes, + }, + }) + + if (!uploadedFile) { + throw new Error(`Failed to upload video: ${video.video.ref.$link}`) + } + + return uploadedFile.id +} + const isUint8ArrayOfArrayBuffer = ( data: Uint8Array, ): data is Uint8Array => data.buffer instanceof ArrayBuffer diff --git a/src/service/eventQueueWorker.ts b/src/service/eventQueueWorker.ts index 67faa24..efc2809 100644 --- a/src/service/eventQueueWorker.ts +++ b/src/service/eventQueueWorker.ts @@ -1,12 +1,13 @@ import { AppBskyEmbedImages, AppBskyEmbedRecordWithMedia, + AppBskyEmbedVideo, AppBskyFeedPost, } from "@atcute/bluesky" import { is } from "@atcute/lexicons/validations" import { retry } from "@std/async/retry" import { buildAtProtoUri } from "../lib/atProto.ts" -import { uploadImages } from "../lib/image.ts" +import { uploadImages, uploadVideo } from "../lib/file.ts" import { getTraqMessageIdByAtProtoUri, savePostMetadata, @@ -86,30 +87,51 @@ export class EventQueueWorker { console.debug("Attempting to post message to traQ", atProtoUri) let images: AppBskyEmbedImages.Image[] | undefined - let imageIds: string[] | undefined + let video: AppBskyEmbedVideo.Main | undefined + let fileIds: string[] | undefined if (is(AppBskyEmbedImages.mainSchema, record.embed)) { images = record.embed.images + } else if (is(AppBskyEmbedVideo.mainSchema, record.embed)) { + video = record.embed } else if ( is( AppBskyEmbedRecordWithMedia.mainSchema, record.embed, - ) && - is(AppBskyEmbedImages.mainSchema, record.embed.media) + ) ) { - images = record.embed.media.images + if (is(AppBskyEmbedImages.mainSchema, record.embed.media)) { + images = record.embed.media.images + } else if (is(AppBskyEmbedVideo.mainSchema, record.embed.media)) { + video = record.embed.media + } } if (images?.length) { - imageIds = await uploadImages({ + const imageFileIds = await uploadImages({ accessToken, did: event.did, images, targetChannelId: userSetting.targetChannelId, }) + fileIds = imageFileIds + + console.debug( + `Uploaded images for post ${atProtoUri}: ${imageFileIds}`, + ) + } + + if (video) { + const videoFileId = await uploadVideo({ + accessToken, + did: event.did, + video, + targetChannelId: userSetting.targetChannelId, + }) + fileIds = fileIds ? [...fileIds, videoFileId] : [videoFileId] console.debug( - `Uploaded images for post ${atProtoUri}: ${imageIds}`, + `Uploaded video for post ${atProtoUri}: ${videoFileId}`, ) } @@ -126,7 +148,7 @@ export class EventQueueWorker { }, body: { content: await messageBuilder.build({ - imageIds, + fileIds, post: record, }), }, diff --git a/src/service/jestream.ts b/src/service/jestream.ts index 967abf6..ff8c8ef 100644 --- a/src/service/jestream.ts +++ b/src/service/jestream.ts @@ -1,4 +1,4 @@ -import { AppBskyEmbedVideo, AppBskyFeedPost } from "@atcute/bluesky" +import { AppBskyFeedPost } from "@atcute/bluesky" import { JetstreamSubscription } from "@atcute/jetstream" import { type Did, is } from "@atcute/lexicons" import { debounce } from "@std/async" @@ -90,14 +90,13 @@ export class JetstreamService { }) if ( - is(AppBskyEmbedVideo.mainSchema, event.commit.record.embed) || - !(await isSelfThread({ + !await isSelfThread({ post: event.commit.record, authorDid: event.did, - })) + }) ) { console.warn( - `Skipping post ${atProtoUri} because it has video or is not a self thread`, + `Skipping post ${atProtoUri} because it is not a self thread`, ) this.updateCursor(event.time_us) diff --git a/src/service/messageBuilder.ts b/src/service/messageBuilder.ts index dc95787..332978d 100644 --- a/src/service/messageBuilder.ts +++ b/src/service/messageBuilder.ts @@ -19,7 +19,7 @@ interface MessageBuilderConstructorParams { interface BuildMessageParams { post: AppBskyFeedPost.Main - imageIds?: string[] + fileIds?: string[] } const encoder = new TextEncoder() @@ -36,7 +36,7 @@ export class MessageBuilder { this.traqAccessToken = traqAccessToken } - async build({ post, imageIds }: BuildMessageParams) { + async build({ post, fileIds }: BuildMessageParams) { let text = post.text if (post.facets?.length) { @@ -74,13 +74,13 @@ export class MessageBuilder { text = decoder.decode(textBytes) } - if (imageIds?.length) { - const imageLinks = imageIds.map((id) => + if (fileIds?.length) { + const fileLinks = fileIds.map((id: string) => `${config.traqBaseUrl}/files/${id}` ) .join("\n") - text = text ? `${text}\n${imageLinks}` : imageLinks + text = text ? `${text}\n${fileLinks}` : fileLinks } if (post.reply) {