Compare commits

...

7 Commits

Author SHA1 Message Date
hdvt
a9742134b2 backup 2025-11-13 23:51:01 +03:00
hdvt
a8b772a5ea Merge remote-tracking branch 'origin/master' 2025-11-04 01:58:18 +03:00
hdvt
ca295b66f9 readme 2025-11-04 01:57:41 +03:00
hdvt
9fdfd91e1d Update README.md 2025-11-04 01:52:55 +03:00
hdvt
7607ec4205 Update README.md 2025-11-04 01:52:38 +03:00
hdvt
b8615d3b90 migration 2025-11-03 21:20:12 +03:00
hdvt
15f77a945a migration 2025-11-03 21:17:46 +03:00
41 changed files with 1161 additions and 99 deletions

4
.gitignore vendored
View File

@@ -41,3 +41,7 @@ bin/
### Mac OS ###
.DS_Store
/core/src/main/java/hdvtdev/telegram/Count.java
/src/
/test/
/.idea/
/gradle/

4
.idea/gradle.xml generated
View File

@@ -8,8 +8,12 @@
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/annotation-processor" />
<option value="$PROJECT_DIR$/core" />
<option value="$PROJECT_DIR$/event-handlers" />
<option value="$PROJECT_DIR$/event-handlers-annotations" />
<option value="$PROJECT_DIR$/longpolling-okhttp" />
<option value="$PROJECT_DIR$/test" />
</set>
</option>
</GradleProjectSettings>

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">

141
README.md
View File

@@ -1,10 +1,143 @@
### English Version
# TeleJ
An elegant Java library for creating Telegram bots, featuring a framework module to reduce boilerplate and speed up development.
## ⚠️ Project Status ⚠️
**This project is currently under active development.** APIs may change, and features are still being added. It is not yet recommended for production use.
## About The Project
TeleJ is a library designed to simplify and accelerate the process of creating Telegram bots in Java. It provides a convenient API and a modular structure that helps you focus on your bot's logic rather than on repetitive code.
## Key Features
* **Simple Setup:** Get started with your bot quickly.
* **Modular Architecture:** Includes a `core` module, an `event-handlers` framework module, and other components that can be included as needed.
* **Modern Approach:** Utilizes modern Java features to create clean and readable code.
* **Extensible:** Easily extendable to add custom functionality.
### Usage Example
Below is a simple example of how to create and run a bot using TeleJ.
```java
public class MyAwesomeBot {
private static final TelegramBot telegramBot = new OkHttpTelegramBot("token");
public static void main(String[] args) {
telegramBot.start(new UpdateConsumer() {
@Override
public void onUpdate(Update update) {
if (update.hasMessage()) {
Message message = update.message();
if (message.hasText()) {
if (message.text().equalsIgnoreCase("ping!")) {
telegramBot.execute(new SendMessage.Builder(
message.chatId(), "pong!").replyToMessage(message.messageId())
.build());
}
}
}
}
});
}
}
```
## Project Structure
The project consists of several modules:
* `core`: The core of the library with all the basic functions for interacting with the Telegram Bot API.
* `event-handlers`: A framework module designed to reduce boilerplate and speed up development by simplifying the handling of events and updates from Telegram.
* `annotation-processor`: An annotation processor to simplify code writing.
* `longpolling-okhttp`: An implementation for receiving updates via Long Polling using OkHttp.
## License
This project is distributed under the MIT License.
***
### Русская версия
# TeleJ
Изящная библиотека на Java для создания ботов в Telegram, с модулем фреймворка для ускорения разработки и уменьшения количества шаблонного кода.
## ⚠️ Статус проекта ⚠️
**Этот проект находится в стадии активной разработки.** API может изменяться, а новые функции все еще добавляются. Пока не рекомендуется использовать его в продакшене.
## О проекте
TeleJ — это библиотека, разработанная для упрощения и ускорения процесса создания ботов для Telegram на языке Java. Она предоставляет удобный API и модульную структуру, которая помогает сосредоточиться на логике вашего бота, а не на повторяющемся коде.
## Основные возможности
* **Простая настройка:** Быстрое начало работы с вашим ботом.
* **Модульная архитектура:** Включает основной модуль (`core`), модуль фреймворка (`event-handlers`) и другие компоненты, которые можно подключать по мере необходимости.
* **Современный подход:** Использует современные возможности Java для создания чистого и читаемого кода.
* **Расширяемость:** Легко расширяется для добавления пользовательского функционала.
### Пример использования
Total lines of code:
Ниже приведен простой пример того, как создать и запустить бота с использованием TeleJ.
```java
public class MyAwesomeBot {
private static final TelegramBot telegramBot = new OkHttpTelegramBot("token");
public static void main(String[] args) {
<p>
<span style="background-color: blue; color: white; padding: 2px 4px;">08 April 2025 22:07:16 code: 6094 docs: 173</span>
<p>
telegramBot.start(new UpdateConsumer() {
@Override
public void onUpdate(Update update) {
if (update.hasMessage()) {
Message message = update.message();
if (message.hasText()) {
if (message.text().equalsIgnoreCase("ping!")) {
telegramBot.execute(new SendMessage.Builder(
message.chatId(), "pong!").replyToMessage(message.messageId())
.build());
}
}
}
}
});
}
}
```
## Структура проекта
Проект состоит из нескольких модулей:
* `core`: Ядро библиотеки со всеми основными функциями для взаимодействия с Telegram Bot API.
* `event-handlers`: Модуль фреймворка, предназначенный для уменьшения бойлерплейта (шаблонного кода) и ускорения разработки за счет упрощения обработки событий и обновлений от Telegram.
* `annotation-processor`: Обработчик аннотаций для упрощения написания кода.
* `longpolling-okhttp`: Реализация получения обновлений через Long Polling с использованием OkHttp.
## Лицензия
Этот проект распространяется под лицензией MIT License.

View File

@@ -0,0 +1,20 @@
plugins {
id 'java'
}
group = 'com.github.hdvtdev'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'com.google.auto.service:auto-service:1.1.1'
implementation project(':core')
implementation 'org.ow2.asm:asm:9.9'
annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
//implementation 'com.fasterxml.jackson.core:jackson-annotations:2.18.3'
implementation project(":event-handlers-annotations")
}

View File

@@ -0,0 +1,172 @@
import com.google.auto.service.AutoService;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.*;
import java.util.stream.Collectors;
import hdvtdev.telegram.handler.annotations.*;
@AutoService(Processor.class)
@SupportedAnnotationTypes({
"hdvtdev.telegram.handler.annotations.OnChosenInlineResult",
"hdvtdev.telegram.handler.annotations.OnChatMemberUpdated",
"hdvtdev.telegram.handler.annotations.OnChatBoostRemoved",
"hdvtdev.telegram.handler.annotations.OnPreCheckoutQuery",
"hdvtdev.telegram.handler.annotations.OnBusinessConnection",
"hdvtdev.telegram.handler.annotations.OnPaidMediaPurchased",
"hdvtdev.telegram.handler.annotations.OnUpdate",
"hdvtdev.telegram.handler.annotations.BotCommand",
"hdvtdev.telegram.handler.annotations.OnPoll",
"hdvtdev.telegram.handler.annotations.OnMessage",
"hdvtdev.telegram.handler.annotations.OnChatJoinRequest",
"hdvtdev.telegram.handler.annotations.OnChatBoostUpdated",
"hdvtdev.telegram.handler.annotations.OnCallbackQuery",
"hdvtdev.telegram.handler.annotations.OnShippingQuery",
"hdvtdev.telegram.handler.annotations.OnInlineQuery",
"hdvtdev.telegram.handler.annotations.OnPollAnswer"})
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class EventHandlersProcessor extends AbstractProcessor {
private final StringBuilder sb = new StringBuilder();
private static final String PACKAGE = "hdvtdev.telegram.handler.annotations.";
private static final String HANDLER_PACKAGE = "hdvtdev.telegram.handler.";
private static final Set<String> SUPPORTED_ANNOTATIONS = Set.of(
OnChosenInlineResult.class,
OnChatMemberUpdated.class,
OnChatBoostRemoved.class,
OnPreCheckoutQuery.class,
OnBusinessConnection.class,
OnPaidMediaPurchased.class,
OnUpdate.class,
OnPoll.class,
OnMessage.class,
OnChatJoinRequest.class,
OnChatBoostUpdated.class,
OnCallbackQuery.class,
OnShippingQuery.class,
OnInlineQuery.class,
OnPollAnswer.class
).stream().map(Class::getName).collect(Collectors.toSet());
private static final Map<String, String> SUPPORTED_PARAMETERS = Map.ofEntries(
Map.entry("Update", "hdvtdev.telegram.core.objects.Update"),
Map.entry("InlineQuery", "hdvtdev.telegram.core.objects.InlineQuery"),
Map.entry("ChatMemberUpdated", "hdvtdev.telegram.core.objects.chat.ChatMemberUpdated"),
Map.entry("ChatBoostRemoved", "hdvtdev.telegram.core.objects.chatboost.ChatBoostRemoved"),
Map.entry("PreCheckoutQuery", "hdvtdev.telegram.core.objects.payment.PreCheckoutQuery"),
Map.entry("BusinessConnection", "hdvtdev.telegram.core.objects.business.BusinessConnection"),
Map.entry("PaidMediaPurchased", "hdvtdev.telegram.core.objects.media.paidmedia.PaidMediaPurchased"),
Map.entry("Poll", "hdvtdev.telegram.core.objects.poll.Poll"),
Map.entry("Message", "hdvtdev.telegram.core.objects.Message"),
Map.entry("ChatJoinRequest", "hdvtdev.telegram.core.objects.chat.ChatJoinRequest"),
Map.entry("ChatBoostUpdated", "hdvtdev.telegram.core.objects.chatboost.ChatBoostUpdated"),
Map.entry("CallbackQuery", "hdvtdev.telegram.core.objects.callback.CallbackQuery"),
Map.entry("ShippingQuery", "hdvtdev.telegram.core.objects.payment.ShippingQuery"),
Map.entry("PollAnswer", "hdvtdev.telegram.core.objects.poll.PollAnswer"),
Map.entry("ChosenInlineResult", "hdvtdev.telegram.core.objects.ChosenInlineResult")
);
public static String getFullParameterName(String simpleName) {
return SUPPORTED_PARAMETERS.get(simpleName);
}
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
messager.printNote("Called");
for (Element element : roundEnvironment.getRootElements()) {
for (Element enclosedElement : element.getEnclosedElements()) {
if (enclosedElement.getKind() == ElementKind.METHOD) {
processMethod((ExecutableElement) enclosedElement);
}
}
}
if (roundEnvironment.processingOver()) {
write();
}
return true;
}
private void write() {
}
private void processMethod(ExecutableElement methodElement) {
String methodName = methodElement.getSimpleName().toString();
List<AnnotationMirror> foundAnnotations = new ArrayList<>();
for (AnnotationMirror mirror : methodElement.getAnnotationMirrors()) {
String mirrorAnnotationName = Util.getAnnotationMirrorName(mirror);
if (SUPPORTED_ANNOTATIONS.contains(PACKAGE + mirrorAnnotationName)) {
foundAnnotations.add(mirror);
}
}
if (foundAnnotations.size() > 1) {
List<String> annotationNames = new ArrayList<>();
for (AnnotationMirror mirror : foundAnnotations) {
annotationNames.add("@" + Util.getAnnotationMirrorName(mirror));
}
messager.printError(String.format("Method %s cannot have multiple mutually exclusive annotations. Found: %s. Please leave only one.",
methodName, String.join(", ", annotationNames)), methodElement);
return;
}
if (foundAnnotations.size() == 1) {
AnnotationMirror annotation = foundAnnotations.getFirst();
String annotationSimpleName = Util.getAnnotationMirrorName(annotation);
var params = methodElement.getParameters();
if (params.isEmpty()) {
messager.printError(String.format("Method %s, annotated with @%s, must have at least one parameter.",
methodName, annotationSimpleName), methodElement);
return;
}
String firstParameterType = Util.getParameterClassName(params.getFirst().asType());
String requiredParameter = Util.checkParameter(annotationSimpleName, firstParameterType);
if (!requiredParameter.isEmpty()) {
messager.printError(String.format("Incorrect parameter type for method %s. Annotation @%s requires %s, but found %s.",
methodName, annotationSimpleName, requiredParameter, firstParameterType), params.getFirst());
return;
}
if (!annotationSimpleName.equals("BotCommand")) {
String paramName = params.getFirst().getSimpleName().toString();
Set<String> filters = new HashSet<>();
AnnotationValue vv = Util.getAnnotationValue(annotation, "filters");
@SuppressWarnings("unchecked")
List<? extends AnnotationValue> enumValues = vv == null ? List.of() : (List<? extends AnnotationValue>) vv.getValue();
for (AnnotationValue v : enumValues) {
if (v.getValue() instanceof VariableElement enumConstant) {
filters.add(enumConstant.getSimpleName().toString());
}
}
sb.append(Generator.generateVoid(methodName, String.format("%s %s", firstParameterType, paramName),
Generator.generateFilterBlock(filters, paramName)));
}
}
}
}

View File

@@ -0,0 +1,42 @@
import java.util.Set;
public final class Generator {
private Generator() {
}
public static String generateVoid(String name, String obj, String block) {
return String.format("""
public static void %s(%s) {
%s
}
""", name, obj, block);
}
public static String generateFilterBlock(Set<String> filters, String objName) {
StringBuilder sb = new StringBuilder();
for (String filter : filters) {
boolean skip = true;
sb.append("if (");
sb.append(objName);
sb.append(".");
for (String part : filter.split("_")) {
part = part.toLowerCase();
if (skip) {
sb.append(part);
skip = false;
} else {
char[] chars = part.toCharArray();
chars[0] = Character.toUpperCase(chars[0]);
sb.append(chars);
}
}
sb.append("()) {");
sb.append("\n /* some code */ \n}\n");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,112 @@
import org.objectweb.asm.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class HandlerWriter {
public static void write(String body) throws IOException {
Consumer<String> consumer = (String s) -> {};
String classFilePath = "models/Handlers.class"; // Путь к вашему файлу
Path path = Paths.get(classFilePath);
// 1. Читаем исходный класс в байты
byte[] originalBytecode = Files.readAllBytes(path);
// 2. Инициализируем ASM
ClassReader classReader = new ClassReader(originalBytecode);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 3. Создаем наш визитор, который найдет и заменит метод
ClassVisitor classVisitor = new MethodReplacer(classWriter);
// 4. Запускаем процесс
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
// 5. Получаем измененный байт-код
byte[] modifiedBytecode = classWriter.toByteArray();
// 6. Перезаписываем исходный файл
Files.write(path, modifiedBytecode);
System.out.println("Метод invoke в файле " + classFilePath + " был успешно заменен.");
}
private static class MethodReplacer extends ClassVisitor {
public MethodReplacer(ClassVisitor classVisitor) {
super(Opcodes.ASM9, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
String targetDescriptor = "(L" + EventHandlersProcessor.getFullParameterName("Update").replace('.', '/') +
";)V";
if ("invoke".equals(name) && targetDescriptor.equals(descriptor)) {
System.out.println("Найден метод 'invoke'. Заменяем его тело...");
// Найден нужный метод. Не передаем его дальше (старое тело удаляется).
// Вместо этого создаем новый MethodVisitor для генерации нового тела.
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
generateNewBody(mv);
// Возвращаем null, так как мы уже создали метод через cv.visitMethod
return null;
}
// Для всех остальных методов - просто передаем их дальше без изменений
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
private void generateNewBody(MethodVisitor mv) {
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello from the new invoke method!");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// Код для: Integer updateId = update.getUpdateId();
// `update` - это первый аргумент, поэтому он в локальной переменной 1 (0 это `this`)
mv.visitVarInsn(Opcodes.ALOAD, 1);
// Предполагаем, что getUpdateId() возвращает Integer. Если он возвращает int, нужно использовать valueOf.
// Для примера, пусть он возвращает Integer.
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, EventHandlersProcessor.getFullParameterName("Update"),
"getUpdateId",
"()Ljava/lang/Long;", false);
// Сохраняем результат в локальную переменную 2
mv.visitVarInsn(Opcodes.ASTORE, 2);
// Код для: System.out.println("Update ID: " + updateId);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// Создаем новый StringBuilder для конкатенации строк
mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("Update ID: ");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// Загружаем updateId из локальной переменной 2
mv.visitVarInsn(Opcodes.ALOAD, 2);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// Завершаем метод (поскольку он void, используем RETURN)
mv.visitInsn(Opcodes.RETURN);
// Указываем максимальный размер стека и количество локальных переменных.
// ASM может вычислить это за нас, если использовать ClassWriter.COMPUTE_FRAMES
mv.visitMaxs(0, 0); // Значения игнорируются при COMPUTE_FRAMES
mv.visitEnd();
}
}
}

View File

@@ -0,0 +1,63 @@
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import java.util.Map;
public final class Util {
private Util() {
}
public static String checkParameter(String simpleAnnotationName, String simpleParameterName) {
String reqParameter = switch (simpleAnnotationName) {
case "OnMessage", "BotCommand" -> "Message";
case "OnBusinessConnection" -> "BusinessConnection";
case "OnCallbackQuery" -> "CallbackQuery";
case "OnChatBoostRemoved" -> "ChatBoostRemoved";
case "OnChatBoostUpdated" -> "ChatBoostUpdated";
case "OnChatJoinRequest" -> "ChatJoinRequest";
case "OnChatMemberUpdated" -> "ChatMemberUpdated";
case "OnChosenInlineResult" -> "ChosenInlineResult";
case "OnInlineQuery" -> "InlineQuery";
case "OnPaidMediaPurchased" -> "PaidMediaPurchased";
case "OnPoll" -> "Poll";
case "OnPollAnswer" -> "PollAnswer";
case "OnPreCheckoutQuery" -> "PreCheckoutQuery";
case "OnShippingQuery" -> "ShippingQuery";
case "OnUpdate" -> "Update";
default -> throw new IllegalStateException("Unexpected value: " + simpleAnnotationName);
};
return reqParameter.equals(simpleParameterName) ? "" : reqParameter;
}
public static String getParameterClassName(TypeMirror typeMirror) {
String methodParameterName = "unknown (probably primitive type)";
if (typeMirror instanceof DeclaredType declaredType) {
Element typeElement = declaredType.asElement();
methodParameterName = typeElement.getSimpleName().toString();
}
return methodParameterName;
}
public static String getAnnotationMirrorName(AnnotationMirror mirror) {
return mirror.getAnnotationType().asElement().getSimpleName().toString();
}
public static AnnotationValue getAnnotationValue(AnnotationMirror annotationMirror, String attributeName) {
Map<? extends ExecutableElement, ? extends AnnotationValue> elementValues = annotationMirror.getElementValues();
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : elementValues.entrySet()) {
if (entry.getKey().getSimpleName().toString().equals(attributeName)) {
return entry.getValue();
}
}
return null;
}
}

View File

@@ -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) {

View File

@@ -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'
}

View File

@@ -0,0 +1,9 @@
package hdvtdev.telegram.core;
import hdvtdev.telegram.core.objects.Update;
public interface HandlersModule {
void dispatch(String botId, Update update);
}

View File

@@ -1,7 +0,0 @@
package hdvtdev.telegram.core;
import java.lang.reflect.Method;
public record InvokeMethod(Method method, Class<?> parameterType) {
}

View File

@@ -0,0 +1,8 @@
package hdvtdev.telegram.core;
public interface JsonModule {
<T> T fromJson(String json, Class<T> type);
String toJson(Object object);
}

View File

@@ -0,0 +1,9 @@
package hdvtdev.telegram.core.annotations;
public @interface TelegramAPI {
String since();
}

View File

@@ -0,0 +1,4 @@
package hdvtdev.telegram.core.objects;
public interface GeneralObject {
}

View File

@@ -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;

View File

@@ -0,0 +1,15 @@
plugins {
id 'java'
}
group = 'com.github.hdvtdev'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
}

View File

@@ -0,0 +1,17 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BotCommand {
String name() default "";
String description();
String botId() default "primary";
}

View File

@@ -0,0 +1,20 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnBusinessConnection {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,25 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnCallbackQuery {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
HAS_MESSAGE,
HAS_INLINE_MESSAGE_ID,
HAS_CHAT_INSTANCE,
HAS_DATA,
HAS_GAME_SHORT_NAME
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnChatBoostRemoved {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnChatBoostUpdated {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnChatJoinRequest {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnChatMemberUpdated {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnChosenInlineResult {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnInlineQuery {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,65 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnMessage {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
HAS_MESSAGE_THREAD_ID,
HAS_SENDER_CHAT,
HAS_SENDER_BOOST_COUNT,
HAS_SENDER_BUSINESS_BOT,
HAS_BUSINESS_CONNECTION_ID,
HAS_FORWARD_ORIGIN,
HAS_REPLY_TO_MESSAGE,
HAS_EXTERNAL_REPLY,
HAS_QUOTE,
HAS_REPLY_TO_STORY,
HAS_VIA_BOT,
HAS_EDIT_DATE,
HAS_MEDIA_GROUP_ID,
HAS_AUTHOR_SIGNATURE,
HAS_TEXT,
HAS_ENTITIES,
HAS_LINK_PREVIEW_OPTIONS,
HAS_EFFECT_ID,
HAS_ANIMATION,
HAS_AUDIO,
HAS_DOCUMENT,
HAS_PAID_MEDIA_INFO,
HAS_PHOTO,
HAS_STICKER,
HAS_STORY,
HAS_VIDEO,
HAS_VIDEO_NOTE,
HAS_VOICE,
HAS_CAPTION,
HAS_CAPTION_ENTITIES,
HAS_CONTACT,
HAS_DICE,
HAS_GAME,
HAS_POLL,
HAS_VENUE,
HAS_LOCATION,
HAS_NEW_CHAT_MEMBERS,
HAS_LEFT_CHAT_MEMBER,
HAS_NEW_CHAT_TITLE,
HAS_NEW_CHAT_PHOTO,
HAS_MESSAGE_AUTO_DELETE_TIMER_CHANGED,
HAS_MIGRATE_TO_CHAT_ID,
HAS_MIGRATE_FROM_CHAT_ID
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnPaidMediaPurchased {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnPoll {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnPollAnswer {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnPreCheckoutQuery {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,21 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnShippingQuery {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,23 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnUpdate {
String botId() default "primary";
Filter[] filters() default {};
enum Filter {
}
}

View File

@@ -0,0 +1,15 @@
package hdvtdev.telegram.handler.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface TelegramBotInstance {
String id() default "primary";
boolean primary();
}

View File

@@ -0,0 +1,18 @@
plugins {
id 'java'
}
group = 'hdvtdev'
version = '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation project(':core')
//annotationProcessor 'com.fasterxml.jackson.core:jackson-annotations:2.18.3'
annotationProcessor project(':annotation-processor')
implementation project(":event-handlers-annotations")
implementation 'org.jetbrains:annotations:26.0.2-1'
}

View File

@@ -0,0 +1,18 @@
package models;
import hdvtdev.telegram.core.HandlersModule;
import hdvtdev.telegram.core.objects.Update;
public final class Handlers implements HandlersModule {
private Handlers() {
}
@Override
public void dispatch(String botId, Update update) {
}
}

View File

@@ -10,15 +10,12 @@ 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:5.2.1'
implementation(project(":core"))
}
tasks.register('fat') {
jar {
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
}

View File

@@ -4,10 +4,9 @@ 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.exceptions.TelegramApiException;
import hdvtdev.telegram.core.exceptions.TelegramApiNetworkException;
import hdvtdev.telegram.core.exceptions.TelegramMethodParsingException;
@@ -22,16 +21,13 @@ 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;
import java.util.function.Consumer;
public class OkHttpTelegramBot implements TelegramBot {
@@ -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 Consumer<Update> 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<? extends UpdateConsumer> updateConsumerClass = builder.updateConsumer == null ? UpdateConsumer.class : builder.updateConsumer.getClass();
Map<Class<?>, Map<String, InvokeMethod>> 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<Update> 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.accept(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);
}

View File

@@ -0,0 +1,9 @@
module longpolling.okhttp {
exports hdvtdev.telegram.longpolling.okhttp;
requires core;
requires okhttp3;
requires com.fasterxml.jackson.core;
requires com.fasterxml.jackson.databind;
}

View File

@@ -2,3 +2,8 @@ rootProject.name = 'TeleJ'
include 'core'
include 'longpolling-okhttp'
include 'test'
include 'event-handlers'
include 'annotation-processor'
include 'event-handlers-annotations'