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));
+ }
+}