Skip to content

Commit

Permalink
feat: cache tickets (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
sralloza authored Jan 17, 2023
1 parent 4b1d1ee commit abf1a56
Show file tree
Hide file tree
Showing 23 changed files with 248 additions and 117 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Configuration is done by setting environment variables.
- ***ADMIN_API_KEY***: API key with admin privileges of the Chore Management API.
- ***API_BASE_URL***: base url where the Chore Management API is deployed.
- ***API_HTTP2***: Enable HTTP/2. Defaults to `true`.
- ***LATEX_CACHE_ENABLED***: Enable the cache for latex generated images. Defaults to `true`.
- ***CHORE_TYPES_CACHE_ENABLED***: Enable or disable the users cache. If is disabled, for every message sent to the bot a GET request will be sent to the API. Defaults to `true`.
- ***CHORES_CACHE_ENABLED***: Enable or disable the chores cache. It manages the `listWeeklyChores` and the `listChores` endpoints. Defaults to `true`.
- ***CHORE_TYPES_CACHE_ENABLED***: Enable or disable the users cache. If is disabled, for every message sent to the bot a GET request will be sent to the API. Defaults to `true`.
- ***LATEX_CACHE_ENABLED***: Enable the cache for latex generated images. Defaults to `true`.
- ***TICKETS_CACHE_ENABLED***: Enable or disable the tickets cache. Defaults to `true`.
- ***USERS_CACHE_ENABLED***: Enable or disable the users cache. If is disabled, for every message sent to the bot a GET request will be sent to the API. Defaults to `true`.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test {
}

group 'es.sralloza'
version '0.2.0'
version '0.3.0'

repositories {
mavenCentral()
Expand Down
13 changes: 4 additions & 9 deletions src/main/java/Module.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.AbstractModule;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import repositories.ChoreManagementRepository;
import repositories.ChoreManagementRepositoryImp;
import repositories.chores.ChoresRepositoryCacheableModule;
import repositories.choretypes.ChoreTypesRepositoryCacheableModule;
import repositories.tickets.TicketsRepositoryCacheableModule;
import repositories.users.UsersRepositoryCacheableModule;
import security.Security;
import security.SecurityImp;
Expand All @@ -15,7 +15,6 @@

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class Module extends AbstractModule {
@Override
Expand All @@ -24,16 +23,12 @@ protected void configure() {
install(new ChoreTypesRepositoryCacheableModule());
install(new UsersRepositoryCacheableModule());
install(new ChoresRepositoryCacheableModule());
install(new TicketsRepositoryCacheableModule());

Config config = ConfigFactory.load();
String botName = config.getString("telegram.bot.username");
ThreadFactory namedThreadFactory =
new ThreadFactoryBuilder().setNameFormat(botName + " Telegram Executor").build();

bind(Executor.class).toInstance(Executors.newSingleThreadExecutor(namedThreadFactory));
bind(Executor.class).toInstance(Executors.newCachedThreadPool());
bind(ChoreManagementRepository.class).to(ChoreManagementRepositoryImp.class);
bind(ChoreManagementService.class).to(ChoreManagementServiceImp.class);
bind(Config.class).toInstance(config);
bind(Config.class).toInstance(ConfigFactory.load());
bind(Security.class).to(SecurityImp.class);
}
}
72 changes: 52 additions & 20 deletions src/main/java/bot/BaseChoreManagementBot.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Consumer;

import static org.telegram.abilitybots.api.db.MapDBContext.offlineInstance;

Expand Down Expand Up @@ -70,58 +71,80 @@ protected void sendMessage(String msgStr, String chatId, boolean markdown) {
}
}

protected Boolean requireUser(MessageContext ctx) {
protected void runCheckingUserRegistered(MessageContext ctx, Consumer<MessageContext> consumer) {
var chatId = ctx.chatId().toString();
var isAuthenticated = security.isAuthenticated(chatId).join();
if (!isAuthenticated) {
sendMessage("You don't have permission to execute this action", chatId, false);
return false;
}
return true;
security.isAuthenticated(chatId)
.thenAcceptAsync(isAuthenticated -> {
if (isAuthenticated) {
consumer.accept(ctx);
} else {
sendMessage("You don't have permission to execute this action", chatId, false);
}
}, executor);
}

protected void handleException(Exception e, String chatId) {
handleException(e, chatId, null);
}

protected static boolean isApiExceptionNormalForUser(APIException exc) {
if (exc.getStatusCode().equals(404) && exc.getMsg().equalsIgnoreCase("Not Found")) {
return false;
}
return exc.getStatusCode() < 500;
}

protected void handleException(Exception e, String chatId, QueryType type) {
log.error("Manually handling exception", e);
Optional.ofNullable(redisService.getMessage(chatId, type))
.ifPresent(messageId -> deleteMessage(chatId, messageId));

if (e.getClass().equals(APIException.class)) {
var exc = (APIException) e;
sendMessage("Error: " + exc.getMsg(), chatId, false);
} else if (e.getCause().getClass().equals(APIException.class)) {
var exc = (APIException) e.getCause();
Exception realException = e;
boolean isApiException = false;
if (e instanceof APIException) {
isApiException = true;
} else if (e.getCause() instanceof APIException) {
isApiException = true;
realException = (Exception) e.getCause();
}
if (isApiException) {
APIException exc = (APIException) realException;
if (!isApiExceptionNormalForUser(exc)) {
sendUnknownError(e, chatId);
return;
}
sendMessage("Error: " + exc.getMsg(), chatId, false);
} else {
sendMessage(Messages.UNKNOWN_ERROR, chatId, true);
String msg = "ERROR:\n" + e.getClass() + " - " + e.getMessage();
sendMessage(msg, String.valueOf(creatorId()), false);
sendUnknownError(e, chatId);
}
}

private void sendUnknownError(Exception e, String chatId) {
sendMessage(Messages.UNKNOWN_ERROR, chatId, true);
String msg = "ERROR:\n" + e.getClass() + " - " + e.getMessage();
sendMessage(msg, String.valueOf(creatorId()), false);
}

protected void sendTable(List<List<String>> table,
String chatId,
String filename,
String keyPrefix,
String emptyMessage) {
if (table.isEmpty()) {
sendMessage(emptyMessage, chatId, false);
return;
}
latexService.genTable(table, filename);
File file = latexService.genTable(table, keyPrefix);

try {
InputFile inputFile = new InputFile(new File(filename));
InputFile inputFile = new InputFile(file);
SendPhoto message = new SendPhoto();
message.setPhoto(inputFile);
message.setChatId(chatId);
this.execute(message);

boolean result = new File(filename).delete();
boolean result = file.delete();
if (!result) {
log.error("Could not delete file {}", filename);
log.error("Could not delete file {}", file.getAbsolutePath());
}
} catch (Exception exc) {
handleException(exc, chatId);
Expand Down Expand Up @@ -181,4 +204,13 @@ protected BiFunction<Void, Throwable, Void> replyHandler(MessageContext ctx, Str
return null;
};
}

protected <T> BiFunction<T, Throwable, T> exceptionHandler(String chatId) {
return (T obj, Throwable throwable) -> {
if (throwable != null) {
handleException((Exception) throwable, chatId);
}
return obj;
};
}
}
25 changes: 11 additions & 14 deletions src/main/java/bot/ChoreManagementBot.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public long creatorId() {
public Ability processMsg() {
return Ability.builder()
.name(DEFAULT)
.action(this::processMsg)
.action(ctx -> runCheckingUserRegistered(ctx, this::processMsg))
.enableStats()
.locality(USER)
.privacy(PUBLIC)
Expand All @@ -79,7 +79,7 @@ public Ability start() {
.info("Starts the bot")
.locality(USER)
.privacy(PUBLIC)
.action(this::sendMenuAsync)
.action(ctx -> runCheckingUserRegistered(ctx, this::sendMenuAsync))
.enableStats()
.build();
}
Expand Down Expand Up @@ -155,9 +155,6 @@ public boolean startFlowSelectTask(MessageContext ctx, List<Chore> tasks, List<C
}

private void processMsg(MessageContext ctx) {
if (!requireUser(ctx)) {
return;
}
if (ctx.update().hasCallbackQuery()) {
processQueryData(ctx);
return;
Expand All @@ -176,21 +173,24 @@ private void processMsg(MessageContext ctx) {
switch (userMessage) {
case UserMessages.TICKETS:
choreTypesFuture = service.listChoreTypes();
service.getTickets(chatId)
service.listTickets(chatId)
.thenCombine(choreTypesFuture, Normalizers::normalizeTickets)
.thenAcceptAsync(tickets -> sendTable(tickets, chatId, TICKETS_TABLE_PNG, Messages.NO_TICKETS_FOUND), executor);
.thenAcceptAsync(tickets -> sendTable(tickets, chatId, "ticketsTable", Messages.NO_TICKETS_FOUND), executor)
.handleAsync(exceptionHandler(chatId), executor);
break;
case UserMessages.TASKS:
choreTypesFuture = service.listChoreTypes();
service.getWeeklyChores(chatId)
.thenCombine(choreTypesFuture, Normalizers::normalizeWeeklyChores)
.thenAcceptAsync(tasks -> sendTable(tasks, chatId, WEEKLY_TASKS_TABLE_PNG, Messages.NO_TASKS), executor);
.thenAcceptAsync(tasks -> sendTable(tasks, chatId, "weeklyTasksTable", Messages.NO_TASKS), executor)
.handleAsync(exceptionHandler(chatId), executor);
break;
case UserMessages.COMPLETE_TASK:
choreTypesFuture = service.listChoreTypes();
service.getChores(chatId)
service.listChores(chatId)
.thenCombineAsync(choreTypesFuture, (choreList, choreTypeList) ->
startFlowSelectTask(ctx, choreList, choreTypeList), executor);
startFlowSelectTask(ctx, choreList, choreTypeList), executor)
.handleAsync(exceptionHandler(chatId), executor);
break;
case UserMessages.SKIP:
silent.forceReply(Messages.ASK_FOR_WEEK_TO_SKIP, ctx.chatId());
Expand Down Expand Up @@ -233,7 +233,7 @@ private void processQueryData(MessageContext ctx) {
switch (callbackData.getType()) {
case COMPLETE_TASK:
service.completeChore(chatId, callbackData.getWeekId(), callbackData.getChoreType())
.handle(callbackQueryHandler(ctx, queryId, Messages.TASK_COMPLETED, QueryType.COMPLETE_TASK));
.handleAsync(callbackQueryHandler(ctx, queryId, Messages.TASK_COMPLETED, QueryType.COMPLETE_TASK), executor);
break;
default:
sendMessage(Messages.UNDEFINED_COMMAND, chatId, false);
Expand All @@ -242,9 +242,6 @@ private void processQueryData(MessageContext ctx) {
}

private void sendMenuAsync(MessageContext ctx) {
if (!requireUser(ctx)) {
return;
}
SendMessage msg = new SendMessage();
msg.setText(Messages.START_MSG);
msg.disableNotification();
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/constants/CacheConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
public class CacheConstants {
public static final int LATEX_CACHE_EXPIRE_SECONDS = 2 * 7 * 24 * 3600;
public static final int USERS_CACHE_EXPIRE_SECONDS = 4 * 7 * 24 * 3600;
public static final int TICKETS_CACHE_EXPIRE_SECONDS = 4 * 7 * 24 * 3600;
public static final int CHORE_TYPES_CACHE_EXPIRE_SECONDS = 4 * 7 * 24 * 3600;
public static final int CHORES_CACHE_EXPIRE_SECONDS = 7 * 24 * 3600;
public static final int WEEKLY_CHORES_CACHE_EXPIRE_SECONDS = 7 * 24 * 3600;

public static final String USERS_REDIS_KEY_PREFIX = "api::users";
public static final String CHORES_REDIS_KEY_PREFIX = "api::chores";
public static final String WEEKLY_CHORES_REDIS_KEY_PREFIX = "api::weeklyChores";
public static final String TICKETS_REDIS_KEY_PREFIX = "api::tickets";
}
43 changes: 22 additions & 21 deletions src/main/java/constants/Messages.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,32 @@
import org.telegram.telegrambots.meta.updateshandlers.SentCallback;

public class Messages {
public static final String START_MSG = "*API connected*";
public static final String START_MSG = "*API connected*";

public static final String NO_TASKS = "No tasks found";
public static final String NO_PENDING_TASKS = "No pending tasks found";
public static final String SELECT_TASK_TO_COMPLETE = "Select task to complete";
public static final String NO_TASKS = "No tasks found";
public static final String NO_PENDING_TASKS = "No pending tasks found";
public static final String SELECT_TASK_TO_COMPLETE = "Select task to complete";

public static final Callback DEFAULT_CALLBACK = new Callback();
public static final String TASK_COMPLETED = "Task completed";
public static final String UNDEFINED_COMMAND = "Undefined command";
public static final String ASK_FOR_WEEK_TO_SKIP = "Write week id to skip (year.number)";
public static final String ASK_FOR_WEEK_TO_UNSKIP = "Write week id to unskip (year.number)";
public static final String UNKNOWN_ERROR = "Unknown error happened\\. More info sent to the creator\\.";
public static final String NO_TICKETS_FOUND = "No tickets found";
public static final Callback DEFAULT_CALLBACK = new Callback();
public static final String TASK_COMPLETED = "Task completed";
public static final String UNDEFINED_COMMAND = "Undefined command";
public static final String ASK_FOR_WEEK_TO_SKIP = "Write week id to skip (year.number)";
public static final String ASK_FOR_WEEK_TO_UNSKIP = "Write week id to unskip (year.number)";
public static final String UNKNOWN_ERROR = "Ha ocurrido un error no contemplado\\. Se ha enviado más información " +
"al administrador para resolver el problema\\.";
public static final String NO_TICKETS_FOUND = "No tickets found";

private static class Callback implements SentCallback<Boolean> {
@Override
public void onResult(BotApiMethod method, Boolean response) {
}
private static class Callback implements SentCallback<Boolean> {
@Override
public void onResult(BotApiMethod method, Boolean response) {
}

@Override
public void onError(BotApiMethod method, TelegramApiRequestException apiException) {
}
@Override
public void onError(BotApiMethod method, TelegramApiRequestException apiException) {
}

@Override
public void onException(BotApiMethod method, Exception exception) {
}
@Override
public void onException(BotApiMethod method, Exception exception) {
}
}
}
4 changes: 4 additions & 0 deletions src/main/java/exceptions/APIException.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ public class APIException extends RuntimeException {
private String url;
private String method;
private Integer statusCode;
private String xCorrelator;
private String apiKey;

public APIException(HttpResponse<String> response) {
super(Generic.getResponseMessage(response));
this.msg = Generic.getResponseMessage(response);
this.url = response.uri().toString();
this.method = response.request().method();
this.statusCode = response.statusCode();
this.xCorrelator = response.headers().firstValue("X-Correlator").orElse(null);
this.apiKey = response.request().headers().firstValue("x-token").orElse(null);
}
}
5 changes: 1 addition & 4 deletions src/main/java/repositories/ChoreManagementRepository.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package repositories;

import models.Ticket;

import java.util.List;
import java.util.concurrent.CompletableFuture;

public interface ChoreManagementRepository {
CompletableFuture<List<Ticket>> getTickets(String userId);
CompletableFuture<Void> skipWeek(String userId, String weekId);

CompletableFuture<Void> unskipWeek(String userId, String weekId);
}
12 changes: 0 additions & 12 deletions src/main/java/repositories/ChoreManagementRepositoryImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,8 @@

import com.google.inject.Inject;
import config.ConfigRepository;
import models.Chore;
import models.ChoreType;
import models.Ticket;
import models.User;
import models.WeeklyChores;
import security.Security;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;

Expand All @@ -20,11 +13,6 @@ public ChoreManagementRepositoryImp(ConfigRepository config, Security security,
super(config, security, executor);
}

public CompletableFuture<List<Ticket>> getTickets(String userId) {
return sendGetRequest("/api/v1/tickets", Ticket[].class, userId)
.thenApply(Arrays::asList);
}

public CompletableFuture<Void> skipWeek(String userId, String weekId) {
return sendPostRequest("/api/v1/users/me/deactivate/" + weekId, null, userId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@

public class ChoreTypesRepositoryNonCached extends BaseRepository implements ChoreTypesRepository {
@Inject
public ChoreTypesRepositoryNonCached(String baseURL, String apiToken, ConfigRepository config,
Security security, Executor executor) {
public ChoreTypesRepositoryNonCached(ConfigRepository config, Security security, Executor executor) {
super(config, security, executor);
}

Expand Down
Loading

0 comments on commit abf1a56

Please sign in to comment.