+79
-4
src/lib/at.ts
+79
-4
src/lib/at.ts
···
25
25
if (!didDoc.ok) throw didDoc.data.error;
26
26
return {
27
27
client: rpc,
28
+
agent,
28
29
did: res.data.did,
29
30
handle: res.data.handle,
30
31
pds: didDoc.data.pds,
···
38
39
altText?: string,
39
40
) => {
40
41
const login = await getSessionClient(did);
41
-
const upload = await login.client.post("com.atproto.repo.uploadBlob", {
42
-
input: blob,
42
+
43
+
const serviceAuthUrl = new URL(
44
+
`${login.pds}/xrpc/com.atproto.server.getServiceAuth`,
45
+
);
46
+
serviceAuthUrl.searchParams.append(
47
+
"aud",
48
+
login.pds!.replace("https://", "did:web:"),
49
+
);
50
+
serviceAuthUrl.searchParams.append("lxm", "com.atproto.repo.uploadBlob");
51
+
serviceAuthUrl.searchParams.append(
52
+
"exp",
53
+
(Math.floor(Date.now() / 1000) + 60 * 30).toString(),
54
+
); // 30 minutes
55
+
56
+
const serviceAuthResponse = await login.agent.handle(
57
+
`${serviceAuthUrl.pathname}${serviceAuthUrl.search}`,
58
+
{
59
+
method: "GET",
60
+
},
61
+
);
62
+
63
+
if (!serviceAuthResponse.ok) {
64
+
const error = await serviceAuthResponse.text();
65
+
throw `failed to get service auth: ${error}`;
66
+
}
67
+
68
+
const serviceAuth = await serviceAuthResponse.json();
69
+
const token = serviceAuth.token;
70
+
71
+
const uploadUrl = new URL(
72
+
"https://video.bsky.app/xrpc/app.bsky.video.uploadVideo",
73
+
);
74
+
uploadUrl.searchParams.append("did", did);
75
+
uploadUrl.searchParams.append("name", "video.mp4");
76
+
77
+
const uploadResponse = await fetch(uploadUrl.toString(), {
78
+
method: "POST",
79
+
headers: {
80
+
Authorization: `Bearer ${token}`,
81
+
"Content-Type": "video/mp4",
82
+
},
83
+
body: blob,
43
84
});
44
-
if (!upload.ok) throw `failed to upload blob: ${upload.data.error}`;
85
+
86
+
if (!uploadResponse.ok) {
87
+
const error = await uploadResponse.text();
88
+
throw `failed to upload video: ${error}`;
89
+
}
90
+
91
+
const jobStatus = await uploadResponse.json();
92
+
let videoBlobRef = jobStatus.blob;
93
+
94
+
while (!videoBlobRef) {
95
+
await new Promise((resolve) => setTimeout(resolve, 1000));
96
+
97
+
const statusResponse = await fetch(
98
+
`https://video.bsky.app/xrpc/app.bsky.video.getJobStatus?jobId=${jobStatus.jobId}`,
99
+
);
100
+
101
+
if (!statusResponse.ok) {
102
+
const error = await statusResponse.json();
103
+
if (error.error === "already_exists" && error.blob) {
104
+
videoBlobRef = error.blob;
105
+
break;
106
+
}
107
+
throw `failed to get job status: ${error.message || error.error}`;
108
+
}
109
+
110
+
const status = await statusResponse.json();
111
+
if (status.jobStatus.blob) {
112
+
videoBlobRef = status.jobStatus.blob;
113
+
} else if (status.jobStatus.state === "JOB_STATE_FAILED") {
114
+
throw `video processing failed: ${status.jobStatus.error || "unknown error"}`;
115
+
}
116
+
}
117
+
45
118
const record: AppBskyFeedPost.Main = {
46
119
$type: "app.bsky.feed.post",
47
120
text: postContent,
48
121
embed: {
49
122
$type: "app.bsky.embed.video",
50
-
video: upload.data.blob,
123
+
video: videoBlobRef,
51
124
alt: altText,
52
125
},
53
126
createdAt: new Date().toISOString(),
54
127
};
128
+
55
129
const result = await login.client.post("com.atproto.repo.createRecord", {
56
130
input: {
57
131
collection: "app.bsky.feed.post",
···
59
133
repo: did,
60
134
},
61
135
});
136
+
62
137
if (!result.ok) throw `failed to upload post: ${result.data.error}`;
63
138
return result.data;
64
139
};
+1
-1
src/lib/oauthMetadata.json
+1
-1
src/lib/oauthMetadata.json
···
4
4
"client_uri": "http://localhost:3000",
5
5
"logo_uri": "http://localhost:3000/favicon.png",
6
6
"redirect_uris": ["http://127.0.0.1:3000/"],
7
-
"scope": "atproto repo:app.bsky.feed.post?action=create blob:video/*",
7
+
"scope": "atproto repo:app.bsky.feed.post?action=create rpc:com.atproto.repo.uploadBlob?aud=* blob:video/*",
8
8
"grant_types": ["authorization_code", "refresh_token"],
9
9
"response_types": ["code"],
10
10
"token_endpoint_auth_method": "none",