• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jreleaser / jreleaser / #544

28 Oct 2025 03:56PM UTC coverage: 48.245% (-0.03%) from 48.273%
#544

push

github

web-flow
feat(announce): Add Reddit announcer support

Fixes #1457

188 of 414 new or added lines in 16 files covered. (45.41%)

6 existing lines in 2 files now uncovered.

26009 of 53910 relevant lines covered (48.25%)

0.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.04
/sdks/jreleaser-reddit-java-sdk/src/main/java/org/jreleaser/sdk/reddit/RedditSdk.java
1
/*
2
 * SPDX-License-Identifier: Apache-2.0
3
 *
4
 * Copyright 2020-2025 The JReleaser authors.
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     https://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
package org.jreleaser.sdk.reddit;
19

20
import com.fasterxml.jackson.annotation.JsonInclude;
21
import com.fasterxml.jackson.databind.DeserializationFeature;
22
import com.fasterxml.jackson.databind.ObjectMapper;
23
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
24
import com.fasterxml.jackson.databind.SerializationFeature;
25
import feign.auth.BasicAuthRequestInterceptor;
26
import feign.form.FormEncoder;
27
import feign.jackson.JacksonDecoder;
28
import feign.jackson.JacksonEncoder;
29
import org.jreleaser.bundle.RB;
30
import org.jreleaser.model.api.JReleaserContext;
31
import org.jreleaser.sdk.commons.ClientUtils;
32
import org.jreleaser.sdk.commons.RestAPIException;
33
import org.jreleaser.sdk.reddit.api.AccessTokenResponse;
34
import org.jreleaser.sdk.reddit.api.RedditAPI;
35
import org.jreleaser.sdk.reddit.api.SubmissionRequest;
36
import org.jreleaser.sdk.reddit.api.SubmissionResponse;
37

38
import static java.util.Objects.requireNonNull;
39
import static org.jreleaser.util.StringUtils.isBlank;
40
import static org.jreleaser.util.StringUtils.requireNonBlank;
41

42
/**
43
 * @author Usman Shaikh
44
 * @since 1.21.0
45
 */
46
public class RedditSdk {
47
    private static final String REDDIT_BASE_URL = "https://www.reddit.com";
48
    private static final String REDDIT_OAUTH_URL = "https://oauth.reddit.com";
49
    
50
    private final JReleaserContext context;
51
    private final RedditAPI authApi;
52
    private final RedditAPI oauthApi;
53
    private final String clientId;
54
    private final String clientSecret;
55
    private final String username;
56
    private final String password;
57
    private final String userAgent;
58
    private final boolean dryrun;
59
    
60
    private String accessToken;
61

62
    private RedditSdk(JReleaserContext context,
63
                      String clientId,
64
                      String clientSecret,
65
                      String username,
66
                      String password,
67
                      int connectTimeout,
68
                      int readTimeout,
69
                      boolean dryrun,
70
                      String baseUrl,
71
                      String oauthUrl) {
1✔
72
        requireNonNull(context, "'context' must not be null");
1✔
73
        requireNonBlank(clientId, "'clientId' must not be blank");
1✔
74
        requireNonBlank(clientSecret, "'clientSecret' must not be blank");
1✔
75
        requireNonBlank(username, "'username' must not be blank");
1✔
76
        requireNonBlank(password, "'password' must not be blank");
1✔
77

78
        ObjectMapper objectMapper = new ObjectMapper()
1✔
79
            .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
1✔
80
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
1✔
81
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
1✔
82
            .configure(SerializationFeature.INDENT_OUTPUT, true);
1✔
83

84
        this.context = context;
1✔
85
        this.clientId = clientId;
1✔
86
        this.clientSecret = clientSecret;
1✔
87
        this.username = username;
1✔
88
        this.password = password;
1✔
89
        this.dryrun = dryrun;
1✔
90
        this.userAgent = String.format("JReleaser/%s by /u/%s", 
1✔
91
            context.getModel().getProject().getVersion(), username);
1✔
92

93
        // API for authentication
94
        this.authApi = ClientUtils.builder(context, connectTimeout, readTimeout)
1✔
95
            .encoder(new FormEncoder(new JacksonEncoder(objectMapper)))
1✔
96
            .decoder(new JacksonDecoder(objectMapper))
1✔
97
            .requestInterceptor(new BasicAuthRequestInterceptor(clientId, clientSecret))
1✔
98
            .requestInterceptor(template -> {
1✔
99
                template.header("User-Agent", userAgent);
1✔
100
                template.body("grant_type=password&username=" + username + "&password=" + password);
1✔
101
            })
1✔
102
            .target(RedditAPI.class, baseUrl);
1✔
103

104
        // API for authenticated requests
105
        this.oauthApi = ClientUtils.builder(context, connectTimeout, readTimeout)
1✔
106
            .encoder(new FormEncoder())
1✔
107
            .decoder(new JacksonDecoder(objectMapper))
1✔
108
            .requestInterceptor(template -> template.header("User-Agent", userAgent))
1✔
109
            .target(RedditAPI.class, oauthUrl);
1✔
110

111
        this.context.getLogger().debug(RB.$("workflow.dryrun"), dryrun);
1✔
112
    }
1✔
113

114
    public void submitTextPost(String subreddit, String title, String text) throws RedditSdkException {
115
        ensureAuthenticated();
1✔
116
        wrap(() -> {
1✔
117
            SubmissionRequest request = SubmissionRequest.forTextPost(subreddit, title, text);
1✔
118
            SubmissionResponse response = oauthApi.submit(accessToken, request);
1✔
119
            if (response.hasErrors()) {
1✔
NEW
120
                throw new RedditSdkException(RB.$("sdk.api.errors", "Reddit", response.getErrors()));
×
121
            }
122
        });
1✔
123
    }
1✔
124

125
    public void submitLinkPost(String subreddit, String title, String url) throws RedditSdkException {
126
        ensureAuthenticated();
1✔
127
        wrap(() -> {
1✔
128
            SubmissionRequest request = SubmissionRequest.forLinkPost(subreddit, title, url);
1✔
129
            SubmissionResponse response = oauthApi.submit(accessToken, request);
1✔
130
            if (response.hasErrors()) {
1✔
NEW
131
                throw new RedditSdkException(RB.$("sdk.api.errors", "Reddit", response.getErrors()));
×
132
            }
133
        });
1✔
134
    }
1✔
135

136
    private void ensureAuthenticated() throws RedditSdkException {
137
        if (accessToken == null) {
1✔
138
            authenticate();
1✔
139
        }
140
    }
1✔
141

142
    private void authenticate() throws RedditSdkException {
143
        wrap(() -> {
1✔
144
            AccessTokenResponse response = authApi.getAccessToken();
1✔
145
            this.accessToken = response.getAccessToken();
1✔
146
        });
1✔
147
    }
1✔
148

149
    private void wrap(Runnable runnable) throws RedditSdkException {
150
        try {
151
            if (!dryrun) {
1✔
152
                runnable.run();
1✔
153
            }
154
        } catch (RestAPIException e) {
1✔
155
            context.getLogger().trace(e);
1✔
156
            throw new RedditSdkException(RB.$("sdk.operation.failed", "Reddit"), e);
1✔
157
        }
1✔
158
    }
1✔
159

160
    public static Builder builder(JReleaserContext context) {
161
        return new Builder(context);
1✔
162
    }
163

164
    public static class Builder {
165
        private final JReleaserContext context;
166
        private String clientId;
167
        private String clientSecret;
168
        private String username;
169
        private String password;
170
        private int connectTimeout = 20;
1✔
171
        private int readTimeout = 60;
1✔
172
        private boolean dryrun;
173
        private String baseUrl;
174
        private String oauthUrl;
175

176
        private Builder(JReleaserContext context) {
1✔
177
            this.context = requireNonNull(context, "'context' must not be null");
1✔
178
        }
1✔
179

180
        public Builder clientId(String clientId) {
181
            this.clientId = requireNonBlank(clientId, "'clientId' must not be blank");
1✔
182
            return this;
1✔
183
        }
184

185
        public Builder clientSecret(String clientSecret) {
186
            this.clientSecret = requireNonBlank(clientSecret, "'clientSecret' must not be blank");
1✔
187
            return this;
1✔
188
        }
189

190
        public Builder username(String username) {
191
            this.username = requireNonBlank(username, "'username' must not be blank");
1✔
192
            return this;
1✔
193
        }
194

195
        public Builder password(String password) {
196
            this.password = requireNonBlank(password, "'password' must not be blank");
1✔
197
            return this;
1✔
198
        }
199

200
        public Builder connectTimeout(int connectTimeout) {
201
            this.connectTimeout = connectTimeout;
1✔
202
            return this;
1✔
203
        }
204

205
        public Builder readTimeout(int readTimeout) {
206
            this.readTimeout = readTimeout;
1✔
207
            return this;
1✔
208
        }
209

210
        public Builder dryrun(boolean dryrun) {
211
            this.dryrun = dryrun;
1✔
212
            return this;
1✔
213
        }
214

215
        public Builder baseUrl(String baseUrl) {
216
            this.baseUrl = baseUrl;
1✔
217
            return this;
1✔
218
        }
219

220
        public Builder oauthUrl(String oauthUrl) {
221
            this.oauthUrl = oauthUrl;
1✔
222
            return this;
1✔
223
        }
224

225
        private void validate() {
226
            requireNonBlank(clientId, "'clientId' must not be blank");
1✔
227
            requireNonBlank(clientSecret, "'clientSecret' must not be blank");
1✔
228
            requireNonBlank(username, "'username' must not be blank");
1✔
229
            requireNonBlank(password, "'password' must not be blank");
1✔
230
            
231
            if (isBlank(baseUrl)) {
1✔
NEW
232
                this.baseUrl = REDDIT_BASE_URL;
×
233
            }
234
            if (isBlank(oauthUrl)) {
1✔
NEW
235
                this.oauthUrl = REDDIT_OAUTH_URL;
×
236
            }
237
        }
1✔
238

239
        public RedditSdk build() {
240
            validate();
1✔
241

242
            return new RedditSdk(context, clientId, clientSecret, username, password,
1✔
243
                connectTimeout, readTimeout, dryrun, baseUrl, oauthUrl);
244
        }
245
    }
246
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc