Compare commits

...

2 Commits

Author SHA1 Message Date
hdvt
b8615d3b90 migration 2025-11-03 21:20:12 +03:00
hdvt
15f77a945a migration 2025-11-03 21:17:46 +03:00
37 changed files with 1024 additions and 87 deletions

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,108 @@
import org.objectweb.asm.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class HandlerWriter {
public static void write(String body) throws IOException {
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 { plugins {
id 'java' id 'java'
id 'application'
} }
group = 'com.github.hdvtdev' group = 'com.github.hdvtdev'
@@ -11,7 +12,16 @@ repositories {
dependencies { dependencies {
implementation(project(":core")) implementation(project(":core"))
annotationProcessor(project(":annotation-processor"))
implementation(project(":longpolling-okhttp")) implementation(project(":longpolling-okhttp"))
implementation(project(":event-handlers"))
implementation(project(":event-handlers-annotations"))
}
application {
mainClass = "Main"
} }
tasks.register('fat', Jar) { tasks.register('fat', Jar) {

View File

@@ -11,6 +11,12 @@ repositories {
} }
dependencies { 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

@@ -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,10 @@
package hdvtdev.telegram.core;
import hdvtdev.telegram.core.objects.Update;
@FunctionalInterface
public interface UpdateExecutor {
void execute(Update update);
}

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 { module core {
requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.databind;
requires org.jetbrains.annotations;
exports hdvtdev.telegram.core.exceptions; exports hdvtdev.telegram.core.exceptions;
exports hdvtdev.telegram.core.objects.command; exports hdvtdev.telegram.core.objects.command;
exports hdvtdev.telegram.core.annotaions;
exports hdvtdev.telegram.core.methods; exports hdvtdev.telegram.core.methods;
exports hdvtdev.telegram.core; exports hdvtdev.telegram.core;
exports hdvtdev.telegram.core.objects; 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,14 @@ repositories {
} }
dependencies { dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.3' implementation platform('com.fasterxml.jackson:jackson-bom:2.18.3')
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
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")) implementation(project(":core"))
} }
tasks.register('fat') {
jar {
from { configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
}

View File

@@ -4,10 +4,10 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; 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.TelegramBot;
import hdvtdev.telegram.core.UpdateConsumer; 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.TelegramApiException;
import hdvtdev.telegram.core.exceptions.TelegramApiNetworkException; import hdvtdev.telegram.core.exceptions.TelegramApiNetworkException;
import hdvtdev.telegram.core.exceptions.TelegramMethodParsingException; import hdvtdev.telegram.core.exceptions.TelegramMethodParsingException;
@@ -22,14 +22,10 @@ import okhttp3.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.InvocationTargetException; 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.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@@ -40,24 +36,10 @@ public class OkHttpTelegramBot implements TelegramBot {
private final String TELEGRAM_FILE_API_URL; private final String TELEGRAM_FILE_API_URL;
private final ObjectMapper json; private final ObjectMapper json;
static { private ScheduledExecutorService scheduler;
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();
if (responseCode != 200) { private final UpdateExecutor updateExecutor;
throw new TelegramApiNetworkException("Telegram API is unreachable. Response code: " + responseCode); private final AtomicLong lastUpdateId = new AtomicLong(0);
}
} catch (IOException e) {
throw new TelegramApiNetworkException("Error checking Telegram API connectivity.", e);
}
}
private ExecutorService thread;
private AtomicLong lastUpdateId;
private int updateLimit = 10; private int updateLimit = 10;
private int updateTimeout = 25; private int updateTimeout = 25;
private final OkHttpClient client = buildOkHttpClient(); private final OkHttpClient client = buildOkHttpClient();
@@ -68,6 +50,7 @@ public class OkHttpTelegramBot implements TelegramBot {
dispatcher.setMaxRequestsPerHost(100); dispatcher.setMaxRequestsPerHost(100);
return new OkHttpClient.Builder() return new OkHttpClient.Builder()
.dispatcher(dispatcher) .dispatcher(dispatcher)
.connectionPool(new ConnectionPool( .connectionPool(new ConnectionPool(
100, 100,
@@ -78,14 +61,37 @@ public class OkHttpTelegramBot implements TelegramBot {
.writeTimeout(updateTimeout, TimeUnit.SECONDS) .writeTimeout(updateTimeout, TimeUnit.SECONDS)
.connectTimeout(updateTimeout, TimeUnit.SECONDS) .connectTimeout(updateTimeout, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) .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(); .build();
} }
private UpdateConsumer updateConsumer; private UpdateConsumer updateConsumer;
private boolean enableHandlers = false;
public OkHttpTelegramBot(String token) { public OkHttpTelegramBot(String token) {
this.json = new ObjectMapper(); 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_API_URL = "https://api.telegram.org/bot" + token + "/";
this.TELEGRAM_FILE_API_URL = "https://api.telegram.org/file/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) { private OkHttpTelegramBot(Builder builder) {
updateLimit = builder.updateLimit; updateLimit = builder.updateLimit;
updateTimeout = builder.updateTimeout; updateTimeout = builder.updateTimeout;
enableHandlers = builder.enableHandlers;
json = builder.objectMapper == null ? new ObjectMapper() : builder.objectMapper; json = builder.objectMapper == null ? new ObjectMapper() : builder.objectMapper;
/* ExecutorService pool = builder.pool;
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));
}
*/ 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_API_URL = "https://api.telegram.org/bot" + builder.token + "/";
this.TELEGRAM_FILE_API_URL = "https://api.telegram.org/file/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);
} }
/** @Override
* Enables a long polling update consumer. If {@link #enableHandlers} is {@code true}, public void start(UpdateConsumer updateConsumer) throws IllegalStateException {
* the specified handlers will be invoked for each received update. if (scheduler != null && !scheduler.isShutdown()) {
* throw new IllegalStateException("Long polling is already running. You must stop it first.");
* @param updateConsumer class that implements {@code UpdateConsumer} }
* @throws IllegalStateException if an {@code UpdateConsumer} is already defined
* @see #enableHandlers
* @since 0.0.1
*/ boolean moduleEnabled = true;
private void setUpdateConsumer(UpdateConsumer updateConsumer) throws IllegalStateException {
if (thread != null) throw new IllegalStateException("Update Consumer is already defined. You must first stop the previous"); try {
this.updateConsumer = updateConsumer; Class<?> handlersModule = Class.forName("Handlers");
this.lastUpdateId = new AtomicLong(0); HandlersModule module = (HandlersModule) handlersModule.getDeclaredConstructor().newInstance();
thread = Executors.newSingleThreadExecutor(); //this.updateConsumer = module.enable(updateConsumer);
thread.execute(this::getUpdates); } 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() { private void getUpdates() {
List<Update> updates = List.of(awaitExecute(new GetUpdates(lastUpdateId.get() + 1, updateLimit, updateTimeout))); List<Update> updates = List.of(awaitExecute(new GetUpdates(lastUpdateId.get() + 1, updateLimit, updateTimeout)));
try {
if (!updates.isEmpty()) { if (!updates.isEmpty()) {
if (updateConsumer != null) CompletableFuture.runAsync(() -> updateConsumer.onUpdates(updates)); for (Update update : updates) {
lastUpdateId.set(updates.getLast().updateId()); updateExecutor.execute(update);
} }
} finally { lastUpdateId.set(updates.getLast().updateId());
if (!thread.isShutdown()) getUpdates();
} }
} }
@Override @Override
public void shutdown() { public void shutdown() {
this.thread.close(); stop();
} }
@Override @Override
@@ -151,7 +172,7 @@ public class OkHttpTelegramBot implements TelegramBot {
Request.Builder request = new Request.Builder() Request.Builder request = new Request.Builder()
.url(TELEGRAM_API_URL + telegramApiMethod.getMethodName()); .url(TELEGRAM_API_URL + telegramApiMethod.getMethodName());
if (body == null) { if (body == null) {
if (telegramApiMethod.getClass().isAnnotationPresent(Jsonable.class)) { if (telegramApiMethod.isJsonable()) {
try { try {
request.post(RequestBody.create(json.writeValueAsString(telegramApiMethod), MediaType.get("application/json; charset=utf-8"))); request.post(RequestBody.create(json.writeValueAsString(telegramApiMethod), MediaType.get("application/json; charset=utf-8")));
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
@@ -168,24 +189,24 @@ public class OkHttpTelegramBot implements TelegramBot {
} }
try (Response response = client.newCall(request.build()).execute()) { 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()) { if (response.isSuccessful()) {
JsonNode rootNode = json.readTree(responseBody); JsonNode rootNode = json.readTree(responseBodyString);
JsonNode resultNode = rootNode.path("result"); JsonNode resultNode = rootNode.path("result");
return json.treeToValue(resultNode, telegramApiMethod.getResponseClass()); return json.treeToValue(resultNode, telegramApiMethod.getResponseClass());
} else { } else {
throw new TelegramApiException(json.readValue(responseBody, TelegramApiException.ErrorResponse.class)); throw new TelegramApiException(json.readValue(responseBodyString, TelegramApiException.ErrorResponse.class));
} }
} catch (IOException e) { } catch (IOException e) {
throw new TelegramApiNetworkException(e); throw new TelegramApiNetworkException(e);
} }
} }
private File getFile(TelegramFile telegramFile, Path targetDirectory) { private File getFile(TelegramFile telegramFile, Path targetDirectory) {
try (Response response = client.newCall(new Request.Builder().url(TELEGRAM_FILE_API_URL + telegramFile.filePath()).build()).execute()) { 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()) if (!response.isSuccessful())
throw new TelegramApiException(json.readValue(responseBody.string(), TelegramApiException.ErrorResponse.class)); throw new TelegramApiException(json.readValue(responseBody.string(), TelegramApiException.ErrorResponse.class));
Path filePath = Files.isDirectory(targetDirectory) ? targetDirectory.resolve(Path.of(telegramFile.filePath()).getFileName()) : targetDirectory; 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 { public static final class Builder {
private int updateLimit = 10; private int updateLimit = 10;
private int updateTimeout = 25; private int updateTimeout = 25;
private boolean enableHandlers = false;
private boolean enableScan = false;
private final String token; private final String token;
private UpdateConsumer updateConsumer; private UpdateConsumer updateConsumer;
private ObjectMapper objectMapper; private ObjectMapper objectMapper;
private ExecutorService pool;
public Builder(String token) { public Builder(String token) {
this.token = token; this.token = token;
} }
public Builder threadPool(ExecutorService pool) {
this.pool = pool;
return this;
}
public Builder objectMapper(ObjectMapper objectMapper) { public Builder objectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
return this; return this;
@@ -233,17 +259,6 @@ public class OkHttpTelegramBot implements TelegramBot {
return this; 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() { public OkHttpTelegramBot build() {
return new OkHttpTelegramBot(this); 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 'core'
include 'longpolling-okhttp' include 'longpolling-okhttp'
include 'test'
include 'event-handlers'
include 'annotation-processor'
include 'event-handlers-annotations'