· 3 months ago · Jun 12, 2025, 02:20 PM
1<?php
2//Text to Bluesky (with link card) sample 2025-06
3//MH+ presents, visit my blog => https://sl-memo.blogspot.com/
4
5date_default_timezone_set('Asia/Tokyo');
6$UAstring = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:139.0) Gecko/20100101 Firefox/139.0";
7
8//入力データ
9$readfile = "text_data.txt";
10//$readdata = explode("\n", file_get_contents($readfile));
11//テスト用なので固定の配列で記述
12$readdata = array(
13 '001<->IMG - 3<->https://www.flickr.com/photos/153589246@N07/37182374834/<->https://live.staticflickr.com/4459/37182374834_a89ca42e10_c.jpg'
14,'002<->Baby Anythings are Cute<->https://www.flickr.com/photos/dennissylvesterhurd/52852294515/<->https://live.staticflickr.com/65535/52852294515_b00fb1e17d_c.jpg'
15,'003<->Labradoodle<->https://www.flickr.com/photos/195591092@N07/52064576115/<->https://live.staticflickr.com/65535/52064576115_636e0487bf_c.jpg'
16,'004<->Day with Monty 1<->https://www.flickr.com/photos/ianlivesey/49847245082/<->https://live.staticflickr.com/65535/49847245082_41ca62b972_c.jpg'
17);
18
19
20$counter = 0;
21
22//Bluesky接続 (Json Web Token (JWT) 取得)
23$handle = "**********************"; //あなたのBluesky ID (username.bsky.social など)
24$password = "**********************"; //あなたのBluesky パスワード
25
26$jwt = NULL;
27$connect_FLG = false;
28$logfile = "text_test.log";
29$log_FLG = false;
30
31//Main処理
32//各行毎で処理する
33foreach($readdata as $lines) {
34 $items = explode("<->", $lines);// 0=番号, 1=タイトル, 2=URL, 3=imageURL
35 //画像URLの無いものは破棄
36 if(!(isset($items[3]))) continue;
37
38 if(!$connect_FLG){
39 $connect_FLG = true;
40 // Json Web Token (JWT) 取得
41 $jwt = login($handle, $password);
42 }
43
44 //-----------------------------------------------------------------------------------------------
45 //Blusky 投稿
46 $title= (string)$items[1];
47 $link = (string)$items[2];
48 $image= (string)$items[3];
49 $text = $title . " #Flickr #PublicDomain \n" . $link;
50
51 $facets = [];
52 //この辺の facets でのタグ付けやLink付けを丸投げでやってくれるライブラリーもあるらしい
53 //Linkを付ける
54 $linkStart = strpos($text, $link);
55 $linkEnd = $linkStart + strlen($link);
56 $facets[] = [
57 'index' => [
58 'byteStart' => $linkStart,
59 'byteEnd' => $linkEnd
60 ],
61 'features' => [
62 [
63 '$type' => 'app.bsky.richtext.facet#link',
64 'uri' => $link
65 ]
66 ]
67 ];
68 //HashTagを付ける
69 $tagstring = "#Flickr";// # を含む
70 $linkStart = strpos($text, $tagstring);
71 $linkEnd = $linkStart + strlen($tagstring);
72 $facets[] = [
73 'index' => [
74 'byteStart' => $linkStart,
75 'byteEnd' => $linkEnd
76 ],
77 'features' => [
78 [
79 '$type' => 'app.bsky.richtext.facet#tag',
80 'tag' => "Flickr" // # を含まない
81 ]
82 ]
83 ];
84 $tagstring = "#PublicDomain";// # を含む
85 $linkStart = strpos($text, $tagstring);
86 $linkEnd = $linkStart + strlen($tagstring);
87 $facets[] = [
88 'index' => [
89 'byteStart' => $linkStart,
90 'byteEnd' => $linkEnd
91 ],
92 'features' => [
93 [
94 '$type' => 'app.bsky.richtext.facet#tag',
95 'tag' => "PublicDomain" // # を含まない
96 ]
97 ]
98 ];
99 $record = [
100 '$type' => "app.bsky.feed.post",
101 'text' => $text,
102 'createdAt' => (new DateTime())->format("c"),
103 'facets' => $facets,
104 ];
105
106 //画像データ取得とUpload
107 $imageUri = uploadImage($jwt, $image);
108
109 $record['embed'] = [
110 '$type' => 'app.bsky.embed.external',
111 'external'=> [
112 'uri' => $link, //リンクカードのURL
113 'title' => $title,
114 'description' => 'Blog記事用のネタ投稿です。descriptionは省略可。titleは省略するとurlになるよ',
115 'thumb' => $imageUri,
116 ],
117 ];
118
119 $response = post_w_link($handle, $jwt, $record);
120
121 file_put_contents($logfile,print_r($response,true),$log_FLG);
122 if(!$log_FLG) $log_FLG = FILE_APPEND;
123
124 if(isset($response['validationStatus'])){
125 if($response['validationStatus'] == 'valid'){
126 //投稿成功
127 $counter++;
128 }
129 }
130 //-----------------------------------------------------------------------------------------------
131
132 usleep(100000 * 3);//0.1秒 x N停止 (sleep 1秒 = usleep 1000000)
133}
134
135
136echo "total : " . (string)$counter ." items post\n";
137
138
139
140/////////////////////////////////////////////////////////////////
141
142function login($handle, $password)
143{
144 $ch = curl_init("https://bsky.social/xrpc/com.atproto.server.createSession");
145 curl_setopt_array($ch, [
146 CURLOPT_CONNECTTIMEOUT => 10,
147 CURLOPT_SSL_VERIFYPEER => false,
148 CURLOPT_RETURNTRANSFER => true,
149 CURLOPT_FOLLOWLOCATION => true,
150 CURLOPT_MAXREDIRS => 1000,
151 CURLOPT_COOKIEJAR => dirname(__FILE__) . '/cookie_bsky.txt',
152 CURLOPT_COOKIEFILE => dirname(__FILE__) . '/cookie_bsky.txt',
153 CURLOPT_POST => true,
154 CURLOPT_USERAGENT => $GLOBALS['UAstring'],
155 CURLOPT_HTTPHEADER => [
156 "Content-Type: application/json",
157 ],
158 CURLOPT_POSTFIELDS => json_encode([
159 "identifier" => $handle,
160 "password" => $password,
161 ]),
162 ]);
163 $response = curl_exec($ch);
164 if(curl_error($ch)){
165 echo "login error : " . curl_error($ch) . "\n";
166 die();
167 }
168 curl_close($ch);
169 $responseJson = json_decode($response, true);
170 if(isset($responseJson["accessJwt"])){
171 return $responseJson["accessJwt"];
172 }else{
173 print_r($responseJson);
174 }
175}
176
177function post_w_link($handle, $jwt, $record)
178{
179 $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.createRecord");
180 curl_setopt_array($ch, [
181 CURLOPT_CONNECTTIMEOUT => 10,
182 CURLOPT_SSL_VERIFYPEER => false,
183 CURLOPT_RETURNTRANSFER => true,
184 CURLOPT_FOLLOWLOCATION => true,
185 CURLOPT_MAXREDIRS => 1000,
186 CURLOPT_COOKIEJAR => dirname(__FILE__) . '/cookie_bsky.txt',
187 CURLOPT_COOKIEFILE => dirname(__FILE__) . '/cookie_bsky.txt',
188 CURLOPT_POST => true,
189 CURLOPT_USERAGENT => $GLOBALS['UAstring'],
190 CURLOPT_HTTPHEADER => [
191 "Content-Type: application/json",
192 "Authorization: Bearer {$jwt}",
193 ],
194 CURLOPT_POSTFIELDS => json_encode([
195 "repo" => $handle,
196 "collection" => "app.bsky.feed.post",
197 "record" => $record,
198 ]),
199 ]);
200 $response = curl_exec($ch);
201 if(curl_error($ch)){
202 echo "post error : " . curl_error($ch) . "\n";
203 die();
204 }
205 curl_close($ch);
206 $responseJson = json_decode($response, true);
207 return $responseJson;
208}
209
210function uploadImage($jwt, $imagePath)
211{
212 $imageData = file_get_contents($imagePath);
213// $mime = mime_content_type($imagePath);
214 //Flickr は形式固定なのでローカル保存しない
215 //他サイト等て混在する場合は一時的にローカル環境に出力してmime_content_typeで判別
216 $mime = 'image/jpeg';
217
218 $ch = curl_init("https://bsky.social/xrpc/com.atproto.repo.uploadBlob");
219 curl_setopt_array($ch, [
220 CURLOPT_CONNECTTIMEOUT => 10,
221 CURLOPT_SSL_VERIFYPEER => false,
222 CURLOPT_RETURNTRANSFER => true,
223 CURLOPT_MAXREDIRS => 1000,
224 CURLOPT_COOKIEJAR => dirname(__FILE__) . '/cookie_bsky.txt',
225 CURLOPT_COOKIEFILE => dirname(__FILE__) . '/cookie_bsky.txt',
226 CURLOPT_POST => true,
227 CURLOPT_USERAGENT => $GLOBALS['UAstring'],
228 CURLOPT_HTTPHEADER => [
229 "Content-Type: $mime",
230 "Authorization: Bearer {$jwt}",
231 ],
232 CURLOPT_POSTFIELDS => $imageData,
233 ]);
234
235 $response = curl_exec($ch);
236 if(curl_error($ch)){
237 echo "upload error : " . curl_error($ch) . "\n";
238 die();
239 }
240 curl_close($ch);
241 $responseJson = json_decode($response, true);
242 return $responseJson['blob'] ?? null; //PHP 7.x 以降要
243 /*
244 3項演算の亜種
245 $x ?? $y; は
246 isset($x) ? $x : $y; と同じ効果です(PHP 7.x以上)
247 */
248}
249?>
250