From 8906d4fba43fd147b3822e80df446a764ac8a1f1 Mon Sep 17 00:00:00 2001 From: mrMigles Date: Fri, 10 Jul 2026 00:59:10 +0500 Subject: [PATCH] refactor: Clean up and enhance bot configuration and error handling - Removed unused proxy configuration and related code from BotConfiguration. - Updated error handling in MongoDataHelper and EducationCache to log exceptions. - Introduced ChatAdminService for managing chat administrators, improving code reuse. - Refactored TelegramBot to use ExecutorService for better thread management. - Enhanced inline query handling with timeout management. - Removed deprecated UserAccessInfo class and related methods. - Updated various processors to utilize ChatAdminService for admin checks. - Improved request handling with URL validation in Request class. This commit streamlines the bot's configuration and enhances error handling across multiple components. --- .mvn/toolchains.xml | 12 -- Dockerfile | 2 +- pom.xml | 16 --- .../botplatform/config/BotConfiguration.java | 75 ++-------- .../core/data/MongoDataHelper.java | 5 + .../core/education/EducationCache.java | 6 +- .../botplatform/core/entity/JSettings.java | 21 --- .../core/entity/UserAccessInfo.java | 37 ----- .../scripting/DefaultShellRules.java | 19 ++- .../botplatform/scripting/util/Request.java | 47 +++++- .../AnonymousChatTokenSecurityFilter.java | 11 +- ...AuthorizationServerSecurityConfigurer.java | 11 -- .../telegram/ChatAdminService.java | 22 +++ .../botplatform/telegram/InlineWorker.java | 2 +- .../botplatform/telegram/TelegramBot.java | 80 +++++++---- .../telegram/TelegramMessageEntity.java | 8 +- .../processor/RemoveLastMessageProcessor.java | 10 +- .../processor/ScriptManagerProcessor.java | 23 ++- .../processor/VoteKickMessageProcessor.java | 16 +-- .../botplatform/web/CommonController.java | 7 - src/main/resources/application.yml | 6 +- .../BotPlatformApplicationTests.java | 40 ------ .../scripting/GroovySandboxSecurityTest.java | 134 ++++++++++++++++++ 23 files changed, 331 insertions(+), 279 deletions(-) delete mode 100644 .mvn/toolchains.xml delete mode 100644 src/main/java/ru/holyway/botplatform/core/entity/UserAccessInfo.java delete mode 100644 src/main/java/ru/holyway/botplatform/security/BotAuthorizationServerSecurityConfigurer.java create mode 100644 src/main/java/ru/holyway/botplatform/telegram/ChatAdminService.java delete mode 100644 src/test/java/ru/holyway/botplatform/BotPlatformApplicationTests.java create mode 100644 src/test/java/ru/holyway/botplatform/scripting/GroovySandboxSecurityTest.java diff --git a/.mvn/toolchains.xml b/.mvn/toolchains.xml deleted file mode 100644 index 3e4b686..0000000 --- a/.mvn/toolchains.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - jdk - - 21 - - - /opt/java/openjdk - - - diff --git a/Dockerfile b/Dockerfile index 24fcaac..1e715f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,4 +19,4 @@ RUN apk update \ && apk add --no-cache ca-certificates fontconfig ttf-dejavu \ && update-ca-certificates \ && rm -rf /var/cache/apk/* -ENTRYPOINT ["java", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", "--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.net=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"] +ENTRYPOINT ["java", "--add-opens=java.base/java.lang=ALL-UNNAMED", "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED", "--add-opens=java.base/java.lang.reflect=ALL-UNNAMED", "--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.net=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"] diff --git a/pom.xml b/pom.xml index cdc27a0..70a8b04 100644 --- a/pom.xml +++ b/pom.xml @@ -266,20 +266,4 @@ - - - - - - - - - - - - - - - - diff --git a/src/main/java/ru/holyway/botplatform/config/BotConfiguration.java b/src/main/java/ru/holyway/botplatform/config/BotConfiguration.java index e002636..6f8870b 100644 --- a/src/main/java/ru/holyway/botplatform/config/BotConfiguration.java +++ b/src/main/java/ru/holyway/botplatform/config/BotConfiguration.java @@ -1,12 +1,11 @@ package ru.holyway.botplatform.config; -import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; -import org.apache.hc.core5.ssl.SSLContexts; -import org.apache.hc.core5.ssl.TrustStrategy; +import org.apache.hc.core5.util.Timeout; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; @@ -32,13 +31,6 @@ import ru.holyway.botplatform.security.AnonymousChatTokenSecurityFilter; import ru.holyway.botplatform.telegram.TelegramBot; -import javax.net.ssl.SSLContext; -import java.net.Authenticator; -import java.net.PasswordAuthentication; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -49,18 +41,6 @@ public class BotConfiguration { @Value("${bot.config.datatype}") private String dataType; - @Value("${proxy.config.host}") - private String proxyHost; - - @Value("${proxy.config.port}") - private String proxyPort; - - @Value("${proxy.config.user}") - private String proxyUser; - - @Value("${proxy.config.pass}") - private String proxyPass; - @Value("${instaprovider.url}") private String instaproviderUrl; @@ -116,72 +96,45 @@ public TaskScheduler scriptScheduler() { return new ConcurrentTaskScheduler(); } + private static final int CONNECT_TIMEOUT_MS = 10_000; + private static final int READ_TIMEOUT_MS = 30_000; + @Bean @Primary - public RestTemplate restTemplate() - throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException { + public RestTemplate restTemplate() { HttpComponentsClientHttpRequestFactory requestFactory = buildRequestFactory(); return new RestTemplate(requestFactory); } @NotNull - private HttpComponentsClientHttpRequestFactory buildRequestFactory() - throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException { - TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true; - - SSLContext sslContext = SSLContexts.custom() - .loadTrustMaterial(null, acceptingTrustStrategy) + private HttpComponentsClientHttpRequestFactory buildRequestFactory() { + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.ofMilliseconds(CONNECT_TIMEOUT_MS)) + .setResponseTimeout(Timeout.ofMilliseconds(READ_TIMEOUT_MS)) .build(); CloseableHttpClient httpClient = HttpClients.custom() + .setDefaultRequestConfig(requestConfig) .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create() - .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() - .setSslContext(sslContext) - .build()) + .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create().build()) .build()) .build(); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(httpClient); + requestFactory.setConnectTimeout(CONNECT_TIMEOUT_MS); return requestFactory; } @Bean("instaproviderTemplate") - public RestTemplate instaproviderTemplate() - throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + public RestTemplate instaproviderTemplate() { HttpComponentsClientHttpRequestFactory requestFactory = buildRequestFactory(); RestTemplate template = new RestTemplate(requestFactory); template.setUriTemplateHandler(new DefaultUriBuilderFactory(instaproviderUrl)); return template; } - private void setProxy(final String host, final String port, final String user, - final String pass) { - System.setProperty("socksProxyHost", host); - System.setProperty("socksProxyPort", port); - if (StringUtils.isNotEmpty(user) && StringUtils.isNotEmpty(pass)) { - System.setProperty("java.net.socks.username", user); - System.setProperty("java.net.socks.password", pass); - } - - Authenticator.setDefault(new ProxyAuth(user, pass)); - } - - public static class ProxyAuth extends Authenticator { - - private PasswordAuthentication auth; - - private ProxyAuth(String user, String password) { - auth = new PasswordAuthentication(user, - password == null ? new char[]{} : password.toCharArray()); - } - - protected PasswordAuthentication getPasswordAuthentication() { - return auth; - } - } - @Bean public RetryTemplate retryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); diff --git a/src/main/java/ru/holyway/botplatform/core/data/MongoDataHelper.java b/src/main/java/ru/holyway/botplatform/core/data/MongoDataHelper.java index 23ef9e3..44a2cd5 100644 --- a/src/main/java/ru/holyway/botplatform/core/data/MongoDataHelper.java +++ b/src/main/java/ru/holyway/botplatform/core/data/MongoDataHelper.java @@ -1,5 +1,7 @@ package ru.holyway.botplatform.core.data; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import ru.holyway.botplatform.core.entity.Chat; @@ -15,6 +17,8 @@ */ public class MongoDataHelper implements DataHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(MongoDataHelper.class); + private JSettings settings; @Autowired @@ -61,6 +65,7 @@ public List getSimple() { try { return simpleRepository.findById("1").map(s -> s.dictionary).orElse(new ArrayList<>()); } catch (Exception e) { + LOGGER.error("Error loading simple dictionary", e); return new ArrayList<>(); } } diff --git a/src/main/java/ru/holyway/botplatform/core/education/EducationCache.java b/src/main/java/ru/holyway/botplatform/core/education/EducationCache.java index 5cc4317..cf8f118 100644 --- a/src/main/java/ru/holyway/botplatform/core/education/EducationCache.java +++ b/src/main/java/ru/holyway/botplatform/core/education/EducationCache.java @@ -51,15 +51,15 @@ public synchronized void init() { list2Easy.clear(); dictionarySize.clear(); - Map> learnWords = null; + Map> learnWords; - List simpleWords = null; + List simpleWords; try { learnWords = dataHelper.getLearn(); simpleWords = dataHelper.getSimple(); - } catch (Exception e) { LOGGER.error("Error loading learning data", e); + return; } for (Map.Entry> line : learnWords.entrySet()) { final List> tokenizedAnswers = new ArrayList<>(); diff --git a/src/main/java/ru/holyway/botplatform/core/entity/JSettings.java b/src/main/java/ru/holyway/botplatform/core/entity/JSettings.java index 23ab0eb..0dc3553 100644 --- a/src/main/java/ru/holyway/botplatform/core/entity/JSettings.java +++ b/src/main/java/ru/holyway/botplatform/core/entity/JSettings.java @@ -10,7 +10,6 @@ */ public class JSettings { - private final static Map userTokens = new ConcurrentHashMap<>(); @Id public String id; private Set muteChats; @@ -170,26 +169,6 @@ public String generateNewToken(final String chatId) { return token; } - public String getUserToken(final String chatId, final String login, final String userName) { - for (Map.Entry userAccessInfo : userTokens.entrySet()) { - if (userAccessInfo.getValue().getChatId().equals(chatId) && userAccessInfo.getValue() - .getUserLogin().equals(login)) { - if (userAccessInfo.getValue().getExpirationTime() > System.currentTimeMillis()) { - return userAccessInfo.getKey(); - } - } - } - - final String token = UUID.randomUUID().toString().replace("-", ""); - userTokens.put(token, new UserAccessInfo(userName, login, chatId)); - - return token; - } - - public UserAccessInfo getUserAccessInfo(final String token) { - return userTokens.get(token); - } - public Set getMuteChats() { return muteChats; } diff --git a/src/main/java/ru/holyway/botplatform/core/entity/UserAccessInfo.java b/src/main/java/ru/holyway/botplatform/core/entity/UserAccessInfo.java deleted file mode 100644 index 8dbeac5..0000000 --- a/src/main/java/ru/holyway/botplatform/core/entity/UserAccessInfo.java +++ /dev/null @@ -1,37 +0,0 @@ -package ru.holyway.botplatform.core.entity; - -import java.util.concurrent.TimeUnit; - -/** - * Created by seiv0814 on 10-11-17. - */ -public class UserAccessInfo { - - private String userName; - private String userLogin; - private String chatId; - private Long expirationTime; - - public String getUserName() { - return userName; - } - - public String getUserLogin() { - return userLogin; - } - - public String getChatId() { - return chatId; - } - - public UserAccessInfo(String userName, String userLogin, String chatId) { - this.userName = userName; - this.userLogin = userLogin; - this.chatId = chatId; - this.expirationTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); - } - - public Long getExpirationTime() { - return expirationTime; - } -} diff --git a/src/main/java/ru/holyway/botplatform/scripting/DefaultShellRules.java b/src/main/java/ru/holyway/botplatform/scripting/DefaultShellRules.java index d2be6f5..e3aafde 100644 --- a/src/main/java/ru/holyway/botplatform/scripting/DefaultShellRules.java +++ b/src/main/java/ru/holyway/botplatform/scripting/DefaultShellRules.java @@ -12,7 +12,6 @@ import ru.holyway.botplatform.scripting.util.*; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.UUID; @@ -27,7 +26,9 @@ public final class DefaultShellRules { BytecodeSequence.class); public static final String[] starImportsWhiteArray = - new String[]{"ru.holyway"}; + new String[]{ + "ru.holyway.botplatform.scripting.entity", + "ru.holyway.botplatform.scripting.util"}; public static final List starImportsWhitelist = Arrays.asList(starImportsWhiteArray); @@ -72,9 +73,19 @@ public final class DefaultShellRules { ParseMode.class.getName()); public static final List receiversBlackList = - Collections.singletonList(Thread.class.getName()); + Arrays.asList( + Thread.class.getName(), + Runtime.class.getName(), + ProcessBuilder.class.getName(), + System.class.getName(), + Class.class.getName(), + ClassLoader.class.getName()); public static List methodsBlacklist = Arrays.asList("getClass", "class", "forName", - "wait", "notify", "notifyAll", "invokeMethod", "finalize", "sleep", "exit"); + "wait", "notify", "notifyAll", "invokeMethod", "finalize", "sleep", "exit", + "exec", "getRuntime", "halt", "load", "loadLibrary", "addShutdownHook", + "newInstance", "getMethod", "getDeclaredMethod", "getField", "getDeclaredField", + "setAccessible", "getMetaClass", "setMetaClass", "define", "defineClass", + "evaluate", "getClassLoader"); } diff --git a/src/main/java/ru/holyway/botplatform/scripting/util/Request.java b/src/main/java/ru/holyway/botplatform/scripting/util/Request.java index 8daa1b7..5d5f1e3 100644 --- a/src/main/java/ru/holyway/botplatform/scripting/util/Request.java +++ b/src/main/java/ru/holyway/botplatform/scripting/util/Request.java @@ -1,11 +1,12 @@ package ru.holyway.botplatform.scripting.util; import com.jayway.jsonpath.JsonPath; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; -import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder; import org.apache.hc.core5.ssl.SSLContexts; +import org.apache.hc.core5.util.Timeout; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -13,7 +14,6 @@ import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -26,13 +26,14 @@ import javax.net.ssl.SSLContext; import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -43,6 +44,9 @@ public class Request { private static final Logger LOGGER = LoggerFactory.getLogger(Request.class); + private static final int CONNECT_TIMEOUT_MS = 10_000; + private static final int READ_TIMEOUT_MS = 30_000; + static { try { SSLContext sslContext = SSLContexts.createDefault(); @@ -50,21 +54,26 @@ public class Request { var connectionManager = PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(SSLConnectionSocketFactoryBuilder.create() .setSslContext(sslContext) - .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) .setTlsVersions("TLSv1.2", "TLSv1.3") .build()) .build(); + RequestConfig requestConfig = RequestConfig.custom() + .setConnectionRequestTimeout(Timeout.ofMilliseconds(CONNECT_TIMEOUT_MS)) + .setResponseTimeout(Timeout.ofMilliseconds(READ_TIMEOUT_MS)) + .build(); + var httpClient = HttpClientBuilder.create() .disableCookieManagement() .useSystemProperties() .setConnectionManager(connectionManager) + .setDefaultRequestConfig(requestConfig) .build(); HttpComponentsClientHttpRequestWithBodyFactory factory = new HttpComponentsClientHttpRequestWithBodyFactory(); factory.setHttpClient(httpClient); - factory.setConnectTimeout(5 * 60 * 1000); + factory.setConnectTimeout(CONNECT_TIMEOUT_MS); restTemplate = new RestTemplate(); restTemplate.setRequestFactory(factory); @@ -190,6 +199,7 @@ private Function performRequest() { if (isLast && scriptContext.getContextValue("request") != null) { response = scriptContext.getContextValue("request"); } else { + assertUrlAllowed(url); try { response = restTemplate.exchange(url, method, httpEntity, String.class).getBody(); } catch (RestClientException e) { @@ -205,6 +215,33 @@ private Function performRequest() { }; } + private static void assertUrlAllowed(String url) { + URI uri; + try { + uri = URI.create(url); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid URL: " + url, e); + } + final String scheme = uri.getScheme(); + if (scheme == null || !(scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) { + throw new IllegalArgumentException("Only http/https URLs are allowed: " + url); + } + final String host = uri.getHost(); + if (host == null) { + throw new IllegalArgumentException("URL has no host: " + url); + } + try { + for (InetAddress address : InetAddress.getAllByName(host)) { + if (address.isLoopbackAddress() || address.isLinkLocalAddress() + || address.isAnyLocalAddress() || address.isMulticastAddress()) { + throw new IllegalArgumentException("Requests to loopback/link-local addresses are not allowed: " + url); + } + } + } catch (java.net.UnknownHostException e) { + throw new IllegalArgumentException("Cannot resolve host: " + host, e); + } + } + public TextJoiner asJson(Object jsonPath) { return TextJoiner.text(scriptContext -> { final String response = performRequest().apply(scriptContext); diff --git a/src/main/java/ru/holyway/botplatform/security/AnonymousChatTokenSecurityFilter.java b/src/main/java/ru/holyway/botplatform/security/AnonymousChatTokenSecurityFilter.java index 30efe63..aa9170c 100644 --- a/src/main/java/ru/holyway/botplatform/security/AnonymousChatTokenSecurityFilter.java +++ b/src/main/java/ru/holyway/botplatform/security/AnonymousChatTokenSecurityFilter.java @@ -9,6 +9,8 @@ import ru.holyway.botplatform.core.data.DataHelper; import jakarta.servlet.http.HttpServletRequest; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; import java.util.Collections; public class AnonymousChatTokenSecurityFilter extends AnonymousAuthenticationFilter { @@ -27,11 +29,16 @@ public AnonymousChatTokenSecurityFilter(String key) { protected Authentication createAuthentication(HttpServletRequest request) { final String token = request.getHeader("Authorization"); final String chatId = request.getParameter("chatId"); - if (StringUtils.isNotEmpty(token) && StringUtils.isNotEmpty(chatId) && dataHelper.getSettings() - .getToken(chatId).equals(token)) { + if (StringUtils.isNotEmpty(token) && StringUtils.isNotEmpty(chatId) + && constantTimeEquals(dataHelper.getSettings().getToken(chatId), token)) { return new AnonymousAuthenticationToken(key, getPrincipal(), Collections.singletonList(new SimpleGrantedAuthority("USER"))); } return super.createAuthentication(request); } + + private static boolean constantTimeEquals(String expected, String actual) { + return MessageDigest.isEqual( + expected.getBytes(StandardCharsets.UTF_8), actual.getBytes(StandardCharsets.UTF_8)); + } } diff --git a/src/main/java/ru/holyway/botplatform/security/BotAuthorizationServerSecurityConfigurer.java b/src/main/java/ru/holyway/botplatform/security/BotAuthorizationServerSecurityConfigurer.java deleted file mode 100644 index a4105c0..0000000 --- a/src/main/java/ru/holyway/botplatform/security/BotAuthorizationServerSecurityConfigurer.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.holyway.botplatform.security; - -import org.springframework.stereotype.Component; - -/** - * Created by seiv0814 on 19-10-17. - */ -@Component -public class BotAuthorizationServerSecurityConfigurer { - -} diff --git a/src/main/java/ru/holyway/botplatform/telegram/ChatAdminService.java b/src/main/java/ru/holyway/botplatform/telegram/ChatAdminService.java new file mode 100644 index 0000000..8d015b0 --- /dev/null +++ b/src/main/java/ru/holyway/botplatform/telegram/ChatAdminService.java @@ -0,0 +1,22 @@ +package ru.holyway.botplatform.telegram; + +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators; +import org.telegram.telegrambots.meta.api.objects.chatmember.ChatMember; +import org.telegram.telegrambots.meta.bots.AbsSender; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +import java.util.List; + +@Component +public class ChatAdminService { + + public List getAdministrators(AbsSender sender, Object chatId) throws TelegramApiException { + return sender.execute(GetChatAdministrators.builder().chatId(chatId.toString()).build()); + } + + public boolean isAdmin(AbsSender sender, Object chatId, Long userId) throws TelegramApiException { + return getAdministrators(sender, chatId).stream() + .anyMatch(chatMember -> chatMember.getUser().getId().equals(userId)); + } +} diff --git a/src/main/java/ru/holyway/botplatform/telegram/InlineWorker.java b/src/main/java/ru/holyway/botplatform/telegram/InlineWorker.java index 8aa49c8..c030bd7 100644 --- a/src/main/java/ru/holyway/botplatform/telegram/InlineWorker.java +++ b/src/main/java/ru/holyway/botplatform/telegram/InlineWorker.java @@ -44,7 +44,7 @@ public void run() { inlineQueryResults.add(InlineQueryResultArticle.builder().id("7").title("Курс в шапке чата c обновлением каждые 5 минут").description("script().when(every(\"5m\")).then(message(139665).edit(builder(text(\"Курс доллара: \").add(request().get(\"https://ru.investing.com/currencies/usd-rub\").asXPath(\"/html/body/div[1]/div[2]/div/div/div/main/div/div[1]/div[2]/div[1]/span\")).add(\" руб, \").add(\"Курс евро: \").add(request().get(\"https://ru.investing.com/currencies/eur-rub\").asXPath(\"/html/body/div[1]/div/div/div/div/main/div/div[1]/div[2]/div[1]/span\")).add(\" руб.\"))))").inputMessageContent(InputTextMessageContent.builder().messageText("script().when(every(\"5m\")).then(message(139665).edit(builder(text(\"Курс доллара: \").add(request().get(\"https://ru.investing.com/currencies/usd-rub\").asXPath(\"/html/body/div[1]/div/div/div/div[2]/main/div/div[1]/div[2]/div[1]/span\")).add(\" руб, \").add(\"Курс евро: \").add(request().get(\"https://ru.investing.com/currencies/eur-rub\").asXPath(\"/html/body/div[1]/div/div/div/div/main/div/div[1]/div[2]/div[1]/span\")).add(\" руб.\"))))").build()).build()); inlineQueryResults.add(InlineQueryResultArticle.builder().id("8").title("История дня: пересылка старых сообщений из чата").description("script().when(cron(\"0 4 * * *\")).then(message.send(\"История дня:\").andThen(loop(wrap(retry(message(number(random(1409,100000)).asLong()).forward())),6)))").inputMessageContent(InputTextMessageContent.builder().messageText("script().when(cron(\"0 4 * * *\")).then(message.send(\"История дня:\").andThen(loop(wrap(retry(message(number(random(1409,100000)).asLong()).forward())),6)))").build()).build()); inlineQueryResults.add(InlineQueryResultArticle.builder().id("9").title("Press F to pay respect").description("script().when(text.eqic(\"F\")).then(message.sendStickerFromSet(\"FforRespect\"))").inputMessageContent(InputTextMessageContent.builder().messageText("script().when(text.eqic(\"F\")).then(message.sendStickerFromSet(\"FforRespect\"))").build()).build()); - inlineQueryResults.add(InlineQueryResultArticle.builder().id("10").title("Игра в дартс").description("script().when(message.json.path(\"$.dice.emoji\").eq(\"\uD83C\uDFAF\")).then(message.send(text(message.user).add(\" набрал \").add(message.json.path(\"$.dice.value\")).add(\" очк.\")).andThen(condition(message.json.path(\"$.dice.value\").eq(\"6\")).then(message.reply(request().get(\"https://api.giphy.com/v1/gifs/search?api_key=ZVaHoP3egMTBDEywdwcXetMDYQFQtVxI&q=hat-off&limit=50\").asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))).otherwise(condition(message.json.path(\"$.dice.value\").eq(\"1\")).then(message.reply(request().get(\"https://api.giphy.com/v1/gifs/search?api_key=ZVaHoP3egMTBDEywdwcXetMDYQFQtVxI&q=you-suck&limit=50\").asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))))))").inputMessageContent(InputTextMessageContent.builder().messageText("script().when(message.json.path(\"$.dice.emoji\").eq(\"\uD83C\uDFAF\")).then(message.send(text(message.user).add(\" набрал \").add(message.json.path(\"$.dice.value\")).add(\" очк.\")).andThen(condition(message.json.path(\"$.dice.value\").eq(\"6\")).then(message.reply(request().get(\"https://api.giphy.com/v1/gifs/search?api_key=ZVaHoP3egMTBDEywdwcXetMDYQFQtVxI&q=hat-off&limit=50\").asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))).otherwise(condition(message.json.path(\"$.dice.value\").eq(\"1\")).then(message.reply(request().get(\"https://api.giphy.com/v1/gifs/search?api_key=ZVaHoP3egMTBDEywdwcXetMDYQFQtVxI&q=you-suck&limit=50\").asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))))))").build()).build()); + inlineQueryResults.add(InlineQueryResultArticle.builder().id("10").title("Игра в дартс").description("script().when(message.json.path(\"$.dice.emoji\").eq(\"\uD83C\uDFAF\")).then(message.send(text(message.user).add(\" набрал \").add(message.json.path(\"$.dice.value\")).add(\" очк.\")).andThen(condition(message.json.path(\"$.dice.value\").eq(\"6\")).then(message.reply(request().get(text(\"https://api.giphy.com/v1/gifs/search?api_key=\").add(secret.get(\"giphy-token\")).add(\"&q=hat-off&limit=50\")).asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))).otherwise(condition(message.json.path(\"$.dice.value\").eq(\"1\")).then(message.reply(request().get(text(\"https://api.giphy.com/v1/gifs/search?api_key=\").add(secret.get(\"giphy-token\")).add(\"&q=you-suck&limit=50\")).asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))))))").inputMessageContent(InputTextMessageContent.builder().messageText("script().when(message.json.path(\"$.dice.emoji\").eq(\"\uD83C\uDFAF\")).then(message.send(text(message.user).add(\" набрал \").add(message.json.path(\"$.dice.value\")).add(\" очк.\")).andThen(condition(message.json.path(\"$.dice.value\").eq(\"6\")).then(message.reply(request().get(text(\"https://api.giphy.com/v1/gifs/search?api_key=\").add(secret.get(\"giphy-token\")).add(\"&q=hat-off&limit=50\")).asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))).otherwise(condition(message.json.path(\"$.dice.value\").eq(\"1\")).then(message.reply(request().get(text(\"https://api.giphy.com/v1/gifs/search?api_key=\").add(secret.get(\"giphy-token\")).add(\"&q=you-suck&limit=50\")).asJson(text(\"$.data[\").add(random(0, 50)).add(\"].bitly_gif_url\")))))))").build()).build()); inlineQueryResults.add(InlineQueryResultArticle.builder().id("11").title("Fast Food Coub").description("script().when(message.text.cic(\"/coub\").or(message.hasCallback().and(message.callback.data.cic(\"coub_\")))).then(var(\"coub_link\").set(text(\"https://coub.com/view/\").add(request().get(\"https://coub.com/api/v2/timeline/explore/random?order_by=&page=1&per_page=1\").asJson(text(\"$.coubs[0].permalink\")))).andThen(condition(message.hasCallback()).then(condition(message.callback.data.eq(\"coub_like\")).then(message.edit(builder().cleanButtons())).otherwise(message.delete())).andThen(builder(var(\"coub_link\")).button(\"\uD83D\uDC4D\",\"coub_like\").button(\"\uD83D\uDC4E\",\"coub_dislike\").send())))").inputMessageContent(InputTextMessageContent.builder().messageText("script().when(message.text.cic(\"/coub\").or(message.hasCallback().and(message.callback.data.cic(\"coub_\")))).then(var(\"coub_link\").set(text(\"https://coub.com/view/\").add(request().get(\"https://coub.com/api/v2/timeline/explore/random?order_by=&page=1&per_page=1\").asJson(text(\"$.coubs[0].permalink\")))).andThen(condition(message.hasCallback()).then(condition(message.callback.data.eq(\"coub_like\")).then(message.edit(builder().cleanButtons())).otherwise(message.delete())).andThen(builder(var(\"coub_link\")).button(\"\uD83D\uDC4D\",\"coub_like\").button(\"\uD83D\uDC4E\",\"coub_dislike\").send())))").build()).build()); inlineQueryResults.add(InlineQueryResultArticle.builder().id("12").title("Инфо по сообщению").description("script().when(message.isReply().and(message.text.eq(\"/info\"))).then(message.send(reply.json))").inputMessageContent(InputTextMessageContent.builder().messageText("script().when(message.isReply().and(message.text.eq(\"/info\"))).then(message.send(reply.json))").build()).build()); try { diff --git a/src/main/java/ru/holyway/botplatform/telegram/TelegramBot.java b/src/main/java/ru/holyway/botplatform/telegram/TelegramBot.java index 378d76c..ae727ec 100644 --- a/src/main/java/ru/holyway/botplatform/telegram/TelegramBot.java +++ b/src/main/java/ru/holyway/botplatform/telegram/TelegramBot.java @@ -20,16 +20,25 @@ import ru.holyway.botplatform.telegram.processor.MessageProcessor; import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * Created by Sergey on 1/17/2017. */ public class TelegramBot extends TelegramLongPollingBot implements Bot { + private static final long ENQUEUE_OFFER_TIMEOUT_MS = 2_000; + private static final long SHUTDOWN_AWAIT_TIMEOUT_MS = 10_000; + @Value("${credential.telegram.login}") private String botName; @@ -46,18 +55,14 @@ public class TelegramBot extends TelegramLongPollingBot implements Bot { private CommonHandler commonMessageHandler; private final List> queues; - private final List consumers; + private final List consumers; private final Integer threadCount; - private BlockingQueue inlineQueryBlockingQueue; - private Thread inlineQueryWorker; + private final BlockingQueue inlineQueryBlockingQueue; + private final ExecutorService inlineQueryWorker; private static final Logger LOGGER = LoggerFactory.getLogger(TelegramBot.class); -// public TelegramBot(DefaultBotOptions options) { -// super(options); -// } - public TelegramBot(final Integer threadCount, final Integer queueSize) { queues = new ArrayList<>(threadCount); consumers = new ArrayList<>(threadCount); @@ -65,15 +70,24 @@ public TelegramBot(final Integer threadCount, final Integer queueSize) { for (int i = 0; i < threadCount; i++) { BlockingQueue queue = new LinkedBlockingDeque<>(queueSize); queues.add(queue); - Thread consumer = new Thread(new WorkerThread(queue, this)); + ExecutorService consumer = Executors.newSingleThreadExecutor(namedThreadFactory("telegram-worker-" + i)); + consumer.submit(new WorkerThread(queue, this)); consumers.add(consumer); - consumer.start(); } this.threadCount = threadCount; inlineQueryBlockingQueue = new LinkedBlockingDeque<>(queueSize); - inlineQueryWorker = new Thread(new InlineWorker(this, inlineQueryBlockingQueue)); - inlineQueryWorker.start(); + inlineQueryWorker = Executors.newSingleThreadExecutor(namedThreadFactory("telegram-inline-worker")); + inlineQueryWorker.submit(new InlineWorker(this, inlineQueryBlockingQueue)); + } + + private static ThreadFactory namedThreadFactory(String name) { + AtomicInteger count = new AtomicInteger(); + return runnable -> { + Thread thread = new Thread(runnable, name + "-" + count.incrementAndGet()); + thread.setDaemon(true); + return thread; + }; } @PostConstruct @@ -88,27 +102,28 @@ public void onUpdateReceived(Update update) { if (update.hasCallbackQuery()) { message = (Message) update.getCallbackQuery().getMessage(); } else if (update.hasInlineQuery()) { - try { - inlineQueryBlockingQueue.put(update.getInlineQuery()); - } catch (InterruptedException e) { - LOGGER.error("InterruptedException due to adding inline worker", e); - } + offerWithTimeout(inlineQueryBlockingQueue, update.getInlineQuery(), "inline query"); return; } else { return; } } int partition = partition(message.getChatId().toString(), threadCount); + BlockingQueue queue = queues.get(partition); + if (queue.remainingCapacity() < 10) { + LOGGER.error("Critical remaining capacity for chat: {}", message.getChatId()); + } + offerWithTimeout(queue, update, "chat " + message.getChatId()); + } + + private void offerWithTimeout(BlockingQueue queue, T item, String description) { try { - if (queues.get(partition).remainingCapacity() < 10) { - LOGGER.error("Critical remaining capacity: " + message.getChatId()); + if (!queue.offer(item, ENQUEUE_OFFER_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + LOGGER.error("Queue full, dropping update for {}", description); } - queues.get(partition).put(update); } catch (InterruptedException e) { - LOGGER.error("Interrupted while queuing update: ", e); + LOGGER.error("Interrupted while queuing update for {}: ", description, e); Thread.currentThread().interrupt(); - } catch (Throwable e) { - LOGGER.error("Error occurred during execution: ", e); } } @@ -135,8 +150,22 @@ public void init() { } @Override + @PreDestroy public void destroy() { + consumers.forEach(ExecutorService::shutdownNow); + inlineQueryWorker.shutdownNow(); + consumers.forEach(this::awaitTermination); + awaitTermination(inlineQueryWorker); + } + private void awaitTermination(ExecutorService executor) { + try { + if (!executor.awaitTermination(SHUTDOWN_AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + LOGGER.warn("Worker did not terminate within {} ms", SHUTDOWN_AWAIT_TIMEOUT_MS); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } @Override @@ -152,7 +181,7 @@ public void sendMessage(String text, String chatId) { } public int partition(String key, int numPartitions) { - return Math.abs(key.hashCode() % numPartitions); + return Math.floorMod(key.hashCode(), numPartitions); } private class WorkerThread implements Runnable { @@ -167,13 +196,12 @@ private WorkerThread(BlockingQueue queue, AbsSender sender) { @Override public void run() { - while (true) { + while (!Thread.currentThread().isInterrupted()) { try { - // Dequeue a chat ID from the priority queue Update update = queue.take(); onUpdateReceivedInternal(update); } catch (InterruptedException e) { - LOGGER.error("Interrupt Error occurred during execution main: ", e); + Thread.currentThread().interrupt(); } catch (Exception e) { LOGGER.error("Error occurred during execution main: ", e); } diff --git a/src/main/java/ru/holyway/botplatform/telegram/TelegramMessageEntity.java b/src/main/java/ru/holyway/botplatform/telegram/TelegramMessageEntity.java index f2e7c81..74136c9 100644 --- a/src/main/java/ru/holyway/botplatform/telegram/TelegramMessageEntity.java +++ b/src/main/java/ru/holyway/botplatform/telegram/TelegramMessageEntity.java @@ -4,6 +4,8 @@ import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import org.apache.logging.log4j.util.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.CallbackQuery; import org.telegram.telegrambots.meta.api.objects.Message; @@ -18,6 +20,8 @@ */ public class TelegramMessageEntity implements MessageEntity { + private static final Logger LOGGER = LoggerFactory.getLogger(TelegramMessageEntity.class); + private final Message message; private final CallbackQuery callbackQuery; @@ -64,8 +68,8 @@ public void reply(String text) { sendMessage.setText(text); try { sender.execute(sendMessage); - } catch (TelegramApiException telegramApiException) { - telegramApiException.printStackTrace(); + } catch (TelegramApiException e) { + LOGGER.error("Error occurred while replying to chat {}", getChatId(), e); } } diff --git a/src/main/java/ru/holyway/botplatform/telegram/processor/RemoveLastMessageProcessor.java b/src/main/java/ru/holyway/botplatform/telegram/processor/RemoveLastMessageProcessor.java index 19f1785..e84c640 100644 --- a/src/main/java/ru/holyway/botplatform/telegram/processor/RemoveLastMessageProcessor.java +++ b/src/main/java/ru/holyway/botplatform/telegram/processor/RemoveLastMessageProcessor.java @@ -3,9 +3,9 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; import org.telegram.telegrambots.meta.api.objects.CallbackQuery; @@ -13,6 +13,7 @@ import org.telegram.telegrambots.meta.api.objects.chatmember.ChatMember; import org.telegram.telegrambots.meta.bots.AbsSender; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; +import ru.holyway.botplatform.telegram.ChatAdminService; import ru.holyway.botplatform.telegram.TelegramMessageEntity; import java.util.List; @@ -23,6 +24,9 @@ public class RemoveLastMessageProcessor implements MessageProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(RemoveLastMessageProcessor.class); + @Autowired + private ChatAdminService chatAdminService; + @Override public boolean isNeedToHandle(TelegramMessageEntity messageEntity) { final String mes = messageEntity.getText(); @@ -58,8 +62,8 @@ public void process(TelegramMessageEntity messageEntity) throws TelegramApiExcep } private boolean hasGrants(TelegramMessageEntity messageEntity) throws TelegramApiException { - List chatMembers = messageEntity.getSender() - .execute(GetChatAdministrators.builder().chatId(messageEntity.getChatId()).build()); + List chatMembers = + chatAdminService.getAdministrators(messageEntity.getSender(), messageEntity.getChatId()); for (ChatMember chatMember : chatMembers) { if (chatMember.getUser().getId().equals(messageEntity.getMessage().getFrom().getId())) { if (chatMember.getStatus().equals("creator") || chatMember.getUser().getCanReadAllGroupMessages()) { diff --git a/src/main/java/ru/holyway/botplatform/telegram/processor/ScriptManagerProcessor.java b/src/main/java/ru/holyway/botplatform/telegram/processor/ScriptManagerProcessor.java index bf7eb33..277f588 100644 --- a/src/main/java/ru/holyway/botplatform/telegram/processor/ScriptManagerProcessor.java +++ b/src/main/java/ru/holyway/botplatform/telegram/processor/ScriptManagerProcessor.java @@ -7,12 +7,10 @@ import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import org.telegram.telegrambots.meta.api.methods.AnswerCallbackQuery; -import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.methods.updatingmessages.DeleteMessage; import org.telegram.telegrambots.meta.api.objects.CallbackQuery; import org.telegram.telegrambots.meta.api.objects.Message; -import org.telegram.telegrambots.meta.api.objects.chatmember.ChatMember; import org.telegram.telegrambots.meta.api.objects.replykeyboard.ForceReplyKeyboard; import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; @@ -20,13 +18,13 @@ import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import ru.holyway.botplatform.scripting.MetricCollector; import ru.holyway.botplatform.scripting.Script; +import ru.holyway.botplatform.telegram.ChatAdminService; import ru.holyway.botplatform.telegram.TelegramMessageEntity; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; @Component @Order(98) @@ -38,6 +36,9 @@ public class ScriptManagerProcessor implements MessageProcessor { @Autowired private ScriptMessageProcessor scriptMessageProcessor; + @Autowired + private ChatAdminService chatAdminService; + @Override public boolean isNeedToHandle(TelegramMessageEntity messageEntity) { return messageEntity.getMessage().hasText() && (messageEntity.getMessage().getText() @@ -54,7 +55,7 @@ public boolean isNeedToHandle(TelegramMessageEntity messageEntity) { public void process(TelegramMessageEntity messageEntity) throws TelegramApiException { if (messageEntity.getMessage().getText() .startsWith("/clear")) { - if (messageEntity.getMessage().getChat().isUserChat() || isAdmin(messageEntity.getSender(), messageEntity.getMessage().getChatId(), messageEntity.getMessage().getFrom().getId())) { + if (messageEntity.getMessage().getChat().isUserChat() || chatAdminService.isAdmin(messageEntity.getSender(), messageEntity.getMessage().getChatId(), messageEntity.getMessage().getFrom().getId())) { scriptMessageProcessor.clearScripts(messageEntity.getChatId()); messageEntity.getSender() .execute( @@ -82,7 +83,7 @@ public void process(TelegramMessageEntity messageEntity) throws TelegramApiExcep final String scriptString = messageEntity.getMessage().getReplyToMessage().getText(); final Script script = scriptMessageProcessor.getScript(messageEntity.getChatId(), scriptString); if (script != null) { - if (messageEntity.getMessage().getChat().isUserChat() || script.getOwner() == 0 || script.getOwner() == messageEntity.getMessage().getFrom().getId() || isAdmin(messageEntity.getSender(), messageEntity.getMessage().getChatId(), messageEntity.getMessage().getFrom().getId())) { + if (messageEntity.getMessage().getChat().isUserChat() || script.getOwner() == 0 || script.getOwner() == messageEntity.getMessage().getFrom().getId() || chatAdminService.isAdmin(messageEntity.getSender(), messageEntity.getMessage().getChatId(), messageEntity.getMessage().getFrom().getId())) { if (scriptMessageProcessor.removeScript(messageEntity.getChatId(), scriptString)) { messageEntity.getSender() .execute( @@ -116,7 +117,7 @@ public void process(TelegramMessageEntity messageEntity) throws TelegramApiExcep scriptMessageProcessor.dataHelper.putToSecretStorage(messageEntity.getChatId(), key, value); messageEntity.getSender() .execute( - SendMessage.builder().chatId(messageEntity.getChatId()).text("Сохранено: secret[" + key + "]=" + value).build()); + SendMessage.builder().chatId(messageEntity.getChatId()).text("Сохранено: secret[" + key + "]").build()); } else if (messageEntity.getMessage().getText() .startsWith("/logs")) { @@ -141,7 +142,7 @@ public void process(TelegramMessageEntity messageEntity) throws TelegramApiExcep .startsWith("/get")) { messageEntity.getSender() .execute( - SendMessage.builder().chatId(messageEntity.getChatId()).text(scriptMessageProcessor.dataHelper.listSecretStorage(messageEntity.getChatId()).toString()).build()); + SendMessage.builder().chatId(messageEntity.getChatId()).text("Сохранённые ключи: " + scriptMessageProcessor.dataHelper.listSecretStorage(messageEntity.getChatId()).keySet()).build()); } else if (messageEntity.getMessage().isReply() && messageEntity.getMessage().getText() .startsWith("script(")) { if (scriptMessageProcessor @@ -180,7 +181,7 @@ public void processCallBack(CallbackQuery callbackQuery, AbsSender sender) final String scriptId = StringUtils.substringAfter(callbackQuery.getData(), "script:delete:"); final Script script = scriptMessageProcessor.getScript(String.valueOf(cbMessage.getChatId()), Integer.valueOf(scriptId)); if (script != null) { - if (cbMessage.getChat().isUserChat() || script.getOwner() == 0 || script.getOwner() == callbackQuery.getFrom().getId() || isAdmin(sender, cbMessage.getChatId(), callbackQuery.getFrom().getId())) { + if (cbMessage.getChat().isUserChat() || script.getOwner() == 0 || script.getOwner() == callbackQuery.getFrom().getId() || chatAdminService.isAdmin(sender, cbMessage.getChatId(), callbackQuery.getFrom().getId())) { if (scriptMessageProcessor.removeScript( String.valueOf(cbMessage.getChatId()), Integer.valueOf(scriptId))) { sender @@ -257,10 +258,4 @@ protected Integer sendControlButtons(TelegramMessageEntity messageEntity, Intege .replyMarkup(keyboardMarkup).build()).getMessageId(); } - private boolean isAdmin(AbsSender sender, final Long chatId, final Long userId) throws TelegramApiException { - List chatMembers = sender - .execute(GetChatAdministrators.builder().chatId(chatId).build()); - List adminUsers = chatMembers.stream().map(chatMember -> chatMember.getUser().getId()).collect(Collectors.toList()); - return adminUsers.contains(userId); - } } diff --git a/src/main/java/ru/holyway/botplatform/telegram/processor/VoteKickMessageProcessor.java b/src/main/java/ru/holyway/botplatform/telegram/processor/VoteKickMessageProcessor.java index 3c52d14..e779cd2 100644 --- a/src/main/java/ru/holyway/botplatform/telegram/processor/VoteKickMessageProcessor.java +++ b/src/main/java/ru/holyway/botplatform/telegram/processor/VoteKickMessageProcessor.java @@ -3,12 +3,12 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; import org.springframework.stereotype.Component; import org.telegram.telegrambots.meta.api.methods.AnswerCallbackQuery; -import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatAdministrators; import org.telegram.telegrambots.meta.api.methods.groupadministration.GetChatMemberCount; import org.telegram.telegrambots.meta.api.methods.groupadministration.RestrictChatMember; import org.telegram.telegrambots.meta.api.methods.pinnedmessages.PinChatMessage; @@ -19,12 +19,12 @@ import org.telegram.telegrambots.meta.api.objects.ChatPermissions; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.User; -import org.telegram.telegrambots.meta.api.objects.chatmember.ChatMember; import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup; import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton; import org.telegram.telegrambots.meta.bots.AbsSender; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import ru.holyway.botplatform.core.data.ProcessorsContext; +import ru.holyway.botplatform.telegram.ChatAdminService; import ru.holyway.botplatform.telegram.TelegramMessageEntity; import java.util.*; @@ -38,6 +38,9 @@ public class VoteKickMessageProcessor implements MessageProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(VoteKickMessageProcessor.class); + @Autowired + private ChatAdminService chatAdminService; + private Map banList = new HashMap<>(); private Integer voteSize = 3; @@ -231,14 +234,7 @@ private void endVote(final String chatID, final Integer messageID, final AbsSend private boolean isAdmin(TelegramMessageEntity messageEntity, User banUser) throws TelegramApiException { - List chatMembers = messageEntity.getSender() - .execute(GetChatAdministrators.builder().chatId(messageEntity.getChatId()).build()); - for (ChatMember chatMember : chatMembers) { - if (chatMember.getUser().getId().equals(banUser.getId())) { - return true; - } - } - return false; + return chatAdminService.isAdmin(messageEntity.getSender(), messageEntity.getChatId(), banUser.getId()); } private static class BanInfo { diff --git a/src/main/java/ru/holyway/botplatform/web/CommonController.java b/src/main/java/ru/holyway/botplatform/web/CommonController.java index 6344f7d..cd0b3ff 100644 --- a/src/main/java/ru/holyway/botplatform/web/CommonController.java +++ b/src/main/java/ru/holyway/botplatform/web/CommonController.java @@ -13,7 +13,6 @@ import ru.holyway.botplatform.core.CommonHandler; import ru.holyway.botplatform.core.MessageEntity; import ru.holyway.botplatform.core.data.DataHelper; -import ru.holyway.botplatform.core.entity.UserAccessInfo; import ru.holyway.botplatform.web.entities.SimpleRequest; import ru.holyway.botplatform.web.entities.SimpleResponse; @@ -99,10 +98,4 @@ public ResponseEntity message(@RequestParam("id") String chatId, final String answer = commonHandler.generateAnswer(messageEntity); return ResponseEntity.ok(answer); } - - @PreAuthorize("permitAll()") - @RequestMapping(value = "/introspect", method = RequestMethod.POST) - public ResponseEntity introspect(@RequestBody String token) { - return ResponseEntity.ok(dataHelper.getSettings().getUserAccessInfo(token)); - } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f52e98b..be52d8a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,8 +4,8 @@ server: spring: data: mongodb: - database: ${DATABASE:localhost} - password: ${PASSWORD:password} + database: ${DATABASE:botplatform} + password: ${PASSWORD} host: ${MONGO_HOST:localhost} port: ${MONGO_PORT:8050} username: ${USERNAME:test} @@ -20,7 +20,7 @@ credential: login: ${TELEGRAM_BOT_NAME:} token: ${TELEGRAM_BOT_TOKEN:} ai: - token: ${AI_TOKEN:token} + token: ${AI_TOKEN:} bot: config: datatype: ${DATATYPE:mongo} diff --git a/src/test/java/ru/holyway/botplatform/BotPlatformApplicationTests.java b/src/test/java/ru/holyway/botplatform/BotPlatformApplicationTests.java deleted file mode 100644 index b914405..0000000 --- a/src/test/java/ru/holyway/botplatform/BotPlatformApplicationTests.java +++ /dev/null @@ -1,40 +0,0 @@ -package ru.holyway.botplatform; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -import java.io.IOException; -import java.text.ParseException; - -@ExtendWith(SpringExtension.class) -public class BotPlatformApplicationTests { - - //private ScriptCompiler scriptCompiler = new GroovyConfiguration().scriptCompiler(); - - @Test - public void contextLoads() throws IOException, ParseException { -// //Script script = scriptCompiler.compile( -// // "script().when(any()).then(sout(get(\"https://jsonplaceholder.typicode.com/todos/1\").asJson(\"title\")))"); -// - //ScriptContext ctx = new ScriptContext(); -// -// Script script = script().when(any()).then(sout(get(text("https://yandex.ru/search/?text=").add(encode("что такое человек?")).value()).asHtml("", "() {public boolean test(String s) {System.exit(0);return true;}}).findFirst().toString(); -// System.out.println(a.replace("&+", " ")); -// -// System.out.println(new RestTemplate().exchange("https://ssyoutube.com/api/ig/story?url=https://instagram.com/stories/avtoinstruktorsaratov/3027039242002599367?utm_source=ig_story_item_share&igshid=MDJmNzVkMjY=", HttpMethod.GET, null, String.class).getBody()); - - } -} diff --git a/src/test/java/ru/holyway/botplatform/scripting/GroovySandboxSecurityTest.java b/src/test/java/ru/holyway/botplatform/scripting/GroovySandboxSecurityTest.java new file mode 100644 index 0000000..1c60725 --- /dev/null +++ b/src/test/java/ru/holyway/botplatform/scripting/GroovySandboxSecurityTest.java @@ -0,0 +1,134 @@ +package ru.holyway.botplatform.scripting; + +import groovy.lang.Binding; +import groovy.lang.GroovyShell; +import org.codehaus.groovy.control.CompilerConfiguration; +import org.codehaus.groovy.control.customizers.ImportCustomizer; +import org.codehaus.groovy.control.customizers.SecureASTCustomizer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.telegram.telegrambots.meta.api.methods.ParseMode; +import ru.holyway.botplatform.core.data.MemoryDataHelper; +import ru.holyway.botplatform.scripting.entity.ArrayEntity; +import ru.holyway.botplatform.scripting.entity.ChatTelegramEntity; +import ru.holyway.botplatform.scripting.entity.ConditionHandler; +import ru.holyway.botplatform.scripting.entity.HistoryMessageEntity; +import ru.holyway.botplatform.scripting.entity.InTimePredicate; +import ru.holyway.botplatform.scripting.entity.LoopHandler; +import ru.holyway.botplatform.scripting.entity.MessageBuilder; +import ru.holyway.botplatform.scripting.entity.MessageScriptEntity; +import ru.holyway.botplatform.scripting.entity.ReplyScriptEntity; +import ru.holyway.botplatform.scripting.entity.StickerEntity; +import ru.holyway.botplatform.scripting.entity.TernaryHandler; +import ru.holyway.botplatform.scripting.entity.TextScriptEntity; +import ru.holyway.botplatform.scripting.entity.TimePredicate; +import ru.holyway.botplatform.scripting.entity.UserScriptEntity; +import ru.holyway.botplatform.scripting.entity.VariableEntity; +import ru.holyway.botplatform.scripting.util.ContextChatStorage; +import ru.holyway.botplatform.scripting.util.NumberOperations; +import ru.holyway.botplatform.scripting.util.Request; +import ru.holyway.botplatform.scripting.util.SecretChatStorage; +import ru.holyway.botplatform.scripting.util.TextJoiner; +import ru.holyway.botplatform.scripting.util.Time; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Exercises the real sandboxed GroovyShell built the same way as + * {@link ru.holyway.botplatform.config.GroovyConfiguration}, instead of a plain GroovyShell, + * so the SecureASTCustomizer/blacklist rules are actually under test. + */ +public class GroovySandboxSecurityTest { + + private ScriptCompiler compiler; + + @BeforeEach + public void setUp() { + List allowedImports = new ArrayList<>(DefaultShellRules.importsWhitelist); + CompilerConfiguration configuration = new CompilerConfiguration() + .addCompilationCustomizers(customAst(allowedImports), customImports(allowedImports)); + + Map mapping = new HashMap<>(); + mapping.put("message", new MessageScriptEntity()); + mapping.put("reply", new ReplyScriptEntity()); + mapping.put("map", new ContextChatStorage(new MemoryDataHelper())); + mapping.put("secret", new SecretChatStorage(new MemoryDataHelper())); + mapping.put("text", new TextScriptEntity()); + mapping.put("user", new UserScriptEntity()); + mapping.put("sticker", new StickerEntity()); + mapping.put("request", new Request()); + mapping.put("chat", new ChatTelegramEntity(null)); + + Binding binding = new Binding(mapping); + GroovyShell shell = new GroovyShell(binding, configuration); + compiler = new ScriptCompilerImpl(shell); + } + + private ImportCustomizer customImports(List allowedImports) { + return new ImportCustomizer() + .addStaticStars( + Script.class.getName(), Time.class.getName(), TextJoiner.class.getName(), + Request.class.getName(), ArrayEntity.class.getName(), ConditionHandler.class.getName(), + TernaryHandler.class.getName(), ChatTelegramEntity.class.getName(), + TimePredicate.class.getName(), InTimePredicate.class.getName(), + HistoryMessageEntity.class.getName(), LoopHandler.class.getName(), + MessageBuilder.class.getName(), VariableEntity.class.getName(), + NumberOperations.class.getName(), ParseMode.class.getName()) + .addImports(allowedImports.toArray(new String[0])) + .addStarImports(DefaultShellRules.starImportsWhiteArray); + } + + private SecureASTCustomizer customAst(List allowedImports) { + SecureASTCustomizer secureAstCustomizer = new SecureASTCustomizer(); + secureAstCustomizer.setClosuresAllowed(false); + secureAstCustomizer.setMethodDefinitionAllowed(false); + secureAstCustomizer.setPackageAllowed(false); + secureAstCustomizer.setIndirectImportCheckEnabled(true); + secureAstCustomizer.setImportsWhitelist(allowedImports); + secureAstCustomizer.setStarImportsWhitelist(DefaultShellRules.starImportsWhitelist); + secureAstCustomizer.setStatementsBlacklist(DefaultShellRules.statementsBlacklist); + secureAstCustomizer.setReceiversBlackList(DefaultShellRules.receiversBlackList); + secureAstCustomizer.addExpressionCheckers( + new MethodsBlacklistExpressionChecker(DefaultShellRules.methodsBlacklist)); + return secureAstCustomizer; + } + + @Test + public void legitimateScriptStillCompiles() { + Script script = compiler.compile( + "script().when(text.cic(\"hello\")).then(message.reply(\"hi\"))"); + + assertNotNull(script); + } + + @ParameterizedTest + @ValueSource(strings = { + "System.exit(1)", + "Runtime.getRuntime().exec('id')", + "Runtime.getRuntime()", + "new ProcessBuilder('id').start()", + "Thread.sleep(1000)", + "new Thread(){}.start()", + "this.getClass().forName('java.lang.Runtime')", + "message.getClass()", + "message.getClass().forName('java.lang.Runtime').getMethod('exec', String.class)", + "Class.forName('java.lang.Runtime')", + "this.getClass().getClassLoader()", + "import java.io.File; new File('/').listFiles()", + "import ru.holyway.botplatform.core.entity.JSettings; new JSettings()", + "import ru.holyway.botplatform.security.AnonymousChatTokenSecurityFilter", + "for (int i = 0; i < 10; i++) { }", + "while (true) { }", + }) + public void maliciousOrDisallowedScriptIsRejected(String maliciousScript) { + assertThrows(Exception.class, () -> compiler.compile(maliciousScript)); + } +}