diff --git a/build.gradle b/build.gradle index 36534f4..1108f05 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'application' } group = 'com.github.hdvtdev' @@ -11,7 +12,16 @@ repositories { dependencies { implementation(project(":core")) + annotationProcessor(project(":annotation-processor")) implementation(project(":longpolling-okhttp")) + implementation(project(":event-handlers")) + implementation(project(":event-handlers-annotations")) + + +} + +application { + mainClass = "Main" } tasks.register('fat', Jar) { diff --git a/core/build.gradle b/core/build.gradle index c816d58..1739e1a 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -11,6 +11,12 @@ repositories { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3' + implementation platform('com.fasterxml.jackson:jackson-bom:2.18.3') + + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.fasterxml.jackson.core:jackson-annotations' + implementation 'com.fasterxml.jackson.core:jackson-databind' + + implementation 'org.jetbrains:annotations:26.0.2-1' } diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java index ef0e284..c6a8edf 100644 --- a/core/src/main/java/module-info.java +++ b/core/src/main/java/module-info.java @@ -1,8 +1,9 @@ module core { requires com.fasterxml.jackson.databind; + requires org.jetbrains.annotations; + exports hdvtdev.telegram.core.exceptions; exports hdvtdev.telegram.core.objects.command; - exports hdvtdev.telegram.core.annotaions; exports hdvtdev.telegram.core.methods; exports hdvtdev.telegram.core; exports hdvtdev.telegram.core.objects; diff --git a/longpolling-okhttp/build.gradle b/longpolling-okhttp/build.gradle index 77cd762..a6a5d4a 100644 --- a/longpolling-okhttp/build.gradle +++ b/longpolling-okhttp/build.gradle @@ -10,15 +10,14 @@ repositories { } dependencies { - implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3' - implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation platform('com.fasterxml.jackson:jackson-bom:2.18.3') + + + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.fasterxml.jackson.core:jackson-databind' + + //implementation 'com.squareup.okhttp3:okhttp:4.12.0' + implementation 'com.squareup.okhttp3:okhttp:5.2.1' implementation(project(":core")) } -tasks.register('fat') { - jar { - from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - } -} - diff --git a/longpolling-okhttp/src/main/java/hdvtdev/telegram/longpolling/okhttp/OkHttpTelegramBot.java b/longpolling-okhttp/src/main/java/hdvtdev/telegram/longpolling/okhttp/OkHttpTelegramBot.java index bfba11b..1bdef5b 100644 --- a/longpolling-okhttp/src/main/java/hdvtdev/telegram/longpolling/okhttp/OkHttpTelegramBot.java +++ b/longpolling-okhttp/src/main/java/hdvtdev/telegram/longpolling/okhttp/OkHttpTelegramBot.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import hdvtdev.telegram.core.InvokeMethod; +import hdvtdev.telegram.core.HandlersModule; import hdvtdev.telegram.core.TelegramBot; import hdvtdev.telegram.core.UpdateConsumer; -import hdvtdev.telegram.core.annotaions.Jsonable; +import hdvtdev.telegram.core.UpdateExecutor; import hdvtdev.telegram.core.exceptions.TelegramApiException; import hdvtdev.telegram.core.exceptions.TelegramApiNetworkException; import hdvtdev.telegram.core.exceptions.TelegramMethodParsingException; @@ -22,14 +22,10 @@ import okhttp3.*; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.HttpURLConnection; -import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.List; -import java.util.Objects; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; @@ -40,24 +36,10 @@ public class OkHttpTelegramBot implements TelegramBot { private final String TELEGRAM_FILE_API_URL; private final ObjectMapper json; - static { - try { - HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.telegram.org").toURL().openConnection(); - connection.setRequestMethod("HEAD"); - connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); - int responseCode = connection.getResponseCode(); + private ScheduledExecutorService scheduler; - if (responseCode != 200) { - throw new TelegramApiNetworkException("Telegram API is unreachable. Response code: " + responseCode); - } - } catch (IOException e) { - throw new TelegramApiNetworkException("Error checking Telegram API connectivity.", e); - } - } - - private ExecutorService thread; - private AtomicLong lastUpdateId; + private final UpdateExecutor updateExecutor; + private final AtomicLong lastUpdateId = new AtomicLong(0); private int updateLimit = 10; private int updateTimeout = 25; private final OkHttpClient client = buildOkHttpClient(); @@ -68,6 +50,7 @@ public class OkHttpTelegramBot implements TelegramBot { dispatcher.setMaxRequestsPerHost(100); return new OkHttpClient.Builder() + .dispatcher(dispatcher) .connectionPool(new ConnectionPool( 100, @@ -78,14 +61,37 @@ public class OkHttpTelegramBot implements TelegramBot { .writeTimeout(updateTimeout, TimeUnit.SECONDS) .connectTimeout(updateTimeout, TimeUnit.SECONDS) .retryOnConnectionFailure(true) + .addInterceptor(chain -> { + Request request = chain.request(); + + int retryDelay = 1000; + int maxRetries = 5; + + for (int attempt = 1; attempt <= maxRetries; attempt++) { + + try { + return chain.proceed(request); + } catch (IOException e) { + if (attempt == maxRetries) throw e; + try { + TimeUnit.MILLISECONDS.sleep(retryDelay); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw e; + } + retryDelay *= 2; + } + } + throw new TelegramApiNetworkException("Network is unreachable"); + }) .build(); } private UpdateConsumer updateConsumer; - private boolean enableHandlers = false; public OkHttpTelegramBot(String token) { this.json = new ObjectMapper(); + this.updateExecutor = (Update update) -> CompletableFuture.runAsync(() -> updateConsumer.onUpdate(update)); this.TELEGRAM_API_URL = "https://api.telegram.org/bot" + token + "/"; this.TELEGRAM_FILE_API_URL = "https://api.telegram.org/file/bot" + token + "/"; } @@ -93,54 +99,69 @@ public class OkHttpTelegramBot implements TelegramBot { private OkHttpTelegramBot(Builder builder) { updateLimit = builder.updateLimit; updateTimeout = builder.updateTimeout; - enableHandlers = builder.enableHandlers; json = builder.objectMapper == null ? new ObjectMapper() : builder.objectMapper; - /* - if (false) { - Class updateConsumerClass = builder.updateConsumer == null ? UpdateConsumer.class : builder.updateConsumer.getClass(); - Map, Map> handlers = builder.enableScan ? ClassFinder.getClasses() : ClassFinder.localScan(updateConsumerClass); - this.messageHandlers = Collections.unmodifiableMap(handlers.get(TextMessageHandler.class)); - this.callbackQueryHandlers = Collections.unmodifiableMap(handlers.get(CallbackQueryHandler.class)); - } - - */ + ExecutorService pool = builder.pool; + + this.updateExecutor = pool == null ? (Update update) -> CompletableFuture.runAsync(() -> updateConsumer.onUpdate(update)) + : (Update update) -> pool.execute(() -> updateConsumer.onUpdate(update)); this.TELEGRAM_API_URL = "https://api.telegram.org/bot" + builder.token + "/"; this.TELEGRAM_FILE_API_URL = "https://api.telegram.org/file/bot" + builder.token + "/"; - if (builder.updateConsumer != null) setUpdateConsumer(builder.updateConsumer); + if (builder.updateConsumer != null) start(builder.updateConsumer); } - /** - * Enables a long polling update consumer. If {@link #enableHandlers} is {@code true}, - * the specified handlers will be invoked for each received update. - * - * @param updateConsumer class that implements {@code UpdateConsumer} - * @throws IllegalStateException if an {@code UpdateConsumer} is already defined - * @see #enableHandlers - * @since 0.0.1 - */ - private void setUpdateConsumer(UpdateConsumer updateConsumer) throws IllegalStateException { - if (thread != null) throw new IllegalStateException("Update Consumer is already defined. You must first stop the previous"); - this.updateConsumer = updateConsumer; - this.lastUpdateId = new AtomicLong(0); - thread = Executors.newSingleThreadExecutor(); - thread.execute(this::getUpdates); + @Override + public void start(UpdateConsumer updateConsumer) throws IllegalStateException { + if (scheduler != null && !scheduler.isShutdown()) { + throw new IllegalStateException("Long polling is already running. You must stop it first."); + } + + + + boolean moduleEnabled = true; + + try { + Class handlersModule = Class.forName("Handlers"); + HandlersModule module = (HandlersModule) handlersModule.getDeclaredConstructor().newInstance(); + //this.updateConsumer = module.enable(updateConsumer); + } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException | IllegalArgumentException | NoSuchMethodException | InstantiationException e) { + moduleEnabled = false; + } + + if (!moduleEnabled) this.updateConsumer = updateConsumer; + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleWithFixedDelay(this::getUpdates, 0, 1, TimeUnit.SECONDS); + } + + + public void stop() { + if (scheduler == null || scheduler.isShutdown()) { + return; + } + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { + scheduler.shutdownNow(); + Thread.currentThread().interrupt(); + } } private void getUpdates() { + List updates = List.of(awaitExecute(new GetUpdates(lastUpdateId.get() + 1, updateLimit, updateTimeout))); - try { - if (!updates.isEmpty()) { - if (updateConsumer != null) CompletableFuture.runAsync(() -> updateConsumer.onUpdates(updates)); - lastUpdateId.set(updates.getLast().updateId()); + if (!updates.isEmpty()) { + for (Update update : updates) { + updateExecutor.execute(update); } - } finally { - if (!thread.isShutdown()) getUpdates(); + lastUpdateId.set(updates.getLast().updateId()); } } @Override public void shutdown() { - this.thread.close(); + stop(); } @Override @@ -151,7 +172,7 @@ public class OkHttpTelegramBot implements TelegramBot { Request.Builder request = new Request.Builder() .url(TELEGRAM_API_URL + telegramApiMethod.getMethodName()); if (body == null) { - if (telegramApiMethod.getClass().isAnnotationPresent(Jsonable.class)) { + if (telegramApiMethod.isJsonable()) { try { request.post(RequestBody.create(json.writeValueAsString(telegramApiMethod), MediaType.get("application/json; charset=utf-8"))); } catch (JsonProcessingException e) { @@ -168,24 +189,24 @@ public class OkHttpTelegramBot implements TelegramBot { } try (Response response = client.newCall(request.build()).execute()) { - String responseBody = Objects.requireNonNull(response.body()).string(); + ResponseBody responseBody = response.body(); + String responseBodyString = responseBody.string(); if (response.isSuccessful()) { - JsonNode rootNode = json.readTree(responseBody); + JsonNode rootNode = json.readTree(responseBodyString); JsonNode resultNode = rootNode.path("result"); return json.treeToValue(resultNode, telegramApiMethod.getResponseClass()); } else { - throw new TelegramApiException(json.readValue(responseBody, TelegramApiException.ErrorResponse.class)); + throw new TelegramApiException(json.readValue(responseBodyString, TelegramApiException.ErrorResponse.class)); } } catch (IOException e) { throw new TelegramApiNetworkException(e); } - } private File getFile(TelegramFile telegramFile, Path targetDirectory) { try (Response response = client.newCall(new Request.Builder().url(TELEGRAM_FILE_API_URL + telegramFile.filePath()).build()).execute()) { - ResponseBody responseBody = Objects.requireNonNull(response.body()); + ResponseBody responseBody = response.body(); if (!response.isSuccessful()) throw new TelegramApiException(json.readValue(responseBody.string(), TelegramApiException.ErrorResponse.class)); Path filePath = Files.isDirectory(targetDirectory) ? targetDirectory.resolve(Path.of(telegramFile.filePath()).getFileName()) : targetDirectory; @@ -203,16 +224,21 @@ public class OkHttpTelegramBot implements TelegramBot { public static final class Builder { private int updateLimit = 10; private int updateTimeout = 25; - private boolean enableHandlers = false; - private boolean enableScan = false; private final String token; private UpdateConsumer updateConsumer; private ObjectMapper objectMapper; + private ExecutorService pool; + public Builder(String token) { this.token = token; } + + public Builder threadPool(ExecutorService pool) { + this.pool = pool; + return this; + } public Builder objectMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; return this; @@ -233,17 +259,6 @@ public class OkHttpTelegramBot implements TelegramBot { return this; } - public Builder enableHandlers() { - this.enableHandlers = true; - return this; - } - - public Builder enableHandlers(boolean enableScan) { - this.enableHandlers = true; - this.enableScan = enableScan; - return this; - } - public OkHttpTelegramBot build() { return new OkHttpTelegramBot(this); } diff --git a/settings.gradle b/settings.gradle index 8dac7dc..1678725 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,8 @@ rootProject.name = 'TeleJ' include 'core' include 'longpolling-okhttp' + +include 'test' +include 'event-handlers' +include 'annotation-processor' +include 'event-handlers-annotations' \ No newline at end of file