1515 */
1616package io .micrometer .release .single ;
1717
18+ import com .fasterxml .jackson .databind .JsonNode ;
19+ import com .fasterxml .jackson .databind .ObjectMapper ;
1820import io .micrometer .release .common .Input ;
1921import org .slf4j .Logger ;
2022import org .slf4j .LoggerFactory ;
2527import java .net .http .HttpRequest ;
2628import java .net .http .HttpResponse ;
2729import java .net .http .HttpResponse .BodyHandlers ;
30+ import java .time .ZonedDateTime ;
2831import java .time .format .DateTimeFormatter ;
2932import java .util .List ;
3033
@@ -44,7 +47,7 @@ void sendNotifications(String repoName, String refName, MilestoneWithDeadline ne
4447
4548 // for tests
4649 BlueSkyNotifier blueSky () {
47- return new BlueSkyNotifier ();
50+ return new BlueSkyNotifier (new ObjectMapper () );
4851 }
4952
5053 // for tests
@@ -106,8 +109,8 @@ private void notifyGoogleChat(String payload) {
106109 .header ("Content-Type" , "application/json" )
107110 .POST (HttpRequest .BodyPublishers .ofString (payload ))
108111 .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 ());
111114 if (send .statusCode () >= 400 ) {
112115 throw new IllegalStateException ("Unexpected response code: " + send .statusCode ());
113116 }
@@ -121,19 +124,25 @@ private void notifyGoogleChat(String payload) {
121124
122125 static class BlueSkyNotifier implements Notifier {
123126
127+ private static final Logger log = LoggerFactory .getLogger (BlueSkyNotifier .class );
128+
129+ private final ObjectMapper objectMapper ;
130+
124131 private final String uriRoot ;
125132
126133 private final String identifier ;
127134
128135 private final String password ;
129136
130- BlueSkyNotifier (String uriRoot , String identifier , String password ) {
137+ BlueSkyNotifier (ObjectMapper objectMapper , String uriRoot , String identifier , String password ) {
138+ this .objectMapper = objectMapper ;
131139 this .uriRoot = uriRoot ;
132140 this .identifier = identifier ;
133141 this .password = password ;
134142 }
135143
136- BlueSkyNotifier () {
144+ BlueSkyNotifier (ObjectMapper objectMapper ) {
145+ this .objectMapper = objectMapper ;
137146 this .uriRoot = "https://bsky.social" ;
138147 this .identifier = Input .getBlueSkyHandle ();
139148 this .password = Input .getBlueSkyPassword ();
@@ -146,26 +155,90 @@ public void sendNotification(String repoName, String refName, MilestoneWithDeadl
146155 return ;
147156 }
148157
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 ()
150165 .uri (URI .create (uriRoot + "/xrpc/com.atproto.server.createSession" ))
151166 .header ("Content-Type" , "application/json" )
152167 .POST (HttpRequest .BodyPublishers
153168 .ofString ("{\" identifier\" :\" " + identifier + "\" ,\" password\" :\" " + password + "\" }" ))
154169 .build ();
155170
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" );
162180 }
181+ return jsonNode .get ("accessJwt" ).asText ();
163182 }
164183 catch (IOException | InterruptedException e ) {
165184 throw new RuntimeException (e );
166185 }
167186 }
168187
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+
169242 }
170243
171244}
0 commit comments