15
15
*/
16
16
package io .micrometer .release .single ;
17
17
18
+ import com .fasterxml .jackson .databind .JsonNode ;
19
+ import com .fasterxml .jackson .databind .ObjectMapper ;
18
20
import io .micrometer .release .common .Input ;
19
21
import org .slf4j .Logger ;
20
22
import org .slf4j .LoggerFactory ;
25
27
import java .net .http .HttpRequest ;
26
28
import java .net .http .HttpResponse ;
27
29
import java .net .http .HttpResponse .BodyHandlers ;
30
+ import java .time .ZonedDateTime ;
28
31
import java .time .format .DateTimeFormatter ;
29
32
import java .util .List ;
30
33
@@ -44,7 +47,7 @@ void sendNotifications(String repoName, String refName, MilestoneWithDeadline ne
44
47
45
48
// for tests
46
49
BlueSkyNotifier blueSky () {
47
- return new BlueSkyNotifier ();
50
+ return new BlueSkyNotifier (new ObjectMapper () );
48
51
}
49
52
50
53
// for tests
@@ -106,8 +109,8 @@ private void notifyGoogleChat(String payload) {
106
109
.header ("Content-Type" , "application/json" )
107
110
.POST (HttpRequest .BodyPublishers .ofString (payload ))
108
111
.build ();
109
- try {
110
- HttpResponse <String > send = HttpClient . newHttpClient () .send (chatRequest , BodyHandlers .ofString ());
112
+ try ( HttpClient httpClient = HttpClient . newHttpClient ()) {
113
+ HttpResponse <String > send = httpClient .send (chatRequest , BodyHandlers .ofString ());
111
114
if (send .statusCode () >= 400 ) {
112
115
throw new IllegalStateException ("Unexpected response code: " + send .statusCode ());
113
116
}
@@ -121,19 +124,25 @@ private void notifyGoogleChat(String payload) {
121
124
122
125
static class BlueSkyNotifier implements Notifier {
123
126
127
+ private static final Logger log = LoggerFactory .getLogger (BlueSkyNotifier .class );
128
+
129
+ private final ObjectMapper objectMapper ;
130
+
124
131
private final String uriRoot ;
125
132
126
133
private final String identifier ;
127
134
128
135
private final String password ;
129
136
130
- BlueSkyNotifier (String uriRoot , String identifier , String password ) {
137
+ BlueSkyNotifier (ObjectMapper objectMapper , String uriRoot , String identifier , String password ) {
138
+ this .objectMapper = objectMapper ;
131
139
this .uriRoot = uriRoot ;
132
140
this .identifier = identifier ;
133
141
this .password = password ;
134
142
}
135
143
136
- BlueSkyNotifier () {
144
+ BlueSkyNotifier (ObjectMapper objectMapper ) {
145
+ this .objectMapper = objectMapper ;
137
146
this .uriRoot = "https://bsky.social" ;
138
147
this .identifier = Input .getBlueSkyHandle ();
139
148
this .password = Input .getBlueSkyPassword ();
@@ -146,26 +155,90 @@ public void sendNotification(String repoName, String refName, MilestoneWithDeadl
146
155
return ;
147
156
}
148
157
149
- HttpRequest blueskyRequest = HttpRequest .newBuilder ()
158
+ String token = getToken ();
159
+
160
+ createPost (token , createPostJson (repoName , refName ));
161
+ }
162
+
163
+ private String getToken () {
164
+ HttpRequest createSessionRequest = HttpRequest .newBuilder ()
150
165
.uri (URI .create (uriRoot + "/xrpc/com.atproto.server.createSession" ))
151
166
.header ("Content-Type" , "application/json" )
152
167
.POST (HttpRequest .BodyPublishers
153
168
.ofString ("{\" identifier\" :\" " + identifier + "\" ,\" password\" :\" " + password + "\" }" ))
154
169
.build ();
155
170
156
- try {
157
- HttpResponse <String > blueskyResponse = HttpClient .newHttpClient ()
158
- .send (blueskyRequest , HttpResponse .BodyHandlers .ofString ());
159
- log .info ("Bluesky response: " + blueskyResponse .body ());
160
- if (blueskyResponse .statusCode () >= 400 ) {
161
- throw new IllegalStateException ("Unexpected response code: " + blueskyResponse .statusCode ());
171
+ try (HttpClient httpClient = HttpClient .newHttpClient ()) {
172
+ HttpResponse <String > createSessionResponse = httpClient .send (createSessionRequest ,
173
+ HttpResponse .BodyHandlers .ofString ());
174
+ if (createSessionResponse .statusCode () >= 400 ) {
175
+ throw new IllegalStateException ("Unexpected response code: " + createSessionResponse .statusCode ());
176
+ }
177
+ JsonNode jsonNode = objectMapper .readTree (createSessionResponse .body ());
178
+ if (!jsonNode .has ("accessJwt" )) {
179
+ throw new IllegalStateException ("Missing JWT in response" );
162
180
}
181
+ return jsonNode .get ("accessJwt" ).asText ();
163
182
}
164
183
catch (IOException | InterruptedException e ) {
165
184
throw new RuntimeException (e );
166
185
}
167
186
}
168
187
188
+ private void createPost (String token , String postJson ) {
189
+ String requestBody = """
190
+ {
191
+ "repo":"%s",
192
+ "collection":"app.bsky.feed.post",
193
+ "record":%s
194
+ }""" .formatted (identifier , postJson );
195
+ HttpRequest createRecordRequest = HttpRequest .newBuilder ()
196
+ .uri (URI .create (uriRoot + "/xrpc/com.atproto.repo.createRecord" ))
197
+ .header ("Content-Type" , "application/json" )
198
+ .header ("Authorization" , "Bearer " + token )
199
+ .POST (HttpRequest .BodyPublishers .ofString (requestBody ))
200
+ .build ();
201
+
202
+ try (HttpClient httpClient = HttpClient .newHttpClient ()) {
203
+ HttpResponse <String > createRecordResponse = httpClient .send (createRecordRequest ,
204
+ BodyHandlers .ofString ());
205
+ if (createRecordResponse .statusCode () >= 400 ) {
206
+ log .error ("Unexpected response code: {}\n Response: {}\n Request: {}" ,
207
+ createRecordResponse .statusCode (), createRecordResponse .body (), requestBody );
208
+ throw new IllegalStateException ("Unexpected response code: " + createRecordResponse .statusCode ());
209
+ }
210
+ else {
211
+ log .debug ("Created record: Request: {}, Response: {}" , requestBody , createRecordResponse .body ());
212
+ }
213
+ String postRevision = objectMapper .readTree (createRecordResponse .body ()).at ("/commit/rev" ).asText ();
214
+ log .info ("Bluesky post created: https://bsky.app/profile/{}/post/{}" , identifier , postRevision );
215
+ }
216
+ catch (IOException | InterruptedException e ) {
217
+ throw new RuntimeException (e );
218
+ }
219
+ }
220
+
221
+ private String createPostJson (String projectName , String versionRef ) {
222
+ String version = versionRef .startsWith ("v" ) ? versionRef .substring (1 ) : versionRef ;
223
+ String postText = "%s %s has been released!\\ n\\ nCheck out the changelog at https://github.com/%s/releases/tag/%s"
224
+ .formatted (projectName , version , projectName , versionRef );
225
+ String facetsJson = createFacetsJson (postText );
226
+ return """
227
+ {
228
+ "$type": "app.bsky.feed.post",
229
+ "text": "%s",
230
+ "createdAt": "%s",
231
+ "facets": [
232
+ %s
233
+ ]
234
+ }""" .formatted (postText , ZonedDateTime .now ().format (DateTimeFormatter .ISO_INSTANT ), facetsJson );
235
+ }
236
+
237
+ private String createFacetsJson (String postText ) {
238
+ // TODO this is needed for the URL in the post to be a hyperlink
239
+ return "" ;
240
+ }
241
+
169
242
}
170
243
171
244
}
0 commit comments