feat: 将 TelegramBots 升级至 8.0.0, 并适配 TelegramBots 的新改动.

将 TelegramBots 升级至新版本, 以支持新的 API.
由于 TelegramBots 发生无法兼容旧版本的重大变更, 因此 ScalaBot 将随着此次更新一同进行重大更改.

BREAKING CHANGE: ScalaBot 所依赖的 TelegramBots 发生重大更改, 所有扩展都需要进行适配.
  有关 TelegramBots 的重大变更说明请参考官方文档.
  ScalaBot 的最低 Java 版本已全部升级至 Java 17 (这是 TelegramBots 的最低兼容性要求), 所有扩展都应该至少迁移至 Java 17 版本.
  ScalaBot 的重大更改:
   - scalabot-extension
     - `net.lamgc.scalabot.extension.util.AbilityBots.getBotAccountId(BaseAbilityBot): long` 已被移除, 由于 BaseAbilityBot 不再允许获取 botToken, 因此该方法被移除. 作为代替, 请通过 `net.lamgc.scalabot.extension.BotExtensionFactory.createExtensionInstance` 所得到的 `BotExtensionCreateOptions` 中获取 botAccountId.

  另外, scalabot-extension 中的 `org.jetbrains.kotlinx.binary-compatibility-validator` 似乎不再对 Java 代码起作用, 因此移除该插件, 并在后续寻找替代品.
  TelegramBots 文档: https://rubenlagus.github.io/TelegramBotsDocumentation/how-to-update-7.html
This commit is contained in:
2024-12-10 23:32:29 +08:00
parent bac7239513
commit e1c87aeae4
23 changed files with 487 additions and 209 deletions

View File

@ -1,9 +0,0 @@
public abstract interface class net/lamgc/scalabot/extension/BotExtensionFactory {
public abstract fun createExtensionInstance (Lorg/telegram/abilitybots/api/bot/BaseAbilityBot;Ljava/io/File;)Lorg/telegram/abilitybots/api/util/AbilityExtension;
}
public class net/lamgc/scalabot/extension/util/AbilityBots {
public static fun cancelReplyState (Lorg/telegram/abilitybots/api/bot/BaseAbilityBot;J)Z
public static fun getBotAccountId (Lorg/telegram/abilitybots/api/bot/BaseAbilityBot;)J
}

View File

@ -3,16 +3,16 @@ plugins {
jacoco
`maven-publish`
signing
id("org.jetbrains.kotlinx.binary-compatibility-validator")
}
dependencies {
implementation("commons-codec:commons-codec:1.16.1")
api("org.telegram:telegrambots-abilities:6.9.7.1")
api("org.telegram:telegrambots-abilities:8.0.0")
api(project(":scalabot-meta"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.1")
testImplementation("org.mockito:mockito-core:5.11.0")
testImplementation("org.telegram:telegrambots-client:8.0.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}
@ -25,8 +25,8 @@ tasks.withType<Javadoc> {
java {
withJavadocJar()
withSourcesJar()
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.test {

View File

@ -10,13 +10,29 @@ import net.lamgc.scalabot.config.ProxyConfig;
@SuppressWarnings("unused")
public class BotExtensionCreateOptions {
private final long botAccountId;
private final ProxyConfig proxy;
public BotExtensionCreateOptions(ProxyConfig proxy) {
public BotExtensionCreateOptions(long botAccountId, ProxyConfig proxy) {
this.botAccountId = botAccountId;
this.proxy = proxy;
}
/**
* 获取 Bot 使用的代理信息.
*
* @return 返回 Bot 中 TelegramClient 所使用的代理配置.
*/
public ProxyConfig getProxy() {
return proxy;
}
/**
* 获取 Bot 的账户 Id.
*
* @return 返回 Bot 的账户 Id.
*/
public long getBotAccountId() {
return botAccountId;
}
}

View File

@ -1,7 +1,8 @@
package net.lamgc.scalabot.extension;
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
import org.telegram.abilitybots.api.util.AbilityExtension;
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
import org.telegram.telegrambots.abilitybots.api.db.DBContext;
import org.telegram.telegrambots.abilitybots.api.util.AbilityExtension;
import java.io.File;
@ -9,7 +10,7 @@ import java.io.File;
* 该接口用于为指定的 {@link BaseAbilityBot} 创建扩展.
*
* <p> 由于 AbilityExtension 无法直接获取 {@link BaseAbilityBot} 的
* 数据库对象 {@link org.telegram.abilitybots.api.db.DBContext},
* 数据库对象 {@link DBContext},
* 所以将通过该接口工厂来创建扩展对象.
*
* @author LamGC
@ -20,30 +21,7 @@ public interface BotExtensionFactory {
/**
* 为给定的 {@link BaseAbilityBot} 对象创建扩展.
*
* <p> 如扩展无使用 {@link org.telegram.abilitybots.api.db.DBContext} 的话,
* 也可以返回扩展单例, 因为 AbilityBot 本身并不禁止多个机器人共用一个扩展对象
* (AbilityBot 只是调用了扩展中的方法来创建 Ability 对象).
*
* @param bot 机器人对象.
* @param shareDataFolder ScalaBot App 为扩展提供的共享数据目录.
* <p>路径格式为:
* <pre> $DATA_ROOT/data/extensions/{GroupId}/{ArtifactId}</pre>
* <b>同一个扩展包的 Factory</b> 接收到的共享数据目录<b>都是一样的</b>,
* 建议将数据存储在数据目录中, 便于数据的存储管理.
* @return 返回为该 Bot 对象创建的扩展对象, 如果不希望为该机器人提供扩展, 可返回 {@code null}.
* @deprecated 请使用 {@link #createExtensionInstance(BaseAbilityBot, File, BotExtensionCreateOptions)},
* 该方法最迟在 1.0.0 正式版中移除.
* @since 0.0.1
*/
@Deprecated(since = "0.7.0", forRemoval = true)
default AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder) {
throw new UnsupportedOperationException("The method has not been implemented.");
}
/**
* 为给定的 {@link BaseAbilityBot} 对象创建扩展.
*
* <p> 如扩展无使用 {@link org.telegram.abilitybots.api.db.DBContext} 的话,
* <p> 如扩展无使用 {@link DBContext} 的话,
* 也可以返回扩展单例, 因为 AbilityBot 本身并不禁止多个机器人共用一个扩展对象
* (AbilityBot 只是调用了扩展中的方法来创建 Ability 对象).
*
@ -57,8 +35,6 @@ public interface BotExtensionFactory {
* @return 返回为该 Bot 对象创建的扩展对象, 如果不希望为该机器人提供扩展, 可返回 {@code null}.
* @since 0.7.0
*/
default AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder, BotExtensionCreateOptions options) {
return createExtensionInstance(bot, shareDataFolder);
}
AbilityExtension createExtensionInstance(BaseAbilityBot bot, File shareDataFolder, BotExtensionCreateOptions options);
}

View File

@ -1,40 +1,14 @@
package net.lamgc.scalabot.extension.util;
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AbilityBots {
private final static Pattern botTokenPattern = Pattern.compile("([1-9]\\d+):([A-Za-z\\d_-]{35,})");
public final class AbilityBots {
private AbilityBots() {
}
/**
* 获取 AbilityBot 的账户 Id.
*
* <p> 账户 Id 来自于 botToken 中, token 的格式为 "[AccountId]:[Secret]".
* <p> 账户 Id 的真实性与 botToken 的有效性有关, 本方法并不会确保 botToken 的有效性, 一般情况下也无需考虑 Id 的有效性,
* 如果有需要, 可尝试通过调用 {@link org.telegram.telegrambots.meta.api.methods.GetMe} 来确保 botToken 的有效性.
*
* @param bot 要获取账户 Id 的 AbilityBot 对象.
* @return 返回 AbilityBot 的账户 Id.
* @throws IllegalArgumentException 当 AbilityBot 的 botToken 格式错误时抛出该异常.
*/
@SuppressWarnings("deprecation")
public static long getBotAccountId(BaseAbilityBot bot) {
// 根据文档说明, 弃用仅针对重写方法, 使用该方法并无大碍.
String botToken = bot.getBotToken();
Matcher matcher = botTokenPattern.matcher(botToken);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid token format.");
}
return Long.parseLong(matcher.group(1));
}
/**
* 取消某一对话的状态机.
*
@ -43,7 +17,7 @@ public class AbilityBots {
* @return 如果状态机存在, 则删除后返回 true, 不存在(未开启任何状态机, 即没有触发任何 Reply)则返回 false.
*/
public static boolean cancelReplyState(BaseAbilityBot bot, long chatId) {
Map<Long, Integer> stateMap = bot.db().getMap("user_state_replies");
Map<Long, Integer> stateMap = bot.getDb().getMap("user_state_replies");
if (!stateMap.containsKey(chatId)) {
return false;
}

View File

@ -2,22 +2,35 @@ package net.lamgc.scalabot.extension.util;
import org.junit.jupiter.api.Test;
import org.mapdb.DBMaker;
import org.telegram.abilitybots.api.bot.AbilityBot;
import org.telegram.abilitybots.api.bot.BaseAbilityBot;
import org.telegram.abilitybots.api.db.MapDBContext;
import org.telegram.abilitybots.api.objects.*;
import org.telegram.abilitybots.api.sender.SilentSender;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.abilitybots.api.bot.AbilityBot;
import org.telegram.telegrambots.abilitybots.api.bot.BaseAbilityBot;
import org.telegram.telegrambots.abilitybots.api.db.MapDBContext;
import org.telegram.telegrambots.abilitybots.api.objects.*;
import org.telegram.telegrambots.abilitybots.api.sender.SilentSender;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.api.objects.User;
import org.telegram.telegrambots.meta.api.objects.message.Message;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
public class AbilityBotsTest {
public static final User USER = new User(1L, "first", false, "last", "username", null, false, false, false, false, false);
public static final User CREATOR = new User(1337L, "creatorFirst", false, "creatorLast", "creatorUsername", null, false, false, false, false, false);
public static final User USER = User.builder()
.userName("username")
.id(1L)
.firstName("first")
.lastName("last")
.isBot(false)
.build();
public static final User CREATOR = User.builder()
.userName("creatorUsername")
.id(1337L)
.firstName("creatorFirst")
.lastName("creatorLast")
.isBot(false)
.build();
static Update mockFullUpdate(BaseAbilityBot bot, User user, String args) {
bot.users().put(USER.getId(), USER);
@ -39,58 +52,51 @@ public class AbilityBotsTest {
return update;
}
@Test
void getBotAccountIdTest() {
String expectToken = "1234567890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
long actual = AbilityBots.getBotAccountId(new TestingAbilityBot(expectToken, "test"));
assertEquals(1234567890, actual);
String badTokenA = "12c34d56a7890:AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
assertThrows(IllegalArgumentException.class, () ->
AbilityBots.getBotAccountId(new TestingAbilityBot(badTokenA, "test")));
String badTokenB = "12c34d56a7890AAHXcNDBRZTKfyPED5Gi3PZDIKPOM6xhxwo";
assertThrows(IllegalArgumentException.class, () ->
AbilityBots.getBotAccountId(new TestingAbilityBot(badTokenB, "test")));
}
@Test
void cancelReplyStateTest() {
User userA = new User(10001L, "first", false, "last", "username", null, false, false, false, false, false);
User userB = new User(10101L, "first", false, "last", "username", null, false, false, false, false, false);
User userA = User.builder()
.id(10001L)
.firstName("first")
.lastName("last")
.userName("username")
.isBot(false)
.build();
User userB = User.builder()
.id(10101L)
.firstName("first")
.lastName("last")
.userName("username")
.isBot(false)
.build();
SilentSender silent = mock(SilentSender.class);
BaseAbilityBot bot = new TestingAbilityBot("", "", silent);
BaseAbilityBot bot = new TestingAbilityBot("", silent);
bot.onRegister();
bot.onUpdateReceived(mockFullUpdate(bot, userA, "/set_reply"));
bot.consume(mockFullUpdate(bot, userA, "/set_reply"));
verify(silent, times(1)).send("Reply set!", userA.getId());
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_01"));
bot.consume(mockFullUpdate(bot, userA, "reply_01"));
verify(silent, times(1)).send("Reply 01", userA.getId());
assertTrue(AbilityBots.cancelReplyState(bot, userA.getId()));
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_02"));
bot.consume(mockFullUpdate(bot, userA, "reply_02"));
verify(silent, never()).send("Reply 02", userA.getId());
assertFalse(AbilityBots.cancelReplyState(bot, userB.getId()));
silent = mock(SilentSender.class);
bot = new TestingAbilityBot("", "", silent);
bot = new TestingAbilityBot("", silent);
bot.onRegister();
bot.onUpdateReceived(mockFullUpdate(bot, userA, "/set_reply"));
bot.consume(mockFullUpdate(bot, userA, "/set_reply"));
verify(silent, times(1)).send("Reply set!", userA.getId());
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_01"));
bot.consume(mockFullUpdate(bot, userA, "reply_01"));
verify(silent, times(1)).send("Reply 01", userA.getId());
bot.onUpdateReceived(mockFullUpdate(bot, userA, "reply_02"));
bot.consume(mockFullUpdate(bot, userA, "reply_02"));
verify(silent, times(1)).send("Reply 02", userA.getId());
}
public static class TestingAbilityBot extends AbilityBot {
public TestingAbilityBot(String botToken, String botUsername) {
super(botToken, botUsername, new MapDBContext(DBMaker.heapDB().make()));
}
public TestingAbilityBot(String botToken, String botUsername, SilentSender silentSender) {
super(botToken, botUsername, new MapDBContext(DBMaker.heapDB().make()));
public TestingAbilityBot(String botUsername, SilentSender silentSender) {
super(new NoOpTelegramClient(), botUsername, new MapDBContext(DBMaker.heapDB().make()));
this.silent = silentSender;
}
@ -101,12 +107,12 @@ public class AbilityBotsTest {
.enableStats()
.locality(Locality.ALL)
.privacy(Privacy.PUBLIC)
.action(ctx -> ctx.bot().silent().send("Reply set!", ctx.chatId()))
.reply(ReplyFlow.builder(db())
.action((bot, upd) -> bot.silent().send("Reply 01", upd.getMessage().getChatId()))
.action(ctx -> ctx.bot().getSilent().send("Reply set!", ctx.chatId()))
.reply(ReplyFlow.builder(getDb())
.action((bot, upd) -> bot.getSilent().send("Reply 01", upd.getMessage().getChatId()))
.onlyIf(upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_01"))
.next(Reply.of((bot, upd) ->
bot.silent().send("Reply 02", upd.getMessage().getChatId()),
bot.getSilent().send("Reply 02", upd.getMessage().getChatId()),
upd -> upd.hasMessage() && upd.getMessage().getText().equals("reply_02")))
.build()
)

View File

@ -0,0 +1,229 @@
package net.lamgc.scalabot.extension.util;
import org.telegram.telegrambots.meta.api.methods.botapimethods.BotApiMethod;
import org.telegram.telegrambots.meta.api.methods.groupadministration.SetChatPhoto;
import org.telegram.telegrambots.meta.api.methods.send.*;
import org.telegram.telegrambots.meta.api.methods.stickers.*;
import org.telegram.telegrambots.meta.api.methods.updates.SetWebhook;
import org.telegram.telegrambots.meta.api.methods.updatingmessages.EditMessageMedia;
import org.telegram.telegrambots.meta.api.objects.File;
import org.telegram.telegrambots.meta.api.objects.message.Message;
import org.telegram.telegrambots.meta.generics.TelegramClient;
import java.io.InputStream;
import java.io.Serializable;
import java.util.List;
import java.util.concurrent.CompletableFuture;
class NoOpTelegramClient implements TelegramClient {
@Override
public <T extends Serializable, Method extends BotApiMethod<T>> CompletableFuture<T> executeAsync(Method method) {
return null;
}
@Override
public <T extends Serializable, Method extends BotApiMethod<T>> T execute(Method method) {
return null;
}
@Override
public Message execute(SendDocument sendDocument) {
return null;
}
@Override
public Message execute(SendPhoto sendPhoto) {
return null;
}
@Override
public Boolean execute(SetWebhook setWebhook) {
return null;
}
@Override
public Message execute(SendVideo sendVideo) {
return null;
}
@Override
public Message execute(SendVideoNote sendVideoNote) {
return null;
}
@Override
public Message execute(SendSticker sendSticker) {
return null;
}
@Override
public Message execute(SendAudio sendAudio) {
return null;
}
@Override
public Message execute(SendVoice sendVoice) {
return null;
}
@Override
public List<Message> execute(SendMediaGroup sendMediaGroup) {
return List.of();
}
@Override
public List<Message> execute(SendPaidMedia sendPaidMedia) {
return List.of();
}
@Override
public Boolean execute(SetChatPhoto setChatPhoto) {
return null;
}
@Override
public Boolean execute(AddStickerToSet addStickerToSet) {
return null;
}
@Override
public Boolean execute(ReplaceStickerInSet replaceStickerInSet) {
return null;
}
@Override
public Boolean execute(SetStickerSetThumbnail setStickerSetThumbnail) {
return null;
}
@Override
public Boolean execute(CreateNewStickerSet createNewStickerSet) {
return null;
}
@Override
public File execute(UploadStickerFile uploadStickerFile) {
return null;
}
@Override
public Serializable execute(EditMessageMedia editMessageMedia) {
return null;
}
@Override
public java.io.File downloadFile(File file) {
return null;
}
@Override
public InputStream downloadFileAsStream(File file) {
return null;
}
@Override
public Message execute(SendAnimation sendAnimation) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendDocument sendDocument) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendPhoto sendPhoto) {
return null;
}
@Override
public CompletableFuture<Boolean> executeAsync(SetWebhook setWebhook) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendVideo sendVideo) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendVideoNote sendVideoNote) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendSticker sendSticker) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendAudio sendAudio) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendVoice sendVoice) {
return null;
}
@Override
public CompletableFuture<List<Message>> executeAsync(SendMediaGroup sendMediaGroup) {
return null;
}
@Override
public CompletableFuture<List<Message>> executeAsync(SendPaidMedia sendPaidMedia) {
return null;
}
@Override
public CompletableFuture<Boolean> executeAsync(SetChatPhoto setChatPhoto) {
return null;
}
@Override
public CompletableFuture<Boolean> executeAsync(AddStickerToSet addStickerToSet) {
return null;
}
@Override
public CompletableFuture<Boolean> executeAsync(ReplaceStickerInSet replaceStickerInSet) {
return null;
}
@Override
public CompletableFuture<Boolean> executeAsync(SetStickerSetThumbnail setStickerSetThumbnail) {
return null;
}
@Override
public CompletableFuture<Boolean> executeAsync(CreateNewStickerSet createNewStickerSet) {
return null;
}
@Override
public CompletableFuture<File> executeAsync(UploadStickerFile uploadStickerFile) {
return null;
}
@Override
public CompletableFuture<Serializable> executeAsync(EditMessageMedia editMessageMedia) {
return null;
}
@Override
public CompletableFuture<Message> executeAsync(SendAnimation sendAnimation) {
return null;
}
@Override
public CompletableFuture<java.io.File> downloadFileAsync(File file) {
return null;
}
@Override
public CompletableFuture<InputStream> downloadFileAsStreamAsync(File file) {
return null;
}
}