Subversion Repositories SmartDukaan

Rev

Rev 37388 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
37388 vikas 1
package com.spice.profitmandi.service.whatsapp;
2
 
3
import org.json.JSONArray;
4
import org.json.JSONObject;
5
 
6
import java.util.ArrayList;
7
import java.util.List;
8
 
9
/**
10
 * Request builders and response parsers for the BotPenguin gateway.
11
 *
37409 vikas 12
 * <p>Deliberately static and Spring-free: the nesting rules below are the part most likely to be got
13
 * wrong, and keeping them here lets them be tested directly with no context and no mocking.</p>
37388 vikas 14
 */
15
public final class BotPenguinPayloads {
16
 
17
    private BotPenguinPayloads() {
18
    }
19
 
20
    // ------------------------------------------------------------------
21
    // Session messages  (POST /wa/send-message)
22
    // ------------------------------------------------------------------
23
 
24
    public static JSONObject textMessage(String userName, String waId, String body, List<String> tags) {
25
        JSONObject payload = base(userName, waId, "text", tags);
26
        payload.put("message", new JSONObject().put("text", body));
27
        return payload;
28
    }
29
 
30
    /**
31
     * Media session message. The gateway names the URL {@code source} and the caption {@code label} —
32
     * not {@code link}/{@code caption} as the template endpoint does.
33
     */
34
    public static JSONObject mediaMessage(String userName, String waId, String type, String source,
35
                                          String label, List<String> tags) {
36
        JSONObject message = new JSONObject().put("source", source);
37
        if (isPresent(label)) {
38
            message.put("label", label);
39
        }
40
        JSONObject payload = base(userName, waId, type, tags);
41
        payload.put("message", message);
42
        return payload;
43
    }
44
 
45
    private static JSONObject base(String userName, String waId, String type, List<String> tags) {
46
        JSONObject payload = new JSONObject();
47
        payload.put("userName", userName);
48
        payload.put("wa_id", waId);
49
        payload.put("type", type);
50
        putTags(payload, tags);
51
        return payload;
52
    }
53
 
54
    // ------------------------------------------------------------------
55
    // Template messages  (POST /wa/send-template)
56
    // ------------------------------------------------------------------
57
 
58
    public static JSONObject template(String userName, String waId, String templateId, String assignTo,
59
                                      List<String> tags, List<WhatsappTemplateComponent> components,
60
                                      boolean nameBasedParams) {
61
        JSONObject payload = new JSONObject();
62
        payload.put("userName", userName);
63
        payload.put("wa_id", waId);
64
        payload.put("templateId", templateId);
65
        // An unrecognised assignTo is a plausible cause of a rejected send, so send it only when set.
66
        if (isPresent(assignTo)) {
67
            payload.put("assignTo", assignTo);
68
        }
69
        putTags(payload, tags);
70
        payload.put("params", params(components, nameBasedParams));
71
        return payload;
72
    }
73
 
74
    private static JSONArray params(List<WhatsappTemplateComponent> components, boolean nameBasedParams) {
75
        JSONArray params = new JSONArray();
76
        if (components == null) {
77
            return params;
78
        }
79
        for (WhatsappTemplateComponent component : components) {
80
            JSONArray parameters = new JSONArray();
81
            for (WhatsappTemplateParam param : component.getParameters()) {
82
                parameters.put(parameter(param, nameBasedParams));
83
            }
84
            params.put(new JSONObject()
85
                    .put("type", component.getType())
86
                    .put("parameters", parameters));
87
        }
88
        return params;
89
    }
90
 
91
    /**
92
     * A text parameter carries a flat {@code text} key; a media parameter carries a nested object named
93
     * after its own type and no {@code text} key at all.
94
     *
95
     * <p>{@code parameter_name} is emitted only for name-based templates. Sending it to a positional
96
     * template — or omitting it on a name-based one — fails without a useful error, which is why the
97
     * template registry carries that flag rather than leaving it to the caller.</p>
98
     */
99
    private static JSONObject parameter(WhatsappTemplateParam param, boolean nameBasedParams) {
100
        JSONObject json = new JSONObject();
101
        json.put("type", param.getType());
102
        if (param.isText()) {
103
            json.put("text", param.getText());
104
        } else {
105
            JSONObject media = new JSONObject().put("link", param.getLink());
106
            if (isPresent(param.getFileName())) {
107
                media.put("filename", param.getFileName());
108
            }
109
            json.put(param.getType(), media);
110
        }
111
        if (nameBasedParams && isPresent(param.getParameterName())) {
112
            json.put("parameter_name", param.getParameterName());
113
        }
114
        return json;
115
    }
116
 
117
    private static void putTags(JSONObject payload, List<String> tags) {
118
        if (tags == null || tags.isEmpty()) {
119
            return;
120
        }
121
        JSONArray array = new JSONArray();
122
        for (String tag : tags) {
123
            if (isPresent(tag)) {
124
                array.put(tag);
125
            }
126
        }
127
        if (array.length() > 0) {
128
            payload.put("tags", array);
129
        }
130
    }
131
 
132
    // ------------------------------------------------------------------
133
    // Responses
134
    // ------------------------------------------------------------------
135
 
136
    /** Outcome of a send, as read from the gateway body. */
137
    public static final class SendResult {
138
        private final boolean success;
139
        private final String externalId;
140
        private final String status;
141
        private final String error;
142
 
143
        SendResult(boolean success, String externalId, String status, String error) {
144
            this.success = success;
145
            this.externalId = externalId;
146
            this.status = status;
147
            this.error = error;
148
        }
149
 
150
        public boolean isSuccess() {
151
            return success;
152
        }
153
 
154
        public String getExternalId() {
155
            return externalId;
156
        }
157
 
158
        public String getStatus() {
159
            return status;
160
        }
161
 
162
        public String getError() {
163
            return error;
164
        }
165
    }
166
 
167
    /**
168
     * Reads a send response.
169
     *
170
     * <p>This has to carry the whole burden of deciding success, because the HTTP client used for these
171
     * calls returns the response body without inspecting the status code — a 401 arrives looking exactly
172
     * like a 200. Treating "the call did not throw" as success would report a delivered invoice to the
173
     * store operator while the customer received nothing, and with no delivery webhook nothing would
174
     * ever contradict it. So: anything that is not an explicit {@code success: true} is a failure, and a
175
     * body we cannot parse is a failure rather than an exception.</p>
176
     */
177
    public static SendResult parseSend(String response) {
178
        if (!isPresent(response)) {
179
            return new SendResult(false, null, null, "empty response");
180
        }
181
        JSONObject json;
182
        try {
183
            json = new JSONObject(response);
184
        } catch (Exception e) {
185
            return new SendResult(false, null, null, "unparseable response: " + truncate(response));
186
        }
187
        if (!json.optBoolean("success", false)) {
188
            return new SendResult(false, null, null, errorText(json, response));
189
        }
190
 
191
        JSONObject data = data(json);
192
        if (data == null) {
193
            return new SendResult(false, null, null, "success with no data: " + truncate(response));
194
        }
195
        String externalId = firstNonEmpty(data, "_id", "id", "messageId");
196
        String status = data.optString("status", null);
197
        if (!isPresent(externalId)) {
198
            // Recorded as sent, but nothing to match a future delivery receipt against.
199
            return new SendResult(true, null, status, "success with no message id");
200
        }
201
        return new SendResult(true, externalId, status, null);
202
    }
203
 
204
    /** Parses the approved-template list. Returns an empty list rather than throwing on a bad body. */
205
    public static List<BotPenguinTemplateInfo> parseTemplates(String response) {
206
        List<BotPenguinTemplateInfo> templates = new ArrayList<>();
207
        if (!isPresent(response)) {
208
            return templates;
209
        }
210
        JSONArray rows;
211
        try {
212
            rows = new JSONObject(response).optJSONArray("data");
213
        } catch (Exception e) {
214
            return templates;
215
        }
216
        if (rows == null) {
217
            return templates;
218
        }
219
        for (int i = 0; i < rows.length(); i++) {
220
            JSONObject row = rows.optJSONObject(i);
221
            if (row == null) {
222
                continue;
223
            }
224
            JSONObject configuration = row.optJSONObject("configuration");
225
            if (configuration == null) {
226
                configuration = new JSONObject();
227
            }
228
            templates.add(new BotPenguinTemplateInfo(
229
                    row.optString("_id", null),
230
                    row.optString("templateId", null),
231
                    configuration.optString("name", null),
232
                    configuration.optString("language", null),
233
                    configuration.optString("parameterFormat", null),
234
                    row.optString("status", null)));
235
        }
236
        return templates;
237
    }
238
 
239
    private static JSONObject data(JSONObject json) {
240
        JSONObject data = json.optJSONObject("data");
241
        if (data != null) {
242
            return data;
243
        }
244
        JSONArray array = json.optJSONArray("data");
245
        if (array != null && array.length() > 0) {
246
            return array.optJSONObject(0);
247
        }
248
        return null;
249
    }
250
 
251
    private static String errorText(JSONObject json, String raw) {
252
        String message = firstNonEmpty(json, "message", "error", "errors");
253
        return isPresent(message) ? message : truncate(raw);
254
    }
255
 
256
    private static String firstNonEmpty(JSONObject json, String... keys) {
257
        for (String key : keys) {
258
            if (json.has(key)) {
259
                String value = json.optString(key, null);
260
                if (isPresent(value)) {
261
                    return value;
262
                }
263
            }
264
        }
265
        return null;
266
    }
267
 
268
    private static String truncate(String value) {
269
        if (value == null) {
270
            return null;
271
        }
272
        return value.length() <= 200 ? value : value.substring(0, 200);
273
    }
274
 
275
    private static boolean isPresent(String value) {
276
        return value != null && !value.trim().isEmpty();
277
    }
278
}