I'm working on a "framework" for Telegram bots and right now everything "just works". I'm now in the process of rethinking about what I wrote and one of the things I dislike a lot is how I implemented the API client.
Right now I have an interface (TelegramBotAp
i) which maps every API endpoint to a method. This interface is implemented by this class:
public class TelegramBotRestApi implements TelegramBotApi {
private final ObjectMapper mapper;
private final RestTemplate restTemplate;
private final UriTemplate apiUriTemplate;
private final UriTemplate fileUriTemplate;
public TelegramBotRestApi(String token, ObjectMapper mapper, RestTemplate restTemplate) {
this.mapper = mapper;
this.restTemplate = restTemplate;
this.apiUriTemplate = new UriTemplate("https://api.telegram.org/bot" + token + "/{method}");
this.fileUriTemplate = new UriTemplate("https://api.telegram.org/file/bot" + token + "/{file_path}");
}
@Override
public User getMe() throws TelegramBotApiException {
TelegramBotRestApiCall.Builder<User> builder = new TelegramBotRestApiCall.Builder<User>("getMe", apiUriTemplate,
mapper, restTemplate, User.class);
builder.setHttpMethod(HttpMethod.GET);
return builder.build().call();
}
@Override
public List<Update> getUpdates(Integer offset, Integer limit, Integer timeout) throws TelegramBotApiException {
TelegramBotRestApiCall.Builder<Update[]> builder = new TelegramBotRestApiCall.Builder<Update[]>("getUpdates",
apiUriTemplate, mapper, restTemplate, Update[].class);
builder.setParam("offset", offset, false).setParam("limit", limit, false).setParam("timeout", timeout, false);
builder.setContentType(MediaType.MULTIPART_FORM_DATA);
Update[] result = builder.build().call();
Arrays.sort(result);
return Collections.unmodifiableList(Arrays.asList(result));
}
@Override
public Message sendMessage(ChatId chatId, String text, ParseMode parseMode, Boolean disableWebPagePreview,
Boolean disableNotification, Integer replyToMessageId, AbstractKeyboardMarkup replyMarkup)
throws TelegramBotApiException {
TelegramBotRestApiCall.Builder<Message> builder = new TelegramBotRestApiCall.Builder<Message>("sendMessage",
apiUriTemplate, mapper, restTemplate, Message.class);
builder.setParam("chat_id", chatId, true).setParam("text", text, true).setParam("parse_mode", parseMode, false)
.setParam("disable_web_page_preview", disableWebPagePreview, false)
.setParam("disable_notification", disableNotification, false)
.setParam("reply_to_message_id", replyToMessageId, false).setParam("reply_markup", replyMarkup, false);
builder.setContentType(MediaType.MULTIPART_FORM_DATA);
return builder.build().call();
}
// many more methods
}
This class is using another class (TelegramBotRestApiCall
) to build the actual call and then execute it:
public class TelegramBotRestApiCall<T> {
private final RestTemplate rest;
@SuppressWarnings("rawtypes")
private static final Map<Class, ParameterizedTypeReference> typeRefs = initializeParameterizedTypeReferences();
@SuppressWarnings("rawtypes")
private static Map<Class, ParameterizedTypeReference> initializeParameterizedTypeReferences() {
Map<Class, ParameterizedTypeReference> tmp = new HashMap<Class, ParameterizedTypeReference>();
tmp.put(User.class, new ParameterizedTypeReference<Response<User>>() {
});
tmp.put(Update[].class, new ParameterizedTypeReference<Response<Update[]>>() {
});
tmp.put(Message.class, new ParameterizedTypeReference<Response<Message>>() {
});
tmp.put(UserProfilePhotos.class, new ParameterizedTypeReference<Response<UserProfilePhotos>>() {
});
tmp.put(TelegramFile.class, new ParameterizedTypeReference<Response<TelegramFile>>() {
});
tmp.put(Boolean.class, new ParameterizedTypeReference<Response<Boolean>>() {
});
tmp.put(Chat.class, new ParameterizedTypeReference<Response<Chat>>() {
});
tmp.put(ChatMember.class, new ParameterizedTypeReference<Response<ChatMember>>() {
});
tmp.put(ChatMember[].class, new ParameterizedTypeReference<Response<ChatMember[]>>() {
});
tmp.put(Integer.class, new ParameterizedTypeReference<Response<Integer>>() {
});
tmp.put(BooleanOrMessage.class, new ParameterizedTypeReference<Response<BooleanOrMessage>>() {
});
// for methods without an output (ie. sendChatAction)
tmp.put(String.class, new ParameterizedTypeReference<Response<String>>() {
});
return tmp;
}
private final ObjectMapper mapper;
private final String method;
private final UriTemplate uriTemplate;
private final HttpMethod httpMethod;
private final HttpEntity<?> entity;
@SuppressWarnings("rawtypes")
private final Class payloadType;
@SuppressWarnings("rawtypes")
private TelegramBotRestApiCall(String method, UriTemplate uriTemplate, ObjectMapper mapper, RestTemplate rest,
HttpMethod httpMethod, HttpEntity<?> entity, Class payloadType) {
this.method = method;
this.uriTemplate = uriTemplate;
this.httpMethod = httpMethod;
this.mapper = mapper;
this.entity = entity;
this.payloadType = payloadType;
this.rest = rest;
}
@SuppressWarnings("unchecked")
public T call() throws TelegramBotApiException {
Response<T> res = null;
try {
res = (Response<T>) rest.exchange(uriTemplate.expand(method), httpMethod, entity, typeRefs.get(payloadType))
.getBody();
} catch (HttpStatusCodeException he) {
Integer statusCode = null;
try {
res = mapper.readValue(he.getResponseBodyAsString(), Response.class);
statusCode = he.getStatusCode() != null ? he.getStatusCode().value() : null;
} catch (IOException e) {
}
throw new TelegramBotApiException(he, statusCode);
} catch (RestClientException e) {
throw new TelegramBotApiException(e);
}
if (!res.ok)
throw new TelegramBotApiException(res.description, res.errorCode);
return res.result;
}
public static class Builder<T> {
private final ObjectMapper mapper;
private final RestTemplate rest;
private final String method;
private final UriTemplate uriTemplate;
private final MultiValueMap<String, Object> body = new LinkedMultiValueMap<String, Object>();
private final HttpHeaders headers = new HttpHeaders();
@SuppressWarnings("rawtypes")
private final Class payloadType;
private HttpMethod httpMethod;
private HttpEntity<?> entity;
@SuppressWarnings("rawtypes")
public Builder(String method, UriTemplate uriTemplate, ObjectMapper mapper, RestTemplate rest, Class payloadType) {
this.method = method;
this.uriTemplate = uriTemplate;
this.mapper = mapper;
this.rest = rest;
this.payloadType = payloadType;
this.httpMethod = HttpMethod.POST;
}
public Builder<T> setParam(String name, Object param, boolean required) {
if (required && param == null)
throw new InvalidParameterException(
String.format("(%s) Null value is not allowed for field: %s", this.method, name));
if (param != null)
this.body.add(name, param);
return this;
}
public Builder<T> setHttpMethod(HttpMethod method) {
this.httpMethod = method;
return this;
}
public Builder<T> setContentType(MediaType type) {
this.headers.setContentType(type);
return this;
}
public TelegramBotRestApiCall<T> build() {
if (this.body.size() > 0 || this.headers.size() > 0) {
this.entity = new HttpEntity<Object>(body, headers);
}
return new TelegramBotRestApiCall<T>(method, uriTemplate, mapper, rest, httpMethod, entity, payloadType);
}
}
}
I feel like this is actually a complete mess and would like to redesign it in a more "clean" way.
One of my main goals is the implementation of the API constraints (string parameters length for example) which I'm almost ignoring right now (I'm only checking for mandatory parameters).
One of the ideas floating in my mind is to create a class for each API method, keep mandatory parameters in the constructor and validate parameters inside the setters. With this change I could then have a single method which would handle every call.
I would be really grateful if you could point me to the "right" direction (I know there's no "right" direction but advice are welcome).
For anyone wondering, the whole project is hosted on github.